Merge remote-tracking branch 'origin/1.9.x' into revert-11585-revert-11402-sync-mongodb

This commit is contained in:
ArnabChatterjee20k
2026-03-20 11:29:37 +05:30
25 changed files with 1550 additions and 148 deletions
+119 -86
View File
@@ -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
+3 -3
View File
@@ -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 的本机主机上完成安装后,服务器可能需要几分钟才能启动。
+3 -3
View File
@@ -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.
+1 -1
View File
@@ -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());
}
}
+1 -1
View File
@@ -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';
+6
View File
@@ -108,6 +108,12 @@
"czproject/git-php": "4.*",
"laravel/pint": "1.*"
},
"repositories": [
{
"type": "vcs",
"url": "https://github.com/utopia-php/database"
}
],
"provide": {
"ext-phpiredis": "*"
},
Generated
+13 -13
View File
@@ -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",
-12
View File
@@ -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
+1 -1
View File
@@ -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',
];
/**
+396
View File
@@ -0,0 +1,396 @@
<?php
namespace Appwrite\Migration\Version;
use Appwrite\Migration\Migration;
use Exception;
use Throwable;
use Utopia\Console;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Conflict;
use Utopia\Database\Exception\Structure;
use Utopia\Database\Exception\Timeout;
class V24 extends Migration
{
/**
* @throws Throwable
*/
public function execute(): void
{
/**
* Disable SubQueries for Performance.
*/
$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 () => []
);
}
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);
}
}
}
}
+2
View File
@@ -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<string, string> 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)',
];
/**
@@ -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);
+23 -3
View File
@@ -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;
+53 -8
View File
@@ -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;
+4 -2
View File
@@ -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;
@@ -843,8 +843,7 @@ class OpenAPI3 extends Format
break;
case 'id':
$type = 'integer';
$format = $rule['format'] ?? 'int32';
$type = 'string';
break;
case 'enum':
@@ -823,8 +823,7 @@ class Swagger2 extends Format
break;
case 'id':
$type = 'integer';
$format = $rule['format'] ?? 'int32';
$type = 'string';
break;
case 'enum':
+1 -1
View File
@@ -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) {
@@ -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;
}
}
@@ -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) {
+2 -2
View File
@@ -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) {
@@ -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',
@@ -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-<PROJECT_ID>`', $validator->getDescription());
}
@@ -0,0 +1,376 @@
<?php
namespace Tests\Unit\Utopia\Request\Filters;
use Appwrite\Utopia\Request\Filter;
use Appwrite\Utopia\Request\Filters\V21;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
class V21Test extends TestCase
{
/**
* @var Filter
*/
protected $filter;
public function setUp(): void
{
$this->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);
}
}
@@ -0,0 +1,488 @@
<?php
namespace Tests\Unit\Utopia\Response\Filters;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Filters\V21;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
class V21Test extends TestCase
{
protected ?V21 $filter = null;
public function setUp(): void
{
$this->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);
}
}