From 8d9abd2f31f5302453b2b505f6d79e6335eac170 Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Fri, 25 Apr 2025 16:06:35 -0700 Subject: [PATCH 01/37] 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/37] 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 ea42b823d8563c2c0638eaedce3694c3b3ac57db Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 30 Apr 2025 22:47:41 +1200 Subject: [PATCH 03/37] Revert name merge issue --- src/Appwrite/Messaging/Adapter/Realtime.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Appwrite/Messaging/Adapter/Realtime.php b/src/Appwrite/Messaging/Adapter/Realtime.php index 18b0d94e7d..be263aa655 100644 --- a/src/Appwrite/Messaging/Adapter/Realtime.php +++ b/src/Appwrite/Messaging/Adapter/Realtime.php @@ -36,12 +36,12 @@ class Realtime extends MessagingAdapter */ public array $subscriptions = []; - private PubSubPool $redis; + private PubSubPool $pubSubPool; public function __construct() { global $register; - $this->redis = new PubSubPool($register->get('pools')->get('pubsub')); + $this->pubSubPool = new PubSubPool($register->get('pools')->get('pubsub')); } /** @@ -148,7 +148,7 @@ class Realtime extends MessagingAdapter $permissionsChanged = array_key_exists('permissionsChanged', $options) && $options['permissionsChanged']; $userId = array_key_exists('userId', $options) ? $options['userId'] : null; - $this->redis->publish('realtime', json_encode([ + $this->pubSubPool->publish('realtime', json_encode([ 'project' => $projectId, 'roles' => $roles, 'permissionsChanged' => $permissionsChanged, From 977a920959bfa5f4786c032a3e84cf0ebd1c8512 Mon Sep 17 00:00:00 2001 From: Steven Nguyen Date: Wed, 30 Apr 2025 17:23:50 -0700 Subject: [PATCH 04/37] 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 05/37] 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 06/37] 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 07/37] 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 08/37] 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 09/37] 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 10/37] 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 406630cb61eaa54b314e183a99f44c932199847a Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 14 May 2025 16:40:47 +1200 Subject: [PATCH 11/37] Update deps --- composer.lock | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/composer.lock b/composer.lock index cf15b7657e..5a3d58a047 100644 --- a/composer.lock +++ b/composer.lock @@ -1109,16 +1109,16 @@ }, { "name": "open-telemetry/api", - "version": "1.2.3", + "version": "1.3.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/api.git", - "reference": "199d7ddda88f5f5619fa73463f1a5a7149ccd1f1" + "reference": "4e3bb38e069876fb73c2ce85c89583bf2b28cd86" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/199d7ddda88f5f5619fa73463f1a5a7149ccd1f1", - "reference": "199d7ddda88f5f5619fa73463f1a5a7149ccd1f1", + "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/4e3bb38e069876fb73c2ce85c89583bf2b28cd86", + "reference": "4e3bb38e069876fb73c2ce85c89583bf2b28cd86", "shasum": "" }, "require": { @@ -1175,7 +1175,7 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-03-05T21:42:54+00:00" + "time": "2025-05-07T12:32:21+00:00" }, { "name": "open-telemetry/context", @@ -1238,16 +1238,16 @@ }, { "name": "open-telemetry/exporter-otlp", - "version": "1.2.1", + "version": "1.3.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/exporter-otlp.git", - "reference": "b7580440b7481a98da97aceabeb46e1b276c8747" + "reference": "19adf03d2b0f91f9e9b1c7f93db6c755c737cf6c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/exporter-otlp/zipball/b7580440b7481a98da97aceabeb46e1b276c8747", - "reference": "b7580440b7481a98da97aceabeb46e1b276c8747", + "url": "https://api.github.com/repos/opentelemetry-php/exporter-otlp/zipball/19adf03d2b0f91f9e9b1c7f93db6c755c737cf6c", + "reference": "19adf03d2b0f91f9e9b1c7f93db6c755c737cf6c", "shasum": "" }, "require": { @@ -1298,7 +1298,7 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-03-06T23:21:56+00:00" + "time": "2025-05-12T00:36:35+00:00" }, { "name": "open-telemetry/gen-otlp-protobuf", @@ -1365,16 +1365,16 @@ }, { "name": "open-telemetry/sdk", - "version": "1.3.0", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/sdk.git", - "reference": "05d9ceb6773b5bddcf485af6d4a6f543bbeb980b" + "reference": "939d3a28395c249a763676458140dad44b3a8011" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/05d9ceb6773b5bddcf485af6d4a6f543bbeb980b", - "reference": "05d9ceb6773b5bddcf485af6d4a6f543bbeb980b", + "url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/939d3a28395c249a763676458140dad44b3a8011", + "reference": "939d3a28395c249a763676458140dad44b3a8011", "shasum": "" }, "require": { @@ -1451,7 +1451,7 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-05-01T23:20:43+00:00" + "time": "2025-05-07T12:32:21+00:00" }, { "name": "open-telemetry/sem-conv", From 3e914a4f1b698485176bbcd171acbc8de9a0ed64 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 30 Apr 2025 22:47:41 +1200 Subject: [PATCH 12/37] Revert name merge issue --- src/Appwrite/Messaging/Adapter/Realtime.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Appwrite/Messaging/Adapter/Realtime.php b/src/Appwrite/Messaging/Adapter/Realtime.php index 1963bdedd6..96793b2683 100644 --- a/src/Appwrite/Messaging/Adapter/Realtime.php +++ b/src/Appwrite/Messaging/Adapter/Realtime.php @@ -36,12 +36,12 @@ class Realtime extends Adapter */ public array $subscriptions = []; - private Pool $pubsubPool; + private PubSubPool $pubSubPool; public function __construct() { global $register; - $this->pubsubPool = $register->get('pools')->get('pubsub'); + $this->pubSubPool = new PubSubPool($register->get('pools')->get('pubsub')); } /** @@ -147,7 +147,7 @@ class Realtime extends Adapter $permissionsChanged = array_key_exists('permissionsChanged', $options) && $options['permissionsChanged']; $userId = array_key_exists('userId', $options) ? $options['userId'] : null; - $message = [ + $this->pubSubPool->publish('realtime', json_encode([ 'project' => $projectId, 'roles' => $roles, 'permissionsChanged' => $permissionsChanged, From 16b2449787954c2211f431323137d252356c3f3e Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 14 May 2025 18:14:07 +1200 Subject: [PATCH 13/37] Revert "Merge pull request #9703 from appwrite/revert-9659-feat-pool-adapter" This reverts commit bf9deb09f588b59399b7207fc93311f3ec2b8fe3, reversing changes made to d312fe22ff264558e240565cac4fcb2f6a24a401. # Conflicts: # app/cli.php # app/init/registers.php # composer.lock # src/Appwrite/Messaging/Adapter/Realtime.php # src/Appwrite/Platform/Tasks/ScheduleBase.php # src/Appwrite/Platform/Tasks/ScheduleExecutions.php # src/Appwrite/Platform/Tasks/ScheduleFunctions.php # src/Appwrite/Platform/Tasks/ScheduleMessages.php --- app/cli.php | 42 ++---- app/controllers/api/health.php | 137 +++++++++--------- app/controllers/api/projects.php | 3 +- app/http.php | 19 +-- app/init/registers.php | 4 +- app/init/resources.php | 52 +++---- app/realtime.php | 110 ++++++++------ app/worker.php | 69 +++------ composer.json | 2 +- composer.lock | 31 ++-- src/Appwrite/Event/Event.php | 7 - src/Appwrite/Messaging/Adapter/Realtime.php | 25 ++-- src/Appwrite/Platform/Tasks/Doctor.php | 50 ++++--- src/Appwrite/Platform/Tasks/ScheduleBase.php | 6 +- .../Platform/Tasks/ScheduleExecutions.php | 12 +- .../Platform/Tasks/ScheduleFunctions.php | 13 +- .../Platform/Tasks/ScheduleMessages.php | 7 +- src/Appwrite/Platform/Workers/Deletes.php | 5 +- src/Appwrite/Platform/Workers/StatsUsage.php | 12 +- .../Platform/Workers/StatsUsageDump.php | 15 +- src/Appwrite/PubSub/Adapter/Pool.php | 46 ++++++ tests/resources/docker/docker-compose.yml | 32 +--- 22 files changed, 317 insertions(+), 382 deletions(-) create mode 100644 src/Appwrite/PubSub/Adapter/Pool.php diff --git a/app/cli.php b/app/cli.php index 9ade97e90c..61d8a90100 100644 --- a/app/cli.php +++ b/app/cli.php @@ -10,12 +10,13 @@ use Appwrite\Event\StatsUsage; use Appwrite\Platform\Appwrite; use Appwrite\Runtimes\Runtimes; use Executor\Executor; -use Swoole\Timer; +use Utopia\Cache\Adapter\Pool as CachePool; use Utopia\Cache\Adapter\Sharding; use Utopia\Cache\Cache; use Utopia\CLI\CLI; use Utopia\CLI\Console; use Utopia\Config\Config; +use Utopia\Database\Adapter\Pool as DatabasePool; use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; @@ -23,11 +24,12 @@ use Utopia\DSN\DSN; use Utopia\Logger\Log; use Utopia\Platform\Service; use Utopia\Pools\Group; +use Utopia\Queue\Broker\Pool as BrokerPool; use Utopia\Queue\Publisher; use Utopia\Registry\Registry; use Utopia\System\System; use Utopia\Telemetry\Adapter\None as NoTelemetry; - +use Swoole\Timer; use function Swoole\Coroutine\run; // Overwriting runtimes to be architecture agnostic for CLI @@ -45,10 +47,7 @@ CLI::setResource('cache', function ($pools) { $adapters = []; foreach ($list as $value) { - $adapters[] = $pools - ->get($value) - ->pop() - ->getResource(); + $adapters[] = new CachePool($pools->get($value)); } return new Cache(new Sharding($adapters)); @@ -68,12 +67,8 @@ CLI::setResource('dbForPlatform', function ($pools, $cache) { $attempts++; try { // Prepare database connection - $dbAdapter = $pools - ->get('console') - ->pop() - ->getResource(); - - $dbForPlatform = new Database($dbAdapter, $cache); + $adapter = new DatabasePool($pools->get('console')); + $dbForPlatform = new Database($adapter, $cache); $dbForPlatform ->setNamespace('_console') @@ -91,7 +86,6 @@ CLI::setResource('dbForPlatform', function ($pools, $cache) { $ready = true; } catch (\Throwable $err) { Console::warning($err->getMessage()); - $pools->get('console')->reclaim(); sleep($sleep); } } while ($attempts < $maxAttempts && !$ready); @@ -141,12 +135,8 @@ CLI::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform return $database; } - $dbAdapter = $pools - ->get($dsn->getHost()) - ->pop() - ->getResource(); - - $database = new Database($dbAdapter, $cache); + $adapter = new DatabasePool($pools->get($dsn->getHost())); + $database = new Database($adapter, $cache); $databases[$dsn->getHost()] = $database; $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); @@ -172,21 +162,15 @@ CLI::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform CLI::setResource('getLogsDB', function (Group $pools, Cache $cache) { $database = null; + return function (?Document $project = null) use ($pools, $cache, $database) { if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') { $database->setTenant($project->getInternalId()); return $database; } - $dbAdapter = $pools - ->get('logs') - ->pop() - ->getResource(); - - $database = new Database( - $dbAdapter, - $cache - ); + $adapter = new DatabasePool($pools->get('logs')); + $database = new Database($adapter, $cache); $database ->setSharedTables(true) @@ -210,7 +194,7 @@ CLI::setResource('queueForStatsResources', function (Publisher $publisher) { return new StatsResources($publisher); }, ['publisher']); CLI::setResource('publisher', function (Group $pools) { - return $pools->get('publisher')->pop()->getResource(); + return new BrokerPool(publisher: $pools->get('publisher')); }, ['pools']); CLI::setResource('queueForFunctions', function (Publisher $publisher) { return new Func($publisher); diff --git a/app/controllers/api/health.php b/app/controllers/api/health.php index 3602ab8ff9..5fe2de5549 100644 --- a/app/controllers/api/health.php +++ b/app/controllers/api/health.php @@ -3,13 +3,16 @@ use Appwrite\ClamAV\Network; use Appwrite\Event\Event; use Appwrite\Extend\Exception; +use Appwrite\PubSub\Adapter\Pool as PubSubPool; use Appwrite\SDK\AuthType; use Appwrite\SDK\ContentType; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\App; +use Utopia\Cache\Adapter\Pool as CachePool; use Utopia\Config\Config; +use Utopia\Database\Adapter\Pool as DatabasePool; use Utopia\Database\Document; use Utopia\Domains\Validator\PublicDomain; use Utopia\Pools\Group; @@ -34,8 +37,8 @@ App::get('/v1/health') namespace: 'health', group: 'health', name: 'get', - auth: [AuthType::KEY], description: '/docs/references/health/get.md', + auth: [AuthType::KEY], responses: [ new SDKResponse( code: Response::STATUS_CODE_OK, @@ -70,11 +73,11 @@ App::get('/v1/health/db') ->groups(['api', 'health']) ->label('scope', 'health.read') ->label('sdk', new Method( - auth: [AuthType::KEY], namespace: 'health', group: 'health', name: 'getDB', description: '/docs/references/health/get-db.md', + auth: [AuthType::KEY], responses: [ new SDKResponse( code: Response::STATUS_CODE_OK, @@ -86,8 +89,8 @@ App::get('/v1/health/db') ->inject('response') ->inject('pools') ->action(function (Response $response, Group $pools) { - $output = []; + $failures = []; $configs = [ 'Console.DB' => Config::getParam('pools-console'), @@ -97,7 +100,7 @@ App::get('/v1/health/db') foreach ($configs as $key => $config) { foreach ($config as $database) { try { - $adapter = $pools->get($database)->pop()->getResource(); + $adapter = new DatabasePool($pools->get($database)); $checkStart = \microtime(true); @@ -108,16 +111,16 @@ App::get('/v1/health/db') 'ping' => \round((\microtime(true) - $checkStart) / 1000) ]); } else { - $failure[] = $database; + $failures[] = $database; } - } catch (\Throwable $th) { - $failure[] = $database; + } catch (\Throwable) { + $failures[] = $database; } } } - if (!empty($failure)) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, 'DB failure on: ' . implode(", ", $failure)); + if (!empty($failures)) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'DB failure on: ' . implode(", ", $failures)); } $response->dynamic(new Document([ @@ -131,11 +134,11 @@ App::get('/v1/health/cache') ->groups(['api', 'health']) ->label('scope', 'health.read') ->label('sdk', new Method( - auth: [AuthType::KEY], namespace: 'health', group: 'health', name: 'getCache', description: '/docs/references/health/get-cache.md', + auth: [AuthType::KEY], responses: [ new SDKResponse( code: Response::STATUS_CODE_OK, @@ -147,44 +150,39 @@ App::get('/v1/health/cache') ->inject('response') ->inject('pools') ->action(function (Response $response, Group $pools) { - $output = []; + $failures = []; $configs = [ 'Cache' => Config::getParam('pools-cache'), ]; foreach ($configs as $key => $config) { - foreach ($config as $database) { + foreach ($config as $cache) { try { - /** @var \Utopia\Cache\Adapter $adapter */ - $adapter = $pools->get($database)->pop()->getResource(); + $adapter = new CachePool($pools->get($cache)); $checkStart = \microtime(true); if ($adapter->ping()) { $output[] = new Document([ - 'name' => $key . " ($database)", + 'name' => $key . " ($cache)", 'status' => 'pass', 'ping' => \round((\microtime(true) - $checkStart) / 1000) ]); } else { - $output[] = new Document([ - 'name' => $key . " ($database)", - 'status' => 'fail', - 'ping' => \round((\microtime(true) - $checkStart) / 1000) - ]); + $failures[] = $cache; } - } catch (\Throwable $th) { - $output[] = new Document([ - 'name' => $key . " ($database)", - 'status' => 'fail', - 'ping' => \round((\microtime(true) - $checkStart) / 1000) - ]); + } catch (\Throwable) { + $failures[] = $cache; } } } + if (!empty($failures)) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Cache failure on: ' . implode(", ", $failures)); + } + $response->dynamic(new Document([ 'statuses' => $output, 'total' => count($output), @@ -196,11 +194,11 @@ App::get('/v1/health/pubsub') ->groups(['api', 'health']) ->label('scope', 'health.read') ->label('sdk', new Method( - auth: [AuthType::KEY], namespace: 'health', group: 'health', name: 'getPubSub', description: '/docs/references/health/get-pubsub.md', + auth: [AuthType::KEY], responses: [ new SDKResponse( code: Response::STATUS_CODE_OK, @@ -212,44 +210,39 @@ App::get('/v1/health/pubsub') ->inject('response') ->inject('pools') ->action(function (Response $response, Group $pools) { - $output = []; + $failures = []; $configs = [ 'PubSub' => Config::getParam('pools-pubsub'), ]; foreach ($configs as $key => $config) { - foreach ($config as $database) { + foreach ($config as $pubsub) { try { - /** @var \Appwrite\PubSub\Adapter $adapter */ - $adapter = $pools->get($database)->pop()->getResource(); + $adapter = new PubSubPool($pools->get($pubsub)); $checkStart = \microtime(true); if ($adapter->ping()) { $output[] = new Document([ - 'name' => $key . " ($database)", + 'name' => $key . " ($pubsub)", 'status' => 'pass', 'ping' => \round((\microtime(true) - $checkStart) / 1000) ]); } else { - $output[] = new Document([ - 'name' => $key . " ($database)", - 'status' => 'fail', - 'ping' => \round((\microtime(true) - $checkStart) / 1000) - ]); + $failures[] = $pubsub; } - } catch (\Throwable $th) { - $output[] = new Document([ - 'name' => $key . " ($database)", - 'status' => 'fail', - 'ping' => \round((\microtime(true) - $checkStart) / 1000) - ]); + } catch (\Throwable) { + $failures[] = $pubsub; } } } + if (!empty($failures)) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Pubsub failure on: ' . implode(", ", $failures)); + } + $response->dynamic(new Document([ 'statuses' => $output, 'total' => count($output), @@ -261,11 +254,11 @@ App::get('/v1/health/time') ->groups(['api', 'health']) ->label('scope', 'health.read') ->label('sdk', new Method( - auth: [AuthType::KEY], namespace: 'health', group: 'health', name: 'getTime', description: '/docs/references/health/get-time.md', + auth: [AuthType::KEY], responses: [ new SDKResponse( code: Response::STATUS_CODE_OK, @@ -325,11 +318,11 @@ App::get('/v1/health/queue/webhooks') ->groups(['api', 'health']) ->label('scope', 'health.read') ->label('sdk', new Method( - auth: [AuthType::KEY], namespace: 'health', group: 'queue', name: 'getQueueWebhooks', description: '/docs/references/health/get-queue-webhooks.md', + auth: [AuthType::KEY], responses: [ new SDKResponse( code: Response::STATUS_CODE_OK, @@ -351,18 +344,18 @@ App::get('/v1/health/queue/webhooks') } $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); - }, ['response']); + }); App::get('/v1/health/queue/logs') ->desc('Get logs queue') ->groups(['api', 'health']) ->label('scope', 'health.read') ->label('sdk', new Method( - auth: [AuthType::KEY], namespace: 'health', group: 'queue', name: 'getQueueLogs', description: '/docs/references/health/get-queue-logs.md', + auth: [AuthType::KEY], responses: [ new SDKResponse( code: Response::STATUS_CODE_OK, @@ -384,18 +377,18 @@ App::get('/v1/health/queue/logs') } $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); - }, ['response']); + }); App::get('/v1/health/certificate') ->desc('Get the SSL certificate for a domain') ->groups(['api', 'health']) ->label('scope', 'health.read') ->label('sdk', new Method( - auth: [AuthType::KEY], namespace: 'health', group: 'health', name: 'getCertificate', description: '/docs/references/health/get-certificate.md', + auth: [AuthType::KEY], responses: [ new SDKResponse( code: Response::STATUS_CODE_OK, @@ -441,18 +434,18 @@ App::get('/v1/health/certificate') 'validTo' => $certificatePayload['validTo_time_t'], 'signatureTypeSN' => $certificatePayload['signatureTypeSN'], ]), Response::MODEL_HEALTH_CERTIFICATE); - }, ['response']); + }); App::get('/v1/health/queue/certificates') ->desc('Get certificates queue') ->groups(['api', 'health']) ->label('scope', 'health.read') ->label('sdk', new Method( - auth: [AuthType::KEY], namespace: 'health', group: 'queue', name: 'getQueueCertificates', description: '/docs/references/health/get-queue-certificates.md', + auth: [AuthType::KEY], responses: [ new SDKResponse( code: Response::STATUS_CODE_OK, @@ -474,18 +467,18 @@ App::get('/v1/health/queue/certificates') } $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); - }, ['response']); + }); App::get('/v1/health/queue/builds') ->desc('Get builds queue') ->groups(['api', 'health']) ->label('scope', 'health.read') ->label('sdk', new Method( - auth: [AuthType::KEY], namespace: 'health', group: 'queue', name: 'getQueueBuilds', description: '/docs/references/health/get-queue-builds.md', + auth: [AuthType::KEY], responses: [ new SDKResponse( code: Response::STATUS_CODE_OK, @@ -507,18 +500,18 @@ App::get('/v1/health/queue/builds') } $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); - }, ['response']); + }); App::get('/v1/health/queue/databases') ->desc('Get databases queue') ->groups(['api', 'health']) ->label('scope', 'health.read') ->label('sdk', new Method( - auth: [AuthType::KEY], namespace: 'health', group: 'queue', name: 'getQueueDatabases', description: '/docs/references/health/get-queue-databases.md', + auth: [AuthType::KEY], responses: [ new SDKResponse( code: Response::STATUS_CODE_OK, @@ -541,18 +534,18 @@ App::get('/v1/health/queue/databases') } $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); - }, ['response']); + }); App::get('/v1/health/queue/deletes') ->desc('Get deletes queue') ->groups(['api', 'health']) ->label('scope', 'health.read') ->label('sdk', new Method( - auth: [AuthType::KEY], namespace: 'health', group: 'queue', name: 'getQueueDeletes', description: '/docs/references/health/get-queue-deletes.md', + auth: [AuthType::KEY], responses: [ new SDKResponse( code: Response::STATUS_CODE_OK, @@ -574,18 +567,18 @@ App::get('/v1/health/queue/deletes') } $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); - }, ['response']); + }); App::get('/v1/health/queue/mails') ->desc('Get mails queue') ->groups(['api', 'health']) ->label('scope', 'health.read') ->label('sdk', new Method( - auth: [AuthType::KEY], namespace: 'health', group: 'queue', name: 'getQueueMails', description: '/docs/references/health/get-queue-mails.md', + auth: [AuthType::KEY], responses: [ new SDKResponse( code: Response::STATUS_CODE_OK, @@ -607,18 +600,18 @@ App::get('/v1/health/queue/mails') } $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); - }, ['response']); + }); App::get('/v1/health/queue/messaging') ->desc('Get messaging queue') ->groups(['api', 'health']) ->label('scope', 'health.read') ->label('sdk', new Method( - auth: [AuthType::KEY], namespace: 'health', group: 'queue', name: 'getQueueMessaging', description: '/docs/references/health/get-queue-messaging.md', + auth: [AuthType::KEY], responses: [ new SDKResponse( code: Response::STATUS_CODE_OK, @@ -640,18 +633,18 @@ App::get('/v1/health/queue/messaging') } $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); - }, ['response']); + }); App::get('/v1/health/queue/migrations') ->desc('Get migrations queue') ->groups(['api', 'health']) ->label('scope', 'health.read') ->label('sdk', new Method( - auth: [AuthType::KEY], namespace: 'health', group: 'queue', name: 'getQueueMigrations', description: '/docs/references/health/get-queue-migrations.md', + auth: [AuthType::KEY], responses: [ new SDKResponse( code: Response::STATUS_CODE_OK, @@ -673,18 +666,18 @@ App::get('/v1/health/queue/migrations') } $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); - }, ['response']); + }); App::get('/v1/health/queue/functions') ->desc('Get functions queue') ->groups(['api', 'health']) ->label('scope', 'health.read') ->label('sdk', new Method( - auth: [AuthType::KEY], namespace: 'health', group: 'queue', name: 'getQueueFunctions', description: '/docs/references/health/get-queue-functions.md', + auth: [AuthType::KEY], responses: [ new SDKResponse( code: Response::STATUS_CODE_OK, @@ -706,18 +699,18 @@ App::get('/v1/health/queue/functions') } $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); - }, ['response']); + }); App::get('/v1/health/queue/stats-resources') ->desc('Get stats resources queue') ->groups(['api', 'health']) ->label('scope', 'health.read') ->label('sdk', new Method( - auth: [AuthType::KEY], namespace: 'health', group: 'queue', name: 'getQueueStatsResources', description: '/docs/references/health/get-queue-stats-resources.md', + auth: [AuthType::KEY], responses: [ new SDKResponse( code: Response::STATUS_CODE_OK, @@ -746,11 +739,11 @@ App::get('/v1/health/queue/stats-usage') ->groups(['api', 'health']) ->label('scope', 'health.read') ->label('sdk', new Method( - auth: [AuthType::KEY], namespace: 'health', group: 'queue', name: 'getQueueUsage', description: '/docs/references/health/get-queue-stats-usage.md', + auth: [AuthType::KEY], responses: [ new SDKResponse( code: Response::STATUS_CODE_OK, @@ -779,11 +772,11 @@ App::get('/v1/health/storage/local') ->groups(['api', 'health']) ->label('scope', 'health.read') ->label('sdk', new Method( - auth: [AuthType::KEY], namespace: 'health', group: 'storage', name: 'getStorageLocal', description: '/docs/references/health/get-storage-local.md', + auth: [AuthType::KEY], responses: [ new SDKResponse( code: Response::STATUS_CODE_OK, @@ -829,11 +822,11 @@ App::get('/v1/health/storage') ->groups(['api', 'health']) ->label('scope', 'health.read') ->label('sdk', new Method( - auth: [AuthType::KEY], namespace: 'health', group: 'storage', name: 'getStorage', description: '/docs/references/health/get-storage.md', + auth: [AuthType::KEY], responses: [ new SDKResponse( code: Response::STATUS_CODE_OK, @@ -877,11 +870,11 @@ App::get('/v1/health/anti-virus') ->groups(['api', 'health']) ->label('scope', 'health.read') ->label('sdk', new Method( - auth: [AuthType::KEY], namespace: 'health', group: 'health', name: 'getAntivirus', description: '/docs/references/health/get-storage-anti-virus.md', + auth: [AuthType::KEY], responses: [ new SDKResponse( code: Response::STATUS_CODE_OK, @@ -923,11 +916,11 @@ App::get('/v1/health/queue/failed/:name') ->groups(['api', 'health']) ->label('scope', 'health.read') ->label('sdk', new Method( - auth: [AuthType::KEY], namespace: 'health', group: 'queue', name: 'getFailedJobs', description: '/docs/references/health/get-failed-queue-jobs.md', + auth: [AuthType::KEY], responses: [ new SDKResponse( code: Response::STATUS_CODE_OK, diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index 839a51a764..c4f0b6a9df 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -24,6 +24,7 @@ use Utopia\App; use Utopia\Audit\Audit; use Utopia\Cache\Cache; use Utopia\Config\Config; +use Utopia\Database\Adapter\Pool as DatabasePool; use Utopia\Database\Database; use Utopia\Database\DateTime; use Utopia\Database\Document; @@ -223,7 +224,7 @@ App::post('/v1/projects') $sharedTables = $sharedTablesV1 || $sharedTablesV2; if (!$sharedTablesV2) { - $adapter = $pools->get($dsn->getHost())->pop()->getResource(); + $adapter = new DatabasePool($pools->get($dsn->getHost())); $dbForProject = new Database($adapter, $cache); if ($sharedTables) { diff --git a/app/http.php b/app/http.php index 963f550b8b..4d78837a8a 100644 --- a/app/http.php +++ b/app/http.php @@ -14,9 +14,11 @@ use Utopia\App; use Utopia\Audit\Audit; use Utopia\CLI\Console; use Utopia\Config\Config; +use Utopia\Database\Adapter\Pool as DatabasePool; use Utopia\Database\Database; use Utopia\Database\DateTime; use Utopia\Database\Document; +use Utopia\Database\Exception\Duplicate as DuplicateException; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; @@ -167,7 +169,7 @@ function createDatabase(App $app, string $resourceKey, string $dbName, array $co $sleep = 1; $attempts = 0; - do { + while (true) { try { $attempts++; $resource = $app->getResource($resourceKey); @@ -176,13 +178,12 @@ function createDatabase(App $app, string $resourceKey, string $dbName, array $co break; // exit loop on success } catch (\Exception $e) { Console::warning(" └── Database not ready. Retrying connection ({$attempts})..."); - $pools->reclaim(); if ($attempts >= $max) { throw new \Exception(' └── Failed to connect to database: ' . $e->getMessage()); } sleep($sleep); } - } while ($attempts < $max); + } Console::success("[Setup] - $dbName database init started..."); @@ -318,11 +319,7 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg $cache = $app->getResource('cache'); foreach ($sharedTablesV2 as $hostname) { - $adapter = $pools - ->get($hostname) - ->pop() - ->getResource(); - + $adapter = new DatabasePool($pools->get($hostname)); $dbForProject = (new Database($adapter, $cache)) ->setDatabase('appwrite') ->setSharedTables(true) @@ -332,7 +329,7 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg try { Console::success('[Setup] - Creating project database: ' . $hostname . '...'); $dbForProject->create(); - } catch (Duplicate) { + } catch (DuplicateException) { Console::success('[Setup] - Skip: metadata table already exists'); } @@ -358,7 +355,6 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg } } - $pools->reclaim(); Console::success('[Setup] - Server database init completed...'); }); @@ -473,6 +469,7 @@ $http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, Swool Console::error('[Error] Message: ' . $th->getMessage()); Console::error('[Error] File: ' . $th->getFile()); Console::error('[Error] Line: ' . $th->getLine()); + Console::error('[Error] Trace: ' . $th->getTraceAsString()); $swooleResponse->setStatusCode(500); @@ -490,8 +487,6 @@ $http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, Swool ]; $swooleResponse->end(\json_encode($output)); - } finally { - $pools->reclaim(); } }); diff --git a/app/init/registers.php b/app/init/registers.php index 1adaaf35ce..415730f936 100644 --- a/app/init/registers.php +++ b/app/init/registers.php @@ -216,13 +216,13 @@ $register->set('pools', function () { 'mysql', 'mariadb' => function () use ($dsnHost, $dsnPort, $dsnUser, $dsnPass, $dsnDatabase) { return new PDOProxy(function () use ($dsnHost, $dsnPort, $dsnUser, $dsnPass, $dsnDatabase) { - return new PDO("mysql:host={$dsnHost};port={$dsnPort};dbname={$dsnDatabase};charset=utf8mb4", $dsnUser, $dsnPass, array( + return new PDO("mysql:host={$dsnHost};port={$dsnPort};dbname={$dsnDatabase};charset=utf8mb4", $dsnUser, $dsnPass, [ \PDO::ATTR_TIMEOUT => 3, // Seconds \PDO::ATTR_PERSISTENT => false, \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC, \PDO::ATTR_EMULATE_PREPARES => true, \PDO::ATTR_STRINGIFY_FETCHES => true - )); + ]); }); }, 'redis' => function () use ($dsnHost, $dsnPort, $dsnPass) { diff --git a/app/init/resources.php b/app/init/resources.php index c719a47344..f48efbe177 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -24,10 +24,12 @@ use Appwrite\Utopia\Request; use Executor\Executor; use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis; use Utopia\App; +use Utopia\Cache\Adapter\Pool as CachePool; use Utopia\Cache\Adapter\Sharding; use Utopia\Cache\Cache; use Utopia\CLI\Console; use Utopia\Config\Config; +use Utopia\Database\Adapter\Pool as DatabasePool; use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Helpers\ID; @@ -37,6 +39,7 @@ use Utopia\DSN\DSN; use Utopia\Locale\Locale; use Utopia\Logger\Log; use Utopia\Pools\Group; +use Utopia\Queue\Broker\Pool as BrokerPool; use Utopia\Queue\Publisher; use Utopia\Storage\Device; use Utopia\Storage\Device\AWS; @@ -72,10 +75,10 @@ App::setResource('localeCodes', function () { // Queues App::setResource('publisher', function (Group $pools) { - return $pools->get('publisher')->pop()->getResource(); + return new BrokerPool(publisher: $pools->get('publisher')); }, ['pools']); App::setResource('consumer', function (Group $pools) { - return $pools->get('consumer')->pop()->getResource(); + return new BrokerPool(consumer: $pools->get('consumer')); }, ['pools']); App::setResource('queueForMessaging', function (Publisher $publisher) { return new Messaging($publisher); @@ -329,12 +332,8 @@ App::setResource('dbForProject', function (Group $pools, Database $dbForPlatform $dsn = new DSN('mysql://' . $project->getAttribute('database')); } - $dbAdapter = $pools - ->get($dsn->getHost()) - ->pop() - ->getResource(); - - $database = new Database($dbAdapter, $cache); + $adapter = new DatabasePool($pools->get($dsn->getHost())); + $database = new Database($adapter, $cache); $database ->setMetadata('host', \gethostname()) @@ -360,12 +359,8 @@ App::setResource('dbForProject', function (Group $pools, Database $dbForPlatform }, ['pools', 'dbForPlatform', 'cache', 'project']); App::setResource('dbForPlatform', function (Group $pools, Cache $cache) { - $dbAdapter = $pools - ->get('console') - ->pop() - ->getResource(); - - $database = new Database($dbAdapter, $cache); + $adapter = new DatabasePool($pools->get('console')); + $database = new Database($adapter, $cache); $database ->setNamespace('_console') @@ -378,7 +373,7 @@ App::setResource('dbForPlatform', function (Group $pools, Cache $cache) { }, ['pools', 'cache']); App::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache) { - $databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools + $databases = []; return function (Document $project) use ($pools, $dbForPlatform, $cache, &$databases) { if ($project->isEmpty() || $project->getId() === 'console') { @@ -420,12 +415,8 @@ App::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform return $database; } - $dbAdapter = $pools - ->get($dsn->getHost()) - ->pop() - ->getResource(); - - $database = new Database($dbAdapter, $cache); + $adapter = new DatabasePool($pools->get($dsn->getHost())); + $database = new Database($adapter, $cache); $databases[$dsn->getHost()] = $database; $configure($database); @@ -435,21 +426,15 @@ App::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform App::setResource('getLogsDB', function (Group $pools, Cache $cache) { $database = null; - return function (?Document $project = null) use ($pools, $cache, $database) { + + return function (?Document $project = null) use ($pools, $cache, &$database) { if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') { $database->setTenant($project->getInternalId()); return $database; } - $dbAdapter = $pools - ->get('logs') - ->pop() - ->getResource(); - - $database = new Database( - $dbAdapter, - $cache - ); + $adapter = new DatabasePool($pools->get('logs')); + $database = new Database($adapter, $cache); $database ->setSharedTables(true) @@ -473,10 +458,7 @@ App::setResource('cache', function (Group $pools, Telemetry $telemetry) { $adapters = []; foreach ($list as $value) { - $adapters[] = $pools - ->get($value) - ->pop() - ->getResource(); + $adapters[] = new CachePool($pools->get($value)); } $cache = new Cache(new Sharding($adapters)); diff --git a/app/realtime.php b/app/realtime.php index 86f9c85fdd..7e6fc0e311 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -5,6 +5,7 @@ use Appwrite\Extend\Exception; use Appwrite\Extend\Exception as AppwriteException; use Appwrite\Messaging\Adapter\Realtime; use Appwrite\Network\Validator\Origin; +use Appwrite\PubSub\Adapter\Pool as PubSubPool; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; use Swoole\Http\Request as SwooleRequest; @@ -15,10 +16,12 @@ use Swoole\Timer; use Utopia\Abuse\Abuse; use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis; use Utopia\App; +use Utopia\Cache\Adapter\Pool as CachePool; use Utopia\Cache\Adapter\Sharding; use Utopia\Cache\Cache; use Utopia\CLI\Console; use Utopia\Config\Config; +use Utopia\Database\Adapter\Pool as DatabasePool; use Utopia\Database\Database; use Utopia\Database\DateTime; use Utopia\Database\Document; @@ -28,13 +31,15 @@ use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; use Utopia\DSN\DSN; use Utopia\Logger\Log; +use Utopia\Pools\Group; +use Utopia\Registry\Registry; use Utopia\System\System; use Utopia\Telemetry\Adapter\None as NoTelemetry; use Utopia\WebSocket\Adapter; use Utopia\WebSocket\Server; /** - * @var \Utopia\Registry\Registry $register + * @var Registry $register */ require_once __DIR__ . '/init.php'; @@ -46,17 +51,17 @@ if (!function_exists('getConsoleDB')) { { global $register; - /** @var \Utopia\Pools\Group $pools */ + static $database = null; + + if ($database !== null) { + return $database; + } + + /** @var Group $pools */ $pools = $register->get('pools'); - $dbAdapter = $pools - ->get('console') - ->pop() - ->getResource() - ; - - $database = new Database($dbAdapter, getCache()); - + $adapter = new DatabasePool($pools->get('console')); + $database = new Database($adapter, getCache()); $database ->setNamespace('_console') ->setMetadata('host', \gethostname()) @@ -72,7 +77,13 @@ if (!function_exists('getProjectDB')) { { global $register; - /** @var \Utopia\Pools\Group $pools */ + static $databases = []; + + if (isset($databases[$project->getInternalId()])) { + return $databases[$project->getInternalId()]; + } + + /** @var Group $pools */ $pools = $register->get('pools'); if ($project->isEmpty() || $project->getId() === 'console') { @@ -86,11 +97,7 @@ if (!function_exists('getProjectDB')) { $dsn = new DSN('mysql://' . $project->getAttribute('database')); } - $adapter = $pools - ->get($dsn->getHost()) - ->pop() - ->getResource(); - + $adapter = new DatabasePool($pools->get($dsn->getHost())); $database = new Database($adapter, getCache()); $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); @@ -111,7 +118,7 @@ if (!function_exists('getProjectDB')) { ->setMetadata('host', \gethostname()) ->setMetadata('project', $project->getId()); - return $database; + return $databases[$project->getInternalId()] = $database; } } @@ -121,20 +128,22 @@ if (!function_exists('getCache')) { { global $register; - $pools = $register->get('pools'); /** @var \Utopia\Pools\Group $pools */ + static $cache = null; + + if ($cache !== null) { + return $cache; + } + + $pools = $register->get('pools'); /** @var Group $pools */ $list = Config::getParam('pools-cache', []); $adapters = []; foreach ($list as $value) { - $adapters[] = $pools - ->get($value) - ->pop() - ->getResource() - ; + $adapters[] = new CachePool($pools->get($value)); } - return new Cache(new Sharding($adapters)); + return $cache = new Cache(new Sharding($adapters)); } } @@ -142,6 +151,12 @@ if (!function_exists('getCache')) { if (!function_exists('getRedis')) { function getRedis(): \Redis { + static $redis = null; + + if ($redis !== null) { + return $redis; + } + $host = System::getEnv('_APP_REDIS_HOST', 'localhost'); $port = System::getEnv('_APP_REDIS_PORT', 6379); $pass = System::getEnv('_APP_REDIS_PASS', ''); @@ -160,21 +175,39 @@ if (!function_exists('getRedis')) { if (!function_exists('getTimelimit')) { function getTimelimit(): TimeLimitRedis { - return new TimeLimitRedis("", 0, 1, getRedis()); + static $timelimit = null; + + if ($timelimit !== null) { + return $timelimit; + } + + return $timelimit = new TimeLimitRedis("", 0, 1, getRedis()); } } if (!function_exists('getRealtime')) { function getRealtime(): Realtime { - return new Realtime(); + static $realtime = null; + + if ($realtime !== null) { + return $realtime; + } + + return $realtime = new Realtime(); } } if (!function_exists('getTelemetry')) { function getTelemetry(int $workerId): Utopia\Telemetry\Adapter { - return new NoTelemetry(); + static $telemetry = null; + + if ($telemetry !== null) { + return $telemetry; + } + + return $telemetry = new NoTelemetry(); } } @@ -273,7 +306,6 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume sleep(DATABASE_RECONNECT_SLEEP); } } while (true); - $register->get('pools')->reclaim(); }); /** @@ -299,9 +331,7 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume Authorization::skip(fn () => $database->updateDocument('realtime', $statsDocument->getId(), $statsDocument)); } catch (Throwable $th) { - call_user_func($logError, $th, "updateWorkerDocument"); - } finally { - $register->get('pools')->reclaim(); + $logError($th, "updateWorkerDocument"); } }); } @@ -370,8 +400,6 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, 'data' => $event['data'] ])); } - - $register->get('pools')->reclaim(); } } /** @@ -407,8 +435,8 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, } $start = time(); - /** @var \Appwrite\PubSub\Adapter $pubsub */ - $pubsub = $register->get('pools')->get('pubsub')->pop()->getResource(); + $pubsub = new PubSubPool($register->get('pools')->get('pubsub')); + if ($pubsub->ping(true)) { $attempts = 0; Console::success('Pub/sub connection established (worker: ' . $workerId . ')'); @@ -436,8 +464,6 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, $realtime->unsubscribe($connection); $realtime->subscribe($projectId, $connection, $roles, $channels); - - $register->get('pools')->reclaim(); } } @@ -463,14 +489,12 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats, } }); } catch (Throwable $th) { - call_user_func($logError, $th, "pubSubConnection"); + $logError($th, "pubSubConnection"); Console::error('Pub/sub error: ' . $th->getMessage()); $attempts++; sleep(DATABASE_RECONNECT_SLEEP); continue; - } finally { - $register->get('pools')->reclaim(); } } @@ -572,7 +596,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $stats->incr($project->getId(), 'connections'); $stats->incr($project->getId(), 'connectionsTotal'); } catch (Throwable $th) { - call_user_func($logError, $th, "initServer"); + $logError($th, "initServer"); // Handle SQL error code is 'HY000' $code = $th->getCode(); @@ -596,8 +620,6 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server, Console::error('[Error] Code: ' . $response['data']['code']); Console::error('[Error] Message: ' . $response['data']['message']); } - } finally { - $register->get('pools')->reclaim(); } }); @@ -696,8 +718,6 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re if ($th->getCode() === 1008) { $server->close($connection, $th->getCode()); } - } finally { - $register->get('pools')->reclaim(); } }); diff --git a/app/worker.php b/app/worker.php index 232e0b3684..1ae2108a62 100644 --- a/app/worker.php +++ b/app/worker.php @@ -20,10 +20,12 @@ use Appwrite\Platform\Appwrite; use Executor\Executor; use Swoole\Runtime; use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis; +use Utopia\Cache\Adapter\Pool as CachePool; use Utopia\Cache\Adapter\Sharding; use Utopia\Cache\Cache; use Utopia\CLI\Console; use Utopia\Config\Config; +use Utopia\Database\Adapter\Pool as DatabasePool; use Utopia\Database\Database; use Utopia\Database\DateTime; use Utopia\Database\Document; @@ -33,6 +35,7 @@ use Utopia\Logger\Log; use Utopia\Logger\Logger; use Utopia\Platform\Service; use Utopia\Pools\Group; +use Utopia\Queue\Broker\Pool as BrokerPool; use Utopia\Queue\Message; use Utopia\Queue\Publisher; use Utopia\Queue\Server; @@ -40,21 +43,17 @@ use Utopia\Registry\Registry; use Utopia\System\System; Authorization::disable(); -Runtime::enableCoroutine(SWOOLE_HOOK_ALL); +Runtime::enableCoroutine(); Server::setResource('register', fn () => $register); Server::setResource('dbForPlatform', function (Cache $cache, Registry $register) { $pools = $register->get('pools'); - $database = $pools - ->get('console') - ->pop() - ->getResource(); + $adapter = new DatabasePool($pools->get('console')); + $dbForPlatform = new Database($adapter, $cache); + $dbForPlatform->setNamespace('_console'); - $adapter = new Database($database, $cache); - $adapter->setNamespace('_console'); - - return $adapter; + return $dbForPlatform; }, ['cache', 'register']); Server::setResource('project', function (Message $message, Database $dbForPlatform) { @@ -82,20 +81,9 @@ Server::setResource('dbForProject', function (Cache $cache, Registry $register, $dsn = new DSN('mysql://' . $project->getAttribute('database')); } - $adapter = $pools - ->get($dsn->getHost()) - ->pop() - ->getResource(); - + $adapter = new DatabasePool($pools->get($dsn->getHost())); $database = new Database($adapter, $cache); - try { - $dsn = new DSN($project->getAttribute('database')); - } catch (\InvalidArgumentException) { - // TODO: Temporary until all projects are using shared tables - $dsn = new DSN('mysql://' . $project->getAttribute('database')); - } - $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); if (\in_array($dsn->getHost(), $sharedTables)) { @@ -150,12 +138,8 @@ Server::setResource('getProjectDB', function (Group $pools, Database $dbForPlatf return $database; } - $dbAdapter = $pools - ->get($dsn->getHost()) - ->pop() - ->getResource(); - - $database = new Database($dbAdapter, $cache); + $adapter = new DatabasePool($pools->get($dsn->getHost())); + $database = new Database($adapter, $cache); $databases[$dsn->getHost()] = $database; @@ -187,15 +171,8 @@ Server::setResource('getLogsDB', function (Group $pools, Cache $cache) { return $database; } - $dbAdapter = $pools - ->get('logs') - ->pop() - ->getResource(); - - $database = new Database( - $dbAdapter, - $cache - ); + $adapter = new DatabasePool($pools->get('logs')); + $database = new Database($adapter, $cache); $database ->setSharedTables(true) @@ -233,11 +210,7 @@ Server::setResource('cache', function (Registry $register) { $adapters = []; foreach ($list as $value) { - $adapters[] = $pools - ->get($value) - ->pop() - ->getResource() - ; + $adapters[] = new CachePool($pools->get($value)); } return new Cache(new Sharding($adapters)); @@ -267,11 +240,11 @@ Server::setResource('timelimit', function (\Redis $redis) { Server::setResource('log', fn () => new Log()); Server::setResource('publisher', function (Group $pools) { - return $pools->get('publisher')->pop()->getResource(); + return new BrokerPool(publisher: $pools->get('publisher')); }, ['pools']); Server::setResource('consumer', function (Group $pools) { - return $pools->get('consumer')->pop()->getResource(); + return new BrokerPool(consumer: $pools->get('consumer')); }, ['pools']); Server::setResource('queueForStatsUsage', function (Publisher $publisher) { @@ -448,13 +421,6 @@ try { $worker = $platform->getWorker(); -$worker - ->shutdown() - ->inject('pools') - ->action(function (Group $pools) { - $pools->reclaim(); - }); - $worker ->error() ->inject('error') @@ -462,8 +428,7 @@ $worker ->inject('log') ->inject('pools') ->inject('project') - ->action(function (Throwable $error, ?Logger $logger, Log $log, Group $pools, Document $project) use ($queueName) { - $pools->reclaim(); + ->action(function (Throwable $error, ?Logger $logger, Log $log, Group $pools, Document $project) use ($worker, $queueName) { $version = System::getEnv('_APP_VERSION', 'UNKNOWN'); if ($logger) { diff --git a/composer.json b/composer.json index 9b2cf7a1ab..914e664d7f 100644 --- a/composer.json +++ b/composer.json @@ -65,7 +65,7 @@ "utopia-php/platform": "0.7.*", "utopia-php/pools": "0.8.*", "utopia-php/preloader": "0.2.*", - "utopia-php/queue": "0.9.*", + "utopia-php/queue": "0.10.*", "utopia-php/registry": "0.5.*", "utopia-php/storage": "0.18.*", "utopia-php/swoole": "0.8.*", diff --git a/composer.lock b/composer.lock index e6bcd919e2..d04d67366e 100644 --- a/composer.lock +++ b/composer.lock @@ -4059,16 +4059,16 @@ }, { "name": "utopia-php/platform", - "version": "0.7.4", + "version": "0.7.5", "source": { "type": "git", "url": "https://github.com/utopia-php/platform.git", - "reference": "a5b93d8177702ec458c3af9137663133c012b71b" + "reference": "8febd7b6e0c0f2cbd2f4289447bcca97496e4aaf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/platform/zipball/a5b93d8177702ec458c3af9137663133c012b71b", - "reference": "a5b93d8177702ec458c3af9137663133c012b71b", + "url": "https://api.github.com/repos/utopia-php/platform/zipball/8febd7b6e0c0f2cbd2f4289447bcca97496e4aaf", + "reference": "8febd7b6e0c0f2cbd2f4289447bcca97496e4aaf", "shasum": "" }, "require": { @@ -4077,11 +4077,11 @@ "php": ">=8.0", "utopia-php/cli": "0.15.*", "utopia-php/framework": "0.33.*", - "utopia-php/queue": "0.9.*" + "utopia-php/queue": "0.10.*" }, "require-dev": { - "laravel/pint": "1.2.*", - "phpunit/phpunit": "^9.3" + "laravel/pint": "1.*", + "phpunit/phpunit": "9.*" }, "type": "library", "autoload": { @@ -4103,9 +4103,9 @@ ], "support": { "issues": "https://github.com/utopia-php/platform/issues", - "source": "https://github.com/utopia-php/platform/tree/0.7.4" + "source": "https://github.com/utopia-php/platform/tree/0.7.5" }, - "time": "2025-03-13T13:00:12+00:00" + "time": "2025-04-17T12:20:16+00:00" }, { "name": "utopia-php/pools", @@ -4214,16 +4214,16 @@ }, { "name": "utopia-php/queue", - "version": "0.9.1", + "version": "0.10.0", "source": { "type": "git", "url": "https://github.com/utopia-php/queue.git", - "reference": "32b6f84c55aae761db5a5ae76cc91ca8dbc8bc32" + "reference": "0eccc559168ea72241c39a4c482d868314666be1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/queue/zipball/32b6f84c55aae761db5a5ae76cc91ca8dbc8bc32", - "reference": "32b6f84c55aae761db5a5ae76cc91ca8dbc8bc32", + "url": "https://api.github.com/repos/utopia-php/queue/zipball/0eccc559168ea72241c39a4c482d868314666be1", + "reference": "0eccc559168ea72241c39a4c482d868314666be1", "shasum": "" }, "require": { @@ -4232,6 +4232,7 @@ "utopia-php/cli": "0.15.*", "utopia-php/fetch": "0.4.*", "utopia-php/framework": "0.33.*", + "utopia-php/pools": "0.8.*", "utopia-php/telemetry": "0.1.*" }, "require-dev": { @@ -4273,9 +4274,9 @@ ], "support": { "issues": "https://github.com/utopia-php/queue/issues", - "source": "https://github.com/utopia-php/queue/tree/0.9.1" + "source": "https://github.com/utopia-php/queue/tree/0.10.0" }, - "time": "2025-03-28T19:49:36+00:00" + "time": "2025-04-17T12:15:52+00:00" }, { "name": "utopia-php/registry", diff --git a/src/Appwrite/Event/Event.php b/src/Appwrite/Event/Event.php index d699a45417..2c735ef2d4 100644 --- a/src/Appwrite/Event/Event.php +++ b/src/Appwrite/Event/Event.php @@ -286,13 +286,6 @@ class Event return $this; } - public function setParamSensitive(string $key): self - { - $this->sensitive[$key] = true; - - return $this; - } - /** * Get param of event. * diff --git a/src/Appwrite/Messaging/Adapter/Realtime.php b/src/Appwrite/Messaging/Adapter/Realtime.php index 96793b2683..be263aa655 100644 --- a/src/Appwrite/Messaging/Adapter/Realtime.php +++ b/src/Appwrite/Messaging/Adapter/Realtime.php @@ -2,14 +2,14 @@ namespace Appwrite\Messaging\Adapter; -use Appwrite\Messaging\Adapter; +use Appwrite\Messaging\Adapter as MessagingAdapter; +use Appwrite\PubSub\Adapter\Pool as PubSubPool; use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Role; -use Utopia\Pools\Pool; -class Realtime extends Adapter +class Realtime extends MessagingAdapter { /** * Connection Tree @@ -132,11 +132,12 @@ class Realtime extends Adapter * Sends an event to the Realtime Server * @param string $projectId * @param array $payload - * @param string $event + * @param array $events * @param array $channels * @param array $roles * @param array $options * @return void + * @throws \Exception */ public function send(string $projectId, array $payload, array $events, array $channels, array $roles, array $options = []): void { @@ -158,9 +159,7 @@ class Realtime extends Adapter 'timestamp' => DateTime::formatTz(DateTime::now()), 'payload' => $payload ] - ]; - - $this->pubsubPool->use(fn (\Appwrite\PubSub\Adapter $pubsub) => $pubsub->publish('realtime', json_encode($message))); + ])); } /** @@ -175,8 +174,9 @@ class Realtime extends Adapter * - 1,121.328 ms (±0.84%) | 1,000,000 Connections / 10,000,000 Subscriptions * * @param array $event + * @return int[]|string[] */ - public function getSubscribers(array $event) + public function getSubscribers(array $event): array { $receivers = []; @@ -230,7 +230,7 @@ class Realtime extends Adapter foreach ($channels as $key => $value) { switch (true) { - case strpos($key, 'account.') === 0: + case \str_starts_with($key, 'account.'): unset($channels[$key]); break; @@ -272,6 +272,7 @@ class Realtime extends Adapter $channels[] = 'account.' . $parts[1]; $roles = [Role::user(ID::custom($parts[1]))->toString()]; break; + case 'migrations': case 'rules': $channels[] = 'console'; $channels[] = 'projects.' . $project->getId(); @@ -352,12 +353,6 @@ class Realtime extends Adapter $roles = [Role::team($project->getAttribute('teamId'))->toString()]; } - break; - case 'migrations': - $channels[] = 'console'; - $channels[] = 'projects.' . $project->getId(); - $projectId = 'console'; - $roles = [Role::team($project->getAttribute('teamId'))->toString()]; break; } diff --git a/src/Appwrite/Platform/Tasks/Doctor.php b/src/Appwrite/Platform/Tasks/Doctor.php index c43afea527..5263133eba 100644 --- a/src/Appwrite/Platform/Tasks/Doctor.php +++ b/src/Appwrite/Platform/Tasks/Doctor.php @@ -3,14 +3,19 @@ namespace Appwrite\Platform\Tasks; use Appwrite\ClamAV\Network; -use Appwrite\PubSub\Adapter; +use Appwrite\PubSub\Adapter\Pool as PubSubPool; +use PHPMailer\PHPMailer\PHPMailer; use Utopia\App; +use Utopia\Cache\Adapter\Pool as CachePool; use Utopia\CLI\Console; use Utopia\Config\Config; +use Utopia\Database\Adapter\Pool as DatabasePool; use Utopia\Domains\Domain; use Utopia\DSN\DSN; use Utopia\Logger\Logger; use Utopia\Platform\Action; +use Utopia\Pools\Group; +use Utopia\Queue\Broker\Pool as BrokerPool; use Utopia\Registry\Registry; use Utopia\Storage\Device\Local; use Utopia\Storage\Storage; @@ -76,9 +81,9 @@ class Doctor extends Action Console::log('🟢 Abuse protection is enabled'); } - $authWhitelistRoot = System::getEnv('_APP_CONSOLE_WHITELIST_ROOT', null); - $authWhitelistEmails = System::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null); - $authWhitelistIPs = System::getEnv('_APP_CONSOLE_WHITELIST_IPS', null); + $authWhitelistRoot = System::getEnv('_APP_CONSOLE_WHITELIST_ROOT'); + $authWhitelistEmails = System::getEnv('_APP_CONSOLE_WHITELIST_EMAILS'); + $authWhitelistIPs = System::getEnv('_APP_CONSOLE_WHITELIST_IPS'); if ( empty($authWhitelistRoot) @@ -114,19 +119,16 @@ class Doctor extends Action } else { Console::log('🟢 Logging adapter is enabled (' . $providerName . ')'); } - } catch (\Throwable $th) { + } catch (\Throwable) { Console::log('🔴 Logging adapter is misconfigured'); } \usleep(200 * 1000); // Sleep for 0.2 seconds - try { - Console::log("\n" . '[Connectivity]'); - } catch (\Throwable $th) { - //throw $th; - } + Console::log("\n" . '[Connectivity]'); - $pools = $register->get('pools'); /** @var \Utopia\Pools\Group $pools */ + /** @var Group $pools */ + $pools = $register->get('pools'); $configs = [ 'Console.DB' => Config::getParam('pools-console'), @@ -136,20 +138,22 @@ class Doctor extends Action foreach ($configs as $key => $config) { foreach ($config as $database) { try { - $adapter = $pools->get($database)->pop()->getResource(); + $adapter = new DatabasePool($pools->get($database)); if ($adapter->ping()) { Console::success('🟢 ' . str_pad("{$key}({$database})", 50, '.') . 'connected'); } else { Console::error('🔴 ' . str_pad("{$key}({$database})", 47, '.') . 'disconnected'); } - } catch (\Throwable $th) { + } catch (\Throwable) { Console::error('🔴 ' . str_pad("{$key}.({$database})", 47, '.') . 'disconnected'); } } } - $pools = $register->get('pools'); /** @var \Utopia\Pools\Group $pools */ + /** @var Group $pools */ + $pools = $register->get('pools'); + $configs = [ 'Cache' => Config::getParam('pools-cache'), 'Queue' => Config::getParam('pools-queue'), @@ -159,15 +163,18 @@ class Doctor extends Action foreach ($configs as $key => $config) { foreach ($config as $pool) { try { - /** @var Adapter $adapter */ - $adapter = $pools->get($pool)->pop()->getResource(); + $adapter = match($key) { + 'Cache' => new CachePool($pools->get($pool)), + 'Queue' => new BrokerPool($pools->get($pool)), + 'PubSub' => new PubSubPool($pools->get($pool)), + }; if ($adapter->ping()) { Console::success('🟢 ' . str_pad("{$key}({$pool})", 50, '.') . 'connected'); } else { Console::error('🔴 ' . str_pad("{$key}({$pool})", 47, '.') . 'disconnected'); } - } catch (\Throwable $th) { + } catch (\Throwable) { Console::error('🔴 ' . str_pad("{$key}({$pool})", 47, '.') . 'disconnected'); } } @@ -185,13 +192,14 @@ class Doctor extends Action } else { Console::error('🔴 ' . str_pad("Antivirus", 47, '.') . 'disconnected'); } - } catch (\Throwable $th) { + } catch (\Throwable) { Console::error('🔴 ' . str_pad("Antivirus", 47, '.') . 'disconnected'); } } try { - $mail = $register->get('smtp'); /* @var $mail \PHPMailer\PHPMailer\PHPMailer */ + /* @var PHPMailer $mail */ + $mail = $register->get('smtp'); $mail->addAddress('demo@example.com', 'Example.com'); $mail->Subject = 'Test SMTP Connection'; @@ -200,7 +208,7 @@ class Doctor extends Action $mail->send(); Console::success('🟢 ' . str_pad("SMTP", 50, '.') . 'connected'); - } catch (\Throwable $th) { + } catch (\Throwable) { Console::error('🔴 ' . str_pad("SMTP", 47, '.') . 'disconnected'); } @@ -274,7 +282,7 @@ class Doctor extends Action Console::error('Failed to check for a newer version' . "\n"); } } - } catch (\Throwable $th) { + } catch (\Throwable) { Console::error('Failed to check for a newer version' . "\n"); } } diff --git a/src/Appwrite/Platform/Tasks/ScheduleBase.php b/src/Appwrite/Platform/Tasks/ScheduleBase.php index afd7d9d22a..3e9907f877 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleBase.php +++ b/src/Appwrite/Platform/Tasks/ScheduleBase.php @@ -13,6 +13,7 @@ use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; use Utopia\Platform\Action; use Utopia\Pools\Group; +use Utopia\Queue\Broker\Pool as BrokerPool; use Utopia\System\System; use Utopia\Telemetry\Adapter as Telemetry; use Utopia\Telemetry\Gauge; @@ -25,6 +26,8 @@ abstract class ScheduleBase extends Action protected array $schedules = []; + protected BrokerPool $publisher; + private ?Histogram $collectSchedulesTelemetryDuration = null; private ?Gauge $collectSchedulesTelemetryCount = null; private ?Gauge $scheduleTelemetryCount = null; @@ -71,6 +74,7 @@ abstract class ScheduleBase extends Action Console::title(\ucfirst(static::getSupportedResource()) . ' scheduler V1'); Console::success(APP_NAME . ' ' . \ucfirst(static::getSupportedResource()) . ' scheduler v1 has started'); + $this->publisher = new BrokerPool($pools->get('publisher')); $this->scheduleTelemetryCount = $telemetry->createGauge('task.schedule.count'); $this->collectSchedulesTelemetryDuration = $telemetry->createHistogram('task.schedule.collect_schedules.duration', 's'); $this->collectSchedulesTelemetryCount = $telemetry->createGauge('task.schedule.collect_schedules.count'); @@ -122,8 +126,6 @@ abstract class ScheduleBase extends Action $schedule->getAttribute('resourceId') ); - $pools->reclaim(); - return [ '$internalId' => $schedule->getInternalId(), '$id' => $schedule->getId(), diff --git a/src/Appwrite/Platform/Tasks/ScheduleExecutions.php b/src/Appwrite/Platform/Tasks/ScheduleExecutions.php index 89d1609a33..331928d265 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleExecutions.php +++ b/src/Appwrite/Platform/Tasks/ScheduleExecutions.php @@ -3,7 +3,6 @@ namespace Appwrite\Platform\Tasks; use Appwrite\Event\Func; -use Swoole\Coroutine as Co; use Utopia\Database\Database; use Utopia\Pools\Group; @@ -29,9 +28,6 @@ class ScheduleExecutions extends ScheduleBase protected function enqueueResources(Group $pools, Database $dbForPlatform, callable $getProjectDB): void { - $queue = $pools->get('publisher')->pop(); - $connection = $queue->getResource(); - $queueForFunctions = new Func($connection); $intervalEnd = (new \DateTime())->modify('+' . self::ENQUEUE_TIMER . ' seconds'); foreach ($this->schedules as $schedule) { @@ -59,8 +55,10 @@ class ScheduleExecutions extends ScheduleBase $this->updateProjectAccess($schedule['project'], $dbForPlatform); - \go(function () use ($queueForFunctions, $schedule, $scheduledAt, $delay, $data) { - Co::sleep($delay); + \go(function () use ($schedule, $delay, $data, $pools) { + \Co::sleep($delay); + + $queueForFunctions = new Func($this->publisher); $queueForFunctions->setType('schedule') // Set functionId instead of function as we don't have $dbForProject @@ -85,7 +83,5 @@ class ScheduleExecutions extends ScheduleBase unset($this->schedules[$schedule['$internalId']]); } - - $queue->reclaim(); } } diff --git a/src/Appwrite/Platform/Tasks/ScheduleFunctions.php b/src/Appwrite/Platform/Tasks/ScheduleFunctions.php index 6788748f3d..649fdb333a 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleFunctions.php +++ b/src/Appwrite/Platform/Tasks/ScheduleFunctions.php @@ -75,12 +75,9 @@ class ScheduleFunctions extends ScheduleBase $delayedExecutions[$delay][] = ['key' => $key, 'nextDate' => $nextDate]; } - foreach ($delayedExecutions as $delay => $schedules) { - \go(function () use ($delay, $schedules, $pools, $dbForPlatform) { - \sleep($delay); // in seconds - - $queue = $pools->get('publisher')->pop(); - $connection = $queue->getResource(); + foreach ($delayedExecutions as $delay => $scheduleKeys) { + \go(function () use ($delay, $scheduleKeys, $pools, $dbForPlatform) { + \Co::sleep($delay); // in seconds foreach ($schedules as $delayConfig) { $scheduleKey = $delayConfig['key']; @@ -93,7 +90,7 @@ class ScheduleFunctions extends ScheduleBase $this->updateProjectAccess($schedule['project'], $dbForPlatform); - $queueForFunctions = new Func($connection); + $queueForFunctions = new Func($this->publisher); $queueForFunctions ->setType('schedule') @@ -105,8 +102,6 @@ class ScheduleFunctions extends ScheduleBase $this->recordEnqueueDelay($delayConfig['nextDate']); } - - $queue->reclaim(); }); } diff --git a/src/Appwrite/Platform/Tasks/ScheduleMessages.php b/src/Appwrite/Platform/Tasks/ScheduleMessages.php index a15df6ed5b..4f7dfab6a0 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleMessages.php +++ b/src/Appwrite/Platform/Tasks/ScheduleMessages.php @@ -40,10 +40,8 @@ class ScheduleMessages extends ScheduleBase continue; } - \go(function () use ($schedule, $scheduledAt, $pools, $dbForPlatform) { - $queue = $pools->get('publisher')->pop(); - $connection = $queue->getResource(); - $queueForMessaging = new Messaging($connection); + \go(function () use ($schedule, $pools, $dbForPlatform) { + $queueForMessaging = new Messaging($this->publisher); $this->updateProjectAccess($schedule['project'], $dbForPlatform); @@ -58,7 +56,6 @@ class ScheduleMessages extends ScheduleBase $schedule['$id'], ); - $queue->reclaim(); $this->recordEnqueueDelay($scheduledAt); unset($this->schedules[$schedule['$internalId']]); }); diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index a61db63de6..427772a6e0 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -61,10 +61,7 @@ class Deletes extends Action ->inject('executionRetention') ->inject('auditRetention') ->inject('log') - ->callback( - fn ($message, Document $project, Database $dbForPlatform, callable $getProjectDB, callable $getLogsDB, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, CertificatesAdapter $certificates, Executor $executor, string $executionRetention, string $auditRetention, Log $log) => - $this->action($message, $project, $dbForPlatform, $getProjectDB, $getLogsDB, $deviceForFiles, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $certificates, $executor, $executionRetention, $auditRetention, $log) - ); + ->callback([$this, 'action']); } /** diff --git a/src/Appwrite/Platform/Workers/StatsUsage.php b/src/Appwrite/Platform/Workers/StatsUsage.php index 66f285fcf5..60fab5d2ea 100644 --- a/src/Appwrite/Platform/Workers/StatsUsage.php +++ b/src/Appwrite/Platform/Workers/StatsUsage.php @@ -325,7 +325,7 @@ class StatsUsage extends Action break; } } catch (Throwable $e) { - console::error("[reducer] " . " {DateTime::now()} " . " {$project->getInternalId()} " . " {$e->getMessage()}"); + Console::error("[reducer] " . " {DateTime::now()} " . " {$project->getInternalId()} " . " {$e->getMessage()}"); } } @@ -344,7 +344,7 @@ class StatsUsage extends Action continue; } - console::log('['.DateTime::now().'] Id: '.$project->getId(). ' InternalId: '.$project->getInternalId(). ' Db: '.$project->getAttribute('database').' ReceivedAt: '.$receivedAt. ' Keys: '.$numberOfKeys); + Console::log('['.DateTime::now().'] Id: '.$project->getId(). ' InternalId: '.$project->getInternalId(). ' Db: '.$project->getAttribute('database').' ReceivedAt: '.$receivedAt. ' Keys: '.$numberOfKeys); try { foreach ($stats['keys'] ?? [] as $key => $value) { @@ -381,7 +381,7 @@ class StatsUsage extends Action } } } catch (Exception $e) { - console::error('[' . DateTime::now() . '] project [' . $project->getInternalId() . '] database [' . $project['database'] . '] ' . ' ' . $e->getMessage()); + Console::error('[' . DateTime::now() . '] project [' . $project->getInternalId() . '] database [' . $project['database'] . '] ' . ' ' . $e->getMessage()); } } @@ -405,7 +405,7 @@ class StatsUsage extends Action } - protected function prepareForLogsDB(Document $project, Document $stat) + protected function prepareForLogsDB(Document $project, Document $stat): void { if (System::getEnv('_APP_STATS_USAGE_DUAL_WRITING', 'disabled') === 'disabled') { return; @@ -430,8 +430,7 @@ class StatsUsage extends Action return; } - $dbForLogs = call_user_func($this->getLogsDB); - $dbForLogs + $dbForLogs = ($this->getLogsDB)() ->setTenant(null) ->setTenantPerDocument(true); @@ -446,6 +445,5 @@ class StatsUsage extends Action } catch (Throwable $th) { Console::error($th->getMessage()); } - $this->register->get('pools')->get('logs')->reclaim(); } } diff --git a/src/Appwrite/Platform/Workers/StatsUsageDump.php b/src/Appwrite/Platform/Workers/StatsUsageDump.php index b9d486e0d8..77ec3f13e6 100644 --- a/src/Appwrite/Platform/Workers/StatsUsageDump.php +++ b/src/Appwrite/Platform/Workers/StatsUsageDump.php @@ -70,9 +70,9 @@ class StatsUsageDump extends Action ]; /** - * @var callable + * @var callable(Document): Database */ - protected mixed $getLogsDB; + protected $getLogsDB; protected array $periods = [ '1h' => 'Y-m-d H:00', @@ -126,10 +126,10 @@ class StatsUsageDump extends Action continue; } - console::log('['.DateTime::now().'] Id: '.$project->getId(). ' InternalId: '.$project->getInternalId(). ' Db: '.$project->getAttribute('database').' ReceivedAt: '.$receivedAt. ' Keys: '.$numberOfKeys); + Console::log('['.DateTime::now().'] Id: '.$project->getId(). ' InternalId: '.$project->getInternalId(). ' Db: '.$project->getAttribute('database').' ReceivedAt: '.$receivedAt. ' Keys: '.$numberOfKeys); try { - /** @var \Utopia\Database\Database $dbForProject */ + /** @var Database $dbForProject */ $dbForProject = $getProjectDB($project); foreach ($stats['keys'] ?? [] as $key => $value) { if ($value == 0) { @@ -169,7 +169,7 @@ class StatsUsageDump extends Action } } } catch (\Exception $e) { - console::error('[' . DateTime::now() . '] project [' . $project->getInternalId() . '] database [' . $project['database'] . '] ' . ' ' . $e->getMessage()); + Console::error('[' . DateTime::now() . '] project [' . $project->getInternalId() . '] database [' . $project['database'] . '] ' . ' ' . $e->getMessage()); } } } @@ -190,8 +190,7 @@ class StatsUsageDump extends Action } } - /** @var \Utopia\Database\Database $dbForLogs*/ - $dbForLogs = call_user_func($this->getLogsDB, $project); + $dbForLogs = ($this->getLogsDB)($project); try { $dbForLogs->createOrUpdateDocumentsWithIncrease( @@ -203,7 +202,5 @@ class StatsUsageDump extends Action } catch (\Throwable $th) { Console::error($th->getMessage()); } - - $this->register->get('pools')->get('logs')->reclaim(); } } diff --git a/src/Appwrite/PubSub/Adapter/Pool.php b/src/Appwrite/PubSub/Adapter/Pool.php new file mode 100644 index 0000000000..a498118dae --- /dev/null +++ b/src/Appwrite/PubSub/Adapter/Pool.php @@ -0,0 +1,46 @@ +delegate(__FUNCTION__, \func_get_args()); + } + + public function subscribe($channels, $callback): void + { + $this->delegate(__FUNCTION__, \func_get_args()); + } + + public function publish($channel, $message): void + { + $this->delegate(__FUNCTION__, \func_get_args()); + } + + /** + * Forward method calls to the internal adapter instance via the pool. + * + * Required because __call() can't be used to implement abstract methods. + * + * @param string $method + * @param array $args + * @return mixed + * @throws DatabaseException + */ + public function delegate(string $method, array $args): mixed + { + return $this->pool->use(function (Adapter $adapter) use ($method, $args) { + return $adapter->{$method}(...$args); + }); + } +} diff --git a/tests/resources/docker/docker-compose.yml b/tests/resources/docker/docker-compose.yml index e549ac27a5..779d63d6ed 100644 --- a/tests/resources/docker/docker-compose.yml +++ b/tests/resources/docker/docker-compose.yml @@ -35,7 +35,7 @@ services: - VERSION=dev restart: unless-stopped ports: - - 9501:80 + - "9501:80" networks: - appwrite labels: @@ -52,15 +52,12 @@ services: - ./phpunit.xml:/usr/src/code/phpunit.xml - ./tests:/usr/src/code/tests - ./app:/usr/src/code/app - # - ./vendor:/usr/src/code/vendor - ./docs:/usr/src/code/docs - ./public:/usr/src/code/public - ./src:/usr/src/code/src - - ./debug:/tmp depends_on: - mariadb - redis - # - clamav environment: - _APP_COMPRESSION_MIN_SIZE_BYTES - _APP_ENV @@ -355,33 +352,6 @@ services: volumes: - appwrite-redis:/data:rw - # clamav: - # image: appwrite/clamav:1.2.0 - # container_name: appwrite-clamav - # restart: unless-stopped - # networks: - # - appwrite - # volumes: - # - appwrite-uploads:/storage/uploads - - - # redis-commander: - # image: rediscommander/redis-commander:latest - # restart: unless-stopped - # networks: - # - appwrite - # environment: - # - REDIS_HOSTS=redis - # ports: - # - "8081:8081" - - # webgrind: - # image: 'jokkedk/webgrind:latest' - # volumes: - # - './debug:/tmp' - # ports: - # - '3001:80' - networks: gateway: appwrite: From af3388a51f9226455f0924b596047eb97f84c5d6 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 14 May 2025 18:16:54 +1200 Subject: [PATCH 14/37] Format --- app/cli.php | 3 ++- composer.lock | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/cli.php b/app/cli.php index 61d8a90100..663423b57a 100644 --- a/app/cli.php +++ b/app/cli.php @@ -10,6 +10,7 @@ use Appwrite\Event\StatsUsage; use Appwrite\Platform\Appwrite; use Appwrite\Runtimes\Runtimes; use Executor\Executor; +use Swoole\Timer; use Utopia\Cache\Adapter\Pool as CachePool; use Utopia\Cache\Adapter\Sharding; use Utopia\Cache\Cache; @@ -29,7 +30,7 @@ use Utopia\Queue\Publisher; use Utopia\Registry\Registry; use Utopia\System\System; use Utopia\Telemetry\Adapter\None as NoTelemetry; -use Swoole\Timer; + use function Swoole\Coroutine\run; // Overwriting runtimes to be architecture agnostic for CLI diff --git a/composer.lock b/composer.lock index d04d67366e..945948739a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1eeb5a0f3560aefd8f71bd0955e30360", + "content-hash": "24b02f9535e6a202fde735137f0ec900", "packages": [ { "name": "adhocore/jwt", From 09152a08e1756620fcc18072e7ff96b54876ec76 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 14 May 2025 18:27:51 +1200 Subject: [PATCH 15/37] Fix merge --- src/Appwrite/Platform/Tasks/ScheduleFunctions.php | 6 +++--- src/Appwrite/Platform/Workers/Deletes.php | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/ScheduleFunctions.php b/src/Appwrite/Platform/Tasks/ScheduleFunctions.php index 649fdb333a..19e068107a 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleFunctions.php +++ b/src/Appwrite/Platform/Tasks/ScheduleFunctions.php @@ -75,9 +75,9 @@ class ScheduleFunctions extends ScheduleBase $delayedExecutions[$delay][] = ['key' => $key, 'nextDate' => $nextDate]; } - foreach ($delayedExecutions as $delay => $scheduleKeys) { - \go(function () use ($delay, $scheduleKeys, $pools, $dbForPlatform) { - \Co::sleep($delay); // in seconds + foreach ($delayedExecutions as $delay => $schedules) { + \go(function () use ($delay, $schedules, $pools, $dbForPlatform) { + \sleep($delay); // in seconds foreach ($schedules as $delayConfig) { $scheduleKey = $delayConfig['key']; diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index 427772a6e0..d47ba9cb15 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -61,7 +61,7 @@ class Deletes extends Action ->inject('executionRetention') ->inject('auditRetention') ->inject('log') - ->callback([$this, 'action']); + ->callback($this->action(...)); } /** From 33fbab7e3d0128afd17a9bdec1f50da4d1377f90 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 14 May 2025 21:28:59 +1200 Subject: [PATCH 16/37] Fix missing capture --- src/Appwrite/Platform/Tasks/ScheduleMessages.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Tasks/ScheduleMessages.php b/src/Appwrite/Platform/Tasks/ScheduleMessages.php index 4f7dfab6a0..5e65f7a8a6 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleMessages.php +++ b/src/Appwrite/Platform/Tasks/ScheduleMessages.php @@ -40,7 +40,7 @@ class ScheduleMessages extends ScheduleBase continue; } - \go(function () use ($schedule, $pools, $dbForPlatform) { + \go(function () use ($schedule, $scheduledAt, $pools, $dbForPlatform) { $queueForMessaging = new Messaging($this->publisher); $this->updateProjectAccess($schedule['project'], $dbForPlatform); From 55236524e336b8108b94e6e8c0d332dce6db9735 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 14 May 2025 21:42:39 +1200 Subject: [PATCH 17/37] Fix merge --- src/Appwrite/Platform/Tasks/ScheduleExecutions.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/ScheduleExecutions.php b/src/Appwrite/Platform/Tasks/ScheduleExecutions.php index 331928d265..99c84e829a 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleExecutions.php +++ b/src/Appwrite/Platform/Tasks/ScheduleExecutions.php @@ -3,6 +3,7 @@ namespace Appwrite\Platform\Tasks; use Appwrite\Event\Func; +use Swoole\Coroutine as Co; use Utopia\Database\Database; use Utopia\Pools\Group; @@ -28,6 +29,7 @@ class ScheduleExecutions extends ScheduleBase protected function enqueueResources(Group $pools, Database $dbForPlatform, callable $getProjectDB): void { + $queueForFunctions = new Func($this->publisher); $intervalEnd = (new \DateTime())->modify('+' . self::ENQUEUE_TIMER . ' seconds'); foreach ($this->schedules as $schedule) { @@ -55,10 +57,8 @@ class ScheduleExecutions extends ScheduleBase $this->updateProjectAccess($schedule['project'], $dbForPlatform); - \go(function () use ($schedule, $delay, $data, $pools) { - \Co::sleep($delay); - - $queueForFunctions = new Func($this->publisher); + \go(function () use ($queueForFunctions, $schedule, $scheduledAt, $delay, $data) { + Co::sleep($delay); $queueForFunctions->setType('schedule') // Set functionId instead of function as we don't have $dbForProject From 651cec65da231ae8eacaa832b70ed8f4e5a6d962 Mon Sep 17 00:00:00 2001 From: Fabian Gruber <1951610+basert@users.noreply.github.com> Date: Thu, 15 May 2025 10:21:38 +0200 Subject: [PATCH 18/37] feat: telemetry for storage devices (#9761) --- app/init/resources.php | 24 ++++++++++++------------ app/worker.php | 29 +++++++++++++++++------------ composer.lock | 41 +++++++++++++++++++++-------------------- 3 files changed, 50 insertions(+), 44 deletions(-) diff --git a/app/init/resources.php b/app/init/resources.php index c719a47344..6bd40c6593 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 () { - return new Local(); -}); +App::setResource('deviceForLocal', function (Telemetry $telemetry) { + return new Device\Telemetry($telemetry, new Local()); +}, ['telemetry']); -App::setResource('deviceForFiles', function ($project) { - return getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId()); -}, ['project']); +App::setResource('deviceForFiles', function ($project, Telemetry $telemetry) { + return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId())); +}, ['project', 'telemetry']); -App::setResource('deviceForFunctions', function ($project) { - return getDevice(APP_STORAGE_FUNCTIONS . '/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('deviceForBuilds', function ($project) { - return getDevice(APP_STORAGE_BUILDS . '/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']); function getDevice(string $root, string $connection = ''): Device { diff --git a/app/worker.php b/app/worker.php index 232e0b3684..29ebc836cd 100644 --- a/app/worker.php +++ b/app/worker.php @@ -37,7 +37,10 @@ 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); @@ -334,21 +337,23 @@ Server::setResource('pools', function (Registry $register) { return $register->get('pools'); }, ['register']); -Server::setResource('deviceForFunctions', function (Document $project) { - return getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId()); -}, ['project']); +Server::setResource('telemetry', fn () => new NoTelemetry()); -Server::setResource('deviceForFiles', function (Document $project) { - return getDevice(APP_STORAGE_UPLOADS . '/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('deviceForBuilds', function (Document $project) { - return getDevice(APP_STORAGE_BUILDS . '/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('deviceForCache', function (Document $project) { - return getDevice(APP_STORAGE_CACHE . '/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( 'isResourceBlocked', diff --git a/composer.lock b/composer.lock index 5e86bfafd2..828915d158 100644 --- a/composer.lock +++ b/composer.lock @@ -709,16 +709,16 @@ }, { "name": "google/protobuf", - "version": "v4.30.2", + "version": "v4.31.0", "source": { "type": "git", "url": "https://github.com/protocolbuffers/protobuf-php.git", - "reference": "a4c4d8565b40b9f76debc9dfeb221412eacb8ced" + "reference": "d59e31ce4bf0e4b48728e90c4d880839edb5be07" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/a4c4d8565b40b9f76debc9dfeb221412eacb8ced", - "reference": "a4c4d8565b40b9f76debc9dfeb221412eacb8ced", + "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/d59e31ce4bf0e4b48728e90c4d880839edb5be07", + "reference": "d59e31ce4bf0e4b48728e90c4d880839edb5be07", "shasum": "" }, "require": { @@ -747,9 +747,9 @@ "proto" ], "support": { - "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.30.2" + "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.31.0" }, - "time": "2025-03-26T18:01:50+00:00" + "time": "2025-05-14T16:17:23+00:00" }, { "name": "league/csv", @@ -3499,16 +3499,16 @@ }, { "name": "utopia-php/database", - "version": "0.69.1", + "version": "0.69.2", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "cc6538e05e25d930244ab938c966d32db0922e83" + "reference": "60591ab073bb80bb9843338754b679bb8169e4ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/cc6538e05e25d930244ab938c966d32db0922e83", - "reference": "cc6538e05e25d930244ab938c966d32db0922e83", + "url": "https://api.github.com/repos/utopia-php/database/zipball/60591ab073bb80bb9843338754b679bb8169e4ed", + "reference": "60591ab073bb80bb9843338754b679bb8169e4ed", "shasum": "" }, "require": { @@ -3549,9 +3549,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/0.69.1" + "source": "https://github.com/utopia-php/database/tree/0.69.2" }, - "time": "2025-05-13T12:00:31+00:00" + "time": "2025-05-14T07:51:44+00:00" }, { "name": "utopia-php/domains", @@ -4331,16 +4331,16 @@ }, { "name": "utopia-php/storage", - "version": "0.18.10", + "version": "0.18.12", "source": { "type": "git", "url": "https://github.com/utopia-php/storage.git", - "reference": "76f31158f4251abb207f7a9b16f7cb0bfdb3b39e" + "reference": "9a2556c39b5f4d9f8e79111fd34ec889b7bb1e97" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/storage/zipball/76f31158f4251abb207f7a9b16f7cb0bfdb3b39e", - "reference": "76f31158f4251abb207f7a9b16f7cb0bfdb3b39e", + "url": "https://api.github.com/repos/utopia-php/storage/zipball/9a2556c39b5f4d9f8e79111fd34ec889b7bb1e97", + "reference": "9a2556c39b5f4d9f8e79111fd34ec889b7bb1e97", "shasum": "" }, "require": { @@ -4353,9 +4353,10 @@ "ext-xz": "*", "ext-zlib": "*", "ext-zstd": "*", - "php": ">=8.0", + "php": ">=8.1", "utopia-php/framework": "0.*.*", - "utopia-php/system": "0.*.*" + "utopia-php/system": "0.*.*", + "utopia-php/telemetry": "0.1.*" }, "require-dev": { "laravel/pint": "1.2.*", @@ -4382,9 +4383,9 @@ ], "support": { "issues": "https://github.com/utopia-php/storage/issues", - "source": "https://github.com/utopia-php/storage/tree/0.18.10" + "source": "https://github.com/utopia-php/storage/tree/0.18.12" }, - "time": "2025-03-03T10:47:54+00:00" + "time": "2025-05-15T07:55:58+00:00" }, { "name": "utopia-php/swoole", From ad3adbda21569b30c63beadc888098cb56852624 Mon Sep 17 00:00:00 2001 From: Darshan Date: Thu, 15 May 2025 14:08:54 +0530 Subject: [PATCH 19/37] fix: owner downgrade. --- app/controllers/api/teams.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index dd64fe0768..9c383803f2 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -1089,7 +1089,10 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId') // Quick check: fetch up to 2 owners to determine if only one exists $ownersCount = $dbForProject->count( collection: 'memberships', - queries: [Query::contains('roles', ['owner'])], + queries: [ + Query::contains('roles', ['owner']), + Query::equal('teamInternalId', [$team->getInternalId()]) + ], max: 2 ); From 15992da36943518c82d8f90864c8321cb98733a0 Mon Sep 17 00:00:00 2001 From: Darshan Date: Thu, 15 May 2025 15:01:23 +0530 Subject: [PATCH 20/37] update: tests. --- tests/e2e/Services/Teams/TeamsConsoleClientTest.php | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/tests/e2e/Services/Teams/TeamsConsoleClientTest.php b/tests/e2e/Services/Teams/TeamsConsoleClientTest.php index 4b5ade7cbf..06358765f6 100644 --- a/tests/e2e/Services/Teams/TeamsConsoleClientTest.php +++ b/tests/e2e/Services/Teams/TeamsConsoleClientTest.php @@ -86,7 +86,7 @@ class TeamsConsoleClientTest extends Scope $session = $data['session'] ?? ''; /** - * Test for SUCCESS + * Test for FAILURE */ $roles = ['developer']; $response = $this->client->call(Client::METHOD_PATCH, '/teams/' . $teamUid . '/memberships/' . $membershipUid, array_merge([ @@ -97,12 +97,8 @@ class TeamsConsoleClientTest extends Scope 'roles' => $roles ]); - $this->assertEquals(200, $response['headers']['status-code']); - $this->assertNotEmpty($response['body']['$id']); - $this->assertNotEmpty($response['body']['userId']); - $this->assertNotEmpty($response['body']['teamId']); - $this->assertCount(count($roles), $response['body']['roles']); - $this->assertEquals($roles[0], $response['body']['roles'][0]); + $this->assertEquals(400, $response['headers']['status-code']); + $this->assertEquals('There must be at least one owner in the organization.', $response['body']['message']); /** * Test for unknown team From b1829bacfe74b865dd2c08d352853c1207025869 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 15 May 2025 23:33:00 +1200 Subject: [PATCH 21/37] 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 22/37] 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 23/37] 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 24/37] 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 25/37] 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 d28831632e88ee593d84d26829a1ad6a5b900a6a Mon Sep 17 00:00:00 2001 From: Fabian Gruber <1951610+basert@users.noreply.github.com> Date: Thu, 15 May 2025 14:45:24 +0200 Subject: [PATCH 26/37] Swap default image quality to -1, keeping source image quality. (#9766) --- app/controllers/api/avatars.php | 6 +++--- app/controllers/api/storage.php | 2 +- composer.lock | 12 ++++++------ 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/controllers/api/avatars.php b/app/controllers/api/avatars.php index 24fa36725f..05a9af81f8 100644 --- a/app/controllers/api/avatars.php +++ b/app/controllers/api/avatars.php @@ -187,7 +187,7 @@ App::get('/v1/avatars/credit-cards/:code') ->param('code', '', new WhiteList(\array_keys(Config::getParam('avatar-credit-cards'))), 'Credit Card Code. Possible values: ' . \implode(', ', \array_keys(Config::getParam('avatar-credit-cards'))) . '.') ->param('width', 100, new Range(0, 2000), 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true) ->param('height', 100, new Range(0, 2000), 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true) - ->param('quality', 100, new Range(0, 100), 'Image quality. Pass an integer between 0 to 100. Defaults to 100.', true) + ->param('quality', -1, new Range(-1, 100), 'Image quality. Pass an integer between 0 to 100. Defaults to keep existing image quality.', true) ->inject('response') ->action(fn (string $code, int $width, int $height, int $quality, Response $response) => $avatarCallback('credit-cards', $code, $width, $height, $quality, $response)); @@ -215,7 +215,7 @@ App::get('/v1/avatars/browsers/:code') ->param('code', '', new WhiteList(\array_keys(Config::getParam('avatar-browsers'))), 'Browser Code.') ->param('width', 100, new Range(0, 2000), 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true) ->param('height', 100, new Range(0, 2000), 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true) - ->param('quality', 100, new Range(0, 100), 'Image quality. Pass an integer between 0 to 100. Defaults to 100.', true) + ->param('quality', -1, new Range(-1, 100), 'Image quality. Pass an integer between 0 to 100. Defaults to keep existing image quality.', true) ->inject('response') ->action(fn (string $code, int $width, int $height, int $quality, Response $response) => $avatarCallback('browsers', $code, $width, $height, $quality, $response)); @@ -243,7 +243,7 @@ App::get('/v1/avatars/flags/:code') ->param('code', '', new WhiteList(\array_keys(Config::getParam('avatar-flags'))), 'Country Code. ISO Alpha-2 country code format.') ->param('width', 100, new Range(0, 2000), 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true) ->param('height', 100, new Range(0, 2000), 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true) - ->param('quality', 100, new Range(0, 100), 'Image quality. Pass an integer between 0 to 100. Defaults to 100.', true) + ->param('quality', -1, new Range(-1, 100), 'Image quality. Pass an integer between 0 to 100. Defaults to keep existing image quality.', true) ->inject('response') ->action(fn (string $code, int $width, int $height, int $quality, Response $response) => $avatarCallback('flags', $code, $width, $height, $quality, $response)); diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php index d7e7c494f7..78c4c5fec9 100644 --- a/app/controllers/api/storage.php +++ b/app/controllers/api/storage.php @@ -943,7 +943,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') ->param('width', 0, new Range(0, 4000), 'Resize preview image width, Pass an integer between 0 to 4000.', true) ->param('height', 0, new Range(0, 4000), 'Resize preview image height, Pass an integer between 0 to 4000.', true) ->param('gravity', Image::GRAVITY_CENTER, new WhiteList(Image::getGravityTypes()), 'Image crop gravity. Can be one of ' . implode(",", Image::getGravityTypes()), true) - ->param('quality', 100, new Range(0, 100), 'Preview image quality. Pass an integer between 0 to 100. Defaults to 100.', true) + ->param('quality', -1, new Range(-1, 100), 'Preview image quality. Pass an integer between 0 to 100. Defaults to keep existing image quality.', true) ->param('borderWidth', 0, new Range(0, 100), 'Preview image border in pixels. Pass an integer between 0 to 100. Defaults to 0.', true) ->param('borderColor', '', new HexColor(), 'Preview image border color. Use a valid HEX color, no # is needed for prefix.', true) ->param('borderRadius', 0, new Range(0, 4000), 'Preview image border radius in pixels. Pass an integer between 0 to 4000.', true) diff --git a/composer.lock b/composer.lock index 828915d158..e108e45171 100644 --- a/composer.lock +++ b/composer.lock @@ -3748,16 +3748,16 @@ }, { "name": "utopia-php/image", - "version": "0.8.2", + "version": "0.8.3", "source": { "type": "git", "url": "https://github.com/utopia-php/image.git", - "reference": "6c736965177f9a9e71311e22b80cfa88511768e9" + "reference": "8820b0e53b3636b7bdf815e92394d333fef06f26" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/image/zipball/6c736965177f9a9e71311e22b80cfa88511768e9", - "reference": "6c736965177f9a9e71311e22b80cfa88511768e9", + "url": "https://api.github.com/repos/utopia-php/image/zipball/8820b0e53b3636b7bdf815e92394d333fef06f26", + "reference": "8820b0e53b3636b7bdf815e92394d333fef06f26", "shasum": "" }, "require": { @@ -3791,9 +3791,9 @@ ], "support": { "issues": "https://github.com/utopia-php/image/issues", - "source": "https://github.com/utopia-php/image/tree/0.8.2" + "source": "https://github.com/utopia-php/image/tree/0.8.3" }, - "time": "2025-04-08T11:31:45+00:00" + "time": "2025-05-15T10:39:28+00:00" }, { "name": "utopia-php/locale", 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 27/37] 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 28/37] 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 29/37] 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", From 117717888572314130278089cb597ddee895b444 Mon Sep 17 00:00:00 2001 From: Fabian Gruber Date: Mon, 19 May 2025 12:40:11 +0200 Subject: [PATCH 30/37] fix: task coroutine hooks --- app/cli.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/cli.php b/app/cli.php index 9ade97e90c..0c91b3c34f 100644 --- a/app/cli.php +++ b/app/cli.php @@ -10,6 +10,7 @@ use Appwrite\Event\StatsUsage; use Appwrite\Platform\Appwrite; use Appwrite\Runtimes\Runtimes; use Executor\Executor; +use Swoole\Runtime; use Swoole\Timer; use Utopia\Cache\Adapter\Sharding; use Utopia\Cache\Cache; @@ -298,4 +299,6 @@ $cli $cli->shutdown()->action(fn () => Timer::clearAll()); +// Enable coroutines, but disable TCP hooks. These don't work until we use `\Utopia\Cache\Adapter\Pool` and `\Utopia\Database\Adapter\Pool`. +Runtime::enableCoroutine(SWOOLE_HOOK_ALL ^ SWOOLE_HOOK_TCP); run($cli->run(...)); From 149dfb965153d8f51126017e1f3a2456fb53241c Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 20 May 2025 20:07:11 +1200 Subject: [PATCH 31/37] Update lock --- composer.lock | 50 +++++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/composer.lock b/composer.lock index c52ecba812..432676ca3e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "24b02f9535e6a202fde735137f0ec900", + "content-hash": "63feb1a7cf4cfa2cc7fa0870236e61ea", "packages": [ { "name": "adhocore/jwt", @@ -3499,16 +3499,16 @@ }, { "name": "utopia-php/database", - "version": "0.69.2", + "version": "0.69.5", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "60591ab073bb80bb9843338754b679bb8169e4ed" + "reference": "4abe53609dfc23b2ea82884d12b149df6a8af2f5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/60591ab073bb80bb9843338754b679bb8169e4ed", - "reference": "60591ab073bb80bb9843338754b679bb8169e4ed", + "url": "https://api.github.com/repos/utopia-php/database/zipball/4abe53609dfc23b2ea82884d12b149df6a8af2f5", + "reference": "4abe53609dfc23b2ea82884d12b149df6a8af2f5", "shasum": "" }, "require": { @@ -3549,9 +3549,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/0.69.2" + "source": "https://github.com/utopia-php/database/tree/0.69.5" }, - "time": "2025-05-14T07:51:44+00:00" + "time": "2025-05-17T08:01:51+00:00" }, { "name": "utopia-php/domains", @@ -3701,16 +3701,16 @@ }, { "name": "utopia-php/framework", - "version": "0.33.19", + "version": "0.33.20", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "64c7b7bb8a8595ffe875fa8d4b7705684dbf46c0" + "reference": "e1c7ab4e0b5b0a9a70256b1e00912e101e76a131" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/64c7b7bb8a8595ffe875fa8d4b7705684dbf46c0", - "reference": "64c7b7bb8a8595ffe875fa8d4b7705684dbf46c0", + "url": "https://api.github.com/repos/utopia-php/http/zipball/e1c7ab4e0b5b0a9a70256b1e00912e101e76a131", + "reference": "e1c7ab4e0b5b0a9a70256b1e00912e101e76a131", "shasum": "" }, "require": { @@ -3742,9 +3742,9 @@ ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/0.33.19" + "source": "https://github.com/utopia-php/http/tree/0.33.20" }, - "time": "2025-03-06T11:37:49+00:00" + "time": "2025-05-18T23:51:21+00:00" }, { "name": "utopia-php/image", @@ -4059,16 +4059,16 @@ }, { "name": "utopia-php/platform", - "version": "0.7.5", + "version": "0.7.6", "source": { "type": "git", "url": "https://github.com/utopia-php/platform.git", - "reference": "8febd7b6e0c0f2cbd2f4289447bcca97496e4aaf" + "reference": "6bc7fbb43ec2b7f9ee5bdef5d4b5e4a81860950b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/platform/zipball/8febd7b6e0c0f2cbd2f4289447bcca97496e4aaf", - "reference": "8febd7b6e0c0f2cbd2f4289447bcca97496e4aaf", + "url": "https://api.github.com/repos/utopia-php/platform/zipball/6bc7fbb43ec2b7f9ee5bdef5d4b5e4a81860950b", + "reference": "6bc7fbb43ec2b7f9ee5bdef5d4b5e4a81860950b", "shasum": "" }, "require": { @@ -4103,9 +4103,9 @@ ], "support": { "issues": "https://github.com/utopia-php/platform/issues", - "source": "https://github.com/utopia-php/platform/tree/0.7.5" + "source": "https://github.com/utopia-php/platform/tree/0.7.6" }, - "time": "2025-04-17T12:20:16+00:00" + "time": "2025-05-18T20:31:24+00:00" }, { "name": "utopia-php/pools", @@ -4771,16 +4771,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "0.40.16", + "version": "0.40.17", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "f1f506da74033f0cb5a11e3dffcfd1ee8daf237d" + "reference": "7e333c1003bfd4763e4d6f3a0a799fde5e7bc4de" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/f1f506da74033f0cb5a11e3dffcfd1ee8daf237d", - "reference": "f1f506da74033f0cb5a11e3dffcfd1ee8daf237d", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/7e333c1003bfd4763e4d6f3a0a799fde5e7bc4de", + "reference": "7e333c1003bfd4763e4d6f3a0a799fde5e7bc4de", "shasum": "" }, "require": { @@ -4816,9 +4816,9 @@ "description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms", "support": { "issues": "https://github.com/appwrite/sdk-generator/issues", - "source": "https://github.com/appwrite/sdk-generator/tree/0.40.16" + "source": "https://github.com/appwrite/sdk-generator/tree/0.40.17" }, - "time": "2025-05-09T12:06:09+00:00" + "time": "2025-05-16T15:10:54+00:00" }, { "name": "doctrine/annotations", From 455554a6a2afe4f54ce3d36d542c0da09caf12b5 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Tue, 20 May 2025 11:06:09 +0100 Subject: [PATCH 32/37] fix: download endpoint --- app/controllers/api/storage.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php index 78c4c5fec9..a96b871bf2 100644 --- a/app/controllers/api/storage.php +++ b/app/controllers/api/storage.php @@ -1225,12 +1225,15 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download') if (!empty($source)) { if (!empty($rangeHeader)) { $response->send(substr($source, $start, ($end - $start + 1))); + return; } $response->send($source); + return; } if (!empty($rangeHeader)) { $response->send($deviceForFiles->read($path, $start, ($end - $start + 1))); + return; } if ($size > APP_STORAGE_READ_BUFFER) { @@ -1383,6 +1386,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view') if (!empty($source)) { if (!empty($rangeHeader)) { $response->send(substr($source, $start, ($end - $start + 1))); + return; } $response->send($source); return; @@ -1534,6 +1538,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/push') if (!empty($source)) { if (!empty($rangeHeader)) { $response->send(substr($source, $start, ($end - $start + 1))); + return; } $response->send($source); return; From fe763093df4d91d21cbc015c237078e26cbf0a0e Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 21 May 2025 00:58:32 +1200 Subject: [PATCH 33/37] Catch query exception on get document for selects --- app/controllers/api/databases.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index 675268b894..138428ef75 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -3785,7 +3785,12 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); } - $document = $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId, $queries); + try { + $document = $dbForProject->getDocument('database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), $documentId, $queries); + } catch (QueryException $e) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); + } + if ($document->isEmpty()) { throw new Exception(Exception::DOCUMENT_NOT_FOUND); } From bc34f95646043aa10224e1015ec7122b2135800c Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 21 May 2025 01:13:08 +1200 Subject: [PATCH 34/37] Update lock --- app/controllers/api/databases.php | 2 +- composer.lock | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index 138428ef75..ec402333e4 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -3790,7 +3790,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/documents/:documen } catch (QueryException $e) { throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); } - + if ($document->isEmpty()) { throw new Exception(Exception::DOCUMENT_NOT_FOUND); } diff --git a/composer.lock b/composer.lock index 432676ca3e..557b61f36c 100644 --- a/composer.lock +++ b/composer.lock @@ -4059,16 +4059,16 @@ }, { "name": "utopia-php/platform", - "version": "0.7.6", + "version": "0.7.7", "source": { "type": "git", "url": "https://github.com/utopia-php/platform.git", - "reference": "6bc7fbb43ec2b7f9ee5bdef5d4b5e4a81860950b" + "reference": "8c43cd866148a7c4c495e3401268429e338004b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/platform/zipball/6bc7fbb43ec2b7f9ee5bdef5d4b5e4a81860950b", - "reference": "6bc7fbb43ec2b7f9ee5bdef5d4b5e4a81860950b", + "url": "https://api.github.com/repos/utopia-php/platform/zipball/8c43cd866148a7c4c495e3401268429e338004b3", + "reference": "8c43cd866148a7c4c495e3401268429e338004b3", "shasum": "" }, "require": { @@ -4103,9 +4103,9 @@ ], "support": { "issues": "https://github.com/utopia-php/platform/issues", - "source": "https://github.com/utopia-php/platform/tree/0.7.6" + "source": "https://github.com/utopia-php/platform/tree/0.7.7" }, - "time": "2025-05-18T20:31:24+00:00" + "time": "2025-05-20T09:23:44+00:00" }, { "name": "utopia-php/pools", From 20f2cbffda764cf3c26fb78b3071855d98f39c6a Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 20 May 2025 14:37:10 +0000 Subject: [PATCH 35/37] Merge pull request #9811 from ArnabChatterjee20k/upsert-single-document-route Upsert single document route --- app/controllers/api/databases.php | 238 ++++++++++++++ docs/references/databases/upsert-document.md | 1 + .../e2e/Services/Databases/DatabasesBase.php | 303 +++++++++++++++++- 3 files changed, 541 insertions(+), 1 deletion(-) create mode 100644 docs/references/databases/upsert-document.md diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index ec402333e4..68d8891067 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -4207,6 +4207,244 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum $response->dynamic($document, Response::MODEL_DOCUMENT); }); +App::put('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId') + ->desc('Upsert document') + ->groups(['api', 'database']) + ->label('event', 'databases.[databaseId].collections.[collectionId].documents.[documentId].upsert') + ->label('scope', 'documents.write') + ->label('resourceType', RESOURCE_TYPE_DATABASES) + ->label('audits.event', 'document.upsert') + ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}/document/{response.$id}') + ->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}') + ->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT * 2) + ->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT) + ->label('sdk', new Method( + namespace: 'databases', + group: 'documents', + name: 'upsertDocument', + description: '/docs/references/databases/upsert-document.md', + auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_DOCUMENT, + ) + ], + contentType: ContentType::JSON + )) + ->param('databaseId', '', new UID(), 'Database ID.') + ->param('collectionId', '', new UID(), 'Collection ID.') + ->param('documentId', '', new CustomId(), 'Document ID.') + ->param('data', [], new JSON(), 'Document data as JSON object. Include all required attributes of the document to be created or updated.') + ->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE]), 'An array of permissions strings. By default, the current permissions are inherited. [Learn more about permissions](https://appwrite.io/docs/permissions).', true) + ->inject('requestTimestamp') + ->inject('response') + ->inject('dbForProject') + ->inject('queueForEvents') + ->inject('queueForStatsUsage') + ->action(function (string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage) { + $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array + + if (empty($data) && \is_null($permissions)) { + throw new Exception(Exception::DOCUMENT_MISSING_PAYLOAD); + } + + $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); + } + + // Map aggregate permissions into the multiple permissions they represent. + $permissions = Permission::aggregate($permissions, [ + Database::PERMISSION_READ, + Database::PERMISSION_UPDATE, + Database::PERMISSION_DELETE, + ]); + + // Users can only manage their own roles, API keys and Admin users can manage any + $roles = Authorization::getRoles(); + if (!$isAPIKey && !$isPrivilegedUser && !\is_null($permissions)) { + foreach (Database::PERMISSIONS as $type) { + foreach ($permissions as $permission) { + $permission = Permission::parse($permission); + if ($permission->getPermission() != $type) { + continue; + } + $role = (new Role( + $permission->getRole(), + $permission->getIdentifier(), + $permission->getDimension() + ))->toString(); + if (!Authorization::isRole($role)) { + throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', $roles) . ')'); + } + } + } + } + + $data['$id'] = $documentId; + $data['$permissions'] = $permissions; + $newDocument = new Document($data); + + $operations = 0; + + $setCollection = (function (Document $collection, Document $document) use (&$setCollection, $dbForProject, $database, &$operations) { + + $operations++; + + $relationships = \array_filter( + $collection->getAttribute('attributes', []), + fn ($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP + ); + + foreach ($relationships as $relationship) { + $related = $document->getAttribute($relationship->getAttribute('key')); + + if (empty($related)) { + continue; + } + + $isList = \is_array($related) && \array_values($related) === $related; + + if ($isList) { + $relations = $related; + } else { + $relations = [$related]; + } + + $relatedCollectionId = $relationship->getAttribute('relatedCollection'); + $relatedCollection = Authorization::skip( + fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $relatedCollectionId) + ); + + foreach ($relations as &$relation) { + // If the relation is an array it can be either update or create a child document. + if ( + \is_array($relation) + && \array_values($relation) !== $relation + && !isset($relation['$id']) + ) { + $relation['$id'] = ID::unique(); + $relation = new Document($relation); + } + if ($relation instanceof Document) { + $oldDocument = Authorization::skip(fn () => $dbForProject->getDocument( + 'database_' . $database->getInternalId() . '_collection_' . $relatedCollection->getInternalId(), + $relation->getId() + )); + $relation->removeAttribute('$collectionId'); + $relation->removeAttribute('$databaseId'); + // Attribute $collection is required for Utopia. + $relation->setAttribute( + '$collection', + 'database_' . $database->getInternalId() . '_collection_' . $relatedCollection->getInternalId() + ); + + if ($oldDocument->isEmpty()) { + if (isset($relation['$id']) && $relation['$id'] === 'unique()') { + $relation['$id'] = ID::unique(); + } + } + $setCollection($relatedCollection, $relation); + } + } + + if ($isList) { + $document->setAttribute($relationship->getAttribute('key'), \array_values($relations)); + } else { + $document->setAttribute($relationship->getAttribute('key'), \reset($relations)); + } + } + }); + + $setCollection($collection, $newDocument); + + $queueForStatsUsage + ->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, \max(1, $operations)) + ->addMetric(str_replace('{databaseInternalId}', $database->getInternalId(), METRIC_DATABASE_ID_OPERATIONS_WRITES), \max(1, $operations)); + + $upserted = []; + try { + $modified = $dbForProject->createOrUpdateDocuments( + 'database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), + [$newDocument], + onNext: function (Document $document) use (&$upserted) { + $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()); + } + + $document = $upserted[0]; + // Add $collectionId and $databaseId for all documents + $processDocument = function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database) { + $document->setAttribute('$databaseId', $database->getId()); + $document->setAttribute('$collectionId', $collection->getId()); + + $relationships = \array_filter( + $collection->getAttribute('attributes', []), + fn ($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP + ); + + foreach ($relationships as $relationship) { + $related = $document->getAttribute($relationship->getAttribute('key')); + + if (empty($related)) { + continue; + } + if (!\is_array($related)) { + $related = [$related]; + } + + $relatedCollectionId = $relationship->getAttribute('relatedCollection'); + $relatedCollection = Authorization::skip( + fn () => $dbForProject->getDocument('database_' . $database->getInternalId(), $relatedCollectionId) + ); + + foreach ($related as $relation) { + if ($relation instanceof Document) { + $processDocument($relatedCollection, $relation); + } + } + } + }; + + $processDocument($collection, $document); + + $relationships = \array_map( + fn ($document) => $document->getAttribute('key'), + \array_filter( + $collection->getAttribute('attributes', []), + fn ($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP + ) + ); + + $queueForEvents + ->setParam('databaseId', $databaseId) + ->setParam('collectionId', $collection->getId()) + ->setParam('documentId', $document->getId()) + ->setContext('collection', $collection) + ->setContext('database', $database) + ->setPayload($response->getPayload(), sensitive: $relationships); + + $response->dynamic($document, Response::MODEL_DOCUMENT); + }); + App::patch('/v1/databases/:databaseId/collections/:collectionId/documents') ->desc('Update documents') ->groups(['api', 'database']) diff --git a/docs/references/databases/upsert-document.md b/docs/references/databases/upsert-document.md new file mode 100644 index 0000000000..a67694cfa6 --- /dev/null +++ b/docs/references/databases/upsert-document.md @@ -0,0 +1 @@ +Create or update a Document. Before using this route, you should create a new collection resource using either a [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection) API or directly from your database console. \ No newline at end of file diff --git a/tests/e2e/Services/Databases/DatabasesBase.php b/tests/e2e/Services/Databases/DatabasesBase.php index 2b60dee856..7c0060ecaa 100644 --- a/tests/e2e/Services/Databases/DatabasesBase.php +++ b/tests/e2e/Services/Databases/DatabasesBase.php @@ -12,7 +12,6 @@ use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Database\Query; use Utopia\Database\Validator\Datetime as DatetimeValidator; -use Utopia\Validator\JSON; trait DatabasesBase { @@ -1683,6 +1682,308 @@ trait DatabasesBase return $data; } + /** + * @depends testCreateIndexes + */ + public function testUpsertDocument(array $data): void + { + $databaseId = $data['databaseId']; + $documentId = ID::unique(); + $document = $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/documents/' . $documentId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'title' => 'Thor: Ragnarok', + 'releaseYear' => 2000 + ], + 'permissions' => [ + Permission::read(Role::users()), + Permission::update(Role::users()), + Permission::delete(Role::users()), + ], + ]); + + $this->assertEquals(200, $document['headers']['status-code']); + $this->assertCount(3, $document['body']['$permissions']); + $document = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/documents/' . $documentId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals('Thor: Ragnarok', $document['body']['title']); + + $document = $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/documents/' . $documentId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'title' => 'Thor: Love and Thunder', + 'releaseYear' => 2000 + ], + 'permissions' => [ + Permission::read(Role::users()), + Permission::update(Role::users()), + Permission::delete(Role::users()), + ], + ]); + + $this->assertEquals(200, $document['headers']['status-code']); + $this->assertEquals('Thor: Love and Thunder', $document['body']['title']); + + $document = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/documents/' . $documentId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals('Thor: Love and Thunder', $document['body']['title']); + + // removing permission to read and delete + $document = $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/documents/' . $documentId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'title' => 'Thor: Love and Thunder', + 'releaseYear' => 2000 + ], + 'permissions' => [ + Permission::update(Role::users()) + ], + ]); + // shouldn't be able to read as no read permission + $document = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/documents/' . $documentId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + switch ($this->getSide()) { + case 'client': + $this->assertEquals(404, $document['headers']['status-code']); + break; + case 'server': + $this->assertEquals(200, $document['headers']['status-code']); + break; + } + // shouldn't be able to delete as no delete permission + $document = $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/documents/' . $documentId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + // simulating for the client + // the document should not be allowed to be deleted as needed downward + if ($this->getSide() === 'client') { + $this->assertEquals(401, $document['headers']['status-code']); + } + // giving the delete permission + $document = $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/documents/' . $documentId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'title' => 'Thor: Love and Thunder', + 'releaseYear' => 2000 + ], + 'permissions' => [ + Permission::read(Role::users()), + Permission::update(Role::users()), + Permission::delete(Role::users()) + ], + ]); + $document = $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/documents/' . $documentId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + $this->assertEquals(204, $document['headers']['status-code']); + + // relationship behaviour + $person = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => 'person-upsert', + 'name' => 'person', + 'permissions' => [ + Permission::read(Role::users()), + Permission::update(Role::users()), + Permission::delete(Role::users()), + Permission::create(Role::users()), + ], + 'documentSecurity' => true, + ]); + + $this->assertEquals(201, $person['headers']['status-code']); + + $library = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => 'library-upsert', + 'name' => 'library', + 'permissions' => [ + Permission::read(Role::users()), + Permission::update(Role::users()), + Permission::create(Role::users()), + Permission::delete(Role::users()), + ], + 'documentSecurity' => true, + ]); + + $this->assertEquals(201, $library['headers']['status-code']); + + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $person['body']['$id'] . '/attributes/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'fullName', + 'size' => 255, + 'required' => false, + ]); + + sleep(1); // Wait for worker + + $relation = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $person['body']['$id'] . '/attributes/relationship', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'relatedCollectionId' => 'library-upsert', + 'type' => Database::RELATION_ONE_TO_ONE, + 'key' => 'library', + 'twoWay' => true, + 'onDelete' => Database::RELATION_MUTATE_CASCADE, + ]); + + sleep(1); // Wait for worker + + $libraryName = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $library['body']['$id'] . '/attributes/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => 'libraryName', + 'size' => 255, + 'required' => true, + ]); + + sleep(1); // Wait for worker + + $this->assertEquals(202, $libraryName['headers']['status-code']); + + // upserting values + $documentId = ID::unique(); + $person1 = $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/collections/' . $person['body']['$id'] . '/documents/'.$documentId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'library' => [ + '$id' => 'library1', + '$permissions' => [ + Permission::read(Role::users()), + Permission::update(Role::users()), + Permission::delete(Role::users()), + ], + 'libraryName' => 'Library 1', + ], + ], + 'permissions' => [ + Permission::read(Role::users()), + Permission::update(Role::users()), + Permission::delete(Role::users()), + ] + ]); + + $this->assertEquals('Library 1', $person1['body']['library']['libraryName']); + $documents = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $person['body']['$id'] . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [ + Query::select(['fullName', 'library.*'])->toString(), + Query::equal('library', ['library1'])->toString(), + ], + ]); + + $this->assertEquals(1, $documents['body']['total']); + $this->assertEquals('Library 1', $documents['body']['documents'][0]['library']['libraryName']); + + + $person1 = $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/collections/' . $person['body']['$id'] . '/documents/'.$documentId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'library' => [ + '$id' => 'library1', + '$permissions' => [ + Permission::read(Role::users()), + Permission::update(Role::users()), + Permission::delete(Role::users()), + ], + 'libraryName' => 'Library 2', + ], + ], + 'permissions' => [ + Permission::read(Role::users()), + Permission::update(Role::users()), + Permission::delete(Role::users()), + ] + ]); + + // data should get updated + $this->assertEquals('Library 2', $person1['body']['library']['libraryName']); + $documents = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $person['body']['$id'] . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [ + Query::select(['fullName', 'library.*'])->toString(), + Query::equal('library', ['library1'])->toString(), + ], + ]); + + $this->assertEquals(1, $documents['body']['total']); + $this->assertEquals('Library 2', $documents['body']['documents'][0]['library']['libraryName']); + + + // data should get added + $person1 = $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/collections/' . $person['body']['$id'] . '/documents/'.ID::unique(), array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'library' => [ + '$id' => 'library2', + '$permissions' => [ + Permission::read(Role::users()), + Permission::update(Role::users()), + Permission::delete(Role::users()), + ], + 'libraryName' => 'Library 2', + ], + ], + 'permissions' => [ + Permission::read(Role::users()), + Permission::update(Role::users()), + Permission::delete(Role::users()), + ] + ]); + + $this->assertEquals('Library 2', $person1['body']['library']['libraryName']); + $documents = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $person['body']['$id'] . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [ + Query::select(['fullName', 'library.*'])->toString() + ], + ]); + $this->assertEquals(2, $documents['body']['total']); + } + /** * @depends testCreateDocument */ From ba3f907028877c565df05fce6d3b9124f5a17f53 Mon Sep 17 00:00:00 2001 From: Fabian Gruber Date: Wed, 21 May 2025 10:57:58 +0200 Subject: [PATCH 36/37] fix(storage): do not allow full range This prevents requests with `Range: 0-` and limits it to APP_STORAGE_READ_BUFFER --- app/controllers/api/storage.php | 60 ++++++++++++++++----------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php index a96b871bf2..4a158bb68e 100644 --- a/app/controllers/api/storage.php +++ b/app/controllers/api/storage.php @@ -996,7 +996,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') $algorithm = $file->getAttribute('algorithm', Compression::NONE); $cipher = $file->getAttribute('openSSLCipher'); $mime = $file->getAttribute('mimeType'); - if (!\in_array($mime, $inputs) || $file->getAttribute('sizeActual') > (int) System::getEnv('_APP_STORAGE_PREVIEW_LIMIT', 20000000)) { + if (!\in_array($mime, $inputs) || $file->getAttribute('sizeActual') > (int) System::getEnv('_APP_STORAGE_PREVIEW_LIMIT', APP_STORAGE_READ_BUFFER)) { if (!\in_array($mime, $inputs)) { $path = (\array_key_exists($mime, $fileLogos)) ? $fileLogos[$mime] : $fileLogos['default']; } else { @@ -1162,13 +1162,6 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download') throw new Exception(Exception::STORAGE_FILE_NOT_FOUND, 'File not found in ' . $path); } - $response - ->setContentType($file->getAttribute('mimeType')) - ->addHeader('Cache-Control', 'private, max-age=3888000') // 45 days - ->addHeader('X-Peak', \memory_get_peak_usage()) - ->addHeader('Content-Disposition', 'attachment; filename="' . $file->getAttribute('name', '') . '"') - ; - $size = $file->getAttribute('sizeOriginal', 0); $rangeHeader = $request->getHeader('range'); @@ -1177,7 +1170,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download') $end = $request->getRangeEnd(); $unit = $request->getRangeUnit(); - if ($end === null) { + if ($end === null || $end - $start > APP_STORAGE_READ_BUFFER) { $end = min(($start + MAX_OUTPUT_CHUNK_SIZE - 1), ($size - 1)); } @@ -1192,6 +1185,13 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download') ->setStatusCode(Response::STATUS_CODE_PARTIALCONTENT); } + $response + ->setContentType($file->getAttribute('mimeType')) + ->addHeader('Cache-Control', 'private, max-age=3888000') // 45 days + ->addHeader('X-Peak', \memory_get_peak_usage()) + ->addHeader('Content-Disposition', 'attachment; filename="' . $file->getAttribute('name', '') . '"') + ; + $source = ''; if (!empty($file->getAttribute('openSSLCipher'))) { // Decrypt $source = $deviceForFiles->read($path); @@ -1321,15 +1321,6 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view') $contentType = $file->getAttribute('mimeType'); } - $response - ->setContentType($contentType) - ->addHeader('Content-Security-Policy', 'script-src none;') - ->addHeader('X-Content-Type-Options', 'nosniff') - ->addHeader('Content-Disposition', 'inline; filename="' . $file->getAttribute('name', '') . '"') - ->addHeader('Cache-Control', 'private, max-age=3888000') // 45 days - ->addHeader('X-Peak', \memory_get_peak_usage()) - ; - $size = $file->getAttribute('sizeOriginal', 0); $rangeHeader = $request->getHeader('range'); @@ -1338,8 +1329,8 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view') $end = $request->getRangeEnd(); $unit = $request->getRangeUnit(); - if ($end === null) { - $end = min(($start + 2000000 - 1), ($size - 1)); + if ($end === null || $end - $start > APP_STORAGE_READ_BUFFER) { + $end = min(($start + APP_STORAGE_READ_BUFFER - 1), ($size - 1)); } if ($unit != 'bytes' || $start >= $end || $end >= $size) { @@ -1353,6 +1344,15 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view') ->setStatusCode(Response::STATUS_CODE_PARTIALCONTENT); } + $response + ->setContentType($contentType) + ->addHeader('Content-Security-Policy', 'script-src none;') + ->addHeader('X-Content-Type-Options', 'nosniff') + ->addHeader('Content-Disposition', 'inline; filename="' . $file->getAttribute('name', '') . '"') + ->addHeader('Cache-Control', 'private, max-age=3888000') // 45 days + ->addHeader('X-Peak', \memory_get_peak_usage()) + ; + $source = ''; if (!empty($file->getAttribute('openSSLCipher'))) { // Decrypt $source = $deviceForFiles->read($path); @@ -1474,14 +1474,6 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/push') $contentType = $file->getAttribute('mimeType'); } - $response - ->setContentType($contentType) - ->addHeader('Content-Security-Policy', 'script-src none;') - ->addHeader('X-Content-Type-Options', 'nosniff') - ->addHeader('Content-Disposition', 'inline; filename="' . $file->getAttribute('name', '') . '"') - ->addHeader('Cache-Control', 'private, max-age=3888000') // 45 days - ->addHeader('X-Peak', \memory_get_peak_usage()); - $size = $file->getAttribute('sizeOriginal', 0); $rangeHeader = $request->getHeader('range'); @@ -1490,8 +1482,8 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/push') $end = $request->getRangeEnd(); $unit = $request->getRangeUnit(); - if ($end === null) { - $end = min(($start + 2000000 - 1), ($size - 1)); + if ($end === null || $end - $start > APP_STORAGE_READ_BUFFER) { + $end = min(($start + APP_STORAGE_READ_BUFFER - 1), ($size - 1)); } if ($unit != 'bytes' || $start >= $end || $end >= $size) { @@ -1505,6 +1497,14 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/push') ->setStatusCode(Response::STATUS_CODE_PARTIALCONTENT); } + $response + ->setContentType($contentType) + ->addHeader('Content-Security-Policy', 'script-src none;') + ->addHeader('X-Content-Type-Options', 'nosniff') + ->addHeader('Content-Disposition', 'inline; filename="' . $file->getAttribute('name', '') . '"') + ->addHeader('Cache-Control', 'private, max-age=3888000') // 45 days + ->addHeader('X-Peak', \memory_get_peak_usage()); + $source = ''; if (!empty($file->getAttribute('openSSLCipher'))) { // Decrypt $source = $deviceForFiles->read($path); From 241a0c88e1da48032739c89dd718ddac8de753fc Mon Sep 17 00:00:00 2001 From: Fabian Gruber Date: Wed, 21 May 2025 16:21:11 +0200 Subject: [PATCH 37/37] fix: task coroutine hooks --- app/cli.php | 3 +-- composer.lock | 12 ++++++------ src/Appwrite/Platform/Tasks/ScheduleBase.php | 10 +++++----- src/Appwrite/Platform/Tasks/ScheduleFunctions.php | 2 +- src/Appwrite/Platform/Tasks/ScheduleMessages.php | 2 +- 5 files changed, 14 insertions(+), 15 deletions(-) diff --git a/app/cli.php b/app/cli.php index aaa8468ef3..7b8d6cd52f 100644 --- a/app/cli.php +++ b/app/cli.php @@ -284,6 +284,5 @@ $cli $cli->shutdown()->action(fn () => Timer::clearAll()); -// Enable coroutines, but disable TCP hooks. These don't work until we use `\Utopia\Cache\Adapter\Pool` and `\Utopia\Database\Adapter\Pool`. -Runtime::enableCoroutine(SWOOLE_HOOK_ALL ^ SWOOLE_HOOK_TCP); +Runtime::enableCoroutine(SWOOLE_HOOK_ALL); run($cli->run(...)); diff --git a/composer.lock b/composer.lock index 557b61f36c..57d627d493 100644 --- a/composer.lock +++ b/composer.lock @@ -4771,16 +4771,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "0.40.17", + "version": "0.40.18", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "7e333c1003bfd4763e4d6f3a0a799fde5e7bc4de" + "reference": "38de4b9c58112d7e83eb75955994c8412a401093" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/7e333c1003bfd4763e4d6f3a0a799fde5e7bc4de", - "reference": "7e333c1003bfd4763e4d6f3a0a799fde5e7bc4de", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/38de4b9c58112d7e83eb75955994c8412a401093", + "reference": "38de4b9c58112d7e83eb75955994c8412a401093", "shasum": "" }, "require": { @@ -4816,9 +4816,9 @@ "description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms", "support": { "issues": "https://github.com/appwrite/sdk-generator/issues", - "source": "https://github.com/appwrite/sdk-generator/tree/0.40.17" + "source": "https://github.com/appwrite/sdk-generator/tree/0.40.18" }, - "time": "2025-05-16T15:10:54+00:00" + "time": "2025-05-21T14:14:47+00:00" }, { "name": "doctrine/annotations", diff --git a/src/Appwrite/Platform/Tasks/ScheduleBase.php b/src/Appwrite/Platform/Tasks/ScheduleBase.php index 888d291a75..ea0476fc33 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleBase.php +++ b/src/Appwrite/Platform/Tasks/ScheduleBase.php @@ -79,16 +79,16 @@ abstract class ScheduleBase extends Action // start with "0" to load all active documents. $lastSyncUpdate = "0"; - $this->collectSchedules($pools, $dbForPlatform, $getProjectDB, $lastSyncUpdate); + $this->collectSchedules($dbForPlatform, $getProjectDB, $lastSyncUpdate); Console::success("Starting timers at " . DateTime::now()); /** * The timer synchronize $schedules copy with database collection. */ - Timer::tick(static::UPDATE_TIMER * 1000, function () use ($pools, $dbForPlatform, $getProjectDB, &$lastSyncUpdate) { + Timer::tick(static::UPDATE_TIMER * 1000, function () use ($dbForPlatform, $getProjectDB, &$lastSyncUpdate) { $time = DateTime::now(); Console::log("Sync tick: Running at $time"); - $this->collectSchedules($pools, $dbForPlatform, $getProjectDB, $lastSyncUpdate); + $this->collectSchedules($dbForPlatform, $getProjectDB, $lastSyncUpdate); }); while (true) { @@ -103,7 +103,7 @@ abstract class ScheduleBase extends Action } } - private function collectSchedules(Group $pools, Database $dbForPlatform, callable $getProjectDB, string &$lastSyncUpdate): void + private function collectSchedules(Database $dbForPlatform, callable $getProjectDB, string &$lastSyncUpdate): void { // If we haven't synced yet, load all active schedules $initialLoad = $lastSyncUpdate === "0"; @@ -115,7 +115,7 @@ abstract class ScheduleBase extends Action * @throws Exception * @var Document $schedule */ - $getSchedule = function (Document $schedule) use ($pools, $dbForPlatform, $getProjectDB): array { + $getSchedule = function (Document $schedule) use ($dbForPlatform, $getProjectDB): array { $project = $dbForPlatform->getDocument('projects', $schedule->getAttribute('projectId')); $resource = $getProjectDB($project)->getDocument( diff --git a/src/Appwrite/Platform/Tasks/ScheduleFunctions.php b/src/Appwrite/Platform/Tasks/ScheduleFunctions.php index 19e068107a..7812b27832 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleFunctions.php +++ b/src/Appwrite/Platform/Tasks/ScheduleFunctions.php @@ -76,7 +76,7 @@ class ScheduleFunctions extends ScheduleBase } foreach ($delayedExecutions as $delay => $schedules) { - \go(function () use ($delay, $schedules, $pools, $dbForPlatform) { + \go(function () use ($delay, $schedules, $dbForPlatform) { \sleep($delay); // in seconds foreach ($schedules as $delayConfig) { diff --git a/src/Appwrite/Platform/Tasks/ScheduleMessages.php b/src/Appwrite/Platform/Tasks/ScheduleMessages.php index 5e65f7a8a6..d23e3de575 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleMessages.php +++ b/src/Appwrite/Platform/Tasks/ScheduleMessages.php @@ -40,7 +40,7 @@ class ScheduleMessages extends ScheduleBase continue; } - \go(function () use ($schedule, $scheduledAt, $pools, $dbForPlatform) { + \go(function () use ($schedule, $scheduledAt, $dbForPlatform) { $queueForMessaging = new Messaging($this->publisher); $this->updateProjectAccess($schedule['project'], $dbForPlatform);