Compare commits

...
Author SHA1 Message Date
Matej Bačo cea242c66f Merge branch '1.9.x' into feat-public-platform-api 2026-04-08 09:29:54 +02:00
Matej Bačo ce4eb563b3 AI review fixes 2026-04-08 09:29:12 +02:00
Matej Bačo 2307d637fb Revert new patch version 2026-04-08 09:10:02 +02:00
Chirag AggarwalandGitHub 6e1f0d57af Merge pull request #11817 from appwrite/fix/specs-unresolved-model
fix: throw RuntimeException for unresolved response models in spec generation
2026-04-08 11:08:30 +05:30
Chirag Aggarwal 62b6ef06e6 fix: add swoole extension to specs CI job 2026-04-08 10:49:50 +05:30
Chirag Aggarwal f5ab593261 fix: make Project model public for server SDK spec generation
The project.updateLabels route uses AuthType::KEY which makes it
available on the server platform, but the Project model had public=false
causing it to be filtered out during spec generation.
2026-04-08 10:47:37 +05:30
Chirag Aggarwal dd4a43b78c fix: throw RuntimeException for unresolved response models in spec generation
Spec generation silently produced a fatal error when a response model
string could not be resolved to a registered model object. Now throws a
clear RuntimeException in both Swagger2 and OpenAPI3 formats, for both
single and array model responses.

Also adds a CI job to run spec generation on every PR so unresolved
models are caught before merge.
2026-04-08 10:41:43 +05:30
Chirag AggarwalandGitHub f71a98a527 Merge pull request #11816 from appwrite/fix/email-session-cache-purge
fix: persist session before purging user cache in email/password login
2026-04-08 10:25:19 +05:30
Chirag Aggarwal 6dba407aed test: add E2E test for email/password session cache race condition
Adds testEmailPasswordSessionNotCorruptedByConcurrentRequests which
reproduces the cross-worker Redis cache race that caused 401s after
login. The test fires a login request, waits for it to reach the cache
purge point, then injects concurrent GET /v1/account requests that
re-cache a stale user document. Verifies the new session is immediately
usable.

Fails against the old ordering (purge before create), passes with the
fix (create before purge).
2026-04-08 10:10:16 +05:30
Luke B. SilverandGitHub 7eef845556 Merge pull request #11794 from blueberry-adii/doc-11793-fix-readme-auth-link
fix appwrite auth broken link in readme
2026-04-07 22:15:29 +01:00
loks0nandClaude Sonnet 4.6 7781d377ae fix: persist session before purging user cache in email/password login
Swap the order of createDocument('sessions') and purgeCachedDocument('users')
in the email/password session creation flow. Previously, the cache was purged
before the session was written, opening a race window in Swoole's async
environment where a concurrent account.get() could re-cache the user with no
sessions, causing sessionVerify to fail with a 401. This matches the correct
ordering already used by the token-based flows (magic URL, OTP, phone).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 21:44:24 +01:00
Chirag AggarwalandGitHub 59bf4663ca Merge pull request #11808 from appwrite/fix/sdk-dev-branch-sync
fix: reset SDK dev branch to base branch before pushing
2026-04-08 00:40:56 +05:30
Matej Bačo 9ea0b2bc2f formatting fix 2026-04-07 15:56:54 +02:00
Matej Bačo 715727853b Fix unit test 2026-04-07 15:56:42 +02:00
Matej Bačo 43d4f709d5 Revert composer changes 2026-04-07 15:00:03 +02:00
Matej Bačo f40050fe6f Revert lockfile changes (failing tests) 2026-04-07 14:51:40 +02:00
Matej Bačo 34dfcba45c Linter fix 2026-04-07 14:08:16 +02:00
Chirag AggarwalandGitHub cb1e31bcd4 Merge pull request #11814 from appwrite/codex/bump-utopia-framework-0-34-18
[codex] Bump utopia-php/framework to 0.34.18
2026-04-07 17:38:06 +05:30
Matej Bačo 3e4e7fc0cd Merge branch '1.9.x' into feat-public-platform-api 2026-04-07 14:08:00 +02:00
Chirag Aggarwal 9403e4d65d Bump utopia-php/framework to 0.34.18 2026-04-07 17:35:32 +05:30
Matej BačoandGitHub 07a49835a6 Merge pull request #11812 from appwrite/chore-migrate-labels-api
Chore: Migrat elabels API
2026-04-07 14:05:31 +02:00
Matej Bačo d32de6f217 Improve tests 2026-04-07 13:30:35 +02:00
Matej Bačo 9b00ce4f1d Add new tests 2026-04-07 13:28:35 +02:00
Matej Bačo 8f6c8f9d8d Migrate to new endpoint 2026-04-07 13:21:36 +02:00
Matej Bačo 23fcb284a1 Fix backwards QA 2026-04-07 12:46:25 +02:00
Matej Bačo d66813d3cf Fix tests + QA fixes 2026-04-07 12:07:21 +02:00
Chirag AggarwalandGitHub 0ce92da41f Merge pull request #11810 from appwrite/codex/fix-post-merge-test-regressions
[codex] Fix post-merge e2e test regressions
2026-04-07 15:10:53 +05:30
Chirag Aggarwal e8ef4e40d7 fix post-merge e2e test regressions 2026-04-07 15:05:07 +05:30
Chirag AggarwalandGitHub d200b07466 Merge pull request #11809 from appwrite/codex/fix-console-null-route
[codex] Handle null console routes in API middleware
2026-04-07 14:42:08 +05:30
Chirag Aggarwal 92abfb31aa fix null route guard placement 2026-04-07 14:40:18 +05:30
Chirag Aggarwal 6c56eee0f4 test console route not found error type 2026-04-07 14:39:48 +05:30
Chirag Aggarwal 399c37d943 fix console null route handling 2026-04-07 14:33:43 +05:30
Matej Bačo f3acadd53c Fix linter 2026-04-07 10:43:57 +02:00
Matej Bačo 331fcee710 Merge branch '1.9.x' into feat-public-platform-api 2026-04-07 10:43:11 +02:00
Chirag AggarwalandGitHub 951a96ae01 Merge pull request #11564 from appwrite/feat/migrate-di-container
feat: migrate from static Http::setResource() to DI Container
2026-04-07 13:58:10 +05:30
Chirag Aggarwal 7864a5b9d1 fix: use --force-with-lease on SDK dev branch push
After resetting dev to the base branch, the remote dev may have
diverged history from squash merges. A plain push would be rejected
as non-fast-forward. Using --force-with-lease safely overwrites
the remote since we just fetched.
2026-04-07 12:52:09 +05:30
Chirag Aggarwal f11bd7ce0e fix: reset SDK dev branch to base branch before pushing
The dev branch was being reset to origin/dev, which retains stale
commits after squash merges. This caused recurring merge conflicts
and inflated PR diffs. Using `checkout -B dev <baseBranch>` ensures
dev always starts fresh from the default branch.
2026-04-07 12:50:18 +05:30
Shimon NewmanandGitHub ed91549681 Merge pull request #11805 from appwrite/revert-schedule-functions-updates
revert ScheduleFunctions.php updates
2026-04-06 22:38:05 +03:00
shimon 04173600ae revert ScheduleFunctions.php updates 2026-04-06 22:33:18 +03:00
Chirag Aggarwal b74d4d45f9 Merge request-scoped cookie resources 2026-04-06 13:21:33 +05:30
Chirag AggarwalandGitHub aa9a58b44d Merge pull request #11799 from appwrite/codex/request-scoped-cookie-domain
Use request-scoped cookie domain resource
2026-04-06 13:18:27 +05:30
Chirag Aggarwal 59a773e9a0 Document migration host local-domain handling 2026-04-06 12:47:06 +05:30
Chirag Aggarwal e3053bb83d Remove dead cookie config defaults 2026-04-06 12:44:48 +05:30
Chirag Aggarwal 1f7fc4bd40 Use request-scoped domain verification 2026-04-06 12:43:05 +05:30
Chirag Aggarwal d1b59ff3f3 Remove unused cookie domain locals 2026-04-06 12:30:48 +05:30
Chirag Aggarwal 221b52bac0 Add request-scoped cookie domain resource 2026-04-06 12:30:25 +05:30
Chirag Aggarwal b8ed30db55 Fix CORS header override for analyze 2026-04-06 12:23:50 +05:30
Chirag AggarwalandGitHub be56317bf2 Merge branch '1.9.x' into feat/migrate-di-container 2026-04-06 12:13:31 +05:30
Chirag AggarwalandGitHub d1549b225b Merge branch '1.9.x' into feat/migrate-di-container 2026-04-06 08:35:58 +05:30
Aditya a06ba57384 fix appwrite auth broken link in readme 2026-04-05 23:39:13 +05:30
Chirag AggarwalandGitHub dffcfe8ee4 Merge branch '1.9.x' into feat/migrate-di-container 2026-04-05 21:16:30 +05:30
Chirag Aggarwal 452440f3c0 fix: use released cli container support 2026-04-05 21:03:17 +05:30
Chirag Aggarwal b236e2546b lock file 2026-04-05 20:10:00 +05:30
Chirag Aggarwal 412d09b801 remove unrelated changes 2026-04-05 20:06:13 +05:30
Chirag AggarwalandGitHub f3b9121a14 Merge branch '1.9.x' into feat/migrate-di-container 2026-04-04 15:57:12 +05:30
Chirag Aggarwal 3018b478ba Fix database transaction and vectors migration flakiness 2026-04-02 17:25:13 +05:30
Chirag Aggarwal 094fe90499 Merge remote-tracking branch 'origin/1.9.x' into feat/migrate-di-container
# Conflicts:
#	app/worker.php
2026-04-02 16:37:58 +05:30
Chirag Aggarwal 4df5f4a18f fix: scope composer lock to intended package updates 2026-04-02 14:06:45 +05:30
Chirag Aggarwal e8bcc94187 fix: keep composer lock scoped to intended updates 2026-04-02 13:59:52 +05:30
Chirag Aggarwal e576cd5082 Merge branch '1.9.x' into feat/migrate-di-container 2026-04-02 13:32:31 +05:30
Chirag Aggarwal 30befc6a60 fix: remove strict pool size exception 2026-04-02 11:41:35 +05:30
Chirag Aggarwal d13644a47a fix: make pool sizing runtime-aware 2026-04-02 11:38:19 +05:30
Chirag Aggarwal 4a905a6ac9 Merge branch '1.9.x' into feat/migrate-di-container
Resolve conflicts keeping DI container migration (container->set pattern)
while incorporating 1.9.x fixes: PHPStan unused variable cleanup in
GraphQL Resolvers, (int) casts in Builds.php, and phpstan-baseline removal.
2026-04-02 11:17:32 +05:30
Matej Bačo b554243447 Merge branch '1.9.x' into feat-public-platform-api 2026-04-01 13:35:46 +02:00
Chirag Aggarwal 15b2ab321e fix: restore pool size validation to prevent silent connection exhaustion
The old guard that threw when workerCount > instanceConnections was
removed during the DI migration, causing pool size to silently floor
to 1. This can lead to connection exhaustion on multi-core hosts.
2026-04-01 16:34:58 +05:30
Chirag Aggarwal 789870b545 fix: preserve multi-value headers like Set-Cookie instead of comma-joining
addHeader() already accumulates multiple values for the same key into an
array internally, so calling it once per value is the correct approach.
Comma-joining violates RFC 6265 for Set-Cookie headers.
2026-04-01 15:43:14 +05:30
Chirag Aggarwal c9f7b7f0d9 fix: address PR review findings from code review
- Add Console::error() fallback in Bus::dispatch() so listener failures
  are visible even without telemetry (C1/M7)
- Remove duplicate $max/$sleep assignments in createDatabase (M1)
- Remove duplicate @param in Event::generateEvents docblock (M2)
- Remove unused $plan parameter from plan resource factory (M3)
- Fix inconsistent indentation in certificate init block (L2)
- Add explicit return null in session resource factory (M6)
2026-04-01 15:42:15 +05:30
Chirag Aggarwal cba7e53898 chore: fix composer 2026-04-01 15:37:34 +05:30
Chirag Aggarwal fb26da5df1 analyze fixes 2026-04-01 15:15:48 +05:30
Chirag Aggarwal eb8455bd76 revert 2026-04-01 14:29:20 +05:30
Chirag AggarwalandGitHub a76a03d988 Merge branch '1.9.x' into feat/migrate-di-container 2026-04-01 14:22:13 +05:30
Chirag Aggarwal cc04c682b0 chore: use phpstan-baseline.neon from 1.9.x 2026-04-01 11:49:43 +05:30
Chirag Aggarwal 66ba483b6a chore: remove inapplicable phpstan baseline entries from 1.9.x merge
$register variable.undefined (app/http.php) and binary op (app/worker.php) suppressions don't apply to this branch's rewritten DI container code.
2026-04-01 11:48:41 +05:30
Chirag Aggarwal 908e408480 Merge remote-tracking branch 'origin/1.9.x' into feat/migrate-di-container
# Conflicts:
#	app/init/resources.php
#	composer.json
#	composer.lock
#	phpstan-baseline.neon
2026-04-01 11:46:13 +05:30
Chirag Aggarwal ff903e7cbb lock file 2026-03-28 20:39:08 +05:30
Chirag AggarwalandGitHub 10ebf942b1 Merge branch '1.9.x' into feat/migrate-di-container 2026-03-28 20:36:08 +05:30
Matej Bačo d9a75c5938 Improved backwards compatibility 2026-03-27 15:34:34 +01:00
Matej Bačo bb80e50d01 Self review after refactor 2026-03-27 14:00:44 +01:00
Matej Bačo 644840ec66 Refactor to new platforms interfaces 2026-03-27 13:45:58 +01:00
Matej Bačo 0d6d6a0a35 Make tests pass 2026-03-25 16:11:01 +01:00
Matej Bačo e8d4065af5 Improve tests human review 2026-03-25 15:54:10 +01:00
Matej Bačo 008110b3f7 Platform API tests 2026-03-25 15:47:11 +01:00
Matej Bačo ae99d59aba CodeQL review 2026-03-24 14:52:58 +01:00
Matej Bačo a06aaaf9ca Remove Db schema changes 2026-03-24 13:55:54 +01:00
Chirag AggarwalandGitHub 4970d1b6f2 Merge branch '1.9.x' into feat/migrate-di-container 2026-03-24 18:15:45 +05:30
Matej Bačo 094cd180b5 Remove leftover 2026-03-24 13:41:12 +01:00
Matej Bačo a8f43f3486 Update DB schema 2026-03-24 13:36:11 +01:00
Matej Bačo 038f4b5992 More backwards compatibility fixes 2026-03-24 13:33:15 +01:00
Matej Bačo c903fb87ac Fix backwards compatibility 2026-03-24 12:53:25 +01:00
Matej Bačo e5841c4cc0 Fix syntax error 2026-03-24 11:29:24 +01:00
Matej Bačo bb7815bd57 Merge branch '1.9.x' into feat-public-platform-api 2026-03-24 11:18:18 +01:00
Chirag Aggarwal 0c33d981a7 fix analyze 2026-03-24 10:47:41 +05:30
Chirag Aggarwal 36d8ad6e7c lock file 2026-03-24 10:21:40 +05:30
Chirag Aggarwal fbce66d500 fix merge conflict 2026-03-24 10:19:34 +05:30
Chirag Aggarwal 89db65299d Merge remote-tracking branch 'origin/1.9.x' into feat/migrate-di-container 2026-03-24 10:15:38 +05:30
Chirag Aggarwal ea6a05be4f fix analyze 2026-03-24 07:51:23 +05:30
Chirag Aggarwal 355acf6824 Merge branch '1.9.x' into feat/migrate-di-container 2026-03-23 23:01:30 +05:30
Chirag Aggarwal c2988efa08 chore: use stable 2026-03-23 22:51:49 +05:30
Matej Bačo 8ccf5094f8 fix multi-model conditions 2026-03-23 16:42:14 +01:00
Matej Bačo 39f2d24907 AI code review fixes 2026-03-23 16:10:12 +01:00
Matej Bačo d3c5a425e7 Implement public platform API 2026-03-23 15:46:11 +01:00
Chirag Aggarwal 4641596a6d use stable 2026-03-23 10:28:26 +05:30
Chirag Aggarwal d932527561 add null collacing 2026-03-23 10:27:10 +05:30
Chirag Aggarwal 89c072e223 fix analyze 2026-03-23 10:20:45 +05:30
Chirag Aggarwal 6421bc8689 fn name 2026-03-23 10:08:19 +05:30
Chirag Aggarwal d008d9bff0 merge conficts 2026-03-23 10:01:27 +05:30
Chirag Aggarwal f6dc6359bb Merge branch '1.9.x' into feat/migrate-di-container 2026-03-23 10:01:03 +05:30
Chirag Aggarwal 032638e896 fix 2026-03-20 15:35:28 +05:30
Chirag Aggarwal 10cc6a8040 fix global pools state 2026-03-20 14:09:43 +05:30
Chirag Aggarwal 9ecdbf5950 func exists 2026-03-20 13:13:07 +05:30
Chirag Aggarwal 55b436c67b lock file 2026-03-20 12:17:28 +05:30
Chirag Aggarwal 05defcc6e0 Merge branch '1.9.x' into feat/migrate-di-container 2026-03-20 12:12:29 +05:30
Chirag Aggarwal 6700340ef3 fix realtime 2026-03-19 23:26:49 +05:30
Chirag Aggarwal 4224f6ea5a merge conficts 2026-03-19 22:15:31 +05:30
Chirag Aggarwal 8d925f3670 merge conficts 2026-03-19 21:35:19 +05:30
Chirag Aggarwal d2875c9bf6 Merge branch '1.8.x' into feat/migrate-di-container 2026-03-19 21:35:06 +05:30
Chirag Aggarwal c002f4afe3 fix specs generation 2026-03-19 21:23:00 +05:30
Chirag Aggarwal ecb8104340 register resources 2026-03-19 17:37:47 +05:30
Chirag Aggarwal e6090800e2 register resources 2026-03-19 17:35:31 +05:30
Chirag AggarwalandGitHub 0237cced6b Merge branch '1.8.x' into feat/migrate-di-container 2026-03-19 14:10:20 +05:30
Chirag Aggarwal 625ec4ce91 sync changes 2026-03-19 14:09:04 +05:30
Chirag Aggarwal 5339592b3f update http 2026-03-19 10:20:47 +05:30
Chirag Aggarwal 9f1d092717 analyze fixes 2026-03-19 10:17:25 +05:30
Chirag Aggarwal e38a4e4347 update queue 2026-03-19 10:08:40 +05:30
Chirag Aggarwal fdbc5b6737 update queue 2026-03-19 10:07:08 +05:30
Chirag Aggarwal fa2b7955d0 update dependencies 2026-03-19 10:04:47 +05:30
Chirag Aggarwal ea11af1e15 chore: worker changes 2026-03-18 17:50:25 +05:30
Chirag AggarwalandGitHub 6cbae88131 Merge branch '1.8.x' into feat/migrate-di-container 2026-03-18 17:14:45 +05:30
Chirag Aggarwal 03b2755aaf fix async execution 2026-03-18 16:43:14 +05:30
Chirag AggarwalandGitHub 2095a94bc5 Merge branch '1.8.x' into feat/migrate-di-container 2026-03-18 14:19:59 +05:30
Chirag Aggarwal df7796d5cb restore phpunit 2026-03-18 14:19:43 +05:30
Chirag Aggarwal d177e3adfc fix pool issue 2026-03-18 11:27:26 +05:30
Chirag Aggarwal d04ac5c3e6 fix pool issue 2026-03-18 09:42:18 +05:30
Chirag Aggarwal 60939da801 fix graphql 2026-03-17 21:39:50 +05:30
Chirag Aggarwal cdb301a293 fix PHPStan errors without regenerating baseline
- Fix dispatch() type hint to use \Swoole\Http\Server instead of Utopia adapter
- Remove unused $register from go() closure in http.php
- Remove unnecessary ?? '' on non-nullable $hostname
- Remove unsupported override: param from addHeader() call
- Update Resolvers.php for new getResource()/execute() signatures
- Migrate Installer/Server.php from static Http::setResource() to container
- Remove stale baseline entries, add 1 for pre-existing Deployment.php issue
2026-03-17 17:30:42 +05:30
Chirag Aggarwal 14a9aa890f fix issues 2026-03-17 17:18:57 +05:30
Chirag Aggarwal d3833d06d5 Merge 1.8.x (accept theirs) 2026-03-17 17:15:54 +05:30
Chirag Aggarwal 5ba19ec763 fix alias 2026-03-17 17:14:35 +05:30
Chirag Aggarwal 0564936c4f update file name 2026-03-17 16:29:21 +05:30
Chirag Aggarwal c2795376d8 update server 2026-03-17 16:25:52 +05:30
Chirag AggarwalandGitHub 2b898dfe50 Merge branch '1.8.x' into feat/migrate-di-container 2026-03-17 15:22:02 +05:30
Chirag Aggarwal fa1404be52 cleanup 2026-03-17 15:20:29 +05:30
Chirag Aggarwal be87675e1c fix realtime 2026-03-17 15:03:55 +05:30
Chirag Aggarwal b1edf75c4e phpunit 2026-03-17 14:29:04 +05:30
Chirag Aggarwal 8a0f092342 fix: only throw container lookup errors 2026-03-17 14:28:37 +05:30
Chirag Aggarwal 87b02de612 baseline 2026-03-17 11:32:15 +05:30
Chirag Aggarwal f8e9f71de3 move resources 2026-03-17 11:19:17 +05:30
Chirag Aggarwal c055c32371 enable coroutines 2026-03-17 10:52:35 +05:30
Chirag Aggarwal d60858cd8f fix phpstan 2026-03-17 10:37:49 +05:30
Chirag Aggarwal 22bd655ad1 fix autoload 2026-03-17 10:28:08 +05:30
Chirag Aggarwal c900b22dc0 fix connection container and view class 2026-03-17 09:52:55 +05:30
Chirag Aggarwal 27e5ade92b fix login 2026-03-17 09:43:54 +05:30
Chirag Aggarwal d9c1b9db2a chore: register request resources seperately 2026-03-17 08:49:43 +05:30
Chirag Aggarwal eb399c7bd0 wip 2026-03-16 23:45:18 +05:30
Chirag Aggarwal b8e51366d7 add missing compression methods 2026-03-16 23:32:31 +05:30
Chirag Aggarwal b75cd993be missing trusted ip headers 2026-03-16 23:27:34 +05:30
Chirag Aggarwal a1bc503ce4 formatting 2026-03-16 23:23:03 +05:30
Chirag Aggarwal e475a7ac5a lock file 2026-03-16 23:19:07 +05:30
Chirag Aggarwal 2dc24dabfd Merge branch '1.8.x' into feat/migrate-di-container 2026-03-16 23:10:09 +05:30
Chirag Aggarwal aaa2a0525f feat: migrate from static Http::setResource() to DI Container
Upgrade utopia-php/framework from 0.33.x to 0.34.x which removes the
static Http::setResource() API. Resources are now registered on a
Utopia\DI\Container instance.

