From 8d9abd2f31f5302453b2b505f6d79e6335eac170 Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Fri, 25 Apr 2025 16:06:35 -0700 Subject: [PATCH 01/17] fix(vcs): add missing attributes to 1.6.x migration --- src/Appwrite/Migration/Migration.php | 6 ++- src/Appwrite/Migration/Version/V21.php | 53 +++++++++++++++++++++++++- 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Migration/Migration.php b/src/Appwrite/Migration/Migration.php index 56016f1057..14c8a9cce2 100644 --- a/src/Appwrite/Migration/Migration.php +++ b/src/Appwrite/Migration/Migration.php @@ -92,7 +92,7 @@ abstract class Migration '1.5.11' => 'V20', '1.6.0' => 'V21', '1.6.1' => 'V21', - '1.6.2' => 'V22', + '1.6.2' => 'V21', ]; /** @@ -374,6 +374,10 @@ abstract class Migration default => 'projects', }; + if ($from === 'files') { + $collectionType = 'buckets'; + } + $collection = $this->collections[$collectionType][$from] ?? null; if (is_null($collection)) { diff --git a/src/Appwrite/Migration/Version/V21.php b/src/Appwrite/Migration/Version/V21.php index 0a89221b12..04e5adc5fb 100644 --- a/src/Appwrite/Migration/Version/V21.php +++ b/src/Appwrite/Migration/Version/V21.php @@ -74,6 +74,27 @@ class V21 extends Migration Console::warning("'accessedAt' from {$id}: {$th->getMessage()}"); } break; + case 'rules': + $attributesToCreate = ['owner', 'region']; + foreach ($attributesToCreate as $attribute) { + // Create attribute + try { + $this->createAttributeFromCollection($this->projectDB, $id, $attribute); + } catch (Throwable $th) { + Console::warning("'$attribute' from {$id}: {$th->getMessage()}"); + } + } + + $indexesToCreate = ['_key_owner', '_key_region']; + foreach ($indexesToCreate as $index) { + // Create index + try { + $this->createIndexFromCollection($this->projectDB, $id, $index); + } catch (Throwable $th) { + Console::warning("'$index' from {$id}: {$th->getMessage()}"); + } + } + break; case 'platforms': // Increase 'type' length to 255 try { @@ -82,6 +103,17 @@ class V21 extends Migration Console::warning("'type' from {$id}: {$th->getMessage()}"); } break; + case 'installations': + $attributesToCreate = ['personalAccessToken', 'personalAccessTokenExpiry', 'personalRefreshToken']; + foreach ($attributesToCreate as $attribute) { + // Create attribute + try { + $this->createAttributeFromCollection($this->projectDB, $id, $attribute); + } catch (Throwable $th) { + Console::warning("'$attribute' from {$id}: {$th->getMessage()}"); + } + } + break; case 'migrations': // Create destination attribute try { @@ -217,11 +249,30 @@ class V21 extends Migration foreach ($this->documentsIterator('buckets') as $bucket) { $bucketId = 'bucket_' . $bucket['$internalId']; + Console::log("Migrating Bucket {$bucketId} {$bucket->getId()} ({$bucket->getAttribute('name')})"); + try { $this->projectDB->updateAttribute($bucketId, 'metadata', size: 65534); + } catch (\Throwable $th) { + Console::warning("'metadata' from {$bucketId}: {$th->getMessage()}"); + } + + try { + $this->createAttributeFromCollection($this->projectDB, $bucketId, 'transformedAt', 'files'); + } catch (\Throwable $th) { + Console::warning("'transformedAt' from {$bucketId}: {$th->getMessage()}"); + } + + try { + $this->createIndexFromCollection($this->projectDB, $bucketId, '_key_transformedAt', 'files'); + } catch (\Throwable $th) { + Console::warning("'_key_transformedAt' from {$bucketId}: {$th->getMessage()}"); + } + + try { $this->projectDB->purgeCachedCollection($bucketId); } catch (\Throwable $th) { - Console::warning("'bucketId' from {$bucketId}: {$th->getMessage()}"); + Console::warning("purging {$bucketId}: {$th->getMessage()}"); } } } From 101283f345e6f9fb3ca3c4022281f39943dca217 Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Fri, 25 Apr 2025 16:07:59 -0700 Subject: [PATCH 02/17] fix: prevent update migration from clearing scopes and resetting specs --- src/Appwrite/Migration/Version/V21.php | 12 ++-- src/Appwrite/Migration/Version/V22.php | 83 -------------------------- 2 files changed, 8 insertions(+), 87 deletions(-) delete mode 100644 src/Appwrite/Migration/Version/V22.php diff --git a/src/Appwrite/Migration/Version/V21.php b/src/Appwrite/Migration/Version/V21.php index 04e5adc5fb..c072c81a34 100644 --- a/src/Appwrite/Migration/Version/V21.php +++ b/src/Appwrite/Migration/Version/V21.php @@ -229,11 +229,15 @@ class V21 extends Migration $document->setAttribute('accessedAt', DateTime::now()); break; case 'functions': - // Add scopes attribute - $document->setAttribute('scopes', []); + // Set scopes attribute + if (empty($document->getAttribute('scopes', []))) { + $document->setAttribute('scopes', []); + } - // Add size attribute - $document->setAttribute('specification', APP_FUNCTION_SPECIFICATION_DEFAULT); + // Set specification attribute + if (empty($document->getAttribute('specification'))) { + $document->setAttribute('specification', APP_FUNCTION_SPECIFICATION_DEFAULT); + } } return $document; diff --git a/src/Appwrite/Migration/Version/V22.php b/src/Appwrite/Migration/Version/V22.php deleted file mode 100644 index 4d15662112..0000000000 --- a/src/Appwrite/Migration/Version/V22.php +++ /dev/null @@ -1,83 +0,0 @@ - null, - fn () => [] - ); - } - - Console::info('Migrating Collections'); - $this->migrateCollections(); - } - - /** - * Migrate Collections. - * - * @return void - * @throws Exception|Throwable - */ - private function migrateCollections(): void - { - $internalProjectId = $this->project->getInternalId(); - $collectionType = match ($internalProjectId) { - 'console' => 'console', - default => 'projects', - }; - - $collections = $this->collections[$collectionType]; - foreach ($collections as $collection) { - $id = $collection['$id']; - - Console::log("Migrating Collection \"{$id}\""); - - $this->projectDB->setNamespace("_$internalProjectId"); - - switch ($id) { - case 'installations': - // Create personalAccessToken attribute - try { - $this->createAttributeFromCollection($this->projectDB, $id, 'personalAccessToken'); - } catch (Throwable $th) { - Console::warning("'personalAccessToken' from {$id}: {$th->getMessage()}"); - } - - // Create personalAccessTokenExpiry attribute - try { - $this->createAttributeFromCollection($this->projectDB, $id, 'personalAccessTokenExpiry'); - } catch (Throwable $th) { - Console::warning("'personalAccessTokenExpiry' from {$id}: {$th->getMessage()}"); - } - - // Create personalRefreshToken attribute - try { - $this->createAttributeFromCollection($this->projectDB, $id, 'personalRefreshToken'); - } catch (Throwable $th) { - Console::warning("'personalRefreshToken' from {$id}: {$th->getMessage()}"); - } - break; - } - - usleep(50000); - } - } -} From 977a920959bfa5f4786c032a3e84cf0ebd1c8512 Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Wed, 30 Apr 2025 17:23:50 -0700 Subject: [PATCH 03/17] chore: add 1.6.2 to CHANGES.md --- CHANGES.md | 264 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 264 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 62db3d525e..bc903e4b31 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,267 @@ +# Version 1.6.2 + +## What's Changed + +### Notable changes + +* Delete git folder to reduce build size in [9076](https://github.com/appwrite/appwrite/pull/9076) +* Upgrade assistant in [9100](https://github.com/appwrite/appwrite/pull/9100) +* Use redis adapter for abuse in [9121](https://github.com/appwrite/appwrite/pull/9121) +* Set base specification CPUs to 0.5 again in [9146](https://github.com/appwrite/appwrite/pull/9146) +* Add new push message parameters in [9060](https://github.com/appwrite/appwrite/pull/9060) +* Update audits to include user type in [9211](https://github.com/appwrite/appwrite/pull/9211) +* Enable HEIC in [9251](https://github.com/appwrite/appwrite/pull/9251) +* Added teamName to membership redirect url in [9269](https://github.com/appwrite/appwrite/pull/9269) +* Add support endpoint url for S3 in [9303](https://github.com/appwrite/appwrite/pull/9303) +* Added RuPay Credit Card Icon in Avatars Service in [5046](https://github.com/appwrite/appwrite/pull/5046) +* Add figma oauth provider in [9623](https://github.com/appwrite/appwrite/pull/9623) +* Update console to version 5.2.58 in [9637](https://github.com/appwrite/appwrite/pull/9637) + +### Fixes + +* Remove failed attribute in [9032](https://github.com/appwrite/appwrite/pull/9032) +* Fix delete notFound attribute in [9038](https://github.com/appwrite/appwrite/pull/9038) +* 🇮🇸 Added missing Icelandic translations for email strings. in [4848](https://github.com/appwrite/appwrite/pull/4848) +* fix doc comment for filter method in [5769](https://github.com/appwrite/appwrite/pull/5769) +* Delete attribute No throwing Exception on not found in [9157](https://github.com/appwrite/appwrite/pull/9157) +* Fix VCS identity collision in [9138](https://github.com/appwrite/appwrite/pull/9138) +* Fix disabling of email-otp when user wants to in [9200](https://github.com/appwrite/appwrite/pull/9200) +* Ensure user can delete session in [9209](https://github.com/appwrite/appwrite/pull/9209) +* Fix resend invitation in [9218](https://github.com/appwrite/appwrite/pull/9218) +* Fix phone number parsing exception handling in [9246](https://github.com/appwrite/appwrite/pull/9246) +* Fix amazon oauth in [9253](https://github.com/appwrite/appwrite/pull/9253) +* Fix slack oauth scopes, and updated to v2 in [9228](https://github.com/appwrite/appwrite/pull/9228) +* Fix forwarded user agent in [9271](https://github.com/appwrite/appwrite/pull/9271) +* Fix WEBP File Preview Rendering Issue in [9321](https://github.com/appwrite/appwrite/pull/9321) +* Fix build memory specifications in [9360](https://github.com/appwrite/appwrite/pull/9360) +* Fix Self Hosting functions by adding missed config in [9373](https://github.com/appwrite/appwrite/pull/9373) +* Fix resend team invite if already accepted in [9348](https://github.com/appwrite/appwrite/pull/9348) +* Fix null errors on team invite in [9391](https://github.com/appwrite/appwrite/pull/9391) +* Fix email (smtp) to multiple recipients in [9243](https://github.com/appwrite/appwrite/pull/9243) +* Fix stats timing by using receivedAt date when available in [9428](https://github.com/appwrite/appwrite/pull/9428) +* Make min/max params optional for attribute update in [9387](https://github.com/appwrite/appwrite/pull/9387) +* Fix blocking of phone sessions when disabled on console in [9447](https://github.com/appwrite/appwrite/pull/9447) +* Fix logging config in [9467](https://github.com/appwrite/appwrite/pull/9467) +* Update audit timestamp origin in [9481](https://github.com/appwrite/appwrite/pull/9481) +* Fix certificates in deletes worker in [9466](https://github.com/appwrite/appwrite/pull/9466) +* Fix console audits delete in [9547](https://github.com/appwrite/appwrite/pull/9547) +* Fix migrations in [9633](https://github.com/appwrite/appwrite/pull/9633) +* Ensure all 4xx errors in OAuth redirect lead to the failure URL in [9679](https://github.com/appwrite/appwrite/pull/9679) +* Treat 0 as unlimited for CPUs and memory in [9638](https://github.com/appwrite/appwrite/pull/9638) +* Add contextual dispatch logic to fix high CPU usage in [9687](https://github.com/appwrite/appwrite/pull/9687) + +### Miscellaneous + +* Merge 1.6.x into feat-custom-cf-hostnames in [8904](https://github.com/appwrite/appwrite/pull/8904) +* Improve compression param checks in [8922](https://github.com/appwrite/appwrite/pull/8922) +* upgrade utopia storage in [8930](https://github.com/appwrite/appwrite/pull/8930) +* Feat migration in [8797](https://github.com/appwrite/appwrite/pull/8797) +* feat fix web routes in [8962](https://github.com/appwrite/appwrite/pull/8962) +* Fix no pool access in [9027](https://github.com/appwrite/appwrite/pull/9027) +* feat: use environment variable to check rules format in [9039](https://github.com/appwrite/appwrite/pull/9039) +* Update storage.php in [9037](https://github.com/appwrite/appwrite/pull/9037) +* Upgrade db 0.53.200 in [9050](https://github.com/appwrite/appwrite/pull/9050) +* Chore: upgrade utopia storage in [9066](https://github.com/appwrite/appwrite/pull/9066) +* Update usage-dump payload in [9085](https://github.com/appwrite/appwrite/pull/9085) +* GitHub Workflows security hardening in [3728](https://github.com/appwrite/appwrite/pull/3728) +* Update add-oauth2-provider.md in [4313](https://github.com/appwrite/appwrite/pull/4313) +* update readme-cn some doc in [5278](https://github.com/appwrite/appwrite/pull/5278) +* Add accessibility features in [7042](https://github.com/appwrite/appwrite/pull/7042) +* Add Appwrite Cloud to read me. in [5445](https://github.com/appwrite/appwrite/pull/5445) +* Migration throw error in [9092](https://github.com/appwrite/appwrite/pull/9092) +* Fix usage payload bug in [9097](https://github.com/appwrite/appwrite/pull/9097) +* chore: replace occurrences of dbForConsole to dbForPlatform in [9096](https://github.com/appwrite/appwrite/pull/9096) +* fix(realtime): decrement connectionCounter only if connection is known in [9055](https://github.com/appwrite/appwrite/pull/9055) +* payload bug fix in [9098](https://github.com/appwrite/appwrite/pull/9098) +* Fix usage payload bug in [9099](https://github.com/appwrite/appwrite/pull/9099) +* Usage payload debug in [9101](https://github.com/appwrite/appwrite/pull/9101) +* Usage payload debug in [9103](https://github.com/appwrite/appwrite/pull/9103) +* Usage payload debug in [9104](https://github.com/appwrite/appwrite/pull/9104) +* Feat: createFunction abuse labels in [9102](https://github.com/appwrite/appwrite/pull/9102) +* Docs-create-document in [9105](https://github.com/appwrite/appwrite/pull/9105) +* Docs: Create document and unknown attribute error messages. in [5427](https://github.com/appwrite/appwrite/pull/5427) +* Fix: update project accessed at from router and schedulers in [9109](https://github.com/appwrite/appwrite/pull/9109) +* chore: initial commit in [9111](https://github.com/appwrite/appwrite/pull/9111) +* chore: optimise webhooks payload in [9115](https://github.com/appwrite/appwrite/pull/9115) +* Revert "chore: initial commit" in [9117](https://github.com/appwrite/appwrite/pull/9117) +* chore: fix attribute name in [9118](https://github.com/appwrite/appwrite/pull/9118) +* Migrate to redis abuse in [9124](https://github.com/appwrite/appwrite/pull/9124) +* Added webhooks usage stats in [9125](https://github.com/appwrite/appwrite/pull/9125) +* chore remove abuse cleanup in [9137](https://github.com/appwrite/appwrite/pull/9137) +* fix: remove abuse delete trigger in [9139](https://github.com/appwrite/appwrite/pull/9139) +* Remove firebase OAuth API endpoints in [9144](https://github.com/appwrite/appwrite/pull/9144) +* chore: release client sdks in [9112](https://github.com/appwrite/appwrite/pull/9112) +* Update general.php in [9155](https://github.com/appwrite/appwrite/pull/9155) +* feat(swoole): allow configuration override of available cpus in [9177](https://github.com/appwrite/appwrite/pull/9177) +* Usage databases api read writes addition in [9142](https://github.com/appwrite/appwrite/pull/9142) +* Fix dead connections in [9190](https://github.com/appwrite/appwrite/pull/9190) +* Add hostname to audits in [9165](https://github.com/appwrite/appwrite/pull/9165) +* chore: shifted authphone usage tracking to api calls in [9191](https://github.com/appwrite/appwrite/pull/9191) +* Revert "Fix dead connections" in [9201](https://github.com/appwrite/appwrite/pull/9201) +* Add assertEventually to messaging provider logs test in [9192](https://github.com/appwrite/appwrite/pull/9192) +* feat project sms usage in [9198](https://github.com/appwrite/appwrite/pull/9198) +* chore: add audit labels to project resources in [9056](https://github.com/appwrite/appwrite/pull/9056) +* fix sms usage in [9207](https://github.com/appwrite/appwrite/pull/9207) +* Update database in [9202](https://github.com/appwrite/appwrite/pull/9202) +* Fix dead connections in [9213](https://github.com/appwrite/appwrite/pull/9213) +* Revert "Fix dead connections" in [9214](https://github.com/appwrite/appwrite/pull/9214) +* Add logs db init for consistency in [9163](https://github.com/appwrite/appwrite/pull/9163) +* Split the collection definitions in [9153](https://github.com/appwrite/appwrite/pull/9153) +* Log path with populated parameters in [9220](https://github.com/appwrite/appwrite/pull/9220) +* Add missing scope on function template in [9208](https://github.com/appwrite/appwrite/pull/9208) +* Add relatedCollection default in [9225](https://github.com/appwrite/appwrite/pull/9225) +* fix: function usage in [9235](https://github.com/appwrite/appwrite/pull/9235) +* feat: optimise events payloads in [9232](https://github.com/appwrite/appwrite/pull/9232) +* Optimise webhook events in [9168](https://github.com/appwrite/appwrite/pull/9168) +* fix: maintenance job missing type in [9238](https://github.com/appwrite/appwrite/pull/9238) +* Update Fetch to 0.3.0 in [9245](https://github.com/appwrite/appwrite/pull/9245) +* Fix maintenance job in [9247](https://github.com/appwrite/appwrite/pull/9247) +* chore: add missing case for executions in [9248](https://github.com/appwrite/appwrite/pull/9248) +* Add index dependency exception in [9226](https://github.com/appwrite/appwrite/pull/9226) +* chore: fix benchmarking test when made from fork in [9233](https://github.com/appwrite/appwrite/pull/9233) +* Update SDK Generator versions in [9188](https://github.com/appwrite/appwrite/pull/9188) +* chore: skipped job instead of throwing error in [9250](https://github.com/appwrite/appwrite/pull/9250) +* Implement new SDK Class on 1.6.x in [9237](https://github.com/appwrite/appwrite/pull/9237) +* Delete collection before Appwrite's attributes in [9256](https://github.com/appwrite/appwrite/pull/9256) +* Feat batch usage dump in [9255](https://github.com/appwrite/appwrite/pull/9255) +* Fix cloud tests in [9261](https://github.com/appwrite/appwrite/pull/9261) +* Usage: Databases reads writes in [9260](https://github.com/appwrite/appwrite/pull/9260) +* Update: Latest sdk specs in [9274](https://github.com/appwrite/appwrite/pull/9274) +* Revert "Feat batch usage dump" in [9276](https://github.com/appwrite/appwrite/pull/9276) +* feat: add fast2SMS adapter in [9263](https://github.com/appwrite/appwrite/pull/9263) +* Update Sdk Generator dependency in [9280](https://github.com/appwrite/appwrite/pull/9280) +* Transformed at addition in [9281](https://github.com/appwrite/appwrite/pull/9281) +* Docs: clarify update endpoints only work on draft messages in [9236](https://github.com/appwrite/appwrite/pull/9236) +* Update sdk generator dependency in [9282](https://github.com/appwrite/appwrite/pull/9282) +* Revert "Transformed at addition" in [9284](https://github.com/appwrite/appwrite/pull/9284) +* replaced init for cloud link in [9285](https://github.com/appwrite/appwrite/pull/9285) +* Add transformed at in [9289](https://github.com/appwrite/appwrite/pull/9289) +* Make migrations use Dynamic keys for destination in [9291](https://github.com/appwrite/appwrite/pull/9291) +* Make sessions limit tests assert eventually in [9298](https://github.com/appwrite/appwrite/pull/9298) +* Chore update database in [9306](https://github.com/appwrite/appwrite/pull/9306) +* feat: add AMQP queues in [9287](https://github.com/appwrite/appwrite/pull/9287) +* fix(test): use assertEventually instead of while(true) in [9308](https://github.com/appwrite/appwrite/pull/9308) +* fix(certificate worker): events are published without queue name in [9309](https://github.com/appwrite/appwrite/pull/9309) +* chore: update utopia-php/queue to 0.8.1 in [9311](https://github.com/appwrite/appwrite/pull/9311) +* chore: update utopia-php/queue to 0.8.2 in [9312](https://github.com/appwrite/appwrite/pull/9312) +* fix(schedule-tasks): revert back to direct pool usage in [9313](https://github.com/appwrite/appwrite/pull/9313) +* feat: custom app schemes in [9262](https://github.com/appwrite/appwrite/pull/9262) +* Revert "feat: custom app schemes" in [9319](https://github.com/appwrite/appwrite/pull/9319) +* Restore "feat: custom app schemes"" in [9320](https://github.com/appwrite/appwrite/pull/9320) +* Revert "Restore "feat: custom app schemes""" in [9323](https://github.com/appwrite/appwrite/pull/9323) +* chore: update dependencies in [9330](https://github.com/appwrite/appwrite/pull/9330) +* Feat: logs DB in [9272](https://github.com/appwrite/appwrite/pull/9272) +* Catch invalid index in [9329](https://github.com/appwrite/appwrite/pull/9329) +* Fix: missing call for image transformations counting in [9342](https://github.com/appwrite/appwrite/pull/9342) +* Fix drop abuse on shared table project delete in [9346](https://github.com/appwrite/appwrite/pull/9346) +* Only run all table mode tests on db update in [9338](https://github.com/appwrite/appwrite/pull/9338) +* Fix: missing periodic metric in [9350](https://github.com/appwrite/appwrite/pull/9350) +* feat(builds): check if function is blocked before building in [9332](https://github.com/appwrite/appwrite/pull/9332) +* feat: batch create audit logs in [9347](https://github.com/appwrite/appwrite/pull/9347) +* Chore: Update migrations in [9355](https://github.com/appwrite/appwrite/pull/9355) +* Fix: metric time was not being written to DB in [9354](https://github.com/appwrite/appwrite/pull/9354) +* Fix patch index validation in [9356](https://github.com/appwrite/appwrite/pull/9356) +* Fix image trnasformation metrics in [9370](https://github.com/appwrite/appwrite/pull/9370) +* Use batch delete in worker in [9375](https://github.com/appwrite/appwrite/pull/9375) +* Fix Model Platform is missing response key: store in [9361](https://github.com/appwrite/appwrite/pull/9361) +* Feat key segmented usage in [9336](https://github.com/appwrite/appwrite/pull/9336) +* Feat messaging metrics in [9353](https://github.com/appwrite/appwrite/pull/9353) +* Fix removed audits for shared v2 in [9388](https://github.com/appwrite/appwrite/pull/9388) +* chore: bump utopia-php/image to 0.8.0 in [9390](https://github.com/appwrite/appwrite/pull/9390) +* Fix outdated CLI commands in documentation in [9122](https://github.com/appwrite/appwrite/pull/9122) +* disable logs display in [9398](https://github.com/appwrite/appwrite/pull/9398) +* Log batches per project in [9403](https://github.com/appwrite/appwrite/pull/9403) +* Batch per project in [9410](https://github.com/appwrite/appwrite/pull/9410) +* Fix: stats resources only queue projects accessed in last 3 hours in [9411](https://github.com/appwrite/appwrite/pull/9411) +* Track options requests in [9397](https://github.com/appwrite/appwrite/pull/9397) +* chore: bump docker-base in [9406](https://github.com/appwrite/appwrite/pull/9406) +* refactor: migrate Realtime::send calls to queueForRealtime in [9325](https://github.com/appwrite/appwrite/pull/9325) +* Revert "Fix: stats resources only queue projects accessed in last 3 hours" in [9424](https://github.com/appwrite/appwrite/pull/9424) +* Remove usage and usage dump in favor of stats-usage and stats-usage-dump in [9339](https://github.com/appwrite/appwrite/pull/9339) +* Fix: disable dual writing in [9429](https://github.com/appwrite/appwrite/pull/9429) +* Disable transformedAt update for console users in [9425](https://github.com/appwrite/appwrite/pull/9425) +* chore: add image transformation stats to usage endpoint in [9393](https://github.com/appwrite/appwrite/pull/9393) +* chore: added timeout to deployment builds in tests in [9426](https://github.com/appwrite/appwrite/pull/9426) +* fix: model for image transformations in usage project in [9442](https://github.com/appwrite/appwrite/pull/9442) +* Feat: calculate database storage in stats-resources in [9443](https://github.com/appwrite/appwrite/pull/9443) +* Activities batch writes in [9438](https://github.com/appwrite/appwrite/pull/9438) +* chore: bump cache 0.12.x in [9412](https://github.com/appwrite/appwrite/pull/9412) +* chore: queue console project for maintenance delete in [9479](https://github.com/appwrite/appwrite/pull/9479) +* chore: added logsdb for deletes worker in [9462](https://github.com/appwrite/appwrite/pull/9462) +* Feat: calculate and log time taken for each project in [9491](https://github.com/appwrite/appwrite/pull/9491) +* chore: update initializing dbForLogs in [9494](https://github.com/appwrite/appwrite/pull/9494) +* Feat bulk audit delete in [9487](https://github.com/appwrite/appwrite/pull/9487) +* Prepare 1.6.2 release in [9499](https://github.com/appwrite/appwrite/pull/9499) +* Regenerate specs in [9497](https://github.com/appwrite/appwrite/pull/9497) +* Regenerate examples in [9498](https://github.com/appwrite/appwrite/pull/9498) +* chore: bump sdk in [9414](https://github.com/appwrite/appwrite/pull/9414) +* update queue to 0.9.* in [9505](https://github.com/appwrite/appwrite/pull/9505) +* Feat improve delete queries in [9507](https://github.com/appwrite/appwrite/pull/9507) +* Feat: Add rule attributes in [9508](https://github.com/appwrite/appwrite/pull/9508) +* Sync main into 1.6.x in [9496](https://github.com/appwrite/appwrite/pull/9496) +* Bump console to version 5.2.53 in [9495](https://github.com/appwrite/appwrite/pull/9495) +* Prepare 1.6.1 release in [9294](https://github.com/appwrite/appwrite/pull/9294) +* Improve delete ordering in [9512](https://github.com/appwrite/appwrite/pull/9512) +* Cleanups in [9511](https://github.com/appwrite/appwrite/pull/9511) +* Feat dynamic regions in [9408](https://github.com/appwrite/appwrite/pull/9408) +* Feat env vars to system lib in [9515](https://github.com/appwrite/appwrite/pull/9515) +* Feat: domains count in [9514](https://github.com/appwrite/appwrite/pull/9514) +* Migration read from db in [9529](https://github.com/appwrite/appwrite/pull/9529) +* feat: add pool telemetry in [9530](https://github.com/appwrite/appwrite/pull/9530) +* Disable PDO persistence since we manage our own pool in [9526](https://github.com/appwrite/appwrite/pull/9526) +* chore: set min operations to 1 for reads and writes in [9536](https://github.com/appwrite/appwrite/pull/9536) +* Remove default region in [9430](https://github.com/appwrite/appwrite/pull/9430) +* Use cursor pagination with bigger limit for maintenance project loop in [9546](https://github.com/appwrite/appwrite/pull/9546) +* chore: stop tests on failure in [9525](https://github.com/appwrite/appwrite/pull/9525) +* chore: only update total count for privileged users in [9554](https://github.com/appwrite/appwrite/pull/9554) +* refactor: initialization of audit retention in [9563](https://github.com/appwrite/appwrite/pull/9563) +* Delete worker queries fixes in [9523](https://github.com/appwrite/appwrite/pull/9523) +* Bump database 0.62.x in [9568](https://github.com/appwrite/appwrite/pull/9568) +* Fix: schedules region filtering in [9577](https://github.com/appwrite/appwrite/pull/9577) +* Deletes worker fix selects for pagination in [9578](https://github.com/appwrite/appwrite/pull/9578) +* Add $permissions for delete documents selects in [9579](https://github.com/appwrite/appwrite/pull/9579) +* chore(audits): return queue pre-fetch results in [9533](https://github.com/appwrite/appwrite/pull/9533) +* Revert "chore(audits): return queue pre-fetch results" in [9586](https://github.com/appwrite/appwrite/pull/9586) +* Feat multi tenant insert in [9573](https://github.com/appwrite/appwrite/pull/9573) +* Add order by for cursor in [9588](https://github.com/appwrite/appwrite/pull/9588) +* Feat update fetch in [9592](https://github.com/appwrite/appwrite/pull/9592) +* Fix tenant casting in [9598](https://github.com/appwrite/appwrite/pull/9598) +* Feat update ws in [9602](https://github.com/appwrite/appwrite/pull/9602) +* Update database in [9603](https://github.com/appwrite/appwrite/pull/9603) +* Fix: image transformation cache in [9608](https://github.com/appwrite/appwrite/pull/9608) +* Remove audit payload in [9610](https://github.com/appwrite/appwrite/pull/9610) +* Sample rate from DSN in [9559](https://github.com/appwrite/appwrite/pull/9559) +* Restrict role change for sole org owner in [9615](https://github.com/appwrite/appwrite/pull/9615) +* chore: update php image to 0.8.1 in [9616](https://github.com/appwrite/appwrite/pull/9616) +* feat: refactor executor setup in [9420](https://github.com/appwrite/appwrite/pull/9420) +* chore: update gitpod.yml config in [9561](https://github.com/appwrite/appwrite/pull/9561) +* chore: update dependencies in [9625](https://github.com/appwrite/appwrite/pull/9625) +* Update migrations lib in [9628](https://github.com/appwrite/appwrite/pull/9628) +* feat: cache telemetry in [9624](https://github.com/appwrite/appwrite/pull/9624) +* Bump console to version 5.2.56 in [9631](https://github.com/appwrite/appwrite/pull/9631) +* Multi region support in [8667](https://github.com/appwrite/appwrite/pull/8667) +* Revert "Multi region support" in [9632](https://github.com/appwrite/appwrite/pull/9632) +* Revert "Revert "Multi region support"" in [9636](https://github.com/appwrite/appwrite/pull/9636) +* Fix tasks in [9644](https://github.com/appwrite/appwrite/pull/9644) +* chore: updated the migration version to 8.6 in [9646](https://github.com/appwrite/appwrite/pull/9646) +* Fix: merge the working of StatsUsage and StatsUsageDump in [9585](https://github.com/appwrite/appwrite/pull/9585) +* Update database in [9643](https://github.com/appwrite/appwrite/pull/9643) +* chore: fix error logging for CLI tasks in [9651](https://github.com/appwrite/appwrite/pull/9651) +* fix: usage test assertion in [9653](https://github.com/appwrite/appwrite/pull/9653) +* Fix keys in [9656](https://github.com/appwrite/appwrite/pull/9656) +* Feat: multi tenant dual writing in [9583](https://github.com/appwrite/appwrite/pull/9583) +* Fix/throwing 400 for null order attributes in [9657](https://github.com/appwrite/appwrite/pull/9657) +* feat: sdk group attribute in [9596](https://github.com/appwrite/appwrite/pull/9596) +* Add configurable function and build size in [9648](https://github.com/appwrite/appwrite/pull/9648) +* feat: update API endpoint in the code examples in [8933](https://github.com/appwrite/appwrite/pull/8933) +* chore: abstract token secret hiding to response model in [9574](https://github.com/appwrite/appwrite/pull/9574) +* chore: update sdks in [9655](https://github.com/appwrite/appwrite/pull/9655) +* feat: allow non-critical events to ignore exceptions when enqueuing the event in [9680](https://github.com/appwrite/appwrite/pull/9680) +* Revert "Add configurable function and build size" in [9681](https://github.com/appwrite/appwrite/pull/9681) +* core: introduce endpoint.docs in specs in [9685](https://github.com/appwrite/appwrite/pull/9685) +* fix: remove content-type header from get request specs in [9666](https://github.com/appwrite/appwrite/pull/9666) +* chore: update flutter sdk in [9691](https://github.com/appwrite/appwrite/pull/9691) + # Version 1.6.1 ## What's Changed From 56a8e38890e689772402095785f5a1f91eefa3a2 Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 6 May 2025 10:50:56 +0300 Subject: [PATCH 04/17] listCollections with big limit --- src/Appwrite/Platform/Workers/Deletes.php | 48 +++++++++-------------- 1 file changed, 18 insertions(+), 30 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index a61db63de6..9e1543b8b6 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -497,45 +497,33 @@ class Deletes extends Action AbuseDatabase::COLLECTION, ]; - $limit = \count($projectCollectionIds) + 25; - $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); $sharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES_V1', '')); $projectTables = !\in_array($dsn->getHost(), $sharedTables); $sharedTablesV1 = \in_array($dsn->getHost(), $sharedTablesV1); $sharedTablesV2 = !$projectTables && !$sharedTablesV1; - $sharedTables = $sharedTablesV1 || $sharedTablesV2; - while (true) { - $collections = $dbForProject->listCollections($limit); + /** + * Consider using cursor + */ + $collections = $dbForProject->listCollections(PHP_INT_MAX); - foreach ($collections as $collection) { - try { - if ($projectTables || !\in_array($collection->getId(), $projectCollectionIds)) { - $dbForProject->deleteCollection($collection->getId()); - } else { - $this->deleteByGroup( - $collection->getId(), - [ - Query::orderAsc() - ], - database: $dbForProject - ); - } - } catch (Throwable $e) { - Console::error('Error deleting '.$collection->getId().' '.$e->getMessage()); + foreach ($collections as $collection) { + try { + if ($projectTables || !\in_array($collection->getId(), $projectCollectionIds)) { + $dbForProject->deleteCollection($collection->getId()); + } else { + $this->deleteByGroup( + $collection->getId(), + [ + Query::orderAsc() + ], + database: $dbForProject + ); } - } - - if ($sharedTables) { - $collectionsIds = \array_map(fn ($collection) => $collection->getId(), $collections); - - if (empty(\array_diff($collectionsIds, $projectCollectionIds))) { - break; - } - } elseif (empty($collections)) { - break; + } catch (Throwable $e) { + Console::error('Error deleting '.$collection->getId().' '.$e->getMessage()); } } From 6264e616d3598e8e5fee1cdf5f0ba9b739f306f6 Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 6 May 2025 12:18:29 +0300 Subject: [PATCH 05/17] Delete using Cursor --- src/Appwrite/Platform/Workers/Deletes.php | 29 +++++++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index 9e1543b8b6..50e09927c8 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -505,11 +505,11 @@ class Deletes extends Action $sharedTablesV2 = !$projectTables && !$sharedTablesV1; /** - * Consider using cursor + * @var $dbForProject Database */ - $collections = $dbForProject->listCollections(PHP_INT_MAX); - - foreach ($collections as $collection) { + var_dump($projectTables); + var_dump($projectCollectionIds); + $dbForProject->foreach(Database::METADATA, function (Document $collection) use ($dbForProject, $projectTables, $projectCollectionIds) { try { if ($projectTables || !\in_array($collection->getId(), $projectCollectionIds)) { $dbForProject->deleteCollection($collection->getId()); @@ -525,7 +525,26 @@ class Deletes extends Action } catch (Throwable $e) { Console::error('Error deleting '.$collection->getId().' '.$e->getMessage()); } - } + }); + + // $collections = $dbForProject->listCollections(PHP_INT_MAX); +// foreach ($collections as $collection) { +// try { +// if ($projectTables || !\in_array($collection->getId(), $projectCollectionIds)) { +// $dbForProject->deleteCollection($collection->getId()); +// } else { +// $this->deleteByGroup( +// $collection->getId(), +// [ +// Query::orderAsc() +// ], +// database: $dbForProject +// ); +// } +// } catch (Throwable $e) { +// Console::error('Error deleting '.$collection->getId().' '.$e->getMessage()); +// } +// } // Delete Platforms $this->deleteByGroup('platforms', [ From abc2795daa24b70a1f3a5368177f7259799cc325 Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 6 May 2025 12:25:56 +0300 Subject: [PATCH 06/17] Delete using Cursor --- src/Appwrite/Platform/Workers/Deletes.php | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index 50e09927c8..6d796f3dda 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -507,8 +507,6 @@ class Deletes extends Action /** * @var $dbForProject Database */ - var_dump($projectTables); - var_dump($projectCollectionIds); $dbForProject->foreach(Database::METADATA, function (Document $collection) use ($dbForProject, $projectTables, $projectCollectionIds) { try { if ($projectTables || !\in_array($collection->getId(), $projectCollectionIds)) { @@ -527,25 +525,6 @@ class Deletes extends Action } }); - // $collections = $dbForProject->listCollections(PHP_INT_MAX); -// foreach ($collections as $collection) { -// try { -// if ($projectTables || !\in_array($collection->getId(), $projectCollectionIds)) { -// $dbForProject->deleteCollection($collection->getId()); -// } else { -// $this->deleteByGroup( -// $collection->getId(), -// [ -// Query::orderAsc() -// ], -// database: $dbForProject -// ); -// } -// } catch (Throwable $e) { -// Console::error('Error deleting '.$collection->getId().' '.$e->getMessage()); -// } -// } - // Delete Platforms $this->deleteByGroup('platforms', [ Query::equal('projectInternalId', [$projectInternalId]), From 6f34592efe055923c1b67ee6efb6ccd8ec936f6f Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 6 May 2025 14:18:26 +0300 Subject: [PATCH 07/17] Add messages --- src/Appwrite/Platform/Workers/Deletes.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index 6d796f3dda..3ebad0363a 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -510,8 +510,10 @@ class Deletes extends Action $dbForProject->foreach(Database::METADATA, function (Document $collection) use ($dbForProject, $projectTables, $projectCollectionIds) { try { if ($projectTables || !\in_array($collection->getId(), $projectCollectionIds)) { + Console::info('DeleteProject deleteCollection "'.$collection->getId().'"'); $dbForProject->deleteCollection($collection->getId()); } else { + Console::info('DeleteProject deleteByGroup "'.$collection->getId().'"'); $this->deleteByGroup( $collection->getId(), [ From c5086d8fba8b90e0e7c4d0ffaf9c2843091cdbde Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 6 May 2025 15:18:51 +0300 Subject: [PATCH 08/17] Different color --- src/Appwrite/Platform/Workers/Deletes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index 3ebad0363a..7003f2b135 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -513,7 +513,7 @@ class Deletes extends Action Console::info('DeleteProject deleteCollection "'.$collection->getId().'"'); $dbForProject->deleteCollection($collection->getId()); } else { - Console::info('DeleteProject deleteByGroup "'.$collection->getId().'"'); + Console::log('DeleteProject deleteByGroup "'.$collection->getId().'"'); $this->deleteByGroup( $collection->getId(), [ From e5a1af3183eeeb3fb0586599fa6a12ca98e4453f Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 8 May 2025 10:43:28 +0300 Subject: [PATCH 09/17] Remove DBG --- src/Appwrite/Platform/Workers/Deletes.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index 7003f2b135..6d796f3dda 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -510,10 +510,8 @@ class Deletes extends Action $dbForProject->foreach(Database::METADATA, function (Document $collection) use ($dbForProject, $projectTables, $projectCollectionIds) { try { if ($projectTables || !\in_array($collection->getId(), $projectCollectionIds)) { - Console::info('DeleteProject deleteCollection "'.$collection->getId().'"'); $dbForProject->deleteCollection($collection->getId()); } else { - Console::log('DeleteProject deleteByGroup "'.$collection->getId().'"'); $this->deleteByGroup( $collection->getId(), [ From b1829bacfe74b865dd2c08d352853c1207025869 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 15 May 2025 23:33:00 +1200 Subject: [PATCH 10/17] Only rethrow auth and timeout in general handler --- app/controllers/api/databases.php | 599 +++++++++++++++--------------- app/controllers/general.php | 28 +- 2 files changed, 299 insertions(+), 328 deletions(-) diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index a72aaf66a3..5eae500119 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -34,6 +34,7 @@ use Utopia\Database\Exception\Limit as LimitException; use Utopia\Database\Exception\NotFound as NotFoundException; use Utopia\Database\Exception\Order as OrderException; use Utopia\Database\Exception\Query as QueryException; +use Utopia\Database\Exception\Relationship as RelationshipException; use Utopia\Database\Exception\Restricted as RestrictedException; use Utopia\Database\Exception\Structure as StructureException; use Utopia\Database\Exception\Timeout as TimeoutException; @@ -101,13 +102,13 @@ function createAttribute(string $databaseId, string $collectionId, Document $att $default = $attribute->getAttribute('default'); $options = $attribute->getAttribute('options', []); - $db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $dbForProject->getDocument('databases', $databaseId); - if ($db->isEmpty()) { + if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); } - $collection = $dbForProject->getDocument('database_' . $db->getInternalId(), $collectionId); + $collection = $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId); if ($collection->isEmpty()) { throw new Exception(Exception::COLLECTION_NOT_FOUND); @@ -130,7 +131,7 @@ function createAttribute(string $databaseId, string $collectionId, Document $att if ($type === Database::VAR_RELATIONSHIP) { $options['side'] = Database::RELATION_SIDE_PARENT; - $relatedCollection = $dbForProject->getDocument('database_' . $db->getInternalId(), $options['relatedCollection'] ?? ''); + $relatedCollection = $dbForProject->getDocument('database_' . $database->getInternalId(), $options['relatedCollection'] ?? ''); if ($relatedCollection->isEmpty()) { throw new Exception(Exception::COLLECTION_NOT_FOUND, 'The related collection was not found.'); } @@ -138,10 +139,10 @@ function createAttribute(string $databaseId, string $collectionId, Document $att try { $attribute = new Document([ - '$id' => ID::custom($db->getInternalId() . '_' . $collection->getInternalId() . '_' . $key), + '$id' => ID::custom($database->getInternalId() . '_' . $collection->getInternalId() . '_' . $key), 'key' => $key, - 'databaseInternalId' => $db->getInternalId(), - 'databaseId' => $db->getId(), + 'databaseInternalId' => $database->getInternalId(), + 'databaseId' => $database->getId(), 'collectionInternalId' => $collection->getInternalId(), 'collectionId' => $collectionId, 'type' => $type, @@ -164,13 +165,13 @@ function createAttribute(string $databaseId, string $collectionId, Document $att } catch (LimitException) { throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED); } catch (\Throwable $e) { - $dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $collectionId); - $dbForProject->purgeCachedCollection('database_' . $db->getInternalId() . '_collection_' . $collection->getInternalId()); + $dbForProject->purgeCachedDocument('database_' . $database->getInternalId(), $collectionId); + $dbForProject->purgeCachedCollection('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId()); throw $e; } - $dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $collectionId); - $dbForProject->purgeCachedCollection('database_' . $db->getInternalId() . '_collection_' . $collection->getInternalId()); + $dbForProject->purgeCachedDocument('database_' . $database->getInternalId(), $collectionId); + $dbForProject->purgeCachedCollection('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId()); if ($type === Database::VAR_RELATIONSHIP && $options['twoWay']) { $twoWayKey = $options['twoWayKey']; @@ -179,53 +180,58 @@ function createAttribute(string $databaseId, string $collectionId, Document $att $options['side'] = Database::RELATION_SIDE_CHILD; try { - $twoWayAttribute = new Document([ - '$id' => ID::custom($db->getInternalId() . '_' . $relatedCollection->getInternalId() . '_' . $twoWayKey), - 'key' => $twoWayKey, - 'databaseInternalId' => $db->getInternalId(), - 'databaseId' => $db->getId(), - 'collectionInternalId' => $relatedCollection->getInternalId(), - 'collectionId' => $relatedCollection->getId(), - 'type' => $type, - 'status' => 'processing', // processing, available, failed, deleting, stuck - 'size' => $size, - 'required' => $required, - 'signed' => $signed, - 'default' => $default, - 'array' => $array, - 'format' => $format, - 'formatOptions' => $formatOptions, - 'filters' => $filters, - 'options' => $options, - ]); + try { + $twoWayAttribute = new Document([ + '$id' => ID::custom($database->getInternalId() . '_' . $relatedCollection->getInternalId() . '_' . $twoWayKey), + 'key' => $twoWayKey, + 'databaseInternalId' => $database->getInternalId(), + 'databaseId' => $database->getId(), + 'collectionInternalId' => $relatedCollection->getInternalId(), + 'collectionId' => $relatedCollection->getId(), + 'type' => $type, + 'status' => 'processing', // processing, available, failed, deleting, stuck + 'size' => $size, + 'required' => $required, + 'signed' => $signed, + 'default' => $default, + 'array' => $array, + 'format' => $format, + 'formatOptions' => $formatOptions, + 'filters' => $filters, + 'options' => $options, + ]); - $dbForProject->checkAttribute($relatedCollection, $twoWayAttribute); - $dbForProject->createDocument('attributes', $twoWayAttribute); - } catch (DuplicateException) { - $dbForProject->deleteDocument('attributes', $attribute->getId()); - throw new Exception(Exception::ATTRIBUTE_ALREADY_EXISTS); - } catch (LimitException) { - $dbForProject->deleteDocument('attributes', $attribute->getId()); - throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED); + $dbForProject->checkAttribute($relatedCollection, $twoWayAttribute); + $dbForProject->createDocument('attributes', $twoWayAttribute); + } catch (DuplicateException) { + throw new Exception(Exception::DOCUMENT_ALREADY_EXISTS); + } catch (LimitException) { + throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED); + } catch (StructureException $e) { + throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $e->getMessage()); + } } catch (\Throwable $e) { - $dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $relatedCollection->getId()); - $dbForProject->purgeCachedCollection('database_' . $db->getInternalId() . '_collection_' . $relatedCollection->getInternalId()); + $dbForProject->deleteDocument('attributes', $attribute->getId()); throw $e; + } finally { + $dbForProject->purgeCachedDocument('database_' . $database->getInternalId(), $collectionId); + $dbForProject->purgeCachedCollection('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId()); } - $dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $relatedCollection->getId()); - $dbForProject->purgeCachedCollection('database_' . $db->getInternalId() . '_collection_' . $relatedCollection->getInternalId()); + // If operation succeeded, purge the cache for the related collection too + $dbForProject->purgeCachedDocument('database_' . $database->getInternalId(), $relatedCollection->getId()); + $dbForProject->purgeCachedCollection('database_' . $database->getInternalId() . '_collection_' . $relatedCollection->getInternalId()); } $queueForDatabase ->setType(DATABASE_TYPE_CREATE_ATTRIBUTE) - ->setDatabase($db) + ->setDatabase($database) ->setCollection($collection) ->setDocument($attribute); $queueForEvents ->setContext('collection', $collection) - ->setContext('database', $db) + ->setContext('database', $database) ->setParam('databaseId', $databaseId) ->setParam('collectionId', $collection->getId()) ->setParam('attributeId', $attribute->getId()); @@ -252,20 +258,17 @@ function updateAttribute( array $options = [], string $newKey = null, ): Document { - $db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - - if ($db->isEmpty()) { + $database = $dbForProject->getDocument('databases', $databaseId); + if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); } - $collection = $dbForProject->getDocument('database_' . $db->getInternalId(), $collectionId); - + $collection = $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId); if ($collection->isEmpty()) { throw new Exception(Exception::COLLECTION_NOT_FOUND); } - $attribute = $dbForProject->getDocument('attributes', $db->getInternalId() . '_' . $collection->getInternalId() . '_' . $key); - + $attribute = $dbForProject->getDocument('attributes', $database->getInternalId() . '_' . $collection->getInternalId() . '_' . $key); if ($attribute->isEmpty()) { throw new Exception(Exception::ATTRIBUTE_NOT_FOUND); } @@ -290,7 +293,7 @@ function updateAttribute( throw new Exception(Exception::ATTRIBUTE_DEFAULT_UNSUPPORTED, 'Cannot set default value for array attributes'); } - $collectionId = 'database_' . $db->getInternalId() . '_collection_' . $collection->getInternalId(); + $collectionId = 'database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(); $attribute ->setAttribute('default', $default) @@ -365,8 +368,14 @@ function updateAttribute( newKey: $newKey, onDelete: $primaryDocumentOptions['onDelete'], ); - } catch (NotFoundException) { - throw new Exception(Exception::ATTRIBUTE_NOT_FOUND); + } catch (IndexException) { + throw new Exception(Exception::INDEX_INVALID); + } catch (LimitException) { + throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED); + } catch (RelationshipException $e) { + throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage()); + } catch (StructureException $e) { + throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $e->getMessage()); } if ($primaryDocumentOptions['twoWay']) { @@ -380,9 +389,10 @@ function updateAttribute( $relatedOptions = \array_merge($relatedAttribute->getAttribute('options'), $options); $relatedAttribute->setAttribute('options', $relatedOptions); - $dbForProject->updateDocument('attributes', $db->getInternalId() . '_' . $relatedCollection->getInternalId() . '_' . $primaryDocumentOptions['twoWayKey'], $relatedAttribute); - $dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $relatedCollection->getId()); + + $dbForProject->updateDocument('attributes', $database->getInternalId() . '_' . $relatedCollection->getInternalId() . '_' . $primaryDocumentOptions['twoWayKey'], $relatedAttribute); + $dbForProject->purgeCachedDocument('database_' . $database->getInternalId(), $relatedCollection->getId()); } } else { try { @@ -395,14 +405,14 @@ function updateAttribute( formatOptions: $options, newKey: $newKey ?? null ); - } catch (TruncateException) { - throw new Exception(Exception::ATTRIBUTE_INVALID_RESIZE); - } catch (NotFoundException) { - throw new Exception(Exception::ATTRIBUTE_NOT_FOUND); - } catch (LimitException) { - throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED); + } catch (DuplicateException) { + throw new Exception(Exception::DOCUMENT_ALREADY_EXISTS); } catch (IndexException $e) { throw new Exception(Exception::INDEX_INVALID, $e->getMessage()); + } catch (LimitException) { + throw new Exception(Exception::ATTRIBUTE_LIMIT_EXCEEDED); + } catch (TruncateException) { + throw new Exception(Exception::ATTRIBUTE_INVALID_RESIZE); } } @@ -410,10 +420,14 @@ function updateAttribute( $originalUid = $attribute->getId(); $attribute - ->setAttribute('$id', ID::custom($db->getInternalId() . '_' . $collection->getInternalId() . '_' . $newKey)) + ->setAttribute('$id', ID::custom($database->getInternalId() . '_' . $collection->getInternalId() . '_' . $newKey)) ->setAttribute('key', $newKey); - $dbForProject->updateDocument('attributes', $originalUid, $attribute); + try { + $dbForProject->updateDocument('attributes', $originalUid, $attribute); + } catch (DuplicateException) { + throw new Exception(Exception::DOCUMENT_ALREADY_EXISTS); + } /** * @var Document $index @@ -435,11 +449,11 @@ function updateAttribute( $attribute = $dbForProject->updateDocument('attributes', $db->getInternalId() . '_' . $collection->getInternalId() . '_' . $key, $attribute); } - $dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $collection->getId()); + $dbForProject->purgeCachedDocument('database_' . $database->getInternalId(), $collection->getId()); $queueForEvents ->setContext('collection', $collection) - ->setContext('database', $db) + ->setContext('database', $database) ->setParam('databaseId', $databaseId) ->setParam('collectionId', $collection->getId()) ->setParam('attributeId', $attribute->getId()); @@ -490,7 +504,9 @@ App::post('/v1/databases') ->inject('queueForStatsUsage') ->action(function (string $databaseId, string $name, bool $enabled, Response $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage) { - $databaseId = $databaseId == 'unique()' ? ID::unique() : $databaseId; + $databaseId = $databaseId === 'unique()' + ? ID::unique() + : $databaseId; try { $dbForProject->createDocument('databases', new Document([ @@ -499,42 +515,41 @@ App::post('/v1/databases') '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); + } catch (StructureException $e) { + throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $e->getMessage()); + } + + $database = $dbForProject->getDocument('databases', $databaseId); + + $collections = (Config::getParam('collections', [])['databases'] ?? [])['collections'] ?? []; + if (empty($collections)) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'The "collections" collection is not configured.'); + } + + $attributes = []; + foreach ($collections['attributes'] as $attribute) { + $attributes[] = new Document($attribute); + } + + $indexes = []; + foreach ($collections['indexes'] as $index) { + $indexes[] = new Document($index); + } + + try { + $dbForProject->createCollection('database_' . $database->getInternalId(), $attributes, $indexes); + } catch (AuthorizationException) { + throw new Exception(Exception::USER_UNAUTHORIZED); + } catch (DuplicateException) { + throw new Exception(Exception::DATABASE_ALREADY_EXISTS); + } catch (IndexException) { + throw new Exception(Exception::INDEX_INVALID); + } catch (LimitException) { + throw new Exception(Exception::COLLECTION_LIMIT_EXCEEDED); + } catch (TimeoutException) { + throw new Exception(Exception::DATABASE_TIMEOUT); } $queueForEvents->setParam('databaseId', $database->getId()); @@ -580,6 +595,7 @@ App::get('/v1/databases') $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 */ @@ -590,8 +606,8 @@ App::get('/v1/databases') } $databaseId = $cursor->getValue(); - $cursorDocument = $dbForProject->getDocument('databases', $databaseId); + $cursorDocument = $dbForProject->getDocument('databases', $databaseId); if ($cursorDocument->isEmpty()) { throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Database '{$databaseId}' for the 'cursor' value not found."); } @@ -599,14 +615,15 @@ App::get('/v1/databases') $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."); + $total = $dbForProject->count('databases', $queries, APP_LIMIT_COUNT); + } catch (OrderException) { + throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL); + } catch (QueryException) { + throw new Exception(Exception::GENERAL_QUERY_INVALID); } + $response->dynamic(new Document([ 'databases' => $databases, 'total' => $total, @@ -636,9 +653,7 @@ App::get('/v1/databases/:databaseId') ->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); } @@ -672,9 +687,7 @@ App::get('/v1/databases/:databaseId/logs') ->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); } @@ -776,17 +789,18 @@ App::put('/v1/databases/:databaseId') ->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 + $database ->setAttribute('name', $name) ->setAttribute('enabled', $enabled) - ->setAttribute('search', implode(' ', [$databaseId, $name]))); + ->setAttribute('search', implode(' ', [$databaseId, $name])); + + $database = $dbForProject->updateDocument('databases', $databaseId, $database); $queueForEvents->setParam('databaseId', $database->getId()); @@ -822,7 +836,6 @@ App::delete('/v1/databases/:databaseId') ->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()) { @@ -830,7 +843,7 @@ App::delete('/v1/databases/:databaseId') } if (!$dbForProject->deleteDocument('databases', $databaseId)) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove collection from DB'); + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove database from database'); } $dbForProject->purgeCachedDocument('databases', $database->getId()); @@ -880,14 +893,15 @@ App::post('/v1/databases/:databaseId/collections') ->inject('mode') ->inject('queueForEvents') ->action(function (string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, bool $enabled, Response $response, Database $dbForProject, string $mode, Event $queueForEvents) { - - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $dbForProject->getDocument('databases', $databaseId); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); } - $collectionId = $collectionId == 'unique()' ? ID::unique() : $collectionId; + $collectionId = $collectionId === 'unique()' + ? ID::unique() + : $collectionId; // Map aggregate permissions into the multiple permissions they represent. $permissions = Permission::aggregate($permissions) ?? []; @@ -903,12 +917,26 @@ App::post('/v1/databases/:databaseId/collections') 'name' => $name, 'search' => implode(' ', [$collectionId, $name]), ])); - - $dbForProject->createCollection('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), permissions: $permissions, documentSecurity: $documentSecurity); } catch (DuplicateException) { throw new Exception(Exception::COLLECTION_ALREADY_EXISTS); } catch (LimitException) { throw new Exception(Exception::COLLECTION_LIMIT_EXCEEDED); + } catch (NotFoundException) { + throw new Exception(Exception::DATABASE_NOT_FOUND); + } + + try { + $dbForProject->createCollection( + id: 'database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), + permissions: $permissions, + documentSecurity: $documentSecurity + ); + } catch (DuplicateException) { + throw new Exception(Exception::COLLECTION_ALREADY_EXISTS); + } catch (IndexException) { + throw new Exception(Exception::INDEX_INVALID); + } catch (LimitException) { + throw new Exception(Exception::COLLECTION_LIMIT_EXCEEDED); } $queueForEvents @@ -948,14 +976,17 @@ App::get('/v1/databases/:databaseId/collections') ->inject('dbForProject') ->inject('mode') ->action(function (string $databaseId, array $queries, string $search, Response $response, Database $dbForProject, string $mode) { - - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $dbForProject->getDocument('databases', $databaseId); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); } - $queries = Query::parseQueries($queries); + try { + $queries = Query::parseQueries($queries); + } catch (QueryException $e) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); + } if (!empty($search)) { $queries[] = Query::search('search', $search); @@ -977,6 +1008,7 @@ App::get('/v1/databases/:databaseId/collections') } $collectionId = $cursor->getValue(); + $cursorDocument = $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId); if ($cursorDocument->isEmpty()) { @@ -986,14 +1018,17 @@ App::get('/v1/databases/:databaseId/collections') $cursor->setValue($cursorDocument); } - $filterQueries = Query::groupByType($queries)['filters']; + $collectionId = 'database_' . $database->getInternalId(); try { - $collections = $dbForProject->find('database_' . $database->getInternalId(), $queries); - $total = $dbForProject->count('database_' . $database->getInternalId(), $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."); + $collections = $dbForProject->find($collectionId, $queries); + $total = $dbForProject->count($collectionId, $queries, APP_LIMIT_COUNT); + } catch (OrderException) { + throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL); + } catch (QueryException) { + throw new Exception(Exception::GENERAL_QUERY_INVALID); } + $response->dynamic(new Document([ 'collections' => $collections, 'total' => $total, @@ -1026,8 +1061,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId') ->inject('dbForProject') ->inject('mode') ->action(function (string $databaseId, string $collectionId, Response $response, Database $dbForProject, string $mode) { - - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $dbForProject->getDocument('databases', $databaseId); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -1070,8 +1104,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/logs') ->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)); + $database = $dbForProject->getDocument('databases', $databaseId); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -1080,7 +1113,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/logs') $collectionDocument = $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId); $collection = $dbForProject->getCollection('database_' . $database->getInternalId() . '_collection_' . $collectionDocument->getInternalId()); - if ($collection->isEmpty()) { + if ($collectionDocument->isEmpty() || $collection->isEmpty()) { throw new Exception(Exception::COLLECTION_NOT_FOUND); } @@ -1186,8 +1219,7 @@ App::put('/v1/databases/:databaseId/collections/:collectionId') ->inject('mode') ->inject('queueForEvents') ->action(function (string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, bool $enabled, Response $response, Database $dbForProject, string $mode, Event $queueForEvents) { - - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $dbForProject->getDocument('databases', $databaseId); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -1206,15 +1238,17 @@ App::put('/v1/databases/:databaseId/collections/:collectionId') $enabled ??= $collection->getAttribute('enabled', true); - $collection = $dbForProject->updateDocument( - 'database_' . $database->getInternalId(), - $collectionId, - $collection + $collection ->setAttribute('name', $name) ->setAttribute('$permissions', $permissions) ->setAttribute('documentSecurity', $documentSecurity) ->setAttribute('enabled', $enabled) - ->setAttribute('search', \implode(' ', [$collectionId, $name])) + ->setAttribute('search', \implode(' ', [$collectionId, $name])); + + $collection = $dbForProject->updateDocument( + 'database_' . $database->getInternalId(), + $collectionId, + $collection ); $dbForProject->updateCollection('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $permissions, $documentSecurity); @@ -1258,8 +1292,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId') ->inject('queueForEvents') ->inject('mode') ->action(function (string $databaseId, string $collectionId, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, string $mode) { - - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $dbForProject->getDocument('databases', $databaseId); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -1326,7 +1359,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/string ->inject('queueForDatabase') ->inject('queueForEvents') ->action(function (string $databaseId, string $collectionId, string $key, ?int $size, ?bool $required, ?string $default, bool $array, bool $encrypt, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { - // Ensure attribute default is within required size $validator = new Text($size, 0); if (!is_null($default) && !$validator->isValid($default)) { @@ -1334,7 +1366,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/string } $filters = []; - if ($encrypt) { $filters[] = 'encrypt'; } @@ -1387,7 +1418,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/email' ->inject('queueForDatabase') ->inject('queueForEvents') ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { - $attribute = createAttribute($databaseId, $collectionId, new Document([ 'key' => $key, 'type' => Database::VAR_STRING, @@ -1490,7 +1520,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/ip') ->inject('queueForDatabase') ->inject('queueForEvents') ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { - $attribute = createAttribute($databaseId, $collectionId, new Document([ 'key' => $key, 'type' => Database::VAR_STRING, @@ -1539,7 +1568,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/url') ->inject('queueForDatabase') ->inject('queueForEvents') ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { - $attribute = createAttribute($databaseId, $collectionId, new Document([ 'key' => $key, 'type' => Database::VAR_STRING, @@ -1590,7 +1618,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/intege ->inject('queueForDatabase') ->inject('queueForEvents') ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { - // Ensure attribute default is within range $min ??= PHP_INT_MIN; $max ??= PHP_INT_MAX; @@ -1668,7 +1695,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/float' ->inject('queueForDatabase') ->inject('queueForEvents') ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?float $min, ?float $max, ?float $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { - // Ensure attribute default is within range $min ??= -PHP_FLOAT_MAX; $max ??= PHP_FLOAT_MAX; @@ -1742,7 +1768,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/boolea ->inject('queueForDatabase') ->inject('queueForEvents') ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?bool $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { - $attribute = createAttribute($databaseId, $collectionId, new Document([ 'key' => $key, 'type' => Database::VAR_BOOLEAN, @@ -1790,7 +1815,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/dateti ->inject('queueForDatabase') ->inject('queueForEvents') ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { - $filters[] = 'datetime'; $attribute = createAttribute($databaseId, $collectionId, new Document([ @@ -1859,7 +1883,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/relati $key ??= $relatedCollectionId; $twoWayKey ??= $collectionId; - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $dbForProject->getDocument('databases', $databaseId); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -1880,6 +1904,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/relati } $attributes = $collection->getAttribute('attributes', []); + /** @var Document[] $attributes */ foreach ($attributes as $attribute) { if ($attribute->getAttribute('type') !== Database::VAR_RELATIONSHIP) { @@ -1968,20 +1993,21 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/attributes') ->inject('response') ->inject('dbForProject') ->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject) { - /** @var Document $database */ - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - + $database = $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); } - $queries = Query::parseQueries($queries); + try { + $queries = Query::parseQueries($queries); + } catch (QueryException $e) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); + } \array_push( $queries, @@ -2005,26 +2031,31 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/attributes') } $attributeId = $cursor->getValue(); - $cursorDocument = Authorization::skip(fn () => $dbForProject->find('attributes', [ - Query::equal('databaseInternalId', [$database->getInternalId()]), - Query::equal('collectionInternalId', [$collection->getInternalId()]), - Query::equal('key', [$attributeId]), - Query::limit(1), - ])); - if (empty($cursorDocument) || $cursorDocument[0]->isEmpty()) { + try { + $cursorDocument = $dbForProject->findOne('attributes', [ + Query::equal('databaseInternalId', [$database->getInternalId()]), + Query::equal('collectionInternalId', [$collection->getInternalId()]), + Query::equal('key', [$attributeId]), + ]); + } catch (QueryException $e) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); + } + + if ($cursorDocument->isEmpty()) { throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Attribute '{$attributeId}' for the 'cursor' value not found."); } - $cursor->setValue($cursorDocument[0]); + $cursor->setValue($cursorDocument); } - $filters = Query::groupByType($queries)['filters']; try { $attributes = $dbForProject->find('attributes', $queries); - $total = $dbForProject->count('attributes', $filters, APP_LIMIT_COUNT); - } catch (OrderException $e) { - throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null."); + $total = $dbForProject->count('attributes', $queries, APP_LIMIT_COUNT); + } catch (OrderException) { + throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL); + } catch (QueryException) { + throw new Exception(Exception::GENERAL_QUERY_INVALID); } $response->dynamic(new Document([ @@ -2069,8 +2100,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/attributes/: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)); + $database = $dbForProject->getDocument('databases', $databaseId); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -2149,7 +2179,6 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/attributes/strin ->inject('dbForProject') ->inject('queueForEvents') ->action(function (string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, ?int $size, ?string $newKey, Response $response, Database $dbForProject, Event $queueForEvents) { - $attribute = updateAttribute( databaseId: $databaseId, collectionId: $collectionId, @@ -2683,21 +2712,20 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/attributes/:key ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') - ->inject('queueForStatsUsage') - ->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, StatsUsage $queueForStatsUsage) { + ->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { + $database = $dbForProject->getDocument('databases', $databaseId); - $db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - - if ($db->isEmpty()) { + if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); } - $collection = $dbForProject->getDocument('database_' . $db->getInternalId(), $collectionId); + + $collection = $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId); if ($collection->isEmpty()) { throw new Exception(Exception::COLLECTION_NOT_FOUND); } - $attribute = $dbForProject->getDocument('attributes', $db->getInternalId() . '_' . $collection->getInternalId() . '_' . $key); + $attribute = $dbForProject->getDocument('attributes', $database->getInternalId() . '_' . $collection->getInternalId() . '_' . $key); if ($attribute->isEmpty()) { throw new Exception(Exception::ATTRIBUTE_NOT_FOUND); @@ -2720,19 +2748,19 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/attributes/:key $attribute = $dbForProject->updateDocument('attributes', $attribute->getId(), $attribute->setAttribute('status', 'deleting')); } - $dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $collectionId); - $dbForProject->purgeCachedCollection('database_' . $db->getInternalId() . '_collection_' . $collection->getInternalId()); + $dbForProject->purgeCachedDocument('database_' . $database->getInternalId(), $collectionId); + $dbForProject->purgeCachedCollection('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId()); if ($attribute->getAttribute('type') === Database::VAR_RELATIONSHIP) { $options = $attribute->getAttribute('options'); if ($options['twoWay']) { - $relatedCollection = $dbForProject->getDocument('database_' . $db->getInternalId(), $options['relatedCollection']); + $relatedCollection = $dbForProject->getDocument('database_' . $database->getInternalId(), $options['relatedCollection']); if ($relatedCollection->isEmpty()) { throw new Exception(Exception::COLLECTION_NOT_FOUND); } - $relatedAttribute = $dbForProject->getDocument('attributes', $db->getInternalId() . '_' . $relatedCollection->getInternalId() . '_' . $options['twoWayKey']); + $relatedAttribute = $dbForProject->getDocument('attributes', $database->getInternalId() . '_' . $relatedCollection->getInternalId() . '_' . $options['twoWayKey']); if ($relatedAttribute->isEmpty()) { throw new Exception(Exception::ATTRIBUTE_NOT_FOUND); @@ -2742,15 +2770,15 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/attributes/:key $dbForProject->updateDocument('attributes', $relatedAttribute->getId(), $relatedAttribute->setAttribute('status', 'deleting')); } - $dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $options['relatedCollection']); - $dbForProject->purgeCachedCollection('database_' . $db->getInternalId() . '_collection_' . $relatedCollection->getInternalId()); + $dbForProject->purgeCachedDocument('database_' . $database->getInternalId(), $options['relatedCollection']); + $dbForProject->purgeCachedCollection('database_' . $database->getInternalId() . '_collection_' . $relatedCollection->getInternalId()); } } $queueForDatabase ->setType(DATABASE_TYPE_DELETE_ATTRIBUTE) ->setCollection($collection) - ->setDatabase($db) + ->setDatabase($database) ->setDocument($attribute); // Select response model based on type and format @@ -2778,7 +2806,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/attributes/:key ->setParam('collectionId', $collection->getId()) ->setParam('attributeId', $attribute->getId()) ->setContext('collection', $collection) - ->setContext('database', $db) + ->setContext('database', $database) ->setPayload($response->output($attribute, $model)); $response->noContent(); @@ -2819,31 +2847,31 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes') ->inject('queueForDatabase') ->inject('queueForEvents') ->action(function (string $databaseId, string $collectionId, string $key, string $type, array $attributes, array $orders, array $lengths, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { + $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - $db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - - if ($db->isEmpty()) { + if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); } - $collection = $dbForProject->getDocument('database_' . $db->getInternalId(), $collectionId); + $collection = $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId); if ($collection->isEmpty()) { throw new Exception(Exception::COLLECTION_NOT_FOUND); } + $limit = $dbForProject->getLimitForIndexes(); + $count = $dbForProject->count('indexes', [ Query::equal('collectionInternalId', [$collection->getInternalId()]), - Query::equal('databaseInternalId', [$db->getInternalId()]) - ], 61); + Query::equal('databaseInternalId', [$database->getInternalId()]) + ], max: $limit); - $limit = $dbForProject->getLimitForIndexes(); if ($count >= $limit) { throw new Exception(Exception::INDEX_LIMIT_EXCEEDED, 'Index limit exceeded'); } - // Convert Document[] to array of attribute metadata + // Convert Document array to array of attribute metadata $oldAttributes = \array_map(fn ($a) => $a->getArrayCopy(), $collection->getAttribute('attributes')); $oldAttributes[] = [ @@ -2855,7 +2883,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes') 'default' => null, 'size' => Database::LENGTH_KEY ]; - $oldAttributes[] = [ 'key' => '$createdAt', 'type' => Database::VAR_DATETIME, @@ -2866,7 +2893,6 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes') 'default' => null, 'size' => 0 ]; - $oldAttributes[] = [ 'key' => '$updatedAt', 'type' => Database::VAR_DATETIME, @@ -2879,7 +2905,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes') ]; foreach ($attributes as $i => $attribute) { - // find attribute metadata in collection document + // Find attribute metadata in collection document $attributeIndex = \array_search($attribute, array_column($oldAttributes, 'key')); if ($attributeIndex === false) { @@ -2907,10 +2933,10 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes') } $index = new Document([ - '$id' => ID::custom($db->getInternalId() . '_' . $collection->getInternalId() . '_' . $key), + '$id' => ID::custom($database->getInternalId() . '_' . $collection->getInternalId() . '_' . $key), 'key' => $key, 'status' => 'processing', // processing, available, failed, deleting, stuck - 'databaseInternalId' => $db->getInternalId(), + 'databaseInternalId' => $database->getInternalId(), 'databaseId' => $databaseId, 'collectionInternalId' => $collection->getInternalId(), 'collectionId' => $collectionId, @@ -2925,6 +2951,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes') $dbForProject->getAdapter()->getMaxIndexLength(), $dbForProject->getAdapter()->getInternalIndexesKeys(), ); + if (!$validator->isValid($index)) { throw new Exception(Exception::INDEX_INVALID, $validator->getDescription()); } @@ -2935,11 +2962,11 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes') throw new Exception(Exception::INDEX_ALREADY_EXISTS); } - $dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $collectionId); + $dbForProject->purgeCachedDocument('database_' . $database->getInternalId(), $collectionId); $queueForDatabase ->setType(DATABASE_TYPE_CREATE_INDEX) - ->setDatabase($db) + ->setDatabase($database) ->setCollection($collection) ->setDocument($index); @@ -2948,7 +2975,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/indexes') ->setParam('collectionId', $collection->getId()) ->setParam('indexId', $index->getId()) ->setContext('collection', $collection) - ->setContext('database', $db); + ->setContext('database', $database); $response ->setStatusCode(Response::STATUS_CODE_ACCEPTED) @@ -2981,8 +3008,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes') ->inject('response') ->inject('dbForProject') ->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject) { - /** @var Document $database */ - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); + $database = $dbForProject->getDocument('databases', $databaseId); if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -3008,6 +3034,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes') $cursor = \array_filter($queries, function ($query) { return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]); }); + $cursor = reset($cursor); if ($cursor) { @@ -3031,12 +3058,13 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes') $cursor->setValue($cursorDocument[0]); } - $filterQueries = Query::groupByType($queries)['filters']; try { - $total = $dbForProject->count('indexes', $filterQueries, APP_LIMIT_COUNT); + $total = $dbForProject->count('indexes', $queries, APP_LIMIT_COUNT); $indexes = $dbForProject->find('indexes', $queries); } 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."); + } catch (QueryException $e) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); } $response->dynamic(new Document([ @@ -3071,12 +3099,12 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes/: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()) { @@ -3084,6 +3112,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') } $index = $collection->find('key', $key, 'indexes'); + if (empty($index)) { throw new Exception(Exception::INDEX_NOT_FOUND); } @@ -3123,21 +3152,21 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') ->inject('queueForDatabase') ->inject('queueForEvents') ->action(function (string $databaseId, string $collectionId, string $key, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { + $database = $dbForProject->getDocument('databases', $databaseId); - $db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - - if ($db->isEmpty()) { + if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); } - $collection = $dbForProject->getDocument('database_' . $db->getInternalId(), $collectionId); + + $collection = $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId); if ($collection->isEmpty()) { throw new Exception(Exception::COLLECTION_NOT_FOUND); } - $index = $dbForProject->getDocument('indexes', $db->getInternalId() . '_' . $collection->getInternalId() . '_' . $key); + $index = $dbForProject->getDocument('indexes', $database->getInternalId() . '_' . $collection->getInternalId() . '_' . $key); - if (empty($index->getId())) { + if ($index->isEmpty()) { throw new Exception(Exception::INDEX_NOT_FOUND); } @@ -3146,11 +3175,11 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') $index = $dbForProject->updateDocument('indexes', $index->getId(), $index->setAttribute('status', 'deleting')); } - $dbForProject->purgeCachedDocument('database_' . $db->getInternalId(), $collectionId); + $dbForProject->purgeCachedDocument('database_' . $database->getInternalId(), $collectionId); $queueForDatabase ->setType(DATABASE_TYPE_DELETE_INDEX) - ->setDatabase($db) + ->setDatabase($database) ->setCollection($collection) ->setDocument($index); @@ -3159,7 +3188,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') ->setParam('collectionId', $collection->getId()) ->setParam('indexId', $index->getId()) ->setContext('collection', $collection) - ->setContext('database', $db) + ->setContext('database', $database) ->setPayload($response->output($index, Response::MODEL_INDEX)); $response->noContent(); @@ -3273,6 +3302,10 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') $isAPIKey = Auth::isAppUser(Authorization::getRoles()); $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + if ($isBulk && !$isAPIKey && !$isPrivilegedUser) { + throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE); + } + $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::DATABASE_NOT_FOUND); @@ -3459,16 +3492,14 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') 'database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documents ); - } catch (StructureException $e) { - throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $e->getMessage()); } catch (DuplicateException) { throw new Exception(Exception::DOCUMENT_ALREADY_EXISTS); } catch (NotFoundException) { throw new Exception(Exception::COLLECTION_NOT_FOUND); - } catch (AuthorizationException) { - throw new Exception(Exception::USER_UNAUTHORIZED); - } catch (TimeoutException) { - throw new Exception(Exception::DATABASE_TIMEOUT); + } catch (RelationshipException $e) { + throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage()); + } catch (StructureException $e) { + throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $e->getMessage()); } $queueForEvents @@ -3567,16 +3598,15 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents') ->inject('dbForProject') ->inject('queueForStatsUsage') ->action(function (string $databaseId, string $collectionId, array $queries, Response $response, Database $dbForProject, StatsUsage $queueForStatsUsage) { - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); $isAPIKey = Auth::isAppUser(Authorization::getRoles()); $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::DATABASE_NOT_FOUND); } $collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId)); - if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::COLLECTION_NOT_FOUND); } @@ -3617,6 +3647,8 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents') $total = $dbForProject->count('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $queries, 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."); + } catch (QueryException $e) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); } $operations = 0; @@ -3684,33 +3716,6 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents') ->addMetric(METRIC_DATABASES_OPERATIONS_READS, \max(1, $operations)) ->addMetric(str_replace('{databaseInternalId}', $database->getInternalId(), METRIC_DATABASE_ID_OPERATIONS_READS), \max(1, $operations)); - $select = \array_reduce($queries, function ($result, $query) { - return $result || ($query->getMethod() === Query::TYPE_SELECT); - }, false); - - // Check if the SELECT query includes $databaseId and $collectionId - $hasDatabaseId = false; - $hasCollectionId = false; - if ($select) { - $hasDatabaseId = \array_reduce($queries, function ($result, $query) { - return $result || ($query->getMethod() === Query::TYPE_SELECT && \in_array('$databaseId', $query->getValues())); - }, false); - $hasCollectionId = \array_reduce($queries, function ($result, $query) { - return $result || ($query->getMethod() === Query::TYPE_SELECT && \in_array('$collectionId', $query->getValues())); - }, false); - } - - if ($select) { - foreach ($documents as $document) { - if (!$hasDatabaseId) { - $document->removeAttribute('$databaseId'); - } - if (!$hasCollectionId) { - $document->removeAttribute('$collectionId'); - } - } - } - $response->dynamic(new Document([ 'total' => $total, 'documents' => $documents, @@ -3745,29 +3750,26 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen ->inject('dbForProject') ->inject('queueForStatsUsage') ->action(function (string $databaseId, string $collectionId, string $documentId, array $queries, Response $response, Database $dbForProject, StatsUsage $queueForStatsUsage) { - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); $isAPIKey = Auth::isAppUser(Authorization::getRoles()); $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::DATABASE_NOT_FOUND); } $collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId)); - if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::COLLECTION_NOT_FOUND); } try { $queries = Query::parseQueries($queries); - $document = $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId, $queries); - } catch (AuthorizationException) { - throw new Exception(Exception::USER_UNAUTHORIZED); } catch (QueryException $e) { throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); } + $document = $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId, $queries); if ($document->isEmpty()) { throw new Exception(Exception::DOCUMENT_NOT_FOUND); } @@ -3856,15 +3858,12 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen ->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); } @@ -3985,11 +3984,10 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum throw new Exception(Exception::DOCUMENT_MISSING_PAYLOAD); } - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::DATABASE_NOT_FOUND); } @@ -4000,9 +3998,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum } // Read permission should not be required for update - /** @var Document $document */ $document = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId)); - if ($document->isEmpty()) { throw new Exception(Exception::DOCUMENT_NOT_FOUND); } @@ -4126,14 +4122,12 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum $document->getId(), $newDocument ); - } catch (AuthorizationException) { - throw new Exception(Exception::USER_UNAUTHORIZED); - } catch (DuplicateException) { - throw new Exception(Exception::DOCUMENT_ALREADY_EXISTS); + } catch (ConflictException) { + throw new Exception(Exception::DOCUMENT_UPDATE_CONFLICT); + } catch (RelationshipException $e) { + throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage()); } catch (StructureException $e) { throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $e->getMessage()); - } catch (NotFoundException $e) { - throw new Exception(Exception::COLLECTION_NOT_FOUND); } // Add $collectionId and $databaseId for all documents @@ -4277,14 +4271,12 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents') } }, ); + } catch (ConflictException) { + throw new Exception(Exception::DOCUMENT_UPDATE_CONFLICT); + } catch (RelationshipException $e) { + throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage()); } catch (StructureException $e) { throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $e->getMessage()); - } catch (NotFoundException) { - throw new Exception(Exception::COLLECTION_NOT_FOUND); - } catch (AuthorizationException) { - throw new Exception(Exception::USER_UNAUTHORIZED); - } catch (TimeoutException) { - throw new Exception(Exception::DATABASE_TIMEOUT); } foreach ($documents as $document) { @@ -4359,15 +4351,25 @@ App::put('/v1/databases/:databaseId/collections/:collectionId/documents') $upserted = []; - $modified = $dbForProject->createOrUpdateDocuments( - 'database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), - $documents, - onNext: function (Document $document) use ($plan, &$upserted) { - if (\count($upserted) < ($plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH)) { - $upserted[] = $document; - } - }, - ); + try { + $modified = $dbForProject->createOrUpdateDocuments( + 'database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), + $documents, + onNext: function (Document $document) use ($plan, &$upserted) { + if (\count($upserted) < ($plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH)) { + $upserted[] = $document; + } + }, + ); + } catch (ConflictException) { + throw new Exception(Exception::DOCUMENT_UPDATE_CONFLICT); + } catch (DuplicateException) { + throw new Exception(Exception::DOCUMENT_ALREADY_EXISTS); + } catch (RelationshipException $e) { + throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage()); + } catch (StructureException $e) { + throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $e->getMessage()); + } foreach ($upserted as $document) { $document->setAttribute('$databaseId', $database->getId()); @@ -4419,24 +4421,21 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu ->inject('queueForEvents') ->inject('queueForStatsUsage') ->action(function (string $databaseId, string $collectionId, string $documentId, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage) { - $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); - $isAPIKey = Auth::isAppUser(Authorization::getRoles()); $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); + $database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId)); if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::DATABASE_NOT_FOUND); } $collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $collectionId)); - if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) { throw new Exception(Exception::COLLECTION_NOT_FOUND); } // 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); } @@ -4446,8 +4445,10 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu 'database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId ); - } catch (NotFoundException $e) { - throw new Exception(Exception::COLLECTION_NOT_FOUND); + } catch (ConflictException) { + throw new Exception(Exception::DOCUMENT_UPDATE_CONFLICT); + } catch (RestrictedException) { + throw new Exception(Exception::DOCUMENT_DELETE_RESTRICTED); } $operations = 0; @@ -4582,12 +4583,10 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents') } }, ); - } catch (NotFoundException) { - throw new Exception(Exception::COLLECTION_NOT_FOUND); - } catch (AuthorizationException) { - throw new Exception(Exception::USER_UNAUTHORIZED); - } catch (TimeoutException) { - throw new Exception(Exception::DATABASE_TIMEOUT); + } catch (ConflictException) { + throw new Exception(Exception::DOCUMENT_UPDATE_CONFLICT); + } catch (RestrictedException) { + throw new Exception(Exception::DOCUMENT_DELETE_RESTRICTED); } foreach ($documents as $document) { @@ -4628,7 +4627,6 @@ App::get('/v1/databases/usage') ->inject('response') ->inject('dbForProject') ->action(function (string $range, Response $response, Database $dbForProject) { - $periods = Config::getParam('usage', []); $stats = $usage = []; $days = $periods[$range]; @@ -4725,9 +4723,7 @@ App::get('/v1/databases/:databaseId/usage') ->inject('response') ->inject('dbForProject') ->action(function (string $databaseId, string $range, Response $response, Database $dbForProject) { - $database = $dbForProject->getDocument('databases', $databaseId); - if ($database->isEmpty()) { throw new Exception(Exception::DATABASE_NOT_FOUND); } @@ -4828,7 +4824,6 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/usage') ->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()); diff --git a/app/controllers/general.php b/app/controllers/general.php index a73d41268f..322787cbd0 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -835,35 +835,11 @@ App::error() break; } break; - case 'Utopia\Database\Exception\Conflict': - $error = new AppwriteException(AppwriteException::DOCUMENT_UPDATE_CONFLICT, previous: $error); - break; - case 'Utopia\Database\Exception\Timeout': - $error = new AppwriteException(AppwriteException::DATABASE_TIMEOUT, previous: $error); - break; - case 'Utopia\Database\Exception\Query': - $error = new AppwriteException(AppwriteException::GENERAL_QUERY_INVALID, $error->getMessage(), previous: $error); - break; - case 'Utopia\Database\Exception\Structure': - $error = new AppwriteException(AppwriteException::DOCUMENT_INVALID_STRUCTURE, $error->getMessage(), previous: $error); - break; - case 'Utopia\Database\Exception\Duplicate': - $error = new AppwriteException(AppwriteException::DOCUMENT_ALREADY_EXISTS); - break; - case 'Utopia\Database\Exception\Restricted': - $error = new AppwriteException(AppwriteException::DOCUMENT_DELETE_RESTRICTED); - break; case 'Utopia\Database\Exception\Authorization': $error = new AppwriteException(AppwriteException::USER_UNAUTHORIZED); break; - case 'Utopia\Database\Exception\Relationship': - $error = new AppwriteException(AppwriteException::RELATIONSHIP_VALUE_INVALID, $error->getMessage(), previous: $error); - break; - case 'Utopia\Database\Exception\NotFound': - $error = new AppwriteException(AppwriteException::COLLECTION_NOT_FOUND, $error->getMessage(), previous: $error); - break; - case 'Utopia\Database\Exception\Dependency': - $error = new AppwriteException(AppwriteException::INDEX_DEPENDENCY, null, previous: $error); + case 'Utopia\Database\Exception\Timeout': + $error = new AppwriteException(AppwriteException::DATABASE_TIMEOUT, previous: $error); break; } From 11e4e9432ed16eaed7580baf87b85086acfbad67 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 16 May 2025 00:14:09 +1200 Subject: [PATCH 11/17] Fix tests --- app/controllers/api/databases.php | 69 ++++++++++++++++++++----------- 1 file changed, 46 insertions(+), 23 deletions(-) diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index 5eae500119..204e855874 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -379,9 +379,8 @@ function updateAttribute( } if ($primaryDocumentOptions['twoWay']) { - $relatedCollection = $dbForProject->getDocument('database_' . $db->getInternalId(), $primaryDocumentOptions['relatedCollection']); - - $relatedAttribute = $dbForProject->getDocument('attributes', $db->getInternalId() . '_' . $relatedCollection->getInternalId() . '_' . $primaryDocumentOptions['twoWayKey']); + $relatedCollection = $dbForProject->getDocument('database_' . $database->getInternalId(), $primaryDocumentOptions['relatedCollection']); + $relatedAttribute = $dbForProject->getDocument('attributes', $database->getInternalId() . '_' . $relatedCollection->getInternalId() . '_' . $primaryDocumentOptions['twoWayKey']); if (!empty($newKey) && $newKey !== $key) { $options['twoWayKey'] = $newKey; @@ -446,7 +445,7 @@ function updateAttribute( } } } else { - $attribute = $dbForProject->updateDocument('attributes', $db->getInternalId() . '_' . $collection->getInternalId() . '_' . $key, $attribute); + $attribute = $dbForProject->updateDocument('attributes', $database->getInternalId() . '_' . $collection->getInternalId() . '_' . $key, $attribute); } $dbForProject->purgeCachedDocument('database_' . $database->getInternalId(), $collection->getId()); @@ -501,8 +500,7 @@ App::post('/v1/databases') ->inject('response') ->inject('dbForProject') ->inject('queueForEvents') - ->inject('queueForStatsUsage') - ->action(function (string $databaseId, string $name, bool $enabled, Response $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage) { + ->action(function (string $databaseId, string $name, bool $enabled, Response $response, Database $dbForProject, Event $queueForEvents) { $databaseId = $databaseId === 'unique()' ? ID::unique() @@ -834,8 +832,7 @@ App::delete('/v1/databases/:databaseId') ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') - ->inject('queueForStatsUsage') - ->action(function (string $databaseId, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, StatsUsage $queueForStatsUsage) { + ->action(function (string $databaseId, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { $database = $dbForProject->getDocument('databases', $databaseId); if ($database->isEmpty()) { @@ -974,8 +971,7 @@ App::get('/v1/databases/:databaseId/collections') ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true) ->inject('response') ->inject('dbForProject') - ->inject('mode') - ->action(function (string $databaseId, array $queries, string $search, Response $response, Database $dbForProject, string $mode) { + ->action(function (string $databaseId, array $queries, string $search, Response $response, Database $dbForProject) { $database = $dbForProject->getDocument('databases', $databaseId); if ($database->isEmpty()) { @@ -1059,8 +1055,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId') ->param('collectionId', '', new UID(), 'Collection ID.') ->inject('response') ->inject('dbForProject') - ->inject('mode') - ->action(function (string $databaseId, string $collectionId, Response $response, Database $dbForProject, string $mode) { + ->action(function (string $databaseId, string $collectionId, Response $response, Database $dbForProject) { $database = $dbForProject->getDocument('databases', $databaseId); if ($database->isEmpty()) { @@ -1290,8 +1285,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId') ->inject('dbForProject') ->inject('queueForDatabase') ->inject('queueForEvents') - ->inject('mode') - ->action(function (string $databaseId, string $collectionId, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, string $mode) { + ->action(function (string $databaseId, string $collectionId, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents) { $database = $dbForProject->getDocument('databases', $databaseId); if ($database->isEmpty()) { @@ -3074,7 +3068,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes') }); App::get('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') - ->alias('/v1/database/collections/:collectionId/indexes/:key', ['databaseId' => 'default']) + ->alias('/v1/database/collections/:collectionId/indexes/:key') ->desc('Get index') ->groups(['api', 'database']) ->label('scope', 'collections.read') @@ -3122,7 +3116,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') App::delete('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') - ->alias('/v1/database/collections/:collectionId/indexes/:key', ['databaseId' => 'default']) + ->alias('/v1/database/collections/:collectionId/indexes/:key') ->desc('Delete index') ->groups(['api', 'database']) ->label('scope', 'collections.write') @@ -3195,7 +3189,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/indexes/:key') }); App::post('/v1/databases/:databaseId/collections/:collectionId/documents') - ->alias('/v1/database/collections/:collectionId/documents', ['databaseId' => 'default']) + ->alias('/v1/database/collections/:collectionId/documents') ->desc('Create document') ->groups(['api', 'database']) ->label('scope', 'documents.write') @@ -3572,7 +3566,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/documents') }); App::get('/v1/databases/:databaseId/collections/:collectionId/documents') - ->alias('/v1/database/collections/:collectionId/documents', ['databaseId' => 'default']) + ->alias('/v1/database/collections/:collectionId/documents') ->desc('List documents') ->groups(['api', 'database']) ->label('scope', 'documents.read') @@ -3716,6 +3710,33 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents') ->addMetric(METRIC_DATABASES_OPERATIONS_READS, \max(1, $operations)) ->addMetric(str_replace('{databaseInternalId}', $database->getInternalId(), METRIC_DATABASE_ID_OPERATIONS_READS), \max(1, $operations)); + $select = \array_reduce($queries, function ($result, $query) { + return $result || ($query->getMethod() === Query::TYPE_SELECT); + }, false); + + // Check if the SELECT query includes $databaseId and $collectionId + $hasDatabaseId = false; + $hasCollectionId = false; + if ($select) { + $hasDatabaseId = \array_reduce($queries, function ($result, $query) { + return $result || ($query->getMethod() === Query::TYPE_SELECT && \in_array('$databaseId', $query->getValues())); + }, false); + $hasCollectionId = \array_reduce($queries, function ($result, $query) { + return $result || ($query->getMethod() === Query::TYPE_SELECT && \in_array('$collectionId', $query->getValues())); + }, false); + } + + if ($select) { + foreach ($documents as $document) { + if (!$hasDatabaseId) { + $document->removeAttribute('$databaseId'); + } + if (!$hasCollectionId) { + $document->removeAttribute('$collectionId'); + } + } + } + $response->dynamic(new Document([ 'total' => $total, 'documents' => $documents, @@ -3723,7 +3744,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents') }); App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId') - ->alias('/v1/database/collections/:collectionId/documents/:documentId', ['databaseId' => 'default']) + ->alias('/v1/database/collections/:collectionId/documents/:documentId') ->desc('Get document') ->groups(['api', 'database']) ->label('scope', 'documents.read') @@ -3830,7 +3851,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen }); App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId/logs') - ->alias('/v1/database/collections/:collectionId/documents/:documentId/logs', ['databaseId' => 'default']) + ->alias('/v1/database/collections/:collectionId/documents/:documentId/logs') ->desc('List document logs') ->groups(['api', 'database']) ->label('scope', 'documents.read') @@ -4124,7 +4145,9 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum ); } catch (ConflictException) { throw new Exception(Exception::DOCUMENT_UPDATE_CONFLICT); - } catch (RelationshipException $e) { + } catch (DuplicateException) { + throw new Exception(Exception::DOCUMENT_ALREADY_EXISTS); + } catch (RelationshipException $e) { throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage()); } catch (StructureException $e) { throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $e->getMessage()); @@ -4387,7 +4410,7 @@ App::put('/v1/databases/:databaseId/collections/:collectionId/documents') }); App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId') - ->alias('/v1/database/collections/:collectionId/documents/:documentId', ['databaseId' => 'default']) + ->alias('/v1/database/collections/:collectionId/documents/:documentId') ->desc('Delete document') ->groups(['api', 'database']) ->label('scope', 'documents.write') @@ -4799,7 +4822,7 @@ App::get('/v1/databases/:databaseId/usage') }); App::get('/v1/databases/:databaseId/collections/:collectionId/usage') - ->alias('/v1/database/:collectionId/usage', ['databaseId' => 'default']) + ->alias('/v1/database/:collectionId/usage') ->desc('Get collection usage stats') ->groups(['api', 'database', 'usage']) ->label('scope', 'collections.read') From 4f2142a47045767fdb79170c9c18f5f9fc5c92ff Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 16 May 2025 00:22:52 +1200 Subject: [PATCH 12/17] Lint --- app/controllers/api/databases.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index 204e855874..e6f7cb61db 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -4147,7 +4147,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum throw new Exception(Exception::DOCUMENT_UPDATE_CONFLICT); } catch (DuplicateException) { throw new Exception(Exception::DOCUMENT_ALREADY_EXISTS); - } catch (RelationshipException $e) { + } catch (RelationshipException $e) { throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage()); } catch (StructureException $e) { throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $e->getMessage()); From 738e256692856c6036e684f3e2bbd5aef66fb9bb Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 16 May 2025 00:25:23 +1200 Subject: [PATCH 13/17] Remove redundant catches --- app/controllers/api/databases.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index e6f7cb61db..37ec19bf3b 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -538,16 +538,12 @@ App::post('/v1/databases') try { $dbForProject->createCollection('database_' . $database->getInternalId(), $attributes, $indexes); - } catch (AuthorizationException) { - throw new Exception(Exception::USER_UNAUTHORIZED); } catch (DuplicateException) { throw new Exception(Exception::DATABASE_ALREADY_EXISTS); } catch (IndexException) { throw new Exception(Exception::INDEX_INVALID); } catch (LimitException) { throw new Exception(Exception::COLLECTION_LIMIT_EXCEEDED); - } catch (TimeoutException) { - throw new Exception(Exception::DATABASE_TIMEOUT); } $queueForEvents->setParam('databaseId', $database->getId()); From 6e40070b98612dfe26efe4c98bbb6ce4f22cac26 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 16 May 2025 00:35:21 +1200 Subject: [PATCH 14/17] Lint --- app/controllers/api/databases.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index 37ec19bf3b..675268b894 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -37,7 +37,6 @@ use Utopia\Database\Exception\Query as QueryException; use Utopia\Database\Exception\Relationship as RelationshipException; use Utopia\Database\Exception\Restricted as RestrictedException; use Utopia\Database\Exception\Structure as StructureException; -use Utopia\Database\Exception\Timeout as TimeoutException; use Utopia\Database\Exception\Truncate as TruncateException; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; From 5fe6359fe6d834a92d47fd12c7f6296eee90e2c2 Mon Sep 17 00:00:00 2001 From: Fabian Gruber <1951610+basert@users.noreply.github.com> Date: Fri, 16 May 2025 10:56:12 +0200 Subject: [PATCH 15/17] Create unique stable cache identifier (#9769) --- app/controllers/shared/api.php | 4 ++-- src/Appwrite/Utopia/Request.php | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 4ac8f0f13a..dfa070063c 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -544,7 +544,7 @@ App::init() $isImageTransformation = $route->getPath() === '/v1/storage/buckets/:bucketId/files/:fileId/preview'; $isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && !Auth::isPrivilegedUser(Authorization::getRoles()); - $key = md5($request->getURI() . '*' . implode('*', $request->getParams()) . '*' . APP_CACHE_BUSTER); + $key = $request->cacheIdentifier(); $cacheLog = Authorization::skip(fn () => $dbForProject->getDocument('cache', $key)); $cache = new Cache( new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId()) @@ -803,7 +803,7 @@ App::shutdown() $resourceType = $parseLabel($pattern, $responsePayload, $requestParams, $user); } - $key = md5($request->getURI() . '*' . implode('*', $request->getParams()) . '*' . APP_CACHE_BUSTER); + $key = $request->cacheIdentifier(); $signature = md5($data['payload']); $cacheLog = Authorization::skip(fn () => $dbForProject->getDocument('cache', $key)); $accessedAt = $cacheLog->getAttribute('accessedAt', ''); diff --git a/src/Appwrite/Utopia/Request.php b/src/Appwrite/Utopia/Request.php index c50dea2713..558f0cdf09 100644 --- a/src/Appwrite/Utopia/Request.php +++ b/src/Appwrite/Utopia/Request.php @@ -220,4 +220,17 @@ class Request extends UtopiaRequest return UtopiaRequest::getUserAgent($default); } + + /** + * Creates a unique stable cache identifier for this GET request. + * Stable-sorts query params, use `serialize` to ensure key&value are part of cache keys. + * + * @return string + */ + public function cacheIdentifier(): string + { + $params = $this->getParams(); + ksort($params); + return md5($this->getURI() . '*' . serialize($params) . '*' . APP_CACHE_BUSTER); + } } From 61ac661851bbdc4509ef74363c7ada435759fe35 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Fri, 16 May 2025 19:18:08 +0200 Subject: [PATCH 16/17] chore: update storage library (#9776) * chore: update storage library * chore: update storage library * chore: revert f802fb9 and 651cec6 * chore: revert composer * chore: revert lockfile --- app/init/resources.php | 24 ++++++++++++------------ app/worker.php | 29 ++++++++++++----------------- 2 files changed, 24 insertions(+), 29 deletions(-) diff --git a/app/init/resources.php b/app/init/resources.php index 6bd40c6593..c719a47344 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -507,21 +507,21 @@ App::setResource('timelimit', function (\Redis $redis) { }; }, ['redis']); -App::setResource('deviceForLocal', function (Telemetry $telemetry) { - return new Device\Telemetry($telemetry, new Local()); -}, ['telemetry']); +App::setResource('deviceForLocal', function () { + return new Local(); +}); -App::setResource('deviceForFiles', function ($project, Telemetry $telemetry) { - return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId())); -}, ['project', 'telemetry']); +App::setResource('deviceForFiles', function ($project) { + return getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId()); +}, ['project']); -App::setResource('deviceForFunctions', function ($project, Telemetry $telemetry) { - return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId())); -}, ['project', 'telemetry']); +App::setResource('deviceForFunctions', function ($project) { + return getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId()); +}, ['project']); -App::setResource('deviceForBuilds', function ($project, Telemetry $telemetry) { - return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId())); -}, ['project', 'telemetry']); +App::setResource('deviceForBuilds', function ($project) { + return getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId()); +}, ['project']); function getDevice(string $root, string $connection = ''): Device { diff --git a/app/worker.php b/app/worker.php index 29ebc836cd..232e0b3684 100644 --- a/app/worker.php +++ b/app/worker.php @@ -37,10 +37,7 @@ use Utopia\Queue\Message; use Utopia\Queue\Publisher; use Utopia\Queue\Server; use Utopia\Registry\Registry; -use Utopia\Storage\Device; use Utopia\System\System; -use Utopia\Telemetry\Adapter as Telemetry; -use Utopia\Telemetry\Adapter\None as NoTelemetry; Authorization::disable(); Runtime::enableCoroutine(SWOOLE_HOOK_ALL); @@ -337,23 +334,21 @@ Server::setResource('pools', function (Registry $register) { return $register->get('pools'); }, ['register']); -Server::setResource('telemetry', fn () => new NoTelemetry()); +Server::setResource('deviceForFunctions', function (Document $project) { + return getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId()); +}, ['project']); -Server::setResource('deviceForFunctions', function (Document $project, Telemetry $telemetry) { - return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId())); -}, ['project', 'telemetry']); +Server::setResource('deviceForFiles', function (Document $project) { + return getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId()); +}, ['project']); -Server::setResource('deviceForFiles', function (Document $project, Telemetry $telemetry) { - return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId())); -}, ['project', 'telemetry']); +Server::setResource('deviceForBuilds', function (Document $project) { + return getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId()); +}, ['project']); -Server::setResource('deviceForBuilds', function (Document $project, Telemetry $telemetry) { - return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId())); -}, ['project', 'telemetry']); - -Server::setResource('deviceForCache', function (Document $project, Telemetry $telemetry) { - return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_CACHE . '/app-' . $project->getId())); -}, ['project', 'telemetry']); +Server::setResource('deviceForCache', function (Document $project) { + return getDevice(APP_STORAGE_CACHE . '/app-' . $project->getId()); +}, ['project']); Server::setResource( 'isResourceBlocked', From 5d25265e5c92de1330aad70252bae8618803f887 Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Fri, 16 May 2025 14:46:24 -0700 Subject: [PATCH 17/17] fix(storage): do not preview gif input/output Processing a gif file can consume large amounts of memory causing the container to crash. As a safety precaution, don't output to gif either. Any attempt to output to gif will fallback to jpg. --- app/config/storage/inputs.php | 1 - app/config/storage/outputs.php | 1 - 2 files changed, 2 deletions(-) diff --git a/app/config/storage/inputs.php b/app/config/storage/inputs.php index 713801cd7c..edcf667d86 100644 --- a/app/config/storage/inputs.php +++ b/app/config/storage/inputs.php @@ -4,7 +4,6 @@ return [ // Accepted inputs files "jpg" => "image/jpeg", "jpeg" => "image/jpeg", - "gif" => "image/gif", "png" => "image/png", "heic" => "image/heic", "webp" => "image/webp", diff --git a/app/config/storage/outputs.php b/app/config/storage/outputs.php index 49548dda50..519ff825fe 100644 --- a/app/config/storage/outputs.php +++ b/app/config/storage/outputs.php @@ -4,7 +4,6 @@ return [ // Accepted outputs files "jpg" => "image/jpeg", "jpeg" => "image/jpeg", - "gif" => "image/gif", "png" => "image/png", "webp" => "image/webp", "heic" => "image/heic",