diff --git a/CHANGES.md b/CHANGES.md index e6dd04b556..548c0d72b0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,100 +1,133 @@ -# Version 1.8.1 +# Version 1.9.0 ## What's Changed ### Notable changes -* Add branch deployments support in [#10486](https://github.com/appwrite/appwrite/pull/10486) -* Add TanStack Start sites support in [#10681](https://github.com/appwrite/appwrite/pull/10681) -* Add Next.js standalone support in [#10747](https://github.com/appwrite/appwrite/pull/10747) -* Add Resend integration in [#10690](https://github.com/appwrite/appwrite/pull/10690) -* Add option to enable/disable image transformations per-bucket in [#10722](https://github.com/appwrite/appwrite/pull/10722) -* Add operators support in [#10735](https://github.com/appwrite/appwrite/pull/10735) and [#10800](https://github.com/appwrite/appwrite/pull/10800) -* Add function and sites stats in [#10786](https://github.com/appwrite/appwrite/pull/10786) -* Add disable count feature in [#10668](https://github.com/appwrite/appwrite/pull/10668) -* Add ElevenLabs site template in [#10782](https://github.com/appwrite/appwrite/pull/10782) -* Add suggested environment variables in [#10795](https://github.com/appwrite/appwrite/pull/10795) -* Update GeoDB database in [#10890](https://github.com/appwrite/appwrite/pull/10890) -* Update Flutter default build runtime in [#10807](https://github.com/appwrite/appwrite/pull/10807) -* Upgrade runtimes in [#10804](https://github.com/appwrite/appwrite/pull/10804) +* Add PostgreSQL database adapter in [#9772](https://github.com/appwrite/appwrite/pull/9772) and [#11293](https://github.com/appwrite/appwrite/pull/11293) +* Add MongoDB support in [#11312](https://github.com/appwrite/appwrite/pull/11312) +* Add new webhooks API in [#11033](https://github.com/appwrite/appwrite/pull/11033) and [#11566](https://github.com/appwrite/appwrite/pull/11566) +* Add schedules API endpoints in [#11331](https://github.com/appwrite/appwrite/pull/11331) +* Add project labels in [#11056](https://github.com/appwrite/appwrite/pull/11056) and project status attribute in [#11291](https://github.com/appwrite/appwrite/pull/11291) +* Add resource-based API key structure in [#11003](https://github.com/appwrite/appwrite/pull/11003) with custom ID support in [#11277](https://github.com/appwrite/appwrite/pull/11277) and list queries in [#11278](https://github.com/appwrite/appwrite/pull/11278) +* Add string types (varchar, text, mediumtext, longtext) for attributes in [#11174](https://github.com/appwrite/appwrite/pull/11174) +* Add encrypt parameter to string attribute types in [#11334](https://github.com/appwrite/appwrite/pull/11334) +* Add int64 format support for integer attributes in [#11123](https://github.com/appwrite/appwrite/pull/11123) +* Add collection and row storage size in [#11254](https://github.com/appwrite/appwrite/pull/11254) and [#11069](https://github.com/appwrite/appwrite/pull/11069) +* Add totalSize on list responses in [#11102](https://github.com/appwrite/appwrite/pull/11102) +* Add custom start command for sites and functions in [#10842](https://github.com/appwrite/appwrite/pull/10842) +* Add separate build/runtime specifications in [#10849](https://github.com/appwrite/appwrite/pull/10849) +* Add deployment retention for sites and functions in [#10959](https://github.com/appwrite/appwrite/pull/10959) +* Add auto-delete old deployments in [#10959](https://github.com/appwrite/appwrite/pull/10959) +* Add custom JWT duration in [#11009](https://github.com/appwrite/appwrite/pull/11009) +* Add multiple application domains support in [#10911](https://github.com/appwrite/appwrite/pull/10911) +* Add GraphQL introspection in [#11159](https://github.com/appwrite/appwrite/pull/11159) +* Add realtime query subscriptions in [#11202](https://github.com/appwrite/appwrite/pull/11202) and [#11237](https://github.com/appwrite/appwrite/pull/11237) +* Add realtime metrics for connections, messages, and bandwidth in [#11438](https://github.com/appwrite/appwrite/pull/11438) and [#11488](https://github.com/appwrite/appwrite/pull/11488) +* Add messaging resource migration support in [#11495](https://github.com/appwrite/appwrite/pull/11495) +* Add cached documents list in [#10832](https://github.com/appwrite/appwrite/pull/10832) +* Add project queries support in [#10990](https://github.com/appwrite/appwrite/pull/10990) +* Add batch document creation in [#10894](https://github.com/appwrite/appwrite/pull/10894) +* Add async screenshots in [#11110](https://github.com/appwrite/appwrite/pull/11110) +* Add new file parameters (encryption, compression) in [#11135](https://github.com/appwrite/appwrite/pull/11135) +* Add new site templates in [#10031](https://github.com/appwrite/appwrite/pull/10031) +* Add VCS repository authorized field in [#11421](https://github.com/appwrite/appwrite/pull/11421) +* Add trusted console projects in [#11248](https://github.com/appwrite/appwrite/pull/11248) + +### Refactoring + +* Refactor to Utopia Platform modules architecture in [#11035](https://github.com/appwrite/appwrite/pull/11035), [#11049](https://github.com/appwrite/appwrite/pull/11049), [#11057](https://github.com/appwrite/appwrite/pull/11057), [#11103](https://github.com/appwrite/appwrite/pull/11103), [#11208](https://github.com/appwrite/appwrite/pull/11208), and [#11398](https://github.com/appwrite/appwrite/pull/11398) +* Refactor auth to single instance in [#10872](https://github.com/appwrite/appwrite/pull/10872) and [#11130](https://github.com/appwrite/appwrite/pull/11130) +* Refactor usage metrics to stateless publisher pattern in [#11449](https://github.com/appwrite/appwrite/pull/11449) +* Refactor messaging and queue in [#10961](https://github.com/appwrite/appwrite/pull/10961) +* Refactor functions schedule in [#10913](https://github.com/appwrite/appwrite/pull/10913) +* Refactor make Bus dispatch synchronous in [#11449](https://github.com/appwrite/appwrite/pull/11449) +* Remove proxy container in [#11039](https://github.com/appwrite/appwrite/pull/11039) + +### Performance + +* Optimize updateDocument() calls to use sparse documents in [#11465](https://github.com/appwrite/appwrite/pull/11465) +* Optimize Dockerfile in [#10947](https://github.com/appwrite/appwrite/pull/10947) +* Improve domain caching in [#11346](https://github.com/appwrite/appwrite/pull/11346) +* Improve memory usage in [#11345](https://github.com/appwrite/appwrite/pull/11345) +* Fix memory leak in [#11067](https://github.com/appwrite/appwrite/pull/11067) and [#11241](https://github.com/appwrite/appwrite/pull/11241) +* Improve realtime performance in [#11251](https://github.com/appwrite/appwrite/pull/11251) +* Enable SMTP keep-alive to reuse connections across mail jobs in [#11496](https://github.com/appwrite/appwrite/pull/11496) ### Fixes -* Fix duplicate document error while creating file in [#10891](https://github.com/appwrite/appwrite/pull/10891) -* Fix "Update external deployment (authorize)" throwing 500 error due to invalid query in [#10888](https://github.com/appwrite/appwrite/pull/10888) -* Fix error setting user password in [#10889](https://github.com/appwrite/appwrite/pull/10889) -* Fix error generating email MFA challenges in [#10884](https://github.com/appwrite/appwrite/pull/10884) -* Fix file token expiry in [#10877](https://github.com/appwrite/appwrite/pull/10877) -* Fix TanStack Nitro default in [#10860](https://github.com/appwrite/appwrite/pull/10860) -* Fix TanStack builds in [#10767](https://github.com/appwrite/appwrite/pull/10767) -* Fix nullable validation in [#10819](https://github.com/appwrite/appwrite/pull/10819) and [#10778](https://github.com/appwrite/appwrite/pull/10778) -* Fix WebP library in [#10738](https://github.com/appwrite/appwrite/pull/10738) -* Fix batch writes in [#10812](https://github.com/appwrite/appwrite/pull/10812) -* Fix error handler error in [#10719](https://github.com/appwrite/appwrite/pull/10719) -* Fix Next 16 compatibility in [#10713](https://github.com/appwrite/appwrite/pull/10713) -* Fix stats usage memory leak in [#10683](https://github.com/appwrite/appwrite/pull/10683) -* Fix author URL in template deployments in [#10535](https://github.com/appwrite/appwrite/pull/10535) -* Fix VCS lock deletion in [#10691](https://github.com/appwrite/appwrite/pull/10691) +* Fix blocked user/resource errors from 401 to 403 in [#11469](https://github.com/appwrite/appwrite/pull/11469) +* Fix OAuth for custom domains in [#10967](https://github.com/appwrite/appwrite/pull/10967) and [#11269](https://github.com/appwrite/appwrite/pull/11269) +* Fix OAuth redirect custom scheme in [#11292](https://github.com/appwrite/appwrite/pull/11292) +* Fix OAuth verified emails in [#10986](https://github.com/appwrite/appwrite/pull/10986) +* Fix MFA recovery code validation in [#10925](https://github.com/appwrite/appwrite/pull/10925) +* Fix users allow updating phone number to empty in [#11521](https://github.com/appwrite/appwrite/pull/11521) +* Fix users optional name error in [#11413](https://github.com/appwrite/appwrite/pull/11413) +* Fix file permissions in [#11026](https://github.com/appwrite/appwrite/pull/11026) +* Fix bulk insert webhook validation in [#11022](https://github.com/appwrite/appwrite/pull/11022) +* Fix execution status update in [#11134](https://github.com/appwrite/appwrite/pull/11134) +* Fix execution timeout status in [#11400](https://github.com/appwrite/appwrite/pull/11400) +* Fix CORS wildcard in [#10956](https://github.com/appwrite/appwrite/pull/10956) +* Fix preflight requests in [#10943](https://github.com/appwrite/appwrite/pull/10943) +* Fix SMTP auth check in [#10939](https://github.com/appwrite/appwrite/pull/10939) +* Fix scheduled executions trigger in [#10922](https://github.com/appwrite/appwrite/pull/10922) +* Fix schedule executions bug in [#10916](https://github.com/appwrite/appwrite/pull/10916) +* Fix deployment enum missing canceled value in [#11179](https://github.com/appwrite/appwrite/pull/11179) +* Fix invalid chunk total in [#11270](https://github.com/appwrite/appwrite/pull/11270) +* Fix sites domains in [#11240](https://github.com/appwrite/appwrite/pull/11240) and [#11355](https://github.com/appwrite/appwrite/pull/11355) +* Fix rule domains in [#11355](https://github.com/appwrite/appwrite/pull/11355) and [#11276](https://github.com/appwrite/appwrite/pull/11276) +* Fix rules deletion in [#11575](https://github.com/appwrite/appwrite/pull/11575) +* Fix VCS template flow in [#11275](https://github.com/appwrite/appwrite/pull/11275) +* Fix VCS comment empty in [#11490](https://github.com/appwrite/appwrite/pull/11490) +* Fix DSN VCS error in [#11364](https://github.com/appwrite/appwrite/pull/11364) +* Fix email URL params encoding in [#11369](https://github.com/appwrite/appwrite/pull/11369) +* Fix missing email warning in [#11378](https://github.com/appwrite/appwrite/pull/11378) +* Fix race condition in builds worker in [#11336](https://github.com/appwrite/appwrite/pull/11336) +* Fix realtime regions in [#11414](https://github.com/appwrite/appwrite/pull/11414) +* Fix realtime errors in [#11573](https://github.com/appwrite/appwrite/pull/11573) +* Fix realtime TablesDB channels in [#11404](https://github.com/appwrite/appwrite/pull/11404) and [#11430](https://github.com/appwrite/appwrite/pull/11430) +* Fix database shared table reconciliation in [#11578](https://github.com/appwrite/appwrite/pull/11578) +* Fix PostgreSQL race condition in shared mode project creation in [#11536](https://github.com/appwrite/appwrite/pull/11536) +* Fix compression enabled env in [#11171](https://github.com/appwrite/appwrite/pull/11171) +* Fix deletes bug in [#10965](https://github.com/appwrite/appwrite/pull/10965) +* Fix devkey scopes in [#10984](https://github.com/appwrite/appwrite/pull/10984) +* Fix phone auth limit in [#11143](https://github.com/appwrite/appwrite/pull/11143) +* Fix relationship document ID validation in [#11193](https://github.com/appwrite/appwrite/pull/11193) +* Fix stale project overwrites OAuth in [#11461](https://github.com/appwrite/appwrite/pull/11461) +* Fix storage health error swallowing in [#11492](https://github.com/appwrite/appwrite/pull/11492) +* Fix Origin validator type error in [#11297](https://github.com/appwrite/appwrite/pull/11297) +* Fix getScreenshot image format in [#11017](https://github.com/appwrite/appwrite/pull/11017) +* Fix migration error handling in [#11457](https://github.com/appwrite/appwrite/pull/11457) +* Fix deprecation warnings in [#11227](https://github.com/appwrite/appwrite/pull/11227) + +### Installer + +* New installer UI in [#11175](https://github.com/appwrite/appwrite/pull/11175) and [#11247](https://github.com/appwrite/appwrite/pull/11247) ### Miscellaneous -* Add CSV export functionality in [#10546](https://github.com/appwrite/appwrite/pull/10546), [#10750](https://github.com/appwrite/appwrite/pull/10750), [#10813](https://github.com/appwrite/appwrite/pull/10813), and [#10847](https://github.com/appwrite/appwrite/pull/10847) -* Add JWT disposition in [#10867](https://github.com/appwrite/appwrite/pull/10867) -* Add screenshots endpoint in [#10675](https://github.com/appwrite/appwrite/pull/10675) -* Add screenshot endpoint stats in [#10706](https://github.com/appwrite/appwrite/pull/10706) -* Add users attributes in [#10688](https://github.com/appwrite/appwrite/pull/10688) -* Add max build duration environment variable in [#10674](https://github.com/appwrite/appwrite/pull/10674) -* Add custom realtime logger in [#10871](https://github.com/appwrite/appwrite/pull/10871) -* Add logs in [#10869](https://github.com/appwrite/appwrite/pull/10869) -* Improve MFA docs endpoint order in [#10793](https://github.com/appwrite/appwrite/pull/10793) -* Auth refactor in [#10758](https://github.com/appwrite/appwrite/pull/10758), [#10837](https://github.com/appwrite/appwrite/pull/10837), [#10682](https://github.com/appwrite/appwrite/pull/10682), and [#10667](https://github.com/appwrite/appwrite/pull/10667) -* Bump assistant to 0.8.4 in [#10887](https://github.com/appwrite/appwrite/pull/10887) -* Bump database to 3.1.5 in [#10766](https://github.com/appwrite/appwrite/pull/10766) -* Bump Utopia DNS in [#10761](https://github.com/appwrite/appwrite/pull/10761) -* Update domains to 0.8.3 in [#10658](https://github.com/appwrite/appwrite/pull/10658) -* Update domains to 0.9.1 in [#10678](https://github.com/appwrite/appwrite/pull/10678) -* Update Apple Swift to 13.3.0 in [#10679](https://github.com/appwrite/appwrite/pull/10679) -* Update Apple Swift in [#10663](https://github.com/appwrite/appwrite/pull/10663) -* Update CLI to 10.2.2 in [#10672](https://github.com/appwrite/appwrite/pull/10672) -* Update to CLI 12.0.0 in [#10853](https://github.com/appwrite/appwrite/pull/10853) -* Update docs examples to use Permission class in [#10707](https://github.com/appwrite/appwrite/pull/10707) -* Update SDK examples docs in [#10855](https://github.com/appwrite/appwrite/pull/10855) -* Release Python SDK in [#10762](https://github.com/appwrite/appwrite/pull/10762) -* Release Flutter 20.3.2 in [#10838](https://github.com/appwrite/appwrite/pull/10838) -* Release Flutter/Dart add screenshot examples in [#10811](https://github.com/appwrite/appwrite/pull/10811) -* Release PHP CLI in [#10791](https://github.com/appwrite/appwrite/pull/10791) -* Release SDKs in [#10817](https://github.com/appwrite/appwrite/pull/10817) -* Update SDKs in [#10694](https://github.com/appwrite/appwrite/pull/10694), [#10729](https://github.com/appwrite/appwrite/pull/10729), and [#10744](https://github.com/appwrite/appwrite/pull/10744) -* Update SDK generator in [#10743](https://github.com/appwrite/appwrite/pull/10743) -* Update database in [#10664](https://github.com/appwrite/appwrite/pull/10664) -* Update README file in [#10763](https://github.com/appwrite/appwrite/pull/10763) -* SDK release documentation in [#10745](https://github.com/appwrite/appwrite/pull/10745) -* SDK release runtime config in [#10765](https://github.com/appwrite/appwrite/pull/10765) -* Sync specs in [#10789](https://github.com/appwrite/appwrite/pull/10789) -* Sync 1.8.0 in [#10677](https://github.com/appwrite/appwrite/pull/10677) -* Add workflow for issue triage in [#10718](https://github.com/appwrite/appwrite/pull/10718) -* Add issue auto-labeler in [#10700](https://github.com/appwrite/appwrite/pull/10700) -* Add AI moderator repo in [#10717](https://github.com/appwrite/appwrite/pull/10717) -* Browser bump in [#10850](https://github.com/appwrite/appwrite/pull/10850) -* Template type enum override in [#10848](https://github.com/appwrite/appwrite/pull/10848) -* VCS reference type in [#10852](https://github.com/appwrite/appwrite/pull/10852) -* Index scope description in [#10851](https://github.com/appwrite/appwrite/pull/10851) -* Config for environment in [#10833](https://github.com/appwrite/appwrite/pull/10833) -* Format instance in [#10830](https://github.com/appwrite/appwrite/pull/10830) -* Replace sleep in webhooks service in [#10656](https://github.com/appwrite/appwrite/pull/10656) -* Update email composer in [#10720](https://github.com/appwrite/appwrite/pull/10720) -* Update facts on GitHub sites and functions in [#10593](https://github.com/appwrite/appwrite/pull/10593) and [#10771](https://github.com/appwrite/appwrite/pull/10771) -* Fix wrong user type in [#10875](https://github.com/appwrite/appwrite/pull/10875) -* Fix limit and offset computation in [#10880](https://github.com/appwrite/appwrite/pull/10880) -* Fix enum examples in [#10828](https://github.com/appwrite/appwrite/pull/10828) -* Fix response models multi-methods in [#10815](https://github.com/appwrite/appwrite/pull/10815) -* Fix undefined variable in [#10654](https://github.com/appwrite/appwrite/pull/10654) -* Fix undefined sequence in [#10652](https://github.com/appwrite/appwrite/pull/10652) -* Fix description in [#10702](https://github.com/appwrite/appwrite/pull/10702) -* Fix warning in builds worker in [#10705](https://github.com/appwrite/appwrite/pull/10705) -* Fix sites create deployment docs in [#10566](https://github.com/appwrite/appwrite/pull/10566) -* Fix test dependencies projects in [#10655](https://github.com/appwrite/appwrite/pull/10655) -* Fix list sites test in [#10726](https://github.com/appwrite/appwrite/pull/10726) +* Add audits upgrade in [#10953](https://github.com/appwrite/appwrite/pull/10953) +* Add graceful workers shutdown in [#11104](https://github.com/appwrite/appwrite/pull/11104) +* Add pool resilience in [#11139](https://github.com/appwrite/appwrite/pull/11139) +* Add function queue job TTL in [#11226](https://github.com/appwrite/appwrite/pull/11226) +* Add cleanup stale executions in [#11146](https://github.com/appwrite/appwrite/pull/11146) +* Add success abuse reset in [#11085](https://github.com/appwrite/appwrite/pull/11085) +* Add SMTP connection validation in [#11079](https://github.com/appwrite/appwrite/pull/11079) +* Add allow custom email sender in [#10945](https://github.com/appwrite/appwrite/pull/10945) +* Add array domains env support in [#11213](https://github.com/appwrite/appwrite/pull/11213) +* Add file create after success hook in [#11054](https://github.com/appwrite/appwrite/pull/11054) +* Add delete subscribers in [#11115](https://github.com/appwrite/appwrite/pull/11115) +* Add cursor plugin in [#11371](https://github.com/appwrite/appwrite/pull/11371) +* Add observability spans in [#11320](https://github.com/appwrite/appwrite/pull/11320), [#11306](https://github.com/appwrite/appwrite/pull/11306), and [#11228](https://github.com/appwrite/appwrite/pull/11228) +* Upgrade PHPStan to v2 with full codebase coverage in [#11550](https://github.com/appwrite/appwrite/pull/11550) +* Upgrade Traefik in [#11265](https://github.com/appwrite/appwrite/pull/11265) +* Upgrade utopia-php/queue in [#11239](https://github.com/appwrite/appwrite/pull/11239) +* Upgrade spomky-labs/otphp in [#11263](https://github.com/appwrite/appwrite/pull/11263) +* Bump utopia-php/database to stable 5.3.15 in [#11573](https://github.com/appwrite/appwrite/pull/11573) +* Bump utopia-php/migration to 1.6.3 in [#11443](https://github.com/appwrite/appwrite/pull/11443) +* Consolidate CI workflows in [#11531](https://github.com/appwrite/appwrite/pull/11531) and [#11551](https://github.com/appwrite/appwrite/pull/11551) +* Hide deprecated methods from docs in [#10933](https://github.com/appwrite/appwrite/pull/10933) +* Deprecate project-level attributes in [#11203](https://github.com/appwrite/appwrite/pull/11203) # Version 1.8.0 diff --git a/README-CN.md b/README-CN.md index afd7eca289..212b5bb08d 100644 --- a/README-CN.md +++ b/README-CN.md @@ -72,7 +72,7 @@ docker run -it --rm \ --volume /var/run/docker.sock:/var/run/docker.sock \ --volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \ --entrypoint="install" \ - appwrite/appwrite:1.8.1 + appwrite/appwrite:1.9.0 ``` ### Windows @@ -84,7 +84,7 @@ docker run -it --rm ^ --volume //var/run/docker.sock:/var/run/docker.sock ^ --volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^ --entrypoint="install" ^ - appwrite/appwrite:1.8.1 + appwrite/appwrite:1.9.0 ``` #### PowerShell @@ -94,7 +94,7 @@ docker run -it --rm ` --volume /var/run/docker.sock:/var/run/docker.sock ` --volume ${pwd}/appwrite:/usr/src/code/appwrite:rw ` --entrypoint="install" ` - appwrite/appwrite:1.8.1 + appwrite/appwrite:1.9.0 ``` 运行后,可以在浏览器上访问 http://localhost 找到 Appwrite 控制台。在非 Linux 的本机主机上完成安装后,服务器可能需要几分钟才能启动。 diff --git a/README.md b/README.md index 4a71579207..457863d236 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ docker run -it --rm \ --volume /var/run/docker.sock:/var/run/docker.sock \ --volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \ --entrypoint="install" \ - appwrite/appwrite:1.8.1 + appwrite/appwrite:1.9.0 ``` ### Windows @@ -87,7 +87,7 @@ docker run -it --rm ^ --volume //var/run/docker.sock:/var/run/docker.sock ^ --volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^ --entrypoint="install" ^ - appwrite/appwrite:1.8.1 + appwrite/appwrite:1.9.0 ``` #### PowerShell @@ -97,7 +97,7 @@ docker run -it --rm ` --volume /var/run/docker.sock:/var/run/docker.sock ` --volume ${pwd}/appwrite:/usr/src/code/appwrite:rw ` --entrypoint="install" ` - appwrite/appwrite:1.8.1 + appwrite/appwrite:1.9.0 ``` Once the Docker installation is complete, go to http://localhost to access the Appwrite console from your browser. Please note that on non-Linux native hosts, the server might take a few minutes to start after completing the installation. diff --git a/app/controllers/general.php b/app/controllers/general.php index 341ac59f7a..1a099c4bde 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -888,7 +888,7 @@ Http::init() $dbForProject = $getProjectDB($project); $request->addFilter(new RequestV20($dbForProject, $route->getPathValues($request))); } - if (version_compare($requestFormat, '1.8.2', '<')) { + if (version_compare($requestFormat, '1.9.0', '<')) { $request->addFilter(new RequestV21()); } } diff --git a/app/init/constants.php b/app/init/constants.php index fe61316955..8fdd6d1a51 100644 --- a/app/init/constants.php +++ b/app/init/constants.php @@ -47,7 +47,7 @@ const APP_RESOURCE_TOKEN_ACCESS = 24 * 60 * 60; // 24 hours const APP_FILE_ACCESS = 24 * 60 * 60; // 24 hours const APP_CACHE_UPDATE = 24 * 60 * 60; // 24 hours const APP_CACHE_BUSTER = 4321; -const APP_VERSION_STABLE = '1.8.2'; +const APP_VERSION_STABLE = '1.9.0'; const APP_DATABASE_ATTRIBUTE_EMAIL = 'email'; const APP_DATABASE_ATTRIBUTE_ENUM = 'enum'; const APP_DATABASE_ATTRIBUTE_IP = 'ip'; diff --git a/composer.json b/composer.json index 0ac1a7912c..607cf83f9a 100644 --- a/composer.json +++ b/composer.json @@ -108,6 +108,12 @@ "czproject/git-php": "4.*", "laravel/pint": "1.*" }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/utopia-php/database" + } + ], "provide": { "ext-phpiredis": "*" }, diff --git a/composer.lock b/composer.lock index ab6258fbfb..9308f93cd5 100644 --- a/composer.lock +++ b/composer.lock @@ -3850,16 +3850,16 @@ }, { "name": "utopia-php/database", - "version": "5.3.16", + "version": "5.3.17", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "f121418c4dc7169576735620fd8c17093c3b851d" + "reference": "cff2b6ed63d3291b74110d086e16ff089fe05993" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/f121418c4dc7169576735620fd8c17093c3b851d", - "reference": "f121418c4dc7169576735620fd8c17093c3b851d", + "url": "https://api.github.com/repos/utopia-php/database/zipball/cff2b6ed63d3291b74110d086e16ff089fe05993", + "reference": "cff2b6ed63d3291b74110d086e16ff089fe05993", "shasum": "" }, "require": { @@ -3902,10 +3902,10 @@ "utopia" ], "support": { - "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/5.3.16" + "source": "https://github.com/utopia-php/database/tree/5.3.17", + "issues": "https://github.com/utopia-php/database/issues" }, - "time": "2026-03-19T10:10:02+00:00" + "time": "2026-03-20T01:18:52+00:00" }, { "name": "utopia-php/detector", @@ -5438,16 +5438,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "1.11.10", + "version": "1.11.11", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "96e6b79a241fc615627a820107ca64bbd1b550a6" + "reference": "cfc37c85161a5515af4cd2f9885a811f51a2483a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/96e6b79a241fc615627a820107ca64bbd1b550a6", - "reference": "96e6b79a241fc615627a820107ca64bbd1b550a6", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/cfc37c85161a5515af4cd2f9885a811f51a2483a", + "reference": "cfc37c85161a5515af4cd2f9885a811f51a2483a", "shasum": "" }, "require": { @@ -5483,9 +5483,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/1.11.10" + "source": "https://github.com/appwrite/sdk-generator/tree/1.11.11" }, - "time": "2026-03-19T05:27:36+00:00" + "time": "2026-03-19T16:21:03+00:00" }, { "name": "brianium/paratest", diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index ee9e50ef62..b2e6c99bb8 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -918,18 +918,6 @@ parameters: count: 1 path: src/Appwrite/Platform/Tasks/StatsResources.php - - - message: '#^Call to static method error\(\) on an unknown class Utopia\\CLI\\Console\.$#' - identifier: class.notFound - count: 1 - path: src/Appwrite/Platform/Tasks/Upgrade.php - - - - message: '#^Call to static method log\(\) on an unknown class Utopia\\CLI\\Console\.$#' - identifier: class.notFound - count: 5 - path: src/Appwrite/Platform/Tasks/Upgrade.php - - message: '#^PHPDoc tag @param references unknown parameter\: \$getProjectDB$#' identifier: parameter.notFound diff --git a/src/Appwrite/Migration/Migration.php b/src/Appwrite/Migration/Migration.php index 8fe35608be..a4f73eb5f2 100644 --- a/src/Appwrite/Migration/Migration.php +++ b/src/Appwrite/Migration/Migration.php @@ -91,7 +91,7 @@ abstract class Migration '1.7.4' => 'V22', '1.8.0' => 'V23', '1.8.1' => 'V23', - '1.8.2' => 'V23', + '1.9.0' => 'V24', ]; /** diff --git a/src/Appwrite/Migration/Version/V24.php b/src/Appwrite/Migration/Version/V24.php new file mode 100644 index 0000000000..dc9ce9d196 --- /dev/null +++ b/src/Appwrite/Migration/Version/V24.php @@ -0,0 +1,396 @@ + null, + fn () => [] + ); + } + + Console::info('Migrating collections'); + $this->migrateCollections(); + + if ($this->project->getSequence() != 'console') { + Console::info('Migrating Databases'); + $this->migrateDatabases(); + } + + Console::info('Migrating Buckets'); + $this->migrateBuckets(); + + Console::info('Migrating documents'); + $this->forEachDocument($this->migrateDocument(...)); + + Console::info('Cleaning up old attributes'); + $this->cleanupOldAttributes(); + } + + /** + * Migrate Collections. + * + * @return void + * @throws Exception|Throwable + */ + private function migrateCollections(): void + { + $projectInternalId = $this->project->getSequence(); + + if (empty($projectInternalId)) { + throw new Exception('Project ID is null'); + } + + $collectionType = match ($projectInternalId) { + 'console' => 'console', + default => 'projects', + }; + + $collections = $this->collections[$collectionType]; + + foreach ($collections as $collection) { + $id = $collection['$id']; + + if (empty($id)) { + continue; + } + + Console::log("Migrating collection \"{$id}\""); + + $this->dbForProject->purgeCachedCollection($id); + $this->dbForProject->purgeCachedDocument(Database::METADATA, $id); + + switch ($id) { + case 'projects': + if ($collectionType === 'console') { + $attributes = [ + 'labels', + 'status', + ]; + try { + $this->createAttributesFromCollection($this->dbForProject, $id, $attributes); + } catch (Throwable $th) { + Console::warning('Failed to create attributes "' . \implode(', ', $attributes) . "\" in collection {$id}: {$th->getMessage()}"); + } + } + $this->dbForProject->purgeCachedCollection($id); + break; + + case 'keys': + if ($collectionType === 'console') { + $attributes = [ + 'resourceType', + 'resourceId', + 'resourceInternalId', + ]; + try { + $this->createAttributesFromCollection($this->dbForProject, $id, $attributes); + } catch (Throwable $th) { + Console::warning('Failed to create attributes "' . \implode(', ', $attributes) . "\" in collection {$id}: {$th->getMessage()}"); + } + + try { + $this->dbForProject->deleteIndex($id, '_key_project'); + } catch (Throwable $th) { + Console::warning("Failed to delete index \"_key_project\" from {$id}: {$th->getMessage()}"); + } + + try { + $this->createIndexFromCollection($this->dbForProject, $id, '_key_resource'); + } catch (Throwable $th) { + Console::warning("Failed to create index \"_key_resource\" from {$id}: {$th->getMessage()}"); + } + } + $this->dbForProject->purgeCachedCollection($id); + break; + + case 'rules': + if ($collectionType === 'console') { + try { + $this->createAttributeFromCollection($this->dbForProject, $id, 'logs'); + } catch (Throwable $th) { + Console::warning("Failed to create attribute \"logs\" in collection {$id}: {$th->getMessage()}"); + } + + $indexesToDelete = [ + '_key_type', + '_key_trigger', + '_key_deploymentResourceType', + '_key_owner', + '_key_region', + '_key_piid_riid_rt', + ]; + foreach ($indexesToDelete as $index) { + try { + $this->dbForProject->deleteIndex($id, $index); + } catch (Throwable $th) { + Console::warning("Failed to delete index \"$index\" from {$id}: {$th->getMessage()}"); + } + } + + $indexesToCreate = [ + '_key_type', + '_key_trigger', + '_key_deploymentResourceType', + '_key_owner', + '_key_piid_diid_drt', + '_key_region_status_createdAt', + ]; + foreach ($indexesToCreate as $index) { + try { + $this->createIndexFromCollection($this->dbForProject, $id, $index); + } catch (Throwable $th) { + Console::warning("Failed to create index \"$index\" from {$id}: {$th->getMessage()}"); + } + } + } + $this->dbForProject->purgeCachedCollection($id); + break; + + case 'teams': + try { + $this->createAttributeFromCollection($this->dbForProject, $id, 'labels'); + } catch (Throwable $th) { + Console::warning("Failed to create attribute \"labels\" in collection {$id}: {$th->getMessage()}"); + } + $this->dbForProject->purgeCachedCollection($id); + break; + + case 'databases': + if ($collectionType === 'projects') { + try { + $this->createAttributeFromCollection($this->dbForProject, $id, 'database'); + } catch (Throwable $th) { + Console::warning("Failed to create attribute \"database\" in collection {$id}: {$th->getMessage()}"); + } + $this->dbForProject->purgeCachedCollection($id); + } + break; + + case 'functions': + $attributes = [ + 'deploymentRetention', + 'startCommand', + 'buildSpecification', + 'runtimeSpecification', + ]; + try { + $this->createAttributesFromCollection($this->dbForProject, $id, $attributes); + } catch (Throwable $th) { + Console::warning('Failed to create attributes "' . \implode(', ', $attributes) . "\" in collection {$id}: {$th->getMessage()}"); + } + $this->dbForProject->purgeCachedCollection($id); + break; + + case 'sites': + $attributes = [ + 'startCommand', + 'deploymentRetention', + 'buildSpecification', + 'runtimeSpecification', + ]; + try { + $this->createAttributesFromCollection($this->dbForProject, $id, $attributes); + } catch (Throwable $th) { + Console::warning('Failed to create attributes "' . \implode(', ', $attributes) . "\" in collection {$id}: {$th->getMessage()}"); + } + $this->dbForProject->purgeCachedCollection($id); + break; + + case 'deployments': + try { + $this->createAttributeFromCollection($this->dbForProject, $id, 'startCommand'); + } catch (Throwable $th) { + Console::warning("Failed to create attribute \"startCommand\" in collection {$id}: {$th->getMessage()}"); + } + $this->dbForProject->purgeCachedCollection($id); + break; + + case 'executions': + try { + $this->dbForProject->deleteIndex($id, '_key_function_internal_id'); + } catch (Throwable $th) { + Console::warning("Failed to delete index \"_key_function_internal_id\" from {$id}: {$th->getMessage()}"); + } + try { + $this->createIndexFromCollection($this->dbForProject, $id, '_key_resourceType'); + } catch (Throwable $th) { + Console::warning("Failed to create index \"_key_resourceType\" from {$id}: {$th->getMessage()}"); + } + $this->dbForProject->purgeCachedCollection($id); + break; + + case 'buckets': + try { + $this->dbForProject->deleteIndex($id, '_fulltext_name'); + } catch (Throwable $th) { + Console::warning("Failed to delete index \"_fulltext_name\" from {$id}: {$th->getMessage()}"); + } + $this->dbForProject->purgeCachedCollection($id); + break; + + case 'providers': + try { + $this->dbForProject->deleteIndex($id, '_key_name'); + } catch (Throwable $th) { + Console::warning("Failed to delete index \"_key_name\" from {$id}: {$th->getMessage()}"); + } + $this->dbForProject->purgeCachedCollection($id); + break; + + case 'topics': + try { + $this->dbForProject->deleteIndex($id, '_key_name'); + } catch (Throwable $th) { + Console::warning("Failed to delete index \"_key_name\" from {$id}: {$th->getMessage()}"); + } + $this->dbForProject->purgeCachedCollection($id); + break; + + default: + break; + } + } + } + + /** + * Migrate all Database tables + * + * @return void + * @throws Exception + */ + private function migrateDatabases(): void + { + $this->dbForProject->foreach('databases', function (Document $database) { + Console::log("Migrating Collections of {$database->getId()} ({$database->getAttribute('name')})"); + + $databaseTable = "database_{$database->getSequence()}"; + $this->dbForProject->purgeCachedCollection($databaseTable); + + $this->dbForProject->foreach($databaseTable, function (Document $collection) use ($databaseTable) { + Console::log("Migrating Collection of {$collection->getId()} ({$collection->getAttribute('name')})"); + + $collectionTable = "{$databaseTable}_collection_{$collection->getSequence()}"; + $this->dbForProject->purgeCachedCollection($collectionTable); + }); + }); + } + + /** + * Migrate all Bucket tables + * + * @return void + * @throws \Exception + * @throws \PDOException + */ + protected function migrateBuckets(): void + { + $this->dbForProject->foreach('buckets', function (Document $bucket) { + Console::log("Migrating Bucket {$bucket->getId()} ({$bucket->getAttribute('name')})"); + + $bucketTable = "bucket_{$bucket->getSequence()}"; + $this->dbForProject->purgeCachedCollection($bucketTable); + }); + } + + /** + * Fix run on each document + * + * @param Document $document + * @return Document + * @throws Conflict + * @throws Structure + * @throws Timeout + * @throws \Utopia\Database\Exception + * @throws \Utopia\Database\Exception\Authorization + * @throws \Utopia\Database\Exception\Query + */ + private function migrateDocument(Document $document): Document + { + switch ($document->getCollection()) { + case 'keys': + $projectInternalId = $document->getAttribute('projectInternalId'); + $projectId = $document->getAttribute('projectId'); + + if (!empty($projectInternalId) && empty($document->getAttribute('resourceInternalId'))) { + $document->setAttribute('resourceType', 'projects'); + $document->setAttribute('resourceId', $projectId); + $document->setAttribute('resourceInternalId', $projectInternalId); + } + break; + default: + break; + } + return $document; + } + + /** + * Clean up old attributes after document migration is complete. + * + * @return void + */ + private function cleanupOldAttributes(): void + { + $collectionType = match ($this->project->getSequence()) { + 'console' => 'console', + default => 'projects', + }; + + if ($collectionType === 'console') { + $attributesToDelete = [ + 'keys' => ['projectInternalId', 'projectId'], + ]; + + foreach ($attributesToDelete as $collectionId => $attributes) { + foreach ($attributes as $attribute) { + try { + $this->dbForProject->deleteAttribute($collectionId, $attribute); + } catch (Throwable $th) { + Console::warning("Failed to delete attribute \"{$attribute}\" from {$collectionId}: {$th->getMessage()}"); + } + } + $this->dbForProject->purgeCachedCollection($collectionId); + } + } + } +} diff --git a/src/Appwrite/Network/Platform.php b/src/Appwrite/Network/Platform.php index ea64ff98c1..1cf5de91d1 100644 --- a/src/Appwrite/Network/Platform.php +++ b/src/Appwrite/Network/Platform.php @@ -35,6 +35,7 @@ class Platform public const SCHEME_ANDROID = 'appwrite-android'; public const SCHEME_WINDOWS = 'appwrite-windows'; public const SCHEME_LINUX = 'appwrite-linux'; + public const SCHEME_TAURI = 'tauri'; /** * @var array Map scheme types to user-friendly platform names. @@ -53,6 +54,7 @@ class Platform self::SCHEME_FIREFOX_EXTENSION => 'Web (Firefox Extension)', self::SCHEME_SAFARI_EXTENSION => 'Web (Safari Extension)', self::SCHEME_EDGE_EXTENSION => 'Web (Edge Extension)', + self::SCHEME_TAURI => 'Web (Tauri)', ]; /** diff --git a/src/Appwrite/Network/Validator/Origin.php b/src/Appwrite/Network/Validator/Origin.php index 8b9974e990..3c4e8a254a 100644 --- a/src/Appwrite/Network/Validator/Origin.php +++ b/src/Appwrite/Network/Validator/Origin.php @@ -69,6 +69,7 @@ class Origin extends Validator Platform::SCHEME_FIREFOX_EXTENSION, Platform::SCHEME_SAFARI_EXTENSION, Platform::SCHEME_EDGE_EXTENSION, + Platform::SCHEME_TAURI, ]; if (in_array($this->scheme, $webPlatforms, true)) { $validator = new Hostname($this->allowedHostnames); diff --git a/src/Appwrite/Platform/Tasks/Install.php b/src/Appwrite/Platform/Tasks/Install.php index 71124c4b39..59a60062eb 100644 --- a/src/Appwrite/Platform/Tasks/Install.php +++ b/src/Appwrite/Platform/Tasks/Install.php @@ -168,8 +168,27 @@ class Install extends Action } } - // Block database type changes on existing installations - $existingDatabase = $vars['_APP_DB_ADAPTER']['default'] ?? null; + // Block database type changes on existing installations. + // Only enforce if the existing config explicitly set _APP_DB_ADAPTER + // (pre-1.9.0 installs never had this variable). + $existingDatabase = null; + foreach ($compose->getServices() as $service) { + if (!$service) { + continue; + } + $svcEnv = $service->getEnvironment()->list(); + if (isset($svcEnv['_APP_DB_ADAPTER'])) { + $existingDatabase = $svcEnv['_APP_DB_ADAPTER']; + break; + } + } + if ($existingDatabase === null) { + $envFilePath = $this->path . '/' . $this->getEnvFileName(); + $rawEnv = @file_get_contents($envFilePath); + if ($rawEnv !== false) { + $existingDatabase = (new Env($rawEnv))->list()['_APP_DB_ADAPTER'] ?? null; + } + } if ($existingDatabase !== null && $existingDatabase !== $database) { Console::error("Cannot change database type from '{$existingDatabase}' to '{$database}'."); Console::error('Changing database types on an existing installation is not supported.'); @@ -279,7 +298,8 @@ class Install extends Action Console::warning("\nUse 'AAAA' if you're using an IPv6 address and 'A' if you're using an IPv4 address.\n"); } } - $database = $userInput['_APP_DB_ADAPTER'] ?? $database; + $userInput['_APP_DB_ADAPTER'] = $userInput['_APP_DB_ADAPTER'] ?? $database; + $database = $userInput['_APP_DB_ADAPTER']; if ($database === 'postgresql') { $userInput['_APP_DB_HOST'] = 'postgresql'; $userInput['_APP_DB_PORT'] = 5432; diff --git a/src/Appwrite/Platform/Tasks/Migrate.php b/src/Appwrite/Platform/Tasks/Migrate.php index cec2f6ec27..b952808998 100644 --- a/src/Appwrite/Platform/Tasks/Migrate.php +++ b/src/Appwrite/Platform/Tasks/Migrate.php @@ -31,7 +31,7 @@ class Migrate extends Action ->inject('dbForPlatform') ->inject('getProjectDB') ->inject('register') - ->inject('authorisation') + ->inject('authorization') ->callback($this->action(...)); } @@ -64,8 +64,43 @@ class Migrate extends Action /** @var Migration $migration */ $migration = new $class(); + // Disable subquery filters that reference new schema columns not yet migrated + $subQueries = [ + 'subQueryAccountKeys', + 'subQueryAttributes', + 'subQueryAuthenticators', + 'subQueryChallenges', + 'subQueryDevKeys', + 'subQueryIndexes', + 'subQueryKeys', + 'subQueryMemberships', + 'subQueryOrganizationKeys', + 'subQueryPlatforms', + 'subQueryProjectVariables', + 'subQuerySessions', + 'subQueryTargets', + 'subQueryTokens', + 'subQueryTopicTargets', + 'subQueryVariables', + 'subQueryWebhooks', + ]; + foreach ($subQueries as $name) { + Database::addFilter( + $name, + fn () => null, + fn () => [] + ); + } + + $dbForPlatform->disableValidation(); + $dbForPlatform->purgeCachedCollection('projects'); + $count = 0; - $total = $dbForPlatform->count('projects') + 1; + try { + $total = $dbForPlatform->count('projects') + 1; + } catch (\Throwable) { + $total = 0; + } $dbForPlatform->foreach('projects', function (Document $project) use ($dbForPlatform, $getProjectDB, $register, $migration, &$count, $total, $authorization) { /** @var Database $dbForProject */ @@ -74,9 +109,14 @@ class Migrate extends Action try { $migration - ->setProject($project, $dbForProject, $dbForPlatform, $authorization, $getProjectDB) - ->setPDO($register->get('db', true)) - ->execute(); + ->setProject($project, $dbForProject, $dbForPlatform, $authorization, $getProjectDB); + + $db = $register->get('db', true); + if ($db instanceof \Utopia\Database\PDO) { + $migration->setPDO($db); + } + + $migration->execute(); } catch (\Throwable $th) { Console::error('Failed to migrate project "' . $project->getId() . '" with error: ' . $th->getMessage()); throw $th; @@ -89,9 +129,14 @@ class Migrate extends Action try { $migration - ->setProject($console, $getProjectDB($console), $dbForPlatform, $authorization, $getProjectDB) - ->setPDO($register->get('db', true)) - ->execute(); + ->setProject($console, $getProjectDB($console), $dbForPlatform, $authorization, $getProjectDB); + + $db = $register->get('db', true); + if ($db instanceof \Utopia\Database\PDO) { + $migration->setPDO($db); + } + + $migration->execute(); } catch (\Throwable $th) { Console::error('Failed to migrate project "console" with error: ' . $th->getMessage()); throw $th; diff --git a/src/Appwrite/Platform/Tasks/Upgrade.php b/src/Appwrite/Platform/Tasks/Upgrade.php index 200d214ead..7214ca2e8c 100644 --- a/src/Appwrite/Platform/Tasks/Upgrade.php +++ b/src/Appwrite/Platform/Tasks/Upgrade.php @@ -4,7 +4,7 @@ namespace Appwrite\Platform\Tasks; use Appwrite\Docker\Compose; use Appwrite\Docker\Env; -use Utopia\CLI\Console; +use Utopia\Console; use Utopia\Validator\Boolean; use Utopia\Validator\Text; @@ -80,7 +80,9 @@ class Upgrade extends Install } if ($database === null) { - throw new \Exception('Database type not found, can not upgrade. Ensure `_APP_DB_ADAPTER` is set in your environment.'); + // Pre-1.9.0 installations only supported MariaDB + $database = 'mariadb'; + Console::info('No _APP_DB_ADAPTER found in existing configuration, defaulting to mariadb.'); } $this->lockedDatabase = $database; diff --git a/src/Appwrite/SDK/Specification/Format/OpenAPI3.php b/src/Appwrite/SDK/Specification/Format/OpenAPI3.php index 7c7fc96b2f..9edf923092 100644 --- a/src/Appwrite/SDK/Specification/Format/OpenAPI3.php +++ b/src/Appwrite/SDK/Specification/Format/OpenAPI3.php @@ -843,8 +843,7 @@ class OpenAPI3 extends Format break; case 'id': - $type = 'integer'; - $format = $rule['format'] ?? 'int32'; + $type = 'string'; break; case 'enum': diff --git a/src/Appwrite/SDK/Specification/Format/Swagger2.php b/src/Appwrite/SDK/Specification/Format/Swagger2.php index 558e911c33..0d79320033 100644 --- a/src/Appwrite/SDK/Specification/Format/Swagger2.php +++ b/src/Appwrite/SDK/Specification/Format/Swagger2.php @@ -823,8 +823,7 @@ class Swagger2 extends Format break; case 'id': - $type = 'integer'; - $format = $rule['format'] ?? 'int32'; + $type = 'string'; break; case 'enum': diff --git a/src/Appwrite/Utopia/Request/Filters/V21.php b/src/Appwrite/Utopia/Request/Filters/V21.php index d51ec28a1e..74e1fcfaff 100644 --- a/src/Appwrite/Utopia/Request/Filters/V21.php +++ b/src/Appwrite/Utopia/Request/Filters/V21.php @@ -6,7 +6,7 @@ use Appwrite\Utopia\Request\Filter; class V21 extends Filter { - // Convert 1.8.0 params to 1.8.2 + // Convert 1.8.0 params to 1.9.0 public function parse(array $content, string $model): array { switch ($model) { diff --git a/src/Appwrite/Utopia/Response/Filters/V21.php b/src/Appwrite/Utopia/Response/Filters/V21.php index 37f9e01b86..b65e26a8b0 100644 --- a/src/Appwrite/Utopia/Response/Filters/V21.php +++ b/src/Appwrite/Utopia/Response/Filters/V21.php @@ -23,6 +23,18 @@ class V21 extends Filter "functions", fn ($item) => $this->parseFunction($item), ), + Response::MODEL_DOCUMENT => $this->parseDocument($content), + Response::MODEL_DOCUMENT_LIST => $this->handleList( + $content, + "documents", + fn ($item) => $this->parseDocument($item), + ), + Response::MODEL_ROW => $this->parseRow($content), + Response::MODEL_ROW_LIST => $this->handleList( + $content, + "rows", + fn ($item) => $this->parseRow($item), + ), default => $content, }; } @@ -46,4 +58,39 @@ class V21 extends Filter unset($content['runtimeSpecification']); return $content; } + + protected function parseDocument(array $content): array + { + return $this->castSequence($content); + } + + protected function parseRow(array $content): array + { + return $this->castSequence($content); + } + + protected function castSequence(array $content): array + { + if (isset($content['$sequence'])) { + $content['$sequence'] = \is_numeric($content['$sequence']) + ? (int)$content['$sequence'] + : 0; + } + + foreach ($content as $key => $value) { + if (\is_array($value)) { + if (isset($value['$id'])) { + $content[$key] = $this->castSequence($value); + } else { + foreach ($value as $i => $item) { + if (\is_array($item) && isset($item['$id'])) { + $content[$key][$i] = $this->castSequence($item); + } + } + } + } + } + + return $content; + } } diff --git a/src/Appwrite/Utopia/Response/Model/Document.php b/src/Appwrite/Utopia/Response/Model/Document.php index e0aca32b3b..3be003418d 100644 --- a/src/Appwrite/Utopia/Response/Model/Document.php +++ b/src/Appwrite/Utopia/Response/Model/Document.php @@ -83,8 +83,8 @@ class Document extends Any $document->removeAttribute('$collection'); $document->removeAttribute('$tenant'); - if (!$document->isEmpty() && \is_numeric($document->getAttribute('$sequence', 0))) { - $document->setAttribute('$sequence', (int)$document->getAttribute('$sequence', 0)); + if (!$document->isEmpty()) { + $document->setAttribute('$sequence', (string)$document->getAttribute('$sequence', '')); } foreach ($document->getAttributes() as $attribute) { diff --git a/src/Appwrite/Utopia/Response/Model/Row.php b/src/Appwrite/Utopia/Response/Model/Row.php index a96df44bc3..4c7426c4bc 100644 --- a/src/Appwrite/Utopia/Response/Model/Row.php +++ b/src/Appwrite/Utopia/Response/Model/Row.php @@ -83,8 +83,8 @@ class Row extends Any $document->removeAttribute('$collection'); $document->removeAttribute('$tenant'); - if (!$document->isEmpty() && \is_numeric($document->getAttribute('$sequence', 0))) { - $document->setAttribute('$sequence', (int)$document->getAttribute('$sequence', 0)); + if (!$document->isEmpty()) { + $document->setAttribute('$sequence', (string)$document->getAttribute('$sequence', '')); } foreach ($document->getAttributes() as $column) { diff --git a/tests/e2e/Services/Databases/DatabasesBase.php b/tests/e2e/Services/Databases/DatabasesBase.php index 7b967d7673..d6c72b0a7b 100644 --- a/tests/e2e/Services/Databases/DatabasesBase.php +++ b/tests/e2e/Services/Databases/DatabasesBase.php @@ -2612,9 +2612,7 @@ trait DatabasesBase } $this->assertTrue(array_key_exists('$sequence', $document1['body'])); - $this->getSupportForIntegerIds() - ? $this->assertIsInt($document1['body']['$sequence']) - : $this->assertIsString($document1['body']['$sequence']); + $this->assertIsString($document1['body']['$sequence']); $this->assertEquals(201, $document2['headers']['status-code']); $this->assertEquals($data['moviesId'], $document2['body'][$this->getContainerIdResponseKey()]); @@ -2694,9 +2692,7 @@ trait DatabasesBase /** * Resubmit same document, nothing to update */ - $this->getSupportForIntegerIds() - ? $this->assertIsInt($document['body']['$sequence']) - : $this->assertIsString($document['body']['$sequence']); + $this->assertIsString($document['body']['$sequence']); $upsertData = [ 'title' => 'Thor: Ragnarok', diff --git a/tests/unit/Network/Validators/OriginTest.php b/tests/unit/Network/Validators/OriginTest.php index 7a19daecbf..64ce71951b 100644 --- a/tests/unit/Network/Validators/OriginTest.php +++ b/tests/unit/Network/Validators/OriginTest.php @@ -73,6 +73,10 @@ class OriginTest extends TestCase $this->assertEquals(false, $validator->isValid('ms-browser-extension://com.company.appname')); $this->assertEquals('Invalid Origin. Register your new client (com.company.appname) as a new Web (Edge Extension) platform on your project console dashboard', $validator->getDescription()); + $this->assertEquals(true, $validator->isValid('tauri://localhost')); + $this->assertEquals(false, $validator->isValid('tauri://example.com')); + $this->assertEquals('Invalid Origin. Register your new client (example.com) as a new Web (Tauri) platform on your project console dashboard', $validator->getDescription()); + $this->assertEquals(false, $validator->isValid('random-scheme://localhost')); $this->assertEquals('Invalid Scheme. The scheme used (random-scheme) in the Origin (random-scheme://localhost) is not supported. If you are using a custom scheme, please change it to `appwrite-callback-`', $validator->getDescription()); } diff --git a/tests/unit/Utopia/Request/Filters/V21Test.php b/tests/unit/Utopia/Request/Filters/V21Test.php new file mode 100644 index 0000000000..f514de71ee --- /dev/null +++ b/tests/unit/Utopia/Request/Filters/V21Test.php @@ -0,0 +1,376 @@ +filter = new V21(); + } + + public function tearDown(): void + { + } + + public static function functionsCreateTemplateDeploymentProvider(): array + { + return [ + 'convert version to type and reference' => [ + [ + 'templateId' => 'template123', + 'version' => '1.0.0', + ], + [ + 'templateId' => 'template123', + 'type' => 'tag', + 'reference' => '1.0.0', + ] + ], + 'handle version with semver string' => [ + [ + 'version' => 'v2.3.1', + 'templateId' => 'template456', + ], + [ + 'type' => 'tag', + 'reference' => 'v2.3.1', + 'templateId' => 'template456', + ] + ], + 'skip conversion when version is empty string' => [ + [ + 'templateId' => 'template123', + 'version' => '', + ], + [ + 'templateId' => 'template123', + 'version' => '', + ] + ], + 'skip conversion when version is missing' => [ + [ + 'templateId' => 'template123', + ], + [ + 'templateId' => 'template123', + ] + ], + 'preserve other fields when converting version' => [ + [ + 'templateId' => 'template123', + 'version' => '3.0.0', + 'activate' => true, + 'buildCommand' => 'npm run build', + ], + [ + 'templateId' => 'template123', + 'type' => 'tag', + 'reference' => '3.0.0', + 'activate' => true, + 'buildCommand' => 'npm run build', + ] + ], + ]; + } + + #[DataProvider('functionsCreateTemplateDeploymentProvider')] + public function testFunctionsCreateTemplateDeployment(array $content, array $expected): void + { + $model = 'functions.createTemplateDeployment'; + + $result = $this->filter->parse($content, $model); + + $this->assertEquals($expected, $result); + } + + public static function sitesCreateTemplateDeploymentProvider(): array + { + return [ + 'convert version to type and reference' => [ + [ + 'templateId' => 'site-template123', + 'version' => '2.0.0', + ], + [ + 'templateId' => 'site-template123', + 'type' => 'tag', + 'reference' => '2.0.0', + ] + ], + 'skip conversion when version is empty' => [ + [ + 'templateId' => 'site-template123', + 'version' => '', + ], + [ + 'templateId' => 'site-template123', + 'version' => '', + ] + ], + 'skip conversion when version is missing' => [ + [ + 'templateId' => 'site-template123', + ], + [ + 'templateId' => 'site-template123', + ] + ], + ]; + } + + #[DataProvider('sitesCreateTemplateDeploymentProvider')] + public function testSitesCreateTemplateDeployment(array $content, array $expected): void + { + $model = 'sites.createTemplateDeployment'; + + $result = $this->filter->parse($content, $model); + + $this->assertEquals($expected, $result); + } + + public static function functionsCreateProvider(): array + { + return [ + 'convert specification to buildSpecification and runtimeSpecification' => [ + [ + 'name' => 'test-function', + 'runtime' => 'node-18.0', + 'specification' => 's-1vcpu-512mb', + ], + [ + 'name' => 'test-function', + 'runtime' => 'node-18.0', + 'buildSpecification' => 's-1vcpu-512mb', + 'runtimeSpecification' => 's-1vcpu-512mb', + ] + ], + 'skip conversion when specification is empty' => [ + [ + 'name' => 'test-function', + 'runtime' => 'node-18.0', + 'specification' => '', + ], + [ + 'name' => 'test-function', + 'runtime' => 'node-18.0', + 'specification' => '', + ] + ], + 'skip conversion when specification is missing' => [ + [ + 'name' => 'test-function', + 'runtime' => 'node-18.0', + ], + [ + 'name' => 'test-function', + 'runtime' => 'node-18.0', + ] + ], + 'preserve other fields when converting specification' => [ + [ + 'name' => 'test-function', + 'specification' => 's-2vcpu-1gb', + 'timeout' => 30, + 'enabled' => true, + ], + [ + 'name' => 'test-function', + 'buildSpecification' => 's-2vcpu-1gb', + 'runtimeSpecification' => 's-2vcpu-1gb', + 'timeout' => 30, + 'enabled' => true, + ] + ], + ]; + } + + #[DataProvider('functionsCreateProvider')] + public function testFunctionsCreate(array $content, array $expected): void + { + $model = 'functions.create'; + + $result = $this->filter->parse($content, $model); + + $this->assertEquals($expected, $result); + } + + public static function sitesCreateProvider(): array + { + return [ + 'convert specification to buildSpecification and runtimeSpecification' => [ + [ + 'name' => 'test-site', + 'framework' => 'nextjs', + 'specification' => 's-1vcpu-512mb', + ], + [ + 'name' => 'test-site', + 'framework' => 'nextjs', + 'buildSpecification' => 's-1vcpu-512mb', + 'runtimeSpecification' => 's-1vcpu-512mb', + ] + ], + 'skip conversion when specification is empty' => [ + [ + 'name' => 'test-site', + 'specification' => '', + ], + [ + 'name' => 'test-site', + 'specification' => '', + ] + ], + 'skip conversion when specification is missing' => [ + [ + 'name' => 'test-site', + ], + [ + 'name' => 'test-site', + ] + ], + ]; + } + + #[DataProvider('sitesCreateProvider')] + public function testSitesCreate(array $content, array $expected): void + { + $model = 'sites.create'; + + $result = $this->filter->parse($content, $model); + + $this->assertEquals($expected, $result); + } + + public static function functionsUpdateProvider(): array + { + return [ + 'convert specification to buildSpecification and runtimeSpecification' => [ + [ + 'name' => 'updated-function', + 'specification' => 's-2vcpu-1gb', + ], + [ + 'name' => 'updated-function', + 'buildSpecification' => 's-2vcpu-1gb', + 'runtimeSpecification' => 's-2vcpu-1gb', + ] + ], + 'skip conversion when specification is missing' => [ + [ + 'name' => 'updated-function', + ], + [ + 'name' => 'updated-function', + ] + ], + ]; + } + + #[DataProvider('functionsUpdateProvider')] + public function testFunctionsUpdate(array $content, array $expected): void + { + $model = 'functions.update'; + + $result = $this->filter->parse($content, $model); + + $this->assertEquals($expected, $result); + } + + public static function sitesUpdateProvider(): array + { + return [ + 'convert specification to buildSpecification and runtimeSpecification' => [ + [ + 'name' => 'updated-site', + 'specification' => 's-2vcpu-1gb', + ], + [ + 'name' => 'updated-site', + 'buildSpecification' => 's-2vcpu-1gb', + 'runtimeSpecification' => 's-2vcpu-1gb', + ] + ], + 'skip conversion when specification is missing' => [ + [ + 'name' => 'updated-site', + ], + [ + 'name' => 'updated-site', + ] + ], + ]; + } + + #[DataProvider('sitesUpdateProvider')] + public function testSitesUpdate(array $content, array $expected): void + { + $model = 'sites.update'; + + $result = $this->filter->parse($content, $model); + + $this->assertEquals($expected, $result); + } + + public static function unmatchedModelProvider(): array + { + return [ + 'unmatched model passes through unchanged' => [ + 'databases.create', + [ + 'name' => 'test-database', + 'databaseId' => 'db123', + ], + [ + 'name' => 'test-database', + 'databaseId' => 'db123', + ] + ], + 'empty content for unmatched model' => [ + 'users.list', + [], + [] + ], + 'content with specification for unmatched model is not converted' => [ + 'deployments.create', + [ + 'specification' => 's-1vcpu-512mb', + 'name' => 'test', + ], + [ + 'specification' => 's-1vcpu-512mb', + 'name' => 'test', + ] + ], + 'content with version for unmatched model is not converted' => [ + 'databases.createDocument', + [ + 'version' => '1.0.0', + 'data' => 'test', + ], + [ + 'version' => '1.0.0', + 'data' => 'test', + ] + ], + ]; + } + + #[DataProvider('unmatchedModelProvider')] + public function testUnmatchedModel(string $model, array $content, array $expected): void + { + $result = $this->filter->parse($content, $model); + + $this->assertEquals($expected, $result); + } +} diff --git a/tests/unit/Utopia/Response/Filters/V21Test.php b/tests/unit/Utopia/Response/Filters/V21Test.php new file mode 100644 index 0000000000..fdff7fdf3f --- /dev/null +++ b/tests/unit/Utopia/Response/Filters/V21Test.php @@ -0,0 +1,488 @@ +filter = new V21(); + } + + public function tearDown(): void + { + } + + public static function functionProvider(): array + { + return [ + 'merge buildSpecification and runtimeSpecification into specification' => [ + [ + 'name' => 'test-function', + 'buildSpecification' => 's-1vcpu-512mb', + 'runtimeSpecification' => 's-1vcpu-256mb', + 'runtime' => 'node-18.0', + ], + [ + 'name' => 'test-function', + 'specification' => 's-1vcpu-512mb', + 'runtime' => 'node-18.0', + ] + ], + 'use buildSpecification when present' => [ + [ + 'buildSpecification' => 's-2vcpu-1gb', + 'runtimeSpecification' => 's-1vcpu-512mb', + ], + [ + 'specification' => 's-2vcpu-1gb', + ] + ], + 'fallback to specification when buildSpecification is missing' => [ + [ + 'specification' => 's-1vcpu-512mb', + 'runtimeSpecification' => 's-1vcpu-256mb', + ], + [ + 'specification' => 's-1vcpu-512mb', + ] + ], + 'handle missing both buildSpecification and specification' => [ + [ + 'name' => 'test-function', + 'runtimeSpecification' => 's-1vcpu-256mb', + ], + [ + 'name' => 'test-function', + 'specification' => null, + ] + ], + 'handle no spec fields at all' => [ + [ + 'name' => 'test-function', + 'runtime' => 'node-18.0', + ], + [ + 'name' => 'test-function', + 'specification' => null, + 'runtime' => 'node-18.0', + ] + ], + 'handle empty buildSpecification string' => [ + [ + 'buildSpecification' => '', + 'runtimeSpecification' => 's-1vcpu-256mb', + ], + [ + 'specification' => '', + ] + ], + ]; + } + + #[DataProvider('functionProvider')] + public function testFunction(array $content, array $expected): void + { + $model = Response::MODEL_FUNCTION; + + $result = $this->filter->parse($content, $model); + + $this->assertEquals($expected, $result); + } + + public static function functionListProvider(): array + { + return [ + 'convert list of functions' => [ + [ + 'total' => 2, + 'functions' => [ + [ + 'name' => 'function-1', + 'buildSpecification' => 's-1vcpu-512mb', + 'runtimeSpecification' => 's-1vcpu-256mb', + ], + [ + 'name' => 'function-2', + 'buildSpecification' => 's-2vcpu-1gb', + 'runtimeSpecification' => 's-1vcpu-512mb', + ], + ], + ], + [ + 'total' => 2, + 'functions' => [ + [ + 'name' => 'function-1', + 'specification' => 's-1vcpu-512mb', + ], + [ + 'name' => 'function-2', + 'specification' => 's-2vcpu-1gb', + ], + ], + ] + ], + 'handle empty function list' => [ + [ + 'total' => 0, + 'functions' => [], + ], + [ + 'total' => 0, + 'functions' => [], + ] + ], + 'handle single function in list' => [ + [ + 'total' => 1, + 'functions' => [ + [ + 'buildSpecification' => 's-1vcpu-512mb', + 'runtimeSpecification' => 's-1vcpu-256mb', + ], + ], + ], + [ + 'total' => 1, + 'functions' => [ + [ + 'specification' => 's-1vcpu-512mb', + ], + ], + ] + ], + ]; + } + + #[DataProvider('functionListProvider')] + public function testFunctionList(array $content, array $expected): void + { + $model = Response::MODEL_FUNCTION_LIST; + + $result = $this->filter->parse($content, $model); + + $this->assertEquals($expected, $result); + } + + public static function siteProvider(): array + { + return [ + 'merge buildSpecification and runtimeSpecification into specification' => [ + [ + 'name' => 'test-site', + 'buildSpecification' => 's-1vcpu-512mb', + 'runtimeSpecification' => 's-1vcpu-256mb', + 'framework' => 'nextjs', + ], + [ + 'name' => 'test-site', + 'specification' => 's-1vcpu-512mb', + 'framework' => 'nextjs', + ] + ], + 'use buildSpecification when present' => [ + [ + 'buildSpecification' => 's-2vcpu-1gb', + 'runtimeSpecification' => 's-1vcpu-512mb', + ], + [ + 'specification' => 's-2vcpu-1gb', + ] + ], + 'fallback to specification when buildSpecification is missing' => [ + [ + 'specification' => 's-1vcpu-512mb', + 'runtimeSpecification' => 's-1vcpu-256mb', + ], + [ + 'specification' => 's-1vcpu-512mb', + ] + ], + 'handle missing both buildSpecification and specification' => [ + [ + 'name' => 'test-site', + 'runtimeSpecification' => 's-1vcpu-256mb', + ], + [ + 'name' => 'test-site', + 'specification' => null, + ] + ], + 'handle no spec fields at all' => [ + [ + 'name' => 'test-site', + 'framework' => 'react', + ], + [ + 'name' => 'test-site', + 'specification' => null, + 'framework' => 'react', + ] + ], + ]; + } + + #[DataProvider('siteProvider')] + public function testSite(array $content, array $expected): void + { + $model = Response::MODEL_SITE; + + $result = $this->filter->parse($content, $model); + + $this->assertEquals($expected, $result); + } + + public static function siteListProvider(): array + { + return [ + 'convert list of sites' => [ + [ + 'total' => 2, + 'sites' => [ + [ + 'name' => 'site-1', + 'buildSpecification' => 's-1vcpu-512mb', + 'runtimeSpecification' => 's-1vcpu-256mb', + ], + [ + 'name' => 'site-2', + 'buildSpecification' => 's-2vcpu-1gb', + 'runtimeSpecification' => 's-1vcpu-512mb', + ], + ], + ], + [ + 'total' => 2, + 'sites' => [ + [ + 'name' => 'site-1', + 'specification' => 's-1vcpu-512mb', + ], + [ + 'name' => 'site-2', + 'specification' => 's-2vcpu-1gb', + ], + ], + ] + ], + 'handle empty site list' => [ + [ + 'total' => 0, + 'sites' => [], + ], + [ + 'total' => 0, + 'sites' => [], + ] + ], + 'handle single site in list' => [ + [ + 'total' => 1, + 'sites' => [ + [ + 'name' => 'my-site', + 'buildSpecification' => 's-1vcpu-512mb', + 'runtimeSpecification' => 's-1vcpu-256mb', + ], + ], + ], + [ + 'total' => 1, + 'sites' => [ + [ + 'name' => 'my-site', + 'specification' => 's-1vcpu-512mb', + ], + ], + ] + ], + ]; + } + + #[DataProvider('siteListProvider')] + public function testSiteList(array $content, array $expected): void + { + $model = Response::MODEL_SITE_LIST; + + $result = $this->filter->parse($content, $model); + + $this->assertEquals($expected, $result); + } + + public static function documentProvider(): array + { + return [ + 'cast $sequence to int' => [ + [ + '$id' => 'doc1', + '$sequence' => '123', + 'name' => 'test', + ], + [ + '$id' => 'doc1', + '$sequence' => 123, + 'name' => 'test', + ] + ], + 'non-numeric $sequence becomes 0' => [ + [ + '$id' => 'doc1', + '$sequence' => 'abc', + ], + [ + '$id' => 'doc1', + '$sequence' => 0, + ] + ], + 'nested relationship document' => [ + [ + '$id' => 'doc1', + '$sequence' => '1', + 'author' => [ + '$id' => 'doc2', + '$sequence' => '2', + 'name' => 'John', + ], + ], + [ + '$id' => 'doc1', + '$sequence' => 1, + 'author' => [ + '$id' => 'doc2', + '$sequence' => 2, + 'name' => 'John', + ], + ] + ], + 'nested array of relationship documents' => [ + [ + '$id' => 'doc1', + '$sequence' => '1', + 'comments' => [ + [ + '$id' => 'doc2', + '$sequence' => '2', + 'text' => 'hello', + ], + [ + '$id' => 'doc3', + '$sequence' => '3', + 'text' => 'world', + ], + ], + ], + [ + '$id' => 'doc1', + '$sequence' => 1, + 'comments' => [ + [ + '$id' => 'doc2', + '$sequence' => 2, + 'text' => 'hello', + ], + [ + '$id' => 'doc3', + '$sequence' => 3, + 'text' => 'world', + ], + ], + ] + ], + 'deeply nested relationships' => [ + [ + '$id' => 'doc1', + '$sequence' => '1', + 'author' => [ + '$id' => 'doc2', + '$sequence' => '2', + 'profile' => [ + '$id' => 'doc3', + '$sequence' => '3', + ], + ], + ], + [ + '$id' => 'doc1', + '$sequence' => 1, + 'author' => [ + '$id' => 'doc2', + '$sequence' => 2, + 'profile' => [ + '$id' => 'doc3', + '$sequence' => 3, + ], + ], + ] + ], + ]; + } + + #[DataProvider('documentProvider')] + public function testDocument(array $content, array $expected): void + { + $result = $this->filter->parse($content, Response::MODEL_DOCUMENT); + + $this->assertEquals($expected, $result); + } + + #[DataProvider('documentProvider')] + public function testRow(array $content, array $expected): void + { + $result = $this->filter->parse($content, Response::MODEL_ROW); + + $this->assertEquals($expected, $result); + } + + public static function defaultPassthroughProvider(): array + { + return [ + 'unmatched model passes through unchanged' => [ + Response::MODEL_DOCUMENT, + [ + 'name' => 'test-doc', + '$id' => 'doc123', + 'data' => 'some-value', + ], + [ + 'name' => 'test-doc', + '$id' => 'doc123', + 'data' => 'some-value', + ] + ], + 'empty content passes through unchanged' => [ + Response::MODEL_DOCUMENT, + [], + [] + ], + 'deployment model passes through unchanged' => [ + Response::MODEL_DEPLOYMENT, + [ + 'id' => 'deployment123', + 'buildSpecification' => 's-1vcpu-512mb', + 'runtimeSpecification' => 's-1vcpu-256mb', + ], + [ + 'id' => 'deployment123', + 'buildSpecification' => 's-1vcpu-512mb', + 'runtimeSpecification' => 's-1vcpu-256mb', + ] + ], + ]; + } + + #[DataProvider('defaultPassthroughProvider')] + public function testDefaultPassthrough(string $model, array $content, array $expected): void + { + $result = $this->filter->parse($content, $model); + + $this->assertEquals($expected, $result); + } +}