- Replace 81 Http::setResource() calls in resources.php with $container->set()
- Refactor http.php to use Swoole HttpServer adapter with shared container
- Refactor realtime.php to use FPM adapter with global container
- Refactor cli.php to use direct $cli->setResource() calls
- Update Specs.php to use local container + FPM adapter
- Update Migrate.php to inject console document instead of creating Http instance
- Update GraphQL Schema.php to use instance setResource()
2026-03-16 23:00:36 +05:30
86 changed files with 6937 additions and 2762 deletions
+21
View File
@@ -161,6 +161,27 @@ jobs:
- name: Run PHPStan
run: composer analyze -- --no-progress
specs:
name: Checks / Specs
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v6
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: swoole
tools: composer:v2
coverage: none
- name: Install dependencies
run: composer install --prefer-dist --no-progress --ignore-platform-reqs
- name: Generate specs
run: _APP_STORAGE_LIMIT=5368709120 php app/cli.php specs --version=latest --git=no
locale:
name: Checks / Locale
runs-on: ubuntu-latest
+1 -1
View File
@@ -44,7 +44,7 @@ Table of Contents:
## Products
- **[Appwrite Auth](https://appwrite.io/docs/products/authentication)** - Secure user authentication with multiple login methods including email/password, SMS, OAuth, anonymous sessions, and magic links. Includes session management, multi-factor authentication, and user verification flows.
- **[Appwrite Auth](https://appwrite.io/docs/products/auth)** - Secure user authentication with multiple login methods including email/password, SMS, OAuth, anonymous sessions, and magic links. Includes session management, multi-factor authentication, and user verification flows.
- **[Appwrite Databases](https://appwrite.io/docs/products/databases)** - Scalable structured data storage with support for databases, tables, and rows. Includes querying, pagination, indexing, and relationships to model complex application data.
+34 -38
View File
@@ -18,13 +18,15 @@ use Swoole\Timer;
use Utopia\Cache\Adapter\Pool as CachePool;
use Utopia\Cache\Adapter\Sharding;
use Utopia\Cache\Cache;
use Utopia\CLI\Adapters\Generic;
use Utopia\CLI\CLI;
use Utopia\Config\Config;
use Utopia\Console;
use Utopia\Database\Adapter\Pool as DatabasePool;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\DI\Dependency;
use Utopia\DI\Container;
use Utopia\DSN\DSN;
use Utopia\Logger\Log;
use Utopia\Platform\Service;
@@ -47,7 +49,7 @@ require_once __DIR__ . '/controllers/general.php';
global $register;
$platform = new Appwrite();
$args = $platform->getEnv('argv');
$args = $_SERVER['argv'] ?? [];
\array_shift($args);
if (! isset($args[0])) {
@@ -56,21 +58,15 @@ if (! isset($args[0])) {
}
$taskName = $args[0];
$container = new Container();
$cli = new CLI(new Generic(), $_SERVER['argv'] ?? [], $container);
$platform->setCli($cli);
$platform->init(Service::TYPE_TASK);
$cli = $platform->getCli();
$setResource = function (string $name, callable $callback, array $injections = []) use ($cli) {
$dependency = new Dependency();
$dependency->setName($name)->setCallback($callback);
foreach ($injections as $injection) {
$dependency->inject($injection);
}
$cli->setResource($dependency);
};
$container->set('register', fn () => $register, []);
$setResource('register', fn () => $register, []);
$setResource('cache', function ($pools) {
$container->set('cache', function ($pools) {
$list = Config::getParam('pools-cache', []);
$adapters = [];
@@ -81,18 +77,18 @@ $setResource('cache', function ($pools) {
return new Cache(new Sharding($adapters));
}, ['pools']);
$setResource('pools', function (Registry $register) {
$container->set('pools', function (Registry $register) {
return $register->get('pools');
}, ['register']);
$setResource('authorization', function () {
$container->set('authorization', function () {
$authorization = new Authorization();
$authorization->disable();
return $authorization;
}, []);
$setResource('dbForPlatform', function ($pools, $cache, $authorization) {
$container->set('dbForPlatform', function ($pools, $cache, $authorization) {
$sleep = 3;
$maxAttempts = 5;
$attempts = 0;
@@ -135,17 +131,17 @@ $setResource('dbForPlatform', function ($pools, $cache, $authorization) {
return $dbForPlatform;
}, ['pools', 'cache', 'authorization']);
$setResource('console', function () {
$container->set('console', function () {
return new Document(Config::getParam('console'));
}, []);
$setResource(
$container->set(
'isResourceBlocked',
fn () => fn (Document $project, string $resourceType, ?string $resourceId) => false,
[]
);
$setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache, $authorization) {
$container->set('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache, $authorization) {
$databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools
return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$databases) {
@@ -207,10 +203,10 @@ $setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $c
};
}, ['pools', 'dbForPlatform', 'cache', 'authorization']);
$setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
$container->set('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
$database = null;
return function (?Document $project = null) use ($pools, $cache, $database, $authorization) {
return function (?Document $project = null) use ($pools, $cache, &$database, $authorization) {
if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant($project->getSequence());
return $database;
@@ -235,41 +231,41 @@ $setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $a
return $database;
};
}, ['pools', 'cache', 'authorization']);
$setResource('publisher', function (Group $pools) {
$container->set('publisher', function (Group $pools) {
return new BrokerPool(publisher: $pools->get('publisher'));
}, ['pools']);
$setResource('publisherDatabases', function (BrokerPool $publisher) {
$container->set('publisherDatabases', function (BrokerPool $publisher) {
return $publisher;
}, ['publisher']);
$setResource('publisherFunctions', function (BrokerPool $publisher) {
$container->set('publisherFunctions', function (BrokerPool $publisher) {
return $publisher;
}, ['publisher']);
$setResource('publisherMigrations', function (BrokerPool $publisher) {
$container->set('publisherMigrations', function (BrokerPool $publisher) {
return $publisher;
}, ['publisher']);
$setResource('publisherMessaging', function (BrokerPool $publisher) {
$container->set('publisherMessaging', function (BrokerPool $publisher) {
return $publisher;
}, ['publisher']);
$setResource('usage', function () {
$container->set('usage', function () {
return new UsageContext();
}, []);
$setResource('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher(
$container->set('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher(
$publisher,
new Queue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME))
), ['publisher']);
$setResource('queueForStatsResources', function (Publisher $publisher) {
$container->set('queueForStatsResources', function (Publisher $publisher) {
return new StatsResources($publisher);
}, ['publisher']);
$setResource('queueForFunctions', function (Publisher $publisher) {
$container->set('queueForFunctions', function (Publisher $publisher) {
return new Func($publisher);
}, ['publisher']);
$setResource('queueForDeletes', function (Publisher $publisher) {
$container->set('queueForDeletes', function (Publisher $publisher) {
return new Delete($publisher);
}, ['publisher']);
$setResource('queueForCertificates', function (Publisher $publisher) {
$container->set('queueForCertificates', function (Publisher $publisher) {
return new Certificate($publisher);
}, ['publisher']);
$setResource('logError', function (Registry $register) {
$container->set('logError', function (Registry $register) {
return function (Throwable $error, string $namespace, string $action) use ($register) {
Console::error('[Error] Timestamp: ' . date('c', time()));
Console::error('[Error] Type: ' . get_class($error));
@@ -321,13 +317,13 @@ $setResource('logError', function (Registry $register) {
};
}, ['register']);
$setResource('executor', fn () => new Executor(), []);
$container->set('executor', fn () => new Executor(), []);
$setResource('bus', function (Registry $register) use ($cli) {
return $register->get('bus')->setResolver(fn (string $name) => $cli->getResource($name));
$container->set('bus', function (Registry $register) use ($container) {
return $register->get('bus')->setResolver(fn (string $name) => $container->get($name));
}, ['register']);
$setResource('telemetry', fn () => new NoTelemetry(), []);
$container->set('telemetry', fn () => new NoTelemetry(), []);
$exitCode = 0;
+3 -3
View File
@@ -594,7 +594,7 @@ $platformCollections = [
'filters' => [],
],
[
'$id' => ID::custom('key'),
'$id' => ID::custom('key'), // For app platforms
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
@@ -605,7 +605,7 @@ $platformCollections = [
'filters' => [],
],
[
'$id' => ID::custom('store'),
'$id' => ID::custom('store'), // Unused at the moment
'type' => Database::VAR_STRING,
'format' => '',
'size' => 256,
@@ -616,7 +616,7 @@ $platformCollections = [
'filters' => [],
],
[
'$id' => ID::custom('hostname'),
'$id' => ID::custom('hostname'), // For web platforms
'type' => Database::VAR_STRING,
'format' => '',
'size' => 256,
+10
View File
@@ -1179,6 +1179,16 @@ return [
'description' => 'Platform with the requested ID could not be found.',
'code' => 404,
],
Exception::PLATFORM_METHOD_UNSUPPORTED => [
'name' => Exception::PLATFORM_METHOD_UNSUPPORTED,
'description' => 'The requested platform has invalid type. Please use corresponding update method for the platform type.',
'code' => 400,
],
Exception::PLATFORM_ALREADY_EXISTS => [
'name' => Exception::PLATFORM_ALREADY_EXISTS,
'description' => 'Platform with the same ID already exists in this project. Try again with a different ID.',
'code' => 409,
],
Exception::VARIABLE_NOT_FOUND => [
'name' => Exception::VARIABLE_NOT_FOUND,
'description' => 'Variable with the requested ID could not be found.',
+52 -34
View File
@@ -207,7 +207,7 @@ function sendSessionAlert(Locale $locale, Document $user, Document $project, arr
}
$createSession = function (string $userId, string $secret, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Store $store, ProofsToken $proofForToken, ProofsCode $proofForCode, Authorization $authorization) {
$createSession = function (string $userId, string $secret, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Store $store, ProofsToken $proofForToken, ProofsCode $proofForCode, bool $domainVerification, ?string $cookieDomain, Authorization $authorization) {
// Attempt to decode secret as a JWT (used by OAuth2 token flow to carry provider info)
$oauthProvider = null;
@@ -345,7 +345,7 @@ $createSession = function (string $userId, string $secret, Request $request, Res
->setProperty('secret', $sessionSecret)
->encode();
if (!Config::getParam('domainVerification')) {
if (!$domainVerification) {
$response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded]));
}
@@ -353,8 +353,8 @@ $createSession = function (string $userId, string $secret, Request $request, Res
$protocol = $request->getProtocol();
$response
->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, null)
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite'))
->setStatusCode(Response::STATUS_CODE_CREATED);
$countryName = $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown'));
@@ -719,7 +719,9 @@ Http::delete('/v1/account/sessions')
->inject('queueForDeletes')
->inject('store')
->inject('proofForToken')
->action(function (Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken) {
->inject('domainVerification')
->inject('cookieDomain')
->action(function (Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain) {
$protocol = $request->getProtocol();
$sessions = $user->getAttribute('sessions', []);
@@ -728,7 +730,7 @@ Http::delete('/v1/account/sessions')
foreach ($sessions as $session) {/** @var Document $session */
$dbForProject->deleteDocument('sessions', $session->getId());
if (!Config::getParam('domainVerification')) {
if (!$domainVerification) {
$response->addHeader('X-Fallback-Cookies', \json_encode([]));
}
@@ -741,8 +743,8 @@ Http::delete('/v1/account/sessions')
// If current session delete the cookies too
$response
->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
->addCookie($store->getKey(), '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'));
->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', $cookieDomain, ('https' == $protocol), true, null)
->addCookie($store->getKey(), '', \time() - 3600, '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite'));
// Use current session for events.
$currentSession = $session;
@@ -849,7 +851,9 @@ Http::delete('/v1/account/sessions/:sessionId')
->inject('queueForDeletes')
->inject('store')
->inject('proofForToken')
->action(function (?string $sessionId, ?\DateTime $requestTimestamp, Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken) {
->inject('domainVerification')
->inject('cookieDomain')
->action(function (?string $sessionId, ?\DateTime $requestTimestamp, Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain) {
$protocol = $request->getProtocol();
$sessionId = ($sessionId === 'current')
@@ -875,13 +879,13 @@ Http::delete('/v1/account/sessions/:sessionId')
->setAttribute('current', true)
->setAttribute('countryName', $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown')));
if (!Config::getParam('domainVerification')) {
if (!$domainVerification) {
$response->addHeader('X-Fallback-Cookies', \json_encode([]));
}
$response
->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
->addCookie($store->getKey(), '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'));
->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', $cookieDomain, ('https' == $protocol), true, null)
->addCookie($store->getKey(), '', \time() - 3600, '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite'));
}
$dbForProject->purgeCachedDocument('users', $user->getId());
@@ -1035,8 +1039,10 @@ Http::post('/v1/account/sessions/email')
->inject('store')
->inject('proofForPassword')
->inject('proofForToken')
->inject('domainVerification')
->inject('cookieDomain')
->inject('authorization')
->action(function (string $email, string $password, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Hooks $hooks, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, Authorization $authorization) {
->action(function (string $email, string $password, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Hooks $hooks, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain, Authorization $authorization) {
$email = \strtolower($email);
$protocol = $request->getProtocol();
@@ -1097,28 +1103,28 @@ Http::post('/v1/account/sessions/email')
]));
}
$dbForProject->purgeCachedDocument('users', $user->getId());
$session = $dbForProject->createDocument('sessions', $session->setAttribute('$permissions', [
Permission::read(Role::user($user->getId())),
Permission::update(Role::user($user->getId())),
Permission::delete(Role::user($user->getId())),
]));
$dbForProject->purgeCachedDocument('users', $user->getId());
$encoded = $store
->setProperty('id', $user->getId())
->setProperty('secret', $secret)
->encode();
if (!Config::getParam('domainVerification')) {
if (!$domainVerification) {
$response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded]));
}
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
$response
->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, null)
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite'))
->setStatusCode(Response::STATUS_CODE_CREATED)
;
@@ -1184,8 +1190,10 @@ Http::post('/v1/account/sessions/anonymous')
->inject('store')
->inject('proofForPassword')
->inject('proofForToken')
->inject('domainVerification')
->inject('cookieDomain')
->inject('authorization')
->action(function (Request $request, Response $response, Locale $locale, User $user, Document $project, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, Authorization $authorization) {
->action(function (Request $request, Response $response, Locale $locale, User $user, Document $project, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain, Authorization $authorization) {
$protocol = $request->getProtocol();
if ('console' === $project->getId()) {
@@ -1276,15 +1284,15 @@ Http::post('/v1/account/sessions/anonymous')
->setProperty('secret', $secret)
->encode();
if (!Config::getParam('domainVerification')) {
if (!$domainVerification) {
$response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded]));
}
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
$response
->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, null)
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite'))
->setStatusCode(Response::STATUS_CODE_CREATED)
;
@@ -1339,7 +1347,9 @@ Http::post('/v1/account/sessions/token')
->inject('store')
->inject('proofForToken')
->inject('proofForCode')
->inject('authorization')
->inject('domainVerification')
->inject('cookieDomain')
->inject('authorization')
->action($createSession);
Http::get('/v1/account/sessions/oauth2/:provider')
@@ -1538,8 +1548,10 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
->inject('proofForPassword')
->inject('proofForToken')
->inject('plan')
->inject('domainVerification')
->inject('cookieDomain')
->inject('authorization')
->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Validator $redirectValidator, Document $devKey, User $user, Database $dbForProject, Database $dbForPlatform, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, array $plan, Authorization $authorization) use ($oauthDefaultSuccess) {
->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Validator $redirectValidator, Document $devKey, User $user, Database $dbForProject, Database $dbForPlatform, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, array $plan, bool $domainVerification, ?string $cookieDomain, Authorization $authorization) use ($oauthDefaultSuccess) {
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$port = $request->getPort();
$callbackBase = $protocol . '://' . $request->getHostname();
@@ -2055,7 +2067,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
->setProperty('secret', $secret)
->encode();
if (!Config::getParam('domainVerification')) {
if (!$domainVerification) {
$response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded]));
}
@@ -2068,14 +2080,14 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
// TODO: Remove this deprecated workaround - support only token
if ($state['success']['path'] == $oauthDefaultSuccess) {
$query['project'] = $project->getId();
$query['domain'] = Config::getParam('cookieDomain');
$query['domain'] = $cookieDomain;
$query['key'] = $store->getKey();
$query['secret'] = $encoded;
}
$response
->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'));
->addCookie($store->getKey() . '_legacy', $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, null)
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite'));
}
if (isset($sessionUpgrade) && $sessionUpgrade && isset($session)) {
@@ -2886,11 +2898,13 @@ Http::put('/v1/account/sessions/magic-url')
->inject('queueForMails')
->inject('store')
->inject('proofForCode')
->inject('domainVerification')
->inject('cookieDomain')
->inject('authorization')
->action(function ($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForCode, $authorization) use ($createSession) {
->action(function ($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForCode, $domainVerification, $cookieDomain, $authorization) use ($createSession) {
$proofForToken = new ProofsToken(TOKEN_LENGTH_MAGIC_URL);
$proofForToken->setHash(new Sha());
$createSession($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForToken, $proofForCode, $authorization);
$createSession($userId, $secret, $request, $response, $user, $dbForProject, $project, $platform, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForToken, $proofForCode, $domainVerification, $cookieDomain, $authorization);
});
Http::put('/v1/account/sessions/phone')
@@ -2936,6 +2950,8 @@ Http::put('/v1/account/sessions/phone')
->inject('store')
->inject('proofForToken')
->inject('proofForCode')
->inject('domainVerification')
->inject('cookieDomain')
->inject('authorization')
->action($createSession);
@@ -3727,7 +3743,9 @@ Http::patch('/v1/account/status')
->inject('dbForProject')
->inject('queueForEvents')
->inject('store')
->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Store $store) {
->inject('domainVerification')
->inject('cookieDomain')
->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Store $store, bool $domainVerification, ?string $cookieDomain) {
$user->setAttribute('status', false);
@@ -3737,14 +3755,14 @@ Http::patch('/v1/account/status')
->setParam('userId', $user->getId())
->setPayload($response->output($user, Response::MODEL_ACCOUNT));
if (!Config::getParam('domainVerification')) {
if (!$domainVerification) {
$response->addHeader('X-Fallback-Cookies', \json_encode([]));
}
$protocol = $request->getProtocol();
$response
->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
->addCookie($store->getKey(), '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
->addCookie($store->getKey() . '_legacy', '', \time() - 3600, '/', $cookieDomain, ('https' == $protocol), true, null)
->addCookie($store->getKey(), '', \time() - 3600, '/', $cookieDomain, ('https' == $protocol), true, Config::getParam('cookieSamesite'))
;
$response->dynamic($user, Response::MODEL_ACCOUNT);
-266
View File
@@ -1093,272 +1093,6 @@ Http::post('/v1/projects/:projectId/jwts')
])]), Response::MODEL_JWT);
});
// Platforms
Http::post('/v1/projects/:projectId/platforms')
->desc('Create platform')
->groups(['api', 'projects'])
->label('audits.event', 'platforms.create')
->label('audits.resource', 'project/{request.projectId}')
->label('scope', 'platforms.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'platforms',
name: 'createPlatform',
description: '/docs/references/projects/create-platform.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
model: Response::MODEL_PLATFORM,
)
]
))
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param(
'type',
null,
new WhiteList([
Platform::TYPE_WEB,
Platform::TYPE_FLUTTER_WEB,
Platform::TYPE_FLUTTER_IOS,
Platform::TYPE_FLUTTER_ANDROID,
Platform::TYPE_FLUTTER_LINUX,
Platform::TYPE_FLUTTER_MACOS,
Platform::TYPE_FLUTTER_WINDOWS,
Platform::TYPE_APPLE_IOS,
Platform::TYPE_APPLE_MACOS,
Platform::TYPE_APPLE_WATCHOS,
Platform::TYPE_APPLE_TVOS,
Platform::TYPE_ANDROID,
Platform::TYPE_UNITY,
Platform::TYPE_REACT_NATIVE_IOS,
Platform::TYPE_REACT_NATIVE_ANDROID,
], true),
'Platform type. Possible values are: web, flutter-web, flutter-ios, flutter-android, flutter-linux, flutter-macos, flutter-windows, apple-ios, apple-macos, apple-watchos, apple-tvos, android, unity, react-native-ios, react-native-android.'
)
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
->param('key', '', new Text(256), 'Package name for Android or bundle ID for iOS or macOS. Max length: 256 chars.', true)
->param('store', '', new Text(256), 'App store or Google Play store ID. Max length: 256 chars.', true)
->param('hostname', '', new Hostname(), 'Platform client hostname. Max length: 256 chars.', true)
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, string $type, string $name, string $key, string $store, string $hostname, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$platform = new Document([
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'projectInternalId' => $project->getSequence(),
'projectId' => $project->getId(),
'type' => $type,
'name' => $name,
'key' => $key,
'store' => $store,
'hostname' => $hostname
]);
$platform = $dbForPlatform->createDocument('platforms', $platform);
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($platform, Response::MODEL_PLATFORM);
});
Http::get('/v1/projects/:projectId/platforms')
->desc('List platforms')
->groups(['api', 'projects'])
->label('scope', 'platforms.read')
->label('sdk', new Method(
namespace: 'projects',
group: 'platforms',
name: 'listPlatforms',
description: '/docs/references/projects/list-platforms.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PLATFORM_LIST,
)
]
))
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, bool $includeTotal, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$platforms = $dbForPlatform->find('platforms', [
Query::equal('projectInternalId', [$project->getSequence()]),
Query::limit(5000),
]);
$response->dynamic(new Document([
'platforms' => $platforms,
'total' => $includeTotal ? count($platforms) : 0,
]), Response::MODEL_PLATFORM_LIST);
});
Http::get('/v1/projects/:projectId/platforms/:platformId')
->desc('Get platform')
->groups(['api', 'projects'])
->label('scope', 'platforms.read')
->label('sdk', new Method(
namespace: 'projects',
group: 'platforms',
name: 'getPlatform',
description: '/docs/references/projects/get-platform.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PLATFORM,
)
]
))
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param('platformId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform unique ID.', false, ['dbForPlatform'])
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, string $platformId, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$platform = $dbForPlatform->findOne('platforms', [
Query::equal('$id', [$platformId]),
Query::equal('projectInternalId', [$project->getSequence()]),
]);
if ($platform->isEmpty()) {
throw new Exception(Exception::PLATFORM_NOT_FOUND);
}
$response->dynamic($platform, Response::MODEL_PLATFORM);
});
Http::put('/v1/projects/:projectId/platforms/:platformId')
->desc('Update platform')
->groups(['api', 'projects'])
->label('scope', 'platforms.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'platforms',
name: 'updatePlatform',
description: '/docs/references/projects/update-platform.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PLATFORM,
)
]
))
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param('platformId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform unique ID.', false, ['dbForPlatform'])
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
->param('key', '', new Text(256), 'Package name for android or bundle ID for iOS. Max length: 256 chars.', true)
->param('store', '', new Text(256), 'App store or Google Play store ID. Max length: 256 chars.', true)
->param('hostname', '', new Hostname(), 'Platform client URL. Max length: 256 chars.', true)
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, string $platformId, string $name, string $key, string $store, string $hostname, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$platform = $dbForPlatform->findOne('platforms', [
Query::equal('$id', [$platformId]),
Query::equal('projectInternalId', [$project->getSequence()]),
]);
if ($platform->isEmpty()) {
throw new Exception(Exception::PLATFORM_NOT_FOUND);
}
$platform
->setAttribute('name', $name)
->setAttribute('key', $key)
->setAttribute('store', $store)
->setAttribute('hostname', $hostname);
$dbForPlatform->updateDocument('platforms', $platform->getId(), $platform);
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
$response->dynamic($platform, Response::MODEL_PLATFORM);
});
Http::delete('/v1/projects/:projectId/platforms/:platformId')
->desc('Delete platform')
->groups(['api', 'projects'])
->label('audits.event', 'platforms.delete')
->label('audits.resource', 'project/{request.projectId}/platform/${request.platformId}')
->label('scope', 'platforms.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'platforms',
name: 'deletePlatform',
description: '/docs/references/projects/delete-platform.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_NOCONTENT,
model: Response::MODEL_NONE,
)
],
contentType: ContentType::NONE
))
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param('platformId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform unique ID.', false, ['dbForPlatform'])
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, string $platformId, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$platform = $dbForPlatform->findOne('platforms', [
Query::equal('$id', [$platformId]),
Query::equal('projectInternalId', [$project->getSequence()]),
]);
if ($platform->isEmpty()) {
throw new Exception(Exception::PLATFORM_NOT_FOUND);
}
$dbForPlatform->deleteDocument('platforms', $platformId);
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
$response->noContent();
});
// CUSTOM SMTP and Templates
Http::patch('/v1/projects/:projectId/smtp')
->desc('Update SMTP')
+6 -33
View File
@@ -61,8 +61,6 @@ use Utopia\System\System;
use Utopia\Validator;
use Utopia\Validator\Text;
Config::setParam('domainVerification', false);
Config::setParam('cookieDomain', 'localhost');
Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE);
function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Authorization $authorization, ?Key $apiKey, DeleteEvent $queueForDeletes, int $executionsRetentionCount)
@@ -754,11 +752,8 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
}
if (\is_array($values)) {
$count = 0;
foreach ($values as $value) {
$override = $count === 0;
$response->addHeader($name, $value, override: $override);
$count++;
$response->addHeader($name, $value);
}
} else {
$response->addHeader($name, $values);
@@ -904,40 +899,16 @@ Http::init()
$locale->setDefault($localeParam);
}
$origin = \parse_url($request->getOrigin($request->getReferer('')), PHP_URL_HOST);
$selfDomain = new Domain($request->getHostname());
$endDomain = new Domain((string)$origin);
Config::setParam(
'domainVerification',
($selfDomain->getRegisterable() === $endDomain->getRegisterable()) &&
$endDomain->getRegisterable() !== ''
);
$localHosts = ['localhost','localhost:'.$request->getPort()];
$migrationHost = System::getEnv('_APP_MIGRATION_HOST');
if (!empty($migrationHost)) {
// Treat the migration host like localhost because internal migration and
// CI traffic may use it before a public domain is configured.
$localHosts[] = $migrationHost;
$localHosts[] = $migrationHost.':'.$request->getPort();
}
$isLocalHost = in_array($request->getHostname(), $localHosts);
$isIpAddress = filter_var($request->getHostname(), FILTER_VALIDATE_IP) !== false;
$isConsoleProject = $project->getAttribute('$id', '') === 'console';
$isConsoleRootSession = System::getEnv('_APP_CONSOLE_ROOT_SESSION', 'disabled') === 'enabled';
Config::setParam(
'cookieDomain',
$isLocalHost || $isIpAddress
? null
: (
$isConsoleProject && $isConsoleRootSession
? '.' . $selfDomain->getRegisterable()
: '.' . $request->getHostname()
)
);
$warnings = [];
/*
@@ -1500,7 +1471,9 @@ Http::error()
try {
$cors = $utopia->getResource('cors');
foreach ($cors->headers($request->getOrigin()) as $name => $value) {
$response->addHeader($name, $value, override: true);
$response
->removeHeader($name)
->addHeader($name, $value);
}
} catch (Throwable) {
// Degrade gracefully - error response without CORS is no worse than before.
+7
View File
@@ -98,6 +98,9 @@ Http::init()
->inject('authorization')
->action(function (Http $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization) {
$route = $utopia->getRoute();
if ($route === null) {
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND);
}
/**
* Handle user authentication and session validation.
@@ -489,6 +492,10 @@ Http::init()
$request->setUser($user);
$route = $utopia->getRoute();
if ($route === null) {
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND);
}
$path = $route->getMatchedPath();
$databaseType = match (true) {
str_contains($path, '/documentsdb') => DATABASE_TYPE_DOCUMENTSDB,
+57 -54
View File
@@ -1,16 +1,13 @@
<?php
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/init.php';
require_once __DIR__ . '/init/span.php';
global $register;
$registerRequestResources = require __DIR__ . '/init/resources/request.php';
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Swoole\Constant;
use Swoole\Http\Request as SwooleRequest;
use Swoole\Http\Response as SwooleResponse;
use Swoole\Http\Server;
use Swoole\Process;
use Swoole\Table;
use Swoole\Timer;
@@ -29,6 +26,7 @@ use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Http\Adapter\Swoole\Server;
use Utopia\Http\Files;
use Utopia\Http\Http;
use Utopia\Logger\Log;
@@ -49,18 +47,35 @@ $certifiedDomains = new Table(100_000);
$certifiedDomains->column('value', Table::TYPE_INT, 1);
$certifiedDomains->create();
Http::setResource('riskyDomains', fn () => $riskyDomains);
Http::setResource('certifiedDomains', fn () => $certifiedDomains);
$http = new Server(
host: "0.0.0.0",
port: System::getEnv('PORT', 80),
mode: SWOOLE_PROCESS,
);
global $container;
$container->set('riskyDomains', fn () => $riskyDomains);
$container->set('certifiedDomains', fn () => $certifiedDomains);
$container->set('pools', function ($register) {
return $register->get('pools');
}, ['register']);
$payloadSize = 12 * (1024 * 1024); // 12MB - adding slight buffer for headers and other data that might be sent with the payload - update later with valid testing
$totalWorkers = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
$swooleAdapter = new Server(
host: "0.0.0.0",
port: System::getEnv('PORT', 80),
settings: [
Constant::OPTION_WORKER_NUM => $totalWorkers,
Constant::OPTION_DISPATCH_FUNC => dispatch(...),
Constant::OPTION_DISPATCH_MODE => SWOOLE_DISPATCH_UIDMOD,
Constant::OPTION_HTTP_COMPRESSION => false,
Constant::OPTION_PACKAGE_MAX_LENGTH => $payloadSize,
Constant::OPTION_OUTPUT_BUFFER_SIZE => $payloadSize,
Constant::OPTION_TASK_WORKER_NUM => 1, // required for the task to fetch domains background
],
container: $container,
);
$container->set('container', fn () => fn () => $swooleAdapter->getContainer());
$http = $swooleAdapter->getServer();
/**
* Assigns HTTP requests to worker threads by analyzing its payload/content.
*
@@ -69,16 +84,16 @@ $totalWorkers = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intva
* riskier tasks to a dedicated worker subset. Prefers idle workers, with fallback to random selection if necessary.
* doc: https://openswoole.com/docs/modules/swoole-server/configuration#dispatch_func
*
* @param Server $server Swoole server instance.
* @param \Swoole\Http\Server $server Swoole server instance.
* @param int $fd client ID
* @param int $type the type of data and its current state
* @param string|null $data Request content for categorization.
* @global int $totalThreads Total number of workers.
* @return int Chosen worker ID for the request.
*/
function dispatch(Server $server, int $fd, int $type, $data = null): int
function dispatch(\Swoole\Http\Server $server, int $fd, int $type, $data = null): int
{
$resolveWorkerId = function (Server $server, $data = null) {
$resolveWorkerId = function (\Swoole\Http\Server $server, $data = null) {
global $totalWorkers, $riskyDomains;
// If data is not set we can send request to any worker
@@ -161,18 +176,6 @@ function dispatch(Server $server, int $fd, int $type, $data = null): int
return $workerId;
}
$http
->set([
Constant::OPTION_WORKER_NUM => $totalWorkers,
Constant::OPTION_DISPATCH_FUNC => dispatch(...),
Constant::OPTION_DISPATCH_MODE => SWOOLE_DISPATCH_UIDMOD,
Constant::OPTION_HTTP_COMPRESSION => false,
Constant::OPTION_PACKAGE_MAX_LENGTH => $payloadSize,
Constant::OPTION_OUTPUT_BUFFER_SIZE => $payloadSize,
Constant::OPTION_TASK_WORKER_NUM => 1, // required for the task to fetch domains background
]);
$http->on(Constant::EVENT_WORKER_START, function ($server, $workerId) {
});
@@ -189,16 +192,14 @@ $http->on(Constant::EVENT_AFTER_RELOAD, function ($server) {
Console::success('Reload completed...');
});
Http::setResource('bus', function ($register, $utopia) {
return $register->get('bus')->setResolver(fn (string $name) => $utopia->getResource($name));
}, ['register', 'utopia']);
$container->set('bus', function ($register) use ($swooleAdapter) {
return $register->get('bus')->setResolver(fn (string $name) => $swooleAdapter->getContainer()->get($name));
}, ['register']);
include __DIR__ . '/controllers/general.php';
function createDatabase(Http $app, string $resourceKey, string $dbName, array $collections, mixed $pools, ?callable $extraSetup = null): void
{
$max = 15;
$sleep = 2;
$max = 15;
$sleep = 2;
$attempts = 0;
@@ -289,13 +290,13 @@ function createDatabase(Http $app, string $resourceKey, string $dbName, array $c
Span::current()?->finish();
}
$http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $totalWorkers, $register) {
$app = new Http('UTC');
$http->on(Constant::EVENT_START, function ($http) use ($payloadSize, $totalWorkers, $swooleAdapter) {
$app = new Http($swooleAdapter, 'UTC');
go(function () use ($register, $app) {
$pools = $register->get('pools');
/** @var \Utopia\Pools\Group $pools */
Http::setResource('pools', fn () => $pools);
/** @var \Utopia\Pools\Group $pools */
$pools = $app->getResource('pools');
go(function () use ($app, $pools) {
/** @var array $collections */
$collections = Config::getParam('collections', []);
@@ -511,14 +512,11 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $tot
});
});
$http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, SwooleResponse $swooleResponse) use ($register, $files) {
$swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files, $swooleAdapter, $registerRequestResources) {
Span::init('http.request');
Http::setResource('swooleRequest', fn () => $swooleRequest);
Http::setResource('swooleResponse', fn () => $swooleResponse);
$request = new Request($swooleRequest);
$response = new Response($swooleResponse);
$request = new Request($utopiaRequest->getSwooleRequest());
$response = new Response($utopiaResponse->getSwooleResponse());
Span::add('http.method', $request->getMethod());
@@ -534,13 +532,18 @@ $http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, Swool
return;
}
$app = new Http('UTC');
$requestContainer = $swooleAdapter->getContainer();
$requestContainer->set('request', fn () => $request);
$requestContainer->set('response', fn () => $response);
$app = new Http($swooleAdapter, 'UTC');
$requestContainer->set('utopia', fn () => $app);
$registerRequestResources($requestContainer);
$app->setCompression(System::getEnv('_APP_COMPRESSION_ENABLED', 'enabled') === 'enabled');
$app->setCompressionMinSize(intval(System::getEnv('_APP_COMPRESSION_MIN_SIZE_BYTES', '1024'))); // 1KB
$pools = $register->get('pools');
Http::setResource('pools', fn () => $pools);
try {
$authorization = $app->getResource('authorization');
@@ -624,6 +627,7 @@ $http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, Swool
}
}
$swooleResponse = $utopiaResponse->getSwooleResponse();
$swooleResponse->setStatusCode(500);
$output = ((Http::isDevelopment())) ? [
@@ -647,11 +651,10 @@ $http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, Swool
});
// Fetch domains every `DOMAIN_SYNC_TIMER` seconds and update in the memory
$http->on(Constant::EVENT_TASK, function () use ($register) {
$http->on(Constant::EVENT_TASK, function () use ($swooleAdapter) {
$lastSyncUpdate = null;
$pools = $register->get('pools');
Http::setResource('pools', fn () => $pools);
$app = new Http('UTC');
$app = new Http($swooleAdapter, 'UTC');
/** @var Utopia\Database\Database $dbForPlatform */
$dbForPlatform = $app->getResource('dbForPlatform');
@@ -726,4 +729,4 @@ $http->on(Constant::EVENT_TASK, function () use ($register) {
});
});
$http->start();
$swooleAdapter->start();
+12 -3
View File
@@ -106,7 +106,12 @@ use Appwrite\Utopia\Response\Model\Mock;
use Appwrite\Utopia\Response\Model\MockNumber;
use Appwrite\Utopia\Response\Model\None;
use Appwrite\Utopia\Response\Model\Phone;
use Appwrite\Utopia\Response\Model\Platform;
use Appwrite\Utopia\Response\Model\PlatformAndroid;
use Appwrite\Utopia\Response\Model\PlatformApple;
use Appwrite\Utopia\Response\Model\PlatformLinux;
use Appwrite\Utopia\Response\Model\PlatformList;
use Appwrite\Utopia\Response\Model\PlatformWeb;
use Appwrite\Utopia\Response\Model\PlatformWindows;
use Appwrite\Utopia\Response\Model\Preferences;
use Appwrite\Utopia\Response\Model\Project;
use Appwrite\Utopia\Response\Model\Provider;
@@ -197,7 +202,6 @@ Response::setModel(new BaseList('Webhooks List', Response::MODEL_WEBHOOK_LIST, '
Response::setModel(new BaseList('API Keys List', Response::MODEL_KEY_LIST, 'keys', Response::MODEL_KEY, true, true));
Response::setModel(new BaseList('Dev Keys List', Response::MODEL_DEV_KEY_LIST, 'devKeys', Response::MODEL_DEV_KEY, true, false));
Response::setModel(new BaseList('Auth Providers List', Response::MODEL_AUTH_PROVIDER_LIST, 'platforms', Response::MODEL_AUTH_PROVIDER, true, false));
Response::setModel(new BaseList('Platforms List', Response::MODEL_PLATFORM_LIST, 'platforms', Response::MODEL_PLATFORM, true, false));
Response::setModel(new BaseList('Countries List', Response::MODEL_COUNTRY_LIST, 'countries', Response::MODEL_COUNTRY));
Response::setModel(new BaseList('Continents List', Response::MODEL_CONTINENT_LIST, 'continents', Response::MODEL_CONTINENT));
Response::setModel(new BaseList('Languages List', Response::MODEL_LANGUAGE_LIST, 'languages', Response::MODEL_LANGUAGE));
@@ -333,7 +337,12 @@ Response::setModel(new Key());
Response::setModel(new DevKey());
Response::setModel(new MockNumber());
Response::setModel(new AuthProvider());
Response::setModel(new Platform());
Response::setModel(new PlatformWeb());
Response::setModel(new PlatformApple());
Response::setModel(new PlatformAndroid());
Response::setModel(new PlatformWindows());
Response::setModel(new PlatformLinux());
Response::setModel(new PlatformList());
Response::setModel(new Variable());
Response::setModel(new Country());
Response::setModel(new Continent());
+2 -13
View File
@@ -245,19 +245,8 @@ $register->set('pools', function () {
$maxConnections = (int) System::getEnv('_APP_CONNECTIONS_MAX', 151);
$instanceConnections = $maxConnections / (int) System::getEnv('_APP_POOL_CLIENTS', 14);
$multiprocessing = System::getEnv('_APP_SERVER_MULTIPROCESS', 'disabled') === 'enabled';
if ($multiprocessing) {
$workerCount = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
} else {
$workerCount = 1;
}
if ($workerCount > $instanceConnections) {
throw new \Exception('Pool size is too small. Increase the number of allowed database connections or decrease the number of workers.', 500);
}
$poolSize = (int)($instanceConnections / $workerCount);
$workerCount = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
$poolSize = max(1, (int)($instanceConnections / $workerCount));
foreach ($connections as $key => $connection) {
$type = $connection['type'] ?? '';
+38 -1372
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+435
View File
@@ -0,0 +1,435 @@
<?php
use Appwrite\Event\Audit;
use Appwrite\Event\Build;
use Appwrite\Event\Certificate;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\Migration;
use Appwrite\Event\Realtime;
use Appwrite\Event\Screenshot;
use Appwrite\Event\Webhook;
use Appwrite\Usage\Context;
use Appwrite\Utopia\Database\Documents\User;
use Utopia\Audit\Adapter\Database as AdapterDatabase;
use Utopia\Audit\Audit as UtopiaAudit;
use Utopia\Cache\Cache;
use Utopia\Console;
use Utopia\Database\Adapter\Pool as DatabasePool;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\DI\Container;
use Utopia\DSN\DSN;
use Utopia\Logger\Log;
use Utopia\Pools\Group;
use Utopia\Queue\Publisher;
use Utopia\Registry\Registry;
use Utopia\Storage\Device\Telemetry as TelemetryDevice;
use Utopia\System\System;
use Utopia\Telemetry\Adapter as Telemetry;
/**
* Register per-job resources on the given container.
* These resources depend on the queue message or keep mutable state and
* must be fresh for each worker job.
*/
return function (Container $container): void {
$container->set('log', fn () => new Log(), []);
$container->set('usage', fn () => new Context(), []);
$container->set('authorization', function () {
$authorization = new Authorization();
$authorization->disable();
return $authorization;
}, []);
$container->set('dbForPlatform', function (Cache $cache, Group $pools, Authorization $authorization) {
$adapter = new DatabasePool($pools->get('console'));
$dbForPlatform = new Database($adapter, $cache);
$dbForPlatform
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setNamespace('_console')
->setDocumentType('users', User::class);
return $dbForPlatform;
}, ['cache', 'pools', 'authorization']);
$container->set('project', function ($message, Database $dbForPlatform) {
$payload = $message->getPayload() ?? [];
$project = new Document($payload['project'] ?? []);
if ($project->isEmpty() || $project->getId() === 'console') {
return $project;
}
return $dbForPlatform->getDocument('projects', $project->getId());
}, ['message', 'dbForPlatform']);
$container->set('dbForProject', function (Cache $cache, Group $pools, Document $project, Database $dbForPlatform, Authorization $authorization) {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForPlatform;
}
try {
$dsn = new DSN($project->getAttribute('database'));
} catch (\InvalidArgumentException) {
// TODO: Temporary until all projects are using shared tables
$dsn = new DSN('mysql://' . $project->getAttribute('database'));
}
$adapter = new DatabasePool($pools->get($dsn->getHost()));
$database = new Database($adapter, $cache);
$database->setDocumentType('users', User::class);
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
->setSharedTables(false)
->setTenant(null)
->setNamespace('_' . $project->getSequence());
}
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
return $database;
}, ['cache', 'pools', 'project', 'dbForPlatform', 'authorization']);
$container->set('getProjectDB', function (Group $pools, Database $dbForPlatform, Cache $cache, Authorization $authorization) {
$databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools
return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$databases): Database {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForPlatform;
}
try {
$dsn = new DSN($project->getAttribute('database'));
} catch (\InvalidArgumentException) {
// TODO: Temporary until all projects are using shared tables
$dsn = new DSN('mysql://' . $project->getAttribute('database'));
}
if (isset($databases[$dsn->getHost()])) {
$database = $databases[$dsn->getHost()];
$database->setAuthorization($authorization);
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
->setSharedTables(false)
->setTenant(null)
->setNamespace('_' . $project->getSequence());
}
return $database;
}
$adapter = new DatabasePool($pools->get($dsn->getHost()));
$database = new Database($adapter, $cache);
$databases[$dsn->getHost()] = $database;
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
->setSharedTables(false)
->setTenant(null)
->setNamespace('_' . $project->getSequence());
}
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
return $database;
};
}, ['pools', 'dbForPlatform', 'cache', 'authorization']);
$container->set('getDatabasesDB', function (Cache $cache, Registry $register, Document $project, Authorization $authorization) {
return function (Document $database, ?Document $projectDocument = null) use ($cache, $register, $project, $authorization): Database {
$projectDocument ??= $project;
$databaseDSN = $database->getAttribute('database', $project->getAttribute('database', ''));
$databaseType = $database->getAttribute('type', '');
// Backwards-compatibility: older or seeded legacy databases may not have a DSN stored
// in the "database" attribute. In that case, fall back to the project's database DSN.
if ($databaseDSN === '') {
$databaseDSN = $projectDocument->getAttribute('database', '');
}
try {
$databaseDSN = new DSN($databaseDSN);
} catch (\InvalidArgumentException) {
$databaseDSN = new DSN('mysql://' . $databaseDSN);
}
try {
$dsn = new DSN($projectDocument->getAttribute('database'));
} catch (\InvalidArgumentException) {
// Temporary fallback until all projects use shared tables
$dsn = new DSN('mysql://' . $projectDocument->getAttribute('database'));
}
$pools = $register->get('pools');
$pool = $pools->get($databaseDSN->getHost());
$adapter = new DatabasePool($pool);
$database = new Database($adapter, $cache);
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization);
$database->getAdapter()->setSupportForAttributes($databaseType !== DOCUMENTSDB);
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables, true)) {
$database
->setSharedTables(true)
->setTenant((int) $projectDocument->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
->setSharedTables(false)
->setTenant(null)
->setNamespace('_' . $projectDocument->getSequence());
}
$database->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
return $database;
};
}, ['cache', 'register', 'project', 'authorization']);
$container->set('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
$database = null;
return function (?Document $project = null) use ($pools, $cache, $authorization, &$database) {
if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant($project->getSequence());
return $database;
}
$adapter = new DatabasePool($pools->get('logs'));
$database = new Database($adapter, $cache);
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setSharedTables(true)
->setNamespace('logsV1')
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER)
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES_WORKER);
if ($project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant($project->getSequence());
}
return $database;
};
}, ['pools', 'cache', 'authorization']);
$container->set('abuseRetention', function () {
return \time() - (int) System::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', 86400); // 1 day
}, []);
$container->set('auditRetention', function (Document $project) {
if ($project->getId() === 'console') {
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE', 15778800)); // 6 months
}
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 1209600)); // 14 days
}, ['project']);
$container->set('executionRetention', function () {
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', 1209600)); // 14 days
}, []);
$container->set('queueForDatabase', function (Publisher $publisher) {
return new EventDatabase($publisher);
}, ['publisher']);
$container->set('queueForMessaging', function (Publisher $publisher) {
return new Messaging($publisher);
}, ['publisher']);
$container->set('queueForMails', function (Publisher $publisher) {
return new Mail($publisher);
}, ['publisher']);
$container->set('queueForBuilds', function (Publisher $publisher) {
return new Build($publisher);
}, ['publisher']);
$container->set('queueForScreenshots', function (Publisher $publisher) {
return new Screenshot($publisher);
}, ['publisher']);
$container->set('queueForDeletes', function (Publisher $publisher) {
return new Delete($publisher);
}, ['publisher']);
$container->set('queueForEvents', function (Publisher $publisher) {
return new Event($publisher);
}, ['publisher']);
$container->set('queueForAudits', function (Publisher $publisher) {
return new Audit($publisher);
}, ['publisher']);
$container->set('queueForWebhooks', function (Publisher $publisher) {
return new Webhook($publisher);
}, ['publisher']);
$container->set('queueForFunctions', function (Publisher $publisher) {
return new Func($publisher);
}, ['publisher']);
$container->set('queueForRealtime', function () {
return new Realtime();
}, []);
$container->set('queueForCertificates', function (Publisher $publisher) {
return new Certificate($publisher);
}, ['publisher']);
$container->set('queueForMigrations', function (Publisher $publisher) {
return new Migration($publisher);
}, ['publisher']);
$container->set('deviceForSites', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
$container->set('deviceForMigrations', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_IMPORTS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
$container->set('deviceForFunctions', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
$container->set('deviceForFiles', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
$container->set('deviceForBuilds', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
$container->set('deviceForCache', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_CACHE . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
$container->set('logError', function (Registry $register, Document $project) {
return function (Throwable $error, string $namespace, string $action, ?array $extras = null) use ($register, $project) {
$logger = $register->get('logger');
if ($logger) {
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
$log = new Log();
$log->setNamespace($namespace);
$log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname()));
$log->setVersion($version);
$log->setType(Log::TYPE_ERROR);
$log->setMessage($error->getMessage());
$log->addTag('code', $error->getCode());
$log->addTag('verboseType', \get_class($error));
$log->addTag('projectId', $project->getId() ?? '');
$log->addExtra('file', $error->getFile());
$log->addExtra('line', $error->getLine());
$log->addExtra('trace', $error->getTraceAsString());
if ($error->getPrevious() !== null) {
if ($error->getPrevious()->getMessage() != $error->getMessage()) {
$log->addExtra('previousMessage', $error->getPrevious()->getMessage());
}
$log->addExtra('previousFile', $error->getPrevious()->getFile());
$log->addExtra('previousLine', $error->getPrevious()->getLine());
}
foreach (($extras ?? []) as $key => $value) {
$log->addExtra($key, $value);
}
$log->setAction($action);
$isProduction = System::getEnv('_APP_ENV', 'development') === 'production';
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
try {
$responseCode = $logger->addLog($log);
Console::info('Error log pushed with status code: ' . $responseCode);
} catch (Throwable $th) {
Console::error('Error pushing log: ' . $th->getMessage());
}
}
Console::warning("Failed: {$error->getMessage()}");
Console::warning($error->getTraceAsString());
if ($error->getPrevious() !== null) {
if ($error->getPrevious()->getMessage() != $error->getMessage()) {
Console::warning("Previous Failed: {$error->getPrevious()->getMessage()}");
}
Console::warning("Previous File: {$error->getPrevious()->getFile()} Line: {$error->getPrevious()->getLine()}");
}
};
}, ['register', 'project']);
$container->set('getAudit', function (Database $dbForPlatform, callable $getProjectDB) {
return function (Document $project) use ($dbForPlatform, $getProjectDB) {
if ($project->isEmpty() || $project->getId() === 'console') {
$adapter = new AdapterDatabase($dbForPlatform);
return new UtopiaAudit($adapter);
}
$dbForProject = $getProjectDB($project);
$adapter = new AdapterDatabase($dbForProject);
return new UtopiaAudit($adapter);
};
}, ['dbForPlatform', 'getProjectDB']);
$container->set('executionsRetentionCount', function (Document $project, array $plan) {
if ($project->getId() === 'console' || empty($plan)) {
return 0;
}
return (int) ($plan['executionsRetentionCount'] ?? 100);
}, ['project', 'plan']);
};
+20 -5
View File
@@ -33,7 +33,9 @@ use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\DI\Container;
use Utopia\DSN\DSN;
use Utopia\Http\Adapter\FPM\Server as HttpServer;
use Utopia\Http\Http;
use Utopia\Logger\Log;
use Utopia\Pools\Group;
@@ -48,6 +50,8 @@ use Utopia\WebSocket\Server;
*/
require_once __DIR__ . '/init.php';
$registerRequestResources ??= require __DIR__ . '/init/resources/request.php';
Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
// Log uncaught exceptions in one line instead of relying on Swoole's full backtrace dump
@@ -240,6 +244,11 @@ if (!function_exists('triggerStats')) {
}
}
global $container;
$container->set('pools', function ($register) {
return $register->get('pools');
}, ['register']);
$realtime = getRealtime();
/**
@@ -614,16 +623,22 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
Console::error('Failed to restart pub/sub...');
});
$server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $register, $stats, &$realtime) {
$app = new Http('UTC');
$server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $register, $stats, &$realtime, $registerRequestResources) {
global $container;
$request = new Request($request);
$response = new Response(new SwooleResponse());
Console::info("Connection open (user: {$connection})");
Http::setResource('pools', fn () => $register->get('pools'));
Http::setResource('request', fn () => $request);
Http::setResource('response', fn () => $response);
$connectionContainer = new Container($container);
$adapter = new HttpServer($connectionContainer);
$app = new Http($adapter, 'UTC');
$connectionContainer->set('utopia', fn () => $app);
$connectionContainer->set('request', fn () => $request);
$connectionContainer->set('response', fn () => $response);
$registerRequestResources($connectionContainer);
$project = null;
$logUser = null;
+42 -530
View File
@@ -1,564 +1,69 @@
<?php
require_once __DIR__ . '/init.php';
$registerWorkerMessageResources = require __DIR__ . '/init/worker/message.php';
use Appwrite\Certificates\LetsEncrypt;
use Appwrite\Event\Audit;
use Appwrite\Event\Build;
use Appwrite\Event\Certificate;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\Migration;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Event\Realtime;
use Appwrite\Event\Screenshot;
use Appwrite\Event\Webhook;
use Appwrite\Platform\Appwrite;
use Appwrite\Usage\Context;
use Appwrite\Utopia\Database\Documents\User;
use Executor\Executor;
use Swoole\Runtime;
use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis;
use Utopia\Audit\Adapter\Database as AdapterDatabase;
use Utopia\Audit\Audit as UtopiaAudit;
use Utopia\Cache\Adapter\Pool as CachePool;
use Utopia\Cache\Adapter\Sharding;
use Utopia\Cache\Cache;
use Utopia\Config\Config;
use Utopia\Console;
use Utopia\Database\Adapter\Pool as DatabasePool;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\DSN\DSN;
use Utopia\Logger\Log;
use Utopia\Logger\Logger;
use Utopia\Platform\Service;
use Utopia\Pools\Group;
use Utopia\Queue\Adapter\Swoole;
use Utopia\Queue\Broker\Pool as BrokerPool;
use Utopia\Queue\Message;
use Utopia\Queue\Publisher;
use Utopia\Queue\Queue;
use Utopia\Queue\Server;
use Utopia\Registry\Registry;
use Utopia\Storage\Device\Telemetry as TelemetryDevice;
use Utopia\System\System;
use Utopia\Telemetry\Adapter as Telemetry;
use Utopia\Telemetry\Adapter\None as NoTelemetry;
Runtime::enableCoroutine();
require_once __DIR__ . '/init/span.php';
global $register;
Server::setResource('register', fn () => $register);
global $container;
$container->set('pools', function ($register) {
return $register->get('pools');
}, ['register']);
Server::setResource('authorization', function () {
$container->set('authorization', function () {
$authorization = new Authorization();
$authorization->disable();
return $authorization;
}, []);
Server::setResource('dbForPlatform', function (Cache $cache, Registry $register, Authorization $authorization) {
$pools = $register->get('pools');
$adapter = new DatabasePool($pools->get('console'));
$dbForPlatform = new Database($adapter, $cache);
$container->set('project', fn () => new Document([]), []);
$dbForPlatform
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setNamespace('_console')
->setDocumentType('users', User::class);
$container->set('log', fn () => new Log(), []);
return $dbForPlatform;
}, ['cache', 'register', 'authorization']);
Server::setResource('project', function (Message $message, Database $dbForPlatform) {
$payload = $message->getPayload() ?? [];
$project = new Document($payload['project'] ?? []);
if ($project->getId() === 'console') {
return $project;
}
return $dbForPlatform->getDocument('projects', $project->getId());
}, ['message', 'dbForPlatform']);
Server::setResource('dbForProject', function (Cache $cache, Registry $register, Message $message, Document $project, Database $dbForPlatform, Authorization $authorization) {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForPlatform;
}
$pools = $register->get('pools');
try {
$dsn = new DSN($project->getAttribute('database'));
} catch (\InvalidArgumentException) {
// TODO: Temporary until all projects are using shared tables
$dsn = new DSN('mysql://' . $project->getAttribute('database'));
}
$adapter = new DatabasePool($pools->get($dsn->getHost()));
$database = new Database($adapter, $cache);
$database->setDocumentType('users', User::class);
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
->setSharedTables(false)
->setTenant(null)
->setNamespace('_' . $project->getSequence());
}
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
return $database;
}, ['cache', 'register', 'message', 'project', 'dbForPlatform', 'authorization']);
Server::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache, Authorization $authorization) {
$databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools
return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$databases): Database {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForPlatform;
}
try {
$dsn = new DSN($project->getAttribute('database'));
} catch (\InvalidArgumentException) {
// TODO: Temporary until all projects are using shared tables
$dsn = new DSN('mysql://' . $project->getAttribute('database'));
}
if (isset($databases[$dsn->getHost()])) {
$database = $databases[$dsn->getHost()];
$database->setAuthorization($authorization);
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
->setSharedTables(false)
->setTenant(null)
->setNamespace('_' . $project->getSequence());
}
return $database;
}
$adapter = new DatabasePool($pools->get($dsn->getHost()));
$database = new Database($adapter, $cache);
$databases[$dsn->getHost()] = $database;
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
->setSharedTables(false)
->setTenant(null)
->setNamespace('_' . $project->getSequence());
}
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
return $database;
};
}, ['pools', 'dbForPlatform', 'cache', 'authorization']);
Server::setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
$database = null;
return function (?Document $project = null) use ($pools, $cache, $database, $authorization) {
if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant($project->getSequence());
return $database;
}
$adapter = new DatabasePool($pools->get('logs'));
$database = new Database($adapter, $cache);
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setSharedTables(true)
->setNamespace('logsV1')
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER)
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES_WORKER);
if ($project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant($project->getSequence());
}
return $database;
};
}, ['pools', 'cache', 'authorization']);
Server::setResource('getDatabasesDB', function (Cache $cache, Registry $register, Document $project, Authorization $authorization) {
return function (Document $database, ?Document $projectDocument = null) use ($cache, $register, $project, $authorization): Database {
$projectDocument ??= $project;
$databaseDSN = $database->getAttribute('database', $project->getAttribute('database', ''));
$databaseType = $database->getAttribute('type', '');
// Backwardscompatibility: older or seeded legacy databases may not have a DSN stored
// in the "database" attribute. In that case, fall back to the project's database DSN.
if ($databaseDSN === '') {
$databaseDSN = $projectDocument->getAttribute('database', '');
}
try {
$databaseDSN = new DSN($databaseDSN);
} catch (\InvalidArgumentException) {
$databaseDSN = new DSN('mysql://'.$databaseDSN);
}
try {
$dsn = new DSN($projectDocument->getAttribute('database'));
} catch (\InvalidArgumentException) {
// Temporary fallback until all projects use shared tables
$dsn = new DSN('mysql://' . $projectDocument->getAttribute('database'));
}
$pools = $register->get('pools');
$pool = $pools->get($databaseDSN->getHost());
$adapter = new DatabasePool($pool);
$database = new Database($adapter, $cache);
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization);
$database->getAdapter()->setSupportForAttributes($databaseType !== DOCUMENTSDB);
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables, true)) {
$database
->setSharedTables(true)
->setTenant((int) $projectDocument->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
->setSharedTables(false)
->setTenant(null)
->setNamespace('_' . $projectDocument->getSequence());
}
$database->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
return $database;
};
}, ['cache', 'register', 'project', 'authorization']);
Server::setResource('abuseRetention', function () {
return time() - (int) System::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', 86400); // 1 day
});
Server::setResource('auditRetention', function (Document $project) {
if ($project->getId() === 'console') {
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE', 15778800)); // 6 months
}
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 1209600)); // 14 days
}, ['project']);
Server::setResource('executionRetention', function () {
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', 1209600)); // 14 days
});
Server::setResource('cache', function (Registry $register) {
$pools = $register->get('pools');
$list = Config::getParam('pools-cache', []);
$adapters = [];
foreach ($list as $value) {
$adapters[] = new CachePool($pools->get($value));
}
return new Cache(new Sharding($adapters));
}, ['register']);
Server::setResource('redis', function () {
$host = System::getEnv('_APP_REDIS_HOST', 'localhost');
$port = System::getEnv('_APP_REDIS_PORT', 6379);
$pass = System::getEnv('_APP_REDIS_PASS', '');
$redis = new \Redis();
@$redis->pconnect($host, (int) $port);
if ($pass) {
$redis->auth($pass);
}
$redis->setOption(\Redis::OPT_READ_TIMEOUT, -1);
return $redis;
});
Server::setResource('timelimit', function (\Redis $redis) {
return function (string $key, int $limit, int $time) use ($redis) {
return new TimeLimitRedis($key, $limit, $time, $redis);
};
}, ['redis']);
Server::setResource('log', fn () => new Log());
Server::setResource('publisher', function (Group $pools) {
return new BrokerPool(publisher: $pools->get('publisher'));
}, ['pools']);
Server::setResource('publisherDatabases', function (BrokerPool $publisher) {
return $publisher;
}, ['publisher']);
Server::setResource('publisherFunctions', function (BrokerPool $publisher) {
return $publisher;
}, ['publisher']);
Server::setResource('publisherMigrations', function (BrokerPool $publisher) {
return $publisher;
}, ['publisher']);
Server::setResource('publisherMessaging', function (BrokerPool $publisher) {
return $publisher;
}, ['publisher']);
Server::setResource('consumer', function (Group $pools) {
$container->set('consumer', function (Group $pools) {
return new BrokerPool(consumer: $pools->get('consumer'));
}, ['pools']);
Server::setResource('consumerDatabases', function (BrokerPool $consumer) {
$container->set('consumerDatabases', function (BrokerPool $consumer) {
return $consumer;
}, ['consumer']);
Server::setResource('consumerMigrations', function (BrokerPool $consumer) {
$container->set('consumerMigrations', function (BrokerPool $consumer) {
return $consumer;
}, ['consumer']);
Server::setResource('consumerStatsUsage', function (BrokerPool $consumer) {
$container->set('consumerStatsUsage', function (BrokerPool $consumer) {
return $consumer;
}, ['consumer']);
Server::setResource('usage', function () {
return new Context();
}, []);
Server::setResource('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher(
$publisher,
new Queue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME))
), ['publisher']);
Server::setResource('queueForDatabase', function (Publisher $publisher) {
return new EventDatabase($publisher);
}, ['publisher']);
Server::setResource('queueForMessaging', function (Publisher $publisher) {
return new Messaging($publisher);
}, ['publisher']);
Server::setResource('queueForMails', function (Publisher $publisher) {
return new Mail($publisher);
}, ['publisher']);
Server::setResource('queueForBuilds', function (Publisher $publisher) {
return new Build($publisher);
}, ['publisher']);
Server::setResource('queueForScreenshots', function (Publisher $publisher) {
return new Screenshot($publisher);
}, ['publisher']);
Server::setResource('queueForDeletes', function (Publisher $publisher) {
return new Delete($publisher);
}, ['publisher']);
Server::setResource('queueForEvents', function (Publisher $publisher) {
return new Event($publisher);
}, ['publisher']);
Server::setResource('queueForAudits', function (Publisher $publisher) {
return new Audit($publisher);
}, ['publisher']);
Server::setResource('queueForWebhooks', function (Publisher $publisher) {
return new Webhook($publisher);
}, ['publisher']);
Server::setResource('queueForFunctions', function (Publisher $publisher) {
return new Func($publisher);
}, ['publisher']);
Server::setResource('queueForRealtime', function () {
return new Realtime();
}, []);
Server::setResource('queueForCertificates', function (Publisher $publisher) {
return new Certificate($publisher);
}, ['publisher']);
Server::setResource('queueForMigrations', function (Publisher $publisher) {
return new Migration($publisher);
}, ['publisher']);
Server::setResource('logger', function (Registry $register) {
return $register->get('logger');
}, ['register']);
Server::setResource('pools', function (Registry $register) {
return $register->get('pools');
}, ['register']);
Server::setResource('telemetry', fn () => new NoTelemetry());
Server::setResource('deviceForSites', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
Server::setResource('deviceForMigrations', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_IMPORTS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
Server::setResource('deviceForFunctions', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
Server::setResource('deviceForFiles', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
Server::setResource('deviceForBuilds', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
Server::setResource('deviceForCache', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_CACHE . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
Server::setResource(
'isResourceBlocked',
fn () => fn (Document $project, string $resourceType, ?string $resourceId) => false
);
Server::setResource('plan', function (array $plan = []) {
return [];
});
Server::setResource('certificates', function () {
$container->set('certificates', function () {
$email = System::getEnv('_APP_EMAIL_CERTIFICATES', System::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS'));
if (empty($email)) {
throw new Exception('You must set a valid security email address (_APP_EMAIL_CERTIFICATES) to issue a LetsEncrypt SSL certificate.');
}
return new LetsEncrypt($email);
});
}, []);
Server::setResource('logError', function (Registry $register, Document $project) {
return function (Throwable $error, string $namespace, string $action, ?array $extras = null) use ($register, $project) {
$logger = $register->get('logger');
if ($logger) {
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
$log = new Log();
$log->setNamespace($namespace);
$log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname()));
$log->setVersion($version);
$log->setType(Log::TYPE_ERROR);
$log->setMessage($error->getMessage());
$log->addTag('code', $error->getCode());
$log->addTag('verboseType', get_class($error));
$log->addTag('projectId', $project->getId() ?? '');
$log->addExtra('file', $error->getFile());
$log->addExtra('line', $error->getLine());
$log->addExtra('trace', $error->getTraceAsString());
if ($error->getPrevious() !== null) {
if ($error->getPrevious()->getMessage() != $error->getMessage()) {
$log->addExtra('previousMessage', $error->getPrevious()->getMessage());
}
$log->addExtra('previousFile', $error->getPrevious()->getFile());
$log->addExtra('previousLine', $error->getPrevious()->getLine());
}
foreach (($extras ?? []) as $key => $value) {
$log->addExtra($key, $value);
}
$log->setAction($action);
$isProduction = System::getEnv('_APP_ENV', 'development') === 'production';
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
try {
$responseCode = $logger->addLog($log);
Console::info('Error log pushed with status code: ' . $responseCode);
} catch (Throwable $th) {
Console::error('Error pushing log: ' . $th->getMessage());
}
}
Console::warning("Failed: {$error->getMessage()}");
Console::warning($error->getTraceAsString());
if ($error->getPrevious() !== null) {
if ($error->getPrevious()->getMessage() != $error->getMessage()) {
Console::warning("Previous Failed: {$error->getPrevious()->getMessage()}");
}
Console::warning("Previous File: {$error->getPrevious()->getFile()} Line: {$error->getPrevious()->getLine()}");
}
};
}, ['register', 'project']);
Server::setResource('executor', fn () => new Executor());
Server::setResource('getAudit', function (Database $dbForPlatform, callable $getProjectDB) {
return function (Document $project) use ($dbForPlatform, $getProjectDB) {
if ($project->isEmpty() || $project->getId() === 'console') {
$adapter = new AdapterDatabase($dbForPlatform);
return new UtopiaAudit($adapter);
}
$dbForProject = $getProjectDB($project);
$adapter = new AdapterDatabase($dbForProject);
return new UtopiaAudit($adapter);
};
}, ['dbForPlatform', 'getProjectDB']);
Server::setResource('executionsRetentionCount', function (Document $project, array $plan) {
if ($project->getId() === 'console' || empty($plan)) {
return 0;
}
return (int) ($plan['executionsRetentionCount'] ?? 100);
}, ['project', 'plan']);
$pools = $register->get('pools');
$platform = new Appwrite();
$args = $platform->getEnv('argv');
$args = $_SERVER['argv'] ?? [];
if (! isset($args[1])) {
Console::error('Missing worker name');
@@ -574,38 +79,45 @@ if (\str_starts_with($workerName, 'databases')) {
$queueName = System::getEnv('_APP_QUEUE_NAME', 'v1-' . strtolower($workerName));
}
/** @var \Utopia\Pools\Group $pools */
$pools = $container->get('pools');
$adapter = new Swoole(
$pools->get('consumer')->pop()->getResource(),
System::getEnv('_APP_WORKERS_NUM', 1),
$queueName
);
$worker = new Server($adapter, $container);
try {
/**
* Any worker can be configured with the following env vars:
* - _APP_WORKERS_NUM The total number of worker processes
* - _APP_WORKER_PER_CORE The number of worker processes per core (ignored if _APP_WORKERS_NUM is set)
* - _APP_QUEUE_NAME The name of the queue to read for database events
*/
$worker->init()->action(function () use ($worker, $registerWorkerMessageResources) {
$registerWorkerMessageResources($worker->getContainer());
});
$container->set('bus', function ($register) use ($worker) {
return $register->get('bus')->setResolver(
fn (string $name) => $worker->getContainer()->get($name)
);
}, ['register']);
$platform->setWorker($worker);
$platform->init(Service::TYPE_WORKER, [
'workersNum' => System::getEnv('_APP_WORKERS_NUM', 1),
'connection' => $pools->get('consumer')->pop()->getResource(),
'workerName' => strtolower($workerName) ?? null,
'queueName' => $queueName,
'workerName' => strtolower($workerName),
]);
} catch (\Throwable $e) {
Console::error($e->getMessage() . ', File: ' . $e->getFile() . ', Line: ' . $e->getLine());
Console::exit(1);
}
$worker = $platform->getWorker();
Server::setResource('bus', function ($register) use ($worker) {
return $register->get('bus')->setResolver(fn (string $name) => $worker->getResource($name));
}, ['register']);
$worker
->error()
->inject('error')
->inject('logger')
->inject('log')
->inject('pools')
->inject('project')
->inject('authorization')
->action(function (Throwable $error, ?Logger $logger, Log $log, Group $pools, Document $project, Authorization $authorization) use ($queueName) {
->action(function (Throwable $error, ?Logger $logger, Log $log, Document $project, Authorization $authorization) use ($queueName) {
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
if ($logger) {
+7 -8
View File
@@ -52,34 +52,34 @@
"appwrite/php-runtimes": "0.19.*",
"appwrite/php-clamav": "2.0.*",
"utopia-php/abuse": "1.2.*",
"utopia-php/agents": "1.2.*",
"utopia-php/analytics": "0.15.*",
"utopia-php/audit": "2.2.*",
"utopia-php/auth": "0.5.*",
"utopia-php/cache": "1.0.*",
"utopia-php/cli": "0.22.*",
"utopia-php/cli": "0.23.*",
"utopia-php/compression": "0.1.*",
"utopia-php/config": "1.*",
"utopia-php/console": "0.1.*",
"utopia-php/database": "5.*",
"utopia-php/agents": "1.*",
"utopia-php/detector": "0.2.*",
"utopia-php/domains": "1.*",
"utopia-php/emails": "0.6.*",
"utopia-php/dns": "1.6.*",
"utopia-php/dsn": "0.2.1",
"utopia-php/framework": "0.33.*",
"utopia-php/framework": "0.34.*",
"utopia-php/fetch": "0.5.*",
"utopia-php/image": "0.8.*",
"utopia-php/locale": "0.8.*",
"utopia-php/logger": "0.6.*",
"utopia-php/messaging": "0.22.*",
"utopia-php/migration": "1.9.*",
"utopia-php/platform": "0.7.*",
"utopia-php/platform": "0.12.*",
"utopia-php/pools": "1.*",
"utopia-php/span": "1.1.*",
"utopia-php/preloader": "0.2.*",
"utopia-php/queue": "0.15.*",
"utopia-php/servers": "0.2.5",
"utopia-php/queue": "0.17.*",
"utopia-php/servers": "0.3.*",
"utopia-php/registry": "0.5.*",
"utopia-php/storage": "1.0.*",
"utopia-php/system": "0.10.*",
@@ -94,8 +94,7 @@
"spomky-labs/otphp": "11.*",
"webonyx/graphql-php": "14.11.*",
"league/csv": "9.14.*",
"enshrined/svg-sanitize": "0.22.*",
"utopia-php/di": "0.1.0"
"enshrined/svg-sanitize": "0.22.*"
},
"require-dev": {
"ext-fileinfo": "*",
Generated
+130 -67
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "4fe91e67f343fbe6deac1fdc7eda949f",
"content-hash": "e9c38bbebc60849e70e3640aaa4422cd",
"packages": [
{
"name": "adhocore/jwt",
@@ -3403,16 +3403,16 @@
},
{
"name": "utopia-php/agents",
"version": "1.3.0",
"version": "1.2.1",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/agents.git",
"reference": "06064fd9fb19b77ae45a12ec7bcbc17670912c30"
"reference": "052227953678a30ecc4b5467401fcb0b2386471e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/agents/zipball/06064fd9fb19b77ae45a12ec7bcbc17670912c30",
"reference": "06064fd9fb19b77ae45a12ec7bcbc17670912c30",
"url": "https://api.github.com/repos/utopia-php/agents/zipball/052227953678a30ecc4b5467401fcb0b2386471e",
"reference": "052227953678a30ecc4b5467401fcb0b2386471e",
"shasum": ""
},
"require": {
@@ -3450,9 +3450,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/agents/issues",
"source": "https://github.com/utopia-php/agents/tree/1.3.0"
"source": "https://github.com/utopia-php/agents/tree/1.2.1"
},
"time": "2026-03-26T03:51:11+00:00"
"time": "2026-02-24T06:03:55+00:00"
},
{
"name": "utopia-php/analytics",
@@ -3658,21 +3658,21 @@
},
{
"name": "utopia-php/cli",
"version": "0.22.0",
"version": "0.23.1",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/cli.git",
"reference": "a7ac387ee626fd27075a87e836fb72c5be38add4"
"reference": "8d1955b8bc4dc631f45d7c7df689ed7b63f70621"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/cli/zipball/a7ac387ee626fd27075a87e836fb72c5be38add4",
"reference": "a7ac387ee626fd27075a87e836fb72c5be38add4",
"url": "https://api.github.com/repos/utopia-php/cli/zipball/8d1955b8bc4dc631f45d7c7df689ed7b63f70621",
"reference": "8d1955b8bc4dc631f45d7c7df689ed7b63f70621",
"shasum": ""
},
"require": {
"php": ">=7.4",
"utopia-php/servers": "0.2.*"
"utopia-php/servers": "0.3.*"
},
"require-dev": {
"laravel/pint": "1.2.*",
@@ -3703,9 +3703,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/cli/issues",
"source": "https://github.com/utopia-php/cli/tree/0.22.0"
"source": "https://github.com/utopia-php/cli/tree/0.23.1"
},
"time": "2025-10-21T10:42:45+00:00"
"time": "2026-04-05T15:27:35+00:00"
},
{
"name": "utopia-php/compression",
@@ -3954,25 +3954,26 @@
},
{
"name": "utopia-php/di",
"version": "0.1.0",
"version": "0.3.2",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/di.git",
"reference": "22490c95f7ac3898ed1c33f1b1b5dd577305ee31"
"reference": "07025d721ed5d9be27932e8e640acf1467fc4b9d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/di/zipball/22490c95f7ac3898ed1c33f1b1b5dd577305ee31",
"reference": "22490c95f7ac3898ed1c33f1b1b5dd577305ee31",
"url": "https://api.github.com/repos/utopia-php/di/zipball/07025d721ed5d9be27932e8e640acf1467fc4b9d",
"reference": "07025d721ed5d9be27932e8e640acf1467fc4b9d",
"shasum": ""
},
"require": {
"php": ">=8.2"
"php": ">=8.2",
"psr/container": "^2.0"
},
"require-dev": {
"laravel/pint": "^1.2",
"laravel/pint": "^1.27",
"phpbench/phpbench": "^1.2",
"phpstan/phpstan": "^1.10",
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "^9.5.25",
"swoole/ide-helper": "4.8.3"
},
@@ -3989,16 +3990,18 @@
],
"description": "A simple and lite library for managing dependency injections",
"keywords": [
"framework",
"http",
"PSR-11",
"container",
"dependency-injection",
"di",
"php",
"upf"
"utopia"
],
"support": {
"issues": "https://github.com/utopia-php/di/issues",
"source": "https://github.com/utopia-php/di/tree/0.1.0"
"source": "https://github.com/utopia-php/di/tree/0.3.2"
},
"time": "2024-08-08T14:35:19+00:00"
"time": "2026-03-21T07:42:10+00:00"
},
{
"name": "utopia-php/dns",
@@ -4268,30 +4271,34 @@
},
{
"name": "utopia-php/framework",
"version": "0.33.41",
"version": "0.34.18",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/http.git",
"reference": "0f3bf2377c867e547c929c3733b8224afee6ef06"
"reference": "c8e7e8fc9b9b68aa874e365c83010fefe8ae8ccc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/http/zipball/0f3bf2377c867e547c929c3733b8224afee6ef06",
"reference": "0f3bf2377c867e547c929c3733b8224afee6ef06",
"url": "https://api.github.com/repos/utopia-php/http/zipball/c8e7e8fc9b9b68aa874e365c83010fefe8ae8ccc",
"reference": "c8e7e8fc9b9b68aa874e365c83010fefe8ae8ccc",
"shasum": ""
},
"require": {
"php": ">=8.3",
"ext-swoole": "*",
"php": ">=8.2",
"utopia-php/compression": "0.1.*",
"utopia-php/di": "0.3.*",
"utopia-php/servers": "0.3.*",
"utopia-php/telemetry": "0.2.*",
"utopia-php/validators": "0.2.*"
},
"require-dev": {
"doctrine/instantiator": "^1.5",
"laravel/pint": "1.*",
"phpbench/phpbench": "1.*",
"phpbench/phpbench": "^1.2",
"phpstan/phpstan": "1.*",
"phpunit/phpunit": "9.*",
"swoole/ide-helper": "^6.0"
"phpunit/phpunit": "^9.5.25",
"swoole/ide-helper": "4.8.3"
},
"type": "library",
"autoload": {
@@ -4303,17 +4310,72 @@
"license": [
"MIT"
],
"description": "A simple, light and advanced PHP framework",
"description": "A simple, light and advanced PHP HTTP framework",
"keywords": [
"framework",
"http",
"php",
"upf"
],
"support": {
"issues": "https://github.com/utopia-php/http/issues",
"source": "https://github.com/utopia-php/http/tree/0.33.41"
"source": "https://github.com/utopia-php/http/tree/0.34.18"
},
"time": "2026-02-24T12:01:28+00:00"
"time": "2026-04-07T08:06:39+00:00"
},
{
"name": "utopia-php/http",
"version": "0.34.16",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/http.git",
"reference": "2b4021ba3f9d476264ce9fd6703d6c79de9add7f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/http/zipball/2b4021ba3f9d476264ce9fd6703d6c79de9add7f",
"reference": "2b4021ba3f9d476264ce9fd6703d6c79de9add7f",
"shasum": ""
},
"require": {
"ext-swoole": "*",
"php": ">=8.2",
"utopia-php/compression": "0.1.*",
"utopia-php/di": "0.3.*",
"utopia-php/servers": "0.3.*",
"utopia-php/telemetry": "0.2.*",
"utopia-php/validators": "0.2.*"
},
"require-dev": {
"doctrine/instantiator": "^1.5",
"laravel/pint": "1.*",
"phpbench/phpbench": "^1.2",
"phpstan/phpstan": "1.*",
"phpunit/phpunit": "^9.5.25",
"swoole/ide-helper": "4.8.3"
},
"type": "library",
"autoload": {
"psr-4": {
"Utopia\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "A simple, light and advanced PHP HTTP framework",
"keywords": [
"framework",
"http",
"php",
"upf"
],
"support": {
"issues": "https://github.com/utopia-php/http/issues",
"source": "https://github.com/utopia-php/http/tree/0.34.16"
},
"time": "2026-03-20T10:39:07+00:00"
},
{
"name": "utopia-php/image",
@@ -4634,30 +4696,30 @@
},
{
"name": "utopia-php/platform",
"version": "0.7.16",
"version": "0.12.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/platform.git",
"reference": "34e67e4b80b5741c380071fe765fbc12a132de4f"
"reference": "068ee46228f0c3972e6b569f2c86b6c80fe583d8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/platform/zipball/34e67e4b80b5741c380071fe765fbc12a132de4f",
"reference": "34e67e4b80b5741c380071fe765fbc12a132de4f",
"url": "https://api.github.com/repos/utopia-php/platform/zipball/068ee46228f0c3972e6b569f2c86b6c80fe583d8",
"reference": "068ee46228f0c3972e6b569f2c86b6c80fe583d8",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-redis": "*",
"php": ">=8.0",
"utopia-php/cli": "0.22.*",
"utopia-php/framework": "0.33.*",
"utopia-php/queue": "0.15.*"
"php": ">=8.1",
"utopia-php/cli": "0.23.*",
"utopia-php/http": "0.34.*",
"utopia-php/queue": "0.17.*",
"utopia-php/servers": "0.3.*"
},
"require-dev": {
"laravel/pint": "1.*",
"phpstan/phpstan": "2.*",
"phpunit/phpunit": "9.*"
"laravel/pint": "1.2.*",
"phpunit/phpunit": "^9.3"
},
"type": "library",
"autoload": {
@@ -4679,9 +4741,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/platform/issues",
"source": "https://github.com/utopia-php/platform/tree/0.7.16"
"source": "https://github.com/utopia-php/platform/tree/0.12.0"
},
"time": "2026-02-11T06:36:48+00:00"
"time": "2026-03-31T14:44:23+00:00"
},
{
"name": "utopia-php/pools",
@@ -4791,32 +4853,33 @@
},
{
"name": "utopia-php/queue",
"version": "0.15.6",
"version": "0.17.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/queue.git",
"reference": "08e361d69610f371382b344c369eef355ca414b4"
"reference": "0fbc7d7312f5cf76ec112513fb93317000901f5f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/queue/zipball/08e361d69610f371382b344c369eef355ca414b4",
"reference": "08e361d69610f371382b344c369eef355ca414b4",
"url": "https://api.github.com/repos/utopia-php/queue/zipball/0fbc7d7312f5cf76ec112513fb93317000901f5f",
"reference": "0fbc7d7312f5cf76ec112513fb93317000901f5f",
"shasum": ""
},
"require": {
"php": ">=8.3",
"php-amqplib/php-amqplib": "^3.7",
"utopia-php/di": "0.3.*",
"utopia-php/fetch": "0.5.*",
"utopia-php/pools": "1.*",
"utopia-php/servers": "0.2.*",
"utopia-php/servers": "0.3.*",
"utopia-php/telemetry": "0.2.*",
"utopia-php/validators": "0.2.*"
},
"require-dev": {
"ext-redis": "*",
"laravel/pint": "^0.2.3",
"laravel/pint": "^1.0",
"phpstan/phpstan": "^1.8",
"phpunit/phpunit": "^9.5.5",
"phpunit/phpunit": "^11.0",
"swoole/ide-helper": "4.8.8",
"workerman/workerman": "^4.0"
},
@@ -4851,9 +4914,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/queue/issues",
"source": "https://github.com/utopia-php/queue/tree/0.15.6"
"source": "https://github.com/utopia-php/queue/tree/0.17.0"
},
"time": "2026-02-23T13:03:51+00:00"
"time": "2026-03-23T16:21:31+00:00"
},
{
"name": "utopia-php/registry",
@@ -4909,21 +4972,21 @@
},
{
"name": "utopia-php/servers",
"version": "0.2.5",
"version": "0.3.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/servers.git",
"reference": "4770e879a90685af4ba14e7e5d95d0a17c7fdf03"
"reference": "235be31200df9437fc96a1c270ffef4c64fafe52"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/servers/zipball/4770e879a90685af4ba14e7e5d95d0a17c7fdf03",
"reference": "4770e879a90685af4ba14e7e5d95d0a17c7fdf03",
"url": "https://api.github.com/repos/utopia-php/servers/zipball/235be31200df9437fc96a1c270ffef4c64fafe52",
"reference": "235be31200df9437fc96a1c270ffef4c64fafe52",
"shasum": ""
},
"require": {
"php": ">=8.0",
"utopia-php/di": "0.1.*",
"php": ">=8.2",
"utopia-php/di": "0.3.*",
"utopia-php/validators": "0.*"
},
"require-dev": {
@@ -4957,9 +5020,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/servers/issues",
"source": "https://github.com/utopia-php/servers/tree/0.2.5"
"source": "https://github.com/utopia-php/servers/tree/0.3.0"
},
"time": "2026-02-10T04:21:53+00:00"
"time": "2026-03-13T11:31:42+00:00"
},
{
"name": "utopia-php/span",
+2
View File
@@ -333,6 +333,8 @@ class Exception extends \Exception
/** Platform */
public const string PLATFORM_NOT_FOUND = 'platform_not_found';
public const string PLATFORM_METHOD_UNSUPPORTED = 'platform_method_unsupported';
public const string PLATFORM_ALREADY_EXISTS = 'platform_already_exists';
/** GraphqQL */
public const string GRAPHQL_NO_QUERY = 'graphql_no_query';
+19 -19
View File
@@ -26,9 +26,9 @@ class Resolvers
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $route, $args) {
$utopia = $utopia->getResource('utopia:graphql', true);
$request = $utopia->getResource('request', true);
$response = $utopia->getResource('response', true);
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
$path = $route->getPath();
foreach ($args as $key => $value) {
@@ -93,9 +93,9 @@ class Resolvers
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $args) {
$utopia = $utopia->getResource('utopia:graphql', true);
$request = $utopia->getResource('request', true);
$response = $utopia->getResource('response', true);
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
$request->setMethod('GET');
$request->setURI($url($databaseId, $collectionId, $args));
@@ -124,9 +124,9 @@ class Resolvers
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
$utopia = $utopia->getResource('utopia:graphql', true);
$request = $utopia->getResource('request', true);
$response = $utopia->getResource('response', true);
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
$request->setMethod('GET');
$request->setURI($url($databaseId, $collectionId, $args));
@@ -160,9 +160,9 @@ class Resolvers
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
$utopia = $utopia->getResource('utopia:graphql', true);
$request = $utopia->getResource('request', true);
$response = $utopia->getResource('response', true);
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
$request->setMethod('POST');
$request->setURI($url($databaseId, $collectionId, $args));
@@ -192,9 +192,9 @@ class Resolvers
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
$utopia = $utopia->getResource('utopia:graphql', true);
$request = $utopia->getResource('request', true);
$response = $utopia->getResource('response', true);
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
$request->setMethod('PATCH');
$request->setURI($url($databaseId, $collectionId, $args));
@@ -222,9 +222,9 @@ class Resolvers
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $args) {
$utopia = $utopia->getResource('utopia:graphql', true);
$request = $utopia->getResource('request', true);
$response = $utopia->getResource('response', true);
$utopia = $utopia->getResource('utopia:graphql');
$request = $utopia->getResource('request');
$response = $utopia->getResource('response');
$request->setMethod('DELETE');
$request->setURI($url($databaseId, $collectionId, $args));
@@ -266,7 +266,7 @@ class Resolvers
try {
$route = $utopia->match($request, fresh: true);
$utopia->execute($route, $request, $response);
$utopia->execute($route, $request);
} catch (\Throwable $e) {
if ($beforeReject) {
$e = $beforeReject($e);
-4
View File
@@ -32,10 +32,6 @@ class Schema
array $urls,
array $params,
): GQLSchema {
Http::setResource('utopia:graphql', static function () use ($utopia) {
return $utopia;
});
if (!empty(self::$schema)) {
return self::$schema;
}
+12 -45
View File
@@ -6,20 +6,10 @@ class Platform
{
public const TYPE_UNKNOWN = 'unknown';
public const TYPE_WEB = 'web';
public const TYPE_FLUTTER_IOS = 'flutter-ios';
public const TYPE_FLUTTER_ANDROID = 'flutter-android';
public const TYPE_FLUTTER_MACOS = 'flutter-macos';
public const TYPE_FLUTTER_WINDOWS = 'flutter-windows';
public const TYPE_FLUTTER_LINUX = 'flutter-linux';
public const TYPE_FLUTTER_WEB = 'flutter-web';
public const TYPE_APPLE_IOS = 'apple-ios';
public const TYPE_APPLE_MACOS = 'apple-macos';
public const TYPE_APPLE_WATCHOS = 'apple-watchos';
public const TYPE_APPLE_TVOS = 'apple-tvos';
public const TYPE_APPLE = 'apple';
public const TYPE_ANDROID = 'android';
public const TYPE_UNITY = 'unity';
public const TYPE_REACT_NATIVE_IOS = 'react-native-ios';
public const TYPE_REACT_NATIVE_ANDROID = 'react-native-android';
public const TYPE_WINDOWS = 'windows';
public const TYPE_LINUX = 'linux';
public const TYPE_SCHEME = 'scheme';
public const SCHEME_HTTP = 'http';
@@ -78,24 +68,14 @@ class Platform
switch ($type) {
case self::TYPE_WEB:
case self::TYPE_FLUTTER_WEB:
if (!empty($hostname)) {
$hostnames[] = $hostname;
}
break;
case self::TYPE_FLUTTER_IOS:
case self::TYPE_FLUTTER_ANDROID:
case self::TYPE_FLUTTER_MACOS:
case self::TYPE_FLUTTER_WINDOWS:
case self::TYPE_FLUTTER_LINUX:
case self::TYPE_ANDROID:
case self::TYPE_APPLE_IOS:
case self::TYPE_APPLE_MACOS:
case self::TYPE_APPLE_WATCHOS:
case self::TYPE_APPLE_TVOS:
case self::TYPE_REACT_NATIVE_IOS:
case self::TYPE_REACT_NATIVE_ANDROID:
case self::TYPE_UNITY:
case self::TYPE_WINDOWS:
case self::TYPE_LINUX:
case self::TYPE_APPLE:
if (!empty($key)) {
$hostnames[] = $key;
}
@@ -121,37 +101,24 @@ class Platform
}
break;
case self::TYPE_WEB:
case self::TYPE_FLUTTER_WEB:
$schemes[] = self::SCHEME_HTTP;
$schemes[] = self::SCHEME_HTTPS;
break;
case self::TYPE_FLUTTER_IOS:
case self::TYPE_APPLE_IOS:
case self::TYPE_REACT_NATIVE_IOS:
$schemes[] = self::SCHEME_IOS;
break;
case self::TYPE_FLUTTER_ANDROID:
case self::TYPE_ANDROID:
case self::TYPE_REACT_NATIVE_ANDROID:
$schemes[] = self::SCHEME_ANDROID;
break;
case self::TYPE_FLUTTER_MACOS:
case self::TYPE_APPLE_MACOS:
case self::TYPE_APPLE:
$schemes[] = self::SCHEME_WATCHOS;
$schemes[] = self::SCHEME_MACOS;
$schemes[] = self::SCHEME_TVOS;
$schemes[] = self::SCHEME_IOS;
break;
case self::TYPE_FLUTTER_WINDOWS:
case self::TYPE_UNITY:
case self::TYPE_WINDOWS:
$schemes[] = self::SCHEME_WINDOWS;
break;
case self::TYPE_FLUTTER_LINUX:
case self::TYPE_LINUX:
$schemes[] = self::SCHEME_LINUX;
break;
case self::TYPE_APPLE_WATCHOS:
$schemes[] = self::SCHEME_WATCHOS;
break;
case self::TYPE_APPLE_TVOS:
$schemes[] = self::SCHEME_TVOS;
break;
default:
break;
}
+16 -16
View File
@@ -145,9 +145,20 @@ class Server
$paths = $this->paths;
$state = $this->state;
Http::setResource('installerState', fn () => $state);
Http::setResource('installerConfig', fn () => $config);
Http::setResource('installerPaths', fn () => $paths);
$adapter = new class ($host, $port, ['worker_num' => 1]) extends SwooleAdapter {
public function getNativeServer(): SwooleServer
{
return $this->server;
}
};
$nativeServer = $adapter->getNativeServer();
$container = $adapter->getContainer();
$container->set('installerState', fn () => $state);
$container->set('installerConfig', fn () => $config);
$container->set('installerPaths', fn () => $paths);
$container->set('swooleServer', fn () => $nativeServer);
// Register routes via Utopia Platform
$platform = new Installer();
@@ -160,17 +171,6 @@ class Server
->inject('response')
->action($errorHandler->action(...));
$adapter = new class ($host, $port, ['worker_num' => 1]) extends SwooleAdapter {
public function getNativeServer(): SwooleServer
{
return $this->server;
}
};
$nativeServer = $adapter->getNativeServer();
Http::setResource('swooleServer', fn () => $nativeServer);
$nativeServer->on('start', function () use ($nativeServer, $port, $readyFile) {
\Swoole\Process::signal(SIGTERM, fn () => $nativeServer->shutdown());
\Swoole\Process::signal(SIGINT, fn () => $nativeServer->shutdown());
@@ -180,7 +180,7 @@ class Server
}
});
$adapter->onRequest(function (Request $request, Response $response) use ($files) {
$adapter->onRequest(function (Request $request, Response $response) use ($adapter, $files) {
// Serve static files from memory
$uri = $request->getURI();
if ($files->isFileLoaded($uri)) {
@@ -190,7 +190,7 @@ class Server
return;
}
$app = new Http('UTC');
$app = new Http($adapter, 'UTC');
$app->run($request, $response);
});
@@ -182,19 +182,33 @@ class Update extends Action
$dbForDatabases = $getDatabasesDB($databaseDoc);
try {
$dbForDatabases->withTransaction(function () use ($dbForDatabases, $dbForProject, $transactionState, $queueForDeletes, $transactionId, &$transaction, &$operations, &$totalOperations, &$databaseOperations, &$currentDocumentId, $authorization) {
$authorization->skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([
'status' => 'committing',
])));
$transaction = $authorization->skip(fn () => $dbForProject->updateDocument(
'transactions',
$transactionId,
new Document(['status' => 'committing'])
));
$operations = $authorization->skip(fn () => $dbForProject->find('transactionLogs', [
Query::equal('transactionInternalId', [$transaction->getSequence()]),
Query::orderAsc(),
Query::limit(PHP_INT_MAX),
]));
$operations = $authorization->skip(fn () => $dbForProject->find('transactionLogs', [
Query::equal('transactionInternalId', [$transaction->getSequence()]),
Query::orderAsc(),
Query::limit(PHP_INT_MAX),
]));
$collections = [];
foreach ($operations as $operation) {
$databaseInternalId = $operation['databaseInternalId'];
$collectionInternalId = $operation['collectionInternalId'];
$collectionId = "database_{$databaseInternalId}_collection_{$collectionInternalId}";
if (!isset($collections[$collectionId])) {
$collections[$collectionId] = $authorization->skip(
fn () => $dbForProject->getCollection($collectionId)
);
}
}
$dbForDatabases->withTransaction(function () use ($dbForDatabases, $transactionState, &$operations, &$totalOperations, &$databaseOperations, &$currentDocumentId, $collections) {
$state = [];
$collections = [];
foreach ($operations as $operation) {
$databaseInternalId = $operation['databaseInternalId'];
@@ -210,11 +224,6 @@ class Update extends Action
$data = $data->getArrayCopy();
}
if (!isset($collections[$collectionId])) {
$collections[$collectionId] = $authorization->skip(
fn () => $dbForProject->getCollection($collectionId)
);
}
$collection = $collections[$collectionId];
if (\is_array($data) && !empty($data)) {
@@ -276,16 +285,17 @@ class Update extends Action
}
}
$transaction = $authorization->skip(fn () => $dbForProject->updateDocument(
'transactions',
$transactionId,
new Document(['status' => 'committed'])
));
$queueForDeletes
->setType(DELETE_TYPE_DOCUMENT)
->setDocument($transaction);
});
$transaction = $authorization->skip(fn () => $dbForProject->updateDocument(
'transactions',
$transactionId,
new Document(['status' => 'committed'])
));
$queueForDeletes
->setType(DELETE_TYPE_DOCUMENT)
->setDocument($transaction);
} catch (NotFoundException $e) {
$authorization->skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([
'status' => 'failed',
@@ -1,20 +1,15 @@
<?php
namespace Appwrite\Platform\Modules\Projects\Http\Projects\Labels;
namespace Appwrite\Platform\Modules\Project\Http\Project\Labels;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\Queries\Projects;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Text;
@@ -27,39 +22,37 @@ class Update extends Action
return 'updateProjectLabels';
}
protected function getQueriesValidator(): Validator
{
return new Projects();
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT)
->setHttpPath('/v1/projects/:projectId/labels')
->setHttpPath('/v1/project/labels')
->httpAlias('/v1/projects/:projectId/labels')
->desc('Update project labels')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->groups(['api', 'project'])
->label('scope', 'project.write')
->label('event', 'labels.*.update')
->label('audits.event', 'project.labels.update')
->label('audits.resource', 'project.labels/{response.$id}')
->label('sdk', new Method(
namespace: 'projects',
group: 'projects',
namespace: 'project',
group: null,
name: 'updateLabels',
description: <<<EOT
Update the project labels by its unique ID. Labels can be used to easily filter projects in an organization.
Update the project labels. Labels can be used to easily filter projects in an organization.
EOT,
auth: [AuthType::ADMIN],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROJECT
model: Response::MODEL_PROJECT,
)
],
contentType: ContentType::JSON
))
->param('projectId', '', new UID(), 'Project unique ID.')
->param('labels', [], new ArrayList(new Text(36, allowList: [...Text::NUMBERS, ...Text::ALPHABET_UPPER, ...Text::ALPHABET_LOWER]), APP_LIMIT_ARRAY_LABELS_SIZE), 'Array of project labels. Replaces the previous labels. Maximum of ' . APP_LIMIT_ARRAY_LABELS_SIZE . ' labels are allowed, each up to 36 alphanumeric characters long.')
->inject('response')
->inject('dbForPlatform')
->inject('project')
->callback($this->action(...));
}
@@ -67,17 +60,11 @@ class Update extends Action
* @param array<string> $labels
*/
public function action(
string $projectId,
array $labels,
Response $response,
Database $dbForPlatform
Database $dbForPlatform,
Document $project
): void {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$labels = (array) \array_values(\array_unique($labels));
$project = $dbForPlatform->updateDocument('projects', $project->getId(), new Document(['labels' => $labels]));
@@ -0,0 +1,105 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\Platforms\Android;
use Appwrite\Event\Event as QueueEvent;
use Appwrite\Extend\Exception;
use Appwrite\Network\Platform;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\Authorization;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Text;
class Create extends Action
{
use HTTP;
public static function getName()
{
return 'createProjectAndroidPlatform';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/project/platforms/android')
->desc('Create project Android platform')
->groups(['api', 'project'])
->label('scope', 'project.write')
->label('event', 'platforms.[platformId].create')
->label('audits.event', 'project.platform.create')
->label('audits.resource', 'project.platform/{response.$id}')
->label('sdk', new Method(
namespace: 'project',
group: 'platforms',
name: 'createAndroidPlatform',
description: <<<EOT
Create a new Android platform for your project. Use this endpoint to register a new Android platform where your users will run your application which will interact with the Appwrite API.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
model: Response::MODEL_PLATFORM_ANDROID,
)
],
))
->param('platformId', '', fn (Database $dbForPlatform) => new CustomId(false, $dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForPlatform'])
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
->param('applicationId', '', new Text(256), 'Android application ID. Max length: 256 chars.')
->inject('response')
->inject('queueForEvents')
->inject('project')
->inject('dbForPlatform')
->inject('authorization')
->callback($this->action(...));
}
public function action(
string $platformId,
string $name,
string $applicationId,
Response $response,
QueueEvent $queueForEvents,
Document $project,
Database $dbForPlatform,
Authorization $authorization,
) {
$platformId = ($platformId == 'unique()') ? ID::unique() : $platformId;
$platform = new Document([
'$id' => $platformId,
'$permissions' => [],
'projectInternalId' => $project->getSequence(),
'projectId' => $project->getId(),
'type' => Platform::TYPE_ANDROID,
'name' => $name,
'key' => $applicationId,
'hostname' => '',
]);
try {
$platform = $authorization->skip(fn () => $dbForPlatform->createDocument('platforms', $platform));
} catch (DuplicateException) {
throw new Exception(Exception::PLATFORM_ALREADY_EXISTS);
}
$authorization->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
$queueForEvents->setParam('platformId', $platform->getId());
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($platform, Response::MODEL_PLATFORM_ANDROID);
}
}
@@ -0,0 +1,103 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\Platforms\Android;
use Appwrite\Event\Event as QueueEvent;
use Appwrite\Extend\Exception;
use Appwrite\Network\Platform;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Text;
class Update extends Action
{
use HTTP;
public static function getName()
{
return 'updateProjectAndroidPlatform';
}
public function __construct()
{
$this->setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT)
->setHttpPath('/v1/project/platforms/android/:platformId')
->desc('Update project Android platform')
->groups(['api', 'project'])
->label('scope', 'project.write')
->label('event', 'platforms.[platformId].update')
->label('audits.event', 'project.platform.update')
->label('audits.resource', 'project.platform/{response.$id}')
->label('sdk', new Method(
namespace: 'project',
group: 'platforms',
name: 'updateAndroidPlatform',
description: <<<EOT
Update an Android platform by its unique ID. Use this endpoint to update the platform's name or application ID.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PLATFORM_ANDROID,
)
]
))
->param('platformId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform ID.', false, ['dbForPlatform'])
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
->param('applicationId', '', new Text(256), 'Android application ID. Max length: 256 chars.')
->inject('response')
->inject('queueForEvents')
->inject('dbForPlatform')
->inject('authorization')
->inject('project')
->callback($this->action(...));
}
public function action(
string $platformId,
string $name,
string $applicationId,
Response $response,
QueueEvent $queueForEvents,
Database $dbForPlatform,
Authorization $authorization,
Document $project,
) {
$platform = $authorization->skip(fn () => $dbForPlatform->getDocument('platforms', $platformId));
if ($platform->isEmpty() || $platform->getAttribute('projectInternalId', '') !== $project->getSequence()) {
throw new Exception(Exception::PLATFORM_NOT_FOUND);
}
if ($platform->getAttribute('type', '') !== Platform::TYPE_ANDROID) {
throw new Exception(Exception::PLATFORM_METHOD_UNSUPPORTED);
}
$updates = new Document([
'name' => $name,
'key' => $applicationId,
]);
try {
$platform = $authorization->skip(fn () => $dbForPlatform->updateDocument('platforms', $platform->getId(), $updates));
} catch (Duplicate) {
throw new Exception(Exception::PLATFORM_ALREADY_EXISTS);
}
$authorization->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
$queueForEvents->setParam('platformId', $platform->getId());
$response->dynamic($platform, Response::MODEL_PLATFORM_ANDROID);
}
}
@@ -0,0 +1,105 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\Platforms\Apple;
use Appwrite\Event\Event as QueueEvent;
use Appwrite\Extend\Exception;
use Appwrite\Network\Platform;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\Authorization;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Text;
class Create extends Action
{
use HTTP;
public static function getName()
{
return 'createProjectApplePlatform';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/project/platforms/apple')
->desc('Create project Apple platform')
->groups(['api', 'project'])
->label('scope', 'project.write')
->label('event', 'platforms.[platformId].create')
->label('audits.event', 'project.platform.create')
->label('audits.resource', 'project.platform/{response.$id}')
->label('sdk', new Method(
namespace: 'project',
group: 'platforms',
name: 'createApplePlatform',
description: <<<EOT
Create a new Apple platform for your project. Use this endpoint to register a new Apple platform where your users will run your application which will interact with the Appwrite API.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
model: Response::MODEL_PLATFORM_APPLE,
)
],
))
->param('platformId', '', fn (Database $dbForPlatform) => new CustomId(false, $dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForPlatform'])
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
->param('bundleIdentifier', '', new Text(256), 'Apple bundle identifier. Max length: 256 chars.')
->inject('response')
->inject('queueForEvents')
->inject('project')
->inject('dbForPlatform')
->inject('authorization')
->callback($this->action(...));
}
public function action(
string $platformId,
string $name,
string $bundleIdentifier,
Response $response,
QueueEvent $queueForEvents,
Document $project,
Database $dbForPlatform,
Authorization $authorization,
) {
$platformId = ($platformId == 'unique()') ? ID::unique() : $platformId;
$platform = new Document([
'$id' => $platformId,
'$permissions' => [],
'projectInternalId' => $project->getSequence(),
'projectId' => $project->getId(),
'type' => Platform::TYPE_APPLE,
'name' => $name,
'key' => $bundleIdentifier,
'hostname' => '',
]);
try {
$platform = $authorization->skip(fn () => $dbForPlatform->createDocument('platforms', $platform));
} catch (DuplicateException) {
throw new Exception(Exception::PLATFORM_ALREADY_EXISTS);
}
$authorization->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
$queueForEvents->setParam('platformId', $platform->getId());
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($platform, Response::MODEL_PLATFORM_APPLE);
}
}
@@ -0,0 +1,103 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\Platforms\Apple;
use Appwrite\Event\Event as QueueEvent;
use Appwrite\Extend\Exception;
use Appwrite\Network\Platform;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Text;
class Update extends Action
{
use HTTP;
public static function getName()
{
return 'updateProjectApplePlatform';
}
public function __construct()
{
$this->setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT)
->setHttpPath('/v1/project/platforms/apple/:platformId')
->desc('Update project Apple platform')
->groups(['api', 'project'])
->label('scope', 'project.write')
->label('event', 'platforms.[platformId].update')
->label('audits.event', 'project.platform.update')
->label('audits.resource', 'project.platform/{response.$id}')
->label('sdk', new Method(
namespace: 'project',
group: 'platforms',
name: 'updateApplePlatform',
description: <<<EOT
Update an Apple platform by its unique ID. Use this endpoint to update the platform's name or bundle identifier.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PLATFORM_APPLE,
)
]
))
->param('platformId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform ID.', false, ['dbForPlatform'])
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
->param('bundleIdentifier', '', new Text(256), 'Apple bundle identifier. Max length: 256 chars.')
->inject('response')
->inject('queueForEvents')
->inject('dbForPlatform')
->inject('authorization')
->inject('project')
->callback($this->action(...));
}
public function action(
string $platformId,
string $name,
string $bundleIdentifier,
Response $response,
QueueEvent $queueForEvents,
Database $dbForPlatform,
Authorization $authorization,
Document $project,
) {
$platform = $authorization->skip(fn () => $dbForPlatform->getDocument('platforms', $platformId));
if ($platform->isEmpty() || $platform->getAttribute('projectInternalId', '') !== $project->getSequence()) {
throw new Exception(Exception::PLATFORM_NOT_FOUND);
}
if ($platform->getAttribute('type', '') !== Platform::TYPE_APPLE) {
throw new Exception(Exception::PLATFORM_METHOD_UNSUPPORTED);
}
$updates = new Document([
'name' => $name,
'key' => $bundleIdentifier,
]);
try {
$platform = $authorization->skip(fn () => $dbForPlatform->updateDocument('platforms', $platform->getId(), $updates));
} catch (Duplicate) {
throw new Exception(Exception::PLATFORM_ALREADY_EXISTS);
}
$authorization->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
$queueForEvents->setParam('platformId', $platform->getId());
$response->dynamic($platform, Response::MODEL_PLATFORM_APPLE);
}
}
@@ -0,0 +1,89 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\Platforms;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Delete extends Action
{
use HTTP;
public static function getName()
{
return 'deleteProjectPlatform';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE)
->setHttpPath('/v1/project/platforms/:platformId')
->httpAlias('/v1/projects/:projectId/platforms/:platformId')
->desc('Delete project platform')
->groups(['api', 'project'])
->label('scope', 'project.write')
->label('event', 'platforms.[platformId].delete')
->label('audits.event', 'project.platform.delete')
->label('audits.resource', 'project.platform/{response.$id}')
->label('sdk', new Method(
namespace: 'project',
group: 'platforms',
name: 'deletePlatform',
description: <<<EOT
Delete a platform by its unique ID. This endpoint removes the platform and all its configurations from the project.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_NOCONTENT,
model: Response::MODEL_NONE,
)
],
contentType: ContentType::NONE
))
->param('platformId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform ID.', false, ['dbForPlatform'])
->inject('response')
->inject('dbForPlatform')
->inject('authorization')
->inject('project')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(
string $platformId,
Response $response,
Database $dbForPlatform,
Authorization $authorization,
Document $project,
Event $queueForEvents,
) {
$platform = $authorization->skip(fn () => $dbForPlatform->getDocument('platforms', $platformId));
if ($platform->isEmpty() || $platform->getAttribute('projectInternalId', '') !== $project->getSequence()) {
throw new Exception(Exception::PLATFORM_NOT_FOUND);
}
if (!$authorization->skip(fn () => $dbForPlatform->deleteDocument('platforms', $platform->getId()))) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove document from DB');
};
$authorization->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
$queueForEvents->setParam('platformId', $platform->getId());
$response->noContent();
}
}
@@ -0,0 +1,91 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\Platforms;
use Appwrite\Extend\Exception;
use Appwrite\Network\Platform;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Get extends Action
{
use HTTP;
public static function getName()
{
return 'getProjectPlatform';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/project/platforms/:platformId')
->httpAlias('/v1/projects/:projectId/platforms/:platformId')
->desc('Get project platform')
->groups(['api', 'project'])
->label('scope', 'project.read')
->label('sdk', new Method(
namespace: 'project',
group: 'platforms',
name: 'getPlatform',
description: <<<EOT
Get a platform by its unique ID. This endpoint returns the platform's details, including its name, type, and key configurations.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: [
Response::MODEL_PLATFORM_WEB,
Response::MODEL_PLATFORM_APPLE,
Response::MODEL_PLATFORM_ANDROID,
Response::MODEL_PLATFORM_WINDOWS,
Response::MODEL_PLATFORM_LINUX,
],
)
]
))
->param('platformId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform ID.', false, ['dbForPlatform'])
->inject('response')
->inject('dbForPlatform')
->inject('authorization')
->inject('project')
->callback($this->action(...));
}
public function action(
string $platformId,
Response $response,
Database $dbForPlatform,
Authorization $authorization,
Document $project
) {
$platform = $authorization->skip(fn () => $dbForPlatform->getDocument('platforms', $platformId));
if ($platform->isEmpty() || $platform->getAttribute('projectInternalId', '') !== $project->getSequence()) {
throw new Exception(Exception::PLATFORM_NOT_FOUND);
}
$type = $platform->getAttribute('type');
$model = match($type) {
Platform::TYPE_WEB => Response::MODEL_PLATFORM_WEB,
Platform::TYPE_APPLE => Response::MODEL_PLATFORM_APPLE,
Platform::TYPE_ANDROID => Response::MODEL_PLATFORM_ANDROID,
Platform::TYPE_WINDOWS => Response::MODEL_PLATFORM_WINDOWS,
Platform::TYPE_LINUX => Response::MODEL_PLATFORM_LINUX,
default => Response::MODEL_PLATFORM_WEB // Backwards compatibility
};
$response->dynamic($platform, $model);
}
}
@@ -0,0 +1,105 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\Platforms\Linux;
use Appwrite\Event\Event as QueueEvent;
use Appwrite\Extend\Exception;
use Appwrite\Network\Platform;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\Authorization;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Text;
class Create extends Action
{
use HTTP;
public static function getName()
{
return 'createProjectLinuxPlatform';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/project/platforms/linux')
->desc('Create project Linux platform')
->groups(['api', 'project'])
->label('scope', 'project.write')
->label('event', 'platforms.[platformId].create')
->label('audits.event', 'project.platform.create')
->label('audits.resource', 'project.platform/{response.$id}')
->label('sdk', new Method(
namespace: 'project',
group: 'platforms',
name: 'createLinuxPlatform',
description: <<<EOT
Create a new Linux platform for your project. Use this endpoint to register a new Linux platform where your users will run your application which will interact with the Appwrite API.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
model: Response::MODEL_PLATFORM_LINUX,
)
],
))
->param('platformId', '', fn (Database $dbForPlatform) => new CustomId(false, $dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForPlatform'])
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
->param('packageName', '', new Text(256), 'Linux package name. Max length: 256 chars.')
->inject('response')
->inject('queueForEvents')
->inject('project')
->inject('dbForPlatform')
->inject('authorization')
->callback($this->action(...));
}
public function action(
string $platformId,
string $name,
string $packageName,
Response $response,
QueueEvent $queueForEvents,
Document $project,
Database $dbForPlatform,
Authorization $authorization,
) {
$platformId = ($platformId == 'unique()') ? ID::unique() : $platformId;
$platform = new Document([
'$id' => $platformId,
'$permissions' => [],
'projectInternalId' => $project->getSequence(),
'projectId' => $project->getId(),
'type' => Platform::TYPE_LINUX,
'name' => $name,
'key' => $packageName,
'hostname' => '', // Web platform attribute
]);
try {
$platform = $authorization->skip(fn () => $dbForPlatform->createDocument('platforms', $platform));
} catch (DuplicateException) {
throw new Exception(Exception::PLATFORM_ALREADY_EXISTS);
}
$authorization->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
$queueForEvents->setParam('platformId', $platform->getId());
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($platform, Response::MODEL_PLATFORM_LINUX);
}
}
@@ -0,0 +1,103 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\Platforms\Linux;
use Appwrite\Event\Event as QueueEvent;
use Appwrite\Extend\Exception;
use Appwrite\Network\Platform;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Text;
class Update extends Action
{
use HTTP;
public static function getName()
{
return 'updateProjectLinuxPlatform';
}
public function __construct()
{
$this->setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT)
->setHttpPath('/v1/project/platforms/linux/:platformId')
->desc('Update project Linux platform')
->groups(['api', 'project'])
->label('scope', 'project.write')
->label('event', 'platforms.[platformId].update')
->label('audits.event', 'project.platform.update')
->label('audits.resource', 'project.platform/{response.$id}')
->label('sdk', new Method(
namespace: 'project',
group: 'platforms',
name: 'updateLinuxPlatform',
description: <<<EOT
Update a Linux platform by its unique ID. Use this endpoint to update the platform's name or package name.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PLATFORM_LINUX,
)
]
))
->param('platformId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform ID.', false, ['dbForPlatform'])
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
->param('packageName', '', new Text(256), 'Linux package name. Max length: 256 chars.')
->inject('response')
->inject('queueForEvents')
->inject('dbForPlatform')
->inject('authorization')
->inject('project')
->callback($this->action(...));
}
public function action(
string $platformId,
string $name,
string $packageName,
Response $response,
QueueEvent $queueForEvents,
Database $dbForPlatform,
Authorization $authorization,
Document $project,
) {
$platform = $authorization->skip(fn () => $dbForPlatform->getDocument('platforms', $platformId));
if ($platform->isEmpty() || $platform->getAttribute('projectInternalId', '') !== $project->getSequence()) {
throw new Exception(Exception::PLATFORM_NOT_FOUND);
}
if ($platform->getAttribute('type', '') !== Platform::TYPE_LINUX) {
throw new Exception(Exception::PLATFORM_METHOD_UNSUPPORTED);
}
$updates = new Document([
'name' => $name,
'key' => $packageName,
]);
try {
$platform = $authorization->skip(fn () => $dbForPlatform->updateDocument('platforms', $platform->getId(), $updates));
} catch (Duplicate) {
throw new Exception(Exception::PLATFORM_ALREADY_EXISTS);
}
$authorization->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
$queueForEvents->setParam('platformId', $platform->getId());
$response->dynamic($platform, Response::MODEL_PLATFORM_LINUX);
}
}
@@ -0,0 +1,173 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\Platforms\Web;
use Appwrite\Event\Event as QueueEvent;
use Appwrite\Extend\Exception;
use Appwrite\Network\Platform;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\Authorization;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Hostname;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
/**
* WARNING: This kind of platform has most complex action, because it holds backwards compatibility too.
* If possible, refer to any other type of platform for APIs, for more simpler endpoint.
*/
class Create extends Action
{
use HTTP;
public static function getName()
{
return 'createProjectWebPlatform';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/project/platforms/web')
->httpAlias('/v1/projects/:projectId/platforms')
->desc('Create project web platform')
->groups(['api', 'project'])
->label('scope', 'project.write')
->label('event', 'platforms.[platformId].create')
->label('audits.event', 'project.platform.create')
->label('audits.resource', 'project.platform/{response.$id}')
->label('sdk', new Method(
namespace: 'project',
group: 'platforms',
name: 'createWebPlatform',
description: <<<EOT
Create a new web platform for your project. Use this endpoint to register a new platform where your users will run your application which will interact with the Appwrite API.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
model: Response::MODEL_PLATFORM_WEB,
)
],
))
->param('platformId', '', fn (Database $dbForPlatform) => new CustomId(false, $dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForPlatform'])
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
->param('hostname', '', new Hostname(), 'Platform web hostname. Max length: 256 chars.', optional: true) // Optional for backwards compatibility
->param('key', '', new Text(256), 'Deprecated: Package name for Android or bundle ID for iOS or macOS. Max length: 256 chars.', optional: true, deprecated: true) // Exists for backwards compatibility
->param('type', '', new Text(256), 'Deprecated: Platform type. Max length: 256 chars.', optional: true, deprecated: true) // Exists for backwards compatibility
->inject('request')
->inject('response')
->inject('queueForEvents')
->inject('project')
->inject('dbForPlatform')
->inject('authorization')
->callback($this->action(...));
}
public function action(
string $platformId,
string $name,
string $hostname,
?string $key, // For backwards compatibility
?string $type, // For backwards compatibility
Request $request,
Response $response,
QueueEvent $queueForEvents,
Document $project,
Database $dbForPlatform,
Authorization $authorization,
) {
$key = $key ?? ''; // App platform attribute, backwards compatibility
$type = $type ?? ''; // App platform attribute, backwards compatibility
// Backwards compatibility
// Used to have: type, name, key, hostname
if (!empty($type)) {
// Validate deprecated type, and rename to new type
$deprecatedTypeMapping = [
// Web
'web' => Platform::TYPE_WEB,
'flutter-web' => Platform::TYPE_WEB,
'unity' => Platform::TYPE_WEB, // Was not officially supported anyway
// Apple
'flutter-macos' => Platform::TYPE_APPLE,
'flutter-ios' => Platform::TYPE_APPLE,
'react-native-ios' => Platform::TYPE_APPLE,
'apple-ios' => Platform::TYPE_APPLE,
'apple-macos' => Platform::TYPE_APPLE,
'apple-watchos' => Platform::TYPE_APPLE,
'apple-tvos' => Platform::TYPE_APPLE,
// Android
'flutter-android' => Platform::TYPE_ANDROID,
'android' => Platform::TYPE_ANDROID,
'react-native-android' => Platform::TYPE_ANDROID,
'flutter-linux' => Platform::TYPE_LINUX,
'flutter-windows' => Platform::TYPE_WINDOWS,
];
$typeValidator = new WhiteList(\array_keys($deprecatedTypeMapping));
if (!$typeValidator->isValid($request->getParam('type', ''))) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Param "type" is invalid: ' . $typeValidator->getDescription());
}
$type = $deprecatedTypeMapping[$request->getParam('type', '')] ?? '';
}
if (!empty($key)) {
// Validate deprecated app id (key)
$keyValidator = new Text(256);
if (!$keyValidator->isValid($key)) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Param "key" is invalid: ' . $keyValidator->getDescription());
}
}
if (empty($key) && empty($type)) {
// Modern request, validate hostname
if (empty($hostname)) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Param "hostname" is not optional.');
}
}
$platformId = ($platformId == 'unique()') ? ID::unique() : $platformId;
$platform = new Document([
'$id' => $platformId,
'$permissions' => [],
'projectInternalId' => $project->getSequence(),
'projectId' => $project->getId(),
'type' => $type ?: Platform::TYPE_WEB, // Preserve type for backwards compatibility
'name' => $name,
'key' => $key,
'hostname' => $hostname
]);
try {
$platform = $authorization->skip(fn () => $dbForPlatform->createDocument('platforms', $platform));
} catch (DuplicateException) {
throw new Exception(Exception::PLATFORM_ALREADY_EXISTS);
}
$authorization->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
$queueForEvents->setParam('platformId', $platform->getId());
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($platform, Response::MODEL_PLATFORM_WEB);
}
}
@@ -0,0 +1,151 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\Platforms\Web;
use Appwrite\Event\Event as QueueEvent;
use Appwrite\Extend\Exception;
use Appwrite\Network\Platform;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Hostname;
use Utopia\Validator\Text;
class Update extends Action
{
use HTTP;
public static function getName()
{
return 'updateProjectWebPlatform';
}
public function __construct()
{
$this->setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT)
->setHttpPath('/v1/project/platforms/web/:platformId')
->httpAlias('/v1/projects/:projectId/platforms/:platformId')
->desc('Update project web platform')
->groups(['api', 'project'])
->label('scope', 'project.write')
->label('event', 'platforms.[platformId].update')
->label('audits.event', 'project.platform.update')
->label('audits.resource', 'project.platform/{response.$id}')
->label('sdk', new Method(
namespace: 'project',
group: 'platforms',
name: 'updateWebPlatform',
description: <<<EOT
Update a web platform by its unique ID. Use this endpoint to update the platform's name or hostname.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PLATFORM_WEB,
)
]
))
->param('platformId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform ID.', false, ['dbForPlatform'])
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
->param('hostname', '', new Hostname(), 'Platform web hostname. Max length: 256 chars.', optional: true) // Optional for backwards compatibility
->param('key', '', new Text(256), 'Package name for Android or bundle ID for iOS or macOS. Max length: 256 chars.', optional: true, deprecated: true) // Exists for backwards compatibility
->inject('response')
->inject('queueForEvents')
->inject('dbForPlatform')
->inject('authorization')
->inject('project')
->callback($this->action(...));
}
public function action(
string $platformId,
string $name,
string $hostname,
?string $key, // For backwards compatibility
Response $response,
QueueEvent $queueForEvents,
Database $dbForPlatform,
Authorization $authorization,
Document $project,
) {
$key = $key ?? ''; // App platform attribute, backwards compatibility
// Backwards compatibility
// Used to have: type, name, key, hostname
if (!empty($key)) {
// Validate deprecated app id (key)
$keyValidator = new Text(256);
if (!$keyValidator->isValid($key)) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Param "key" is invalid: ' . $keyValidator->getDescription());
}
}
// One day, ideally, we ensure hostname is not empty
// But for backwards compatibility backend must threat it as optional for now
$platform = $authorization->skip(fn () => $dbForPlatform->getDocument('platforms', $platformId));
if ($platform->isEmpty() || $platform->getAttribute('projectInternalId', '') !== $project->getSequence()) {
throw new Exception(Exception::PLATFORM_NOT_FOUND);
}
// Wrapped in if, for backwards compatibility
if (!empty($hostname)) {
$supportedTypes = [
Platform::TYPE_WEB,
// Backwards compatibility
'flutter-web',
'unity',
'flutter-macos',
'flutter-ios',
'react-native-ios',
'apple-ios',
'apple-macos',
'apple-watchos',
'apple-tvos',
'flutter-android',
'react-native-android',
'flutter-windows',
'flutter-linux',
];
if (!in_array($platform->getAttribute('type', ''), $supportedTypes)) {
throw new Exception(Exception::PLATFORM_METHOD_UNSUPPORTED);
}
}
$updates = new Document([
'name' => $name,
]);
// Wrapped in if, for backwards compatibility
if (!empty($hostname)) {
$updates->setAttribute('hostname', $hostname);
}
// Backwards compatibility
if (!empty($key)) {
$updates->setAttribute('key', $key);
}
try {
$platform = $authorization->skip(fn () => $dbForPlatform->updateDocument('platforms', $platform->getId(), $updates));
} catch (Duplicate) {
throw new Exception(Exception::PLATFORM_ALREADY_EXISTS);
}
$authorization->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
$queueForEvents->setParam('platformId', $platform->getId());
$response->dynamic($platform, Response::MODEL_PLATFORM_WEB);
}
}
@@ -0,0 +1,105 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\Platforms\Windows;
use Appwrite\Event\Event as QueueEvent;
use Appwrite\Extend\Exception;
use Appwrite\Network\Platform;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\Authorization;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Text;
class Create extends Action
{
use HTTP;
public static function getName()
{
return 'createProjectWindowsPlatform';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/project/platforms/windows')
->desc('Create project Windows platform')
->groups(['api', 'project'])
->label('scope', 'project.write')
->label('event', 'platforms.[platformId].create')
->label('audits.event', 'project.platform.create')
->label('audits.resource', 'project.platform/{response.$id}')
->label('sdk', new Method(
namespace: 'project',
group: 'platforms',
name: 'createWindowsPlatform',
description: <<<EOT
Create a new Windows platform for your project. Use this endpoint to register a new Windows platform where your users will run your application which will interact with the Appwrite API.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
model: Response::MODEL_PLATFORM_WINDOWS,
)
],
))
->param('platformId', '', fn (Database $dbForPlatform) => new CustomId(false, $dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForPlatform'])
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
->param('packageIdentifierName', '', new Text(256), 'Windows package identifier name. Max length: 256 chars.')
->inject('response')
->inject('queueForEvents')
->inject('project')
->inject('dbForPlatform')
->inject('authorization')
->callback($this->action(...));
}
public function action(
string $platformId,
string $name,
string $packageIdentifierName,
Response $response,
QueueEvent $queueForEvents,
Document $project,
Database $dbForPlatform,
Authorization $authorization,
) {
$platformId = ($platformId == 'unique()') ? ID::unique() : $platformId;
$platform = new Document([
'$id' => $platformId,
'$permissions' => [],
'projectInternalId' => $project->getSequence(),
'projectId' => $project->getId(),
'type' => Platform::TYPE_WINDOWS,
'name' => $name,
'key' => $packageIdentifierName,
'hostname' => '',
]);
try {
$platform = $authorization->skip(fn () => $dbForPlatform->createDocument('platforms', $platform));
} catch (DuplicateException) {
throw new Exception(Exception::PLATFORM_ALREADY_EXISTS);
}
$authorization->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
$queueForEvents->setParam('platformId', $platform->getId());
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($platform, Response::MODEL_PLATFORM_WINDOWS);
}
}
@@ -0,0 +1,103 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\Platforms\Windows;
use Appwrite\Event\Event as QueueEvent;
use Appwrite\Extend\Exception;
use Appwrite\Network\Platform;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Text;
class Update extends Action
{
use HTTP;
public static function getName()
{
return 'updateProjectWindowsPlatform';
}
public function __construct()
{
$this->setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT)
->setHttpPath('/v1/project/platforms/windows/:platformId')
->desc('Update project Windows platform')
->groups(['api', 'project'])
->label('scope', 'project.write')
->label('event', 'platforms.[platformId].update')
->label('audits.event', 'project.platform.update')
->label('audits.resource', 'project.platform/{response.$id}')
->label('sdk', new Method(
namespace: 'project',
group: 'platforms',
name: 'updateWindowsPlatform',
description: <<<EOT
Update a Windows platform by its unique ID. Use this endpoint to update the platform's name or package identifier name.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PLATFORM_WINDOWS,
)
]
))
->param('platformId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform ID.', false, ['dbForPlatform'])
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
->param('packageIdentifierName', '', new Text(256), 'Windows package identifier name. Max length: 256 chars.')
->inject('response')
->inject('queueForEvents')
->inject('dbForPlatform')
->inject('authorization')
->inject('project')
->callback($this->action(...));
}
public function action(
string $platformId,
string $name,
string $packageIdentifierName,
Response $response,
QueueEvent $queueForEvents,
Database $dbForPlatform,
Authorization $authorization,
Document $project,
) {
$platform = $authorization->skip(fn () => $dbForPlatform->getDocument('platforms', $platformId));
if ($platform->isEmpty() || $platform->getAttribute('projectInternalId', '') !== $project->getSequence()) {
throw new Exception(Exception::PLATFORM_NOT_FOUND);
}
if ($platform->getAttribute('type', '') !== Platform::TYPE_WINDOWS) {
throw new Exception(Exception::PLATFORM_METHOD_UNSUPPORTED);
}
$updates = new Document([
'name' => $name,
'key' => $packageIdentifierName,
]);
try {
$platform = $authorization->skip(fn () => $dbForPlatform->updateDocument('platforms', $platform->getId(), $updates));
} catch (Duplicate) {
throw new Exception(Exception::PLATFORM_ALREADY_EXISTS);
}
$authorization->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
$queueForEvents->setParam('platformId', $platform->getId());
$response->dynamic($platform, Response::MODEL_PLATFORM_WINDOWS);
}
}
@@ -0,0 +1,125 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\Platforms;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\Queries\Platforms;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Order as OrderException;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
class XList extends Action
{
use HTTP;
public static function getName()
{
return 'listProjectPlatforms';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/project/platforms')
->httpAlias('/v1/projects/:projectId/platforms')
->desc('List project platforms')
->groups(['api', 'project'])
->label('scope', 'project.read')
->label('sdk', new Method(
namespace: 'project',
group: 'platforms',
name: 'listPlatforms',
description: <<<EOT
Get a list of all platforms in the project. This endpoint returns an array of all platforms and their configurations.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PLATFORM_LIST,
)
]
))
->param('queries', [], new Platforms(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Platforms::ALLOWED_ATTRIBUTES), true)
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('project')
->inject('response')
->inject('dbForPlatform')
->inject('authorization')
->callback($this->action(...));
}
/**
* @param array<string> $queries
*/
public function action(
array $queries,
bool $includeTotal,
Document $project,
Response $response,
Database $dbForPlatform,
Authorization $authorization,
) {
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
foreach ($queries as $query) {
if (\in_array($query->getAttribute(), ['bundleIdentifier', 'applicationId', 'packageIdentifierName', 'packageName'])) {
$query->setAttribute('key');
}
}
$queries[] = Query::equal('projectInternalId', [$project->getSequence()]);
$cursor = Query::getCursorQueries($queries, false);
$cursor = \reset($cursor);
if ($cursor !== false) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
}
$platformId = $cursor->getValue();
$cursorDocument = $authorization->skip(fn () => $dbForPlatform->findOne('platforms', [
Query::equal('$id', [$platformId]),
Query::equal('projectInternalId', [$project->getSequence()]),
]));
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Platform '{$platformId}' for the 'cursor' value not found.");
}
$cursor->setValue($cursorDocument);
}
$filterQueries = Query::groupByType($queries)['filters'];
try {
$platforms = $authorization->skip(fn () => $dbForPlatform->find('platforms', $queries));
$total = $includeTotal ? $authorization->skip(fn () => $dbForPlatform->count('platforms', $filterQueries, APP_LIMIT_COUNT)) : 0;
} catch (OrderException $e) {
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
}
$response->dynamic(new Document([
'platforms' => $platforms,
'total' => $total,
]), Response::MODEL_PLATFORM_LIST);
}
}
@@ -4,7 +4,6 @@ namespace Appwrite\Platform\Modules\Project\Http\Project\Variables;
use Appwrite\Event\Event as QueueEvent;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
@@ -19,7 +18,7 @@ use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
class Create extends Base
class Create extends Action
{
use HTTP;
@@ -4,7 +4,6 @@ namespace Appwrite\Platform\Modules\Project\Http\Project\Variables;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
@@ -16,7 +15,7 @@ use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Delete extends Base
class Delete extends Action
{
use HTTP;
@@ -3,7 +3,6 @@
namespace Appwrite\Platform\Modules\Project\Http\Project\Variables;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
@@ -13,7 +12,7 @@ use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Get extends Base
class Get extends Action
{
use HTTP;
@@ -4,7 +4,6 @@ namespace Appwrite\Platform\Modules\Project\Http\Project\Variables;
use Appwrite\Event\Event as QueueEvent;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
@@ -19,7 +18,7 @@ use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
class Update extends Base
class Update extends Action
{
use HTTP;
@@ -3,7 +3,6 @@
namespace Appwrite\Platform\Modules\Project\Http\Project\Variables;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
@@ -19,7 +18,7 @@ use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
class XList extends Base
class XList extends Action
{
use HTTP;
@@ -3,6 +3,20 @@
namespace Appwrite\Platform\Modules\Project\Services;
use Appwrite\Platform\Modules\Project\Http\Init;
use Appwrite\Platform\Modules\Project\Http\Project\Labels\Update as UpdateProjectLabels;
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Android\Create as CreateAndroidPlatform;
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Android\Update as UpdateAndroidPlatform;
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Apple\Create as CreateApplePlatform;
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Apple\Update as UpdateApplePlatform;
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Delete as DeletePlatform;
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Get as GetPlatform;
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Linux\Create as CreateLinuxPlatform;
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Linux\Update as UpdateLinuxPlatform;
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Web\Create as CreateWebPlatform;
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Web\Update as UpdateWebPlatform;
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Windows\Create as CreateWindowsPlatform;
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Windows\Update as UpdateWindowsPlatform;
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\XList as ListPlatforms;
use Appwrite\Platform\Modules\Project\Http\Project\Variables\Create as CreateVariable;
use Appwrite\Platform\Modules\Project\Http\Project\Variables\Delete as DeleteVariable;
use Appwrite\Platform\Modules\Project\Http\Project\Variables\Get as GetVariable;
@@ -20,10 +34,28 @@ class Http extends Service
$this->addAction(Init::getName(), new Init());
// Project
$this->addAction(UpdateProjectLabels::getName(), new UpdateProjectLabels());
// Variables
$this->addAction(CreateVariable::getName(), new CreateVariable());
$this->addAction(ListVariables::getName(), new ListVariables());
$this->addAction(GetVariable::getName(), new GetVariable());
$this->addAction(DeleteVariable::getName(), new DeleteVariable());
$this->addAction(UpdateVariable::getName(), new UpdateVariable());
// Platforms
$this->addAction(DeletePlatform::getName(), new DeletePlatform());
$this->addAction(UpdateWebPlatform::getName(), new UpdateWebPlatform());
$this->addAction(UpdateApplePlatform::getName(), new UpdateApplePlatform());
$this->addAction(UpdateAndroidPlatform::getName(), new UpdateAndroidPlatform());
$this->addAction(UpdateWindowsPlatform::getName(), new UpdateWindowsPlatform());
$this->addAction(UpdateLinuxPlatform::getName(), new UpdateLinuxPlatform());
$this->addAction(CreateWebPlatform::getName(), new CreateWebPlatform());
$this->addAction(CreateApplePlatform::getName(), new CreateApplePlatform());
$this->addAction(CreateAndroidPlatform::getName(), new CreateAndroidPlatform());
$this->addAction(CreateWindowsPlatform::getName(), new CreateWindowsPlatform());
$this->addAction(CreateLinuxPlatform::getName(), new CreateLinuxPlatform());
$this->addAction(GetPlatform::getName(), new GetPlatform());
$this->addAction(ListPlatforms::getName(), new ListPlatforms());
}
}
@@ -8,7 +8,6 @@ use Appwrite\Platform\Modules\Projects\Http\DevKeys\Get as GetDevKey;
use Appwrite\Platform\Modules\Projects\Http\DevKeys\Update as UpdateDevKey;
use Appwrite\Platform\Modules\Projects\Http\DevKeys\XList as ListDevKeys;
use Appwrite\Platform\Modules\Projects\Http\Projects\Create as CreateProject;
use Appwrite\Platform\Modules\Projects\Http\Projects\Labels\Update as UpdateProjectLabels;
use Appwrite\Platform\Modules\Projects\Http\Projects\Team\Update as UpdateProjectTeam;
use Appwrite\Platform\Modules\Projects\Http\Projects\Update as UpdateProject;
use Appwrite\Platform\Modules\Projects\Http\Projects\XList as ListProjects;
@@ -31,7 +30,6 @@ class Http extends Service
$this->addAction(CreateProject::getName(), new CreateProject());
$this->addAction(UpdateProject::getName(), new UpdateProject());
$this->addAction(ListProjects::getName(), new ListProjects());
$this->addAction(UpdateProjectLabels::getName(), new UpdateProjectLabels());
$this->addAction(UpdateProjectTeam::getName(), new UpdateProjectTeam());
$this->addAction(CreateSchedule::getName(), new CreateSchedule());
@@ -74,10 +74,12 @@ class Update extends Action
->inject('queueForEvents')
->inject('store')
->inject('proofForToken')
->inject('domainVerification')
->inject('cookieDomain')
->callback($this->action(...));
}
public function action(string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Authorization $authorization, $project, Reader $geodb, Event $queueForEvents, Store $store, Token $proofForToken)
public function action(string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Authorization $authorization, $project, Reader $geodb, Event $queueForEvents, Store $store, Token $proofForToken, bool $domainVerification, ?string $cookieDomain)
{
$protocol = $request->getProtocol();
@@ -162,7 +164,7 @@ class Update extends Action
->setProperty('secret', $secret)
->encode();
if (!Config::getParam('domainVerification')) {
if (!$domainVerification) {
$response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded]));
}
@@ -172,7 +174,7 @@ class Update extends Action
value: $encoded,
expire: (new \DateTime($expire))->getTimestamp(),
path: '/',
domain: Config::getParam('cookieDomain'),
domain: $cookieDomain,
secure: ('https' === $protocol),
httponly: true
)
@@ -181,7 +183,7 @@ class Update extends Action
value: $encoded,
expire: (new \DateTime($expire))->getTimestamp(),
path: '/',
domain: Config::getParam('cookieDomain'),
domain: $cookieDomain,
secure: ('https' === $protocol),
httponly: true,
sameSite: Config::getParam('cookieSamesite')
@@ -5,7 +5,6 @@ namespace Appwrite\Platform\Modules\Webhooks\Http\Webhooks;
use Appwrite\Event\Event as QueueEvent;
use Appwrite\Event\Validator\Event;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
@@ -25,7 +24,7 @@ use Utopia\Validator\Multiple;
use Utopia\Validator\Text;
use Utopia\Validator\URL;
class Create extends Base
class Create extends Action
{
use HTTP;
@@ -4,7 +4,6 @@ namespace Appwrite\Platform\Modules\Webhooks\Http\Webhooks;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
@@ -18,7 +17,7 @@ use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Delete extends Base
class Delete extends Action
{
use HTTP;
@@ -3,7 +3,6 @@
namespace Appwrite\Platform\Modules\Webhooks\Http\Webhooks;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
@@ -16,7 +15,7 @@ use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Get extends Base
class Get extends Action
{
use HTTP;
@@ -4,7 +4,6 @@ namespace Appwrite\Platform\Modules\Webhooks\Http\Webhooks\Signature;
use Appwrite\Event\Event as QueueEvent;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
@@ -17,7 +16,7 @@ use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Update extends Base
class Update extends Action
{
use HTTP;
@@ -5,7 +5,6 @@ namespace Appwrite\Platform\Modules\Webhooks\Http\Webhooks;
use Appwrite\Event\Event as QueueEvent;
use Appwrite\Event\Validator\Event;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
@@ -24,7 +23,7 @@ use Utopia\Validator\Multiple;
use Utopia\Validator\Text;
use Utopia\Validator\URL;
class Update extends Base
class Update extends Action
{
use HTTP;
@@ -3,7 +3,6 @@
namespace Appwrite\Platform\Modules\Webhooks\Http\Webhooks;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
@@ -20,7 +19,7 @@ use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
class XList extends Base
class XList extends Action
{
use HTTP;
+3 -4
View File
@@ -9,7 +9,6 @@ use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception;
use Utopia\Database\Validator\Authorization;
use Utopia\Http\Http;
use Utopia\Platform\Action;
use Utopia\Registry\Registry;
use Utopia\Validator\Text;
@@ -32,6 +31,7 @@ class Migrate extends Action
->inject('getProjectDB')
->inject('register')
->inject('authorization')
->inject('console')
->callback($this->action(...));
}
@@ -48,7 +48,8 @@ class Migrate extends Action
Database $dbForPlatform,
callable $getProjectDB,
Registry $register,
Authorization $authorization
Authorization $authorization,
Document $console
): void {
if (!\array_key_exists($version, Migration::$versions)) {
@@ -125,8 +126,6 @@ class Migrate extends Action
Console::log('Migrated ' . ++$count . '/' . $total . ' projects...');
});
$console = (new Http('UTC'))->getResource('console');
try {
$migration
->setProject($console, $getProjectDB($console), $dbForPlatform, $authorization, $getProjectDB);
+5 -19
View File
@@ -639,29 +639,15 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
} catch (\Throwable) {
}
// Checkout dev branch (or create if it doesn't exist)
// Create or checkout dev branch from the base branch
// This ensures dev always starts from the latest base branch,
// avoiding history divergence caused by squash merges.
try {
$repo->execute('checkout', '-f', $gitBranch);
$repo->execute('checkout', '-B', $gitBranch, $repoBranch);
} catch (\Throwable) {
$repo->execute('checkout', '-b', $gitBranch);
}
// Fetch dev branch, or push to create it on remote
try {
$repo->execute('fetch', 'origin', $gitBranch, '--quiet', '--no-tags', '--depth', '1');
} catch (\Throwable) {
try {
$repo->execute('push', '-u', 'origin', $gitBranch, '--quiet');
} catch (\Throwable) {
}
}
// Sync with remote dev branch
try {
$repo->execute('reset', '--hard', "origin/{$gitBranch}");
} catch (\Throwable) {
}
// Backup .github before cleaning working tree
$githubDir = $target . '/.github';
$githubBackup = \sys_get_temp_dir() . '/.github-backup-' . \getmypid();
@@ -699,7 +685,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
return true;
}
$repo->execute('push', '-u', 'origin', $gitBranch, '--quiet');
$repo->execute('push', '--force-with-lease', '-u', 'origin', $gitBranch, '--quiet');
} catch (\Throwable $e) {
Console::warning(" Git push failed: " . $e->getMessage());
return false;
@@ -61,7 +61,7 @@ class ScheduleFunctions extends ScheduleBase
$nextDate = $cron->getNextRunDate();
$next = DateTime::format($nextDate);
$currentTick = $next <= $timeFrame;
$currentTick = $next < $timeFrame;
if (!$currentTick) {
continue;
@@ -88,7 +88,7 @@ class ScheduleFunctions extends ScheduleBase
$scheduleKey = $delayConfig['key'];
// Ensure schedule was not deleted
if (!\array_key_exists($scheduleKey, $this->schedules)) {
continue;
return;
}
$schedule = $this->schedules[$scheduleKey];
+22 -7
View File
@@ -2,6 +2,7 @@
namespace Appwrite\Platform\Tasks;
use Appwrite\Network\Validator\Redirect;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Specification\Format\OpenAPI3;
@@ -18,6 +19,9 @@ use Utopia\Config\Config;
use Utopia\Console;
use Utopia\Database\Adapter\MySQL;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\DI\Container;
use Utopia\Http\Adapter\FPM\Server as FPMServer;
use Utopia\Http\Http;
use Utopia\Http\Request as UtopiaRequest;
use Utopia\Http\Response as UtopiaResponse;
@@ -336,11 +340,17 @@ class Specs extends Action
$mocks = ($mode === 'mocks');
// Mock dependencies
Http::setResource('request', fn () => $this->getRequest());
Http::setResource('response', fn () => $response);
Http::setResource('dbForPlatform', fn () => new Database(new MySQL(''), new Cache(new None())));
Http::setResource('dbForProject', fn () => new Database(new MySQL(''), new Cache(new None())));
// Mock dependencies needed by param validator injections in route definitions
$specsContainer = new Container();
$specsContainer->set('request', fn () => $this->getRequest());
$specsContainer->set('response', fn () => $response);
$specsContainer->set('dbForPlatform', fn () => new Database(new MySQL(''), new Cache(new None())));
$specsContainer->set('dbForProject', fn () => new Database(new MySQL(''), new Cache(new None())));
$specsContainer->set('redirectValidator', fn () => new Redirect([], []));
$specsContainer->set('project', fn () => new Document([]));
$specsContainer->set('passwordsDictionary', fn () => []);
$specsContainer->set('localeCodes', fn () => \array_map(fn ($locale) => $locale['code'], Config::getParam('locale-codes', [])));
$specsContainer->set('plan', fn () => []);
$platforms = static::getPlatforms();
$authCounts = $this->getAuthCounts();
@@ -438,7 +448,7 @@ class Specs extends Action
}
$arguments = [
new Http('UTC'),
new Http(new FPMServer($specsContainer), 'UTC'),
$services,
$routes,
$models,
@@ -472,7 +482,12 @@ class Specs extends Action
? $specsDir . '/' . $format . '-mocks-' . $platform . '.json'
: $specsDir . '/' . $format . '-' . $version . '-' . $platform . '.json';
$parsedSpecs = $specs->parse();
try {
$parsedSpecs = $specs->parse();
} catch (\RuntimeException $e) {
throw new \RuntimeException("Spec generation failed for {$platform} ({$format}): " . $e->getMessage(), 0, $e);
}
$encodedSpecs = \json_encode($parsedSpecs, JSON_PRETTY_PRINT);
unset($parsedSpecs);
+12 -1
View File
@@ -2,10 +2,14 @@
namespace Appwrite\Promises;
use Swoole\Coroutine;
use Swoole\Coroutine\Channel;
use Utopia\DI\Container;
class Swoole extends Promise
{
private const REQUEST_CONTAINER_CONTEXT_KEY = '__utopia_http_request_container';
public function __construct(?callable $executor = null)
{
parent::__construct($executor);
@@ -16,7 +20,14 @@ class Swoole extends Promise
callable $resolve,
callable $reject
): void {
\go(function () use ($executor, $resolve, $reject) {
$parentContainer = (Coroutine::getCid() !== -1)
? (Coroutine::getContext()[self::REQUEST_CONTAINER_CONTEXT_KEY] ?? null)
: null;
\go(function () use ($executor, $resolve, $reject, $parentContainer) {
if ($parentContainer !== null) {
Coroutine::getContext()[self::REQUEST_CONTAINER_CONTEXT_KEY] = new Container($parentContainer);
}
try {
$executor($resolve, $reject);
} catch (\Throwable $exception) {
@@ -769,6 +769,9 @@ abstract class Format
protected function getNestedModels(Model $model, array &$usedModels): void
{
foreach ($model->getRules() as $rule) {
if (($rule['hidden'] ?? false) === true) {
continue;
}
if (!in_array($model->getType(), $usedModels)) {
continue;
}
@@ -278,6 +278,18 @@ class OpenAPI3 extends Format
}
}
if (\is_string($model)) {
throw new \RuntimeException("Unresolved response model '{$model}' for method '{$sdk->getNamespace()}.{$sdk->getMethodName()}'. Ensure the model is registered.");
}
if (\is_array($model)) {
foreach ($model as $m) {
if (\is_string($m)) {
throw new \RuntimeException("Unresolved response model '{$m}' for method '{$sdk->getNamespace()}.{$sdk->getMethodName()}'. Ensure the model is registered.");
}
}
}
if (!(\is_array($model)) && $model->isNone()) {
$temp['responses'][(string)$response->getCode() ?? '500'] = [
'description' => in_array($produces, [
@@ -821,6 +833,10 @@ class OpenAPI3 extends Format
}
foreach ($model->getRules() as $name => $rule) {
if (($rule['hidden'] ?? false) === true) {
continue;
}
$type = '';
$format = null;
$items = null;
@@ -285,6 +285,18 @@ class Swagger2 extends Format
}
}
if (\is_string($model)) {
throw new \RuntimeException("Unresolved response model '{$model}' for method '{$sdk->getNamespace()}.{$sdk->getMethodName()}'. Ensure the model is registered.");
}
if (\is_array($model)) {
foreach ($model as $m) {
if (\is_string($m)) {
throw new \RuntimeException("Unresolved response model '{$m}' for method '{$sdk->getNamespace()}.{$sdk->getMethodName()}'. Ensure the model is registered.");
}
}
}
if (!(\is_array($model)) && $model->isNone()) {
$temp['responses'][(string)$response->getCode() ?? '500'] = [
'description' => in_array($produces, [
@@ -801,6 +813,10 @@ class Swagger2 extends Format
}
foreach ($model->getRules() as $name => $rule) {
if (($rule['hidden'] ?? false) === true) {
continue;
}
$type = '';
$format = null;
$items = null;
@@ -0,0 +1,25 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Queries;
class Platforms extends Base
{
public const ALLOWED_ATTRIBUTES = [
'type',
'name',
'hostname',
'bundleIdentifier',
'applicationId',
'packageIdentifierName',
'packageName',
];
/**
* Expression constructor
*
*/
public function __construct()
{
parent::__construct('platforms', self::ALLOWED_ATTRIBUTES);
}
}
@@ -11,6 +11,63 @@ class V21 extends Filter
public function parse(array $content, string $model): array
{
switch ($model) {
// Web is special case compared to others, because it holds backwards compatibility logic
case 'project.createWebPlatform':
$content = $this->fillPlatformId($content);
$content = $this->removePlatformStore($content);
// Keep 'key' for backwards compatibility
break;
case 'project.updateWebPlatform':
$content = $this->removePlatformStore($content);
// Keep 'key' for backwards compatibility
break;
case 'project.createApplePlatform':
$content = $this->fillPlatformId($content);
$content = $this->removePlatformStore($content);
$content = $this->replacePlatformKey($content, 'bundleIdentifier');
unset($content['hostname']); // Hostname unsupported
break;
case 'project.updateApplePlatform':
$content = $this->removePlatformStore($content);
$content = $this->replacePlatformKey($content, 'bundleIdentifier');
unset($content['hostname']); // Hostname unsupported
break;
case 'project.createAndroidPlatform':
$content = $this->fillPlatformId($content);
$content = $this->removePlatformStore($content);
$content = $this->replacePlatformKey($content, 'applicationId');
unset($content['hostname']); // Hostname unsupported
break;
case 'project.updateAndroidPlatform':
$content = $this->removePlatformStore($content);
$content = $this->replacePlatformKey($content, 'applicationId');
unset($content['hostname']); // Hostname unsupported
break;
case 'project.createWindowsPlatform':
$content = $this->fillPlatformId($content);
$content = $this->removePlatformStore($content);
$content = $this->replacePlatformKey($content, 'packageIdentifierName');
unset($content['hostname']); // Hostname unsupported
break;
case 'project.updateWindowsPlatform':
$content = $this->removePlatformStore($content);
$content = $this->replacePlatformKey($content, 'packageIdentifierName');
unset($content['hostname']); // Hostname unsupported
break;
case 'project.createLinuxPlatform':
$content = $this->fillPlatformId($content);
$content = $this->removePlatformStore($content);
$content = $this->replacePlatformKey($content, 'packageName');
unset($content['hostname']); // Hostname unsupported
break;
case 'project.updateLinuxPlatform':
$content = $this->removePlatformStore($content);
$content = $this->replacePlatformKey($content, 'packageName');
unset($content['hostname']); // Hostname unsupported
break;
case 'project.listPlatforms':
$content = $this->preservePlatformsQueries($content);
break;
case 'webhooks.create':
$content = $this->fillWebhookid($content);
break;
@@ -79,4 +136,33 @@ class V21 extends Filter
return $content;
}
protected function fillPlatformId(array $content): array
{
$content['platformId'] = $content['platformId'] ?? 'unique()';
return $content;
}
protected function replacePlatformKey(array $content, string $newKey): array
{
$content[$newKey] = $content[$newKey] ?? $content['key'] ?? null;
unset($content['key']);
return $content;
}
protected function removePlatformStore(array $content): array
{
unset($content['store']);
return $content;
}
protected function preservePlatformsQueries(array $content): array
{
$content['queries'] = $content['queries'] ?? [
Query::limit(5000)
];
return $content;
}
}
+12 -2
View File
@@ -256,7 +256,11 @@ class Response extends SwooleResponse
public const MODEL_MOCK_NUMBER = 'mockNumber';
public const MODEL_AUTH_PROVIDER = 'authProvider';
public const MODEL_AUTH_PROVIDER_LIST = 'authProviderList';
public const MODEL_PLATFORM = 'platform';
public const MODEL_PLATFORM_APPLE = 'platformApple';
public const MODEL_PLATFORM_ANDROID = 'platformAndroid';
public const MODEL_PLATFORM_WINDOWS = 'platformWindows';
public const MODEL_PLATFORM_LINUX = 'platformLinux';
public const MODEL_PLATFORM_WEB = 'platformWeb';
public const MODEL_PLATFORM_LIST = 'platformList';
public const MODEL_VARIABLE = 'variable';
public const MODEL_VARIABLE_LIST = 'variableList';
@@ -476,7 +480,13 @@ class Response extends SwooleResponse
foreach ($rule['type'] as $type) {
$condition = false;
foreach ($this->getModel($type)->conditions as $attribute => $val) {
$condition = $item->getAttribute($attribute) === $val;
if (\is_array($val)) {
$condition = \in_array($item->getAttribute($attribute), $val);
} else {
$condition = $item->getAttribute($attribute) === $val;
}
if (!$condition) {
break;
}
@@ -11,6 +11,23 @@ class V21 extends Filter
public function parse(array $content, string $model): array
{
return match ($model) {
// Web is special case, it has backwards compatibility
Response::MODEL_PLATFORM_WEB => $this->parsePlatform($content),
Response::MODEL_PLATFORM_APPLE => $this->parsePlatform($content),
Response::MODEL_PLATFORM_ANDROID => $this->parsePlatform($content),
Response::MODEL_PLATFORM_WINDOWS => $this->parsePlatform($content),
Response::MODEL_PLATFORM_LINUX => $this->parsePlatform($content),
Response::MODEL_PLATFORM_LIST => $this->handleList(
$content,
"platforms",
fn ($item) => $this->parsePlatform($item),
),
Response::MODEL_PROJECT => $this->parseProjectForPlatform($content),
Response::MODEL_PROJECT_LIST => $this->handleList(
$content,
"projects",
fn ($item) => $this->parseProjectForPlatform($item),
),
Response::MODEL_USER => $this->parseUser($content),
Response::MODEL_USER_LIST => $this->handleList(
$content,
@@ -107,4 +124,34 @@ class V21 extends Filter
return $content;
}
protected function parseProjectForPlatform(array $content): array
{
// Parse platforms under project, since it's a subquery
$content['platforms'] = \array_map(fn ($item) => $this->parsePlatform($item), $content['platforms']);
return $content;
}
protected function parsePlatform(array $content): array
{
// Map platform-specific identifier fields back to 'key'
$content['key'] =
($content['bundleIdentifier'] ?? '')
?: ($content['applicationId'] ?? '')
?: ($content['packageIdentifierName'] ?? '')
?: ($content['packageName'] ?? '')
?: ($content['key'] ?? '')
?: '';
unset($content['bundleIdentifier']);
unset($content['applicationId']);
unset($content['packageIdentifierName']);
unset($content['packageName']);
// Restore fields removed in v1.9
$content['store'] = $content['store'] ?? '';
$content['hostname'] = $content['hostname'] ?? '';
return $content;
}
}
@@ -1,100 +0,0 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class Platform extends Model
{
/**
* @var bool
*/
protected bool $public = false;
public function __construct()
{
$this
->addRule('$id', [
'type' => self::TYPE_STRING,
'description' => 'Platform ID.',
'default' => '',
'example' => '5e5ea5c16897e',
])
->addRule('$createdAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Platform creation date in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('$updatedAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Platform update date in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('name', [
'type' => self::TYPE_STRING,
'description' => 'Platform name.',
'default' => '',
'example' => 'My Web App',
])
->addRule('type', [
'type' => self::TYPE_ENUM,
'description' => 'Platform type. Possible values are: web, flutter-web, flutter-ios, flutter-android, flutter-linux, flutter-macos, flutter-windows, apple-ios, apple-macos, apple-watchos, apple-tvos, android, unity, react-native-ios, react-native-android.',
'default' => '',
'example' => 'web',
'enum' => ['web', 'flutter-web', 'flutter-ios', 'flutter-android', 'flutter-linux', 'flutter-macos', 'flutter-windows', 'apple-ios', 'apple-macos', 'apple-watchos', 'apple-tvos', 'android', 'unity', 'react-native-ios', 'react-native-android'],
])
->addRule('key', [
'type' => self::TYPE_STRING,
'description' => 'Platform Key. iOS bundle ID or Android package name. Empty string for other platforms.',
'default' => '',
'example' => 'com.company.appname',
])
->addRule('store', [
'type' => self::TYPE_STRING,
'description' => 'App store or Google Play store ID.',
'example' => '',
])
->addRule('hostname', [
'type' => self::TYPE_STRING,
'description' => 'Web app hostname. Empty string for other platforms.',
'default' => '',
'example' => 'app.example.com',
])
->addRule('httpUser', [
'type' => self::TYPE_STRING,
'description' => 'HTTP basic authentication username.',
'default' => '',
'example' => 'username',
])
->addRule('httpPass', [
'type' => self::TYPE_STRING,
'description' => 'HTTP basic authentication password.',
'default' => '',
'example' => 'password',
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Platform';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_PLATFORM;
}
}
@@ -0,0 +1,58 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Network\Platform;
use Appwrite\Utopia\Response;
use Utopia\Database\Document;
class PlatformAndroid extends PlatformBase
{
public function __construct()
{
$this->conditions = [
'type' => Platform::TYPE_ANDROID,
];
parent::__construct();
$this
->addRule('applicationId', [
'type' => self::TYPE_STRING,
'description' => 'Android application ID.',
'default' => '',
'example' => 'com.company.appname',
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Platform Android';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_PLATFORM_ANDROID;
}
public function filter(Document $document): Document
{
// DB level: 'key'
// API level: 'applicationId'
$document->setAttribute('applicationId', $document->getAttribute('key', null));
$document->removeAttribute('key');
return $document;
}
}
@@ -0,0 +1,58 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Network\Platform;
use Appwrite\Utopia\Response;
use Utopia\Database\Document;
class PlatformApple extends PlatformBase
{
public function __construct()
{
$this->conditions = [
'type' => Platform::TYPE_APPLE,
];
parent::__construct();
$this
->addRule('bundleIdentifier', [
'type' => self::TYPE_STRING,
'description' => 'Apple bundle identifier.',
'default' => '',
'example' => 'com.company.appname',
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Platform Apple';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_PLATFORM_APPLE;
}
public function filter(Document $document): Document
{
// DB level: 'key'
// API level: 'bundleIdentifier'
$document->setAttribute('bundleIdentifier', $document->getAttribute('key', null));
$document->removeAttribute('key');
return $document;
}
}
@@ -0,0 +1,57 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Network\Platform;
use Appwrite\Utopia\Response\Model;
abstract class PlatformBase extends Model
{
public function getSupportedTypes(): array
{
return [
Platform::TYPE_WINDOWS,
Platform::TYPE_APPLE,
Platform::TYPE_ANDROID,
Platform::TYPE_LINUX,
Platform::TYPE_WEB,
];
}
public function __construct()
{
$this
->addRule('$id', [
'type' => self::TYPE_STRING,
'description' => 'Platform ID.',
'default' => '',
'example' => '5e5ea5c16897e',
])
->addRule('$createdAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Platform creation date in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('$updatedAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Platform update date in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('name', [
'type' => self::TYPE_STRING,
'description' => 'Platform name.',
'default' => '',
'example' => 'My Web App',
])
->addRule('type', [
'type' => self::TYPE_ENUM,
'description' => 'Platform type. Possible values are: ' . implode(', ', self::getSupportedTypes()) . '.',
'default' => '',
'example' => Platform::TYPE_WEB,
'enum' => self::getSupportedTypes(),
])
;
}
}
@@ -0,0 +1,58 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Network\Platform;
use Appwrite\Utopia\Response;
use Utopia\Database\Document;
class PlatformLinux extends PlatformBase
{
public function __construct()
{
$this->conditions = [
'type' => Platform::TYPE_LINUX,
];
parent::__construct();
$this
->addRule('packageName', [
'type' => self::TYPE_STRING,
'description' => 'Linux package name.',
'default' => '',
'example' => 'com.company.appname',
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Platform Linux';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_PLATFORM_LINUX;
}
public function filter(Document $document): Document
{
// DB level: 'key'
// API level: 'packageName'
$document->setAttribute('packageName', $document->getAttribute('key', null));
$document->removeAttribute('key');
return $document;
}
}
@@ -0,0 +1,53 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class PlatformList extends Model
{
public function __construct()
{
$this
->addRule('total', [
'type' => self::TYPE_INTEGER,
'description' => 'Total number of platforms in the given project.',
'default' => 0,
'example' => 5,
])
->addRule('platforms', [
'type' => [
Response::MODEL_PLATFORM_WEB,
Response::MODEL_PLATFORM_APPLE,
Response::MODEL_PLATFORM_ANDROID,
Response::MODEL_PLATFORM_WINDOWS,
Response::MODEL_PLATFORM_LINUX,
],
'description' => 'List of platforms.',
'default' => [],
'array' => true
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Platforms List';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_PLATFORM_LIST;
}
}
@@ -0,0 +1,71 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Network\Platform;
use Appwrite\Utopia\Response;
class PlatformWeb extends PlatformBase
{
public function __construct()
{
$this->conditions = [
'type' => [
Platform::TYPE_WEB,
// Backwards compatibility
'flutter-web',
'unity',
'flutter-macos',
'flutter-ios',
'react-native-ios',
'apple-ios',
'apple-macos',
'apple-watchos',
'apple-tvos',
'flutter-android',
'react-native-android',
'flutter-windows',
'flutter-linux',
],
];
parent::__construct();
$this
->addRule('hostname', [
'type' => self::TYPE_STRING,
'description' => 'Web app hostname. Empty string for other platforms.',
'default' => '',
'example' => 'app.example.com',
])
// Backwards compatibility
->addRule('key', [
'hidden' => true,
'type' => self::TYPE_STRING,
'description' => 'Deprecated for old versions using alias endpoint to create universal platform.',
'default' => '',
'example' => 'com.company.appname',
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Platform Web';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_PLATFORM_WEB;
}
}
@@ -0,0 +1,58 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Network\Platform;
use Appwrite\Utopia\Response;
use Utopia\Database\Document;
class PlatformWindows extends PlatformBase
{
public function __construct()
{
$this->conditions = [
'type' => Platform::TYPE_WINDOWS,
];
parent::__construct();
$this
->addRule('packageIdentifierName', [
'type' => self::TYPE_STRING,
'description' => 'Windows package identifier name.',
'default' => '',
'example' => 'com.company.appname',
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Platform Windows';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_PLATFORM_WINDOWS;
}
public function filter(Document $document): Document
{
// DB level: 'key'
// API level: 'packageIdentifierName'
$document->setAttribute('packageIdentifierName', $document->getAttribute('key', null));
$document->removeAttribute('key');
return $document;
}
}
@@ -9,11 +9,6 @@ use Utopia\Database\Document;
class Project extends Model
{
/**
* @var bool
*/
protected bool $public = false;
public function __construct()
{
$this
@@ -200,7 +195,13 @@ class Project extends Model
'array' => true,
])
->addRule('platforms', [
'type' => Response::MODEL_PLATFORM,
'type' => [
Response::MODEL_PLATFORM_WEB,
Response::MODEL_PLATFORM_APPLE,
Response::MODEL_PLATFORM_ANDROID,
Response::MODEL_PLATFORM_WINDOWS,
Response::MODEL_PLATFORM_LINUX,
],
'description' => 'List of Platforms.',
'default' => [],
'example' => new \stdClass(),
@@ -7,11 +7,6 @@ use Appwrite\Utopia\Response\Model;
class Webhook extends Model
{
/**
* @var bool
*/
protected bool $public = true;
public function __construct()
{
$this
+2 -2
View File
@@ -1324,8 +1324,8 @@ class UsageTest extends Scope
$this->assertEquals($requestsTotal, $response['body']['requests'][array_key_last($response['body']['requests'])]['value']);
$this->validateDates($response['body']['requests']);
// vectordbTotal should reflect only VectorsDB instances, not relational databases.
$this->assertEquals($vectordbTotal, $response['body']['vectordbDatabasesTotal']);
$this->assertEquals($documentsTotal, $response['body']['vectordbDocumentsTotal']);
$this->assertEquals($vectordbTotal, $response['body']['vectorsdbDatabasesTotal']);
$this->assertEquals($documentsTotal, $response['body']['vectorsdbDocumentsTotal']);
});
$response = $this->client->call(
@@ -4150,4 +4150,178 @@ class AccountCustomClientTest extends Scope
$this->assertEquals(401, $verification3['headers']['status-code']);
}
/**
* Test that a new email/password session is immediately usable even when
* a concurrent request re-populates the user cache between the cache purge
* and session creation.
*
* Regression test for: purging the user cache BEFORE persisting the session
* allows a concurrent request (from a different Swoole worker) to re-cache
* a stale user document that lacks the new session, causing sessionVerify
* to fail with 401 on subsequent requests using the new session.
*/
public function testEmailPasswordSessionNotCorruptedByConcurrentRequests(): void
{
$projectId = $this->getProject()['$id'];
$endpoint = $this->client->getEndpoint();
$email = uniqid('race_', true) . getmypid() . '@localhost.test';
$password = 'password123!';
// Create user
$response = $this->client->call(Client::METHOD_POST, '/account', [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], [
'userId' => ID::unique(),
'email' => $email,
'password' => $password,
'name' => 'Race Test User',
]);
$this->assertEquals(201, $response['headers']['status-code']);
// Login to get session A
$responseA = $this->client->call(Client::METHOD_POST, '/account/sessions/email', [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], [
'email' => $email,
'password' => $password,
]);
$this->assertEquals(201, $responseA['headers']['status-code']);
$sessionA = $responseA['cookies']['a_session_' . $projectId];
// Verify session A works
$verifyA = $this->client->call(Client::METHOD_GET, '/account', [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'cookie' => 'a_session_' . $projectId . '=' . $sessionA,
]);
$this->assertEquals(200, $verifyA['headers']['status-code']);
/**
* Race condition scenario:
* 1. Start login B via curl_multi (non-blocking)
* 2. Drive the transfer for ~150ms so login B reaches purgeCachedDocument
* (findOne ~15ms + Argon2 hash verify ~60ms + middleware overhead)
* 3. THEN add GET requests to curl_multi - these hit different workers and
* re-cache a stale user document (without session B) during the window
* between purgeCachedDocument and createDocument
* 4. After all complete, verify session B is usable
*/
for ($attempt = 0; $attempt < 5; $attempt++) {
$loginCookies = [];
$multi = curl_multi_init();
// Start login B first (alone)
$loginHandle = curl_init("{$endpoint}/account/sessions/email");
curl_setopt_array($loginHandle, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'origin: http://localhost',
'content-type: application/json',
"x-appwrite-project: {$projectId}",
],
CURLOPT_POSTFIELDS => \json_encode([
'email' => $email,
'password' => $password,
]),
CURLOPT_HEADERFUNCTION => function ($curl, $header) use (&$loginCookies) {
if (\stripos($header, 'set-cookie:') === 0) {
$cookiePart = \trim(\substr($header, 11));
$eqPos = \strpos($cookiePart, '=');
if ($eqPos !== false) {
$name = \substr($cookiePart, 0, $eqPos);
$rest = \substr($cookiePart, $eqPos + 1);
$semiPos = \strpos($rest, ';');
$loginCookies[$name] = $semiPos !== false
? \substr($rest, 0, $semiPos)
: $rest;
}
}
return \strlen($header);
},
]);
curl_multi_add_handle($multi, $loginHandle);
// Drive the login transfer forward and wait for the server to start
// processing the login (past hash verification + cache purge).
$deadline = \microtime(true) + 0.15; // 150ms
do {
curl_multi_exec($multi, $active);
curl_multi_select($multi, 0.005);
} while (\microtime(true) < $deadline && $active);
// NOW add GET requests - they arrive after the cache purge
// but before session creation (which is delayed by the usleep or I/O).
$getHandles = [];
for ($i = 0; $i < 10; $i++) {
$gh = curl_init("{$endpoint}/account");
curl_setopt_array($gh, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'origin: http://localhost',
'content-type: application/json',
"x-appwrite-project: {$projectId}",
"cookie: a_session_{$projectId}={$sessionA}",
],
]);
curl_multi_add_handle($multi, $gh);
$getHandles[] = $gh;
}
// Drive all to completion
do {
$status = curl_multi_exec($multi, $active);
if ($active) {
curl_multi_select($multi, 0.05);
}
} while ($active && $status === CURLM_OK);
$loginStatus = curl_getinfo($loginHandle, CURLINFO_HTTP_CODE);
curl_multi_remove_handle($multi, $loginHandle);
curl_close($loginHandle);
foreach ($getHandles as $gh) {
curl_multi_remove_handle($multi, $gh);
curl_close($gh);
}
curl_multi_close($multi);
$this->assertEquals(201, $loginStatus, 'Login for session B should succeed');
$sessionBCookie = $loginCookies["a_session_{$projectId}"] ?? null;
$this->assertNotNull($sessionBCookie, 'Session B cookie should be set');
// THE CRITICAL CHECK: verify session B is usable immediately
$verifyB = $this->client->call(Client::METHOD_GET, '/account', [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'cookie' => "a_session_{$projectId}={$sessionBCookie}",
]);
$this->assertEquals(
200,
$verifyB['headers']['status-code'],
'Session B must be immediately usable after login. '
. 'A 401 here means a stale user cache (without the new session) was served. '
. 'The fix is to create the session document BEFORE purging the user cache.'
);
// Clean up session B for next iteration
$this->client->call(Client::METHOD_DELETE, '/account/sessions/current', [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'cookie' => "a_session_{$projectId}={$sessionBCookie}",
]);
}
}
}
+224
View File
@@ -0,0 +1,224 @@
<?php
namespace Tests\E2E\Services\Project;
trait LabelsBase
{
// Update labels tests
public function testUpdateLabels(): void
{
$response = $this->updateLabels(['frontend', 'backend']);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertIsArray($response['body']['labels']);
$this->assertCount(2, $response['body']['labels']);
$this->assertContains('frontend', $response['body']['labels']);
$this->assertContains('backend', $response['body']['labels']);
// Cleanup
$this->updateLabels([]);
}
public function testUpdateLabelsReplace(): void
{
$response = $this->updateLabels(['alpha', 'beta']);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertCount(2, $response['body']['labels']);
$this->assertContains('alpha', $response['body']['labels']);
$this->assertContains('beta', $response['body']['labels']);
// Replace with new labels
$response = $this->updateLabels(['gamma', 'delta', 'epsilon']);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertCount(3, $response['body']['labels']);
$this->assertContains('gamma', $response['body']['labels']);
$this->assertContains('delta', $response['body']['labels']);
$this->assertContains('epsilon', $response['body']['labels']);
$this->assertNotContains('alpha', $response['body']['labels']);
$this->assertNotContains('beta', $response['body']['labels']);
// Cleanup
$this->updateLabels([]);
}
public function testUpdateLabelsEmpty(): void
{
// Set some labels first
$response = $this->updateLabels(['toRemove']);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertCount(1, $response['body']['labels']);
// Clear all labels
$response = $this->updateLabels([]);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertIsArray($response['body']['labels']);
$this->assertCount(0, $response['body']['labels']);
}
public function testUpdateLabelsDeduplicated(): void
{
$response = $this->updateLabels(['duplicate', 'duplicate', 'unique']);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertCount(2, $response['body']['labels']);
$this->assertContains('duplicate', $response['body']['labels']);
$this->assertContains('unique', $response['body']['labels']);
// Cleanup
$this->updateLabels([]);
}
public function testUpdateLabelsSingleLabel(): void
{
$response = $this->updateLabels(['solo']);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertCount(1, $response['body']['labels']);
$this->assertContains('solo', $response['body']['labels']);
// Cleanup
$this->updateLabels([]);
}
public function testUpdateLabelsWithoutAuthentication(): void
{
$response = $this->updateLabels(['unauthorized'], false);
$this->assertSame(401, $response['headers']['status-code']);
}
public function testUpdateLabelsInvalidLabelTooLong(): void
{
$response = $this->updateLabels([str_repeat('a', 37)]);
$this->assertSame(400, $response['headers']['status-code']);
}
public function testUpdateLabelsInvalidLabelCharacters(): void
{
$response = $this->updateLabels(['invalid-label!']);
$this->assertSame(400, $response['headers']['status-code']);
}
public function testUpdateLabelsAlphanumericOnly(): void
{
$response = $this->updateLabels(['ABC123', 'lowercase', 'UPPERCASE', '0123456789']);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertCount(4, $response['body']['labels']);
// Cleanup
$this->updateLabels([]);
}
public function testUpdateLabelsMaxLength(): void
{
$label = str_repeat('a', 36);
$response = $this->updateLabels([$label]);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertCount(1, $response['body']['labels']);
$this->assertContains($label, $response['body']['labels']);
// Cleanup
$this->updateLabels([]);
}
public function testUpdateLabelsIdempotent(): void
{
$labels = ['stable', 'production'];
$first = $this->updateLabels($labels);
$this->assertSame(200, $first['headers']['status-code']);
$second = $this->updateLabels($labels);
$this->assertSame(200, $second['headers']['status-code']);
$this->assertSame($first['body']['labels'], $second['body']['labels']);
// Cleanup
$this->updateLabels([]);
}
public function testUpdateLabelsDeduplicatedOrder(): void
{
$response = $this->updateLabels(['b', 'a', 'b']);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertCount(2, $response['body']['labels']);
$this->assertSame('b', $response['body']['labels'][0]);
$this->assertSame('a', $response['body']['labels'][1]);
// Cleanup
$this->updateLabels([]);
}
public function testUpdateLabelsInvalidHyphen(): void
{
$response = $this->updateLabels(['my-label']);
$this->assertSame(400, $response['headers']['status-code']);
}
public function testUpdateLabelsInvalidUnderscore(): void
{
$response = $this->updateLabels(['my_label']);
$this->assertSame(400, $response['headers']['status-code']);
}
public function testUpdateLabelsInvalidSpace(): void
{
$response = $this->updateLabels(['my label']);
$this->assertSame(400, $response['headers']['status-code']);
}
public function testUpdateLabelsInvalidEmptyString(): void
{
$response = $this->updateLabels(['']);
$this->assertSame(400, $response['headers']['status-code']);
}
public function testUpdateLabelsResponseModel(): void
{
$response = $this->updateLabels(['test']);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertArrayHasKey('$id', $response['body']);
$this->assertArrayHasKey('name', $response['body']);
$this->assertArrayHasKey('labels', $response['body']);
$this->assertIsArray($response['body']['labels']);
$this->assertContains('test', $response['body']['labels']);
// Cleanup
$this->updateLabels([]);
}
// Helpers
/**
* @param array<string> $labels
*/
protected function updateLabels(array $labels, bool $authenticated = true): mixed
{
$headers = [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
];
if ($authenticated) {
$headers = array_merge($headers, $this->getHeaders());
}
return $this->client->call(\Tests\E2E\Client::METHOD_PUT, '/project/labels', $headers, [
'labels' => $labels,
]);
}
}
@@ -0,0 +1,14 @@
<?php
namespace Tests\E2E\Services\Project;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideConsole;
class LabelsConsoleClientTest extends Scope
{
use LabelsBase;
use ProjectCustom;
use SideConsole;
}
@@ -0,0 +1,14 @@
<?php
namespace Tests\E2E\Services\Project;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideServer;
class LabelsCustomServerTest extends Scope
{
use LabelsBase;
use ProjectCustom;
use SideServer;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,14 @@
<?php
namespace Tests\E2E\Services\Project;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideConsole;
class PlatformsConsoleClientTest extends Scope
{
use PlatformsBase;
use ProjectCustom;
use SideConsole;
}
@@ -0,0 +1,14 @@
<?php
namespace Tests\E2E\Services\Project;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideServer;
class PlatformsCustomServerTest extends Scope
{
use PlatformsBase;
use ProjectCustom;
use SideServer;
}
@@ -151,6 +151,7 @@ trait ProjectsBase
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/platforms', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), [
'type' => 'web',
'name' => 'Web App',
@@ -163,6 +164,7 @@ trait ProjectsBase
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/platforms', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), [
'type' => 'flutter-ios',
'name' => 'Flutter App (iOS)',
@@ -175,6 +177,7 @@ trait ProjectsBase
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/platforms', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), [
'type' => 'flutter-android',
'name' => 'Flutter App (Android)',
@@ -187,6 +190,7 @@ trait ProjectsBase
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/platforms', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), [
'type' => 'flutter-web',
'name' => 'Flutter App (Web)',
@@ -199,6 +203,7 @@ trait ProjectsBase
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/platforms', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), [
'type' => 'apple-ios',
'name' => 'iOS App',
@@ -211,6 +216,7 @@ trait ProjectsBase
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/platforms', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), [
'type' => 'apple-macos',
'name' => 'macOS App',
@@ -223,6 +229,7 @@ trait ProjectsBase
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/platforms', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), [
'type' => 'apple-watchos',
'name' => 'watchOS App',
@@ -235,6 +242,7 @@ trait ProjectsBase
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/platforms', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), [
'type' => 'apple-tvos',
'name' => 'tvOS App',
@@ -3754,6 +3754,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/platforms', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), [
'type' => 'web',
'name' => 'Web App',
@@ -3773,6 +3774,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/platforms', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), [
'type' => 'flutter-ios',
'name' => 'Flutter App (iOS)',
@@ -3781,7 +3783,7 @@ class ProjectsConsoleClientTest extends Scope
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertEquals('flutter-ios', $response['body']['type']);
$this->assertEquals('apple', $response['body']['type']); // Origianlly flutter-ios, but new version renames
$this->assertEquals('Flutter App (iOS)', $response['body']['name']);
$this->assertEquals('com.example.ios', $response['body']['key']);
$this->assertEquals('', $response['body']['store']);
@@ -3792,6 +3794,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/platforms', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), [
'type' => 'flutter-android',
'name' => 'Flutter App (Android)',
@@ -3800,7 +3803,7 @@ class ProjectsConsoleClientTest extends Scope
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertEquals('flutter-android', $response['body']['type']);
$this->assertEquals('android', $response['body']['type']); // Origianlly flutter-android, but new version renames
$this->assertEquals('Flutter App (Android)', $response['body']['name']);
$this->assertEquals('com.example.android', $response['body']['key']);
$this->assertEquals('', $response['body']['store']);
@@ -3811,6 +3814,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/platforms', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), [
'type' => 'flutter-web',
'name' => 'Flutter App (Web)',
@@ -3819,7 +3823,7 @@ class ProjectsConsoleClientTest extends Scope
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertEquals('flutter-web', $response['body']['type']);
$this->assertEquals('web', $response['body']['type']); // Origianlly flutter-web, but new version renames
$this->assertEquals('Flutter App (Web)', $response['body']['name']);
$this->assertEquals('', $response['body']['key']);
$this->assertEquals('', $response['body']['store']);
@@ -3830,6 +3834,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/platforms', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), [
'type' => 'apple-ios',
'name' => 'iOS App',
@@ -3838,7 +3843,7 @@ class ProjectsConsoleClientTest extends Scope
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertEquals('apple-ios', $response['body']['type']);
$this->assertEquals('apple', $response['body']['type']); // Origianlly apple-ios, but new version renames
$this->assertEquals('iOS App', $response['body']['name']);
$this->assertEquals('com.example.ios', $response['body']['key']);
$this->assertEquals('', $response['body']['store']);
@@ -3849,6 +3854,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/platforms', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), [
'type' => 'apple-macos',
'name' => 'macOS App',
@@ -3857,7 +3863,7 @@ class ProjectsConsoleClientTest extends Scope
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertEquals('apple-macos', $response['body']['type']);
$this->assertEquals('apple', $response['body']['type']); // Origianlly apple-macos, but new version renames
$this->assertEquals('macOS App', $response['body']['name']);
$this->assertEquals('com.example.macos', $response['body']['key']);
$this->assertEquals('', $response['body']['store']);
@@ -3868,6 +3874,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/platforms', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), [
'type' => 'apple-watchos',
'name' => 'watchOS App',
@@ -3876,7 +3883,7 @@ class ProjectsConsoleClientTest extends Scope
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertEquals('apple-watchos', $response['body']['type']);
$this->assertEquals('apple', $response['body']['type']); // Origianlly apple-watchos, but new version renames
$this->assertEquals('watchOS App', $response['body']['name']);
$this->assertEquals('com.example.watchos', $response['body']['key']);
$this->assertEquals('', $response['body']['store']);
@@ -3887,6 +3894,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/platforms', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), [
'type' => 'apple-tvos',
'name' => 'tvOS App',
@@ -3895,7 +3903,7 @@ class ProjectsConsoleClientTest extends Scope
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertEquals('apple-tvos', $response['body']['type']);
$this->assertEquals('apple', $response['body']['type']); // Origianlly apple-tvos, but new version renames
$this->assertEquals('tvOS App', $response['body']['name']);
$this->assertEquals('com.example.tvos', $response['body']['key']);
$this->assertEquals('', $response['body']['store']);
@@ -3909,6 +3917,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/platforms', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), [
'type' => 'unknown',
'name' => 'Web App',
@@ -3927,6 +3936,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/platforms', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), []);
$this->assertEquals(200, $response['headers']['status-code']);
@@ -3950,6 +3960,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/platforms/' . $platformWebId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), []);
$this->assertEquals(200, $response['headers']['status-code']);
@@ -3966,12 +3977,13 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/platforms/' . $platformFultteriOSId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), []);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertEquals($platformFultteriOSId, $response['body']['$id']);
$this->assertEquals('flutter-ios', $response['body']['type']);
$this->assertEquals('apple', $response['body']['type']);
$this->assertEquals('Flutter App (iOS)', $response['body']['name']);
$this->assertEquals('com.example.ios', $response['body']['key']);
$this->assertEquals('', $response['body']['store']);
@@ -3982,12 +3994,13 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/platforms/' . $platformFultterAndroidId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), []);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertEquals($platformFultterAndroidId, $response['body']['$id']);
$this->assertEquals('flutter-android', $response['body']['type']);
$this->assertEquals('android', $response['body']['type']);
$this->assertEquals('Flutter App (Android)', $response['body']['name']);
$this->assertEquals('com.example.android', $response['body']['key']);
$this->assertEquals('', $response['body']['store']);
@@ -3998,12 +4011,13 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/platforms/' . $platformFultterWebId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), []);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertEquals($platformFultterWebId, $response['body']['$id']);
$this->assertEquals('flutter-web', $response['body']['type']);
$this->assertEquals('web', $response['body']['type']);
$this->assertEquals('Flutter App (Web)', $response['body']['name']);
$this->assertEquals('', $response['body']['key']);
$this->assertEquals('', $response['body']['store']);
@@ -4014,12 +4028,13 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/platforms/' . $platformAppleIosId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), []);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertEquals($platformAppleIosId, $response['body']['$id']);
$this->assertEquals('apple-ios', $response['body']['type']);
$this->assertEquals('apple', $response['body']['type']);
$this->assertEquals('iOS App', $response['body']['name']);
$this->assertEquals('com.example.ios', $response['body']['key']);
$this->assertEquals('', $response['body']['store']);
@@ -4030,12 +4045,13 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/platforms/' . $platformAppleMacOsId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), []);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertEquals($platformAppleMacOsId, $response['body']['$id']);
$this->assertEquals('apple-macos', $response['body']['type']);
$this->assertEquals('apple', $response['body']['type']);
$this->assertEquals('macOS App', $response['body']['name']);
$this->assertEquals('com.example.macos', $response['body']['key']);
$this->assertEquals('', $response['body']['store']);
@@ -4046,12 +4062,13 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/platforms/' . $platformAppleWatchOsId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), []);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertEquals($platformAppleWatchOsId, $response['body']['$id']);
$this->assertEquals('apple-watchos', $response['body']['type']);
$this->assertEquals('apple', $response['body']['type']);
$this->assertEquals('watchOS App', $response['body']['name']);
$this->assertEquals('com.example.watchos', $response['body']['key']);
$this->assertEquals('', $response['body']['store']);
@@ -4062,12 +4079,13 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/platforms/' . $platformAppleTvOsId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), []);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertEquals($platformAppleTvOsId, $response['body']['$id']);
$this->assertEquals('apple-tvos', $response['body']['type']);
$this->assertEquals('apple', $response['body']['type']);
$this->assertEquals('tvOS App', $response['body']['name']);
$this->assertEquals('com.example.tvos', $response['body']['key']);
$this->assertEquals('', $response['body']['store']);
@@ -4079,6 +4097,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/platforms/error', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), []);
$this->assertEquals(404, $response['headers']['status-code']);
@@ -4094,6 +4113,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PUT, '/projects/' . $id . '/platforms/' . $platformWebId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), [
'name' => 'Web App 2',
'hostname' => 'localhost-new',
@@ -4113,6 +4133,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PUT, '/projects/' . $id . '/platforms/' . $platformFultteriOSId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), [
'name' => 'Flutter App (iOS) 2',
'key' => 'com.example.ios2',
@@ -4121,7 +4142,7 @@ class ProjectsConsoleClientTest extends Scope
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertEquals($platformFultteriOSId, $response['body']['$id']);
$this->assertEquals('flutter-ios', $response['body']['type']);
$this->assertEquals('apple', $response['body']['type']); // Origianlly flutter-ios, but new version renames
$this->assertEquals('Flutter App (iOS) 2', $response['body']['name']);
$this->assertEquals('com.example.ios2', $response['body']['key']);
$this->assertEquals('', $response['body']['store']);
@@ -4132,6 +4153,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PUT, '/projects/' . $id . '/platforms/' . $platformFultterAndroidId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), [
'name' => 'Flutter App (Android) 2',
'key' => 'com.example.android2',
@@ -4140,7 +4162,7 @@ class ProjectsConsoleClientTest extends Scope
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertEquals($platformFultterAndroidId, $response['body']['$id']);
$this->assertEquals('flutter-android', $response['body']['type']);
$this->assertEquals('android', $response['body']['type']); // Origianlly flutter-android, but new version renames
$this->assertEquals('Flutter App (Android) 2', $response['body']['name']);
$this->assertEquals('com.example.android2', $response['body']['key']);
$this->assertEquals('', $response['body']['store']);
@@ -4151,6 +4173,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PUT, '/projects/' . $id . '/platforms/' . $platformFultterWebId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), [
'name' => 'Flutter App (Web) 2',
'hostname' => 'flutter2.appwrite.io',
@@ -4159,7 +4182,7 @@ class ProjectsConsoleClientTest extends Scope
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertEquals($platformFultterWebId, $response['body']['$id']);
$this->assertEquals('flutter-web', $response['body']['type']);
$this->assertEquals('web', $response['body']['type']); // Originally flutter-web, but new version renames
$this->assertEquals('Flutter App (Web) 2', $response['body']['name']);
$this->assertEquals('', $response['body']['key']);
$this->assertEquals('', $response['body']['store']);
@@ -4170,6 +4193,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PUT, '/projects/' . $id . '/platforms/' . $platformAppleIosId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), [
'name' => 'iOS App 2',
'key' => 'com.example.ios2',
@@ -4178,7 +4202,7 @@ class ProjectsConsoleClientTest extends Scope
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertEquals($platformAppleIosId, $response['body']['$id']);
$this->assertEquals('apple-ios', $response['body']['type']);
$this->assertEquals('apple', $response['body']['type']); // Originally apple-ios, but new version renames
$this->assertEquals('iOS App 2', $response['body']['name']);
$this->assertEquals('com.example.ios2', $response['body']['key']);
$this->assertEquals('', $response['body']['store']);
@@ -4189,6 +4213,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PUT, '/projects/' . $id . '/platforms/' . $platformAppleMacOsId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), [
'name' => 'macOS App 2',
'key' => 'com.example.macos2',
@@ -4197,7 +4222,7 @@ class ProjectsConsoleClientTest extends Scope
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertEquals($platformAppleMacOsId, $response['body']['$id']);
$this->assertEquals('apple-macos', $response['body']['type']);
$this->assertEquals('apple', $response['body']['type']); // Originally apple-macos, but new version renames
$this->assertEquals('macOS App 2', $response['body']['name']);
$this->assertEquals('com.example.macos2', $response['body']['key']);
$this->assertEquals('', $response['body']['store']);
@@ -4208,6 +4233,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PUT, '/projects/' . $id . '/platforms/' . $platformAppleWatchOsId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), [
'name' => 'watchOS App 2',
'key' => 'com.example.watchos2',
@@ -4216,7 +4242,7 @@ class ProjectsConsoleClientTest extends Scope
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertEquals($platformAppleWatchOsId, $response['body']['$id']);
$this->assertEquals('apple-watchos', $response['body']['type']);
$this->assertEquals('apple', $response['body']['type']); // Originally apple-watchos, but new version renames
$this->assertEquals('watchOS App 2', $response['body']['name']);
$this->assertEquals('com.example.watchos2', $response['body']['key']);
$this->assertEquals('', $response['body']['store']);
@@ -4227,6 +4253,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PUT, '/projects/' . $id . '/platforms/' . $platformAppleTvOsId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), [
'name' => 'tvOS App 2',
'key' => 'com.example.tvos2',
@@ -4235,7 +4262,7 @@ class ProjectsConsoleClientTest extends Scope
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertEquals($platformAppleTvOsId, $response['body']['$id']);
$this->assertEquals('apple-tvos', $response['body']['type']);
$this->assertEquals('apple', $response['body']['type']); // Originally apple-tvos, but new version renames
$this->assertEquals('tvOS App 2', $response['body']['name']);
$this->assertEquals('com.example.tvos2', $response['body']['key']);
$this->assertEquals('', $response['body']['store']);
@@ -4247,6 +4274,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PUT, '/projects/' . $id . '/platforms/error', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), [
'name' => 'Flutter App (Android) 2',
'key' => 'com.example.android2',
@@ -4265,6 +4293,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/platforms', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), [
'type' => 'web',
'name' => 'Web App',
@@ -4277,6 +4306,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/platforms', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), [
'type' => 'flutter-ios',
'name' => 'Flutter App (iOS)',
@@ -4289,6 +4319,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/platforms', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), [
'type' => 'flutter-android',
'name' => 'Flutter App (Android)',
@@ -4301,6 +4332,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/platforms', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), [
'type' => 'flutter-web',
'name' => 'Flutter App (Web)',
@@ -4313,6 +4345,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/platforms', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), [
'type' => 'apple-ios',
'name' => 'iOS App',
@@ -4325,6 +4358,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/platforms', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), [
'type' => 'apple-macos',
'name' => 'macOS App',
@@ -4337,6 +4371,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/platforms', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), [
'type' => 'apple-watchos',
'name' => 'watchOS App',
@@ -4349,6 +4384,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/platforms', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), [
'type' => 'apple-tvos',
'name' => 'tvOS App',
@@ -4360,6 +4396,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_DELETE, '/projects/' . $id . '/platforms/' . $platformWebId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), []);
$this->assertEquals(204, $response['headers']['status-code']);
@@ -4368,6 +4405,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/platforms/' . $platformWebId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), []);
$this->assertEquals(404, $response['headers']['status-code']);
@@ -4375,6 +4413,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_DELETE, '/projects/' . $id . '/platforms/' . $platformFultteriOSId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), []);
$this->assertEquals(204, $response['headers']['status-code']);
@@ -4383,6 +4422,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/platforms/' . $platformFultteriOSId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), []);
$this->assertEquals(404, $response['headers']['status-code']);
@@ -4390,6 +4430,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_DELETE, '/projects/' . $id . '/platforms/' . $platformFultterAndroidId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), []);
$this->assertEquals(204, $response['headers']['status-code']);
@@ -4398,6 +4439,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/platforms/' . $platformFultterAndroidId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), []);
$this->assertEquals(404, $response['headers']['status-code']);
@@ -4405,6 +4447,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_DELETE, '/projects/' . $id . '/platforms/' . $platformFultterWebId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), []);
$this->assertEquals(204, $response['headers']['status-code']);
@@ -4413,6 +4456,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/platforms/' . $platformFultterWebId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), []);
$this->assertEquals(404, $response['headers']['status-code']);
@@ -4420,6 +4464,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_DELETE, '/projects/' . $id . '/platforms/' . $platformAppleIosId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), []);
$this->assertEquals(204, $response['headers']['status-code']);
@@ -4428,6 +4473,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/platforms/' . $platformAppleIosId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), []);
$this->assertEquals(404, $response['headers']['status-code']);
@@ -4435,6 +4481,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_DELETE, '/projects/' . $id . '/platforms/' . $platformAppleMacOsId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), []);
$this->assertEquals(204, $response['headers']['status-code']);
@@ -4443,6 +4490,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/platforms/' . $platformAppleMacOsId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), []);
$this->assertEquals(404, $response['headers']['status-code']);
@@ -4450,6 +4498,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_DELETE, '/projects/' . $id . '/platforms/' . $platformAppleWatchOsId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), []);
$this->assertEquals(204, $response['headers']['status-code']);
@@ -4458,6 +4507,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/platforms/' . $platformAppleWatchOsId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), []);
$this->assertEquals(404, $response['headers']['status-code']);
@@ -4465,6 +4515,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_DELETE, '/projects/' . $id . '/platforms/' . $platformAppleTvOsId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), []);
$this->assertEquals(204, $response['headers']['status-code']);
@@ -4473,6 +4524,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/platforms/' . $platformAppleTvOsId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.8.0',
], $this->getHeaders()), []);
$this->assertEquals(404, $response['headers']['status-code']);
+1 -1
View File
@@ -9,7 +9,7 @@ class MockPublisher implements Publisher
{
private array $events = [];
public function enqueue(Queue $queue, array $payload): bool
public function enqueue(Queue $queue, array $payload, bool $priority = false): bool
{
if (!isset($this->events[$queue->name])) {
$this->events[$queue->name] = [];