Compare commits

...
453 Commits
Author SHA1 Message Date
ArnabChatterjee20k ede1c3d769 added tests for the sequence filter 2025-07-18 10:55:31 +05:30
ArnabChatterjee20k e97d10061f Merge branch '1.8.x' into dat-571 2025-07-18 10:28:27 +05:30
ArnabChatterjee20k 16c3f2c55a Merge branch '1.8.x' into dat-571 2025-07-18 10:26:01 +05:30
Jake BarnbyandGitHub ebdc6c9016 Merge pull request #10019 from appwrite/lazy-load-relationships
Lazy load relationships
2025-07-18 12:52:01 +12:00
Darshan 944f5ed7c1 update: mirror tests to Tables api. 2025-07-17 18:58:56 +05:30
Darshan 5b8751d270 add: changes to module structure. 2025-07-17 18:23:33 +05:30
Darshan ee317b0b8d Merge branch '1.8.x' into 'lazy-load-relationships'. 2025-07-17 18:15:16 +05:30
ArnabChatterjee20k a2b6ed3513 added response filters for version older than 1.8.x 2025-07-07 14:11:35 +05:30
Jake Barnby b2fa28754d Merge remote-tracking branch 'origin/1.7.x' into 1.8.x
# Conflicts:
#	.github/workflows/benchmark.yml
#	composer.lock
#	src/Appwrite/Event/Database.php
2025-07-04 10:00:45 -04:00
Matej BačoandGitHub 1cf9963bb6 Merge pull request #10104 from appwrite/chore-fix-site-template-test
Fix site template test
2025-07-04 15:17:09 +02:00
Khushboo Verma 2545977d47 Fix site template test 2025-07-04 17:46:21 +05:30
Torsten DittmannandGitHub dbadfa7bf8 Merge pull request #9650 from appwrite/feat-exp-scheme
feat: exp scheme
2025-07-04 14:09:54 +02:00
Chirag Aggarwal 37c7411405 Trigger Build 2025-07-04 16:51:30 +05:30
Chirag Aggarwal bc8b10a69e fix: origin tests 2025-07-04 16:46:14 +05:30
Chirag AggarwalandGitHub df9ef1ace9 Merge branch '1.7.x' into feat-exp-scheme 2025-07-04 15:22:44 +05:30
Matej BačoandGitHub 32903d9a81 Merge pull request #9999 from appwrite/chore-update-env-var-used-for-domain
Use _APP_CONSOLE_DOMAIN, if not found, then use _APP_DOMAIN
2025-07-04 09:41:42 +02:00
Khushboo VermaandGitHub 131cdefac5 Merge branch '1.7.x' into chore-update-env-var-used-for-domain 2025-07-04 10:40:38 +05:30
Jake BarnbyandGitHub d205ac4579 Merge pull request #10099 from appwrite/feat-redis-publisher-scheduler
Add redis publisher to schedule base if available
2025-07-03 18:49:25 +00:00
Jake Barnby 5bb998b02d Add API + CLI stubs 2025-07-03 14:24:40 -04:00
Jake Barnby 7d37f5b066 Lint 2025-07-03 14:18:19 -04:00
Jake Barnby 2a73dddbdb Fix health 2025-07-03 14:09:08 -04:00
Jake Barnby f224c09c10 Use correct publisher in schedule tasks 2025-07-03 14:08:54 -04:00
Jake Barnby 6725b31b65 Add redis publisher to schedule base if available 2025-07-03 13:55:13 -04:00
Chirag AggarwalandGitHub e721a9f11c Merge branch '1.7.x' into feat-exp-scheme 2025-07-03 22:40:46 +05:30
Chirag Aggarwal 72f08e281b chore: make check consistent 2025-07-03 22:39:12 +05:30
Chirag Aggarwal 431a26e151 chore: removing loggin 2025-07-03 22:37:31 +05:30
Chirag Aggarwal 172ba84ec8 chore: add logging 2025-07-03 22:25:46 +05:30
Torsten DittmannandGitHub 7c0216c592 Merge pull request #10098 from appwrite/fix/jwt-auth-db-admin-mode
fix: JWT authentication database selection for admin mode
2025-07-03 18:29:54 +02:00
Chirag Aggarwal b6f11144b8 chore: popular schemes 2025-07-03 21:39:23 +05:30
Ariel WeinbergerandGitHub 199b9f7e6d fix nitpick 2025-07-03 11:06:36 -05:00
Ariel WeinbergerandGitHub d9486f38c4 fix: jwt auth database selection for admin mode
Use dbForPlatform for JWT user lookup in admin mode
2025-07-03 10:54:51 -05:00
Chirag Aggarwal 4747700eae chore: add logging 2025-07-03 21:17:43 +05:30
Chirag Aggarwal bb9a271fa8 chore: add scheme validation 2025-07-03 16:39:41 +05:30
Chirag AggarwalandGitHub ef2dfb6cca Merge branch '1.7.x' into feat-exp-scheme 2025-07-03 16:37:17 +05:30
Chirag Aggarwal f62c3135df add null check for platform 2025-07-03 16:26:56 +05:30
Khushboo VermaandGitHub 642d046d65 Merge branch '1.7.x' into chore-update-env-var-used-for-domain 2025-07-03 00:38:33 +05:30
Jake BarnbyandGitHub d38b25e0b1 Merge pull request #10090 from appwrite/csv-fix
Fix: path and type
2025-07-02 19:04:00 +00:00
Khushboo Verma 9064fcc24e Merge branch 'chore-update-env-var-used-for-domain' of github.com:appwrite/appwrite into chore-update-env-var-used-for-domain 2025-07-03 00:08:58 +05:30
Khushboo Verma 0f37a0e0ef Fix tests 2025-07-03 00:08:29 +05:30
Khushboo Verma 253195698e Merge branch '1.7.x' into chore-update-env-var-used-for-domain 2025-07-03 00:02:51 +05:30
Jake Barnby 55dc965dd6 Update lib 2025-07-02 14:32:05 -04:00
Jake Barnby 2ce3118ba9 Remove path update 2025-07-02 13:36:46 -04:00
Jake Barnby ce8ae2294a Update lock 2025-07-02 12:58:37 -04:00
Jake Barnby b1724e9c9d Update lock 2025-07-02 10:40:35 -04:00
Darshan ed2d12f896 update: full path on appwrite. 2025-07-02 19:55:42 +05:30
Darshan 6f5d3d53c0 bump: for tests. 2025-07-02 19:51:36 +05:30
Darshan 6d792fcb6c revert: lock. 2025-07-02 19:20:30 +05:30
Darshan fc1a9a4e16 fix: path and revert deps. 2025-07-02 19:13:13 +05:30
Darshan 0e50fb7ec5 add: migrations lib logging for errors. 2025-07-02 17:04:26 +05:30
Darshan d34635becb add: logging for errors. 2025-07-02 15:13:42 +05:30
Darshan eb1d4c68ea add: logging. 2025-07-02 14:54:25 +05:30
Darshan beec2e780d Merge branch '1.7.x' into csv-fix 2025-07-02 14:35:38 +05:30
Darshan aa9a82d8ce fix: document attribute type. 2025-07-02 14:32:16 +05:30
Jake BarnbyandGitHub 49a5e01a53 Merge pull request #10086 from appwrite/revert-10051-chore-upgrade-utopia-queue
Revert "chore: update composer.json and composer.lock to include appwrite-lab…"
2025-07-01 22:44:59 +00:00
Jake Barnby 1cd69e26f8 Reapply "chore: update composer.json and composer.lock to include appwrite-lab…"
This reverts commit 99ccd64b4c.
2025-07-01 18:24:52 -04:00
Jake BarnbyandGitHub 99ccd64b4c Revert "chore: update composer.json and composer.lock to include appwrite-lab…" 2025-07-01 18:24:05 -04:00
Jake BarnbyandGitHub aa03045382 Merge pull request #10085 from appwrite/fix-tests
Fix tests, for `Cloud`
2025-07-01 14:28:23 +00:00
Darshan 4d0bd1d74f add: delay, retries. 2025-07-01 18:10:19 +05:30
Darshan 385e4e14b8 add: delay, retries. 2025-07-01 17:33:11 +05:30
Matej BačoandGitHub 67bdf8ffdc Merge pull request #10034 from appwrite/fix-push-to-existing-repo
Fix connect to existing repo flow
2025-07-01 12:11:48 +02:00
Khushboo VermaandGitHub 3fa04727c7 Merge branch '1.7.x' into fix-push-to-existing-repo 2025-07-01 14:47:52 +05:30
Khushboo VermaandGitHub 2da811956c Merge branch '1.7.x' into chore-update-env-var-used-for-domain 2025-07-01 12:22:36 +05:30
Jake BarnbyandGitHub a24c941e07 Merge pull request #9693 from appwrite/database-aliases
Database aliases, new terminologies and a migration to modules
2025-07-01 03:18:43 +00:00
Jake BarnbyandGitHub e561a38524 Merge pull request #10051 from appwrite/chore-upgrade-utopia-queue
chore: update composer.json and composer.lock to include appwrite-lab…
2025-07-01 02:51:31 +00:00
Jake Barnby a1af742c4b Update lock 2025-06-30 22:28:09 -04:00
Jake Barnby 98dccf6d15 Map null on missing default 2025-06-30 22:27:29 -04:00
Jake Barnby 609e1adcf0 Catch 2025-06-30 22:27:18 -04:00
Jake BarnbyandGitHub 38719dcb4a Merge branch '1.8.x' into database-aliases 2025-07-01 02:15:07 +00:00
Jake Barnby 72cd501f21 Merge remote-tracking branch 'origin/1.7.x' into chore-upgrade-utopia-queue 2025-06-30 21:01:47 -04:00
Khushboo Verma 58c1e2d6e2 Merge branch '1.7.x' into chore-update-env-var-used-for-domain 2025-07-01 02:29:08 +05:30
Khushboo Verma 5959457211 Merge branch '1.7.x' into fix-push-to-existing-repo 2025-07-01 01:24:45 +05:30
Steven NguyenandGitHub beb8e17013 Merge pull request #10079 from appwrite/chore-patch-cache-miss
patch: increase cache ttl to ensure hits
2025-06-30 12:04:45 -07:00
Christy JacobandGitHub 448a8df1a2 Merge branch '1.7.x' into chore-patch-cache-miss 2025-06-30 21:12:46 +04:00
Christy Jacob fd7628efe8 refactor: streamline cache handling in App::shutdown() and ensure file modification time updates 2025-06-30 21:03:30 +04:00
Christy Jacob d6326758a8 patch: increase cache ttl to ensure hits 2025-06-30 20:58:00 +04:00
Chirag Aggarwal 2d00052db6 fix: general test 2025-06-30 20:50:29 +05:30
Chirag Aggarwal db56361ad9 fix: add back empty checks 2025-06-30 19:48:05 +05:30
Chirag Aggarwal 843d4d9ea0 chore: use origin validator 2025-06-30 19:29:42 +05:30
Chirag Aggarwal 8a2ccd32b4 fix: remove redundant check 2025-06-30 19:09:15 +05:30
Chirag Aggarwal 39db3a3066 fix: order 2025-06-30 19:07:49 +05:30
Chirag Aggarwal 9242ef85eb fix: order 2025-06-30 19:03:12 +05:30
Chirag Aggarwal 332c82e61e fix: lint 2025-06-30 19:00:54 +05:30
Chirag Aggarwal bfee98ca87 Merge branch '1.7.x' into feat-exp-scheme 2025-06-30 18:38:25 +05:30
Matej BačoandGitHub 36aa22793f Merge pull request #10043 from appwrite/fix-rules-platform-check
Fix: Platform check for Sites with automatic rule
2025-06-30 14:10:14 +02:00
Khushboo Verma ae72688c8e Merge branch '1.7.x' into fix-push-to-existing-repo 2025-06-30 15:45:54 +05:30
Christy Jacob 63a02e11ac Fix default value assignment in Mapper class to handle null cases 2025-06-27 18:42:02 +04:00
Matej Bačo ba7903af55 Merge branch '1.7.x' into fix-rules-platform-check 2025-06-27 12:46:06 +02:00
Christy JacobandGitHub cb19798284 Merge pull request #10070 from appwrite/update-cli-8.1.0
chore: update cli to 8.1.0 and add changelog
2025-06-27 14:26:32 +04:00
Chirag Aggarwal 156daca044 fix: composer 2025-06-27 15:50:32 +05:30
Chirag Aggarwal f056a8ba2f fix: composer 2025-06-27 15:49:08 +05:30
Chirag Aggarwal 151711aba9 Merge branch '1.7.x' into update-cli-8.1.0 2025-06-27 15:48:33 +05:30
Christy Jacob 14a4bb8828 Merge branch 'chore-upgrade-utopia-queue' of github.com:appwrite/appwrite into chore-upgrade-utopia-queue 2025-06-27 13:42:17 +04:00
Christy Jacob a36525373d Merge branch '1.7.x' of github.com:appwrite/appwrite into chore-upgrade-utopia-queue 2025-06-27 13:37:33 +04:00
Chirag Aggarwal 5a48fd6300 chore: update cli to 8.1.0 and add changelog 2025-06-27 11:43:56 +05:30
Chirag AggarwalandGitHub 8027251224 Merge branch '1.7.x' into feat-exp-scheme 2025-06-27 09:21:27 +05:30
Chirag Aggarwal fffdab23f9 chore: update scheme check 2025-06-27 08:29:13 +05:30
Matej BačoandGitHub 718cefdd94 Merge pull request #10066 from appwrite/fix-account-test-flakyness
Fix: flakyness of account tests
2025-06-26 22:18:00 +02:00
Matej Bačo ee225834d9 Fix default param syntax 2025-06-26 21:39:00 +02:00
Matej Bačo 23f350c7d1 Merge branch '1.7.x' into fix-account-test-flakyness 2025-06-26 21:33:09 +02:00
Jake BarnbyandGitHub 7aed35a588 Merge pull request #10065 from appwrite/dat-565
Updated utopia database
2025-06-26 19:26:28 +00:00
Matej Bačo b12ea0b3b1 Fix merge conflict bug 2025-06-26 21:22:38 +02:00
Matej BačoandGitHub 3a8dafe763 Merge branch '1.7.x' into fix-account-test-flakyness 2025-06-26 21:21:52 +02:00
Matej BačoandGitHub 42a0d64137 Merge pull request #10068 from appwrite/chore-preview-logging
Chore: Preview endpoint logging
2025-06-26 20:53:30 +02:00
Matej Bačo df902d2624 Fix account tests using old token 2025-06-26 20:47:59 +02:00
Matej Bačo a356e86ced Add previe wendpoint logging 2025-06-26 20:17:26 +02:00
Matej Bačo bda9adc52e Fix sms mock endpoint 2025-06-26 20:04:56 +02:00
Matej Bačo 8dcc3c4981 Merge branch '1.7.x' into fix-account-test-flakyness 2025-06-26 19:26:23 +02:00
Christy JacobandGitHub 368f928fa9 Merge pull request #10067 from appwrite/fix-parameter-order
fix: correct parameter order in action function for robots.txt route
2025-06-26 21:05:12 +04:00
Christy Jacob ee2796f870 fix: correct parameter order in action function for robots.txt route 2025-06-26 20:49:52 +04:00
Matej BačoandGitHub d4b53175dc Update tests/e2e/Scopes/Scope.php 2025-06-26 18:33:22 +02:00
Matej Bačo 572ff06f53 Fix robots.txt 5xx error 2025-06-26 18:32:03 +02:00
Matej Bačo 87c100db28 Improve General tests logging 2025-06-26 18:09:07 +02:00
Matej Bačo 918ad8979d Fix flakyness of account tests 2025-06-26 18:07:59 +02:00
ArnabChatterjee20k b36431d45c empty commit 2025-06-26 21:26:25 +05:30
ArnabChatterjee20k c76fa53f55 updated the db version 2025-06-26 21:13:12 +05:30
Matej BačoandGitHub 2fa3ca27af Merge pull request #10052 from appwrite/fix-logs-order
Fix: logs order
2025-06-26 16:32:35 +02:00
Chirag Aggarwal 1555abcbd6 chore: use lowercase for check 2025-06-26 19:59:58 +05:30
Chirag Aggarwal f8e74d073a chore: fix scheme check, remove logging 2025-06-26 19:48:57 +05:30
Chirag Aggarwal be40f1cbe3 chore: remove check for scheme 2025-06-26 19:44:41 +05:30
Chirag Aggarwal 043cead66b chore: fix logging 2025-06-26 19:32:15 +05:30
Chirag Aggarwal 7e378a3a1e chore: more logging 2025-06-26 19:10:35 +05:30
Chirag Aggarwal e13089b1ce chore: more logging 2025-06-26 19:09:14 +05:30
Chirag Aggarwal 9861f60666 chore: add logging 2025-06-26 18:55:51 +05:30
Chirag Aggarwal 06ce606ac7 Merge branch '1.7.x' into feat-exp-scheme 2025-06-26 18:38:10 +05:30
Christy JacobandGitHub 1dfba85bcc Merge pull request #10054 from appwrite/chore-fix-tasks
refactor: replace Console loop with Swoole Timer for stats resource m…
2025-06-26 15:12:05 +04:00
Christy Jacob 204b00850d Refactor log message documentation in Action class to improve clarity and formatting 2025-06-26 15:09:57 +04:00
Christy JacobGitHubcoderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
5761320bfb Update src/Appwrite/Platform/Action.php
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-06-26 15:05:55 +04:00
Christy Jacob dbc7e5b42d Fix output format flag in benchmark workflow for consistency 2025-06-26 15:05:10 +04:00
Christy JacobandGitHub 22900e3de8 Merge pull request #10061 from appwrite/fix-missing-injections
Add: missing injections
2025-06-26 13:24:08 +04:00
Matej BačoandGitHub e5621f3218 Merge pull request #10062 from appwrite/add-runtimeEntrypoint
chore: add runtimeEntrypoint param
2025-06-26 10:29:21 +02:00
Chirag Aggarwal 276a294391 chore: update createExecution 2025-06-26 13:37:43 +05:30
Chirag Aggarwal 8bf3a8bf5d chore: add runtimeEntrypoint param 2025-06-26 13:29:06 +05:30
Darshan 72956d1995 fix: misaligned param order. 2025-06-26 13:07:11 +05:30
Darshan 038b5d7418 fix: lint. 2025-06-26 12:40:56 +05:30
Darshan 8f5b53d16c revert: composer.lock. 2025-06-26 12:36:37 +05:30
Darshan d2753bd491 add: missing injections. 2025-06-26 12:34:38 +05:30
Jake Barnby 921b2bcc5d Merge remote-tracking branch 'origin/1.7.x' into chore-upgrade-utopia-queue 2025-06-25 18:58:08 -04:00
Jake BarnbyandGitHub e8982e6b11 Merge pull request #10056 from appwrite/feat-update-logging
Add database worker job logging
2025-06-25 17:59:59 -04:00
Jake Barnby 7508a22ddf Lint 2025-06-25 16:49:12 -04:00
Jake Barnby 620266d9d6 Remove heartbeat 2025-06-25 16:40:02 -04:00
Jake Barnby f0ea206f29 Fix bench 2025-06-25 15:56:07 -04:00
Jake Barnby d52d8f7697 Fix API dump 2025-06-25 15:47:57 -04:00
Jake Barnby 255be3babf Ensure heartbeat is cleared on destruct 2025-06-25 14:52:14 -04:00
Jake Barnby e37cea5146 Add more trigger logging 2025-06-25 14:50:29 -04:00
Jake Barnby d7d7fa3096 Add trigger logging 2025-06-25 14:47:23 -04:00
Jake Barnby 92a04535ac Add database worker job logging 2025-06-25 14:35:17 -04:00
Christy Jacob c479a11850 Add logging functionality to Action class with dump method and refactor disableSubqueries to use class filters 2025-06-25 21:56:30 +04:00
Christy Jacob 3cd31e2c06 Add disableSubqueries method to Action class and integrate it into StatsResources initialization 2025-06-25 21:46:15 +04:00
Christy Jacob 5173e03636 Merge branch '1.7.x' of github.com:appwrite/appwrite into chore-fix-tasks 2025-06-25 21:33:30 +04:00
Christy JacobandGitHub 3fae6ae91c Merge pull request #10011 from appwrite/PLA-3018
Override project via custom domains log
2025-06-25 19:49:26 +04:00
shimon 19152f50dc Merge branch '1.7.x' of github.com:appwrite/appwrite into PLA-3018
# Conflicts:
#	.github/workflows/benchmark.yml
2025-06-25 18:48:53 +03:00
Matej BačoandGitHub bfbcd648f1 Merge pull request #9967 from appwrite/feat-use-first-class-callable-syntax
Use ->action(...)) instead of ->callback([$this, 'action']);
2025-06-25 17:13:50 +02:00
Matej BačoandGitHub f07abce6f8 Merge branch '1.7.x' into feat-use-first-class-callable-syntax 2025-06-25 16:46:30 +02:00
Matej BačoandGitHub 975f653e3e Merge pull request #9969 from appwrite/feat-add-branch-url-for-first-git-deployment
Create commit & branch url for first git deployment when site is linked to repo
2025-06-25 16:45:41 +02:00
Matej Bačo c85cfe132e Improve flaky tests logging 2025-06-25 15:53:59 +02:00
Matej Bačo 4fff9b94f8 Fix benchmark param name 2025-06-25 14:45:58 +02:00
Matej Bačo 4b5fa0652a Merge branch '1.7.x' into fix-rules-platform-check 2025-06-25 14:44:52 +02:00
Matej Bačo f1590652cb Improve benchmark logging 2025-06-25 14:30:50 +02:00
Matej Bačo df82340dae Improve code quality 2025-06-25 14:22:55 +02:00
Darshan 67e546faa8 fix: namespace for table logs. 2025-06-25 17:50:54 +05:30
Matej Bačo 56347e42df Fix syntax bug 2025-06-25 14:20:20 +02:00
Matej Bačo 6cb3e35c64 Merge branch '1.7.x' into fix-logs-order 2025-06-25 13:41:49 +02:00
Matej BačoandGitHub 213cbf055a Merge pull request #10053 from appwrite/fix-build-duration
Fix: build duration calculation
2025-06-25 13:41:24 +02:00
Matej BačoandGitHub f394f3de62 Merge pull request #10055 from appwrite/chore-fix-benchmark-ci
chore: fix benchmark CI
2025-06-25 13:36:11 +02:00
Darshan bce666e81b add: scopes on migrations and update tests. 2025-06-25 17:02:21 +05:30
Christy Jacob 93651c776e chore: fix benchmark CI 2025-06-25 15:32:19 +04:00
Christy Jacob 347f035105 chore: fix benchmark CI 2025-06-25 15:27:51 +04:00
Matej Bačo b6d4baf4dc Fix tests 2025-06-25 13:14:08 +02:00
Christy Jacob c268b632fb chore: update benchmark command to use JSON output format and adjust timer interval in StatsResources 2025-06-25 14:52:13 +04:00
Christy Jacob 17428ef433 refactor: replace Console loop with Swoole Timer for stats resource management 2025-06-25 14:18:22 +04:00
Matej Bačo 4b1966997b Fix build duration calculation 2025-06-25 10:30:07 +02:00
shimon e712ba5763 Merge branch '1.7.x' of github.com:appwrite/appwrite into PLA-3018 2025-06-25 10:18:07 +03:00
Darshan d7d8dbdbd4 fix: name. 2025-06-25 11:53:21 +05:30
Chirag Aggarwal 1a9664a679 chore: fix benchmark 2025-06-25 11:43:18 +05:30
Darshan 789064e295 add: new scopes to the api. 2025-06-25 11:29:45 +05:30
Chirag Aggarwal 65b5af1b00 chore: fix tests 2025-06-25 11:27:23 +05:30
Chirag Aggarwal 6dda6e1fda chore: remove console log 2025-06-25 11:25:39 +05:30
Darshan 7f4d6233a2 add: validators to swagger2. 2025-06-25 11:00:09 +05:30
Chirag Aggarwal c331a3caae chore: minor fixes 2025-06-25 11:00:07 +05:30
Chirag Aggarwal dbc58655b2 Merge branch '1.7.x' into feat-exp-scheme 2025-06-25 07:53:28 +05:30
Christy Jacob b646eb7758 chore: update composer.json and composer.lock to upgrade utopia-php/queue to version 0.12.0 and appwrite-labs/php-amqplib to version 0.1.1 2025-06-25 00:23:21 +04:00
Matej Bačo c04c2d271c Remove logs, fix edge logs missing 2025-06-24 16:03:28 +02:00
Matej Bačo b5bc240c0d Fix ssr separation; add edge debugging logs 2025-06-24 15:56:06 +02:00
Matej Bačo 5dcc508870 Fix logs parsing 2025-06-24 15:38:39 +02:00
Matej Bačo 6996cc28a0 Fix logs order 2025-06-24 14:47:47 +02:00
Christy Jacob fe080153e6 chore: update benchmark workflow to use JSON output format 2025-06-24 15:28:07 +04:00
Christy Jacob 4759407696 chore: update composer.json and composer.lock to include appwrite-labs/php-amqplib and adjust utopia-php/queue version 2025-06-24 14:57:30 +04:00
Steven NguyenandGitHub 030d37fd5f Merge pull request #10036 from appwrite/update-executor
chore: update executor to fix s3 endpoint bug
2025-06-23 19:23:03 -07:00
shimon 213b1ae20e benchmark cli 2025-06-23 20:57:39 +03:00
shimon 4e0faf67f7 benchmark cli 2025-06-23 20:39:52 +03:00
shimon 3059eecbeb benchmark cli 2025-06-23 20:27:12 +03:00
shimon dd8894ad1e Merge remote-tracking branch 'origin/PLA-3018' into PLA-3018 2025-06-23 20:21:25 +03:00
shimon 28f420f10c Merge branch '1.7.x' of github.com:appwrite/appwrite into PLA-3018 2025-06-23 20:20:43 +03:00
54748b7d32 Update app/controllers/general.php
Co-authored-by: Steven Nguyen <1477010+stnguyen90@users.noreply.github.com>
2025-06-23 20:50:59 +04:00
Matej Bačo b69ff36ddb Manual QA fixes 2025-06-23 11:57:06 +02:00
Matej Bačo c1bd493fbe Move refferer to resource; re-use in OPTIONS; re-use in platform check 2025-06-23 11:28:18 +02:00
Darshan ffccdef68c fix: oha as per latest updates. 2025-06-23 12:58:46 +05:30
Darshan 6a74273564 fix: oha as per latest updates. 2025-06-23 12:45:34 +05:30
Darshan b52c49389c fix: oha as per latest updates. 2025-06-23 12:28:14 +05:30
Darshan 597230006f fix: oha as per latest updates. 2025-06-23 12:23:26 +05:30
Darshan 7969e57134 ci: empty commit 2025-06-23 12:11:00 +05:30
DarshanandGitHub 7e026b11b4 Merge branch '1.8.x' into lazy-load-relationships 2025-06-23 10:45:55 +05:30
Darshan d338007d68 update: re-add filters since the PR is in draft and will be merged in the end. 2025-06-23 10:44:25 +05:30
Chirag Aggarwal 2ac4d9f901 chore: update executor to fix s3 endpoint bug 2025-06-21 07:59:54 +05:30
Khushboo Verma 1319bf7ec1 Fix connect to existing repo flow 2025-06-21 01:41:41 +04:00
shimon d8c88fb420 composer 2025-06-19 19:44:33 +03:00
shimon 68fc0f898f set log projectId on the router method 2025-06-19 17:15:43 +03:00
Darshan 4521302cb8 fix: graphql test for complex queries. 2025-06-19 18:36:32 +05:30
Darshan 531f50a2c7 fix: sdk namespacing and graphql tests. 2025-06-19 17:59:29 +05:30
Darshan 7ebe8f6c9e revert: legacy tests namespacing to old. 2025-06-19 17:34:16 +05:30
shimon 0f8c5fe292 Merge branch '1.7.x', commit 'c0c30fe767f8c197b8793bf562f57146e5ba15a8' of github.com:appwrite/appwrite into PLA-3018 2025-06-19 12:46:34 +03:00
DarshanandGitHub c08db975d6 Merge branch '1.8.x' into database-aliases 2025-06-19 14:52:13 +05:30
Darshan 5287a3d619 address comment. 2025-06-19 14:52:01 +05:30
Darshan caf2308300 address comment. 2025-06-19 14:50:15 +05:30
Darshan 3d6acaa19c address comments. 2025-06-19 14:49:42 +05:30
Darshan 60ab787894 address comments. 2025-06-19 14:33:58 +05:30
Darshan 03b4026233 remove: leftover. 2025-06-19 14:33:23 +05:30
Darshan 1d23e2ec12 remove: use HTTP. update: inline context logic. 2025-06-19 14:31:54 +05:30
Darshan e5caa85629 update: init with module structure. 2025-06-19 11:02:31 +05:30
Darshan c41d37c982 change: namespace. 2025-06-19 10:54:56 +05:30
Darshan c93d92abef rename: tests from collections to legacy. 2025-06-19 10:52:15 +05:30
Jake BarnbyandGitHub 53a00deb0a Merge pull request #10010 from ArnabChatterjee20k/dat-522
Added auth option to allow invalidating existing sessions
2025-06-18 17:41:41 -04:00
ArnabChatterjee20k d053282bcb linting 2025-06-19 02:29:21 +05:30
ArnabChatterjee20k 225b6e8413 updated tests and project invaldate session 2025-06-19 02:26:58 +05:30
Jake BarnbyandGitHub da83470f4d Merge pull request #10020 from appwrite/optimize-process-documents
Improve `processDocuments`
2025-06-18 13:06:18 -04:00
Khushboo Verma e65ed58ca4 Merge branch 'chore-update-env-var-used-for-domain' of github.com:appwrite/appwrite into chore-update-env-var-used-for-domain 2025-06-18 22:17:11 +05:30
Khushboo Verma 095159d89b Merge branch '1.7.x' into chore-update-env-var-used-for-domain 2025-06-18 22:15:19 +05:30
DarshanandGitHub 5e9cabf115 Merge branch '1.8.x' into lazy-load-relationships 2025-06-18 18:36:47 +05:30
Christy JacobandGitHub 509392c48b Merge branch '1.7.x' into chore-update-env-var-used-for-domain 2025-06-18 13:44:04 +04:00
ArnabChatterjee20k 56494ca427 updated invalidation session during the change password in the account endpoint 2025-06-18 13:30:24 +05:30
Darshan 6214c2d104 update: disable 1.8.x request/response filters for lazy-loading-relationships. 2025-06-18 12:07:00 +05:30
Darshan d69189bd55 ci: empty commit 2025-06-18 12:02:08 +05:30
Darshan d6bf748a07 address comments. 2025-06-18 12:00:19 +05:30
Khushboo Verma 520006864d Merge branch '1.7.x' into chore-update-env-var-used-for-domain 2025-06-18 11:07:25 +05:30
Khushboo Verma 5a3e44a36d Replace == with === 2025-06-18 11:06:35 +05:30
ArnabChatterjee20k 8d62ada545 updated example of the authInvalidateSession 2025-06-18 10:54:26 +05:30
Khushboo Verma 22cda269b1 Update console 2025-06-17 18:37:02 +05:30
Darshan 16a8753509 update: comment. 2025-06-17 15:03:11 +05:30
Darshan 1fbbbee310 update: add cached logic to processDocument. 2025-06-17 15:01:13 +05:30
Khushboo Verma e8e7ee01b4 Merge branch 'fix-github-check-urls' into chore-update-env-var-used-for-domain 2025-06-17 14:01:50 +05:30
Khushboo Verma b7ffd2ca94 Revert some changes 2025-06-17 13:57:21 +05:30
Khushboo Verma 9356fb4cb4 Merge branch '1.7.x' into chore-update-env-var-used-for-domain 2025-06-17 12:16:37 +05:30
Khushboo Verma 41bc8272ff Add env var to docker-compose 2025-06-17 12:15:52 +05:30
Darshan 1e39d8d6ee revert: sanity check tests! 2025-06-17 12:01:50 +05:30
Darshan bfb383e28a test: backwards compat for request/response filters on latest 1.6.x. 2025-06-17 11:47:40 +05:30
Darshan 57d8185b16 test: backwards compat for request/response filters. 2025-06-17 11:26:22 +05:30
Darshan deb8b3a789 revert: stuff that is not supposed to be in this branch. 2025-06-17 11:11:47 +05:30
Darshan 16a3275c81 Merge branch '1.8.x' into 'lazy-load-relationships'. 2025-06-17 10:36:16 +05:30
Darshan 8c57672032 Merge commit 'e704e96290' into lazy-load-relationships 2025-06-17 10:34:54 +05:30
DarshanandGitHub 1337b759de fix: remove event for bulk api support. 2025-06-17 10:15:45 +05:30
DarshanandGitHub 34e4299e10 Merge branch '1.8.x' into database-aliases 2025-06-17 10:13:38 +05:30
Darshan e236aac0ea fix: events logic 😮‍💨 2025-06-17 09:57:35 +05:30
ArnabChatterjee20k 28fbd64864 updated session invalidation tests , models and descriptions 2025-06-17 00:40:32 +05:30
ArnabChatterjee20k 9bf4361761 updated indentation 2025-06-16 23:48:08 +05:30
ArnabChatterjee20k 7a0ba95231 updated onPasswordChange to invalidate session 2025-06-16 23:05:52 +05:30
Khushboo Verma ad60ff9972 Update url in general.php 2025-06-16 19:16:20 +05:30
Khushboo Verma 8c8abfdece Add var_dumps to debug 2025-06-16 18:47:50 +05:30
Khushboo Verma 5e345c0388 Revert some changes 2025-06-16 17:29:01 +05:30
Khushboo Verma 362618c5af Merge branch 'fix-github-check-urls' into chore-update-env-var-used-for-domain 2025-06-16 17:19:55 +05:30
shimon c0c30fe767 remove duplicate use 2025-06-15 12:40:22 +03:00
shimon d7e2552999 Merge branch '1.7.x' of github.com:appwrite/appwrite into PLA-3018 2025-06-15 12:38:50 +03:00
shimon 7d54a6191d remove duplicate use 2025-06-15 12:13:15 +03:00
shimon 4d41c90367 remove duplicate use 2025-06-15 12:00:39 +03:00
shimon 96c035401b Override project resource to update hooks, since x-appwrite-project is not available when executing custom domain function 2025-06-15 11:30:57 +03:00
ArnabChatterjee20k 774291b5d8 added onPasswordChange property to the console project 2025-06-14 18:15:41 +05:30
ArnabChatterjee20k 74614e8d58 updated description of on password change 2025-06-14 18:09:20 +05:30
ArnabChatterjee20k fde2f278e8 added invalidating sessions for the project users 2025-06-14 18:07:42 +05:30
Khushboo VermaandGitHub 28db9a289b Merge branch '1.7.x' into feat-add-branch-url-for-first-git-deployment 2025-06-13 19:08:14 +05:30
Khushboo Verma fc656167d5 Composer update 2025-06-13 18:11:51 +05:30
Khushboo Verma 37388fcf1b Change php version in composer.lock 2025-06-13 18:08:19 +05:30
Khushboo Verma 55be41f633 Resolve merge conflicts 2025-06-13 18:06:47 +05:30
Khushboo Verma cee6420fa2 Use env vars instead of request 2025-06-13 15:50:53 +05:30
Khushboo Verma 8bac836483 Merge branch 'fix-github-check-urls' into chore-update-env-var-used-for-domain 2025-06-13 15:18:31 +05:30
Darshan 8d74083da3 remove: filter queries. 2025-06-13 15:18:21 +05:30
Khushboo Verma 01aa9fbb48 Merge branch 'chore-update-env-var-used-for-domain' of github.com:appwrite/appwrite into chore-update-env-var-used-for-domain 2025-06-13 15:18:17 +05:30
Khushboo VermaandGitHub d7c3100fe9 Merge branch '1.7.x' into chore-update-env-var-used-for-domain 2025-06-13 15:18:02 +05:30
Darshan 8cbd06173d update: realtime channels checks for tables api. 2025-06-13 15:11:18 +05:30
Darshan 9cfff792fd add: graphql tests for bulk apis to server tests. 2025-06-13 14:13:26 +05:30
Darshan 6e1fa0526d add: graphql tests for bulk apis to server tests. 2025-06-13 14:07:33 +05:30
Darshan 2e05844ca2 add: graphql tests for bulk apis. 2025-06-13 14:04:23 +05:30
Darshan a13a6a6913 update: naming convention for table tests. 2025-06-13 11:06:51 +05:30
Darshan 6f16895e8b Merge branch '1.8.x' into 'database-aliases'. 2025-06-13 10:44:44 +05:30
Darshan 44870b9810 fix: pending tests. 2025-06-13 10:26:05 +05:30
Darshan 5f8c811f15 fix: missing context. 2025-06-13 10:08:07 +05:30
Khushboo Verma 43f9254302 Merge branch '1.7.x' into chore-update-env-var-used-for-domain 2025-06-12 20:32:02 +05:30
Darshan 018aad5339 fix: one more test... 2025-06-12 20:03:41 +05:30
Darshan 58e878653e fix: one more test before I sign out 👋 2025-06-12 20:00:53 +05:30
Darshan b248c9b640 fix: a bunch of tests 🚧 2025-06-12 19:58:28 +05:30
Darshan 1f31998092 fix: route name 😒 2025-06-12 19:48:28 +05:30
Darshan 90d18a9bd5 add: increment, decrement document/row attribute/column; fixes: misc. 2025-06-12 18:24:54 +05:30
Darshan 3426847b17 add: update documents/rows. 2025-06-12 17:31:37 +05:30
Khushboo Verma 710b8bda56 Use _APP_CONSOLE_DOMAIN, if not found, then use _APP_DOMAIN 2025-06-12 17:27:21 +05:30
Darshan 2994cdf874 update: model. 2025-06-12 17:14:50 +05:30
Darshan 1c87e765de add: delete documents/rows 2025-06-12 17:12:43 +05:30
Darshan 919f358f3f fix: audit event. 2025-06-12 16:58:18 +05:30
Darshan a6ae715e3a update: descriptions. 2025-06-12 16:55:31 +05:30
Darshan 44b8054ed7 add: upsert documents/rows. 2025-06-12 16:55:15 +05:30
Darshan ad34cf3a62 add: lengths to column indexes. 2025-06-12 16:17:27 +05:30
Darshan ffec769f4a fix: index lengths. 2025-06-12 16:16:55 +05:30
Darshan b1f7f08b9a remove: duplicate events. 2025-06-12 16:06:15 +05:30
Darshan d502869c3d fix: index lengths. 2025-06-12 15:21:06 +05:30
Darshan d6b55730eb fix: string encryption attribute in model. 2025-06-12 15:10:28 +05:30
Darshan 9334b98308 fix: duplicate call to collection creation. 2025-06-12 14:40:33 +05:30
Darshan db128d1fb4 update: better syntax for callbacks. 2025-06-12 14:39:47 +05:30
Darshan 95bc85f18d add: upsert for collections and tables. 2025-06-12 14:31:28 +05:30
Darshan 7beae21535 fix: missing context! 2025-06-12 14:20:22 +05:30
Darshan 30eb206dc8 Merge branch '1.8.x' into database-aliases 2025-06-12 11:41:11 +05:30
Darshan 7169e67245 add/update: createDocuments on module structure. 2025-06-11 20:05:09 +05:30
Darshan 8e2eff7894 rollback: auth exception as per 1.8.x source. 2025-06-11 16:53:14 +05:30
Darshan 3e1adc6004 remove: $isTablesAPI var. 2025-06-11 16:51:27 +05:30
Darshan 86a9598a51 update: remove not-found and index dependency exceptions from global handler. 2025-06-11 16:51:09 +05:30
Darshan dc439d1a16 update: remove authorization from global handler. 2025-06-11 16:46:21 +05:30
Darshan 6b53337a23 update: remove duplicate exceptions from global handler. 2025-06-11 16:44:32 +05:30
Darshan 3a88c74109 update: remove structure exceptions from global handler. 2025-06-11 16:18:06 +05:30
Darshan e6268fe5a6 update: remove query exceptions from global handler. 2025-06-11 15:58:22 +05:30
Darshan e07a1817ab update: remove conflict, restrict and relationship exceptions from global handler. 2025-06-11 13:42:55 +05:30
Darshan 3f81847267 update: getInternalId() -> getSequence() 2025-06-11 12:51:31 +05:30
Darshan 1ac22c0ae0 Merge branch '1.8.x' into 'database-aliases'. 2025-06-11 12:36:33 +05:30
Darshan fcdddf7bf0 address comments. 2025-06-11 12:21:48 +05:30
Khushboo VermaandGitHub ef12f49277 Merge branch '1.7.x' into feat-add-branch-url-for-first-git-deployment 2025-06-10 13:08:34 +05:30
Khushboo Verma fa8e5b5b8e Add commit url 2025-06-04 19:11:07 +05:30
Khushboo Verma 54552bf1b4 Create branch url for first git deployment 2025-06-04 18:34:01 +05:30
Khushboo Verma 6530d31e7a Use ->action(...)) 2025-06-04 14:07:43 +05:30
Darshan 087ccfe347 remove: todo. 2025-05-10 12:06:17 +05:30
Darshan b0d9215ea5 fix: abuse tests! 2025-05-10 11:57:32 +05:30
Darshan 63d591e766 fix: method name. 2025-05-10 11:39:46 +05:30
Darshan e50d7df56c fix: audits event. 2025-05-10 11:38:28 +05:30
Darshan 9ba29d4779 update: var name. 2025-05-10 11:36:55 +05:30
Darshan 15e0385723 update: exception name. 2025-05-10 11:36:13 +05:30
Darshan 6bdfe3b895 address naming comments. 2025-05-10 11:34:20 +05:30
Darshan afa8341f95 add: table apis to webhook custom server tests. 2025-05-10 11:01:36 +05:30
Darshan 45370f91c3 add: table apis to webhook tests. 2025-05-10 10:52:43 +05:30
Darshan ba26eae2c3 Merge remote-tracking branch 'origin/database-aliases' into database-aliases 2025-05-10 10:36:26 +05:30
Darshan 5dca1ff0d6 update: fix test variable. 2025-05-10 10:36:16 +05:30
Darshan fbe2609f7b update: simplify setting events context param. 2025-05-10 10:34:44 +05:30
Darshan 5022a052ce update: test coverage for databases. 2025-05-10 10:20:23 +05:30
Darshan 68994f2634 fix: graphql tests. 2025-05-10 10:19:01 +05:30
Darshan 594f8595ae update: database server test for graphql <> tables api. 2025-05-10 10:10:23 +05:30
Darshan 02af05b409 fix: endpoint in abuse test. 2025-05-10 10:10:03 +05:30
Darshan d1ae06550f update: realtime events logic. 2025-05-10 10:09:43 +05:30
DarshanandGitHub e1c5b3f59e Merge branch '1.7.x' into database-aliases 2025-05-10 09:38:17 +05:30
Darshan a067b77e90 update: model; add|update: graphql tests [wip]. 2025-05-09 20:04:02 +05:30
Darshan c542ba033f patch: graphql tests <> collections. 2025-05-09 18:15:17 +05:30
Darshan d5e94a57be revert: graphql tests for collections api. 2025-05-09 17:38:02 +05:30
Darshan 241779e046 add: table api tests to abuse. 2025-05-09 17:34:41 +05:30
Darshan f16ba8dee3 add: table api tests. 2025-05-09 17:25:36 +05:30
Darshan 1756acd89c fix: realtime channel count. 2025-05-09 15:16:10 +05:30
Darshan 94fbf19e54 fix|add: tables api tests to realtime. 2025-05-09 15:11:14 +05:30
Darshan d3f1c4f852 revert: tests to collections API. 2025-05-09 15:10:44 +05:30
Darshan f10be55f92 update: short the callbacks. 2025-05-09 14:49:44 +05:30
Darshan a807dd97d9 fix: realtime events. 2025-05-09 14:47:01 +05:30
Darshan a8dc5c3797 add: table tests to usage. 2025-05-09 11:10:53 +05:30
Darshan 65dbebd103 misc: fix error types. 2025-05-09 11:09:58 +05:30
Darshan 938b376f3c fix: var ref. 2025-05-09 10:02:14 +05:30
Darshan ef30e827c7 misc: changes to the databases worker. 2025-05-09 09:52:04 +05:30
Darshan 0ecd97d027 fix: wrong param. 2025-05-09 09:33:52 +05:30
Darshan 2d74765ba0 update: fix exception. 2025-05-08 20:00:01 +05:30
Darshan c5074fd52f update: move around tests. 2025-05-08 19:53:31 +05:30
Darshan 5c0717458f update: the usage model. 2025-05-08 19:51:16 +05:30
Darshan 36e6617090 update: the usage modals. 2025-05-08 19:47:35 +05:30
Darshan 8006c9d60b misc: fixes. 2025-05-08 19:36:59 +05:30
Darshan 44a5fef345 misc: name fixes. 2025-05-08 19:17:06 +05:30
Darshan 4e83fc856a update: split service registrations into smaller registries. 2025-05-08 15:38:07 +05:30
Darshan c4a26fbb95 fix: another pesky sdk namespace 🤨. 2025-05-08 15:37:00 +05:30
Darshan eafa9bb393 fix: namespaces, again. 2025-05-08 15:33:41 +05:30
Darshan d7023b5a69 fix: namespaces. 2025-05-08 15:33:00 +05:30
Darshan 97d1a535df fix: missing path :\ 2025-05-08 15:30:52 +05:30
Darshan 6fba4a5f08 update: sdk namespaces. 2025-05-08 14:20:11 +05:30
Darshan fe693a9469 add: indexes api to tables. 2025-05-08 14:15:00 +05:30
Darshan da70da985b update: use nested directory structure. 2025-05-08 12:55:54 +05:30
Darshan 96b89693fc update: use abstract for getResponseModel. 2025-05-08 12:47:21 +05:30
Darshan cb64484c44 update: abstraction over document and row apis. 2025-05-08 11:59:55 +05:30
Darshan fc456b4cca update: abstraction over attribute and column apis. 2025-05-08 10:40:35 +05:30
Darshan b55521e3de update: address minor comments by coderabbitai. 2025-05-08 09:36:13 +05:30
Darshan 6493759536 Merge remote-tracking branch 'origin/database-aliases' into database-aliases
# Conflicts:
#	app/init/constants.php
2025-05-07 20:02:23 +05:30
Darshan bc8a38ba55 update: abstraction over attribute and column apis [WIP]. 2025-05-07 20:01:46 +05:30
Darshan 69cc4f0bb6 update: handle the context logic, use defaults. 2025-05-07 17:32:54 +05:30
DarshanandGitHub b534d3843e Merge branch '1.7.x' into database-aliases 2025-05-07 17:03:17 +05:30
Darshan 0f94b800a5 update: abstraction over table and collection apis. 2025-05-07 17:02:24 +05:30
Darshan 6d1afea436 misc. 2025-05-07 14:14:12 +05:30
Darshan f2f23be648 update: events, errors, realtime. 2025-05-07 14:11:28 +05:30
DarshanandGitHub 04386eea7c Merge pull request #9720 from appwrite/response-filters-for-databases
Request, Response filters for databases
2025-05-07 13:38:19 +05:30
Darshan 8610687b87 update: events schema, response models for database usages. 2025-05-07 13:23:27 +05:30
DarshanandGitHub 28a74e664d Merge pull request #9728 from appwrite/update-exceptions
Update Exceptions
2025-05-07 11:31:49 +05:30
DarshanandGitHub 0266376315 Merge branch 'response-filters-for-databases' into update-exceptions 2025-05-07 11:30:48 +05:30
Darshan 7f023f29c5 update: keep both tables and collections api errors. 2025-05-07 11:30:31 +05:30
Darshan 46ccf6f537 update: re-add legacy models; remove: filters. 2025-05-07 11:13:15 +05:30
DarshanandGitHub 026150971f Merge pull request #9722 from appwrite/events-compatibility
Events compat based on version
2025-05-07 10:34:36 +05:30
Darshan ee117b816a manage: events and channels. 2025-05-07 10:33:54 +05:30
DarshanandGitHub 80915fc5e9 Merge branch 'events-compatibility' into update-exceptions 2025-05-06 17:03:52 +05:30
DarshanandGitHub 9a45a0fc7b Merge branch 'response-filters-for-databases' into events-compatibility 2025-05-06 17:02:58 +05:30
Darshan ed9306ef8d update: change filter name for attributes <> columns. 2025-05-06 17:00:57 +05:30
Darshan 3e813f6cba update: change filter name for collections <> tables. 2025-05-06 17:00:12 +05:30
Darshan 6eb1c3ae68 update: rename remaining variables. 2025-05-06 16:58:56 +05:30
Darshan 7a52ace9b2 update: attributes <> columns errors. 2025-05-06 16:53:20 +05:30
Darshan 6e8dbfc674 update: documents <> rows errors. 2025-05-06 16:48:20 +05:30
Darshan 59e5ce0664 update: collections <> tables errors. 2025-05-06 16:44:19 +05:30
Darshan d70d001f5a add: backported channels. 2025-05-06 12:52:05 +05:30
Darshan a0ffcd54dc Merge branch 'response-filters-for-databases' into events-compatibility 2025-05-06 12:17:02 +05:30
Darshan b1735ef945 remove: aliasing filter. 2025-05-06 12:14:54 +05:30
Darshan 9ec897f7d6 fix: filtering 2025-05-06 12:05:06 +05:30
Darshan 86d195c1c0 update: request, response filters. 2025-05-06 11:31:35 +05:30
Darshan 3a8f097c17 fix: realtime channels; update: row model. 2025-05-06 11:31:05 +05:30
Darshan 253c428ca2 remove: filter. 2025-05-06 11:30:15 +05:30
Darshan 9e1105b61f update: tests. 2025-05-06 11:29:56 +05:30
Darshan 56077d59f7 fix: graphql tests. 2025-05-06 10:51:11 +05:30
Darshan 565e6b2ad8 fix: migrations test <> csv imports. 2025-05-05 17:03:17 +05:30
Darshan 67a8f522e5 fix: project and general usage tests. 2025-05-05 14:45:55 +05:30
Darshan b905af91af fix: project usage documents to rows. 2025-05-05 14:43:09 +05:30
Darshan 37fb34ae12 wip: events compat based on version. 2025-05-05 14:33:27 +05:30
Darshan 75c9db8031 update: tests to use the 1.7.x changes on databases. 2025-05-05 14:07:34 +05:30
Darshan e2202e9ffa remove: old attributes anyway. 2025-05-05 14:05:24 +05:30
Darshan d30fd56573 patch: index module to use/specify columns instead of attributes. 2025-05-05 14:04:39 +05:30
Darshan 71ad9c203e fix: response modals, mappings, filters. 2025-05-05 13:53:57 +05:30
Darshan bdfab30fd4 Merge branch 'database-aliases' into response-filters-for-databases 2025-05-05 10:58:08 +05:30
Darshan 6cb44399c1 update: services. 2025-05-05 10:57:54 +05:30
Darshan cabf32559a update: return keys. 2025-05-05 09:50:57 +05:30
Darshan 7a53680786 update: document response model to row; refactor: old collection to table models on response types. 2025-05-05 09:42:33 +05:30
Darshan 675d404b9b update: attribute* response model to column*. 2025-05-05 09:30:27 +05:30
Darshan 8504fbc0b9 update: collection response model to table. 2025-05-05 09:14:20 +05:30
Darshan d184d1c7b1 fix: lint! 2025-05-04 18:50:24 +05:30
Darshan e0a4c698bc fix: float column creation! 2025-05-04 18:43:32 +05:30
Darshan 2554adb41e update: error messages. 2025-05-04 18:29:11 +05:30
DarshanandGitHub c3d3665383 Merge branch '1.7.x' into database-aliases 2025-05-04 18:18:53 +05:30
Darshan 6fac164d46 update: keep message same. 2025-05-04 18:18:28 +05:30
Darshan ad16bd281e add: database module to platform registry. 2025-05-04 18:16:08 +05:30
Darshan 57a9218649 remove: database controller 👋 2025-05-04 18:10:09 +05:30
Darshan 4cd8f49f82 add: usage. 2025-05-04 18:09:39 +05:30
Darshan 286c695b47 add: move and register row modules. 2025-05-04 17:52:29 +05:30
Darshan fd7801fe26 add: register index and table modules. 2025-05-04 17:24:38 +05:30
Darshan 76785d58c6 fix: imports, lint. 2025-05-04 17:16:52 +05:30
Darshan 3271ef5769 update: move Index ops to module. 2025-05-04 17:16:37 +05:30
Darshan 8efc1543e9 update: move Table ops to module. 2025-05-04 17:05:14 +05:30
Darshan 1945a64083 update: move DB ops to module. 2025-05-04 14:08:10 +05:30
Darshan 9c1ec6fe02 update: move the init hook. 2025-05-04 13:37:25 +05:30
Darshan b3b9961860 init: move to module structure for database; start with columns. 2025-05-04 13:22:26 +05:30
Darshan 7ffd5cfde6 fix: graphql tests, again. 2025-04-27 14:47:59 +05:30
Darshan 9bf1fdc2ff fix: graphql tests. 2025-04-27 14:27:07 +05:30
Darshan 9cccd89439 fix: graphql tests. 2025-04-27 14:08:26 +05:30
Darshan f94ecc885a fix: tests as per new events system. 2025-04-27 10:36:21 +05:30
Darshan 949f58522d change: events system. 2025-04-27 10:33:44 +05:30
Darshan 89ce5a6e10 add: todo. 2025-04-26 16:07:46 +05:30
Darshan 21715448ac fix|update: database tests for new error messages. 2025-04-26 16:07:32 +05:30
Darshan 4f1f9bb4a2 add: filter for latest changes. 2025-04-26 16:06:53 +05:30
Darshan 814be66b98 update: use new terminologies on path params and local variables. 2025-04-26 16:05:07 +05:30
Luke B. SilverandGitHub d82812cc04 Merge branch '1.6.x' into feat-exp-scheme 2025-04-15 10:11:31 +01:00
loks0n 2427f950b5 fix: realtime 2025-04-14 14:34:22 +01:00
loks0n e551e4ad5b chore: fmt 2025-04-14 14:23:01 +01:00
loks0n 67a313cef3 fix: tests 2025-04-14 13:50:48 +01:00
loks0n 3c2b88dc86 chore: remove unused 2025-04-14 13:02:02 +01:00
loks0n 944624f13a chore: fmt 2025-04-14 12:59:39 +01:00
loks0n 4ac2571135 Merge branch '1.6.x' of https://github.com/appwrite/appwrite into feat-exp-scheme 2025-04-14 12:58:25 +01:00
loks0n 0a4c430ae7 feat: exp scheme 2025-04-14 12:56:42 +01:00
340 changed files with 33663 additions and 6298 deletions
+1
View File
@@ -22,6 +22,7 @@ _APP_OPTIONS_FORCE_HTTPS=disabled
_APP_OPTIONS_ROUTER_FORCE_HTTPS=disabled
_APP_OPENSSL_KEY_V1=your-secret-key
_APP_DOMAIN=traefik
_APP_CONSOLE_DOMAIN=localhost
_APP_DOMAIN_FUNCTIONS=functions.localhost
_APP_DOMAIN_SITES=sites.localhost
_APP_DOMAIN_TARGET_CNAME=test.localhost
+3 -2
View File
@@ -64,8 +64,9 @@ jobs:
sudo wget -O /usr/share/keyrings/azlux-archive-keyring.gpg https://azlux.fr/repo.gpg
sudo apt update
sudo apt install oha
oha --version
- name: Benchmark PR
run: 'oha -z 180s http://localhost/v1/health/version -j > benchmark.json'
run: 'oha -z 180s http://localhost/v1/health/version --output-format json > benchmark.json'
- name: Cleaning
run: docker compose down -v
- name: Installing latest version
@@ -78,7 +79,7 @@ jobs:
docker compose up -d
sleep 10
- name: Benchmark Latest
run: oha -z 180s http://localhost/v1/health/version -j > benchmark-latest.json
run: oha -z 180s http://localhost/v1/health/version --output-format json > benchmark-latest.json
- name: Prepare comment
run: |
echo '## :sparkles: Benchmark results' > benchmark.txt
+4 -4
View File
@@ -145,7 +145,8 @@ jobs:
Account,
Avatars,
Console,
Databases,
Databases/Legacy,
Databases/Tables,
Functions,
FunctionsSchedule,
GraphQL,
@@ -213,7 +214,8 @@ jobs:
Account,
Avatars,
Console,
Databases,
Databases/Legacy,
Databases/Tables,
Functions,
FunctionsSchedule,
GraphQL,
@@ -283,8 +285,6 @@ jobs:
name: E2E Service Test (Dev Keys)
runs-on: ubuntu-latest
needs: setup
strategy:
fail-fast: false
steps:
- name: checkout
uses: actions/checkout@v4
+6 -4
View File
@@ -188,16 +188,18 @@ CLI::setResource('getLogsDB', function (Group $pools, Cache $cache) {
return $database;
};
}, ['pools', 'cache']);
CLI::setResource('publisher', function (Group $pools) {
return new BrokerPool(publisher: $pools->get('publisher'));
}, ['pools']);
CLI::setResource('publisherRedis', function () {
// Stub
});
CLI::setResource('queueForStatsUsage', function (Publisher $publisher) {
return new StatsUsage($publisher);
}, ['publisher']);
CLI::setResource('queueForStatsResources', function (Publisher $publisher) {
return new StatsResources($publisher);
}, ['publisher']);
CLI::setResource('publisher', function (Group $pools) {
return new BrokerPool(publisher: $pools->get('publisher'));
}, ['pools']);
CLI::setResource('queueForFunctions', function (Publisher $publisher) {
return new Func($publisher);
}, ['publisher']);
+4 -3
View File
@@ -5,7 +5,7 @@
*/
use Appwrite\Auth\Auth;
use Appwrite\Network\Validator\Origin;
use Appwrite\Network\Platform;
use Utopia\Database\Helpers\ID;
use Utopia\System\System;
@@ -23,7 +23,7 @@ $console = [
[
'$collection' => ID::custom('platforms'),
'name' => 'Localhost',
'type' => Origin::CLIENT_TYPE_WEB,
'type' => Platform::TYPE_WEB,
'hostname' => 'localhost',
], // Current host is added on app init
],
@@ -39,7 +39,8 @@ $console = [
'invites' => System::getEnv('_APP_CONSOLE_INVITES', 'enabled') === 'enabled',
'limit' => (System::getEnv('_APP_CONSOLE_WHITELIST_ROOT', 'enabled') === 'enabled') ? 1 : 0, // limit signup to 1 user
'duration' => Auth::TOKEN_EXPIRATION_LOGIN_LONG, // 1 Year in seconds
'sessionAlerts' => System::getEnv('_APP_CONSOLE_SESSION_ALERTS', 'disabled') === 'enabled'
'sessionAlerts' => System::getEnv('_APP_CONSOLE_SESSION_ALERTS', 'disabled') === 'enabled',
'invalidateSessions' => true
],
'authWhitelistEmails' => (!empty(System::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null))) ? \explode(',', System::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null)) : [],
'authWhitelistIPs' => (!empty(System::getEnv('_APP_CONSOLE_WHITELIST_IPS', null))) ? \explode(',', System::getEnv('_APP_CONSOLE_WHITELIST_IPS', null)) : [],
+146 -6
View File
@@ -69,9 +69,14 @@ return [
'description' => 'The request contains one or more invalid arguments. Please refer to the endpoint documentation.',
'code' => 400,
],
Exception::GENERAL_QUERY_LIMIT_EXCEEDED => [
'name' => Exception::GENERAL_QUERY_LIMIT_EXCEEDED,
'description' => 'Query limit exceeded for the current attribute. Usage of more than 100 query values on a single attribute is prohibited.',
Exception::GENERAL_ATTRIBUTE_QUERY_LIMIT_EXCEEDED => [
'name' => Exception::GENERAL_ATTRIBUTE_QUERY_LIMIT_EXCEEDED,
'description' => 'Query limit exceeded for the current attribute.',
'code' => 400,
],
Exception::GENERAL_COLUMN_QUERY_LIMIT_EXCEEDED => [
'name' => Exception::GENERAL_COLUMN_QUERY_LIMIT_EXCEEDED,
'description' => 'Query limit exceeded for the current column.',
'code' => 400,
],
Exception::GENERAL_QUERY_INVALID => [
@@ -668,7 +673,7 @@ return [
],
Exception::DATABASE_QUERY_ORDER_NULL => [
'name' => Exception::DATABASE_QUERY_ORDER_NULL,
'description' => 'The order attribute had a null value. Cursor pagination requires all documents order attribute values are non-null.',
'description' => 'The order attribute/column had a null value. Cursor pagination requires all documents/rows order attribute/column values are non-null.',
'code' => 400,
],
@@ -689,6 +694,23 @@ return [
'code' => 400,
],
/** Tables */
Exception::TABLE_NOT_FOUND => [
'name' => Exception::TABLE_NOT_FOUND,
'description' => 'Table with the requested ID could not be found.',
'code' => 404,
],
Exception::TABLE_ALREADY_EXISTS => [
'name' => Exception::TABLE_ALREADY_EXISTS,
'description' => 'A table with the requested ID already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
'code' => 409,
],
Exception::TABLE_LIMIT_EXCEEDED => [
'name' => Exception::TABLE_LIMIT_EXCEEDED,
'description' => 'The maximum number of tables has been reached.',
'code' => 400,
],
/** Documents */
Exception::DOCUMENT_NOT_FOUND => [
'name' => Exception::DOCUMENT_NOT_FOUND,
@@ -726,6 +748,43 @@ return [
'code' => 403,
],
/** Rows */
Exception::ROW_NOT_FOUND => [
'name' => Exception::ROW_NOT_FOUND,
'description' => 'Row with the requested ID could not be found.',
'code' => 404,
],
Exception::ROW_INVALID_STRUCTURE => [
'name' => Exception::ROW_INVALID_STRUCTURE,
'description' => 'The row structure is invalid. Please ensure the columns match the table definition.',
'code' => 400,
],
Exception::ROW_MISSING_DATA => [
'name' => Exception::ROW_MISSING_DATA,
'description' => 'The row data is missing. Try again with row data populated',
'code' => 400,
],
Exception::ROW_MISSING_PAYLOAD => [
'name' => Exception::ROW_MISSING_PAYLOAD,
'description' => 'The row data and permissions are missing. You must provide either row data or permissions to be updated.',
'code' => 400,
],
Exception::ROW_ALREADY_EXISTS => [
'name' => Exception::ROW_ALREADY_EXISTS,
'description' => 'Row with the requested ID already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
'code' => 409,
],
Exception::ROW_UPDATE_CONFLICT => [
'name' => Exception::ROW_UPDATE_CONFLICT,
'description' => 'Remote row is newer than local.',
'code' => 409,
],
Exception::ROW_DELETE_RESTRICTED => [
'name' => Exception::ROW_DELETE_RESTRICTED,
'description' => 'Row cannot be deleted because it is referenced by another row.',
'code' => 403,
],
/** Attributes */
Exception::ATTRIBUTE_NOT_FOUND => [
'name' => Exception::ATTRIBUTE_NOT_FOUND,
@@ -772,13 +831,67 @@ return [
'description' => 'The attribute type is invalid.',
'code' => 400,
],
Exception::ATTRIBUTE_INVALID_RESIZE => [
'name' => Exception::ATTRIBUTE_INVALID_RESIZE,
'description' => 'Existing data is too large for new size, truncate your existing data then try again.',
'code' => 400,
],
/** Exists for both Attributes & Columns */
Exception::RELATIONSHIP_VALUE_INVALID => [
'name' => Exception::RELATIONSHIP_VALUE_INVALID,
'description' => 'The relationship value is invalid.',
'code' => 400,
],
Exception::ATTRIBUTE_INVALID_RESIZE => [
'name' => Exception::ATTRIBUTE_INVALID_RESIZE,
/** Columns */
Exception::COLUMN_NOT_FOUND => [
'name' => Exception::COLUMN_NOT_FOUND,
'description' => 'Column with the requested ID could not be found.',
'code' => 404,
],
Exception::COLUMN_UNKNOWN => [
'name' => Exception::COLUMN_UNKNOWN,
'description' => 'The column required for the index could not be found. Please confirm all your columns are in the available state.',
'code' => 400,
],
Exception::COLUMN_NOT_AVAILABLE => [
'name' => Exception::COLUMN_NOT_AVAILABLE,
'description' => 'The requested column is not yet available. Please try again later.',
'code' => 400,
],
Exception::COLUMN_FORMAT_UNSUPPORTED => [
'name' => Exception::COLUMN_FORMAT_UNSUPPORTED,
'description' => 'The requested column format is not supported.',
'code' => 400,
],
Exception::COLUMN_DEFAULT_UNSUPPORTED => [
'name' => Exception::COLUMN_DEFAULT_UNSUPPORTED,
'description' => 'Default values cannot be set for array or required columns.',
'code' => 400,
],
Exception::COLUMN_ALREADY_EXISTS => [
'name' => Exception::COLUMN_ALREADY_EXISTS,
'description' => 'Column with the requested key already exists. Column keys must be unique, try again with a different key.',
'code' => 409,
],
Exception::COLUMN_LIMIT_EXCEEDED => [
'name' => Exception::COLUMN_LIMIT_EXCEEDED,
'description' => 'The maximum number or size of columns for this table has been reached.',
'code' => 400,
],
Exception::COLUMN_VALUE_INVALID => [
'name' => Exception::COLUMN_VALUE_INVALID,
'description' => 'The column value is invalid. Please check the type, range and value of the column.',
'code' => 400,
],
Exception::COLUMN_TYPE_INVALID => [
'name' => Exception::COLUMN_TYPE_INVALID,
'description' => 'The column type is invalid.',
'code' => 400,
],
Exception::COLUMN_INVALID_RESIZE => [
'name' => Exception::COLUMN_INVALID_RESIZE,
'description' => "Existing data is too large for new size, truncate your existing data then try again.",
'code' => 400,
],
@@ -810,6 +923,33 @@ return [
'code' => 409,
],
/** Column Indexes, same as Indexes but with different type */
Exception::COLUMN_INDEX_NOT_FOUND => [
'name' => Exception::COLUMN_INDEX_NOT_FOUND,
'description' => 'Index with the requested ID could not be found.',
'code' => 404,
],
Exception::COLUMN_INDEX_LIMIT_EXCEEDED => [
'name' => Exception::COLUMN_INDEX_LIMIT_EXCEEDED,
'description' => 'The maximum number of indexes has been reached.',
'code' => 400,
],
Exception::COLUMN_INDEX_ALREADY_EXISTS => [
'name' => Exception::COLUMN_INDEX_ALREADY_EXISTS,
'description' => 'Index with the requested key already exists. Try again with a different key.',
'code' => 409,
],
Exception::COLUMN_INDEX_INVALID => [
'name' => Exception::COLUMN_INDEX_INVALID,
'description' => 'Index invalid.',
'code' => 400,
],
Exception::COLUMN_INDEX_DEPENDENCY => [
'name' => Exception::COLUMN_INDEX_DEPENDENCY,
'description' => 'Column cannot be renamed or deleted. Please remove the associated index first.',
'code' => 409,
],
/** Project Errors */
Exception::PROJECT_NOT_FOUND => [
'name' => Exception::PROJECT_NOT_FOUND,
+50
View File
@@ -95,6 +95,56 @@ return [
'$model' => Response::MODEL_DATABASE,
'$resource' => true,
'$description' => 'This event triggers on any database event.',
'tables' => [
'$model' => Response::MODEL_TABLE,
'$resource' => true,
'$description' => 'This event triggers on any table event.',
'rows' => [
'$model' => Response::MODEL_ROW,
'$resource' => true,
'$description' => 'This event triggers on any rows event.',
'create' => [
'$description' => 'This event triggers when a row is created.',
],
'delete' => [
'$description' => 'This event triggers when a row is deleted.'
],
'update' => [
'$description' => 'This event triggers when a row is updated.'
],
],
'indexes' => [
'$model' => Response::MODEL_COLUMN_INDEX,
'$resource' => true,
'$description' => 'This event triggers on any indexes event.',
'create' => [
'$description' => 'This event triggers when an index is created.',
],
'delete' => [
'$description' => 'This event triggers when an index is deleted.'
]
],
'columns' => [
'$model' => Response::MODEL_COLUMN,
'$resource' => true,
'$description' => 'This event triggers on any columns event.',
'create' => [
'$description' => 'This event triggers when a column is created.',
],
'delete' => [
'$description' => 'This event triggers when an column is deleted.'
]
],
'create' => [
'$description' => 'This event triggers when a table is created.'
],
'delete' => [
'$description' => 'This event triggers when a table is deleted.',
],
'update' => [
'$description' => 'This event triggers when a table is updated.',
]
],
'collections' => [
'$model' => Response::MODEL_COLLECTION,
'$resource' => true,
+1 -1
View File
@@ -217,7 +217,7 @@ return [
[
'key' => 'cli',
'name' => 'Command Line',
'version' => '8.0.2',
'version' => '8.1.0',
'url' => 'https://github.com/appwrite/sdk-for-cli',
'package' => 'https://www.npmjs.com/package/appwrite-cli',
'enabled' => true,
+8
View File
@@ -14,6 +14,8 @@ $member = [
'teams.write',
'documents.read',
'documents.write',
'rows.read',
'rows.write',
'files.read',
'files.write',
'projects.read',
@@ -37,6 +39,8 @@ $admins = [
'teams.write',
'documents.read',
'documents.write',
'rows.read',
'rows.write',
'files.read',
'files.write',
'buckets.read',
@@ -47,6 +51,8 @@ $admins = [
'databases.write',
'collections.read',
'collections.write',
'tables.read',
'tables.write',
'platforms.read',
'platforms.write',
'projects.write',
@@ -97,6 +103,8 @@ return [
'sessions.write',
'documents.read',
'documents.write',
'rows.read',
'rows.write',
'files.read',
'files.write',
'locale.read',
+18
View File
@@ -28,12 +28,24 @@ return [ // List of publicly visible scopes
'collections.write' => [
'description' => 'Access to create, update, and delete your project\'s database collections',
],
'tables.read' => [
'description' => 'Access to read your project\'s database tables',
],
'tables.write' => [
'description' => 'Access to create, update, and delete your project\'s database tables',
],
'attributes.read' => [
'description' => 'Access to read your project\'s database collection\'s attributes',
],
'attributes.write' => [
'description' => 'Access to create, update, and delete your project\'s database collection\'s attributes',
],
'columns.read' => [
'description' => 'Access to read your project\'s database table\'s columns',
],
'columns.write' => [
'description' => 'Access to create, update, and delete your project\'s database table\'s columns',
],
'indexes.read' => [
'description' => 'Access to read your project\'s database collection\'s indexes',
],
@@ -46,6 +58,12 @@ return [ // List of publicly visible scopes
'documents.write' => [
'description' => 'Access to create, update, and delete your project\'s database documents',
],
'rows.read' => [
'description' => 'Access to read your project\'s database rows',
],
'rows.write' => [
'description' => 'Access to create, update, and delete your project\'s database rows',
],
'files.read' => [
'description' => 'Access to read your project\'s storage files and preview images',
],
+1 -1
View File
@@ -58,7 +58,7 @@ return [
'name' => 'Databases',
'subtitle' => 'The Databases service allows you to create structured collections of documents, query and filter lists of documents',
'description' => '/docs/services/databases.md',
'controller' => 'api/databases.php',
'controller' => '', // Uses modules
'sdk' => true,
'docs' => true,
'docsUrl' => 'https://appwrite.io/docs/client/databases',
+13 -10
View File
@@ -635,7 +635,7 @@ return [
'type' => 'url'
]
],
'scopes' => ['databases.read', 'databases.write', 'collections.write', 'attributes.write', 'documents.read', 'documents.write']
'scopes' => ['databases.read', 'databases.write', 'collections.write', 'tables.write', 'attributes.write', 'columns.write', 'documents.read', 'rows.read', 'documents.write', 'rows.write']
],
[
'icon' => 'icon-algolia',
@@ -717,7 +717,7 @@ return [
'type' => 'password'
],
],
'scopes' => ['databases.read', 'collections.read', 'documents.read']
'scopes' => ['databases.read', 'collections.read', 'tables.read', 'documents.read', 'rows.read']
],
[
'icon' => 'icon-meilisearch',
@@ -811,7 +811,7 @@ return [
'type' => 'text'
],
],
'scopes' => ['databases.read', 'collections.read', 'documents.read']
'scopes' => ['databases.read', 'collections.read', 'tables.read', 'documents.read', 'rows.read']
],
[
'icon' => 'icon-vonage',
@@ -1139,7 +1139,7 @@ return [
'type' => 'text'
]
],
'scopes' => ['databases.read', 'databases.write', 'collections.write', 'attributes.write', 'documents.read', 'documents.write']
'scopes' => ['databases.read', 'databases.write', 'collections.write', 'tables.write', 'attributes.write', 'columns.write', 'documents.read', 'rows.read', 'documents.write', 'rows.write']
],
[
'icon' => 'icon-chat',
@@ -1268,7 +1268,7 @@ return [
'type' => 'password'
]
],
'scopes' => ['databases.read', 'databases.write', 'collections.read', 'collections.write', 'attributes.write', 'documents.read', 'documents.write', 'buckets.read', 'buckets.write', 'files.read']
'scopes' => ['databases.read', 'databases.write', 'collections.read', 'tables.read', 'collections.write', 'tables.write', 'attributes.write', 'columns.write', 'documents.read', 'rows.read', 'documents.write', 'rows.write', 'buckets.read', 'buckets.write', 'files.read']
],
[
'icon' => 'icon-eye',
@@ -1327,7 +1327,7 @@ return [
'type' => 'password'
]
],
'scopes' => ['databases.read', 'databases.write', 'collections.read', 'collections.write', 'attributes.write', 'documents.read', 'documents.write', 'buckets.read', 'buckets.write', 'files.read']
'scopes' => ['databases.read', 'databases.write', 'collections.read', 'tables.read', 'collections.write', 'tables.write', 'attributes.write', 'columns.write', 'documents.read', 'rows.read', 'documents.write', 'rows.write', 'buckets.read', 'buckets.write', 'files.read']
],
[
'icon' => 'icon-text',
@@ -1386,7 +1386,7 @@ return [
'type' => 'password'
]
],
'scopes' => ['databases.read', 'databases.write', 'collections.read', 'collections.write', 'attributes.write', 'documents.read', 'documents.write', 'buckets.read', 'buckets.write', 'files.read']
'scopes' => ['databases.read', 'databases.write', 'collections.read', 'tables.read', 'collections.write', 'tables.write', 'attributes.write', 'columns.write', 'documents.read', 'rows.read', 'documents.write', 'rows.write', 'buckets.read', 'buckets.write', 'files.read']
],
[
'icon' => 'icon-chat',
@@ -1395,7 +1395,10 @@ return [
'score' => 5,
'tagline' => 'Convert text to speech using the Hugging Face inference API.',
'permissions' => ['any'],
'events' => ['databases.*.collections.*.documents.*.create'],
'events' => [
'databases.*.tables.*.rows.*.create',
'databases.*.collections.*.documents.*.create',
],
'cron' => '',
'timeout' => 15,
'useCases' => ['ai'],
@@ -1666,7 +1669,7 @@ return [
'type' => 'text'
]
],
'scopes' => ['databases.read', 'collections.read', 'documents.read']
'scopes' => ['databases.read', 'collections.read', 'tables.read', 'documents.read', 'rows.read']
],
[
'icon' => 'icon-chip',
@@ -1730,7 +1733,7 @@ return [
'type' => 'text'
]
],
'scopes' => ['databases.read', 'collections.read', 'documents.read']
'scopes' => ['databases.read', 'collections.read', 'tables.read', 'documents.read', 'rows.read']
],
[
'icon' => 'icon-chat',
+2 -7
View File
@@ -6,13 +6,8 @@ use Utopia\System\System;
* List of Appwrite Sites templates
*/
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
$hostname = System::getEnv('_APP_DOMAIN');
// TODO: Development override
if (System::getEnv('_APP_ENV') === 'development') {
$hostname = 'localhost';
}
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$hostname = System::getEnv('_APP_DOMAIN', '');
$url = $protocol . '://' . $hostname;
+20 -9
View File
@@ -21,6 +21,7 @@ use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception;
use Appwrite\Hooks\Hooks;
use Appwrite\Network\Validator\Email;
use Appwrite\Network\Validator\Redirect;
use Appwrite\OpenSSL\OpenSSL;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@@ -60,7 +61,6 @@ use Utopia\System\System;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Assoc;
use Utopia\Validator\Boolean;
use Utopia\Validator\Host;
use Utopia\Validator\Text;
use Utopia\Validator\URL;
use Utopia\Validator\WhiteList;
@@ -1182,8 +1182,8 @@ App::get('/v1/account/sessions/oauth2/:provider')
->label('abuse-limit', 50)
->label('abuse-key', 'ip:{ip}')
->param('provider', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders')), true), 'OAuth2 Provider. Currently, supported providers are: ' . \implode(', ', \array_keys(\array_filter(Config::getParam('oAuthProviders'), fn ($node) => (!$node['mock'])))) . '.')
->param('success', '', fn ($clients, $devKey) => $devKey->isEmpty() ? new Host($clients) : new URL(), 'URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['clients', 'devKey'])
->param('failure', '', fn ($clients, $devKey) => $devKey->isEmpty() ? new Host($clients) : new URL(), 'URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['clients', 'devKey'])
->param('success', '', fn ($platforms, $devKey) => $devKey->isEmpty() ? new Redirect($platforms) : new URL(), 'URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['platforms', 'devKey'])
->param('failure', '', fn ($platforms, $devKey) => $devKey->isEmpty() ? new Redirect($platforms) : new URL(), 'URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['platforms', 'devKey'])
->param('scopes', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true)
->inject('request')
->inject('response')
@@ -1282,7 +1282,6 @@ App::post('/v1/account/sessions/oauth2/callback/:provider/:projectId')
->inject('request')
->inject('response')
->action(function (string $projectId, string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response) {
$domain = $request->getHostname();
$protocol = $request->getProtocol();
@@ -1779,8 +1778,8 @@ App::get('/v1/account/tokens/oauth2/:provider')
->label('abuse-limit', 50)
->label('abuse-key', 'ip:{ip}')
->param('provider', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders')), true), 'OAuth2 Provider. Currently, supported providers are: ' . \implode(', ', \array_keys(\array_filter(Config::getParam('oAuthProviders'), fn ($node) => (!$node['mock'])))) . '.')
->param('success', '', fn ($clients, $devKey) => $devKey->isEmpty() ? new Host($clients) : new URL(), 'URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['clients', 'devKey'])
->param('failure', '', fn ($clients, $devKey) => $devKey->isEmpty() ? new Host($clients) : new URL(), 'URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['clients', 'devKey'])
->param('success', '', fn ($platforms, $devKey) => $devKey->isEmpty() ? new Redirect($platforms) : new URL(), 'URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['platforms', 'devKey'])
->param('failure', '', fn ($platforms, $devKey) => $devKey->isEmpty() ? new Redirect($platforms) : new URL(), 'URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['platforms', 'devKey'])
->param('scopes', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true)
->inject('request')
->inject('response')
@@ -1860,7 +1859,7 @@ App::post('/v1/account/tokens/magic-url')
->label('abuse-key', ['url:{url},email:{param-email}', 'url:{url},ip:{ip}'])
->param('userId', '', new CustomId(), 'Unique 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.')
->param('email', '', new Email(), 'User email.')
->param('url', '', fn ($clients, $devKey) => $devKey->isEmpty() ? new Host($clients) : new URL(), 'URL to redirect the user back to your app from the magic URL login. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['clients', 'devKey'])
->param('url', '', fn ($platforms, $devKey) => $devKey->isEmpty() ? new Redirect($platforms) : new URL(), 'URL to redirect the user back to your app from the magic URL login. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['platforms', 'devKey'])
->param('phrase', false, new Boolean(), 'Toggle for security phrase. If enabled, email will be send with a randomly generated phrase and the phrase will also be included in the response. Confirming phrases match increases the security of your authentication flow.', true)
->inject('request')
->inject('response')
@@ -2843,6 +2842,18 @@ App::patch('/v1/account/password')
->setAttribute('hash', Auth::DEFAULT_ALGO)
->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS);
$sessions = $user->getAttribute('sessions', []);
$current = Auth::sessionVerify($sessions, Auth::$secret);
$invalidate = $project->getAttribute('auths', default: [])['invalidateSessions'] ?? false;
if ($invalidate && !empty($current)) {
foreach ($sessions as $session) {
/** @var Document $session */
if ($session->getId() !== $current) {
$dbForProject->deleteDocument('sessions', $session->getId());
}
}
}
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
$queueForEvents->setParam('userId', $user->getId());
@@ -3146,7 +3157,7 @@ App::post('/v1/account/recovery')
->label('abuse-limit', 10)
->label('abuse-key', ['url:{url},email:{param-email}', 'url:{url},ip:{ip}'])
->param('email', '', new Email(), 'User email.')
->param('url', '', fn ($clients, $devKey) => $devKey->isEmpty() ? new Host($clients) : new URL(), 'URL to redirect the user back to your app from the recovery email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', false, ['clients', 'devKey'])
->param('url', '', fn ($platforms, $devKey) => $devKey->isEmpty() ? new Redirect($platforms) : new URL(), 'URL to redirect the user back to your app from the recovery email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', false, ['platforms', 'devKey'])
->inject('request')
->inject('response')
->inject('user')
@@ -3413,7 +3424,7 @@ App::post('/v1/account/verification')
))
->label('abuse-limit', 10)
->label('abuse-key', 'url:{url},userId:{userId}')
->param('url', '', fn ($clients, $devKey) => $devKey->isEmpty() ? new Host($clients) : new URL(), 'URL to redirect the user back to your app from the verification email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', false, ['clients', 'devKey']) // TODO add built-in confirm page
->param('url', '', fn ($platforms, $devKey) => $devKey->isEmpty() ? new Redirect($platforms) : new URL(), 'URL to redirect the user back to your app from the verification email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', false, ['platforms', 'devKey']) // TODO add built-in confirm page
->inject('request')
->inject('response')
->inject('project')
File diff suppressed because it is too large Load Diff
+14 -4
View File
@@ -523,11 +523,16 @@ App::get('/v1/health/queue/databases')
->param('name', 'database_db_main', new Text(256), 'Queue name for which to check the queue size', true)
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
->inject('publisher')
->inject('publisherRedis')
->inject('response')
->action(function (string $name, int|string $threshold, Publisher $publisher, Response $response) {
->action(function (string $name, int|string $threshold, Publisher $publisher, ?Publisher $publisherRedis, Response $response) {
$threshold = \intval($threshold);
$size = $publisher->getQueueSize(new Queue($name));
$isRedisFallback = \str_contains(System::getEnv('_APP_WORKER_REDIS_FALLBACK', ''), 'databases');
$size = $isRedisFallback
? $publisherRedis->getQueueSize(new Queue($name))
: $publisher->getQueueSize(new Queue($name));
if ($size >= $threshold) {
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
@@ -655,11 +660,16 @@ App::get('/v1/health/queue/migrations')
))
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
->inject('publisher')
->inject('publisherRedis')
->inject('response')
->action(function (int|string $threshold, Publisher $publisher, Response $response) {
->action(function (int|string $threshold, Publisher $publisher, ?Publisher $publisherRedis, Response $response) {
$threshold = \intval($threshold);
$size = $publisher->getQueueSize(new Queue(Event::MIGRATIONS_QUEUE_NAME));
$isRedisFallback = \str_contains(System::getEnv('_APP_WORKER_REDIS_FALLBACK', ''), 'migrations');
$size = $isRedisFallback
? $publisherRedis->getQueueSize(new Queue(Event::MIGRATIONS_QUEUE_NAME))
: $publisher->getQueueSize(new Queue(Event::MIGRATIONS_QUEUE_NAME));
if ($size >= $threshold) {
throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}.");
+3 -3
View File
@@ -361,7 +361,7 @@ App::post('/v1/migrations/csv')
$hasCompression = $compression !== Compression::NONE;
$migrationId = ID::unique();
$newPath = $deviceForImports->getPath('/' . $migrationId . '_' . $fileId . '.csv');
$newPath = $deviceForImports->getPath($migrationId . '_' . $fileId . '.csv');
if ($hasEncryption || $hasCompression) {
$source = $deviceForFiles->read($path);
@@ -410,8 +410,8 @@ App::post('/v1/migrations/csv')
'resources' => $resources,
'resourceId' => $resourceId,
'resourceType' => Resource::TYPE_DATABASE,
'statusCounters' => [],
'resourceData' => [],
'statusCounters' => '{}',
'resourceData' => '{}',
'errors' => [],
'options' => [
'path' => $newPath,
+1
View File
@@ -354,6 +354,7 @@ App::get('/v1/project/usage')
'executionsMbSecondsTotal' => $total[METRIC_EXECUTIONS_MB_SECONDS],
'buildsMbSecondsTotal' => $total[METRIC_BUILDS_MB_SECONDS],
'documentsTotal' => $total[METRIC_DOCUMENTS],
'rowsTotal' => $total[METRIC_DOCUMENTS],
'databasesTotal' => $total[METRIC_DATABASES],
'databasesStorageTotal' => $total[METRIC_DATABASES_STORAGE],
'usersTotal' => $total[METRIC_USERS],
+40 -2
View File
@@ -8,8 +8,8 @@ use Appwrite\Event\Mail;
use Appwrite\Event\Validator\Event;
use Appwrite\Extend\Exception;
use Appwrite\Hooks\Hooks;
use Appwrite\Network\Platform;
use Appwrite\Network\Validator\Email;
use Appwrite\Network\Validator\Origin;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
@@ -127,6 +127,7 @@ App::post('/v1/projects')
'membershipsUserName' => false,
'membershipsUserEmail' => false,
'membershipsMfa' => false,
'invalidateSessions' => true
];
foreach ($auth as $method) {
@@ -1790,7 +1791,7 @@ App::post('/v1/projects/:projectId/platforms')
]
))
->param('projectId', '', new UID(), 'Project unique ID.')
->param('type', null, new WhiteList([Origin::CLIENT_TYPE_WEB, Origin::CLIENT_TYPE_FLUTTER_WEB, Origin::CLIENT_TYPE_FLUTTER_IOS, Origin::CLIENT_TYPE_FLUTTER_ANDROID, Origin::CLIENT_TYPE_FLUTTER_LINUX, Origin::CLIENT_TYPE_FLUTTER_MACOS, Origin::CLIENT_TYPE_FLUTTER_WINDOWS, Origin::CLIENT_TYPE_APPLE_IOS, Origin::CLIENT_TYPE_APPLE_MACOS, Origin::CLIENT_TYPE_APPLE_WATCHOS, Origin::CLIENT_TYPE_APPLE_TVOS, Origin::CLIENT_TYPE_ANDROID, Origin::CLIENT_TYPE_UNITY, Origin::CLIENT_TYPE_REACT_NATIVE_IOS, Origin::CLIENT_TYPE_REACT_NATIVE_ANDROID], true), 'Platform type.')
->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.')
->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)
@@ -2500,3 +2501,40 @@ App::delete('/v1/projects/:projectId/templates/email/:type/:locale')
'message' => $template['message']
]), Response::MODEL_EMAIL_TEMPLATE);
});
App::patch('/v1/projects/:projectId/auth/session-invalidation')
->desc('Update invalidate session option of the project')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'auth',
name: 'updateSessionInvalidation',
description: '/docs/references/projects/update-session-invalidation.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROJECT,
)
]
))
->param('projectId', '', new UID(), 'Project unique ID.')
->param('enabled', false, new Boolean(), 'Update authentication session invalidation status. Use this endpoint to enable or disable session invalidation on password change')
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, bool $invalidateSessions, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$auths = $project->getAttribute('auths', []);
$auths['invalidateSessions'] = $invalidateSessions;
$dbForPlatform->updateDocument('projects', $project->getId(), $project
->setAttribute('auths', $auths));
$response->dynamic($project, Response::MODEL_PROJECT);
});
+18 -1
View File
@@ -18,6 +18,7 @@ use Appwrite\Utopia\Database\Validator\Queries\Buckets;
use Appwrite\Utopia\Database\Validator\Queries\Files;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
@@ -953,12 +954,14 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
->param('output', '', new WhiteList(\array_keys(Config::getParam('storage-outputs')), true), 'Output format type (jpeg, jpg, png, gif and webp).', true)
// NOTE: this is only for the sdk generator and is not used in the action below and is utilised in `resources.php` for `resourceToken`.
->param('token', '', new Text(512), 'File token for accessing this file.', true)
->inject('request')
->inject('response')
->inject('dbForProject')
->inject('resourceToken')
->inject('deviceForFiles')
->inject('deviceForLocal')
->action(function (string $bucketId, string $fileId, int $width, int $height, string $gravity, int $quality, int $borderWidth, string $borderColor, int $borderRadius, float $opacity, int $rotation, string $background, string $output, ?string $token, Response $response, Database $dbForProject, Document $resourceToken, Device $deviceForFiles, Device $deviceForLocal) {
->inject('project')
->action(function (string $bucketId, string $fileId, int $width, int $height, string $gravity, int $quality, int $borderWidth, string $borderColor, int $borderRadius, float $opacity, int $rotation, string $background, string $output, ?string $token, Request $request, Response $response, Database $dbForProject, Document $resourceToken, Device $deviceForFiles, Device $deviceForLocal, Document $project) {
if (!\extension_loaded('imagick')) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing');
@@ -1035,8 +1038,12 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
$output = empty($type) ? (array_search($mime, $outputs) ?? 'jpg') : $type;
}
$startTime = \microtime(true);
$source = $deviceForFiles->read($path);
$downloadTime = \microtime(true) - $startTime;
if (!empty($cipher)) { // Decrypt
$source = OpenSSL::decrypt(
$source,
@@ -1048,6 +1055,8 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
);
}
$decryptionTime = \microtime(true) - $startTime - $downloadTime;
switch ($algorithm) {
case Compression::ZSTD:
$compressor = new Zstd();
@@ -1059,6 +1068,8 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
break;
}
$decompressionTime = \microtime(true) - $startTime - $downloadTime - $decryptionTime;
try {
$image = new Image($source);
} catch (ImagickException $e) {
@@ -1089,6 +1100,12 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
$data = $image->output($output, $quality);
$renderingTime = \microtime(true) - $startTime - $downloadTime - $decryptionTime - $decompressionTime;
$totalTime = \microtime(true) - $startTime;
Console::info("File preview rendered,project=" . $project->getId() . ",bucket=" . $bucketId . ",file=" . $file->getId() . ",uri=" . $request->getURI() . ",total=" . $totalTime . ",rendering=" . $renderingTime . ",decryption=" . $decryptionTime . ",decompression=" . $decompressionTime . ",download=" . $downloadTime);
$contentType = (\array_key_exists($output, $outputs)) ? $outputs[$output] : $outputs['jpg'];
//Do not update transformedAt if it's a console user
+2 -2
View File
@@ -11,6 +11,7 @@ use Appwrite\Event\Messaging;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception;
use Appwrite\Network\Validator\Email;
use Appwrite\Network\Validator\Redirect;
use Appwrite\Platform\Workers\Deletes;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@@ -50,7 +51,6 @@ use Utopia\Locale\Locale;
use Utopia\System\System;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Assoc;
use Utopia\Validator\Host;
use Utopia\Validator\Text;
use Utopia\Validator\URL;
use Utopia\Validator\WhiteList;
@@ -466,7 +466,7 @@ App::post('/v1/teams/:teamId/memberships')
}
return new ArrayList(new Key(), APP_LIMIT_ARRAY_PARAMS_SIZE);
}, 'Array of strings. Use this param to set the user roles in the team. A role can be any string. Learn more about [roles and permissions](https://appwrite.io/docs/permissions). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 32 characters long.', false, ['project'])
->param('url', '', fn ($clients, $devKey) => $devKey->isEmpty() ? new Host($clients) : new URL(), 'URL to redirect the user back to your app from the invitation email. This parameter is not required when an API key is supplied. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['clients', 'devKey']) // TODO add our own built-in confirm page
->param('url', '', fn ($platforms, $devKey) => $devKey->isEmpty() ? new Redirect($platforms) : new URL(), 'URL to redirect the user back to your app from the invitation email. This parameter is not required when an API key is supplied. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['platforms', 'devKey']) // TODO add our own built-in confirm page
->param('name', '', new Text(128), 'Name of the new team member. Max length: 128 chars.', true)
->inject('response')
->inject('project')
+13
View File
@@ -649,6 +649,8 @@ App::get('/v1/users')
$total = $dbForProject->count('users', $filterQueries, APP_LIMIT_COUNT);
} catch (OrderException $e) {
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$response->dynamic(new Document([
'users' => $users,
@@ -1351,6 +1353,17 @@ App::patch('/v1/users/:userId/password')
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
$sessions = $user->getAttribute('sessions', []);
$invalidate = $project->getAttribute('auths', default: [])['invalidateSessions'] ?? false;
if ($invalidate) {
foreach ($sessions as $session) {
/** @var Document $session */
$dbForProject->deleteDocument('sessions', $session->getId());
}
}
$dbForProject->purgeCachedDocument('users', $user->getId());
$queueForEvents->setParam('userId', $user->getId());
$response->dynamic($user, Response::MODEL_USER);
+14 -8
View File
@@ -4,6 +4,7 @@ use Appwrite\Auth\OAuth2\Github as OAuth2Github;
use Appwrite\Event\Build;
use Appwrite\Event\Delete;
use Appwrite\Extend\Exception;
use Appwrite\Network\Validator\Redirect;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
@@ -53,7 +54,6 @@ use Utopia\Detector\Detector\Runtime;
use Utopia\Detector\Detector\Strategy;
use Utopia\System\System;
use Utopia\Validator\Boolean;
use Utopia\Validator\Host;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
use Utopia\VCS\Adapter\Git\GitHub;
@@ -116,8 +116,10 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
}
$commentStatus = $isAuthorized ? 'waiting' : 'failed';
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$hostname = System::getEnv('_APP_CONSOLE_DOMAIN', System::getEnv('_APP_DOMAIN', ''));
$authorizeUrl = $request->getProtocol() . '://' . $request->getHostname() . "/console/git/authorize-contributor?projectId={$projectId}&installationId={$installationId}&repositoryId={$repositoryId}&providerPullRequestId={$providerPullRequestId}";
$authorizeUrl = $protocol . '://' . $hostname . "/console/git/authorize-contributor?projectId={$projectId}&installationId={$installationId}&repositoryId={$repositoryId}&providerPullRequestId={$providerPullRequestId}";
$action = $isAuthorized ? ['type' => 'logs'] : ['type' => 'authorize', 'url' => $authorizeUrl];
@@ -378,7 +380,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
}
$owner = $github->getOwnerName($providerInstallationId);
$providerTargetUrl = $request->getProtocol() . '://' . $request->getHostname() . "/console/project-$region-$projectId/$resourceCollection/$resourceType-$resourceId";
$providerTargetUrl = $protocol . '://' . $hostname . "/console/project-$region-$projectId/$resourceCollection/$resourceType-$resourceId";
$github->updateCommitStatus($repositoryName, $providerCommitHash, $owner, 'pending', $message, $providerTargetUrl, $name);
}
@@ -424,8 +426,8 @@ App::get('/v1/vcs/github/authorize')
type: MethodType::WEBAUTH,
hide: true,
))
->param('success', '', fn ($clients) => new Host($clients), 'URL to redirect back to console after a successful installation attempt.', true, ['clients'])
->param('failure', '', fn ($clients) => new Host($clients), 'URL to redirect back to console after a failed installation attempt.', true, ['clients'])
->param('success', '', fn ($platforms) => new Redirect($platforms), 'URL to redirect back to console after a successful installation attempt.', true, ['platforms'])
->param('failure', '', fn ($platforms) => new Redirect($platforms), 'URL to redirect back to console after a failed installation attempt.', true, ['platforms'])
->inject('request')
->inject('response')
->inject('project')
@@ -437,6 +439,8 @@ App::get('/v1/vcs/github/authorize')
]);
$appName = System::getEnv('_APP_VCS_GITHUB_APP_NAME');
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$hostname = System::getEnv('_APP_CONSOLE_DOMAIN', System::getEnv('_APP_DOMAIN', ''));
if (empty($appName)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'GitHub App name is not configured. Please configure VCS (Version Control System) variables in .env file.');
@@ -444,7 +448,7 @@ App::get('/v1/vcs/github/authorize')
$url = "https://github.com/apps/$appName/installations/new?" . \http_build_query([
'state' => $state,
'redirect_uri' => $request->getProtocol() . '://' . $request->getHostname() . "/v1/vcs/github/callback"
'redirect_uri' => $protocol . '://' . $hostname . "/v1/vcs/github/callback"
]);
$response
@@ -494,10 +498,12 @@ App::get('/v1/vcs/github/callback')
}
$region = $project->getAttribute('region', 'default');
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$hostname = System::getEnv('_APP_CONSOLE_DOMAIN', System::getEnv('_APP_DOMAIN', ''));
$defaultState = [
'success' => $request->getProtocol() . '://' . $request->getHostname() . "/console/project-$region-$projectId/settings/git-installations",
'failure' => $request->getProtocol() . '://' . $request->getHostname() . "/console/project-$region-$projectId/settings/git-installations",
'success' => $protocol . '://' . $hostname . "/console/project-$region-$projectId/settings/git-installations",
'failure' => $protocol . '://' . $hostname . "/console/project-$region-$projectId/settings/git-installations",
];
$state = \array_merge($defaultState, $state ?? []);
+46 -53
View File
@@ -22,11 +22,13 @@ use Appwrite\Utopia\Request\Filters\V16 as RequestV16;
use Appwrite\Utopia\Request\Filters\V17 as RequestV17;
use Appwrite\Utopia\Request\Filters\V18 as RequestV18;
use Appwrite\Utopia\Request\Filters\V19 as RequestV19;
use Appwrite\Utopia\Request\Filters\V20 as RequestV20;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Filters\V16 as ResponseV16;
use Appwrite\Utopia\Response\Filters\V17 as ResponseV17;
use Appwrite\Utopia\Response\Filters\V18 as ResponseV18;
use Appwrite\Utopia\Response\Filters\V19 as ResponseV19;
use Appwrite\Utopia\Response\Filters\V20 as ResponseV20;
use Appwrite\Utopia\View;
use Executor\Executor;
use MaxMind\Db\Reader;
@@ -49,14 +51,13 @@ use Utopia\Logger\Log\User;
use Utopia\Logger\Logger;
use Utopia\Platform\Service;
use Utopia\System\System;
use Utopia\Validator\Hostname;
use Utopia\Validator\Text;
Config::setParam('domainVerification', false);
Config::setParam('cookieDomain', 'localhost');
Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE);
function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, string $previewHostname, ?Key $apiKey)
function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, string $previewHostname, ?Key $apiKey)
{
$host = $request->getHostname() ?? '';
if (!empty($previewHostname)) {
@@ -76,7 +77,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
}
$errorView = __DIR__ . '/../views/general/error.phtml';
$url = (System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https') . '://' . System::getEnv('_APP_DOMAIN', '');
$url = (System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https') . '://' . System::getEnv('_APP_CONSOLE_DOMAIN', System::getEnv('_APP_DOMAIN', ''));
if ($rule->isEmpty()) {
$appDomainFunctionsFallback = System::getEnv('_APP_DOMAIN_FUNCTIONS_FALLBACK', '');
@@ -118,6 +119,11 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
$project->setAttribute('accessedAt', DateTime::now());
Authorization::skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $project));
}
/**
* Set projectId to update the Error hook logger, since x-appwrite-project is not available when executing custom domain function
*/
$log->addTag('projectId', $project->getId());
}
if (array_key_exists('proxy', $project->getAttribute('services', []))) {
@@ -262,7 +268,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
}
if (!$authorized) {
$url = (System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https') . "://" . System::getEnv('_APP_DOMAIN');
$url = (System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https') . "://" . System::getEnv('_APP_CONSOLE_DOMAIN', System::getEnv('_APP_DOMAIN', ''));
$response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
@@ -448,7 +454,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
$vars[$var->getAttribute('key')] = $var->getAttribute('value', '');
}
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$hostname = System::getEnv('_APP_DOMAIN');
$endpoint = $protocol . '://' . $hostname . "/v1";
@@ -787,13 +793,14 @@ App::init()
->inject('swooleRequest')
->inject('request')
->inject('response')
->inject('log')
->inject('console')
->inject('project')
->inject('dbForPlatform')
->inject('getProjectDB')
->inject('locale')
->inject('localeCodes')
->inject('clients')
->inject('platforms')
->inject('geodb')
->inject('queueForStatsUsage')
->inject('queueForEvents')
@@ -804,7 +811,9 @@ App::init()
->inject('previewHostname')
->inject('devKey')
->inject('apiKey')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Document $console, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, array $clients, Reader $geodb, StatsUsage $queueForStatsUsage, Event $queueForEvents, Certificate $queueForCertificates, Func $queueForFunctions, Executor $executor, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey) {
->inject('httpReferrer')
->inject('httpReferrerSafe')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $console, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, array $platforms, Reader $geodb, StatsUsage $queueForStatsUsage, Event $queueForEvents, Certificate $queueForCertificates, Func $queueForFunctions, Executor $executor, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, string $httpReferrer, string $httpReferrerSafe) {
/*
* Appwrite Router
*/
@@ -812,7 +821,7 @@ App::init()
$mainDomain = System::getEnv('_APP_DOMAIN', '');
// Only run Router when external domain
if ($host !== $mainDomain || !empty($previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -843,6 +852,10 @@ App::init()
if (version_compare($requestFormat, '1.7.0', '<')) {
$request->addFilter(new RequestV19());
}
if (version_compare($requestFormat, '1.8.0', '<')) {
$dbForProject = $getProjectDB($project);
$request->addFilter(new RequestV20($dbForProject, $route));
}
}
$domain = $request->getHostname();
@@ -936,42 +949,9 @@ App::init()
$locale->setDefault($localeParam);
}
$referrer = $request->getReferer();
$origin = \parse_url($request->getOrigin($referrer), PHP_URL_HOST);
$protocol = \parse_url($request->getOrigin($referrer), PHP_URL_SCHEME);
$port = \parse_url($request->getOrigin($referrer), PHP_URL_PORT);
$refDomainOrigin = 'localhost';
$validator = new Hostname($clients);
if ($validator->isValid($origin)) {
$refDomainOrigin = $origin;
} elseif (!empty($origin)) {
// Auto-allow domains with linked rule
if (System::getEnv('_APP_RULES_FORMAT') === 'md5') {
$rule = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', md5($origin ?? '')));
} else {
$rule = Authorization::skip(
fn () => $dbForPlatform->find('rules', [
Query::equal('domain', [$origin]),
Query::limit(1)
])
)[0] ?? new Document();
}
if (!$rule->isEmpty() && $rule->getAttribute('projectInternalId') === $project->getSequence()) {
$refDomainOrigin = $origin;
}
}
$refDomain = (!empty($protocol) ? $protocol : $request->getProtocol()) . '://' . $refDomainOrigin . (!empty($port) ? ':' . $port : '');
$refDomain = (!$route->getLabel('origin', false)) // This route is publicly accessible
? $refDomain
: (!empty($protocol) ? $protocol : $request->getProtocol()) . '://' . $origin . (!empty($port) ? ':' . $port : '');
$origin = \parse_url($request->getOrigin($httpReferrer), PHP_URL_HOST);
$selfDomain = new Domain($request->getHostname());
$endDomain = new Domain((string)$origin);
Config::setParam(
'domainVerification',
($selfDomain->getRegisterable() === $endDomain->getRegisterable()) &&
@@ -1012,6 +992,9 @@ App::init()
if (version_compare($responseFormat, '1.7.0', '<')) {
$response->addFilter(new ResponseV19());
}
if (version_compare($responseFormat, '1.8.0', '<')) {
$response->addFilter(new ResponseV20());
}
if (version_compare($responseFormat, APP_VERSION_STABLE, '>')) {
$response->addHeader('X-Appwrite-Warning', "The current SDK is built for Appwrite " . $responseFormat . ". However, the current Appwrite server version is " . APP_VERSION_STABLE . ". Please downgrade your SDK to match the Appwrite version: https://appwrite.io/docs/sdks");
}
@@ -1043,7 +1026,7 @@ App::init()
->addHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE')
->addHeader('Access-Control-Allow-Headers', 'Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Dev-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-Appwrite-Timeout, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, X-Appwrite-ID, X-Appwrite-Timestamp, Content-Range, Range, Cache-Control, Expires, Pragma, X-Forwarded-For, X-Forwarded-User-Agent')
->addHeader('Access-Control-Expose-Headers', 'X-Appwrite-Session, X-Fallback-Cookies')
->addHeader('Access-Control-Allow-Origin', $refDomain)
->addHeader('Access-Control-Allow-Origin', $httpReferrerSafe)
->addHeader('Access-Control-Allow-Credentials', 'true');
if (!$devKey->isEmpty()) {
@@ -1056,14 +1039,16 @@ App::init()
* Skip this check for non-web platforms which are not required to send an origin header
*/
$origin = $request->getOrigin($request->getReferer(''));
$originValidator = new Origin(\array_merge($project->getAttribute('platforms', []), $console->getAttribute('platforms', [])));
$originValidator = new Origin($platforms);
if (
!$originValidator->isValid($origin)
&& $devKey->isEmpty()
$devKey->isEmpty()
&& !empty($origin)
&& !$originValidator->isValid($origin)
&& \in_array($request->getMethod(), [Request::METHOD_POST, Request::METHOD_PUT, Request::METHOD_PATCH, Request::METHOD_DELETE])
&& $route->getLabel('origin', false) !== '*'
&& empty($request->getHeader('x-appwrite-key', ''))
&& \parse_url($httpReferrerSafe, PHP_URL_HOST) === 'localhost'
) {
throw new AppwriteException(AppwriteException::GENERAL_UNKNOWN_ORIGIN, $originValidator->getDescription());
}
@@ -1074,6 +1059,7 @@ App::options()
->inject('swooleRequest')
->inject('request')
->inject('response')
->inject('log')
->inject('dbForPlatform')
->inject('getProjectDB')
->inject('queueForEvents')
@@ -1086,7 +1072,7 @@ App::options()
->inject('project')
->inject('devKey')
->inject('apiKey')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey) {
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey) {
/*
* Appwrite Router
*/
@@ -1094,7 +1080,7 @@ App::options()
$mainDomain = System::getEnv('_APP_DOMAIN', '');
// Only run Router when external domain
if ($host !== $mainDomain || !empty($previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -1267,7 +1253,12 @@ App::error()
$log->addTag('url', $request->getURI());
$log->addTag('verboseType', get_class($error));
$log->addTag('code', $error->getCode());
$log->addTag('projectId', $project->getId());
$tags = $log->getTags();
if (!isset($tags['projectId'])) {
$log->addTag('projectId', $project->getId());
}
$log->addTag('hostname', $request->getHostname());
$log->addTag('locale', (string)$request->getParam('locale', $request->getHeader('x-appwrite-locale', '')));
@@ -1385,6 +1376,7 @@ App::get('/robots.txt')
->inject('swooleRequest')
->inject('request')
->inject('response')
->inject('log')
->inject('dbForPlatform')
->inject('getProjectDB')
->inject('queueForEvents')
@@ -1395,7 +1387,7 @@ App::get('/robots.txt')
->inject('isResourceBlocked')
->inject('previewHostname')
->inject('apiKey')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, string $previewHostname, ?Key $apiKey) {
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, string $previewHostname, ?Key $apiKey) {
$host = $request->getHostname() ?? '';
$mainDomain = System::getEnv('_APP_DOMAIN', '');
@@ -1403,7 +1395,7 @@ App::get('/robots.txt')
$template = new View(__DIR__ . '/../views/general/robots.phtml');
$response->text($template->render(false));
} else {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -1417,6 +1409,7 @@ App::get('/humans.txt')
->inject('swooleRequest')
->inject('request')
->inject('response')
->inject('log')
->inject('dbForPlatform')
->inject('getProjectDB')
->inject('queueForEvents')
@@ -1427,7 +1420,7 @@ App::get('/humans.txt')
->inject('isResourceBlocked')
->inject('previewHostname')
->inject('apiKey')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, string $previewHostname, ?Key $apiKey) {
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, string $previewHostname, ?Key $apiKey) {
$host = $request->getHostname() ?? '';
$mainDomain = System::getEnv('_APP_DOMAIN', '');
@@ -1435,7 +1428,7 @@ App::get('/humans.txt')
$template = new View(__DIR__ . '/../views/general/humans.phtml');
$response->text($template->render(false));
} else {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) {
$utopia->getRoute()?->label('router', true);
}
}
+14 -4
View File
@@ -22,6 +22,7 @@ use Utopia\Abuse\Abuse;
use Utopia\App;
use Utopia\Cache\Adapter\Filesystem;
use Utopia\Cache\Cache;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
@@ -563,7 +564,7 @@ App::init()
$cache = new Cache(
new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId())
);
$timestamp = 60 * 60 * 24 * 30;
$timestamp = 60 * 60 * 24 * 180; // Temporarily increase the TTL to 180 day to ensure files in the cache are still fetched.
$data = $cache->load($key, $timestamp);
if (!empty($data) && !$cacheLog->isEmpty()) {
@@ -797,6 +798,12 @@ App::shutdown()
}
if (!empty($queueForDatabase->getType())) {
Console::info("Triggering database event: \n" . \json_encode([
'projectId' => $project->getId(),
'databaseId' => $queueForDatabase->getDatabase()?->getId(),
'collectionId' => $queueForDatabase->getCollection()?->getId(),
'documentId' => $queueForDatabase->getDocument()?->getId(),
]));
$queueForDatabase->trigger();
}
@@ -824,6 +831,10 @@ App::shutdown()
$resourceType = $parseLabel($pattern, $responsePayload, $requestParams, $user);
}
$cache = new Cache(
new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId())
);
$key = $request->cacheIdentifier();
$signature = md5($data['payload']);
$cacheLog = Authorization::skip(fn () => $dbForProject->getDocument('cache', $key));
@@ -841,12 +852,11 @@ App::shutdown()
} elseif (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_CACHE_UPDATE)) > $accessedAt) {
$cacheLog->setAttribute('accessedAt', $now);
Authorization::skip(fn () => $dbForProject->updateDocument('cache', $cacheLog->getId(), $cacheLog));
// Overwrite the file every APP_CACHE_UPDATE seconds to update the file modified time that is used in the TTL checks in cache->load()
$cache->save($key, $data['payload']);
}
if ($signature !== $cacheLog->getAttribute('signature')) {
$cache = new Cache(
new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId())
);
$cache->save($key, $data['payload']);
}
}
+2 -1
View File
@@ -36,7 +36,8 @@ App::setMode(System::getEnv('_APP_ENV', App::MODE_TYPE_PRODUCTION));
if (!App::isProduction()) {
// Allow specific domains to skip public domain validation in dev environment
// Useful for existing tests involving webhooks
PublicDomain::allow(['request-catcher']);
PublicDomain::allow(['request-catcher-sms']);
PublicDomain::allow(['request-catcher-webhook']);
}
$register->set('logger', function () {
// Register error logger
+86 -32
View File
@@ -19,6 +19,7 @@ use Appwrite\Event\StatsUsage;
use Appwrite\Event\Webhook;
use Appwrite\Extend\Exception;
use Appwrite\GraphQL\Schema;
use Appwrite\Network\Platform;
use Appwrite\Network\Validator\Origin;
use Appwrite\Utopia\Request;
use Executor\Executor;
@@ -79,9 +80,15 @@ App::setResource('localeCodes', function () {
App::setResource('publisher', function (Group $pools) {
return new BrokerPool(publisher: $pools->get('publisher'));
}, ['pools']);
App::setResource('publisherRedis', function () {
// Stub
});
App::setResource('consumer', function (Group $pools) {
return new BrokerPool(consumer: $pools->get('consumer'));
}, ['pools']);
App::setResource('consumerRedis', function () {
// Stub
});
App::setResource('queueForMessaging', function (Publisher $publisher) {
return new Messaging($publisher);
}, ['publisher']);
@@ -121,11 +128,11 @@ App::setResource('queueForCertificates', function (Publisher $publisher) {
App::setResource('queueForMigrations', function (Publisher $publisher) {
return new Migration($publisher);
}, ['publisher']);
App::setResource('clients', function ($request, $console, $project) {
App::setResource('platforms', function (Request $request, Document $console, Document $project) {
$console->setAttribute('platforms', [ // Always allow current host
'$collection' => ID::custom('platforms'),
'name' => 'Current Host',
'type' => Origin::CLIENT_TYPE_WEB,
'type' => Platform::TYPE_WEB,
'hostname' => $request->getHostname(),
], Document::SET_TYPE_APPEND);
@@ -138,39 +145,32 @@ App::setResource('clients', function ($request, $console, $project) {
}
$console->setAttribute('platforms', [
'$collection' => ID::custom('platforms'),
'type' => Origin::CLIENT_TYPE_WEB,
'type' => Platform::TYPE_WEB,
'name' => $hostname,
'hostname' => $hostname,
], Document::SET_TYPE_APPEND);
}
/**
* Get All verified client URLs for both console and current projects
* + Filter for duplicated entries
*/
$clientsConsole = \array_map(
fn ($node) => $node['hostname'],
\array_filter(
$console->getAttribute('platforms', []),
fn ($node) => (isset($node['type']) && ($node['type'] === Origin::CLIENT_TYPE_WEB) && !empty($node['hostname']))
)
);
$clients = $clientsConsole;
$platforms = $project->getAttribute('platforms', []);
foreach ($platforms as $node) {
if (
isset($node['type']) &&
($node['type'] === Origin::CLIENT_TYPE_WEB ||
$node['type'] === Origin::CLIENT_TYPE_FLUTTER_WEB) &&
!empty($node['hostname'])
) {
$clients[] = $node['hostname'];
}
// Add `exp` and `appwrite-callback-{projectId}` schemes
if (!$project->isEmpty() && $project->getId() !== 'console') {
$project->setAttribute('platforms', [
'$collection' => ID::custom('platforms'),
'type' => Platform::TYPE_SCHEME,
'name' => 'Expo',
'key' => 'exp',
], Document::SET_TYPE_APPEND);
$project->setAttribute('platforms', [
'$collection' => ID::custom('platforms'),
'type' => Platform::TYPE_SCHEME,
'name' => 'Appwrite Callback',
'key' => 'appwrite-callback-' . $project->getId(),
], Document::SET_TYPE_APPEND);
}
return \array_unique($clients);
return [
...$console->getAttribute('platforms', []),
...$project->getAttribute('platforms', []),
];
}, ['request', 'console', 'project']);
App::setResource('user', function ($mode, $project, $console, $request, $response, $dbForProject, $dbForPlatform) {
@@ -222,7 +222,9 @@ App::setResource('user', function ($mode, $project, $console, $request, $respons
Auth::$unique = $session['id'] ?? '';
Auth::$secret = $session['secret'] ?? '';
if (APP_MODE_ADMIN !== $mode) {
if ($mode === APP_MODE_ADMIN) {
$user = $dbForPlatform->getDocument('users', Auth::$unique);
} else {
if ($project->isEmpty()) {
$user = new Document([]);
} else {
@@ -232,8 +234,6 @@ App::setResource('user', function ($mode, $project, $console, $request, $respons
$user = $dbForProject->getDocument('users', Auth::$unique);
}
}
} else {
$user = $dbForPlatform->getDocument('users', Auth::$unique);
}
if (
@@ -264,7 +264,11 @@ App::setResource('user', function ($mode, $project, $console, $request, $respons
$jwtUserId = $payload['userId'] ?? '';
if (!empty($jwtUserId)) {
$user = $dbForProject->getDocument('users', $jwtUserId);
if ($mode === APP_MODE_ADMIN) {
$user = $dbForPlatform->getDocument('users', $jwtUserId);
} else {
$user = $dbForProject->getDocument('users', $jwtUserId);
}
}
$jwtSessionId = $payload['sessionId'] ?? '';
@@ -684,6 +688,7 @@ App::setResource('schema', function ($utopia, $dbForProject) {
},
];
// NOTE: `params` and `urls` are not used internally in the `Schema::build` function below!
$params = [
'list' => function (string $databaseId, string $collectionId, array $args) {
return [ 'queries' => $args['queries']];
@@ -944,3 +949,52 @@ App::setResource('resourceToken', function ($project, $dbForProject, $request) {
}
return new Document([]);
}, ['project', 'dbForProject', 'request']);
App::setResource('httpReferrer', function (Request $request): string {
$referrer = $request->getReferer();
return $referrer;
}, ['request']);
App::setResource('httpReferrerSafe', function (Request $request, string $httpReferrer, array $platforms, Database $dbForPlatform, Document $project, App $utopia): string {
$origin = \parse_url($request->getOrigin($httpReferrer), PHP_URL_HOST);
$protocol = \parse_url($request->getOrigin($httpReferrer), PHP_URL_SCHEME);
$port = \parse_url($request->getOrigin($httpReferrer), PHP_URL_PORT);
$referrer = (!empty($protocol) ? $protocol : $request->getProtocol()) . '://' . $origin . (!empty($port) ? ':' . $port : '');
// Safe if route is publicly accessible
$route = $utopia->getRoute();
if ($route->getLabel('origin', false)) {
return $referrer;
}
// Safe if added as web platform
$originValidator = new Origin($platforms);
if ($originValidator->isValid($request->getOrigin($httpReferrer))) {
return $referrer;
}
// Safe if rule with same project ID exists
if (!empty($origin)) {
if (System::getEnv('_APP_RULES_FORMAT') === 'md5') {
$rule = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', md5($origin ?? '')));
} else {
$rule = Authorization::skip(
fn () => $dbForPlatform->find('rules', [
Query::equal('domain', [$origin]),
Query::limit(1)
])
)[0] ?? new Document();
}
if (!$rule->isEmpty() && $rule->getAttribute('projectInternalId') === $project->getSequence()) {
return $referrer;
}
}
// Unsafe; Localhost is always safe for ease of local development
$origin = 'localhost';
$protocol = \parse_url($request->getOrigin($httpReferrer), PHP_URL_SCHEME);
$port = \parse_url($request->getOrigin($httpReferrer), PHP_URL_PORT);
$referrer = (!empty($protocol) ? $protocol : $request->getProtocol()) . '://' . $origin . (!empty($port) ? ':' . $port : '');
return $referrer;
}, ['request', 'httpReferrer', 'platforms', 'dbForPlatform', 'project', 'utopia']);
+3 -3
View File
@@ -532,7 +532,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
}
$timelimit = $app->getResource('timelimit');
$console = $app->getResource('console'); /** @var Document $console */
$platforms = $app->getResource('platforms');
$user = $app->getResource('user'); /** @var Document $user */
/*
@@ -557,9 +557,9 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
* Skip this check for non-web platforms which are not required to send an origin header.
*/
$origin = $request->getOrigin();
$originValidator = new Origin(array_merge($project->getAttribute('platforms', []), $console->getAttribute('platforms', [])));
$originValidator = new Origin($platforms);
if (!$originValidator->isValid($origin) && $project->getId() !== 'console') {
if (!empty($origin) && !$originValidator->isValid($origin) && $project->getId() !== 'console') {
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, $originValidator->getDescription());
}
+1 -1
View File
@@ -15,7 +15,7 @@ $labelClass = '';
$buttons = [];
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
$hostname = System::getEnv('_APP_DOMAIN');
$hostname = System::getEnv('_APP_CONSOLE_DOMAIN', System::getEnv('_APP_DOMAIN', ''));
// TODO: remove this later
if (System::getEnv('_APP_ENV') === 'development') {
$hostname = 'localhost';
+1 -1
View File
@@ -864,7 +864,7 @@ $image = $this->getParam('image', '');
<<: *x-logging
restart: unless-stopped
stop_signal: SIGINT
image: openruntimes/executor:0.7.20
image: openruntimes/executor:0.7.22
networks:
- appwrite
- runtimes
+8
View File
@@ -247,10 +247,18 @@ Server::setResource('publisher', function (Group $pools) {
return new BrokerPool(publisher: $pools->get('publisher'));
}, ['pools']);
Server::setResource('publisherRedis', function () {
// Stub
});
Server::setResource('consumer', function (Group $pools) {
return new BrokerPool(consumer: $pools->get('consumer'));
}, ['pools']);
Server::setResource('consumerRedis', function () {
// Stub
});
Server::setResource('queueForStatsUsage', function (Publisher $publisher) {
return new StatsUsage($publisher);
}, ['publisher']);
Generated
+100 -94
View File
@@ -1113,16 +1113,16 @@
},
{
"name": "open-telemetry/api",
"version": "1.3.0",
"version": "1.4.0",
"source": {
"type": "git",
"url": "https://github.com/opentelemetry-php/api.git",
"reference": "4e3bb38e069876fb73c2ce85c89583bf2b28cd86"
"reference": "b3a9286f9c1c8247c83493c5b1fa475cd0cec7f7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/opentelemetry-php/api/zipball/4e3bb38e069876fb73c2ce85c89583bf2b28cd86",
"reference": "4e3bb38e069876fb73c2ce85c89583bf2b28cd86",
"url": "https://api.github.com/repos/opentelemetry-php/api/zipball/b3a9286f9c1c8247c83493c5b1fa475cd0cec7f7",
"reference": "b3a9286f9c1c8247c83493c5b1fa475cd0cec7f7",
"shasum": ""
},
"require": {
@@ -1142,7 +1142,7 @@
]
},
"branch-alias": {
"dev-main": "1.1.x-dev"
"dev-main": "1.4.x-dev"
}
},
"autoload": {
@@ -1179,7 +1179,7 @@
"issues": "https://github.com/open-telemetry/opentelemetry-php/issues",
"source": "https://github.com/open-telemetry/opentelemetry-php"
},
"time": "2025-05-07T12:32:21+00:00"
"time": "2025-06-19T23:36:51+00:00"
},
{
"name": "open-telemetry/context",
@@ -1242,16 +1242,16 @@
},
{
"name": "open-telemetry/exporter-otlp",
"version": "1.3.1",
"version": "1.3.2",
"source": {
"type": "git",
"url": "https://github.com/opentelemetry-php/exporter-otlp.git",
"reference": "8b3ca1f86d01429c73b407bf1a2075d9c187001e"
"reference": "196f3a1dbce3b2c0f8110d164232c11ac00ddbb2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/opentelemetry-php/exporter-otlp/zipball/8b3ca1f86d01429c73b407bf1a2075d9c187001e",
"reference": "8b3ca1f86d01429c73b407bf1a2075d9c187001e",
"url": "https://api.github.com/repos/opentelemetry-php/exporter-otlp/zipball/196f3a1dbce3b2c0f8110d164232c11ac00ddbb2",
"reference": "196f3a1dbce3b2c0f8110d164232c11ac00ddbb2",
"shasum": ""
},
"require": {
@@ -1302,7 +1302,7 @@
"issues": "https://github.com/open-telemetry/opentelemetry-php/issues",
"source": "https://github.com/open-telemetry/opentelemetry-php"
},
"time": "2025-05-21T12:02:20+00:00"
"time": "2025-06-16T00:24:51+00:00"
},
{
"name": "open-telemetry/gen-otlp-protobuf",
@@ -1369,22 +1369,22 @@
},
{
"name": "open-telemetry/sdk",
"version": "1.5.0",
"version": "1.6.0",
"source": {
"type": "git",
"url": "https://github.com/opentelemetry-php/sdk.git",
"reference": "cd0d7367599717fc29e04eb8838ec061e6c2c657"
"reference": "1c0371794e4c0700afd4a9d4d8511cb5e3f78e6a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/cd0d7367599717fc29e04eb8838ec061e6c2c657",
"reference": "cd0d7367599717fc29e04eb8838ec061e6c2c657",
"url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/1c0371794e4c0700afd4a9d4d8511cb5e3f78e6a",
"reference": "1c0371794e4c0700afd4a9d4d8511cb5e3f78e6a",
"shasum": ""
},
"require": {
"ext-json": "*",
"nyholm/psr7-server": "^1.1",
"open-telemetry/api": "~1.0 || ~1.1",
"open-telemetry/api": "~1.4.0",
"open-telemetry/context": "^1.0",
"open-telemetry/sem-conv": "^1.0",
"php": "^8.1",
@@ -1407,6 +1407,10 @@
"type": "library",
"extra": {
"spi": {
"OpenTelemetry\\API\\Configuration\\ConfigEnv\\EnvComponentLoader": [
"OpenTelemetry\\API\\Instrumentation\\Configuration\\General\\ConfigEnv\\EnvComponentLoaderHttpConfig",
"OpenTelemetry\\API\\Instrumentation\\Configuration\\General\\ConfigEnv\\EnvComponentLoaderPeerConfig"
],
"OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\HookManagerInterface": [
"OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\ExtensionHookManager"
]
@@ -1455,20 +1459,20 @@
"issues": "https://github.com/open-telemetry/opentelemetry-php/issues",
"source": "https://github.com/open-telemetry/opentelemetry-php"
},
"time": "2025-05-22T02:33:34+00:00"
"time": "2025-06-19T23:36:51+00:00"
},
{
"name": "open-telemetry/sem-conv",
"version": "1.32.0",
"version": "1.32.1",
"source": {
"type": "git",
"url": "https://github.com/opentelemetry-php/sem-conv.git",
"reference": "16585cc0dbc3032a318e274043454679430d2ebf"
"reference": "94daa85ea61a8e2b7e1b0af6be0e875bedda7c22"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/opentelemetry-php/sem-conv/zipball/16585cc0dbc3032a318e274043454679430d2ebf",
"reference": "16585cc0dbc3032a318e274043454679430d2ebf",
"url": "https://api.github.com/repos/opentelemetry-php/sem-conv/zipball/94daa85ea61a8e2b7e1b0af6be0e875bedda7c22",
"reference": "94daa85ea61a8e2b7e1b0af6be0e875bedda7c22",
"shasum": ""
},
"require": {
@@ -1512,7 +1516,7 @@
"issues": "https://github.com/open-telemetry/opentelemetry-php/issues",
"source": "https://github.com/open-telemetry/opentelemetry-php"
},
"time": "2025-05-05T03:58:53+00:00"
"time": "2025-06-24T02:32:27+00:00"
},
{
"name": "paragonie/constant_time_encoding",
@@ -1874,16 +1878,16 @@
},
{
"name": "phpseclib/phpseclib",
"version": "3.0.44",
"version": "3.0.46",
"source": {
"type": "git",
"url": "https://github.com/phpseclib/phpseclib.git",
"reference": "1d0b5e7e1434678411787c5a0535e68907cf82d9"
"reference": "56483a7de62a6c2a6635e42e93b8a9e25d4f0ec6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/1d0b5e7e1434678411787c5a0535e68907cf82d9",
"reference": "1d0b5e7e1434678411787c5a0535e68907cf82d9",
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/56483a7de62a6c2a6635e42e93b8a9e25d4f0ec6",
"reference": "56483a7de62a6c2a6635e42e93b8a9e25d4f0ec6",
"shasum": ""
},
"require": {
@@ -1964,7 +1968,7 @@
],
"support": {
"issues": "https://github.com/phpseclib/phpseclib/issues",
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.44"
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.46"
},
"funding": [
{
@@ -1980,7 +1984,7 @@
"type": "tidelift"
}
],
"time": "2025-06-15T09:59:26+00:00"
"time": "2025-06-26T16:29:55+00:00"
},
{
"name": "psr/container",
@@ -2323,21 +2327,20 @@
},
{
"name": "ramsey/uuid",
"version": "4.8.1",
"version": "4.9.0",
"source": {
"type": "git",
"url": "https://github.com/ramsey/uuid.git",
"reference": "fdf4dd4e2ff1813111bd0ad58d7a1ddbb5b56c28"
"reference": "4e0e23cc785f0724a0e838279a9eb03f28b092a0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ramsey/uuid/zipball/fdf4dd4e2ff1813111bd0ad58d7a1ddbb5b56c28",
"reference": "fdf4dd4e2ff1813111bd0ad58d7a1ddbb5b56c28",
"url": "https://api.github.com/repos/ramsey/uuid/zipball/4e0e23cc785f0724a0e838279a9eb03f28b092a0",
"reference": "4e0e23cc785f0724a0e838279a9eb03f28b092a0",
"shasum": ""
},
"require": {
"brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13",
"ext-json": "*",
"php": "^8.0",
"ramsey/collection": "^1.2 || ^2.0"
},
@@ -2396,9 +2399,9 @@
],
"support": {
"issues": "https://github.com/ramsey/uuid/issues",
"source": "https://github.com/ramsey/uuid/tree/4.8.1"
"source": "https://github.com/ramsey/uuid/tree/4.9.0"
},
"time": "2025-06-01T06:28:46+00:00"
"time": "2025-06-25T14:20:11+00:00"
},
{
"name": "spomky-labs/otphp",
@@ -2544,16 +2547,16 @@
},
{
"name": "symfony/http-client",
"version": "v7.3.0",
"version": "v7.3.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client.git",
"reference": "57e4fb86314015a695a750ace358d07a7e37b8a9"
"reference": "4403d87a2c16f33345dca93407a8714ee8c05a64"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-client/zipball/57e4fb86314015a695a750ace358d07a7e37b8a9",
"reference": "57e4fb86314015a695a750ace358d07a7e37b8a9",
"url": "https://api.github.com/repos/symfony/http-client/zipball/4403d87a2c16f33345dca93407a8714ee8c05a64",
"reference": "4403d87a2c16f33345dca93407a8714ee8c05a64",
"shasum": ""
},
"require": {
@@ -2565,6 +2568,7 @@
},
"conflict": {
"amphp/amp": "<2.5",
"amphp/socket": "<1.1",
"php-http/discovery": "<1.15",
"symfony/http-foundation": "<6.4"
},
@@ -2577,7 +2581,6 @@
"require-dev": {
"amphp/http-client": "^4.2.1|^5.0",
"amphp/http-tunnel": "^1.0|^2.0",
"amphp/socket": "^1.1",
"guzzlehttp/promises": "^1.4|^2.0",
"nyholm/psr7": "^1.0",
"php-http/httplug": "^1.0|^2.0",
@@ -2619,7 +2622,7 @@
"http"
],
"support": {
"source": "https://github.com/symfony/http-client/tree/v7.3.0"
"source": "https://github.com/symfony/http-client/tree/v7.3.1"
},
"funding": [
{
@@ -2635,7 +2638,7 @@
"type": "tidelift"
}
],
"time": "2025-05-02T08:23:16+00:00"
"time": "2025-06-28T07:58:39+00:00"
},
{
"name": "symfony/http-client-contracts",
@@ -2957,16 +2960,16 @@
},
{
"name": "tbachert/spi",
"version": "v1.0.3",
"version": "v1.0.5",
"source": {
"type": "git",
"url": "https://github.com/Nevay/spi.git",
"reference": "506a79c98e1a51522e76ee921ccb6c62d52faf3a"
"reference": "e7078767866d0a9e0f91d3f9d42a832df5e39002"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Nevay/spi/zipball/506a79c98e1a51522e76ee921ccb6c62d52faf3a",
"reference": "506a79c98e1a51522e76ee921ccb6c62d52faf3a",
"url": "https://api.github.com/repos/Nevay/spi/zipball/e7078767866d0a9e0f91d3f9d42a832df5e39002",
"reference": "e7078767866d0a9e0f91d3f9d42a832df5e39002",
"shasum": ""
},
"require": {
@@ -2984,7 +2987,7 @@
"extra": {
"class": "Nevay\\SPI\\Composer\\Plugin",
"branch-alias": {
"dev-main": "0.2.x-dev"
"dev-main": "1.0.x-dev"
},
"plugin-optional": true
},
@@ -3003,9 +3006,9 @@
],
"support": {
"issues": "https://github.com/Nevay/spi/issues",
"source": "https://github.com/Nevay/spi/tree/v1.0.3"
"source": "https://github.com/Nevay/spi/tree/v1.0.5"
},
"time": "2025-04-02T19:38:14+00:00"
"time": "2025-06-29T15:42:06+00:00"
},
{
"name": "thecodingmachine/safe",
@@ -3490,16 +3493,16 @@
},
{
"name": "utopia-php/database",
"version": "0.71.7",
"version": "0.71.9",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
"reference": "aa0116b2380125907fc18c82662be8e74c54091f"
"reference": "eb2f759020bba617e99dd67973a9bd949b47f54e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/database/zipball/aa0116b2380125907fc18c82662be8e74c54091f",
"reference": "aa0116b2380125907fc18c82662be8e74c54091f",
"url": "https://api.github.com/repos/utopia-php/database/zipball/eb2f759020bba617e99dd67973a9bd949b47f54e",
"reference": "eb2f759020bba617e99dd67973a9bd949b47f54e",
"shasum": ""
},
"require": {
@@ -3540,9 +3543,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/database/issues",
"source": "https://github.com/utopia-php/database/tree/0.71.7"
"source": "https://github.com/utopia-php/database/tree/0.71.9"
},
"time": "2025-06-17T23:59:10+00:00"
"time": "2025-07-02T16:37:41+00:00"
},
{
"name": "utopia-php/detector",
@@ -3939,16 +3942,16 @@
},
{
"name": "utopia-php/messaging",
"version": "0.18.0",
"version": "0.18.1",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/messaging.git",
"reference": "c151aa5d4d475c788ca15c210b5b2017e21c41d6"
"reference": "5d1245207a61d7ca065daddad7ac5f1d5640152f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/messaging/zipball/c151aa5d4d475c788ca15c210b5b2017e21c41d6",
"reference": "c151aa5d4d475c788ca15c210b5b2017e21c41d6",
"url": "https://api.github.com/repos/utopia-php/messaging/zipball/5d1245207a61d7ca065daddad7ac5f1d5640152f",
"reference": "5d1245207a61d7ca065daddad7ac5f1d5640152f",
"shasum": ""
},
"require": {
@@ -3984,22 +3987,22 @@
],
"support": {
"issues": "https://github.com/utopia-php/messaging/issues",
"source": "https://github.com/utopia-php/messaging/tree/0.18.0"
"source": "https://github.com/utopia-php/messaging/tree/0.18.1"
},
"time": "2025-05-15T05:00:03+00:00"
"time": "2025-06-26T18:26:07+00:00"
},
{
"name": "utopia-php/migration",
"version": "0.10.1",
"version": "0.10.4",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/migration.git",
"reference": "ea1c585df7ec5f346f061a11581fc9a91679966f"
"reference": "0c85917482db172b3ccdc0704e42af3c1cc89361"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/migration/zipball/ea1c585df7ec5f346f061a11581fc9a91679966f",
"reference": "ea1c585df7ec5f346f061a11581fc9a91679966f",
"url": "https://api.github.com/repos/utopia-php/migration/zipball/0c85917482db172b3ccdc0704e42af3c1cc89361",
"reference": "0c85917482db172b3ccdc0704e42af3c1cc89361",
"shasum": ""
},
"require": {
@@ -4040,9 +4043,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/migration/issues",
"source": "https://github.com/utopia-php/migration/tree/0.10.1"
"source": "https://github.com/utopia-php/migration/tree/0.10.4"
},
"time": "2025-05-26T15:29:19+00:00"
"time": "2025-07-02T18:31:09+00:00"
},
{
"name": "utopia-php/orchestration",
@@ -4807,16 +4810,16 @@
"packages-dev": [
{
"name": "appwrite/sdk-generator",
"version": "0.41.8",
"version": "0.41.11",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-generator.git",
"reference": "93ffb24b25b376ca4423e3a5caf6f916673af4b2"
"reference": "60122cb613a5a1c82667ecc2217e351654a8d404"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/93ffb24b25b376ca4423e3a5caf6f916673af4b2",
"reference": "93ffb24b25b376ca4423e3a5caf6f916673af4b2",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/60122cb613a5a1c82667ecc2217e351654a8d404",
"reference": "60122cb613a5a1c82667ecc2217e351654a8d404",
"shasum": ""
},
"require": {
@@ -4852,9 +4855,9 @@
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
"support": {
"issues": "https://github.com/appwrite/sdk-generator/issues",
"source": "https://github.com/appwrite/sdk-generator/tree/0.41.8"
"source": "https://github.com/appwrite/sdk-generator/tree/0.41.11"
},
"time": "2025-06-18T13:20:45+00:00"
"time": "2025-07-04T09:56:24+00:00"
},
{
"name": "doctrine/annotations",
@@ -5081,16 +5084,16 @@
},
{
"name": "laravel/pint",
"version": "v1.22.1",
"version": "v1.23.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/pint.git",
"reference": "941d1927c5ca420c22710e98420287169c7bcaf7"
"reference": "9ab851dba4faa51a3c3223dd3d07044129021024"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/pint/zipball/941d1927c5ca420c22710e98420287169c7bcaf7",
"reference": "941d1927c5ca420c22710e98420287169c7bcaf7",
"url": "https://api.github.com/repos/laravel/pint/zipball/9ab851dba4faa51a3c3223dd3d07044129021024",
"reference": "9ab851dba4faa51a3c3223dd3d07044129021024",
"shasum": ""
},
"require": {
@@ -5101,10 +5104,10 @@
"php": "^8.2.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.75.0",
"illuminate/view": "^11.44.7",
"larastan/larastan": "^3.4.0",
"laravel-zero/framework": "^11.36.1",
"friendsofphp/php-cs-fixer": "^3.76.0",
"illuminate/view": "^11.45.1",
"larastan/larastan": "^3.5.0",
"laravel-zero/framework": "^11.45.0",
"mockery/mockery": "^1.6.12",
"nunomaduro/termwind": "^2.3.1",
"pestphp/pest": "^2.36.0"
@@ -5114,6 +5117,9 @@
],
"type": "project",
"autoload": {
"files": [
"overrides/Runner/Parallel/ProcessFactory.php"
],
"psr-4": {
"App\\": "app/",
"Database\\Seeders\\": "database/seeders/",
@@ -5143,20 +5149,20 @@
"issues": "https://github.com/laravel/pint/issues",
"source": "https://github.com/laravel/pint"
},
"time": "2025-05-08T08:38:12+00:00"
"time": "2025-07-03T10:37:47+00:00"
},
{
"name": "matthiasmullie/minify",
"version": "1.3.74",
"version": "1.3.75",
"source": {
"type": "git",
"url": "https://github.com/matthiasmullie/minify.git",
"reference": "a2593286a4135d03c6a6a9e9aeded5d41e931ce4"
"reference": "76ba4a5f555fd7bf4aa408af608e991569076671"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/matthiasmullie/minify/zipball/a2593286a4135d03c6a6a9e9aeded5d41e931ce4",
"reference": "a2593286a4135d03c6a6a9e9aeded5d41e931ce4",
"url": "https://api.github.com/repos/matthiasmullie/minify/zipball/76ba4a5f555fd7bf4aa408af608e991569076671",
"reference": "76ba4a5f555fd7bf4aa408af608e991569076671",
"shasum": ""
},
"require": {
@@ -5205,7 +5211,7 @@
],
"support": {
"issues": "https://github.com/matthiasmullie/minify/issues",
"source": "https://github.com/matthiasmullie/minify/tree/1.3.74"
"source": "https://github.com/matthiasmullie/minify/tree/1.3.75"
},
"funding": [
{
@@ -5213,7 +5219,7 @@
"type": "github"
}
],
"time": "2025-06-12T08:06:04+00:00"
"time": "2025-06-25T09:56:19+00:00"
},
{
"name": "matthiasmullie/path-converter",
@@ -7252,16 +7258,16 @@
},
{
"name": "symfony/console",
"version": "v7.3.0",
"version": "v7.3.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "66c1440edf6f339fd82ed6c7caa76cb006211b44"
"reference": "9e27aecde8f506ba0fd1d9989620c04a87697101"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/66c1440edf6f339fd82ed6c7caa76cb006211b44",
"reference": "66c1440edf6f339fd82ed6c7caa76cb006211b44",
"url": "https://api.github.com/repos/symfony/console/zipball/9e27aecde8f506ba0fd1d9989620c04a87697101",
"reference": "9e27aecde8f506ba0fd1d9989620c04a87697101",
"shasum": ""
},
"require": {
@@ -7326,7 +7332,7 @@
"terminal"
],
"support": {
"source": "https://github.com/symfony/console/tree/v7.3.0"
"source": "https://github.com/symfony/console/tree/v7.3.1"
},
"funding": [
{
@@ -7342,7 +7348,7 @@
"type": "tidelift"
}
],
"time": "2025-05-24T10:34:04+00:00"
"time": "2025-06-27T19:55:54+00:00"
},
{
"name": "symfony/filesystem",
+17 -5
View File
@@ -116,6 +116,7 @@ services:
- _APP_OPTIONS_ROUTER_FORCE_HTTPS
- _APP_OPENSSL_KEY_V1
- _APP_DOMAIN
- _APP_CONSOLE_DOMAIN
- _APP_DOMAIN_TARGET_CNAME
- _APP_DOMAIN_TARGET_AAAA
- _APP_DOMAIN_TARGET_A
@@ -213,7 +214,7 @@ services:
appwrite-console:
<<: *x-logging
container_name: appwrite-console
image: appwrite/console:6.0.41
image: appwrite/console:6.1.2
restart: unless-stopped
networks:
- appwrite
@@ -323,7 +324,8 @@ services:
depends_on:
- redis
- mariadb
- request-catcher
- request-catcher-sms
- request-catcher-webhook
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -481,6 +483,7 @@ services:
- _APP_OPTIONS_FORCE_HTTPS
- _APP_OPTIONS_ROUTER_FORCE_HTTPS
- _APP_DOMAIN
- _APP_CONSOLE_DOMAIN
- _APP_STORAGE_DEVICE
- _APP_STORAGE_S3_ACCESS_KEY
- _APP_STORAGE_S3_SECRET
@@ -951,7 +954,7 @@ services:
hostname: exc1
<<: *x-logging
stop_signal: SIGINT
image: openruntimes/executor:0.7.20
image: openruntimes/executor:0.7.22
restart: unless-stopped
networks:
- appwrite
@@ -1075,15 +1078,24 @@ services:
networks:
- appwrite
request-catcher: # used mainly for dev tests
request-catcher-webhook: # used mainly for dev tests (mock HTTP webhook)
image: appwrite/requestcatcher:1.0.0
container_name: appwrite-requestcatcher
container_name: appwrite-requestcatcher-webhook
<<: *x-logging
ports:
- "9504:5000"
networks:
- appwrite
request-catcher-sms: # used mainly for dev tests (mock SMS auth secret)
image: appwrite/requestcatcher:1.0.0
container_name: appwrite-requestcatcher-sms
<<: *x-logging
ports:
- "9507:5000"
networks:
- appwrite
adminer:
image: adminer
container_name: appwrite-adminer
@@ -0,0 +1 @@
Invalidate all existing sessions. An optional auth security setting for projects, and enabled by default for console project.
+61 -1
View File
@@ -1 +1,61 @@
# Change Log
# Change Log
## 8.1.0
* Add multi-region support to `init` command
* Update `init` command to clear previous configuration in `appwrite.json`
* Update localConfig to store multi-region endpoint
* Fix throw error when creating unknown attribute instead of timing out
* Fix equal comparison of large numbers and BigNumber instances using proper equality checks
* Fix duplication of reasons when comparing localConfig with remoteConfig
* Fix `firstOrNull()` to `firstOrNull` in types generation for dart
* Refactor to use `isCloud()` method consistently
## 8.0.2
* Add Type generation fixes:
* Properly handle enum attributes in dart, java and kotlin
* Fix initialisation of null attributes in dart's fromMap method
* Fix relationships and enums in swift
## 8.0.1
* Add `resourceId` and `resourceType` attributes to `createRedirectRule`
* Add `providerReference` to vcs command for getting repository contents
* Add warning comment to `bulk updateDocuments` method
* Fix type generation for enums in Typescript and PHP language
## 8.0.0
* Add `types` command to generate language specific typings for collections. Currently supports - `php`, `swift`, `dart`, `js`, `ts`, `kotlin` and `java`
* Update bulk operation docs to include experiment feature warnings
* Remove assistant service and commands
## 7.0.0
* Add `sites` command
* Add `tokens` command
* Add `devKeys` support to `projects` command
* Add `init site`, `pull site` and `push site` commands
* Add bulk operation methods like `createDocuments`, `deleteDocuments` etc.
* Add new upsert methods: `upsertDocument` and `upsertDocuments`
* Update GET requests to not include content-type header
## 6.2.3
* Fix hot swapping error in `python-ml` function
## 6.2.2
* Fix GitHub builds by adding `qemu-system` package
* Fix attribute creation timed out
## 6.2.1
* Add `listOrganizations` method to `organizations` service and fix init project command
## 6.2.0
* Add specifications support to CLI
* Update package version
* Fix: Missed specifications param when updating a function
+65 -1
View File
@@ -10,8 +10,15 @@ class Database extends Event
{
protected string $type = '';
protected ?Document $database = null;
protected ?Document $collection = null;
// tables api
protected ?Document $row = null;
protected ?Document $table = null;
// collections api
protected ?Document $document = null;
protected ?Document $collection = null;
public function __construct(protected Publisher $publisher)
{
@@ -54,6 +61,61 @@ class Database extends Event
return $this;
}
/**
* Returns set database for this event.
*
* @return null|Document
*/
public function getDatabase(): ?Document
{
return $this->database;
}
/**
* Set the table for this database event.
*
* @param Document $table
* @return self
*/
public function setTable(Document $table): self
{
$this->table = $table;
return $this;
}
/**
* Returns set table for this event.
*
* @return null|Document
*/
public function getTable(): ?Document
{
return $this->table;
}
/**
* Set the row for this database event.
*
* @param Document $row
* @return self
*/
public function setRow(Document $row): self
{
$this->row = $row;
return $this;
}
/**
* Returns set row for this database event.
* @return null|Document
*/
public function getRow(): ?Document
{
return $this->row;
}
/**
* Set the collection for this database event.
*
@@ -123,6 +185,8 @@ class Database extends Event
'project' => $this->project,
'user' => $this->user,
'type' => $this->type,
'table' => $this->table,
'row' => $this->row,
'collection' => $this->collection,
'document' => $this->document,
'database' => $this->database,
+47 -1
View File
@@ -569,7 +569,12 @@ class Event
/**
* Force a non-assoc array.
*/
return \array_values($events);
$eventValues = \array_values($events);
/**
* Return a combined list of table, collection events.
*/
return Event::mirrorCollectionEvents($pattern, $eventValues[0], $eventValues);
}
/**
@@ -590,4 +595,45 @@ class Event
$this->context = $event->context;
return $this;
}
/**
* Adds `table` events for `collection` events.
*
* Example:
*
* `databases.*.collections.*.documents.*.update` →\
* `[databases.*.collections.*.documents.*.update, databases.*.tables.*.rows.*.update]`
*/
private static function mirrorCollectionEvents(string $pattern, string $firstEvent, array $events): array
{
$tableEventMap = [
'documents' => 'rows',
'collections' => 'tables',
'attributes' => 'columns',
];
if (
str_contains($pattern, 'databases.') &&
str_contains($firstEvent, 'collections')
) {
$pairedEvents = [];
foreach ($events as $event) {
$pairedEvents[] = $event;
if (str_contains($event, 'collections')) {
$tableSideEvent = str_replace(
array_keys($tableEventMap),
array_values($tableEventMap),
$event
);
$pairedEvents[] = $tableSideEvent;
}
}
$events = $pairedEvents;
}
return $events;
}
}
+5 -3
View File
@@ -73,19 +73,21 @@ class Realtime extends Event
}
$allEvents = Event::generateEvents($this->getEvent(), $this->getParams());
$payload = new Document($this->getPayload());
$db = $this->getContext('database');
$collection = $this->getContext('collection');
$bucket = $this->getContext('bucket');
// Can be Tables API or Collections API; generated channels include both!
$tableOrCollection = $this->getContext('table') ?? $this->getContext('collection');
$target = RealtimeAdapter::fromPayload(
// Pass first, most verbose event pattern
event: $allEvents[0],
payload: $payload,
project: $this->getProject(),
database: $db,
collection: $collection,
collection: $tableOrCollection,
bucket: $bucket,
);
+37 -3
View File
@@ -48,7 +48,8 @@ class Exception extends \Exception
public const GENERAL_SMTP_DISABLED = 'general_smtp_disabled';
public const GENERAL_PHONE_DISABLED = 'general_phone_disabled';
public const GENERAL_ARGUMENT_INVALID = 'general_argument_invalid';
public const GENERAL_QUERY_LIMIT_EXCEEDED = 'general_query_limit_exceeded';
public const GENERAL_COLUMN_QUERY_LIMIT_EXCEEDED = 'general_column_query_limit_exceeded';
public const GENERAL_ATTRIBUTE_QUERY_LIMIT_EXCEEDED = 'general_attribute_query_limit_exceeded';
public const GENERAL_QUERY_INVALID = 'general_query_invalid';
public const GENERAL_ROUTE_NOT_FOUND = 'general_route_not_found';
public const GENERAL_CURSOR_NOT_FOUND = 'general_cursor_not_found';
@@ -198,6 +199,11 @@ class Exception extends \Exception
public const COLLECTION_ALREADY_EXISTS = 'collection_already_exists';
public const COLLECTION_LIMIT_EXCEEDED = 'collection_limit_exceeded';
/** Tables */
public const TABLE_NOT_FOUND = 'table_not_found';
public const TABLE_ALREADY_EXISTS = 'table_already_exists';
public const TABLE_LIMIT_EXCEEDED = 'table_limit_exceeded';
/** Documents */
public const DOCUMENT_NOT_FOUND = 'document_not_found';
public const DOCUMENT_INVALID_STRUCTURE = 'document_invalid_structure';
@@ -207,7 +213,16 @@ class Exception extends \Exception
public const DOCUMENT_UPDATE_CONFLICT = 'document_update_conflict';
public const DOCUMENT_DELETE_RESTRICTED = 'document_delete_restricted';
/** Attribute */
/** Rows */
public const ROW_NOT_FOUND = 'row_not_found';
public const ROW_INVALID_STRUCTURE = 'row_invalid_structure';
public const ROW_MISSING_DATA = 'row_missing_data';
public const ROW_MISSING_PAYLOAD = 'row_missing_payload';
public const ROW_ALREADY_EXISTS = 'row_already_exists';
public const ROW_UPDATE_CONFLICT = 'row_update_conflict';
public const ROW_DELETE_RESTRICTED = 'row_delete_restricted';
/** Attributes */
public const ATTRIBUTE_NOT_FOUND = 'attribute_not_found';
public const ATTRIBUTE_UNKNOWN = 'attribute_unknown';
public const ATTRIBUTE_NOT_AVAILABLE = 'attribute_not_available';
@@ -219,6 +234,18 @@ class Exception extends \Exception
public const ATTRIBUTE_TYPE_INVALID = 'attribute_type_invalid';
public const ATTRIBUTE_INVALID_RESIZE = 'attribute_invalid_resize';
/** Columns */
public const COLUMN_NOT_FOUND = 'column_not_found';
public const COLUMN_UNKNOWN = 'column_unknown';
public const COLUMN_NOT_AVAILABLE = 'column_not_available';
public const COLUMN_FORMAT_UNSUPPORTED = 'column_format_unsupported';
public const COLUMN_DEFAULT_UNSUPPORTED = 'column_default_unsupported';
public const COLUMN_ALREADY_EXISTS = 'column_already_exists';
public const COLUMN_LIMIT_EXCEEDED = 'column_limit_exceeded';
public const COLUMN_VALUE_INVALID = 'column_value_invalid';
public const COLUMN_TYPE_INVALID = 'column_type_invalid';
public const COLUMN_INVALID_RESIZE = 'column_invalid_resize';
/** Relationship */
public const RELATIONSHIP_VALUE_INVALID = 'relationship_value_invalid';
@@ -229,6 +256,13 @@ class Exception extends \Exception
public const INDEX_INVALID = 'index_invalid';
public const INDEX_DEPENDENCY = 'index_dependency';
/** Column Indexes */
public const COLUMN_INDEX_NOT_FOUND = 'column_index_not_found';
public const COLUMN_INDEX_LIMIT_EXCEEDED = 'column_index_limit_exceeded';
public const COLUMN_INDEX_ALREADY_EXISTS = 'column_index_already_exists';
public const COLUMN_INDEX_INVALID = 'column_index_invalid';
public const COLUMN_INDEX_DEPENDENCY = 'column_index_dependency';
/** Projects */
public const PROJECT_NOT_FOUND = 'project_not_found';
public const PROJECT_PROVIDER_DISABLED = 'project_provider_disabled';
@@ -316,7 +350,7 @@ class Exception extends \Exception
public const MESSAGE_MISSING_SCHEDULE = 'message_missing_schedule';
/** Targets */
public const TARGET_PROVIDER_INVALID_TYPE = 'target_provider_invalid_type';
public const TARGET_PROVIDER_INVALID_TYPE = 'target_provider_invalid_type';
/** Schedules */
public const SCHEDULE_NOT_FOUND = 'schedule_not_found';
+22 -23
View File
@@ -226,7 +226,7 @@ class Mapper
];
if (!$rule['required']) {
$fields[$escapedKey]['defaultValue'] = $rule['default'];
$fields[$escapedKey]['defaultValue'] = $rule['default'] ?? null;
}
}
@@ -290,7 +290,9 @@ class Mapper
case 'Utopia\Database\Validator\Authorization':
case 'Appwrite\Utopia\Database\Validator\Queries\Base':
case 'Appwrite\Utopia\Database\Validator\Queries\Buckets':
case 'Appwrite\Utopia\Database\Validator\Queries\Tables':
case 'Appwrite\Utopia\Database\Validator\Queries\Collections':
case 'Appwrite\Utopia\Database\Validator\Queries\Columns':
case 'Appwrite\Utopia\Database\Validator\Queries\Attributes':
case 'Appwrite\Utopia\Database\Validator\Queries\Indexes':
case 'Appwrite\Utopia\Database\Validator\Queries\Databases':
@@ -425,7 +427,9 @@ class Mapper
switch ($name) {
case 'Attributes':
return static::getAttributeImplementation($object);
return static::getColumnImplementation($object);
case 'Columns':
return static::getColumnImplementation($object, true);
case 'HashOptions':
return static::getHashOptionsImplementation($object);
}
@@ -433,29 +437,24 @@ class Mapper
throw new Exception('Unknown union type: ' . $name);
}
private static function getAttributeImplementation(array $object): Type
private static function getColumnImplementation(array $object, bool $isColumns = false): Type
{
switch ($object['type']) {
case 'string':
return match ($object['format'] ?? '') {
'email' => static::model('AttributeEmail'),
'url' => static::model('AttributeUrl'),
'ip' => static::model('AttributeIp'),
default => static::model('AttributeString'),
};
case 'integer':
return static::model('AttributeInteger');
case 'double':
return static::model('AttributeFloat');
case 'boolean':
return static::model('AttributeBoolean');
case 'datetime':
return static::model('AttributeDatetime');
case 'relationship':
return static::model('AttributeRelationship');
}
$prefix = $isColumns ? 'Column' : 'Attribute';
throw new Exception('Unknown attribute implementation');
return match ($object['type']) {
'string' => match ($object['format'] ?? '') {
'email' => static::model("{$prefix}Email"),
'url' => static::model("{$prefix}Url"),
'ip' => static::model("{$prefix}Ip"),
default => static::model("{$prefix}String"),
},
'integer' => static::model("{$prefix}Integer"),
'double' => static::model("{$prefix}Float"),
'boolean' => static::model("{$prefix}Boolean"),
'datetime' => static::model("{$prefix}Datetime"),
'relationship' => static::model("{$prefix}Relationship"),
default => throw new Exception('Unknown ' . strtolower($prefix) . ' implementation'),
};
}
private static function getHashOptionsImplementation(array $object): Type
+11 -12
View File
@@ -230,7 +230,7 @@ class Realtime extends MessagingAdapter
foreach ($channels as $key => $value) {
switch (true) {
case \str_starts_with($key, 'account.'):
case str_starts_with($key, 'account.'):
unset($channels[$key]);
break;
@@ -273,6 +273,7 @@ class Realtime extends MessagingAdapter
$roles = [Role::user(ID::custom($parts[1]))->toString()];
break;
case 'rules':
case 'migrations':
$channels[] = 'console';
$channels[] = 'projects.' . $project->getId();
$projectId = 'console';
@@ -297,19 +298,24 @@ class Realtime extends MessagingAdapter
$roles = [Role::team(ID::custom($parts[1]))->toString()];
break;
case 'databases':
if (in_array($parts[4] ?? [], ['attributes', 'indexes'])) {
$resource = $parts[4] ?? '';
if (in_array($resource, ['columns', 'attributes', 'indexes'])) {
$channels[] = 'console';
$channels[] = 'projects.' . $project->getId();
$projectId = 'console';
$roles = [Role::team($project->getAttribute('teamId'))->toString()];
} elseif (($parts[4] ?? '') === 'documents') {
} elseif (in_array($resource, ['rows', 'documents'])) {
if ($database->isEmpty()) {
throw new \Exception('Database needs to be passed to Realtime for Document events in the Database.');
throw new \Exception('Database needs to be passed to Realtime for Document/Row events in the Database.');
}
if ($collection->isEmpty()) {
throw new \Exception('Collection needs to be passed to Realtime for Document events in the Database.');
throw new \Exception('Collection or the Table needs to be passed to Realtime for Document/Row events in the Database.');
}
$channels[] = 'rows';
$channels[] = 'databases.' . $database->getId() . '.tables.' . $payload->getAttribute('$tableId') . '.rows';
$channels[] = 'databases.' . $database->getId() . '.tables.' . $payload->getAttribute('$tableId') . '.rows.' . $payload->getId();
$channels[] = 'documents';
$channels[] = 'databases.' . $database->getId() . '.collections.' . $payload->getAttribute('$collectionId') . '.documents';
$channels[] = 'databases.' . $database->getId() . '.collections.' . $payload->getAttribute('$collectionId') . '.documents.' . $payload->getId();
@@ -334,7 +340,6 @@ class Realtime extends MessagingAdapter
}
break;
case 'functions':
if ($parts[2] === 'executions') {
if (!empty($payload->getRead())) {
@@ -361,12 +366,6 @@ class Realtime extends MessagingAdapter
$roles = [Role::team($project->getAttribute('teamId'))->toString()];
}
break;
case 'migrations':
$channels[] = 'console';
$channels[] = 'projects.' . $project->getId();
$projectId = 'console';
$roles = [Role::team($project->getAttribute('teamId'))->toString()];
break;
}
return [
+151
View File
@@ -0,0 +1,151 @@
<?php
namespace Appwrite\Network;
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_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_SCHEME = 'scheme';
public const SCHEME_HTTP = 'http';
public const SCHEME_HTTPS = 'https';
public const SCHEME_IOS = 'appwrite-ios';
public const SCHEME_MACOS = 'appwrite-macos';
public const SCHEME_WATCHOS = 'appwrite-watchos';
public const SCHEME_TVOS = 'appwrite-tvos';
public const SCHEME_ANDROID = 'appwrite-android';
public const SCHEME_WINDOWS = 'appwrite-windows';
public const SCHEME_LINUX = 'appwrite-linux';
/**
* @var array<string, string> Map scheme types to user-friendly platform names.
*/
private static array $names = [
self::SCHEME_HTTP => 'Web',
self::SCHEME_HTTPS => 'Web',
self::SCHEME_IOS => 'iOS',
self::SCHEME_MACOS => 'macOS',
self::SCHEME_WATCHOS => 'watchOS',
self::SCHEME_TVOS => 'tvOS',
self::SCHEME_ANDROID => 'Android',
self::SCHEME_WINDOWS => 'Windows',
self::SCHEME_LINUX => 'Linux',
];
/**
* Get user-friendly platform name from a scheme.
*
* @param string|null $scheme
* @return string Empty string if scheme is not found.
*/
public static function getNameByScheme(?string $scheme): string
{
return self::$names[$scheme] ?? '';
}
public static function getHostnames(array $platforms): array
{
$hostnames = [];
foreach ($platforms as $platform) {
$type = strtolower($platform['type'] ?? self::TYPE_UNKNOWN);
$hostname = strtolower($platform['hostname'] ?? '');
$key = strtolower($platform['key'] ?? '');
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:
if (!empty($key)) {
$hostnames[] = $key;
}
break;
default:
break;
}
}
return array_unique($hostnames);
}
public static function getSchemes(array $platforms): array
{
$schemes = [];
foreach ($platforms as $platform) {
$type = strtolower($platform['type'] ?? self::TYPE_UNKNOWN);
$scheme = strtolower($platform['key'] ?? '');
switch ($type) {
case self::TYPE_SCHEME:
if (!empty($scheme) && preg_match('/^[a-z][a-z0-9+.-]*$/', $scheme)) {
$schemes[] = $scheme;
}
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:
$schemes[] = self::SCHEME_MACOS;
break;
case self::TYPE_FLUTTER_WINDOWS:
case self::TYPE_UNITY:
$schemes[] = self::SCHEME_WINDOWS;
break;
case self::TYPE_FLUTTER_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;
}
}
return array_unique($schemes);
}
}
+66 -117
View File
@@ -2,150 +2,77 @@
namespace Appwrite\Network\Validator;
use Appwrite\Network\Platform;
use Utopia\Validator;
use Utopia\Validator\Hostname;
class Origin extends Validator
{
public const CLIENT_TYPE_UNKNOWN = 'unknown';
public const CLIENT_TYPE_WEB = 'web';
public const CLIENT_TYPE_FLUTTER_IOS = 'flutter-ios';
public const CLIENT_TYPE_FLUTTER_ANDROID = 'flutter-android';
public const CLIENT_TYPE_FLUTTER_MACOS = 'flutter-macos';
public const CLIENT_TYPE_FLUTTER_WINDOWS = 'flutter-windows';
public const CLIENT_TYPE_FLUTTER_LINUX = 'flutter-linux';
public const CLIENT_TYPE_FLUTTER_WEB = 'flutter-web';
public const CLIENT_TYPE_APPLE_IOS = 'apple-ios';
public const CLIENT_TYPE_APPLE_MACOS = 'apple-macos';
public const CLIENT_TYPE_APPLE_WATCHOS = 'apple-watchos';
public const CLIENT_TYPE_APPLE_TVOS = 'apple-tvos';
public const CLIENT_TYPE_ANDROID = 'android';
public const CLIENT_TYPE_UNITY = 'unity';
public const CLIENT_TYPE_REACT_NATIVE_IOS = 'react-native-ios';
public const CLIENT_TYPE_REACT_NATIVE_ANDROID = 'react-native-android';
public const SCHEME_TYPE_HTTP = 'http';
public const SCHEME_TYPE_HTTPS = 'https';
public const SCHEME_TYPE_IOS = 'appwrite-ios';
public const SCHEME_TYPE_MACOS = 'appwrite-macos';
public const SCHEME_TYPE_WATCHOS = 'appwrite-watchos';
public const SCHEME_TYPE_TVOS = 'appwrite-tvos';
public const SCHEME_TYPE_ANDROID = 'appwrite-android';
public const SCHEME_TYPE_WINDOWS = 'appwrite-windows';
public const SCHEME_TYPE_LINUX = 'appwrite-linux';
protected array $hostnames = [];
protected array $schemes = [];
protected ?string $scheme = null;
protected ?string $host = null;
/**
* @var array
* Constructor
*
* @param array<\Utopia\Database\Document> $platforms
*/
protected $platforms = [
self::SCHEME_TYPE_HTTP => 'Web',
self::SCHEME_TYPE_HTTPS => 'Web',
self::SCHEME_TYPE_IOS => 'iOS',
self::SCHEME_TYPE_MACOS => 'macOS',
self::SCHEME_TYPE_WATCHOS => 'watchOS',
self::SCHEME_TYPE_TVOS => 'tvOS',
self::SCHEME_TYPE_ANDROID => 'Android',
self::SCHEME_TYPE_WINDOWS => 'Windows',
self::SCHEME_TYPE_LINUX => 'Linux',
];
/**
* @var array
*/
protected $clients = [
];
/**
* @var string
*/
protected $client = self::CLIENT_TYPE_UNKNOWN;
/**
* @var string
*/
protected $host = '';
/**
* @param string $target
*/
public function __construct($platforms)
public function __construct(array $platforms)
{
foreach ($platforms as $platform) {
$type = (isset($platform['type'])) ? $platform['type'] : '';
switch ($type) {
case self::CLIENT_TYPE_WEB:
case self::CLIENT_TYPE_FLUTTER_WEB:
$this->clients[] = (isset($platform['hostname'])) ? $platform['hostname'] : '';
break;
case self::CLIENT_TYPE_FLUTTER_IOS:
case self::CLIENT_TYPE_FLUTTER_ANDROID:
case self::CLIENT_TYPE_FLUTTER_MACOS:
case self::CLIENT_TYPE_FLUTTER_WINDOWS:
case self::CLIENT_TYPE_FLUTTER_LINUX:
case self::CLIENT_TYPE_ANDROID:
case self::CLIENT_TYPE_APPLE_IOS:
case self::CLIENT_TYPE_APPLE_MACOS:
case self::CLIENT_TYPE_APPLE_WATCHOS:
case self::CLIENT_TYPE_APPLE_TVOS:
case self::CLIENT_TYPE_REACT_NATIVE_IOS:
case self::CLIENT_TYPE_REACT_NATIVE_ANDROID:
$this->clients[] = (isset($platform['key'])) ? $platform['key'] : '';
break;
default:
# code...
break;
}
}
$this->hostnames = Platform::getHostnames($platforms);
$this->schemes = Platform::getSchemes($platforms);
}
public function getDescription(): string
{
if (!\array_key_exists($this->client, $this->platforms)) {
return 'Unsupported platform';
}
return 'Invalid Origin. Register your new client (' . $this->host . ') as a new '
. $this->platforms[$this->client] . ' platform on your project console dashboard';
}
/**
* Check if Origin has been allowed
* for access to the API
*
* @param mixed $origin
*
* Check if Origin is valid.
* @param mixed $origin The Origin URI.
* @return bool
*/
public function isValid($origin): bool
{
if (!is_string($origin)) {
$this->scheme = null;
$this->host = null;
if (!is_string($origin) || empty($origin)) {
return false;
}
$scheme = \parse_url($origin, PHP_URL_SCHEME);
$host = \parse_url($origin, PHP_URL_HOST);
$this->scheme = $this->parseScheme($origin);
$this->host = strtolower(parse_url($origin, PHP_URL_HOST) ?? '');
$this->host = $host;
$this->client = $scheme;
if (in_array($this->scheme, [Platform::SCHEME_HTTP, Platform::SCHEME_HTTPS], true)) {
$validator = new Hostname($this->hostnames);
return $validator->isValid($this->host);
}
if (empty($host)) {
if (!empty($this->scheme) && in_array($this->scheme, $this->schemes, true)) {
return true;
}
$validator = new Hostname($this->clients);
return false;
}
return $validator->isValid($host);
/**
* Get Description
* @return string
*/
public function getDescription(): string
{
$platform = $this->scheme ? Platform::getNameByScheme($this->scheme) : null;
$host = $this->host ? '(' . $this->host . ')' : '';
if (empty($this->host) && empty($this->scheme)) {
return 'Invalid Origin.';
}
return 'Invalid Origin. Register your new client ' . $host . ' as a new '
. $platform . ' platform on your project console dashboard';
}
/**
* Is array
*
* Function will return true if object is array.
*
* @return bool
*/
public function isArray(): bool
@@ -155,13 +82,35 @@ class Origin extends Validator
/**
* Get Type
*
* Returns validator type.
*
* @return string
*/
public function getType(): string
{
return self::TYPE_STRING;
}
/**
* Parses the scheme from a URI string.
*
* @param string $uri The URI string to parse.
* @return string|null The extracted scheme string (e.g., "http", "exp", "mailto")
*/
public function parseScheme(string $uri): ?string
{
$uri = trim($uri);
if ($uri === '') {
return null; // No scheme in empty string
}
$scheme = parse_url($uri, PHP_URL_SCHEME);
if ($scheme === false) {
if (preg_match('/^([a-z][a-z0-9+.-]*):/i', $uri, $matches)) {
return $matches[1];
} else {
return null;
}
} else {
return $scheme;
}
}
}
@@ -0,0 +1,25 @@
<?php
namespace Appwrite\Network\Validator;
use Appwrite\Network\Platform;
class Redirect extends Origin
{
/**
* Get Description
* @return string
*/
public function getDescription(): string
{
$platform = Platform::getNameByScheme($this->scheme);
$host = $this->host ? '(' . $this->host . ')' : '';
if (empty($this->host) && empty($this->scheme)) {
return 'Invalid URI.';
}
return 'Invalid URI. Register your new client ' . $host . ' as a new '
. $platform . ' platform on your project console dashboard';
}
}
+62
View File
@@ -3,7 +3,10 @@
namespace Appwrite\Platform;
use Swoole\Coroutine as Co;
use Utopia\CLI\Console;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Platform\Action as UtopiaAction;
@@ -16,6 +19,12 @@ class Action extends UtopiaAction
*/
protected mixed $logError;
protected array $filters = [
'subQueryKeys', 'subQueryWebhooks', 'subQueryPlatforms', 'subQueryProjectVariables', 'subQueryBlocks', 'subQueryDevKeys', // Project
'subQueryAuthenticators', 'subQuerySessions', 'subQueryTokens', 'subQueryChallenges', 'subQueryMemberships', 'subQueryTargets', 'subQueryTopicTargets',// Users
'subQueryVariables', // Sites
];
/**
* Foreach Document
* Call provided callback for each document in the collection
@@ -87,4 +96,57 @@ class Action extends UtopiaAction
$latestDocument = $results[array_key_last($results)];
}
}
public function disableSubqueries()
{
$filters = $this->filters;
foreach ($filters as $filter) {
Database::addFilter(
$filter,
function (mixed $value) {
return;
},
function (mixed $value, Document $document, Database $database) {
return [];
}
);
}
}
/**
* Dump Log Message
*
* Logs messages to console with timestamp, method context, and project details.
* Supports multiple log types: success, error, log, warning, and info (default).
*
* @param string $method The calling method name
* @param string $log The log message
* @param string $type The log type (success, error, log, warning, info)
* @param Document|null $project The project document for context
* @param string $collectionId The collection identifier
* @return void
*/
public function dump(string $method, string $log, string $type = 'info', ?Document $project = null, string $collectionId = ''): void
{
if (empty($project)) {
$project = new Document([]);
}
switch ($type) {
case 'success':
Console::success("[" . DateTime::now() . "] " . $method . ' ' . $type . ' ' . $project->getSequence() . ' ' . $project->getId() . ' ' . $collectionId . ' ' . $log);
break;
case 'error':
Console::error("[" . DateTime::now() . "] " . $method . ' ' . $type . ' ' . $project->getSequence() . ' ' . $project->getId() . ' ' . $collectionId . ' ' . $log);
break;
case 'log':
Console::log("[" . DateTime::now() . "] " . $method . ' ' . $type . ' ' . $project->getSequence() . ' ' . $project->getId() . ' ' . $collectionId . ' ' . $log);
break;
case 'warning':
Console::warning("[" . DateTime::now() . "] " . $method . ' ' . $type . ' ' . $project->getSequence() . ' ' . $project->getId() . ' ' . $collectionId . ' ' . $log);
break;
default:
Console::info("[" . DateTime::now() . "] " . $method . ' ' . $type . ' ' . $project->getSequence() . ' ' . $project->getId() . ' ' . $collectionId . ' ' . $log);
}
}
}
+2
View File
@@ -4,6 +4,7 @@ namespace Appwrite\Platform;
use Appwrite\Platform\Modules\Console;
use Appwrite\Platform\Modules\Core;
use Appwrite\Platform\Modules\Databases;
use Appwrite\Platform\Modules\Functions;
use Appwrite\Platform\Modules\Projects;
use Appwrite\Platform\Modules\Proxy;
@@ -16,6 +17,7 @@ class Appwrite extends Platform
public function __construct()
{
parent::__construct(new Core());
$this->addModule(new Databases\Module());
$this->addModule(new Projects\Module());
$this->addModule(new Functions\Module());
$this->addModule(new Sites\Module());
@@ -6,6 +6,7 @@ use Appwrite\Event\Build;
use Appwrite\Extend\Exception;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
@@ -226,6 +227,73 @@ class Base extends Action
]))
);
if (!empty($commitDetails['commitHash'])) {
$domain = "commit-" . substr($commitDetails['commitHash'], 0, 16) . ".{$sitesDomain}";
$ruleId = md5($domain);
try {
Authorization::skip(
fn () => $dbForPlatform->createDocument('rules', new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
'projectInternalId' => $project->getSequence(),
'domain' => $domain,
'type' => 'deployment',
'trigger' => 'deployment',
'deploymentId' => $deployment->getId(),
'deploymentInternalId' => $deployment->getSequence(),
'deploymentResourceType' => 'site',
'deploymentResourceId' => $site->getId(),
'deploymentResourceInternalId' => $site->getSequence(),
'deploymentVcsProviderBranch' => $providerBranch,
'status' => 'verified',
'certificateId' => '',
'search' => implode(' ', [$ruleId, $domain]),
'owner' => 'Appwrite',
'region' => $project->getAttribute('region')
]))
);
} catch (Duplicate $err) {
// Ignore, rule already exists; will be updated by builds worker
}
}
// VCS branch preview
if (!empty($providerBranch)) {
$branchPrefix = substr($providerBranch, 0, 16);
if (strlen($providerBranch) > 16) {
$remainingChars = substr($providerBranch, 16);
$branchPrefix .= '-' . substr(hash('sha256', $remainingChars), 0, 7);
}
$resourceProjectHash = substr(hash('sha256', $site->getId() . $project->getId()), 0, 7);
$domain = "branch-{$branchPrefix}-{$resourceProjectHash}.{$sitesDomain}";
$ruleId = md5($domain);
try {
Authorization::skip(
fn () => $dbForPlatform->createDocument('rules', new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
'projectInternalId' => $project->getSequence(),
'domain' => $domain,
'type' => 'deployment',
'trigger' => 'deployment',
'deploymentId' => $deployment->getId(),
'deploymentInternalId' => $deployment->getSequence(),
'deploymentResourceType' => 'site',
'deploymentResourceId' => $site->getId(),
'deploymentResourceInternalId' => $site->getSequence(),
'deploymentVcsProviderBranch' => $providerBranch,
'status' => 'verified',
'certificateId' => '',
'search' => implode(' ', [$ruleId, $domain]),
'owner' => 'Appwrite',
'region' => $project->getAttribute('region')
]))
);
} catch (Duplicate $err) {
// Ignore, rule already exists; will be updated by builds worker
}
}
$queueForBuilds
->setType(BUILD_TYPE_DEPLOYMENT)
->setResource($site)
@@ -57,7 +57,7 @@ class Get extends Action
->param('type', '', new WhiteList(['rules']), 'Resource type.')
->inject('response')
->inject('dbForPlatform')
->callback([$this, 'action']);
->callback($this->action(...));
}
public function action(
@@ -0,0 +1,24 @@
<?php
/**
* Constants for identifying database resource types.
*
* - Tables vs. Collections,
* - Rows vs. Documents, and
* - Columns vs. Attributes
*
* are functionally equivalent and share the same underlying API structure.
*
* These constants help distinguish the context of an action,
* enabling accurate error messages, realtime event triggers, and other context-aware behaviors.
*/
const ROWS = 'row';
const TABLES = 'table';
const COLUMNS = 'column';
const COLUMN_INDEX = 'columnIndex';
const INDEX = 'index';
const DOCUMENTS = 'document';
const ATTRIBUTES = 'attribute';
const COLLECTIONS = 'collection';
@@ -0,0 +1,109 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections;
use Appwrite\Extend\Exception;
use Utopia\Platform\Action as UtopiaAction;
use Utopia\Platform\Scope\HTTP;
abstract class Action extends UtopiaAction
{
/**
* The current API context (either 'table' or 'collection').
*/
private ?string $context = COLLECTIONS;
/**
* Get the response model used in the SDK and HTTP responses.
*/
abstract protected function getResponseModel(): string;
public function setHttpPath(string $path): UtopiaAction
{
if (str_contains($path, '/:databaseId/tables')) {
$this->context = TABLES;
}
return parent::setHttpPath($path);
}
/**
* Get the current API context.
*/
final protected function getContext(): string
{
return $this->context;
}
/**
* Get the key used in event parameters (e.g., 'collectionId' or 'tableId').
*/
final protected function getEventsParamKey(): string
{
return $this->getContext() . 'Id';
}
/**
* Determine if the current action is for the Collections API.
*/
final protected function isCollectionsAPI(): bool
{
return $this->getContext() === COLLECTIONS;
}
/**
* Get the SDK group name for the current action.
*/
final protected function getSdkGroup(): string
{
return $this->isCollectionsAPI() ? 'collections' : 'tables';
}
/**
* Get the SDK namespace for the current action.
*/
final protected function getSdkNamespace(): string
{
return $this->isCollectionsAPI() ? 'databases' : 'tables';
}
/**
* Get the exception to throw when the resource already exists.
*/
final protected function getDuplicateException(): string
{
return $this->isCollectionsAPI()
? Exception::COLLECTION_ALREADY_EXISTS
: Exception::TABLE_ALREADY_EXISTS;
}
/**
* Get the appropriate index invalid exception.
*/
final protected function getInvalidIndexException(): string
{
return $this->isCollectionsAPI()
? Exception::INDEX_INVALID
: Exception::COLUMN_INDEX_INVALID;
}
/**
* Get the exception to throw when the resource is not found.
*/
final protected function getNotFoundException(): string
{
return $this->isCollectionsAPI()
? Exception::COLLECTION_NOT_FOUND
: Exception::TABLE_NOT_FOUND;
}
/**
* Get the exception to throw when the resource limit is exceeded.
*/
final protected function getLimitException(): string
{
return $this->isCollectionsAPI()
? Exception::COLLECTION_LIMIT_EXCEEDED
: Exception::TABLE_LIMIT_EXCEEDED;
}
}
@@ -0,0 +1,646 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response as UtopiaResponse;
use Throwable;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Exception\Index as IndexException;
use Utopia\Database\Exception\Limit as LimitException;
use Utopia\Database\Exception\Relationship as RelationshipException;
use Utopia\Database\Exception\Structure as StructureException;
use Utopia\Database\Exception\Truncate as TruncateException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Structure;
use Utopia\Platform\Action as UtopiaAction;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Range;
abstract class Action extends UtopiaAction
{
/**
* @var string|null The current context (either 'column' or 'attribute')
*/
private ?string $context = ATTRIBUTES;
/**
* Get the correct response model.
*/
abstract protected function getResponseModel(): string|array;
public function setHttpPath(string $path): UtopiaAction
{
if (str_contains($path, '/:databaseId/tables')) {
$this->context = COLUMNS;
}
return parent::setHttpPath($path);
}
/**
* Get the current context.
*/
final protected function getContext(): string
{
return $this->context;
}
/**
* Returns true if current context is Collections API.
*/
final protected function isCollectionsAPI(): bool
{
// columns in tables context
// attributes in collections context
return $this->getContext() === ATTRIBUTES;
}
/**
* Get the SDK group name for the current action.
*
* Can be used for XList operations as well!
*/
final protected function getSdkGroup(): string
{
return $this->isCollectionsAPI() ? 'attributes' : 'columns';
}
/**
* Get the SDK namespace for the current action.
*/
final protected function getSdkNamespace(): string
{
return $this->isCollectionsAPI() ? 'databases' : 'tables';
}
/**
* Get the appropriate parent level not found exception.
*/
final protected function getParentNotFoundException(): string
{
return $this->isCollectionsAPI()
? Exception::COLLECTION_NOT_FOUND
: Exception::TABLE_NOT_FOUND;
}
/**
* Get the appropriate not found exception.
*/
final protected function getNotFoundException(): string
{
return $this->isCollectionsAPI()
? Exception::ATTRIBUTE_NOT_FOUND
: Exception::COLUMN_NOT_FOUND;
}
/**
* Get the appropriate not found exception.
*/
final protected function getIndexDependencyException(): string
{
return $this->isCollectionsAPI()
? Exception::INDEX_DEPENDENCY
: Exception::COLUMN_INDEX_DEPENDENCY;
}
/**
* Get the appropriate already exists exception.
*/
final protected function getDuplicateException(): string
{
return $this->isCollectionsAPI()
? Exception::ATTRIBUTE_ALREADY_EXISTS
: Exception::COLUMN_ALREADY_EXISTS;
}
/**
* Get the correct invalid structure message.
*/
final protected function getInvalidStructureException(): string
{
return $this->isCollectionsAPI()
? Exception::DOCUMENT_INVALID_STRUCTURE
: Exception::ROW_INVALID_STRUCTURE;
}
/**
* Get the appropriate limit exceeded exception.
*/
final protected function getLimitException(): string
{
return $this->isCollectionsAPI()
? Exception::ATTRIBUTE_LIMIT_EXCEEDED
: Exception::COLUMN_LIMIT_EXCEEDED;
}
/**
* Get the appropriate index invalid exception.
*/
final protected function getInvalidIndexException(): string
{
return $this->isCollectionsAPI()
? Exception::INDEX_INVALID
: Exception::COLUMN_INDEX_INVALID;
}
/**
* Get the correct default unsupported message.
*/
final protected function getDefaultUnsupportedException(): string
{
return $this->isCollectionsAPI()
? Exception::ATTRIBUTE_DEFAULT_UNSUPPORTED
: Exception::COLUMN_DEFAULT_UNSUPPORTED;
}
/**
* Get the correct format unsupported message.
*/
final protected function getFormatUnsupportedException(): string
{
return $this->isCollectionsAPI()
? Exception::ATTRIBUTE_FORMAT_UNSUPPORTED
: Exception::COLUMN_FORMAT_UNSUPPORTED;
}
/**
* Get the exception for invalid type or format mismatch.
*/
final protected function getTypeInvalidException(): string
{
return $this->isCollectionsAPI()
? Exception::ATTRIBUTE_TYPE_INVALID
: Exception::COLUMN_TYPE_INVALID;
}
/**
* Get the exception for resizing invalid attributes/columns.
*/
final protected function getInvalidResizeException(): string
{
return $this->isCollectionsAPI()
? Exception::ATTRIBUTE_INVALID_RESIZE
: Exception::COLUMN_INVALID_RESIZE;
}
/**
* Get the exception for invalid attributes/columns value.
*/
final protected function getInvalidValueException(): string
{
return $this->isCollectionsAPI()
? Exception::ATTRIBUTE_VALUE_INVALID
: Exception::COLUMN_VALUE_INVALID;
}
/**
* Get the exception for non-available column/attribute.
*/
final protected function getNotAvailableException(): string
{
return $this->isCollectionsAPI()
? Exception::ATTRIBUTE_NOT_AVAILABLE
: Exception::COLUMN_NOT_AVAILABLE;
}
/**
* Get the correct collections context for Events queue.
*/
final protected function getCollectionsEventsContext(): string
{
return $this->isCollectionsAPI() ? 'collection' : 'table';
}
/**
* Get the proper column/attribute type based on set context.
*/
final protected function getModel(string $type, string $format): string
{
$isCollections = $this->isCollectionsAPI();
return match ($type) {
Database::VAR_BOOLEAN => $isCollections
? UtopiaResponse::MODEL_ATTRIBUTE_BOOLEAN
: UtopiaResponse::MODEL_COLUMN_BOOLEAN,
Database::VAR_INTEGER => $isCollections
? UtopiaResponse::MODEL_ATTRIBUTE_INTEGER
: UtopiaResponse::MODEL_COLUMN_INTEGER,
Database::VAR_FLOAT => $isCollections
? UtopiaResponse::MODEL_ATTRIBUTE_FLOAT
: UtopiaResponse::MODEL_COLUMN_FLOAT,
Database::VAR_DATETIME => $isCollections
? UtopiaResponse::MODEL_ATTRIBUTE_DATETIME
: UtopiaResponse::MODEL_COLUMN_DATETIME,
Database::VAR_RELATIONSHIP => $isCollections
? UtopiaResponse::MODEL_ATTRIBUTE_RELATIONSHIP
: UtopiaResponse::MODEL_COLUMN_RELATIONSHIP,
Database::VAR_STRING => match ($format) {
APP_DATABASE_ATTRIBUTE_EMAIL => $isCollections
? UtopiaResponse::MODEL_ATTRIBUTE_EMAIL
: UtopiaResponse::MODEL_COLUMN_EMAIL,
APP_DATABASE_ATTRIBUTE_ENUM => $isCollections
? UtopiaResponse::MODEL_ATTRIBUTE_ENUM
: UtopiaResponse::MODEL_COLUMN_ENUM,
APP_DATABASE_ATTRIBUTE_IP => $isCollections
? UtopiaResponse::MODEL_ATTRIBUTE_IP
: UtopiaResponse::MODEL_COLUMN_IP,
APP_DATABASE_ATTRIBUTE_URL => $isCollections
? UtopiaResponse::MODEL_ATTRIBUTE_URL
: UtopiaResponse::MODEL_COLUMN_URL,
default => $isCollections
? UtopiaResponse::MODEL_ATTRIBUTE_STRING
: UtopiaResponse::MODEL_COLUMN_STRING,
},
default => $isCollections
? UtopiaResponse::MODEL_ATTRIBUTE
: UtopiaResponse::MODEL_COLUMN,
};
}
final protected function createAttribute(string $databaseId, string $collectionId, Document $attribute, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): Document
{
$key = $attribute->getAttribute('key');
$type = $attribute->getAttribute('type', '');
$size = $attribute->getAttribute('size', 0);
$required = $attribute->getAttribute('required', true);
$signed = $attribute->getAttribute('signed', true); // integers are signed by default
$array = $attribute->getAttribute('array', false);
$format = $attribute->getAttribute('format', '');
$formatOptions = $attribute->getAttribute('formatOptions', []);
$filters = $attribute->getAttribute('filters', []); // filters are hidden from the endpoint
$default = $attribute->getAttribute('default');
$options = $attribute->getAttribute('options', []);
$db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($db->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = $dbForProject->getDocument('database_' . $db->getSequence(), $collectionId);
if ($collection->isEmpty()) {
throw new Exception($this->getParentNotFoundException());
}
if (!empty($format)) {
if (!Structure::hasFormat($format, $type)) {
throw new Exception($this->getFormatUnsupportedException(), "Format $format not available for $type columns.");
}
}
// Must throw here since dbForProject->createAttribute is performed by db worker
if ($required && isset($default)) {
throw new Exception($this->getDefaultUnsupportedException(), 'Cannot set default value for required ' . $this->getContext());
}
if ($array && isset($default)) {
throw new Exception($this->getDefaultUnsupportedException(), 'Cannot set default value for array ' . $this->getContext() . 's');
}
if ($type === Database::VAR_RELATIONSHIP) {
$options['side'] = Database::RELATION_SIDE_PARENT;
$relatedCollection = $dbForProject->getDocument('database_' . $db->getSequence(), $options['relatedCollection'] ?? '');
if ($relatedCollection->isEmpty()) {
$parent = $this->isCollectionsAPI() ? 'collection' : 'table';
throw new Exception($this->getParentNotFoundException(), "The related $parent was not found.");
}
}
try {
$attribute = new Document([
'$id' => ID::custom($db->getSequence() . '_' . $collection->getSequence() . '_' . $key),
'key' => $key,
'databaseInternalId' => $db->getSequence(),
'databaseId' => $db->getId(),
'collectionInternalId' => $collection->getSequence(),
'collectionId' => $collectionId,
'type' => $type,
'status' => 'processing', // processing, available, failed, deleting, stuck
'size' => $size,
'required' => $required,
'signed' => $signed,
'default' => $default,
'array' => $array,
'format' => $format,
'formatOptions' => $formatOptions,
'filters' => $filters,
'options' => $options,
]);
$dbForProject->checkAttribute($collection, $attribute);
$attribute = $dbForProject->createDocument('attributes', $attribute);
} catch (DuplicateException) {
throw new Exception($this->getDuplicateException());
} catch (LimitException) {
throw new Exception($this->getLimitException());
} catch (Throwable $e) {
$dbForProject->purgeCachedDocument('database_' . $db->getSequence(), $collectionId);
$dbForProject->purgeCachedCollection('database_' . $db->getSequence() . '_collection_' . $collection->getSequence());
throw $e;
}
$dbForProject->purgeCachedDocument('database_' . $db->getSequence(), $collectionId);
$dbForProject->purgeCachedCollection('database_' . $db->getSequence() . '_collection_' . $collection->getSequence());
if ($type === Database::VAR_RELATIONSHIP && $options['twoWay']) {
$twoWayKey = $options['twoWayKey'];
$options['relatedCollection'] = $collection->getId();
$options['twoWayKey'] = $key;
$options['side'] = Database::RELATION_SIDE_CHILD;
try {
$twoWayAttribute = new Document([
'$id' => ID::custom($db->getSequence() . '_' . $relatedCollection->getSequence() . '_' . $twoWayKey),
'key' => $twoWayKey,
'databaseInternalId' => $db->getSequence(),
'databaseId' => $db->getId(),
'collectionInternalId' => $relatedCollection->getSequence(),
'collectionId' => $relatedCollection->getId(),
'type' => $type,
'status' => 'processing', // processing, available, failed, deleting, stuck
'size' => $size,
'required' => $required,
'signed' => $signed,
'default' => $default,
'array' => $array,
'format' => $format,
'formatOptions' => $formatOptions,
'filters' => $filters,
'options' => $options,
]);
$dbForProject->checkAttribute($relatedCollection, $twoWayAttribute);
$dbForProject->createDocument('attributes', $twoWayAttribute);
} catch (DuplicateException) {
throw new Exception($this->getDuplicateException());
} catch (LimitException) {
throw new Exception($this->getLimitException());
} catch (StructureException) {
throw new Exception($this->getInvalidStructureException());
} catch (Throwable $e) {
$dbForProject->deleteDocument('attributes', $attribute->getId());
throw $e;
} finally {
$dbForProject->purgeCachedDocument('database_' . $db->getSequence(), $collectionId);
$dbForProject->purgeCachedCollection('database_' . $db->getSequence() . '_collection_' . $collection->getSequence());
}
// If operation succeeded, purge the cache for the related collection too
$dbForProject->purgeCachedDocument('database_' . $db->getSequence(), $relatedCollection->getId());
$dbForProject->purgeCachedCollection('database_' . $db->getSequence() . '_collection_' . $relatedCollection->getSequence());
}
$queueForDatabase
->setType(DATABASE_TYPE_CREATE_ATTRIBUTE)
->setDatabase($db);
if ($this->isCollectionsAPI()) {
$queueForDatabase
->setDocument($attribute)
->setCollection($collection);
} else {
$queueForDatabase
->setRow($attribute)
->setTable($collection);
}
$queueForEvents
->setContext('database', $db)
->setParam('databaseId', $databaseId)
->setParam('collectionId', $collection->getId())
->setParam('tableId', $collection->getId())
->setParam('attributeId', $attribute->getId())
->setParam('columnId', $attribute->getId())
->setContext($this->getCollectionsEventsContext(), $collection);
$response->setStatusCode(SwooleResponse::STATUS_CODE_CREATED);
return $attribute;
}
final protected function updateAttribute(string $databaseId, string $collectionId, string $key, Database $dbForProject, Event $queueForEvents, string $type, int $size = null, string $filter = null, string|bool|int|float $default = null, bool $required = null, int|float|null $min = null, int|float|null $max = null, array $elements = null, array $options = [], string $newKey = null): Document
{
$db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($db->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = $dbForProject->getDocument('database_' . $db->getSequence(), $collectionId);
if ($collection->isEmpty()) {
throw new Exception($this->getParentNotFoundException());
}
$attribute = $dbForProject->getDocument('attributes', $db->getSequence() . '_' . $collection->getSequence() . '_' . $key);
if ($attribute->isEmpty()) {
throw new Exception($this->getNotFoundException());
}
if ($attribute->getAttribute('status') !== 'available') {
throw new Exception($this->getNotAvailableException());
}
if ($attribute->getAttribute(('type') !== $type)) {
throw new Exception($this->getTypeInvalidException());
}
if ($attribute->getAttribute('type') === Database::VAR_STRING && $attribute->getAttribute(('filter') !== $filter)) {
throw new Exception($this->getTypeInvalidException());
}
if ($required && isset($default)) {
throw new Exception($this->getDefaultUnsupportedException(), 'Cannot set default value for required ' . $this->getContext());
}
if ($attribute->getAttribute('array', false) && isset($default)) {
throw new Exception($this->getDefaultUnsupportedException(), 'Cannot set default value for array ' . $this->getContext() . 's');
}
$collectionId = 'database_' . $db->getSequence() . '_collection_' . $collection->getSequence();
$attribute
->setAttribute('default', $default)
->setAttribute('required', $required);
if (!empty($size)) {
$attribute->setAttribute('size', $size);
}
switch ($attribute->getAttribute('format')) {
case APP_DATABASE_ATTRIBUTE_INT_RANGE:
case APP_DATABASE_ATTRIBUTE_FLOAT_RANGE:
$min ??= $attribute->getAttribute('formatOptions')['min'];
$max ??= $attribute->getAttribute('formatOptions')['max'];
if ($min > $max) {
throw new Exception($this->getInvalidValueException(), 'Minimum value must be lesser than maximum value');
}
if ($attribute->getAttribute('format') === APP_DATABASE_ATTRIBUTE_INT_RANGE) {
$validator = new Range($min, $max, Database::VAR_INTEGER);
} else {
$validator = new Range($min, $max, Database::VAR_FLOAT);
if (!is_null($default)) {
$default = \floatval($default);
}
}
if (!is_null($default) && !$validator->isValid($default)) {
throw new Exception($this->getInvalidValueException(), $validator->getDescription());
}
$options = [
'min' => $min,
'max' => $max
];
$attribute->setAttribute('formatOptions', $options);
break;
case APP_DATABASE_ATTRIBUTE_ENUM:
if (empty($elements)) {
throw new Exception($this->getInvalidValueException(), 'Enum elements must not be empty');
}
foreach ($elements as $element) {
if (\strlen($element) === 0) {
throw new Exception($this->getInvalidValueException(), 'Each enum element must not be empty');
}
}
if (!is_null($default) && !in_array($default, $elements)) {
throw new Exception($this->getInvalidValueException(), 'Default value not found in elements');
}
$options = [
'elements' => $elements
];
$attribute->setAttribute('formatOptions', $options);
break;
}
if ($type === Database::VAR_RELATIONSHIP) {
$primaryDocumentOptions = \array_merge($attribute->getAttribute('options', []), $options);
$attribute->setAttribute('options', $primaryDocumentOptions);
try {
$dbForProject->updateRelationship(
collection: $collectionId,
id: $key,
newKey: $newKey,
onDelete: $primaryDocumentOptions['onDelete'],
);
} catch (IndexException) {
throw new Exception(Exception::INDEX_INVALID);
} catch (LimitException) {
throw new Exception($this->getLimitException());
} catch (RelationshipException $e) {
throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage());
} catch (StructureException $e) {
throw new Exception($this->getInvalidStructureException(), $e->getMessage());
}
if ($primaryDocumentOptions['twoWay']) {
$relatedCollection = $dbForProject->getDocument('database_' . $db->getSequence(), $primaryDocumentOptions['relatedCollection']);
$relatedAttribute = $dbForProject->getDocument('attributes', $db->getSequence() . '_' . $relatedCollection->getSequence() . '_' . $primaryDocumentOptions['twoWayKey']);
if (!empty($newKey) && $newKey !== $key) {
$options['twoWayKey'] = $newKey;
}
$relatedOptions = \array_merge($relatedAttribute->getAttribute('options'), $options);
$relatedAttribute->setAttribute('options', $relatedOptions);
$dbForProject->updateDocument('attributes', $db->getSequence() . '_' . $relatedCollection->getSequence() . '_' . $primaryDocumentOptions['twoWayKey'], $relatedAttribute);
$dbForProject->purgeCachedDocument('database_' . $db->getSequence(), $relatedCollection->getId());
}
} else {
try {
$dbForProject->updateAttribute(
collection: $collectionId,
id: $key,
size: $size,
required: $required,
default: $default,
formatOptions: $options,
newKey: $newKey ?? null
);
} catch (DuplicateException) {
throw new Exception($this->getDuplicateException());
} catch (IndexException $e) {
throw new Exception($this->getInvalidIndexException(), $e->getMessage());
} catch (LimitException) {
throw new Exception($this->getLimitException());
} catch (TruncateException) {
throw new Exception($this->getInvalidResizeException());
}
}
if (!empty($newKey) && $key !== $newKey) {
$originalUid = $attribute->getId();
$attribute
->setAttribute('$id', ID::custom($db->getSequence() . '_' . $collection->getSequence() . '_' . $newKey))
->setAttribute('key', $newKey);
try {
$dbForProject->updateDocument('attributes', $originalUid, $attribute);
} catch (DuplicateException) {
throw new Exception($this->getDuplicateException());
}
/**
* @var Document $index
*/
foreach ($collection->getAttribute('indexes') as $index) {
/**
* @var string[] $attribute
*/
$attribute = $index->getAttribute('attributes', []);
$found = \array_search($key, $attribute);
if ($found !== false) {
$attribute[$found] = $newKey;
$index->setAttribute('attributes', $attribute);
$dbForProject->updateDocument('indexes', $index->getId(), $index);
}
}
} else {
$attribute = $dbForProject->updateDocument('attributes', $db->getSequence() . '_' . $collection->getSequence() . '_' . $key, $attribute);
}
$dbForProject->purgeCachedDocument('database_' . $db->getSequence(), $collection->getId());
$queueForEvents
->setContext('database', $db)
->setParam('databaseId', $databaseId)
->setParam('collectionId', $collection->getId())
->setParam('tableId', $collection->getId())
->setParam('attributeId', $attribute->getId())
->setParam('columnId', $attribute->getId())
->setContext($this->getCollectionsEventsContext(), $collection);
return $attribute;
}
}
@@ -0,0 +1,84 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Boolean;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
class Create extends Action
{
public static function getName(): string
{
return 'createBooleanAttribute';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_ATTRIBUTE_BOOLEAN;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/boolean')
->desc('Create boolean attribute')
->groups(['api', 'database', 'schema'])
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('audits.event', 'attribute.create')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/create-boolean-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_ACCEPTED,
model: $this->getResponseModel(),
)
]
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('default', null, new Boolean(), 'Default value for attribute when not provided. Cannot be set when attribute is required.', true)
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?bool $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void
{
$attribute = $this->createAttribute($databaseId, $collectionId, new Document([
'key' => $key,
'type' => Database::VAR_BOOLEAN,
'size' => 0,
'required' => $required,
'default' => $default,
'array' => $array,
]), $response, $dbForProject, $queueForDatabase, $queueForEvents);
$response
->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED)
->dynamic($attribute, $this->getResponseModel());
}
}
@@ -0,0 +1,87 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Boolean;
use Appwrite\Event\Event;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
class Update extends Action
{
public static function getName(): string
{
return 'updateBooleanAttribute';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_ATTRIBUTE_BOOLEAN;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/boolean/:key')
->desc('Update boolean attribute')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].update')
->label('audits.event', 'attribute.update')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/update-boolean-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#createCollection).')
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('default', null, new Nullable(new Boolean()), 'Default value for attribute when not provided. Cannot be set when attribute is required.')
->param('newKey', null, new Key(), 'New attribute key.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?bool $default, ?string $newKey, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents): void
{
$attribute = $this->updateAttribute(
databaseId: $databaseId,
collectionId: $collectionId,
key: $key,
dbForProject: $dbForProject,
queueForEvents: $queueForEvents,
type: Database::VAR_BOOLEAN,
default: $default,
required: $required,
newKey: $newKey
);
$response
->setStatusCode(SwooleResponse::STATUS_CODE_OK)
->dynamic($attribute, $this->getResponseModel());
}
}
@@ -0,0 +1,94 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Datetime;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Datetime as DatetimeValidator;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
class Create extends Action
{
public static function getName(): string
{
return 'createDatetimeAttribute';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_ATTRIBUTE_DATETIME;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/datetime')
->desc('Create datetime attribute')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
->label('audits.event', 'attribute.create')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/create-datetime-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_ACCEPTED,
model: $this->getResponseModel()
)
]
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#createCollection).')
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('default', null, fn (Database $dbForProject) => new DatetimeValidator($dbForProject->getAdapter()->getMinDateTime(), $dbForProject->getAdapter()->getMaxDateTime()), 'Default value for the attribute in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Cannot be set when attribute is required.', true, ['dbForProject'])
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void
{
$attribute = $this->createAttribute(
$databaseId,
$collectionId,
new Document([
'key' => $key,
'type' => Database::VAR_DATETIME,
'size' => 0,
'required' => $required,
'default' => $default,
'array' => $array,
'filters' => ['datetime'],
]),
$response,
$dbForProject,
$queueForDatabase,
$queueForEvents
);
$response
->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED)
->dynamic($attribute, $this->getResponseModel());
}
}
@@ -0,0 +1,88 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Datetime;
use Appwrite\Event\Event;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Datetime as DatetimeValidator;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
class Update extends Action
{
public static function getName(): string
{
return 'updateDatetimeAttribute';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_ATTRIBUTE_DATETIME;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/datetime/:key')
->desc('Update datetime attribute')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].update')
->label('audits.event', 'attribute.update')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/update-datetime-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel()
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('default', null, fn (Database $dbForProject) => new Nullable(new DatetimeValidator($dbForProject->getAdapter()->getMinDateTime(), $dbForProject->getAdapter()->getMaxDateTime())), 'Default value for attribute when not provided. Cannot be set when attribute is required.', injections: ['dbForProject'])
->param('newKey', null, new Key(), 'New attribute key.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, ?string $newKey, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents): void
{
$attribute = $this->updateAttribute(
databaseId: $databaseId,
collectionId: $collectionId,
key: $key,
dbForProject: $dbForProject,
queueForEvents: $queueForEvents,
type: Database::VAR_DATETIME,
default: $default,
required: $required,
newKey: $newKey
);
$response
->setStatusCode(SwooleResponse::STATUS_CODE_OK)
->dynamic($attribute, $this->getResponseModel());
}
}
@@ -0,0 +1,154 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes;
use Appwrite\Event\Database as EventDatabase;
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 as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\IndexDependency as IndexDependencyValidator;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
class Delete extends Action
{
public static function getName(): string
{
return 'deleteAttribute';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_NONE;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_DELETE)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/:key')
->desc('Delete attribute')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].update')
->label('audits.event', 'attribute.delete')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/delete-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_NOCONTENT,
model: UtopiaResponse::MODEL_NONE,
)
],
contentType: ContentType::NONE
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('key', '', new Key(), 'Attribute Key.')
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void
{
$db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($db->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = $dbForProject->getDocument('database_' . $db->getSequence(), $collectionId);
if ($collection->isEmpty()) {
throw new Exception($this->getParentNotFoundException());
}
$attribute = $dbForProject->getDocument('attributes', $db->getSequence() . '_' . $collection->getSequence() . '_' . $key);
if ($attribute->isEmpty()) {
throw new Exception($this->getNotFoundException());
}
$validator = new IndexDependencyValidator(
$collection->getAttribute('indexes'),
$dbForProject->getAdapter()->getSupportForCastIndexArray(),
);
if (!$validator->isValid($attribute)) {
throw new Exception($this->getIndexDependencyException());
}
if ($attribute->getAttribute('status') === 'available') {
$attribute = $dbForProject->updateDocument('attributes', $attribute->getId(), $attribute->setAttribute('status', 'deleting'));
}
$dbForProject->purgeCachedDocument('database_' . $db->getSequence(), $collectionId);
$dbForProject->purgeCachedCollection('database_' . $db->getSequence() . '_collection_' . $collection->getSequence());
if ($attribute->getAttribute('type') === Database::VAR_RELATIONSHIP) {
$options = $attribute->getAttribute('options');
if ($options['twoWay']) {
$relatedCollection = $dbForProject->getDocument('database_' . $db->getSequence(), $options['relatedCollection']);
if ($relatedCollection->isEmpty()) {
throw new Exception($this->getParentNotFoundException());
}
$relatedAttribute = $dbForProject->getDocument('attributes', $db->getSequence() . '_' . $relatedCollection->getSequence() . '_' . $options['twoWayKey']);
if ($relatedAttribute->isEmpty()) {
throw new Exception($this->getNotFoundException());
}
if ($relatedAttribute->getAttribute('status') === 'available') {
$dbForProject->updateDocument('attributes', $relatedAttribute->getId(), $relatedAttribute->setAttribute('status', 'deleting'));
}
$dbForProject->purgeCachedDocument('database_' . $db->getSequence(), $options['relatedCollection']);
$dbForProject->purgeCachedCollection('database_' . $db->getSequence() . '_collection_' . $relatedCollection->getSequence());
}
}
$queueForDatabase
->setDatabase($db)
->setType(DATABASE_TYPE_DELETE_ATTRIBUTE);
if ($this->isCollectionsAPI()) {
$queueForDatabase
->setRow($attribute)
->setTable($collection);
} else {
$queueForDatabase
->setDocument($attribute)
->setCollection($collection);
}
$type = $attribute->getAttribute('type');
$format = $attribute->getAttribute('format');
$model = $this->getModel($type, $format);
$queueForEvents
->setContext('database', $db)
->setParam('databaseId', $databaseId)
->setParam('collectionId', $collection->getId())
->setParam('tableId', $collection->getId())
->setParam('attributeId', $attribute->getId())
->setParam('columnId', $attribute->getId())
->setPayload($response->output($attribute, $model))
->setContext($this->getCollectionsEventsContext(), $collection);
$response->noContent();
}
}
@@ -0,0 +1,94 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Email;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Network\Validator\Email;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
class Create extends Action
{
public static function getName(): string
{
return 'createEmailAttribute';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_ATTRIBUTE_EMAIL;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/email')
->desc('Create email attribute')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
->label('audits.event', 'attribute.create')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/create-email-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_ACCEPTED,
model: $this->getResponseModel(),
)
]
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('default', null, new Email(), 'Default value for attribute when not provided. Cannot be set when attribute is required.', true)
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void
{
$attribute = $this->createAttribute(
$databaseId,
$collectionId,
new Document([
'key' => $key,
'type' => Database::VAR_STRING,
'size' => 254,
'required' => $required,
'default' => $default,
'array' => $array,
'format' => APP_DATABASE_ATTRIBUTE_EMAIL,
]),
$response,
$dbForProject,
$queueForDatabase,
$queueForEvents
);
$response
->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED)
->dynamic($attribute, $this->getResponseModel());
}
}
@@ -0,0 +1,89 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Email;
use Appwrite\Event\Event;
use Appwrite\Network\Validator\Email;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
class Update extends Action
{
public static function getName(): string
{
return 'updateEmailAttribute';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_ATTRIBUTE_EMAIL;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/email/:key')
->desc('Update email attribute')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].update')
->label('audits.event', 'attribute.update')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/update-email-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('default', null, new Nullable(new Email()), 'Default value for attribute when not provided. Cannot be set when attribute is required.')
->param('newKey', null, new Key(), 'New Attribute Key.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, ?string $newKey, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents): void
{
$attribute = $this->updateAttribute(
databaseId: $databaseId,
collectionId: $collectionId,
key: $key,
dbForProject: $dbForProject,
queueForEvents: $queueForEvents,
type: Database::VAR_STRING,
filter: APP_DATABASE_ATTRIBUTE_EMAIL,
default: $default,
required: $required,
newKey: $newKey
);
$response
->setStatusCode(SwooleResponse::STATUS_CODE_OK)
->dynamic($attribute, $this->getResponseModel());
}
}
@@ -0,0 +1,102 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Enum;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
class Create extends Action
{
public static function getName(): string
{
return 'createEnumAttribute';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_ATTRIBUTE_ENUM;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/enum')
->desc('Create enum attribute')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
->label('audits.event', 'attribute.create')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/create-attribute-enum.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_ACCEPTED,
model: $this->getResponseModel(),
)
]
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('key', '', new Key(), 'Attribute Key.')
->param('elements', [], new ArrayList(new Text(Database::LENGTH_KEY), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of enum values.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('default', null, new Text(0), 'Default value for attribute when not provided. Cannot be set when attribute is required.', true)
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, array $elements, ?bool $required, ?string $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void
{
if (!is_null($default) && !\in_array($default, $elements, true)) {
throw new Exception($this->getInvalidValueException(), 'Default value not found in elements');
}
$attribute = $this->createAttribute(
$databaseId,
$collectionId,
new Document([
'key' => $key,
'type' => Database::VAR_STRING,
'size' => Database::LENGTH_KEY,
'required' => $required,
'default' => $default,
'array' => $array,
'format' => APP_DATABASE_ATTRIBUTE_ENUM,
'formatOptions' => ['elements' => $elements],
]),
$response,
$dbForProject,
$queueForDatabase,
$queueForEvents
);
$response
->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED)
->dynamic($attribute, $this->getResponseModel());
}
}
@@ -0,0 +1,92 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Enum;
use Appwrite\Event\Event;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
class Update extends Action
{
public static function getName(): string
{
return 'updateEnumAttribute';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_ATTRIBUTE_ENUM;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/enum/:key')
->desc('Update enum attribute')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].update')
->label('audits.event', 'attribute.update')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/update-enum-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('key', '', new Key(), 'Attribute Key.')
->param('elements', null, new ArrayList(new Text(Database::LENGTH_KEY), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Updated list of enum values.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('default', null, new Nullable(new Text(0)), 'Default value for attribute when not provided. Cannot be set when attribute is required.')
->param('newKey', null, new Key(), 'New Attribute Key.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, ?array $elements, ?bool $required, ?string $default, ?string $newKey, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents): void
{
$attribute = $this->updateAttribute(
databaseId: $databaseId,
collectionId: $collectionId,
key: $key,
dbForProject: $dbForProject,
queueForEvents: $queueForEvents,
type: Database::VAR_STRING,
filter: APP_DATABASE_ATTRIBUTE_ENUM,
default: $default,
required: $required,
elements: $elements,
newKey: $newKey
);
$response
->setStatusCode(SwooleResponse::STATUS_CODE_OK)
->dynamic($attribute, $this->getResponseModel());
}
}
@@ -0,0 +1,109 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Float;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\FloatValidator;
use Utopia\Validator\Range;
class Create extends Action
{
public static function getName(): string
{
return 'createFloatAttribute';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_ATTRIBUTE_FLOAT;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/float')
->desc('Create float attribute')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
->label('audits.event', 'attribute.create')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/create-float-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_ACCEPTED,
model: $this->getResponseModel(),
)
]
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('min', null, new FloatValidator(), 'Minimum value.', true)
->param('max', null, new FloatValidator(), 'Maximum value.', true)
->param('default', null, new FloatValidator(), 'Default value. Cannot be set when required.', true)
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?float $min, ?float $max, ?float $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void
{
$min ??= -PHP_FLOAT_MAX;
$max ??= PHP_FLOAT_MAX;
if ($min > $max) {
throw new Exception($this->getInvalidValueException(), 'Minimum value must be lesser than maximum value');
}
$validator = new Range($min, $max, Database::VAR_FLOAT);
if (!\is_null($default) && !$validator->isValid($default)) {
throw new Exception($this->getInvalidValueException(), $validator->getDescription());
}
$attribute = $this->createAttribute($databaseId, $collectionId, new Document([
'key' => $key,
'type' => Database::VAR_FLOAT,
'size' => 0,
'required' => $required,
'default' => $default,
'array' => $array,
'format' => APP_DATABASE_ATTRIBUTE_FLOAT_RANGE,
'formatOptions' => ['min' => $min, 'max' => $max],
]), $response, $dbForProject, $queueForDatabase, $queueForEvents);
$formatOptions = $attribute->getAttribute('formatOptions', []);
if (!empty($formatOptions)) {
$attribute->setAttribute('min', \floatval($formatOptions['min']));
$attribute->setAttribute('max', \floatval($formatOptions['max']));
}
$response
->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED)
->dynamic($attribute, $this->getResponseModel());
}
}
@@ -0,0 +1,98 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Float;
use Appwrite\Event\Event;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\FloatValidator;
use Utopia\Validator\Nullable;
class Update extends Action
{
public static function getName(): string
{
return 'updateFloatAttribute';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_ATTRIBUTE_FLOAT;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/float/:key')
->desc('Update float attribute')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].update')
->label('audits.event', 'attribute.update')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/update-float-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('min', null, new FloatValidator(), 'Minimum value.', true)
->param('max', null, new FloatValidator(), 'Maximum value.', true)
->param('default', null, new Nullable(new FloatValidator()), 'Default value. Cannot be set when required.')
->param('newKey', null, new Key(), 'New Attribute Key.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?float $min, ?float $max, ?float $default, ?string $newKey, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents): void
{
$attribute = $this->updateAttribute(
databaseId: $databaseId,
collectionId: $collectionId,
key: $key,
dbForProject: $dbForProject,
queueForEvents: $queueForEvents,
type: Database::VAR_FLOAT,
default: $default,
required: $required,
min: $min,
max: $max,
newKey: $newKey
);
$formatOptions = $attribute->getAttribute('formatOptions', []);
if (!empty($formatOptions)) {
$attribute->setAttribute('min', \floatval($formatOptions['min']));
$attribute->setAttribute('max', \floatval($formatOptions['max']));
}
$response
->setStatusCode(SwooleResponse::STATUS_CODE_OK)
->dynamic($attribute, $this->getResponseModel());
}
}
@@ -0,0 +1,100 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
class Get extends Action
{
public static function getName(): string
{
return 'getAttribute';
}
protected function getResponseModel(): string|array
{
return [
UtopiaResponse::MODEL_ATTRIBUTE_BOOLEAN,
UtopiaResponse::MODEL_ATTRIBUTE_INTEGER,
UtopiaResponse::MODEL_ATTRIBUTE_FLOAT,
UtopiaResponse::MODEL_ATTRIBUTE_EMAIL,
UtopiaResponse::MODEL_ATTRIBUTE_ENUM,
UtopiaResponse::MODEL_ATTRIBUTE_URL,
UtopiaResponse::MODEL_ATTRIBUTE_IP,
UtopiaResponse::MODEL_ATTRIBUTE_DATETIME,
UtopiaResponse::MODEL_ATTRIBUTE_RELATIONSHIP,
UtopiaResponse::MODEL_ATTRIBUTE_STRING,
];
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/:key')
->desc('Get attribute')
->groups(['api', 'database'])
->label('scope', 'collections.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/get-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel()
)
]
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('key', '', new Key(), 'Attribute Key.')
->inject('response')
->inject('dbForProject')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, UtopiaResponse $response, Database $dbForProject): void
{
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId);
if ($collection->isEmpty()) {
throw new Exception($this->getParentNotFoundException());
}
$attribute = $dbForProject->getDocument('attributes', $database->getSequence() . '_' . $collection->getSequence() . '_' . $key);
if ($attribute->isEmpty()) {
throw new Exception($this->getNotFoundException());
}
$type = $attribute->getAttribute('type');
$format = $attribute->getAttribute('format');
$options = $attribute->getAttribute('options', []);
$filters = $attribute->getAttribute('filters', []);
foreach ($options as $key => $option) {
$attribute->setAttribute($key, $option);
}
$model = $this->getModel($type, $format);
$attribute->setAttribute('encrypt', in_array('encrypt', $filters));
$response->dynamic($attribute, $model);
}
}
@@ -0,0 +1,94 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\IP;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\IP;
class Create extends Action
{
public static function getName(): string
{
return 'createIpAttribute';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_ATTRIBUTE_IP;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/ip')
->desc('Create IP address attribute')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
->label('audits.event', 'attribute.create')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/create-ip-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_ACCEPTED,
model: $this->getResponseModel(),
)
]
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('default', null, new IP(), 'Default value. Cannot be set when attribute is required.', true)
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void
{
$attribute = $this->createAttribute(
$databaseId,
$collectionId,
new Document([
'key' => $key,
'type' => Database::VAR_STRING,
'size' => 39,
'required' => $required,
'default' => $default,
'array' => $array,
'format' => APP_DATABASE_ATTRIBUTE_IP,
]),
$response,
$dbForProject,
$queueForDatabase,
$queueForEvents
);
$response
->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED)
->dynamic($attribute, $this->getResponseModel());
}
}
@@ -0,0 +1,89 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\IP;
use Appwrite\Event\Event;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\IP;
use Utopia\Validator\Nullable;
class Update extends Action
{
public static function getName(): string
{
return 'updateIpAttribute';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_ATTRIBUTE_IP;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/ip/:key')
->desc('Update IP address attribute')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].update')
->label('audits.event', 'attribute.update')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/update-ip-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('default', null, new Nullable(new IP()), 'Default value. Cannot be set when attribute is required.')
->param('newKey', null, new Key(), 'New Attribute Key.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, ?string $newKey, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents): void
{
$attribute = $this->updateAttribute(
databaseId: $databaseId,
collectionId: $collectionId,
key: $key,
dbForProject: $dbForProject,
queueForEvents: $queueForEvents,
type: Database::VAR_STRING,
filter: APP_DATABASE_ATTRIBUTE_IP,
default: $default,
required: $required,
newKey: $newKey
);
$response
->setStatusCode(SwooleResponse::STATUS_CODE_OK)
->dynamic($attribute, $this->getResponseModel());
}
}
@@ -0,0 +1,111 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Integer;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\Integer;
use Utopia\Validator\Range;
class Create extends Action
{
public static function getName(): string
{
return 'createIntegerAttribute';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_ATTRIBUTE_INTEGER;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/integer')
->desc('Create integer attribute')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
->label('audits.event', 'attribute.create')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/create-integer-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_ACCEPTED,
model: $this->getResponseModel(),
)
]
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('min', null, new Integer(), 'Minimum value', true)
->param('max', null, new Integer(), 'Maximum value', true)
->param('default', null, new Integer(), 'Default value. Cannot be set when attribute is required.', true)
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void
{
$min ??= \PHP_INT_MIN;
$max ??= \PHP_INT_MAX;
if ($min > $max) {
throw new Exception($this->getInvalidValueException(), 'Minimum value must be lesser than maximum value');
}
$validator = new Range($min, $max, Database::VAR_INTEGER);
if (!\is_null($default) && !$validator->isValid($default)) {
throw new Exception($this->getInvalidValueException(), $validator->getDescription());
}
$size = $max > 2147483647 ? 8 : 4;
$attribute = $this->createAttribute($databaseId, $collectionId, new Document([
'key' => $key,
'type' => Database::VAR_INTEGER,
'size' => $size,
'required' => $required,
'default' => $default,
'array' => $array,
'format' => APP_DATABASE_ATTRIBUTE_INT_RANGE,
'formatOptions' => ['min' => $min, 'max' => $max],
]), $response, $dbForProject, $queueForDatabase, $queueForEvents);
$formatOptions = $attribute->getAttribute('formatOptions', []);
if (!empty($formatOptions)) {
$attribute->setAttribute('min', \intval($formatOptions['min']));
$attribute->setAttribute('max', \intval($formatOptions['max']));
}
$response
->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED)
->dynamic($attribute, $this->getResponseModel());
}
}
@@ -0,0 +1,98 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Integer;
use Appwrite\Event\Event;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\Integer;
use Utopia\Validator\Nullable;
class Update extends Action
{
public static function getName(): string
{
return 'updateIntegerAttribute';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_ATTRIBUTE_INTEGER;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/integer/:key')
->desc('Update integer attribute')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].update')
->label('audits.event', 'attribute.update')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/update-integer-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('min', null, new Integer(), 'Minimum value', true)
->param('max', null, new Integer(), 'Maximum value', true)
->param('default', null, new Nullable(new Integer()), 'Default value. Cannot be set when attribute is required.')
->param('newKey', null, new Key(), 'New Attribute Key.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, ?string $newKey, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents): void
{
$attribute = $this->updateAttribute(
databaseId: $databaseId,
collectionId: $collectionId,
key: $key,
dbForProject: $dbForProject,
queueForEvents: $queueForEvents,
type: Database::VAR_INTEGER,
default: $default,
required: $required,
min: $min,
max: $max,
newKey: $newKey
);
$formatOptions = $attribute->getAttribute('formatOptions', []);
if (!empty($formatOptions)) {
$attribute->setAttribute('min', \intval($formatOptions['min']));
$attribute->setAttribute('max', \intval($formatOptions['max']));
}
$response
->setStatusCode(SwooleResponse::STATUS_CODE_OK)
->dynamic($attribute, $this->getResponseModel());
}
}
@@ -0,0 +1,157 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Relationship;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\WhiteList;
class Create extends Action
{
public static function getName(): string
{
return 'createRelationshipAttribute';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_ATTRIBUTE_RELATIONSHIP;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/relationship')
->desc('Create relationship attribute')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
->label('audits.event', 'attribute.create')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/create-relationship-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_ACCEPTED,
model: $this->getResponseModel()
)
]
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('relatedCollectionId', '', new UID(), 'Related Collection ID.')
->param('type', '', new WhiteList([
Database::RELATION_ONE_TO_ONE,
Database::RELATION_MANY_TO_ONE,
Database::RELATION_MANY_TO_MANY,
Database::RELATION_ONE_TO_MANY
], true), 'Relation type')
->param('twoWay', false, new Boolean(), 'Is Two Way?', true)
->param('key', null, new Key(), 'Attribute Key.', true)
->param('twoWayKey', null, new Key(), 'Two Way Attribute Key.', true)
->param('onDelete', Database::RELATION_MUTATE_RESTRICT, new WhiteList([
Database::RELATION_MUTATE_CASCADE,
Database::RELATION_MUTATE_RESTRICT,
Database::RELATION_MUTATE_SET_NULL
], true), 'Constraints option', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $relatedCollectionId, string $type, bool $twoWay, ?string $key, ?string $twoWayKey, string $onDelete, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void
{
$key ??= $relatedCollectionId;
$twoWayKey ??= $collectionId;
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId);
$collection = $dbForProject->getCollection('database_' . $database->getSequence() . '_collection_' . $collection->getSequence());
if ($collection->isEmpty()) {
throw new Exception($this->getParentNotFoundException());
}
$relatedCollectionDocument = $dbForProject->getDocument('database_' . $database->getSequence(), $relatedCollectionId);
$relatedCollection = $dbForProject->getCollection('database_' . $database->getSequence() . '_collection_' . $relatedCollectionDocument->getSequence());
if ($relatedCollection->isEmpty()) {
throw new Exception($this->getParentNotFoundException());
}
$attributes = $collection->getAttribute('attributes', []);
foreach ($attributes as $attribute) {
if ($attribute->getAttribute('type') !== Database::VAR_RELATIONSHIP) {
continue;
}
if (\strtolower($attribute->getId()) === \strtolower($key)) {
throw new Exception($this->getDuplicateException());
}
if (
\strtolower($attribute->getAttribute('options')['twoWayKey']) === \strtolower($twoWayKey) &&
$attribute->getAttribute('options')['relatedCollection'] === $relatedCollection->getId()
) {
throw new Exception($this->getDuplicateException(), 'Attribute with the requested key already exists. Attribute keys must be unique, try again with a different key.');
}
if (
$type === Database::RELATION_MANY_TO_MANY &&
$attribute->getAttribute('options')['relationType'] === Database::RELATION_MANY_TO_MANY &&
$attribute->getAttribute('options')['relatedCollection'] === $relatedCollection->getId()
) {
$parentType = $this->isCollectionsAPI() ? 'collection' : 'table';
throw new Exception($this->getDuplicateException(), "Creating more than one \"manyToMany\" relationship on the same $parentType is currently not permitted.");
}
}
$attribute = $this->createAttribute($databaseId, $collectionId, new Document([
'key' => $key,
'type' => Database::VAR_RELATIONSHIP,
'size' => 0,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
'options' => [
'relatedCollection' => $relatedCollectionId,
'relationType' => $type,
'twoWay' => $twoWay,
'twoWayKey' => $twoWayKey,
'onDelete' => $onDelete,
]
]), $response, $dbForProject, $queueForDatabase, $queueForEvents);
foreach ($attribute->getAttribute('options', []) as $k => $option) {
$attribute->setAttribute($k, $option);
}
$response
->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED)
->dynamic($attribute, $this->getResponseModel());
}
}
@@ -0,0 +1,103 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Relationship;
use Appwrite\Event\Event;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\WhiteList;
class Update extends Action
{
public static function getName(): string
{
return 'updateRelationshipAttribute';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_ATTRIBUTE_RELATIONSHIP;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/:key/relationship')
->desc('Update relationship attribute')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].update')
->label('audits.event', 'attribute.update')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/update-relationship-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel()
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('key', '', new Key(), 'Attribute Key.')
->param('onDelete', null, new WhiteList([
Database::RELATION_MUTATE_CASCADE,
Database::RELATION_MUTATE_RESTRICT,
Database::RELATION_MUTATE_SET_NULL
], true), 'Constraints option', true)
->param('newKey', null, new Key(), 'New Attribute Key.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(
string $databaseId,
string $collectionId,
string $key,
?string $onDelete,
?string $newKey,
UtopiaResponse $response,
Database $dbForProject,
Event $queueForEvents
): void {
$attribute = $this->updateAttribute(
databaseId: $databaseId,
collectionId: $collectionId,
key: $key,
dbForProject: $dbForProject,
queueForEvents: $queueForEvents,
type: Database::VAR_RELATIONSHIP,
required: false,
options: [
'onDelete' => $onDelete
],
newKey: $newKey
);
foreach ($attribute->getAttribute('options', []) as $k => $option) {
$attribute->setAttribute($k, $option);
}
$response
->setStatusCode(SwooleResponse::STATUS_CODE_OK)
->dynamic($attribute, $this->getResponseModel());
}
}
@@ -0,0 +1,137 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\String;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator;
use Utopia\Validator\Boolean;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
class Create extends Action
{
public static function getName(): string
{
return 'createStringAttribute';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_ATTRIBUTE_STRING;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/string')
->desc('Create string attribute')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
->label('audits.event', 'attribute.create')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/create-string-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_ACCEPTED,
model: $this->getResponseModel()
)
]
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('key', '', new Key(), 'Attribute Key.')
->param('size', null, new Range(1, APP_DATABASE_ATTRIBUTE_STRING_MAX_LENGTH, Validator::TYPE_INTEGER), 'Attribute size for text attributes, in number of characters.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('default', null, new Text(0, 0), 'Default value for attribute when not provided. Cannot be set when attribute is required.', true)
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->param('encrypt', false, new Boolean(), 'Toggle encryption for the attribute. Encryption enhances security by not storing any plain text values in the database. However, encrypted attributes cannot be queried.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('queueForEvents')
->inject('plan')
->callback($this->action(...));
}
public function action(
string $databaseId,
string $collectionId,
string $key,
?int $size,
?bool $required,
?string $default,
bool $array,
bool $encrypt,
UtopiaResponse $response,
Database $dbForProject,
EventDatabase $queueForDatabase,
Event $queueForEvents,
array $plan
): void {
if ($encrypt && !empty($plan) && !($plan['databasesAllowEncrypt'] ?? false)) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Encrypted string ' . $this->getSdkGroup() . ' are not available on your plan. Please upgrade to create encrypted string ' . $this->getSdkGroup() . '.');
}
if ($encrypt && $size < APP_DATABASE_ENCRYPT_SIZE_MIN) {
throw new Exception(
Exception::GENERAL_BAD_REQUEST,
"Size too small. Encrypted strings require a minimum size of " . APP_DATABASE_ENCRYPT_SIZE_MIN . " characters."
);
}
// Ensure default fits in the given size
$validator = new Text($size, 0);
if (!is_null($default) && !$validator->isValid($default)) {
throw new Exception($this->getInvalidValueException(), $validator->getDescription());
}
$filters = [];
if ($encrypt) {
$filters[] = 'encrypt';
}
$attribute = $this->createAttribute(
$databaseId,
$collectionId,
new Document([
'key' => $key,
'type' => Database::VAR_STRING,
'size' => $size,
'required' => $required,
'default' => $default,
'array' => $array,
'filters' => $filters,
]),
$response,
$dbForProject,
$queueForDatabase,
$queueForEvents
);
$attribute->setAttribute('encrypt', $encrypt);
$response
->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED)
->dynamic($attribute, $this->getResponseModel());
}
}
@@ -0,0 +1,102 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\String;
use Appwrite\Event\Event;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
class Update extends Action
{
public static function getName(): string
{
return 'updateStringAttribute';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_ATTRIBUTE_STRING;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/string/:key')
->desc('Update string attribute')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].update')
->label('audits.event', 'attribute.update')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/update-string-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('default', null, new Nullable(new Text(0, 0)), 'Default value for attribute when not provided. Cannot be set when attribute is required.')
->param('size', null, new Range(1, APP_DATABASE_ATTRIBUTE_STRING_MAX_LENGTH, Validator::TYPE_INTEGER), 'Maximum size of the string attribute.', true)
->param('newKey', null, new Key(), 'New Attribute Key.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(
string $databaseId,
string $collectionId,
string $key,
?bool $required,
?string $default,
?int $size,
?string $newKey,
UtopiaResponse $response,
Database $dbForProject,
Event $queueForEvents
): void {
$attribute = $this->updateAttribute(
databaseId: $databaseId,
collectionId: $collectionId,
key: $key,
dbForProject: $dbForProject,
queueForEvents: $queueForEvents,
type: Database::VAR_STRING,
size: $size,
default: $default,
required: $required,
newKey: $newKey
);
$response
->setStatusCode(SwooleResponse::STATUS_CODE_OK)
->dynamic($attribute, $this->getResponseModel());
}
}
@@ -0,0 +1,96 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\URL;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\URL;
class Create extends Action
{
public static function getName(): string
{
return 'createUrlAttribute';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_ATTRIBUTE_URL;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/url')
->desc('Create URL attribute')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
->label('audits.event', 'attribute.create')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/create-url-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_ACCEPTED,
model: $this->getResponseModel(),
)
]
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('default', null, new URL(), 'Default value for attribute when not provided. Cannot be set when attribute is required.', true)
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(
string $databaseId,
string $collectionId,
string $key,
?bool $required,
?string $default,
bool $array,
UtopiaResponse $response,
Database $dbForProject,
EventDatabase $queueForDatabase,
Event $queueForEvents
): void {
$attribute = $this->createAttribute($databaseId, $collectionId, new Document([
'key' => $key,
'type' => Database::VAR_STRING,
'size' => 2000,
'required' => $required,
'default' => $default,
'array' => $array,
'format' => APP_DATABASE_ATTRIBUTE_URL,
]), $response, $dbForProject, $queueForDatabase, $queueForEvents);
$response
->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED)
->dynamic($attribute, $this->getResponseModel());
}
}
@@ -0,0 +1,98 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\URL;
use Appwrite\Event\Event;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\URL;
class Update extends Action
{
public static function getName(): string
{
return 'updateUrlAttribute';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_ATTRIBUTE_URL;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/url/:key')
->desc('Update URL attribute')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].update')
->label('audits.event', 'attribute.update')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/update-url-attribute.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('default', null, new Nullable(new URL()), 'Default value for attribute when not provided. Cannot be set when attribute is required.')
->param('newKey', null, new Key(), 'New Attribute Key.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(
string $databaseId,
string $collectionId,
string $key,
?bool $required,
?string $default,
?string $newKey,
UtopiaResponse $response,
Database $dbForProject,
Event $queueForEvents
): void {
$attribute = $this->updateAttribute(
$databaseId,
$collectionId,
$key,
$dbForProject,
$queueForEvents,
type: Database::VAR_STRING,
filter: APP_DATABASE_ATTRIBUTE_URL,
default: $default,
required: $required,
newKey: $newKey
);
$response
->setStatusCode(SwooleResponse::STATUS_CODE_OK)
->dynamic($attribute, $this->getResponseModel());
}
}
@@ -0,0 +1,142 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\Queries\Attributes;
use Appwrite\Utopia\Response as UtopiaResponse;
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\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
class XList extends Action
{
public static function getName(): string
{
return 'listAttributes';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_ATTRIBUTE_LIST;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes')
->desc('List attributes')
->groups(['api', 'database'])
->label('scope', 'collections.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/list-attributes.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel()
)
]
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('queries', [], new Attributes(), '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(', ', Attributes::ALLOWED_ATTRIBUTES), true)
->inject('response')
->inject('dbForProject')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, array $queries, UtopiaResponse $response, Database $dbForProject): void
{
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId);
if ($collection->isEmpty()) {
throw new Exception($this->getParentNotFoundException());
}
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
\array_push(
$queries,
Query::equal('databaseInternalId', [$database->getSequence()]),
Query::equal('collectionInternalId', [$collection->getSequence()])
);
$cursor = \array_filter(
$queries,
fn ($query) => \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE])
);
$cursor = \reset($cursor);
if ($cursor) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
}
$attributeId = $cursor->getValue();
try {
$cursorDocument = $dbForProject->findOne('attributes', [
Query::equal('databaseInternalId', [$database->getSequence()]),
Query::equal('collectionInternalId', [$collection->getSequence()]),
Query::equal('key', [$attributeId]),
]);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
if ($cursorDocument->isEmpty()) {
$type = ucfirst($this->getContext());
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "$type '$attributeId' for the 'cursor' value not found.");
}
$cursor->setValue($cursorDocument);
}
try {
$attributes = $dbForProject->find('attributes', $queries);
$total = $dbForProject->count('attributes', $queries, APP_LIMIT_COUNT);
} catch (OrderException $e) {
$documents = $this->isCollectionsAPI() ? 'documents' : 'rows';
$attribute = $this->isCollectionsAPI() ? 'attribute' : 'column';
$message = "The order $attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all $documents order $attribute values are non-null.";
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, $message);
} catch (QueryException) {
throw new Exception(Exception::GENERAL_QUERY_INVALID);
}
foreach ($attributes as $attribute) {
if ($attribute->getAttribute('type') === Database::VAR_STRING) {
$filters = $attribute->getAttribute('filters', []);
$attribute->setAttribute('encrypt', in_array('encrypt', $filters));
}
}
$response->dynamic(new Document([
'total' => $total,
$this->getSdkGroup() => $attributes,
]), $this->getResponseModel());
}
}
@@ -0,0 +1,133 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections;
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\Database\Validator\CustomId;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Exception\Index as IndexException;
use Utopia\Database\Exception\Limit as LimitException;
use Utopia\Database\Exception\NotFound as NotFoundException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Permissions;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
class Create extends Action
{
public static function getName(): string
{
return 'createCollection';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_COLLECTION;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/databases/:databaseId/collections')
->desc('Create collections')
->groups(['api', 'database'])
->label('event', 'databases.[databaseId].collections.[collectionId].create')
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('audits.event', 'collection.create')
->label('audits.resource', 'database/{request.databaseId}/collection/{response.$id}')
->label('sdk', new Method(
namespace: 'databases',
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/create-collection.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_CREATED,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new CustomId(), 'Unique 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.')
->param('name', '', new Text(128), 'Collection name. Max length: 128 chars.')
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of permissions strings. By default, no user is granted with any permissions. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('documentSecurity', false, new Boolean(true), 'Enables configuring permissions for individual documents. A user needs one of document or collection level permissions to access a document. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('enabled', true, new Boolean(), 'Is collection enabled? When set to \'disabled\', users cannot access the collection but Server SDKs with and API key can still read and write to the collection. No data is lost when this is toggled.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, bool $enabled, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents): void
{
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collectionId = $collectionId === 'unique()' ? ID::unique() : $collectionId;
// Map aggregate permissions into the multiple permissions they represent.
$permissions = Permission::aggregate($permissions) ?? [];
try {
$collection = $dbForProject->createDocument('database_' . $database->getSequence(), new Document([
'$id' => $collectionId,
'databaseInternalId' => $database->getSequence(),
'databaseId' => $databaseId,
'$permissions' => $permissions,
'documentSecurity' => $documentSecurity,
'enabled' => $enabled,
'name' => $name,
'search' => \implode(' ', [$collectionId, $name]),
]));
} catch (DuplicateException) {
throw new Exception($this->getDuplicateException());
} catch (LimitException) {
throw new Exception($this->getLimitException());
} catch (NotFoundException) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
try {
$dbForProject->createCollection(
id: 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
permissions: $permissions,
documentSecurity: $documentSecurity
);
} catch (DuplicateException) {
throw new Exception($this->getDuplicateException());
} catch (IndexException) {
throw new Exception($this->getInvalidIndexException());
} catch (LimitException) {
throw new Exception($this->getLimitException());
}
$queueForEvents
->setContext('database', $database)
->setParam('databaseId', $databaseId)
->setParam($this->getEventsParamKey(), $collection->getId());
$response
->setStatusCode(SwooleResponse::STATUS_CODE_CREATED)
->dynamic($collection, $this->getResponseModel());
}
}
@@ -0,0 +1,102 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections;
use Appwrite\Event\Database as EventDatabase;
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 as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
class Delete extends Action
{
public static function getName(): string
{
return 'deleteCollection';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_COLLECTION;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_DELETE)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId')
->desc('Delete collection')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].delete')
->label('audits.event', 'collection.delete')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: 'databases',
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/delete-collection.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_NOCONTENT,
model: UtopiaResponse::MODEL_NONE,
)
],
contentType: ContentType::NONE
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void
{
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId);
if ($collection->isEmpty()) {
throw new Exception($this->getNotFoundException());
}
if (!$dbForProject->deleteDocument('database_' . $database->getSequence(), $collectionId)) {
$type = $this->getContext();
throw new Exception(Exception::GENERAL_SERVER_ERROR, "Failed to remove $type from DB");
}
$dbForProject->purgeCachedCollection('database_' . $database->getSequence() . '_collection_' . $collection->getSequence());
$queueForDatabase
->setType(DATABASE_TYPE_DELETE_COLLECTION)
->setDatabase($database);
if ($this->isCollectionsAPI()) {
$queueForDatabase->setCollection($collection);
} else {
$queueForDatabase->setTable($collection);
}
$queueForEvents
->setParam('databaseId', $databaseId)
->setContext('database', $database)
->setParam($this->getEventsParamKey(), $collection->getId())
->setPayload($response->output($collection, $this->getResponseModel()));
$response->noContent();
}
}
@@ -0,0 +1,282 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents;
use Appwrite\Extend\Exception;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Platform\Action as UtopiaAction;
abstract class Action extends UtopiaAction
{
/**
* @var string|null The current context (either 'row' or 'document')
*/
private ?string $context = DOCUMENTS;
/**
* Get the response model used in the SDK and HTTP responses.
*/
abstract protected function getResponseModel(): string;
public function setHttpPath(string $path): UtopiaAction
{
if (str_contains($path, '/:databaseId/tables')) {
$this->context = ROWS;
}
return parent::setHttpPath($path);
}
/**
* Get the plural of the given name.
*
* Used for endpoints with multiple sdk methods.
*/
final protected function getBulkActionName(string $name): string
{
return "{$name}s";
}
/**
* Get the current context.
*/
final protected function getContext(): string
{
return $this->context;
}
/**
* Returns true if current context is Collections API.
*/
final protected function isCollectionsAPI(): bool
{
// rows in tables api context
// documents in collections api context
return $this->getContext() === DOCUMENTS;
}
/**
* Get the SDK group name for the current action.
*
* Can be used for XList operations as well!
*/
final protected function getSdkGroup(): string
{
return $this->isCollectionsAPI() ? 'documents' : 'rows';
}
/**
* Get the SDK namespace for the current action.
*/
final protected function getSdkNamespace(): string
{
return $this->isCollectionsAPI() ? 'databases' : 'tables';
}
/**
* Get the correct attribute/column structure context for errors.
*/
final protected function getStructureContext(): string
{
return $this->isCollectionsAPI() ? 'attributes' : 'columns';
}
/**
* Get the appropriate parent level not found exception.
*/
final protected function getParentNotFoundException(): string
{
return $this->isCollectionsAPI()
? Exception::COLLECTION_NOT_FOUND
: Exception::TABLE_NOT_FOUND;
}
/**
* Get the appropriate attribute/column not found exception.
*/
final protected function getStructureNotFoundException(): string
{
return $this->isCollectionsAPI()
? Exception::ATTRIBUTE_NOT_FOUND
: Exception::COLUMN_NOT_FOUND;
}
/**
* Get the appropriate not found exception.
*/
final protected function getNotFoundException(): string
{
return $this->isCollectionsAPI()
? Exception::DOCUMENT_NOT_FOUND
: Exception::ROW_NOT_FOUND;
}
/**
* Get the appropriate already exists exception.
*/
final protected function getDuplicateException(): string
{
return $this->isCollectionsAPI()
? Exception::DOCUMENT_ALREADY_EXISTS
: Exception::ROW_ALREADY_EXISTS;
}
/**
* Get the appropriate conflict exception.
*/
final protected function getConflictException(): string
{
return $this->isCollectionsAPI()
? Exception::DOCUMENT_UPDATE_CONFLICT
: Exception::ROW_UPDATE_CONFLICT;
}
/**
* Get the appropriate delete restricted exception.
*/
final protected function getRestrictedException(): string
{
return $this->isCollectionsAPI()
? Exception::DOCUMENT_DELETE_RESTRICTED
: Exception::ROW_DELETE_RESTRICTED;
}
/**
* Get the correct invalid structure message.
*/
final protected function getInvalidStructureException(): string
{
return $this->isCollectionsAPI()
? Exception::DOCUMENT_INVALID_STRUCTURE
: Exception::ROW_INVALID_STRUCTURE;
}
/**
* Get the appropriate missing data exception.
*/
final protected function getMissingDataException(): string
{
return $this->isCollectionsAPI()
? Exception::DOCUMENT_MISSING_DATA
: Exception::ROW_MISSING_DATA;
}
/**
* Get the exception to throw when the resource limit is exceeded.
*/
final protected function getLimitException(): string
{
return $this->isCollectionsAPI()
? Exception::ATTRIBUTE_LIMIT_EXCEEDED
: Exception::COLUMN_LIMIT_EXCEEDED;
}
/**
* Get the appropriate missing payload exception.
*/
final protected function getMissingPayloadException(): string
{
return $this->isCollectionsAPI()
? Exception::DOCUMENT_MISSING_PAYLOAD
: Exception::ROW_MISSING_PAYLOAD;
}
/**
* Get the correct collections context for Events queue.
*/
final protected function getCollectionsEventsContext(): string
{
return $this->isCollectionsAPI() ? 'collection' : 'table';
}
/**
* Resolves relationships in a document and attaches metadata.
*/
final protected function processDocument(
/* database */
Document $database,
Document $collection,
Document $document,
Database $dbForProject,
/* options */
array &$collectionsCache,
?int &$operations = null,
): bool {
if ($operations !== null && $document->isEmpty()) {
return false;
}
if ($operations !== null) {
$operations++;
}
$collectionId = $collection->getId();
$document->removeAttribute('$collection');
$document->setAttribute('$databaseId', $database->getId());
$document->setAttribute('$collectionId', $collectionId);
$relationships = $collectionsCache[$collectionId] ??= \array_filter(
$collection->getAttribute('attributes', []),
fn ($attr) => $attr->getAttribute('type') === Database::VAR_RELATIONSHIP
);
foreach ($relationships as $relationship) {
$key = $relationship->getAttribute('key');
$related = $document->getAttribute($key);
if (empty($related)) {
if (\in_array(\gettype($related), ['array', 'object']) && $operations !== null) {
$operations++;
}
continue;
}
$relations = \is_array($related) ? $related : [$related];
$relatedCollectionId = $relationship->getAttribute('relatedCollection');
if (!isset($collectionsCache[$relatedCollectionId])) {
$relatedCollectionDoc = Authorization::skip(
fn () => $dbForProject->getDocument(
'database_' . $database->getSequence(),
$relatedCollectionId
)
);
$collectionsCache[$relatedCollectionId] = \array_filter(
$relatedCollectionDoc->getAttribute('attributes', []),
fn ($attr) => $attr->getAttribute('type') === Database::VAR_RELATIONSHIP
);
}
foreach ($relations as $relation) {
if ($relation instanceof Document) {
$relatedCollection = new Document([
'$id' => $relatedCollectionId,
'attributes' => $collectionsCache[$relatedCollectionId],
]);
$this->processDocument(
database: $database,
collection: $relatedCollection,
document: $relation,
dbForProject: $dbForProject,
collectionsCache: $collectionsCache,
operations: $operations
);
}
}
if (\is_array($related)) {
$document->setAttribute($relationship->getAttribute('key'), \array_values($relations));
} elseif (empty($relations)) {
$document->setAttribute($relationship->getAttribute('key'), null);
}
}
return true;
}
}
@@ -0,0 +1,122 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Attribute;
use Appwrite\Event\Event;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Exception\Conflict as ConflictException;
use Utopia\Database\Exception\Limit as LimitException;
use Utopia\Database\Exception\NotFound as NotFoundException;
use Utopia\Database\Exception\Type as TypeException;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Numeric;
class Decrement extends Action
{
public static function getName(): string
{
return 'decrementDocumentAttribute';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_DOCUMENT;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId/:attribute/decrement')
->desc('Decrement document attribute')
->groups(['api', 'database'])
->label('event', 'databases.[databaseId].collections.[collectionId].documents.[documentId].decrement')
->label('scope', 'documents.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('audits.event', 'documents.decrement')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}')
->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT * 2)
->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/decrement-document-attribute.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('documentId', '', new UID(), 'Document ID.')
->param('attribute', '', new Key(), 'Attribute key.')
->param('value', 1, new Numeric(), 'Value to increment the attribute by. The value must be a number.', true)
->param('min', null, new Numeric(), 'Minimum value for the attribute. If the current value is lesser than this value, an exception will be thrown.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->inject('queueForStatsUsage')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $min, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage): void
{
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId));
if ($collection->isEmpty()) {
throw new Exception($this->getParentNotFoundException());
}
try {
$document = $dbForProject->decreaseDocumentAttribute(
collection: 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
id: $documentId,
attribute: $attribute,
value: $value,
min: $min
);
} catch (ConflictException) {
throw new Exception($this->getConflictException());
} catch (NotFoundException) {
throw new Exception($this->getStructureNotFoundException());
} catch (LimitException) {
throw new Exception($this->getLimitException(), $this->getSdkNamespace() . ' "' . $attribute . '" has reached the minimum value of ' . $min);
} catch (TypeException) {
throw new Exception(Exception::ATTRIBUTE_TYPE_INVALID, $this->getSdkNamespace() . ' "' . $attribute . '" is not a number');
}
$queueForStatsUsage
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, 1)
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), 1);
$queueForEvents
->setParam('databaseId', $databaseId)
->setContext('database', $database)
->setParam('collectionId', $collectionId)
->setParam('tableId', $collectionId)
->setContext($this->getCollectionsEventsContext(), $collection);
$response->dynamic($document, $this->getResponseModel());
}
}
@@ -0,0 +1,122 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Attribute;
use Appwrite\Event\Event;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Exception\Conflict as ConflictException;
use Utopia\Database\Exception\Limit as LimitException;
use Utopia\Database\Exception\NotFound as NotFoundException;
use Utopia\Database\Exception\Type as TypeException;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Numeric;
class Increment extends Action
{
public static function getName(): string
{
return 'incrementDocumentAttribute';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_DOCUMENT;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId/:attribute/increment')
->desc('Increment document attribute')
->groups(['api', 'database'])
->label('event', 'databases.[databaseId].collections.[collectionId].documents.[documentId].increment')
->label('scope', 'documents.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('audits.event', 'documents.increment')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}')
->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT * 2)
->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/increment-document-attribute.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('documentId', '', new UID(), 'Document ID.')
->param('attribute', '', new Key(), 'Attribute key.')
->param('value', 1, new Numeric(), 'Value to increment the attribute by. The value must be a number.', true)
->param('max', null, new Numeric(), 'Maximum value for the attribute. If the current value is greater than this value, an error will be thrown.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->inject('queueForStatsUsage')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $max, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage): void
{
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId));
if ($collection->isEmpty()) {
throw new Exception($this->getParentNotFoundException());
}
try {
$document = $dbForProject->increaseDocumentAttribute(
collection: 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
id: $documentId,
attribute: $attribute,
value: $value,
max: $max
);
} catch (ConflictException) {
throw new Exception($this->getConflictException());
} catch (NotFoundException) {
throw new Exception($this->getStructureNotFoundException());
} catch (LimitException) {
throw new Exception($this->getLimitException(), $this->getSdkNamespace() . ' "' . $attribute . '" has reached the maximum value of ' . $max);
} catch (TypeException) {
throw new Exception(Exception::ATTRIBUTE_TYPE_INVALID, $this->getSdkNamespace() . ' "' . $attribute . '" is not a number');
}
$queueForStatsUsage
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, 1)
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), 1);
$queueForEvents
->setParam('databaseId', $databaseId)
->setContext('database', $database)
->setParam('collectionId', $collectionId)
->setParam('tableId', $collectionId)
->setContext($this->getCollectionsEventsContext(), $collection);
$response->dynamic($document, $this->getResponseModel());
}
}
@@ -0,0 +1,133 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Bulk;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Conflict as ConflictException;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Exception\Restricted as RestrictedException;
use Utopia\Database\Query;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Text;
class Delete extends Action
{
public static function getName(): string
{
return 'deleteDocuments';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_DOCUMENT_LIST;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_DELETE)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/documents')
->desc('Delete documents')
->groups(['api', 'database'])
->label('scope', 'documents.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('audits.event', 'documents.delete')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}')
->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT)
->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/delete-documents.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('queries', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), '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.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForStatsUsage')
->inject('plan')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, array $queries, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, array $plan): void
{
$database = $dbForProject->getDocument('databases', $databaseId);
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId);
if ($collection->isEmpty()) {
throw new Exception($this->getParentNotFoundException());
}
$hasRelationships = \array_filter(
$collection->getAttribute('attributes', []),
fn ($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP
);
if ($hasRelationships) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk delete is not supported for ' . $this->getSdkNamespace() . ' with relationship attributes');
}
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$documents = [];
try {
$modified = $dbForProject->deleteDocuments(
'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
$queries,
onNext: function (Document $document) use ($plan, &$documents) {
if (\count($documents) < ($plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH)) {
$documents[] = $document;
}
},
);
} catch (ConflictException) {
throw new Exception($this->getConflictException());
} catch (RestrictedException) {
throw new Exception($this->getRestrictedException());
}
foreach ($documents as $document) {
$document->setAttribute('$databaseId', $database->getId());
$document->setAttribute('$collectionId', $collection->getId());
}
$queueForStatsUsage
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, \max(1, $modified))
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), \max(1, $modified));
$response->dynamic(new Document([
'total' => $modified,
$this->getSdkGroup() => $documents,
]), $this->getResponseModel());
}
}
@@ -0,0 +1,155 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Bulk;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Conflict as ConflictException;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Exception\Relationship as RelationshipException;
use Utopia\Database\Exception\Structure as StructureException;
use Utopia\Database\Query;
use Utopia\Database\Validator\Permissions;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\ArrayList;
use Utopia\Validator\JSON;
use Utopia\Validator\Text;
class Update extends Action
{
public static function getName(): string
{
return 'updateDocuments';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_DOCUMENT_LIST;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/documents')
->desc('Update documents')
->groups(['api', 'database'])
->label('scope', 'documents.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('audits.event', 'documents.update')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}')
->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT * 2)
->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/update-documents.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('data', [], new JSON(), 'Document data as JSON object. Include only attribute and value pairs to be updated.', true)
->param('queries', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), '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.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForStatsUsage')
->inject('plan')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string|array $data, array $queries, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, array $plan): void
{
$data = \is_string($data)
? \json_decode($data, true)
: $data;
if (empty($data)) {
throw new Exception($this->getMissingPayloadException());
}
$database = $dbForProject->getDocument('databases', $databaseId);
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId);
if ($collection->isEmpty()) {
throw new Exception($this->getParentNotFoundException());
}
$hasRelationships = \array_filter(
$collection->getAttribute('attributes', []),
fn ($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP
);
if ($hasRelationships) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk update is not supported for ' . $this->getSdkNamespace() . ' with relationship attributes');
}
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
if ($data['$permissions']) {
$validator = new Permissions();
if (!$validator->isValid($data['$permissions'])) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, $validator->getDescription());
}
}
$documents = [];
try {
$modified = $dbForProject->updateDocuments(
'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
new Document($data),
$queries,
onNext: function (Document $document) use ($plan, &$documents) {
if (\count($documents) < ($plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH)) {
$documents[] = $document;
}
},
);
} catch (ConflictException) {
throw new Exception($this->getConflictException());
} catch (RelationshipException $e) {
throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage());
} catch (StructureException $e) {
throw new Exception($this->getInvalidStructureException(), $e->getMessage());
}
foreach ($documents as $document) {
$document->setAttribute('$databaseId', $database->getId());
$document->setAttribute('$collectionId', $collection->getId());
}
$queueForStatsUsage
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, \max(1, $modified))
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), \max(1, $modified));
$response->dynamic(new Document([
'total' => $modified,
$this->getSdkGroup() => $documents
]), $this->getResponseModel());
}
}
@@ -0,0 +1,137 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Bulk;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Conflict as ConflictException;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Exception\Relationship as RelationshipException;
use Utopia\Database\Exception\Structure as StructureException;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\ArrayList;
use Utopia\Validator\JSON;
class Upsert extends Action
{
public static function getName(): string
{
return 'upsertDocuments';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_DOCUMENT_LIST;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PUT)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/documents')
->desc('Create or update documents')
->groups(['api', 'database'])
->label('scope', 'documents.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('audits.event', 'document.create')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}')
->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT * 2)
->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
->label('sdk', [
new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/upsert-documents.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_CREATED,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON,
)
])
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('documents', [], fn (array $plan) => new ArrayList(new JSON(), $plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH), 'Array of document data as JSON objects. May contain partial documents.', false, ['plan'])
->inject('response')
->inject('dbForProject')
->inject('queueForStatsUsage')
->inject('plan')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, array $documents, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, array $plan): void
{
$database = $dbForProject->getDocument('databases', $databaseId);
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId);
if ($collection->isEmpty()) {
throw new Exception($this->getParentNotFoundException());
}
$hasRelationships = \array_filter(
$collection->getAttribute('attributes', []),
fn ($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP
);
if ($hasRelationships) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk upsert is not supported for ' . $this->getSdkNamespace() . ' with relationship attributes');
}
foreach ($documents as $key => $document) {
$documents[$key] = new Document($document);
}
$upserted = [];
try {
$modified = $dbForProject->createOrUpdateDocuments(
'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
$documents,
onNext: function (Document $document) use ($plan, &$upserted) {
if (\count($upserted) < ($plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH)) {
$upserted[] = $document;
}
},
);
} catch (ConflictException) {
throw new Exception($this->getConflictException());
} catch (DuplicateException) {
throw new Exception($this->getDuplicateException());
} catch (RelationshipException $e) {
throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage());
} catch (StructureException $e) {
throw new Exception($this->getInvalidStructureException(), $e->getMessage());
}
foreach ($upserted as $document) {
$document->setAttribute('$databaseId', $database->getId());
$document->setAttribute('$collectionId', $collection->getId());
}
$queueForStatsUsage
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, \max(1, $modified))
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), \max(1, $modified));
$response->dynamic(new Document([
'total' => $modified,
$this->getSdkGroup() => $upserted
]), $this->getResponseModel());
}
}
@@ -0,0 +1,410 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents;
use Appwrite\Auth\Auth;
use Appwrite\Event\Event;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Parameter;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Exception\NotFound as NotFoundException;
use Utopia\Database\Exception\Relationship as RelationshipException;
use Utopia\Database\Exception\Structure as StructureException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Permissions;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\ArrayList;
use Utopia\Validator\JSON;
class Create extends Action
{
public static function getName(): string
{
return 'createDocument';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_DOCUMENT;
}
protected function getBulkResponseModel(): string
{
return UtopiaResponse::MODEL_DOCUMENT_LIST;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/documents')
->desc('Create document')
->groups(['api', 'database'])
->label('scope', 'documents.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('audits.event', 'document.create')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}')
->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT * 2)
->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
->label('sdk', [
new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/create-document.md',
auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_CREATED,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON,
parameters: [
new Parameter('databaseId', optional: false),
new Parameter('collectionId', optional: false),
new Parameter('documentId', optional: false),
new Parameter('data', optional: false),
new Parameter('permissions', optional: true),
]
),
new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: $this->getBulkActionName(self::getName()),
description: '/docs/references/databases/create-documents.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_CREATED,
model: $this->getBulkResponseModel(),
)
],
contentType: ContentType::JSON,
parameters: [
new Parameter('databaseId', optional: false),
new Parameter('collectionId', optional: false),
new Parameter('documents', optional: false),
]
)
])
->param('databaseId', '', new UID(), 'Database ID.')
->param('documentId', '', new CustomId(), 'Document 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.', true)
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection). Make sure to define attributes before creating documents.')
->param('data', [], new JSON(), 'Document data as JSON object.', true)
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE]), 'An array of permissions strings. By default, only the current user is granted all permissions. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('documents', [], fn (array $plan) => new ArrayList(new JSON(), $plan['databasesBatchSize'] ?? APP_LIMIT_DATABASE_BATCH), 'Array of documents data as JSON objects.', true, ['plan'])
->inject('response')
->inject('dbForProject')
->inject('user')
->inject('queueForEvents')
->inject('queueForStatsUsage')
->callback($this->action(...));
}
public function action(string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, ?array $documents, UtopiaResponse $response, Database $dbForProject, Document $user, Event $queueForEvents, StatsUsage $queueForStatsUsage): void
{
$data = \is_string($data)
? \json_decode($data, true)
: $data;
/**
* Determine which internal path to call, single or bulk
*/
if (empty($data) && empty($documents)) {
// No single or bulk documents provided
throw new Exception($this->getMissingDataException());
}
if (!empty($data) && !empty($documents)) {
// Both single and bulk documents provided
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'You can only send one of the following parameters: data, ' . $this->getSdkGroup());
}
if (!empty($data) && empty($documentId)) {
// Single document provided without document ID
$document = $this->isCollectionsAPI() ? 'Document' : 'Row';
$message = "$document ID is required when creating a single " . strtolower($document) . '.';
throw new Exception($this->getMissingDataException(), $message);
}
if (!empty($documents) && !empty($documentId)) {
// Bulk documents provided with document ID
$documentId = $this->isCollectionsAPI() ? 'documentId' : 'rowId';
throw new Exception(
Exception::GENERAL_BAD_REQUEST,
"Param \"$documentId\" is not allowed when creating multiple " . $this->getSdkGroup() . ', set "$id" on each instead.'
);
}
if (!empty($documents) && !empty($permissions)) {
// Bulk documents provided with permissions
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Param "permissions" is disallowed when creating multiple ' . $this->getSdkGroup() . ', set "$permissions" on each instead');
}
$isBulk = true;
if (!empty($data)) {
// Single document provided, convert to single item array
// But remember that it was single to respond with a single document
$isBulk = false;
$documents = [$data];
}
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
if ($isBulk && !$isAPIKey && !$isPrivilegedUser) {
throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE);
}
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId));
if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception($this->getParentNotFoundException());
}
$hasRelationships = \array_filter(
$collection->getAttribute('attributes', []),
fn ($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP
);
if ($isBulk && $hasRelationships) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk create is not supported for ' . $this->getSdkNamespace() .' with relationship ' . $this->getStructureContext());
}
$setPermissions = function (Document $document, ?array $permissions) use ($user, $isAPIKey, $isPrivilegedUser, $isBulk) {
$allowedPermissions = [
Database::PERMISSION_READ,
Database::PERMISSION_UPDATE,
Database::PERMISSION_DELETE,
];
// If bulk, we need to validate permissions explicitly per document
if ($isBulk) {
$permissions = $document['$permissions'] ?? null;
if (!empty($permissions)) {
$validator = new Permissions();
if (!$validator->isValid($permissions)) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, $validator->getDescription());
}
}
}
$permissions = Permission::aggregate($permissions, $allowedPermissions);
// Add permissions for current the user if none were provided.
if (\is_null($permissions)) {
$permissions = [];
if (!empty($user->getId())) {
foreach ($allowedPermissions as $permission) {
$permissions[] = (new Permission($permission, 'user', $user->getId()))->toString();
}
}
}
// Users can only manage their own roles, API keys and Admin users can manage any
if (!$isAPIKey && !$isPrivilegedUser) {
foreach (Database::PERMISSIONS as $type) {
foreach ($permissions as $permission) {
$permission = Permission::parse($permission);
if ($permission->getPermission() != $type) {
continue;
}
$role = (new Role(
$permission->getRole(),
$permission->getIdentifier(),
$permission->getDimension()
))->toString();
if (!Authorization::isRole($role)) {
throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', Authorization::getRoles()) . ')');
}
}
}
}
$document->setAttribute('$permissions', $permissions);
};
$operations = 0;
$checkPermissions = function (Document $collection, Document $document, string $permission) use (&$checkPermissions, $dbForProject, $database, &$operations) {
$operations++;
$documentSecurity = $collection->getAttribute('documentSecurity', false);
$validator = new Authorization($permission);
$valid = $validator->isValid($collection->getPermissionsByType($permission));
if (($permission === Database::PERMISSION_UPDATE && !$documentSecurity) || !$valid) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
if ($permission === Database::PERMISSION_UPDATE) {
$valid = $valid || $validator->isValid($document->getUpdate());
if ($documentSecurity && !$valid) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
}
$relationships = \array_filter(
$collection->getAttribute('attributes', []),
fn ($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP
);
foreach ($relationships as $relationship) {
$related = $document->getAttribute($relationship->getAttribute('key'));
if (empty($related)) {
continue;
}
$isList = \is_array($related) && \array_values($related) === $related;
if ($isList) {
$relations = $related;
} else {
$relations = [$related];
}
$relatedCollectionId = $relationship->getAttribute('relatedCollection');
$relatedCollection = Authorization::skip(
fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $relatedCollectionId)
);
foreach ($relations as &$relation) {
if (
\is_array($relation)
&& \array_values($relation) !== $relation
&& !isset($relation['$id'])
) {
$relation['$id'] = ID::unique();
$relation = new Document($relation);
}
if ($relation instanceof Document) {
$current = Authorization::skip(
fn () => $dbForProject->getDocument('database_' . $database->getSequence() . '_collection_' . $relatedCollection->getSequence(), $relation->getId())
);
if ($current->isEmpty()) {
$type = Database::PERMISSION_CREATE;
if (isset($relation['$id']) && $relation['$id'] === 'unique()') {
$relation['$id'] = ID::unique();
}
} else {
$relation->removeAttribute('$collectionId');
$relation->removeAttribute('$databaseId');
$relation->setAttribute('$collection', $relatedCollection->getId());
$type = Database::PERMISSION_UPDATE;
}
$checkPermissions($relatedCollection, $relation, $type);
}
}
if ($isList) {
$document->setAttribute($relationship->getAttribute('key'), \array_values($relations));
} else {
$document->setAttribute($relationship->getAttribute('key'), \reset($relations));
}
}
};
$documents = \array_map(function ($document) use ($collection, $permissions, $checkPermissions, $isBulk, $documentId, $setPermissions) {
$document['$collection'] = $collection->getId();
// Determine the source ID depending on whether it's a bulk operation.
$sourceId = $isBulk
? ($document['$id'] ?? ID::unique())
: $documentId;
// If bulk, we need to validate ID explicitly
if ($isBulk) {
$validator = new CustomId();
if (!$validator->isValid($sourceId)) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, $validator->getDescription());
}
}
// Assign a unique ID if needed, otherwise use the provided ID.
$document['$id'] = $sourceId === 'unique()' ? ID::unique() : $sourceId;
$document = new Document($document);
$setPermissions($document, $permissions);
$checkPermissions($collection, $document, Database::PERMISSION_CREATE);
return $document;
}, $documents);
try {
$dbForProject->createDocuments(
'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
$documents
);
} catch (DuplicateException) {
throw new Exception($this->getDuplicateException());
} catch (NotFoundException) {
throw new Exception($this->getParentNotFoundException());
} catch (RelationshipException $e) {
throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage());
} catch (StructureException $e) {
throw new Exception($this->getInvalidStructureException(), $e->getMessage());
}
$queueForEvents
->setParam('databaseId', $databaseId)
->setContext('database', $database)
->setParam('collectionId', $collection->getId())
->setParam('tableId', $collection->getId())
->setContext($this->getCollectionsEventsContext(), $collection);
$collectionsCache = [];
foreach ($documents as $document) {
$this->processDocument(
database: $database,
collection: $collection,
document: $document,
dbForProject: $dbForProject,
collectionsCache: $collectionsCache,
);
}
$queueForStatsUsage
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, \max(1, $operations))
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), \max(1, $operations)); // per collection
$response->setStatusCode(SwooleResponse::STATUS_CODE_CREATED);
if ($isBulk) {
$response->dynamic(new Document([
'total' => count($documents),
$this->getSdkGroup() => $documents
]), $this->getBulkResponseModel());
return;
}
$queueForEvents
->setParam('documentId', $documents[0]->getId())
->setParam('rowId', $documents[0]->getId())
->setEvent('databases.[databaseId].collections.[collectionId].documents.[documentId].create');
$response->dynamic(
$documents[0],
$this->getResponseModel()
);
}
}
@@ -0,0 +1,149 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents;
use Appwrite\Auth\Auth;
use Appwrite\Event\Event;
use Appwrite\Event\StatsUsage;
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 as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Exception\Conflict as ConflictException;
use Utopia\Database\Exception\Restricted as RestrictedException;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
class Delete extends Action
{
public static function getName(): string
{
return 'deleteDocument';
}
/**
* 1. `SDKResponse` uses `UtopiaResponse::MODEL_NONE`.
* 2. But we later need the actual return type for events queue below!
*/
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_DOCUMENT;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_DELETE)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId')
->desc('Delete document')
->groups(['api', 'database'])
->label('scope', 'documents.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].documents.[documentId].delete')
->label('audits.event', 'document.delete')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}/document/{request.documentId}')
->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}')
->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT)
->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/delete-document.md',
auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_NOCONTENT,
model: UtopiaResponse::MODEL_NONE,
)
],
contentType: ContentType::NONE
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('documentId', '', new UID(), 'Document ID.')
->inject('requestTimestamp')
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->inject('queueForStatsUsage')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $documentId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage): void
{
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId));
if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception($this->getParentNotFoundException());
}
// Read permission should not be required for delete
$document = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $documentId));
if ($document->isEmpty()) {
throw new Exception($this->getNotFoundException());
}
try {
$dbForProject->withRequestTimestamp($requestTimestamp, function () use ($dbForProject, $database, $collection, $documentId) {
$dbForProject->deleteDocument(
'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
$documentId
);
});
} catch (ConflictException) {
throw new Exception($this->getConflictException());
} catch (RestrictedException) {
throw new Exception($this->getRestrictedException());
}
$collectionsCache = [];
$this->processDocument(
database: $database,
collection: $collection,
document: $document,
dbForProject: $dbForProject,
collectionsCache: $collectionsCache,
);
$queueForStatsUsage
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, 1)
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), 1); // per collection
$response->addHeader('X-Debug-Operations', 1);
$relationships = \array_map(
fn ($document) => $document->getAttribute('key'),
\array_filter(
$collection->getAttribute('attributes', []),
fn ($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP
)
);
$queueForEvents
->setParam('databaseId', $databaseId)
->setContext('database', $database)
->setParam('collectionId', $collection->getId())
->setParam('tableId', $collection->getId())
->setParam('documentId', $document->getId())
->setParam('rowId', $document->getId())
->setContext($this->getCollectionsEventsContext(), $collection)
->setPayload($response->output($document, $this->getResponseModel()), sensitive: $relationships);
$response->noContent();
}
}
@@ -0,0 +1,126 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents;
use Appwrite\Auth\Auth;
use Appwrite\Event\StatsUsage;
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 as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Text;
class Get extends Action
{
public static function getName(): string
{
return 'getDocument';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_DOCUMENT;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId')
->desc('Get document')
->groups(['api', 'database'])
->label('scope', 'documents.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/get-document.md',
auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('documentId', '', new UID(), 'Document ID.')
->param('queries', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), '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.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForStatsUsage')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $documentId, array $queries, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage): void
{
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId));
if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception($this->getParentNotFoundException());
}
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
try {
$selects = Query::groupByType($queries)['selections'] ?? [];
if (! empty($selects)) {
// has selects, allow relationship on documents!
$document = $dbForProject->getDocument('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $documentId, $queries);
} else {
// has no selects, disable relationship looping on documents!
$document = $dbForProject->skipRelationships(fn () => $dbForProject->getDocument('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $documentId, $queries));
}
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
if ($document->isEmpty()) {
throw new Exception($this->getNotFoundException());
}
$operations = 0;
$collectionsCache = [];
$this->processDocument(
database: $database,
collection: $collection,
document: $document,
dbForProject: $dbForProject,
collectionsCache: $collectionsCache,
operations: $operations
);
$queueForStatsUsage
->addMetric(METRIC_DATABASES_OPERATIONS_READS, max($operations, 1))
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_READS), $operations);
$response->addHeader('X-Debug-Operations', $operations);
$response->dynamic($document, $this->getResponseModel());
}
}
@@ -0,0 +1,158 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Logs;
use Appwrite\Detector\Detector;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use MaxMind\Db\Reader;
use Utopia\Audit\Audit;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Queries;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\UID;
use Utopia\Locale\Locale;
use Utopia\Swoole\Response as SwooleResponse;
class XList extends Action
{
public static function getName(): string
{
return 'listDocumentLogs';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_LOG_LIST;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId/logs')
->desc('List document logs')
->groups(['api', 'database'])
->label('scope', 'documents.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: 'logs',
name: self::getName(),
description: '/docs/references/databases/get-document-logs.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON,
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('documentId', '', new UID(), 'Document ID.')
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->inject('response')
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $documentId, array $queries, UtopiaResponse $response, Database $dbForProject, Locale $locale, Reader $geodb): void
{
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId);
if ($collection->isEmpty()) {
throw new Exception($this->getParentNotFoundException());
}
$document = $dbForProject->getDocument('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $documentId);
if ($document->isEmpty()) {
throw new Exception($this->getNotFoundException());
}
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
// Temp fix for logs
$queries[] = Query::or([
Query::greaterThan('$createdAt', DateTime::format(new \DateTime('2025-02-26T01:30+00:00'))),
Query::lessThan('$createdAt', DateTime::format(new \DateTime('2025-02-13T00:00+00:00'))),
]);
$audit = new Audit($dbForProject);
// getContext() => `document` or `row`.
$resource = 'database/' . $databaseId . '/collection/' . $collectionId . '/' .$this->getContext(). '/' . $document->getId();
$logs = $audit->getLogsByResource($resource, $queries);
$output = [];
foreach ($logs as $i => &$log) {
$log['userAgent'] = (!empty($log['userAgent'])) ? $log['userAgent'] : 'UNKNOWN';
$detector = new Detector($log['userAgent']);
$detector->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then)
$os = $detector->getOS();
$client = $detector->getClient();
$device = $detector->getDevice();
$output[$i] = new Document([
'event' => $log['event'],
'userId' => $log['data']['userId'],
'userEmail' => $log['data']['userEmail'] ?? null,
'userName' => $log['data']['userName'] ?? null,
'mode' => $log['data']['mode'] ?? null,
'ip' => $log['ip'],
'time' => $log['time'],
'osCode' => $os['osCode'],
'osName' => $os['osName'],
'osVersion' => $os['osVersion'],
'clientType' => $client['clientType'],
'clientCode' => $client['clientCode'],
'clientName' => $client['clientName'],
'clientVersion' => $client['clientVersion'],
'clientEngine' => $client['clientEngine'],
'clientEngineVersion' => $client['clientEngineVersion'],
'deviceName' => $device['deviceName'],
'deviceBrand' => $device['deviceBrand'],
'deviceModel' => $device['deviceModel']
]);
$record = $geodb->get($log['ip']);
if ($record) {
$output[$i]['countryCode'] = $locale->getText('countries.' . strtolower($record['country']['iso_code']), false) ? \strtolower($record['country']['iso_code']) : '--';
$output[$i]['countryName'] = $locale->getText('countries.' . strtolower($record['country']['iso_code']), $locale->getText('locale.country.unknown'));
} else {
$output[$i]['countryCode'] = '--';
$output[$i]['countryName'] = $locale->getText('locale.country.unknown');
}
}
$response->dynamic(new Document([
'logs' => $output,
'total' => $audit->countLogsByResource($resource, $queries),
]), $this->getResponseModel());
}
}
@@ -0,0 +1,276 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents;
use Appwrite\Auth\Auth;
use Appwrite\Event\Event;
use Appwrite\Event\StatsUsage;
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 as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Conflict as ConflictException;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Exception\Relationship as RelationshipException;
use Utopia\Database\Exception\Structure as StructureException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Permissions;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\JSON;
class Update extends Action
{
public static function getName(): string
{
return 'updateDocument';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_DOCUMENT;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId')
->desc('Update document')
->groups(['api', 'database'])
->label('event', 'databases.[databaseId].collections.[collectionId].documents.[documentId].update')
->label('scope', 'documents.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('audits.event', 'document.update')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}/document/{response.$id}')
->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}')
->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT * 2)
->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/update-document.md',
auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('documentId', '', new UID(), 'Document ID.')
->param('data', [], new JSON(), 'Document data as JSON object. Include only attribute and value pairs to be updated.', true)
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE]), 'An array of permissions strings. By default, the current permissions are inherited. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->inject('requestTimestamp')
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->inject('queueForStatsUsage')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?\DateTime $requestTimestamp, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage): void
{
$data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array
if (empty($data) && \is_null($permissions)) {
throw new Exception($this->getMissingPayloadException());
}
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId));
if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception($this->getParentNotFoundException());
}
// Read permission should not be required for update
/** @var Document $document */
$document = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $documentId));
if ($document->isEmpty()) {
throw new Exception($this->getNotFoundException());
}
// Map aggregate permissions into the multiple permissions they represent.
$permissions = Permission::aggregate($permissions, [
Database::PERMISSION_READ,
Database::PERMISSION_UPDATE,
Database::PERMISSION_DELETE,
]);
// Users can only manage their own roles, API keys and Admin users can manage any
$roles = Authorization::getRoles();
if (!$isAPIKey && !$isPrivilegedUser && !\is_null($permissions)) {
foreach (Database::PERMISSIONS as $type) {
foreach ($permissions as $permission) {
$permission = Permission::parse($permission);
if ($permission->getPermission() != $type) {
continue;
}
$role = (new Role(
$permission->getRole(),
$permission->getIdentifier(),
$permission->getDimension()
))->toString();
if (!Authorization::isRole($role)) {
throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', $roles) . ')');
}
}
}
}
if (\is_null($permissions)) {
$permissions = $document->getPermissions() ?? [];
}
$data['$id'] = $documentId;
$data['$permissions'] = $permissions;
$newDocument = new Document($data);
$operations = 0;
$setCollection = (function (Document $collection, Document $document) use (&$setCollection, $dbForProject, $database, &$operations) {
$operations++;
$relationships = \array_filter(
$collection->getAttribute('attributes', []),
fn ($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP
);
foreach ($relationships as $relationship) {
$related = $document->getAttribute($relationship->getAttribute('key'));
if (empty($related)) {
continue;
}
$isList = \is_array($related) && \array_values($related) === $related;
if ($isList) {
$relations = $related;
} else {
$relations = [$related];
}
$relatedCollectionId = $relationship->getAttribute('relatedCollection');
$relatedCollection = Authorization::skip(
fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $relatedCollectionId)
);
foreach ($relations as &$relation) {
// If the relation is an array it can be either update or create a child document.
if (
\is_array($relation)
&& \array_values($relation) !== $relation
&& !isset($relation['$id'])
) {
$relation['$id'] = ID::unique();
$relation = new Document($relation);
}
if ($relation instanceof Document) {
$oldDocument = Authorization::skip(fn () => $dbForProject->getDocument(
'database_' . $database->getSequence() . '_collection_' . $relatedCollection->getSequence(),
$relation->getId()
));
$relation->removeAttribute('$collectionId');
$relation->removeAttribute('$databaseId');
// Attribute $collection is required for Utopia.
$relation->setAttribute(
'$collection',
'database_' . $database->getSequence() . '_collection_' . $relatedCollection->getSequence()
);
if ($oldDocument->isEmpty()) {
if (isset($relation['$id']) && $relation['$id'] === 'unique()') {
$relation['$id'] = ID::unique();
}
}
$setCollection($relatedCollection, $relation);
}
}
if ($isList) {
$document->setAttribute($relationship->getAttribute('key'), \array_values($relations));
} else {
$document->setAttribute($relationship->getAttribute('key'), \reset($relations));
}
}
});
$setCollection($collection, $newDocument);
$queueForStatsUsage
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, max($operations, 1))
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), $operations);
try {
$document = $dbForProject->withRequestTimestamp(
$requestTimestamp,
fn () => $dbForProject->updateDocument(
'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
$document->getId(),
$newDocument
)
);
} catch (ConflictException) {
throw new Exception($this->getConflictException());
} catch (DuplicateException) {
throw new Exception($this->getDuplicateException());
} catch (RelationshipException $e) {
throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage());
} catch (StructureException $e) {
throw new Exception($this->getInvalidStructureException(), $e->getMessage());
}
$collectionsCache = [];
$this->processDocument(
database: $database,
collection: $collection,
document: $document,
dbForProject: $dbForProject,
collectionsCache: $collectionsCache,
);
$response->dynamic($document, $this->getResponseModel());
$relationships = \array_map(
fn ($document) => $document->getAttribute('key'),
\array_filter(
$collection->getAttribute('attributes', []),
fn ($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP
)
);
$queueForEvents
->setParam('databaseId', $databaseId)
->setContext('database', $database)
->setParam('collectionId', $collection->getId())
->setParam('tableId', $collection->getId())
->setParam('documentId', $document->getId())
->setParam('rowId', $document->getId())
->setContext($this->getCollectionsEventsContext(), $collection)
->setPayload($response->getPayload(), sensitive: $relationships);
}
}
@@ -0,0 +1,268 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents;
use Appwrite\Auth\Auth;
use Appwrite\Event\Event;
use Appwrite\Event\StatsUsage;
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\Database\Validator\CustomId;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Conflict as ConflictException;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Exception\Relationship as RelationshipException;
use Utopia\Database\Exception\Structure as StructureException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Permissions;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\JSON;
class Upsert extends Action
{
public static function getName(): string
{
return 'upsertDocument';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_DOCUMENT;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PUT)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId')
->desc('Create or update a document')
->groups(['api', 'database'])
->label('event', 'databases.[databaseId].collections.[collectionId].documents.[documentId].upsert')
->label('scope', 'documents.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('audits.event', 'document.upsert')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}/document/{response.$id}')
->label('abuse-key', 'ip:{ip},method:{method},url:{url},userId:{userId}')
->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT * 2)
->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
->label('sdk', [
new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/upsert-document.md',
auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_CREATED,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
),
])
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('documentId', '', new CustomId(), 'Document ID.')
->param('data', [], new JSON(), 'Document data as JSON object. Include all required attributes of the document to be created or updated.')
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE, [Database::PERMISSION_READ, Database::PERMISSION_UPDATE, Database::PERMISSION_DELETE, Database::PERMISSION_WRITE]), 'An array of permissions strings. By default, the current permissions are inherited. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->inject('requestTimestamp')
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->inject('queueForStatsUsage')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?\DateTime $requestTimestamp, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage): void
{
$data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array
if (empty($data) && \is_null($permissions)) {
throw new Exception($this->getMissingPayloadException());
}
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId));
if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception($this->getParentNotFoundException());
}
// Map aggregate permissions into the multiple permissions they represent.
$permissions = Permission::aggregate($permissions, [
Database::PERMISSION_READ,
Database::PERMISSION_UPDATE,
Database::PERMISSION_DELETE,
]);
// Users can only manage their own roles, API keys and Admin users can manage any
$roles = Authorization::getRoles();
if (!$isAPIKey && !$isPrivilegedUser && !\is_null($permissions)) {
foreach (Database::PERMISSIONS as $type) {
foreach ($permissions as $permission) {
$permission = Permission::parse($permission);
if ($permission->getPermission() != $type) {
continue;
}
$role = (new Role(
$permission->getRole(),
$permission->getIdentifier(),
$permission->getDimension()
))->toString();
if (!Authorization::isRole($role)) {
throw new Exception(Exception::USER_UNAUTHORIZED, 'Permissions must be one of: (' . \implode(', ', $roles) . ')');
}
}
}
}
$data['$id'] = $documentId;
$data['$permissions'] = $permissions;
$newDocument = new Document($data);
$operations = 0;
$setCollection = (function (Document $collection, Document $document) use (&$setCollection, $dbForProject, $database, &$operations) {
$operations++;
$relationships = \array_filter(
$collection->getAttribute('attributes', []),
fn ($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP
);
foreach ($relationships as $relationship) {
$related = $document->getAttribute($relationship->getAttribute('key'));
if (empty($related)) {
continue;
}
$isList = \is_array($related) && \array_values($related) === $related;
if ($isList) {
$relations = $related;
} else {
$relations = [$related];
}
$relatedCollectionId = $relationship->getAttribute('relatedCollection');
$relatedCollection = Authorization::skip(
fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $relatedCollectionId)
);
foreach ($relations as &$relation) {
// If the relation is an array it can be either update or create a child document.
if (
\is_array($relation)
&& \array_values($relation) !== $relation
&& !isset($relation['$id'])
) {
$relation['$id'] = ID::unique();
$relation = new Document($relation);
}
if ($relation instanceof Document) {
$oldDocument = Authorization::skip(fn () => $dbForProject->getDocument(
'database_' . $database->getSequence() . '_collection_' . $relatedCollection->getSequence(),
$relation->getId()
));
$relation->removeAttribute('$collectionId');
$relation->removeAttribute('$databaseId');
// Attribute $collection is required for Utopia.
$relation->setAttribute(
'$collection',
'database_' . $database->getSequence() . '_collection_' . $relatedCollection->getSequence()
);
if ($oldDocument->isEmpty()) {
if (isset($relation['$id']) && $relation['$id'] === 'unique()') {
$relation['$id'] = ID::unique();
}
}
$setCollection($relatedCollection, $relation);
}
}
if ($isList) {
$document->setAttribute($relationship->getAttribute('key'), \array_values($relations));
} else {
$document->setAttribute($relationship->getAttribute('key'), \reset($relations));
}
}
});
$setCollection($collection, $newDocument);
$queueForStatsUsage
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, \max(1, $operations))
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), \max(1, $operations));
$upserted = [];
try {
$dbForProject->createOrUpdateDocuments(
'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
[$newDocument],
onNext: function (Document $document) use (&$upserted) {
$upserted[] = $document;
},
);
} catch (ConflictException) {
throw new Exception($this->getConflictException());
} catch (DuplicateException) {
throw new Exception($this->getDuplicateException());
} catch (RelationshipException $e) {
throw new Exception(Exception::RELATIONSHIP_VALUE_INVALID, $e->getMessage());
} catch (StructureException $e) {
throw new Exception($this->getInvalidStructureException(), $e->getMessage());
}
$collectionsCache = [];
$document = $upserted[0];
$this->processDocument(
database: $database,
collection: $collection,
document: $document,
dbForProject: $dbForProject,
collectionsCache: $collectionsCache,
);
$relationships = \array_map(
fn ($document) => $document->getAttribute('key'),
\array_filter(
$collection->getAttribute('attributes', []),
fn ($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP
)
);
$queueForEvents
->setParam('databaseId', $databaseId)
->setContext('database', $database)
->setParam('collectionId', $collection->getId())
->setParam('tableId', $collection->getId())
->setParam('documentId', $document->getId())
->setParam('rowId', $document->getId())
->setContext($this->getCollectionsEventsContext(), $collection)
->setPayload($response->getPayload(), sensitive: $relationships);
$response->dynamic(
$document,
$this->getResponseModel()
);
}
}
@@ -0,0 +1,210 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents;
use Appwrite\Auth\Auth;
use Appwrite\Event\StatsUsage;
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 as UtopiaResponse;
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\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Text;
class XList extends Action
{
public static function getName(): string
{
return 'listDocuments';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_DOCUMENT_LIST;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/documents')
->desc('List documents')
->groups(['api', 'database'])
->label('scope', 'documents.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/list-documents.md',
auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('queries', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), '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.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForStatsUsage')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, array $queries, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage): void
{
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
if ($database->isEmpty() || (!$database->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId));
if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception($this->getParentNotFoundException());
}
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
/**
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
*/
$cursor = \array_filter($queries, function ($query) {
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
});
$cursor = \reset($cursor);
if ($cursor) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
}
$documentId = $cursor->getValue();
$cursorDocument = Authorization::skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $documentId));
if ($cursorDocument->isEmpty()) {
$type = ucfirst($this->getContext());
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "$type '{$documentId}' for the 'cursor' value not found.");
}
$cursor->setValue($cursorDocument);
}
$selectQueries = [];
try {
$selectQueries = Query::groupByType($queries)['selections'] ?? [];
if (! empty($selectQueries)) {
// has selects, allow relationship on documents!
$documents = $dbForProject->find('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $queries);
} else {
// has no selects, disable relationship looping on documents!
/* @type Document[] $documents */
$documents = $dbForProject->skipRelationships(fn () => $dbForProject->find('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $queries));
}
$total = $dbForProject->count('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $queries, APP_LIMIT_COUNT);
} catch (OrderException $e) {
$documents = $this->isCollectionsAPI() ? 'documents' : 'rows';
$attribute = $this->isCollectionsAPI() ? 'attribute' : 'column';
$message = "The order $attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all $documents order $attribute values are non-null.";
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, $message);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$operations = 0;
$collectionsCache = [];
foreach ($documents as $document) {
$this->processDocument(
database: $database,
collection: $collection,
document: $document,
dbForProject: $dbForProject,
collectionsCache: $collectionsCache,
operations: $operations,
);
}
$queueForStatsUsage
->addMetric(METRIC_DATABASES_OPERATIONS_READS, max($operations, 1))
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_READS), $operations);
$response->addHeader('X-Debug-Operations', $operations);
$select = \array_reduce($queries, function ($result, $query) {
return $result || ($query->getMethod() === Query::TYPE_SELECT);
}, false);
// Check if the SELECT query includes $databaseId and $collectionId
$hasWildcard = false;
$hasDatabaseId = false;
$hasCollectionId = false;
$hasSelectQueries = !empty($selectQueries);
if ($hasSelectQueries) {
foreach ($selectQueries as $query) {
if ($query->getMethod() !== Query::TYPE_SELECT) {
continue;
}
$values = $query->getValues();
if (\in_array('*', $values, true)) {
$hasWildcard = true;
break;
}
if (\in_array('$databaseId', $values, true)) {
$hasDatabaseId = true;
}
if (\in_array('$collectionId', $values, true)) {
$hasCollectionId = true;
}
}
if (!$hasWildcard) {
foreach ($documents as $document) {
if (!$hasDatabaseId) {
$document->removeAttribute('$databaseId');
}
if (!$hasCollectionId) {
$document->removeAttribute('$collectionId');
}
}
}
}
$response->dynamic(new Document([
'total' => $total,
// rows or documents
$this->getSdkGroup() => $documents,
]), $this->getResponseModel());
}
}
@@ -0,0 +1,74 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections;
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 as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
class Get extends Action
{
public static function getName(): string
{
return 'getCollection';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_COLLECTION;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId')
->desc('Get collection')
->groups(['api', 'database'])
->label('scope', 'collections.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: 'databases',
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/get-collection.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->inject('response')
->inject('dbForProject')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, UtopiaResponse $response, Database $dbForProject): void
{
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId);
if ($collection->isEmpty()) {
throw new Exception($this->getNotFoundException());
}
$response->dynamic($collection, $this->getResponseModel());
}
}
@@ -0,0 +1,156 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Indexes;
use Appwrite\Extend\Exception;
use Utopia\Platform\Action as UtopiaAction;
abstract class Action extends UtopiaAction
{
/**
* The current API context (either 'columnIndex' or 'index').
*/
private ?string $context = INDEX;
/**
* Get the response model used in the SDK and HTTP responses.
*/
abstract protected function getResponseModel(): string;
public function setHttpPath(string $path): UtopiaAction
{
if (str_contains($path, '/:databaseId/tables')) {
$this->context = COLUMN_INDEX;
}
return parent::setHttpPath($path);
}
/**
* Get the current API's parent context.
*/
final protected function getParentContext(): string
{
return $this->getContext() === INDEX ? ATTRIBUTES : COLUMNS;
}
/**
* Get the current API context.
*/
final protected function getContext(): string
{
return $this->context;
}
/**
* Determine if the current action is for the Collections API.
*/
final protected function isCollectionsAPI(): bool
{
return $this->getParentContext() === ATTRIBUTES;
}
/**
* Get the SDK group name for the current action.
*/
final protected function getSdkGroup(): string
{
return 'indexes';
}
/**
* Get the SDK namespace for the current action.
*/
final protected function getSdkNamespace(): string
{
return $this->isCollectionsAPI() ? 'databases' : 'tables';
}
/**
* Get the exception to throw when the parent is unknown.
*/
final protected function getParentUnknownException(): string
{
return $this->isCollectionsAPI()
? Exception::ATTRIBUTE_UNKNOWN
: Exception::COLUMN_UNKNOWN;
}
/**
* Get the appropriate grandparent level not found exception.
*/
final protected function getGrandParentNotFoundException(): string
{
return $this->isCollectionsAPI()
? Exception::COLLECTION_NOT_FOUND
: Exception::TABLE_NOT_FOUND;
}
/**
* Get the appropriate not found exception.
*/
final protected function getNotFoundException(): string
{
return $this->isCollectionsAPI()
? Exception::INDEX_NOT_FOUND
: Exception::COLUMN_INDEX_NOT_FOUND;
}
/**
* Get the exception to throw when the parent type is invalid.
*/
final protected function getParentInvalidTypeException(): string
{
return $this->isCollectionsAPI()
? Exception::ATTRIBUTE_TYPE_INVALID
: Exception::COLUMN_TYPE_INVALID;
}
/**
* Get the exception to throw when the index type is invalid.
*/
final protected function getInvalidTypeException(): string
{
return $this->isCollectionsAPI()
? Exception::INDEX_INVALID
: Exception::COLUMN_INDEX_INVALID;
}
/**
* Get the exception to throw when the resource already exists.
*/
final protected function getDuplicateException(): string
{
return $this->isCollectionsAPI()
? Exception::INDEX_ALREADY_EXISTS
: Exception::COLUMN_INDEX_ALREADY_EXISTS;
}
/**
* Get the exception to throw when the resource limit is exceeded.
*/
final protected function getLimitException(): string
{
return $this->isCollectionsAPI()
? Exception::INDEX_LIMIT_EXCEEDED
: Exception::COLUMN_INDEX_LIMIT_EXCEEDED;
}
/**
* Get the exception to throw when the parent attribute/column is not in `available` state.
*/
final protected function getParentNotAvailableException(): string
{
return $this->isCollectionsAPI()
? Exception::ATTRIBUTE_NOT_AVAILABLE
: Exception::COLUMN_NOT_AVAILABLE;
}
/**
* Get the correct collections context for Events queue.
*/
final protected function getCollectionsEventsContext(): string
{
return $this->isCollectionsAPI() ? 'collection' : 'table';
}
}
@@ -0,0 +1,230 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Indexes;
use Appwrite\Event\Database as EventDatabase;
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 as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Index as IndexValidator;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Integer;
use Utopia\Validator\Nullable;
use Utopia\Validator\WhiteList;
class Create extends Action
{
public static function getName(): string
{
return 'createIndex';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_INDEX;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/indexes')
->desc('Create index')
->groups(['api', 'database'])
->label('event', 'databases.[databaseId].collections.[collectionId].indexes.[indexId].create')
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('audits.event', 'index.create')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/create-index.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_ACCEPTED,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('key', null, new Key(), 'Index Key.')
->param('type', null, new WhiteList([Database::INDEX_KEY, Database::INDEX_FULLTEXT, Database::INDEX_UNIQUE]), 'Index type.')
->param('attributes', null, new ArrayList(new Key(true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of attributes to index. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' attributes are allowed, each 32 characters long.')
->param('orders', [], new ArrayList(new WhiteList(['ASC', 'DESC'], false, Database::VAR_STRING), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of index orders. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' orders are allowed.', true)
->param('lengths', [], new ArrayList(new Nullable(new Integer()), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Length of index. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE, optional: true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, string $type, array $attributes, array $orders, array $lengths, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void
{
$db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($db->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = $dbForProject->getDocument('database_' . $db->getSequence(), $collectionId);
if ($collection->isEmpty()) {
// table or collection.
throw new Exception($this->getGrandParentNotFoundException());
}
$count = $dbForProject->count('indexes', [
Query::equal('collectionInternalId', [$collection->getSequence()]),
Query::equal('databaseInternalId', [$db->getSequence()])
], 61);
$limit = $dbForProject->getLimitForIndexes();
if ($count >= $limit) {
throw new Exception($this->getLimitException(), 'Index limit exceeded');
}
// Convert Document array to array of attribute metadata
$oldAttributes = \array_map(fn ($a) => $a->getArrayCopy(), $collection->getAttribute('attributes'));
$oldAttributes[] = [
'key' => '$id',
'type' => Database::VAR_STRING,
'status' => 'available',
'required' => true,
'array' => false,
'default' => null,
'size' => Database::LENGTH_KEY
];
$oldAttributes[] = [
'key' => '$createdAt',
'type' => Database::VAR_DATETIME,
'status' => 'available',
'signed' => false,
'required' => false,
'array' => false,
'default' => null,
'size' => 0
];
$oldAttributes[] = [
'key' => '$updatedAt',
'type' => Database::VAR_DATETIME,
'status' => 'available',
'signed' => false,
'required' => false,
'array' => false,
'default' => null,
'size' => 0
];
$contextType = $this->getParentContext();
foreach ($attributes as $i => $attribute) {
// find attribute metadata in collection document
$attributeIndex = \array_search($attribute, array_column($oldAttributes, 'key'));
if ($attributeIndex === false) {
throw new Exception($this->getParentUnknownException(), "Unknown $contextType: " . $attribute . ". Verify the $contextType name or create the $contextType.");
}
$attributeStatus = $oldAttributes[$attributeIndex]['status'];
$attributeType = $oldAttributes[$attributeIndex]['type'];
$attributeArray = $oldAttributes[$attributeIndex]['array'] ?? false;
if ($attributeType === Database::VAR_RELATIONSHIP) {
throw new Exception($this->getParentInvalidTypeException(), "Cannot create an index for a relationship $contextType: " . $oldAttributes[$attributeIndex]['key']);
}
// ensure attribute is available
if ($attributeStatus !== 'available') {
$contextType = ucfirst($contextType);
throw new Exception($this->getParentNotAvailableException(), "$contextType not available: " . $oldAttributes[$attributeIndex]['key']);
}
$lengths[$i] ??= null;
if ($attributeArray === true) {
$lengths[$i] = Database::ARRAY_INDEX_LENGTH;
$orders[$i] = null;
}
}
$index = new Document([
'$id' => ID::custom($db->getSequence() . '_' . $collection->getSequence() . '_' . $key),
'key' => $key,
'status' => 'processing', // processing, available, failed, deleting, stuck
'databaseInternalId' => $db->getSequence(),
'databaseId' => $databaseId,
'collectionInternalId' => $collection->getSequence(),
'collectionId' => $collectionId,
'type' => $type,
'attributes' => $attributes,
'lengths' => $lengths,
'orders' => $orders,
]);
$validator = new IndexValidator(
$collection->getAttribute('attributes'),
$dbForProject->getAdapter()->getMaxIndexLength(),
$dbForProject->getAdapter()->getInternalIndexesKeys(),
);
if (!$validator->isValid($index)) {
throw new Exception($this->getInvalidTypeException(), $validator->getDescription());
}
try {
$index = $dbForProject->createDocument('indexes', $index);
} catch (DuplicateException) {
throw new Exception($this->getDuplicateException());
}
$dbForProject->purgeCachedDocument('database_' . $db->getSequence(), $collectionId);
$queueForDatabase
->setType(DATABASE_TYPE_CREATE_INDEX)
->setDatabase($db);
if ($this->isCollectionsAPI()) {
$queueForDatabase
->setCollection($collection)
->setDocument($index);
} else {
$queueForDatabase
->setTable($collection)
->setRow($index);
}
$queueForEvents
->setContext('database', $db)
->setParam('databaseId', $databaseId)
->setParam('indexId', $index->getId())
->setParam('collectionId', $collection->getId())
->setParam('tableId', $collection->getId())
->setContext($this->getCollectionsEventsContext(), $collection);
$response
->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED)
->dynamic($index, $this->getResponseModel());
}
}
@@ -0,0 +1,123 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Indexes;
use Appwrite\Event\Database as EventDatabase;
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 as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
class Delete extends Action
{
public static function getName(): string
{
return 'deleteIndex';
}
/**
* 1. `SDKResponse` uses `UtopiaResponse::MODEL_NONE`.
* 2. But we later need the actual return type for events queue below!
*/
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_INDEX;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_DELETE)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/indexes/:key')
->desc('Delete index')
->groups(['api', 'database'])
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].indexes.[indexId].update')
->label('audits.event', 'index.delete')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/delete-index.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_NOCONTENT,
model: UtopiaResponse::MODEL_NONE,
)
],
contentType: ContentType::NONE
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('key', '', new Key(), 'Index Key.')
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void
{
$db = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($db->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = $dbForProject->getDocument('database_' . $db->getSequence(), $collectionId);
if ($collection->isEmpty()) {
// table or collection.
throw new Exception($this->getGrandParentNotFoundException());
}
$index = $dbForProject->getDocument('indexes', $db->getSequence() . '_' . $collection->getSequence() . '_' . $key);
if (empty($index->getId())) {
throw new Exception($this->getNotFoundException());
}
// Only update status if removing available index
if ($index->getAttribute('status') === 'available') {
$index = $dbForProject->updateDocument('indexes', $index->getId(), $index->setAttribute('status', 'deleting'));
}
$dbForProject->purgeCachedDocument('database_' . $db->getSequence(), $collectionId);
$queueForDatabase
->setType(DATABASE_TYPE_DELETE_INDEX)
->setDatabase($db);
if ($this->isCollectionsAPI()) {
$queueForDatabase
->setCollection($collection)
->setDocument($index);
} else {
$queueForDatabase
->setTable($collection)
->setRow($index);
}
$queueForEvents
->setContext('database', $db)
->setParam('databaseId', $databaseId)
->setParam('indexId', $index->getId())
->setParam('tableId', $collection->getId())
->setParam('collectionId', $collection->getId())
->setContext($this->getCollectionsEventsContext(), $collection)
->setPayload($response->output($index, $this->getResponseModel()));
$response->noContent();
}
}
@@ -0,0 +1,81 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Indexes;
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 as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
class Get extends Action
{
public static function getName(): string
{
return 'getIndex';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_INDEX;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/indexes/:key')
->desc('Get index')
->groups(['api', 'database'])
->label('scope', 'collections.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/get-index.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('key', null, new Key(), 'Index Key.')
->inject('response')
->inject('dbForProject')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, UtopiaResponse $response, Database $dbForProject): void
{
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId);
if ($collection->isEmpty()) {
// table or collection.
throw new Exception($this->getGrandParentNotFoundException());
}
$index = $collection->find('key', $key, 'indexes');
if (empty($index)) {
throw new Exception($this->getNotFoundException());
}
$response->dynamic($index, $this->getResponseModel());
}
}
@@ -0,0 +1,139 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Indexes;
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\Database\Validator\Queries\Indexes;
use Appwrite\Utopia\Response as UtopiaResponse;
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\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
class XList extends Action
{
public static function getName(): string
{
return 'listIndexes';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_INDEX_LIST;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/indexes')
->desc('List indexes')
->groups(['api', 'database'])
->label('scope', 'collections.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/list-indexes.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('queries', [], new Indexes(), '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(', ', Indexes::ALLOWED_ATTRIBUTES), true)
->inject('response')
->inject('dbForProject')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, array $queries, UtopiaResponse $response, Database $dbForProject): void
{
/** @var Document $database */
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId);
if ($collection->isEmpty()) {
// table or collection.
throw new Exception($this->getGrandParentNotFoundException());
}
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
\array_push(
$queries,
Query::equal('databaseId', [$databaseId]),
Query::equal('collectionId', [$collectionId]),
);
/**
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
*/
$cursor = \array_filter($queries, function ($query) {
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
});
$cursor = reset($cursor);
if ($cursor) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
}
$indexId = $cursor->getValue();
$cursorDocument = Authorization::skip(fn () => $dbForProject->find('indexes', [
Query::equal('collectionInternalId', [$collection->getSequence()]),
Query::equal('databaseInternalId', [$database->getSequence()]),
Query::equal('key', [$indexId]),
Query::limit(1)
]));
if (empty($cursorDocument) || $cursorDocument[0]->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Index '{$indexId}' for the 'cursor' value not found.");
}
$cursor->setValue($cursorDocument[0]);
}
try {
$total = $dbForProject->count('indexes', $queries, APP_LIMIT_COUNT);
$indexes = $dbForProject->find('indexes', $queries);
} catch (OrderException $e) {
$documents = $this->isCollectionsAPI() ? 'documents' : 'rows';
$attribute = $this->isCollectionsAPI() ? 'attribute' : 'column';
$message = "The order $attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all $documents order $attribute values are non-null.";
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, $message);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$response->dynamic(new Document([
'total' => $total,
'indexes' => $indexes,
]), $this->getResponseModel());
}
}
@@ -0,0 +1,154 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Logs;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use DeviceDetector\DeviceDetector as Detector;
use MaxMind\Db\Reader;
use Utopia\Audit\Audit;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Queries;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\UID;
use Utopia\Locale\Locale;
use Utopia\Swoole\Response as SwooleResponse;
class XList extends Action
{
public static function getName(): string
{
return 'listCollectionLogs';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_LOG_LIST;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/logs')
->desc('List collection logs')
->groups(['api', 'database'])
->label('scope', 'collections.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: 'databases',
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/get-collection-logs.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->inject('response')
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, array $queries, UtopiaResponse $response, Database $dbForProject, Locale $locale, Reader $geodb): void
{
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collectionDocument = $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId);
$collection = $dbForProject->getCollection('database_' . $database->getSequence() . '_collection_' . $collectionDocument->getSequence());
if ($collection->isEmpty()) {
throw new Exception($this->getNotFoundException());
}
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
// Temp fix for logs
$queries[] = Query::or([
Query::greaterThan('$createdAt', DateTime::format(new \DateTime('2025-02-26T01:30+00:00'))),
Query::lessThan('$createdAt', DateTime::format(new \DateTime('2025-02-13T00:00+00:00'))),
]);
$audit = new Audit($dbForProject);
$resource = 'database/' . $databaseId . '/' . $this->getContext() . '/' . $collectionId;
$logs = $audit->getLogsByResource($resource, $queries);
$output = [];
foreach ($logs as $i => &$log) {
$log['userAgent'] = (!empty($log['userAgent'])) ? $log['userAgent'] : 'UNKNOWN';
$detector = new Detector($log['userAgent']);
$detector->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then)
$os = $detector->getOS();
$client = $detector->getClient();
$device = $detector->getDevice();
$output[$i] = new Document([
'event' => $log['event'],
'userId' => $log['data']['userId'],
'userEmail' => $log['data']['userEmail'] ?? null,
'userName' => $log['data']['userName'] ?? null,
'mode' => $log['data']['mode'] ?? null,
'ip' => $log['ip'],
'time' => $log['time'],
'osCode' => $os['osCode'],
'osName' => $os['osName'],
'osVersion' => $os['osVersion'],
'clientType' => $client['clientType'],
'clientCode' => $client['clientCode'],
'clientName' => $client['clientName'],
'clientVersion' => $client['clientVersion'],
'clientEngine' => $client['clientEngine'],
'clientEngineVersion' => $client['clientEngineVersion'],
'deviceName' => $device['deviceName'],
'deviceBrand' => $device['deviceBrand'],
'deviceModel' => $device['deviceModel']
]);
$record = $geodb->get($log['ip']);
if ($record) {
$output[$i]['countryCode'] = $locale->getText('countries.' . strtolower($record['country']['iso_code']), false) ? \strtolower($record['country']['iso_code']) : '--';
$output[$i]['countryName'] = $locale->getText('countries.' . strtolower($record['country']['iso_code']), $locale->getText('locale.country.unknown'));
} else {
$output[$i]['countryCode'] = '--';
$output[$i]['countryName'] = $locale->getText('locale.country.unknown');
}
}
$response->dynamic(new Document([
'logs' => $output,
'total' => $audit->countLogsByResource($resource, $queries),
]), $this->getResponseModel());
}
}
@@ -0,0 +1,110 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections;
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 as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Permissions;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
class Update extends Action
{
public static function getName(): string
{
return 'updateCollection';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_COLLECTION;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PUT)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId')
->desc('Update collection')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].update')
->label('audits.event', 'collection.update')
->label('audits.resource', 'database/{request.databaseId}/collections/{request.collectionId}')
->label('sdk', new Method(
namespace: 'databases',
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/update-collection.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('name', null, new Text(128), 'Collection name. Max length: 128 chars.')
->param('permissions', null, new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE), 'An array of permission strings. By default, the current permissions are inherited. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('documentSecurity', false, new Boolean(true), 'Enables configuring permissions for individual documents. A user needs one of document or collection level permissions to access a document. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('enabled', true, new Boolean(), 'Is collection enabled? When set to \'disabled\', users cannot access the collection but Server SDKs with and API key can still read and write to the collection. No data is lost when this is toggled.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, bool $enabled, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents): void
{
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId);
if ($collection->isEmpty()) {
throw new Exception($this->getNotFoundException());
}
$permissions ??= $collection->getPermissions() ?? [];
// Map aggregate permissions into the multiple permissions they represent.
$permissions = Permission::aggregate($permissions);
$enabled ??= $collection->getAttribute('enabled', true);
$collection = $dbForProject->updateDocument(
'database_' . $database->getSequence(),
$collectionId,
$collection
->setAttribute('name', $name)
->setAttribute('$permissions', $permissions)
->setAttribute('documentSecurity', $documentSecurity)
->setAttribute('enabled', $enabled)
->setAttribute('search', \implode(' ', [$collectionId, $name]))
);
$dbForProject->updateCollection('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $permissions, $documentSecurity);
$queueForEvents
->setContext('database', $database)
->setParam('databaseId', $databaseId)
->setParam($this->getEventsParamKey(), $collection->getId());
$response->dynamic($collection, $this->getResponseModel());
}
}
@@ -0,0 +1,136 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Usage;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\WhiteList;
class Get extends Action
{
public static function getName(): string
{
return 'getCollectionUsage';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_USAGE_COLLECTION;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/usage')
->desc('Get collection usage stats')
->groups(['api', 'database', 'usage'])
->label('scope', 'collections.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: 'databases',
group: null,
name: self::getName(),
description: '/docs/references/databases/get-collection-usage.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel()
)
],
contentType: ContentType::JSON,
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true)
->param('collectionId', '', new UID(), 'Collection ID.')
->inject('response')
->inject('dbForProject')
->callback($this->action(...));
}
public function action(string $databaseId, string $range, string $collectionId, UtopiaResponse $response, Database $dbForProject): void
{
$database = $dbForProject->getDocument('databases', $databaseId);
$collectionDocument = $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId);
$collection = $dbForProject->getCollection('database_' . $database->getSequence() . '_collection_' . $collectionDocument->getSequence());
if ($collection->isEmpty()) {
throw new Exception($this->getNotFoundException());
}
$periods = Config::getParam('usage', []);
$stats = $usage = [];
$days = $periods[$range];
$metrics = [
str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$database->getSequence(), $collectionDocument->getSequence()], METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS),
];
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {
foreach ($metrics as $metric) {
$result = $dbForProject->findOne('stats', [
Query::equal('metric', [$metric]),
Query::equal('period', ['inf'])
]);
$stats[$metric]['total'] = $result['value'] ?? 0;
$limit = $days['limit'];
$period = $days['period'];
$results = $dbForProject->find('stats', [
Query::equal('metric', [$metric]),
Query::equal('period', [$period]),
Query::limit($limit),
Query::orderDesc('time'),
]);
$stats[$metric]['data'] = [];
foreach ($results as $result) {
$stats[$metric]['data'][$result->getAttribute('time')] = [
'value' => $result->getAttribute('value'),
];
}
}
});
$format = match ($days['period']) {
'1h' => 'Y-m-d\TH:00:00.000P',
'1d' => 'Y-m-d\T00:00:00.000P',
};
foreach ($metrics as $metric) {
$usage[$metric]['total'] = $stats[$metric]['total'];
$usage[$metric]['data'] = [];
$leap = time() - ($days['limit'] * $days['factor']);
while ($leap < time()) {
$leap += $days['factor'];
$formatDate = date($format, $leap);
$usage[$metric]['data'][] = [
'value' => $stats[$metric]['data'][$formatDate]['value'] ?? 0,
'date' => $formatDate,
];
}
}
$prefix = $this->isCollectionsAPI() ? 'documents' : 'rows';
// prefix, prefixTotal
$usageDocument = new Document([
'range' => $range,
$prefix => $usage[$metrics[0]]['data'],
$prefix . 'Total' => $usage[$metrics[0]]['total'],
]);
$response->dynamic($usageDocument, $this->getResponseModel());
}
}
@@ -0,0 +1,122 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections;
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\Database\Validator\Queries\Collections;
use Appwrite\Utopia\Response as UtopiaResponse;
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\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Text;
class XList extends Action
{
public static function getName(): string
{
return 'listCollections';
}
protected function getResponseModel(): string
{
return UtopiaResponse::MODEL_COLLECTION_LIST;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/databases/:databaseId/collections')
->desc('List collections')
->groups(['api', 'database'])
->label('scope', 'collections.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: 'databases',
group: $this->getSdkGroup(),
name: self::getName(),
description: '/docs/references/databases/list-collections.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('queries', [], new Collections(), '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(', ', Collections::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->inject('response')
->inject('dbForProject')
->callback($this->action(...));
}
public function action(string $databaseId, array $queries, string $search, UtopiaResponse $response, Database $dbForProject): void
{
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
if (!empty($search)) {
$queries[] = Query::search('search', $search);
}
/**
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
*/
$cursor = \array_filter($queries, function ($query) {
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
});
$cursor = reset($cursor);
if ($cursor) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
}
$collectionIdId = $cursor->getValue();
$cursorDocument = $dbForProject->getDocument('database_' . $database->getSequence(), $collectionIdId);
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, ucfirst($this->getContext()) . " '$collectionIdId' for the 'cursor' value not found.");
}
$cursor->setValue($cursorDocument);
}
try {
$collections = $dbForProject->find('database_' . $database->getSequence(), $queries);
$total = $dbForProject->count('database_' . $database->getSequence(), $queries, APP_LIMIT_COUNT);
} catch (OrderException) {
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL);
} catch (QueryException) {
throw new Exception(Exception::GENERAL_QUERY_INVALID);
}
$response->dynamic(new Document([
'total' => $total,
$this->getSdkGroup() => $collections,
]), $this->getResponseModel());
}
}
@@ -0,0 +1,122 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases;
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\Database\Validator\CustomId;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Exception\Index as IndexException;
use Utopia\Database\Exception\Limit as LimitException;
use Utopia\Database\Exception\Structure as StructureException;
use Utopia\Database\Helpers\ID;
use Utopia\Platform\Action;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
class Create extends Action
{
public static function getName(): string
{
return 'createDatabase';
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/databases')
->desc('Create database')
->groups(['api', 'database'])
->label('event', 'databases.[databaseId].create')
->label('scope', 'databases.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('audits.event', 'database.create')
->label('audits.resource', 'database/{response.$id}')
->label('sdk', new Method(
namespace: 'databases',
group: 'databases',
name: 'create',
description: '/docs/references/databases/create.md',
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_CREATED,
model: UtopiaResponse::MODEL_DATABASE,
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new CustomId(), 'Unique 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.')
->param('name', '', new Text(128), 'Database name. Max length: 128 chars.')
->param('enabled', true, new Boolean(), 'Is the database enabled? When set to \'disabled\', users cannot access the database but Server SDKs with an API key can still read and write to the database. No data is lost when this is toggled.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(string $databaseId, string $name, bool $enabled, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents): void
{
$databaseId = $databaseId == 'unique()' ? ID::unique() : $databaseId;
try {
$dbForProject->createDocument('databases', new Document([
'$id' => $databaseId,
'name' => $name,
'enabled' => $enabled,
'search' => implode(' ', [$databaseId, $name]),
]));
} catch (DuplicateException) {
throw new Exception(Exception::DATABASE_ALREADY_EXISTS);
} catch (StructureException $e) {
// TODO: @Jake, how do we handle this document/row?
// there's no context awareness at this level on what the api is.
throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $e->getMessage());
}
$database = $dbForProject->getDocument('databases', $databaseId);
$collections = (Config::getParam('collections', [])['databases'] ?? [])['collections'] ?? [];
if (empty($collections)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'The "collections" collection is not configured.');
}
$attributes = [];
foreach ($collections['attributes'] as $attribute) {
$attributes[] = new Document($attribute);
}
$indexes = [];
foreach ($collections['indexes'] as $index) {
$indexes[] = new Document($index);
}
try {
$dbForProject->createCollection('database_' . $database->getSequence(), $attributes, $indexes);
} catch (DuplicateException) {
throw new Exception(Exception::DATABASE_ALREADY_EXISTS);
} catch (IndexException) {
throw new Exception(Exception::INDEX_INVALID);
} catch (LimitException) {
// TODO: @Jake, how do we handle this collection/table?
// there's no context awareness at this level on what the api is.
throw new Exception(Exception::COLLECTION_LIMIT_EXCEEDED);
}
$queueForEvents->setParam('databaseId', $database->getId());
$response
->setStatusCode(SwooleResponse::STATUS_CODE_CREATED)
->dynamic($database, UtopiaResponse::MODEL_DATABASE);
}
}

Some files were not shown because too many files have changed in this diff Show More