Compare commits

..
Author SHA1 Message Date
loks0n 5d15632bf8 dev: split development compose 2026-02-10 14:56:36 +00:00
Matej BačoandGitHub 51f036491a Merge pull request #11278 from appwrite/feat-list-api-keys-queries
Feat: queries param on list api keys
2026-02-10 12:47:00 +01:00
Matej Bačo 4df0934023 Comment fix 2026-02-10 12:01:38 +01:00
Luke B. SilverandGitHub 5e974713b7 Merge pull request #11281 from appwrite/feat/worker-executions
feat: worker executions
2026-02-10 10:56:30 +00:00
Matej Bačo e2071bd5dd add backwards compatibility 2026-02-10 11:48:29 +01:00
Matej Bačo 0621a32aa6 Fix tests; speed up tests; fix 5xx error 2026-02-10 11:43:22 +01:00
Matej Bačo e1cbbe3943 Merge branch '1.8.x' into feat-list-api-keys-queries 2026-02-10 10:46:56 +01:00
Matej BačoandGitHub 226e36db95 Merge pull request #11277 from appwrite/feat-api-key-custom-id
Feat: custom ID for API keys
2026-02-10 10:46:00 +01:00
Matej BačoandGitHub faedf49488 Merge pull request #11276 from appwrite/fix-rule-oauth
Fix: rule oauth flow
2026-02-10 10:27:25 +01:00
loks0n b432918385 feat: worker executions 2026-02-09 20:20:44 +00:00
Matej Bačo a87263a571 Fix failing test 2026-02-09 20:07:33 +01:00
Matej Bačo dafa97879c AI review fixes 2026-02-09 17:12:30 +01:00
Matej Bačo a263afeff1 AI quality fixes 2026-02-09 17:10:00 +01:00
Matej Bačo 29915ddd3b Manual fixes of agent code 2026-02-09 16:58:44 +01:00
Matej Bačo c0f5fa90cb Fix AI review 2026-02-09 16:53:18 +01:00
Matej Bačo e666dc9504 AI review fixes 2026-02-09 16:42:14 +01:00
Matej Bačo 3dc69ba62a Revert unwanted push 2026-02-09 15:55:49 +01:00
Matej Bačo 7bf5f2d360 Fix bug 5xx error 2026-02-09 15:55:35 +01:00
Matej Bačo 6df5556473 Implement queries param to list api keys 2026-02-09 14:58:36 +01:00
Matej Bačo 96e85c0bab AI pr review 2026-02-09 14:35:47 +01:00
Matej Bačo 40ab50ec9d formatting fix 2026-02-09 14:34:52 +01:00
Matej Bačo 9b762dde40 formatting fix 2026-02-09 14:34:36 +01:00
Matej Bačo 615aff0714 Allow custom ID for API keys 2026-02-09 14:34:05 +01:00
Matej BačoandGitHub 994850c5a6 Merge pull request #11275 from appwrite/fix-vcs-template-flow
Fix VCS template flow
2026-02-09 14:00:39 +01:00
Matej Bačo 525b929e54 Fix implementation, add tests 2026-02-09 13:57:19 +01:00
Matej Bačo 074ffad826 Improve origin unit tests 2026-02-09 13:46:15 +01:00
Matej Bačo 36c87d109a Fix rule oauth flow 2026-02-09 13:45:18 +01:00
Matej Bačo e09dd98f36 Fix site vcs deployment bug too 2026-02-09 13:30:46 +01:00
Matej Bačo 96e1221a5f Fix implementation 2026-02-09 13:27:31 +01:00
Matej Bačo e13f4c8545 Fix VCS template flow 2026-02-09 13:26:33 +01:00
HemachandarandGitHub 353b7f2a49 Move VCS auth & callback APIs to Modules (#11274)
* Move VCS auth & callback APIs to Modules

* lint

* rename
2026-02-09 15:55:37 +05:30
HemachandarandGitHub f7ab8e46d3 Improve certificate dns failure logs (#11268)
* Better logs for DNS failures

* some more

* appwrite exception

* type

* refactor

* tiny

* better logs
2026-02-09 15:45:41 +05:30
Chirag AggarwalandGitHub bec6725900 Merge pull request #11273 from appwrite/release-cli-13.3.1
chore: release cli 13.3.1
2026-02-09 13:59:31 +05:30
Chirag Aggarwal 494ce5fc6a chore: release cli 13.3.1 2026-02-09 13:14:28 +05:30
Matej BačoandGitHub bae7b92dd9 Merge pull request #11269 from appwrite/fix-oauth-for-rules
Fix redirect url approval for oauth flow
2026-02-08 15:08:30 +01:00
Matej Bačo 801707c407 Linter fix 2026-02-08 14:47:47 +01:00
Matej Bačo 94581adfcb Improve dev key tests 2026-02-08 14:44:21 +01:00
Matej Bačo fd323feae8 Simplify diff 2026-02-08 14:37:35 +01:00
Matej Bačo 3a0dc60a4c Add test; fix implementation; pr reviews 2026-02-08 14:36:34 +01:00
Matej Bačo bed46a6bef Fix redirect url approval for oauth flow 2026-02-08 14:12:23 +01:00
Chirag AggarwalandGitHub ba5273872b Merge pull request #11267 from appwrite/fix/disable-execution-writes-nyc-api
fix: disable execution document writes for nyc region in API
2026-02-07 21:45:07 +05:30
Chirag Aggarwal f1639e0b67 fix: disable execution document writes for nyc region in API
Temporarily skip execution createDocument calls in the Executions
Create handler when _APP_REGION is set to 'nyc' to reduce database
load. Marked with TODO comments for removal.
2026-02-07 21:42:10 +05:30
Chirag AggarwalandGitHub e8fe9222b2 Merge pull request #11266 from appwrite/fix/disable-execution-writes-nyc
fix: disable execution document writes for nyc region
2026-02-07 21:20:58 +05:30
Chirag Aggarwal a520a67da8 fix: disable execution document writes for nyc region
Temporarily skip all execution createDocument and updateDocument calls
in the Functions worker when _APP_REGION is set to 'nyc' to reduce
database load. Marked with TODO comments for removal.
2026-02-07 21:19:00 +05:30
Luke B. SilverandGitHub 9c03c5fab3 Merge pull request #11265 from appwrite/chore/upgrade-traefik
chore: upgrade traefik
2026-02-06 20:14:38 +00:00
loks0n d864a61f93 chore: upgrade traefik 2026-02-06 19:16:37 +00:00
Chirag AggarwalandGitHub fb1cbdca6c Merge pull request #11263 from appwrite/upgrade-spomky-labs/otphp
chore: upgrade spomky-labs/otphp to latest
2026-02-06 20:14:26 +05:30
Chirag Aggarwal ab35806372 update system lib 2026-02-06 15:43:59 +05:30
Luke B. SilverandGitHub 9612ed338f Merge pull request #11262 from appwrite/chore-remove-auth-skip-from-workers
chore: remove auth skip from workers
2026-02-06 10:13:11 +00:00
Chirag Aggarwal 1219391c76 update composer 2026-02-06 15:27:42 +05:30
HemachandarandGitHub da7f5b7336 Move VCS repository APIs to Modules (#11259)
* Move VCS repository APIs to Modules

* add to http

* lint

* strategy fix

* rename to detections
2026-02-06 15:22:44 +05:30
loks0n ec418e60b7 chore: remove auth skip from workers 2026-02-06 09:44:10 +00:00
Chirag Aggarwal 455004a536 chore: upgrade spomky-labs/otphp to latest 2026-02-06 15:12:37 +05:30
Matej BačoandGitHub 2ad0135890 Merge pull request #11249 from appwrite/ser-1092-v2
Create rule in SSL task
2026-02-06 09:34:26 +01:00
Jake BarnbyandGitHub 6414c0ebda Merge pull request #11201 from appwrite/get-cursor-queries
Get cursor queries
2026-02-06 07:15:35 +00:00
Jake BarnbyandGitHub 801befeacd Merge branch '1.8.x' into get-cursor-queries 2026-02-06 05:59:48 +00:00
Jake BarnbyandGitHub 941d40e704 Merge pull request #11260 from appwrite/localhost-endpoint
Localhost endpoint CSV
2026-02-06 05:49:55 +00:00
fogelito 093bc7bf3d Merge branch '1.8.x' of https://github.com/appwrite/appwrite into localhost-endpoint 2026-02-06 07:14:07 +02:00
fogelito 90f10ce685 Use https 2026-02-06 07:13:39 +02:00
Hemachandar 45e74d7b3d rule 2026-02-06 00:14:15 +05:30
Chirag AggarwalandGitHub 4090ce853e Merge pull request #11256 from appwrite/release-cli-13.3.0
release cli sdk 13.3.0
2026-02-05 17:52:36 +05:30
Chirag Aggarwal af5c0da1d9 release cli sdk 13.3.0 2026-02-05 17:05:22 +05:30
Jake BarnbyandGitHub b5bb4c2e07 Merge pull request #11254 from appwrite/feat-collection-size
Expose row bytes used/bytes free on table/collection
2026-02-05 11:25:02 +00:00
Shmuel FogelandGitHub c492cfe9ab Merge pull request #11229 from appwrite/localhost-endpoint
Localhost endpoint
2026-02-05 13:00:00 +02:00
Jake Barnby a866f0a69f Gen specs 2026-02-05 23:50:16 +13:00
Jake Barnby 3b2c05eb8d Update desc 2026-02-05 23:50:10 +13:00
fogelito e28ba64dbf Merge branch '1.8.x' of https://github.com/appwrite/appwrite into localhost-endpoint 2026-02-05 12:36:25 +02:00
Jake BarnbyandGitHub 747b400616 Merge pull request #11251 from appwrite/fix-realtime-performance
Realtime query performance improvements
2026-02-05 10:15:58 +00:00
fogelito cb58da7279 Merge branch '1.8.x' of https://github.com/appwrite/appwrite into localhost-endpoint 2026-02-05 12:13:02 +02:00
Matej BačoandGitHub 106458a79d Merge pull request #11208 from appwrite/ser-539-modules
Move project create & update APIs to Modules
2026-02-05 11:11:09 +01:00
fogelito d40b1707ba Remove var_dump 2026-02-05 12:09:54 +02:00
fogelito 8600d0e64f Remove var_dump 2026-02-05 12:04:57 +02:00
Jake BarnbyandGitHub 2bc8d80273 Merge branch '1.8.x' into feat-collection-size 2026-02-05 09:55:23 +00:00
Jake BarnbyandGitHub 1d94e60a37 Merge branch '1.8.x' into fix-realtime-performance 2026-02-05 09:54:42 +00:00
Jake Barnby 4f7337759f Expose row bytes used/bytes free on table/collection 2026-02-05 22:53:04 +13:00
Jake BarnbyandGitHub fed8354c52 Merge pull request #11237 from appwrite/realtime-query-subscriptions
Realtime query subscriptions
2026-02-05 09:36:54 +00:00
Jake BarnbyandClaude Opus 4.5 e21614088f fix: Address CodeRabbit review comments
- Fix OR precheck bug: skip attribute existence check when OR conditions
  exist, since OR can match with partial attributes present
- Remove dead code in app/realtime.php (unused $names and $channels variables)
- Add tests for OR queries with missing attributes in one branch

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 22:24:26 +13:00
Jake BarnbyandClaude Opus 4.5 18125156d2 fix: Update compileAndFilter return type to nullable array
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 22:14:41 +13:00
Jake BarnbyandClaude Opus 4.5 765d334674 fix: Return null instead of empty array for non-matching queries
RuntimeQuery::filter() now returns null when the query doesn't match,
instead of an empty array. This distinguishes between "no match" and
"match with empty payload", fixing the issue where subscriptions with
empty payloads weren't being delivered.

Updated Realtime::getSubscribers() to check for null instead of using
!empty(), and updated all tests to expect null for non-matches.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 22:08:43 +13:00
Jake BarnbyandClaude Opus 4.5 3e31094e1e fix: Fix unit test failures for RuntimeQuery and Messaging tests
- Add missing isSelectAll() method to RuntimeQuery class
- Update RuntimeQueryTest to use compile() before filter() since filter()
  expects pre-compiled query arrays, not Query objects
- Remove incorrect break statement in Realtime::getSubscribers() that was
  stopping iteration after the first matching channel, causing subscribers
  to not be found for subsequent channels
- Use local variable $subscriptionsByChannel to avoid unused variable warning

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 21:57:00 +13:00
fogelito 074470c2c2 check _APP_MIGRATION_HOST 2026-02-05 10:35:35 +02:00
ArnabChatterjee20k f8f2f9ca25 removed reduntant test 2026-02-05 14:03:26 +05:30
fogelito aff180c517 docker-compose.yml 2026-02-05 10:30:21 +02:00
fogelito 5d1b63fd35 docker-compose.yml 2026-02-05 10:29:45 +02:00
fogelito 64f8b9195b _APP_MIGRATION_HOST 2026-02-05 10:12:55 +02:00
fogelito 12b7f94c1f _APP_MIGRATION_HOST 2026-02-05 10:06:11 +02:00
Jake Barnby e490f375ee Batch groups 2026-02-05 20:48:40 +13:00
ArnabChatterjee20kandGitHub 96ea3391d3 Merge branch '1.8.x' into realtime-query-subscriptions 2026-02-05 13:01:28 +05:30
fogelito 85ea4c8904 Merge branch '1.8.x' of https://github.com/appwrite/appwrite into localhost-endpoint 2026-02-05 08:54:50 +02:00
fogelito 7e385fe87f localhosts 2026-02-05 08:52:12 +02:00
Jake Barnby d06759e33e Cleanup 2026-02-05 18:47:54 +13:00
Jake Barnby ecf0a1ec67 Avoid closures on hot path 2026-02-05 18:47:49 +13:00
Jake Barnby ead2afb9d4 Early exit on select all 2026-02-05 18:47:27 +13:00
Hemachandar a30f12cd66 tiny 2026-02-04 19:31:00 +05:30
Hemachandar 0003443c77 Create rule in SSL task 2026-02-04 19:23:27 +05:30
fogelito 4cf6f4e066 Merge branch '1.8.x' of https://github.com/appwrite/appwrite into localhost-endpoint 2026-02-04 13:58:22 +02:00
fogelito 186ed5b61f fix graphQL 2026-02-04 12:29:20 +02:00
fogelito 522c9d3272 Merge branch '1.8.x' of https://github.com/appwrite/appwrite into localhost-endpoint 2026-02-04 12:16:24 +02:00
fogelito 5ff31e92d7 Merge branch '1.8.x' of https://github.com/appwrite/appwrite into localhost-endpoint
# Conflicts:
#	composer.lock
2026-02-04 12:03:28 +02:00
fogelito 1db42c6fc7 revert lock 2026-02-04 11:43:56 +02:00
fogelito febb7dc10a Merge branch '1.8.x' of https://github.com/appwrite/appwrite into localhost-endpoint 2026-02-04 10:53:26 +02:00
fogelito ffd568aecd Update 1.8.x 2026-02-04 10:05:54 +02:00
fogelito d811d1c534 Merge branch '1.8.x' of https://github.com/appwrite/appwrite into localhost-endpoint
# Conflicts:
#	composer.lock
2026-02-04 10:04:52 +02:00
fogelito c2b87cff66 $this->webEndpoint, 2026-02-04 09:53:37 +02:00
fogelito ca03176ad6 $this->webEndpoint, 2026-02-04 09:53:19 +02:00
fogelito 9bafa90ac8 _APP_HOSTNAMES 2026-02-03 17:39:29 +02:00
fogelito 736b72164e ci cd 2026-02-03 16:14:00 +02:00
ArnabChatterjee20k f82662e84e added dot escaping fix for channel names 2026-02-03 18:33:35 +05:30
fogelito 7d193d9c36 force localhost 2026-02-03 13:41:02 +02:00
fogelito 8b4f4d2fea localhost 2026-02-03 12:43:37 +02:00
fogelito 3da3b6a324 pull changes 2026-02-03 11:49:53 +02:00
fogelito 20e209038d Merge branch '1.8.x' of https://github.com/appwrite/appwrite into localhost-endpoint 2026-02-03 11:49:17 +02:00
fogelito c0cbf991ab Run tests 2026-02-03 11:48:48 +02:00
fogelito 91f3bf0244 Run tests 2026-02-03 11:47:21 +02:00
fogelito f1a2e869a2 localhost setUp 2026-02-03 10:11:03 +02:00
fogelito 95ce53b66e Exception 2026-01-28 15:15:33 +02:00
fogelito f35aad0816 Remove extra line 2026-01-28 14:55:09 +02:00
fogelito 69e6c0afc0 getCursorQueries 2026-01-28 14:53:24 +02:00
97 changed files with 6930 additions and 5246 deletions
+1
View File
@@ -10,6 +10,7 @@ _APP_CONSOLE_SESSION_ALERTS=enabled
_APP_CONSOLE_WHITELIST_IPS=
_APP_CONSOLE_COUNTRIES_DENYLIST=AQ
_APP_CONSOLE_HOSTNAMES=localhost,appwrite.io,*.appwrite.io
_APP_MIGRATION_HOST=appwrite
_APP_SYSTEM_EMAIL_NAME=Appwrite
_APP_SYSTEM_EMAIL_ADDRESS=noreply@appwrite.io
_APP_SYSTEM_TEAM_EMAIL=team@appwrite.io
+4 -6
View File
@@ -71,12 +71,10 @@ jobs:
run: docker compose down -v
- name: Installing latest version
run: |
rm docker-compose.yml
rm .env
curl https://appwrite.io/install/compose -o docker-compose.yml
curl https://appwrite.io/install/env -o .env
sed -i 's/_APP_OPTIONS_ABUSE=enabled/_APP_OPTIONS_ABUSE=disabled/g' .env
docker compose up -d
curl https://appwrite.io/install/compose -o docker-compose.latest.yml
curl https://appwrite.io/install/env -o .env.latest
sed -i 's/_APP_OPTIONS_ABUSE=enabled/_APP_OPTIONS_ABUSE=disabled/g' .env.latest
docker compose -f docker-compose.latest.yml --env-file .env.latest up -d
sleep 10
- name: Benchmark Latest
run: oha -z 180s http://localhost/v1/health/version --output-format json > benchmark-latest.json
+1
View File
@@ -81,6 +81,7 @@ RUN chmod +x /usr/local/bin/doctor && \
chmod +x /usr/local/bin/worker-certificates && \
chmod +x /usr/local/bin/worker-databases && \
chmod +x /usr/local/bin/worker-deletes && \
chmod +x /usr/local/bin/worker-executions && \
chmod +x /usr/local/bin/worker-functions && \
chmod +x /usr/local/bin/worker-mails && \
chmod +x /usr/local/bin/worker-messaging && \
+5
View File
@@ -1139,6 +1139,11 @@ return [
'description' => 'Key with the requested ID could not be found.',
'code' => 404,
],
Exception::KEY_ALREADY_EXISTS => [
'name' => Exception::KEY_ALREADY_EXISTS,
'description' => 'Key with the same ID already exists. Try again with a different ID.',
'code' => 409,
],
Exception::PLATFORM_NOT_FOUND => [
'name' => Exception::PLATFORM_NOT_FOUND,
'description' => 'Platform with the requested ID could not be found.',
+1
View File
@@ -22,6 +22,7 @@ return [
'hostnames' => array_filter(array_unique([
System::getEnv('_APP_DOMAIN', 'localhost'),
System::getEnv('_APP_CONSOLE_DOMAIN', 'localhost'),
System::getEnv('_APP_MIGRATION_HOST'),
])),
'platformName' => APP_EMAIL_PLATFORM_NAME,
'logoUrl' => APP_EMAIL_LOGO_URL,
+1 -1
View File
@@ -227,7 +227,7 @@ return [
[
'key' => 'cli',
'name' => 'Command Line',
'version' => '13.2.1',
'version' => '13.3.1',
'url' => 'https://github.com/appwrite/sdk-for-cli',
'package' => 'https://www.npmjs.com/package/appwrite-cli',
'enabled' => true,
+61 -61
View File
@@ -555,7 +555,7 @@
"x-appwrite": {
"method": "updateMFA",
"group": "mfa",
"weight": 253,
"weight": 250,
"cookies": false,
"type": "",
"demo": "account\/update-mfa.md",
@@ -627,7 +627,7 @@
"x-appwrite": {
"method": "createMfaAuthenticator",
"group": "mfa",
"weight": 255,
"weight": 252,
"cookies": false,
"type": "",
"demo": "account\/create-mfa-authenticator.md",
@@ -751,7 +751,7 @@
"x-appwrite": {
"method": "updateMfaAuthenticator",
"group": "mfa",
"weight": 256,
"weight": 253,
"cookies": false,
"type": "",
"demo": "account\/update-mfa-authenticator.md",
@@ -891,7 +891,7 @@
"x-appwrite": {
"method": "deleteMfaAuthenticator",
"group": "mfa",
"weight": 257,
"weight": 254,
"cookies": false,
"type": "",
"demo": "account\/delete-mfa-authenticator.md",
@@ -1015,7 +1015,7 @@
"x-appwrite": {
"method": "createMfaChallenge",
"group": "mfa",
"weight": 261,
"weight": 258,
"cookies": false,
"type": "",
"demo": "account\/create-mfa-challenge.md",
@@ -1149,7 +1149,7 @@
"x-appwrite": {
"method": "updateMfaChallenge",
"group": "mfa",
"weight": 262,
"weight": 259,
"cookies": false,
"type": "",
"demo": "account\/update-mfa-challenge.md",
@@ -1287,7 +1287,7 @@
"x-appwrite": {
"method": "listMfaFactors",
"group": "mfa",
"weight": 254,
"weight": 251,
"cookies": false,
"type": "",
"demo": "account\/list-mfa-factors.md",
@@ -1388,7 +1388,7 @@
"x-appwrite": {
"method": "getMfaRecoveryCodes",
"group": "mfa",
"weight": 260,
"weight": 257,
"cookies": false,
"type": "",
"demo": "account\/get-mfa-recovery-codes.md",
@@ -1487,7 +1487,7 @@
"x-appwrite": {
"method": "createMfaRecoveryCodes",
"group": "mfa",
"weight": 258,
"weight": 255,
"cookies": false,
"type": "",
"demo": "account\/create-mfa-recovery-codes.md",
@@ -1586,7 +1586,7 @@
"x-appwrite": {
"method": "updateMfaRecoveryCodes",
"group": "mfa",
"weight": 259,
"weight": 256,
"cookies": false,
"type": "",
"demo": "account\/update-mfa-recovery-codes.md",
@@ -4051,7 +4051,7 @@
"x-appwrite": {
"method": "getBrowser",
"group": null,
"weight": 264,
"weight": 261,
"cookies": false,
"type": "location",
"demo": "avatars\/get-browser.md",
@@ -4179,7 +4179,7 @@
"x-appwrite": {
"method": "getCreditCard",
"group": null,
"weight": 263,
"weight": 260,
"cookies": false,
"type": "location",
"demo": "avatars\/get-credit-card.md",
@@ -4313,7 +4313,7 @@
"x-appwrite": {
"method": "getFavicon",
"group": null,
"weight": 267,
"weight": 264,
"cookies": false,
"type": "location",
"demo": "avatars\/get-favicon.md",
@@ -4373,7 +4373,7 @@
"x-appwrite": {
"method": "getFlag",
"group": null,
"weight": 265,
"weight": 262,
"cookies": false,
"type": "location",
"demo": "avatars\/get-flag.md",
@@ -4863,7 +4863,7 @@
"x-appwrite": {
"method": "getImage",
"group": null,
"weight": 266,
"weight": 263,
"cookies": false,
"type": "location",
"demo": "avatars\/get-image.md",
@@ -4947,7 +4947,7 @@
"x-appwrite": {
"method": "getInitials",
"group": null,
"weight": 269,
"weight": 266,
"cookies": false,
"type": "location",
"demo": "avatars\/get-initials.md",
@@ -5041,7 +5041,7 @@
"x-appwrite": {
"method": "getQR",
"group": null,
"weight": 268,
"weight": 265,
"cookies": false,
"type": "location",
"demo": "avatars\/get-qr.md",
@@ -5135,7 +5135,7 @@
"x-appwrite": {
"method": "getScreenshot",
"group": null,
"weight": 270,
"weight": 267,
"cookies": false,
"type": "location",
"demo": "avatars\/get-screenshot.md",
@@ -5888,7 +5888,7 @@
"x-appwrite": {
"method": "listTransactions",
"group": "transactions",
"weight": 346,
"weight": 343,
"cookies": false,
"type": "",
"demo": "databases\/list-transactions.md",
@@ -5955,7 +5955,7 @@
"x-appwrite": {
"method": "createTransaction",
"group": "transactions",
"weight": 342,
"weight": 339,
"cookies": false,
"type": "",
"demo": "databases\/create-transaction.md",
@@ -6026,7 +6026,7 @@
"x-appwrite": {
"method": "getTransaction",
"group": "transactions",
"weight": 343,
"weight": 340,
"cookies": false,
"type": "",
"demo": "databases\/get-transaction.md",
@@ -6090,7 +6090,7 @@
"x-appwrite": {
"method": "updateTransaction",
"group": "transactions",
"weight": 344,
"weight": 341,
"cookies": false,
"type": "",
"demo": "databases\/update-transaction.md",
@@ -6168,7 +6168,7 @@
"x-appwrite": {
"method": "deleteTransaction",
"group": "transactions",
"weight": 345,
"weight": 342,
"cookies": false,
"type": "",
"demo": "databases\/delete-transaction.md",
@@ -6234,7 +6234,7 @@
"x-appwrite": {
"method": "createOperations",
"group": "transactions",
"weight": 347,
"weight": 344,
"cookies": false,
"type": "",
"demo": "databases\/create-operations.md",
@@ -6319,7 +6319,7 @@
"x-appwrite": {
"method": "listDocuments",
"group": "documents",
"weight": 297,
"weight": 294,
"cookies": false,
"type": "",
"demo": "databases\/list-documents.md",
@@ -6431,7 +6431,7 @@
"x-appwrite": {
"method": "createDocument",
"group": "documents",
"weight": 289,
"weight": 286,
"cookies": false,
"type": "",
"demo": "databases\/create-document.md",
@@ -6592,7 +6592,7 @@
"x-appwrite": {
"method": "getDocument",
"group": "documents",
"weight": 290,
"weight": 287,
"cookies": false,
"type": "",
"demo": "databases\/get-document.md",
@@ -6703,7 +6703,7 @@
"x-appwrite": {
"method": "upsertDocument",
"group": "documents",
"weight": 293,
"weight": 290,
"cookies": false,
"type": "",
"demo": "databases\/upsert-document.md",
@@ -6858,7 +6858,7 @@
"x-appwrite": {
"method": "updateDocument",
"group": "documents",
"weight": 291,
"weight": 288,
"cookies": false,
"type": "",
"demo": "databases\/update-document.md",
@@ -6970,7 +6970,7 @@
"x-appwrite": {
"method": "deleteDocument",
"group": "documents",
"weight": 295,
"weight": 292,
"cookies": false,
"type": "",
"demo": "databases\/delete-document.md",
@@ -7077,7 +7077,7 @@
"x-appwrite": {
"method": "decrementDocumentAttribute",
"group": "documents",
"weight": 300,
"weight": 297,
"cookies": false,
"type": "",
"demo": "databases\/decrement-document-attribute.md",
@@ -7206,7 +7206,7 @@
"x-appwrite": {
"method": "incrementDocumentAttribute",
"group": "documents",
"weight": 299,
"weight": 296,
"cookies": false,
"type": "",
"demo": "databases\/increment-document-attribute.md",
@@ -7335,7 +7335,7 @@
"x-appwrite": {
"method": "listExecutions",
"group": "executions",
"weight": 447,
"weight": 444,
"cookies": false,
"type": "",
"demo": "functions\/list-executions.md",
@@ -7422,7 +7422,7 @@
"x-appwrite": {
"method": "createExecution",
"group": "executions",
"weight": 445,
"weight": 442,
"cookies": false,
"type": "",
"demo": "functions\/create-execution.md",
@@ -7540,7 +7540,7 @@
"x-appwrite": {
"method": "getExecution",
"group": "executions",
"weight": 446,
"weight": 443,
"cookies": false,
"type": "",
"demo": "functions\/get-execution.md",
@@ -7615,7 +7615,7 @@
"x-appwrite": {
"method": "query",
"group": "graphql",
"weight": 190,
"weight": 187,
"cookies": false,
"type": "graphql",
"demo": "graphql\/query.md",
@@ -7669,7 +7669,7 @@
"x-appwrite": {
"method": "mutation",
"group": "graphql",
"weight": 189,
"weight": 186,
"cookies": false,
"type": "graphql",
"demo": "graphql\/mutation.md",
@@ -8155,7 +8155,7 @@
"x-appwrite": {
"method": "createSubscriber",
"group": "subscribers",
"weight": 237,
"weight": 234,
"cookies": false,
"type": "",
"demo": "messaging\/create-subscriber.md",
@@ -8239,7 +8239,7 @@
"x-appwrite": {
"method": "deleteSubscriber",
"group": "subscribers",
"weight": 241,
"weight": 238,
"cookies": false,
"type": "",
"demo": "messaging\/delete-subscriber.md",
@@ -8315,7 +8315,7 @@
"x-appwrite": {
"method": "listFiles",
"group": "files",
"weight": 543,
"weight": 540,
"cookies": false,
"type": "",
"demo": "storage\/list-files.md",
@@ -8414,7 +8414,7 @@
"x-appwrite": {
"method": "createFile",
"group": "files",
"weight": 541,
"weight": 538,
"cookies": false,
"type": "upload",
"demo": "storage\/create-file.md",
@@ -8516,7 +8516,7 @@
"x-appwrite": {
"method": "getFile",
"group": "files",
"weight": 542,
"weight": 539,
"cookies": false,
"type": "",
"demo": "storage\/get-file.md",
@@ -8590,7 +8590,7 @@
"x-appwrite": {
"method": "updateFile",
"group": "files",
"weight": 544,
"weight": 541,
"cookies": false,
"type": "",
"demo": "storage\/update-file.md",
@@ -8682,7 +8682,7 @@
"x-appwrite": {
"method": "deleteFile",
"group": "files",
"weight": 545,
"weight": 542,
"cookies": false,
"type": "",
"demo": "storage\/delete-file.md",
@@ -8751,7 +8751,7 @@
"x-appwrite": {
"method": "getFileDownload",
"group": "files",
"weight": 547,
"weight": 544,
"cookies": false,
"type": "location",
"demo": "storage\/get-file-download.md",
@@ -8831,7 +8831,7 @@
"x-appwrite": {
"method": "getFilePreview",
"group": "files",
"weight": 546,
"weight": 543,
"cookies": false,
"type": "location",
"demo": "storage\/get-file-preview.md",
@@ -9061,7 +9061,7 @@
"x-appwrite": {
"method": "getFileView",
"group": "files",
"weight": 548,
"weight": 545,
"cookies": false,
"type": "location",
"demo": "storage\/get-file-view.md",
@@ -9148,7 +9148,7 @@
"x-appwrite": {
"method": "listTransactions",
"group": "transactions",
"weight": 419,
"weight": 416,
"cookies": false,
"type": "",
"demo": "tablesdb\/list-transactions.md",
@@ -9218,7 +9218,7 @@
"x-appwrite": {
"method": "createTransaction",
"group": "transactions",
"weight": 415,
"weight": 412,
"cookies": false,
"type": "",
"demo": "tablesdb\/create-transaction.md",
@@ -9292,7 +9292,7 @@
"x-appwrite": {
"method": "getTransaction",
"group": "transactions",
"weight": 416,
"weight": 413,
"cookies": false,
"type": "",
"demo": "tablesdb\/get-transaction.md",
@@ -9359,7 +9359,7 @@
"x-appwrite": {
"method": "updateTransaction",
"group": "transactions",
"weight": 417,
"weight": 414,
"cookies": false,
"type": "",
"demo": "tablesdb\/update-transaction.md",
@@ -9440,7 +9440,7 @@
"x-appwrite": {
"method": "deleteTransaction",
"group": "transactions",
"weight": 418,
"weight": 415,
"cookies": false,
"type": "",
"demo": "tablesdb\/delete-transaction.md",
@@ -9509,7 +9509,7 @@
"x-appwrite": {
"method": "createOperations",
"group": "transactions",
"weight": 420,
"weight": 417,
"cookies": false,
"type": "",
"demo": "tablesdb\/create-operations.md",
@@ -9597,7 +9597,7 @@
"x-appwrite": {
"method": "listRows",
"group": "rows",
"weight": 411,
"weight": 408,
"cookies": false,
"type": "",
"demo": "tablesdb\/list-rows.md",
@@ -9708,7 +9708,7 @@
"x-appwrite": {
"method": "createRow",
"group": "rows",
"weight": 403,
"weight": 400,
"cookies": false,
"type": "",
"demo": "tablesdb\/create-row.md",
@@ -9864,7 +9864,7 @@
"x-appwrite": {
"method": "getRow",
"group": "rows",
"weight": 404,
"weight": 401,
"cookies": false,
"type": "",
"demo": "tablesdb\/get-row.md",
@@ -9974,7 +9974,7 @@
"x-appwrite": {
"method": "upsertRow",
"group": "rows",
"weight": 407,
"weight": 404,
"cookies": false,
"type": "",
"demo": "tablesdb\/upsert-row.md",
@@ -10124,7 +10124,7 @@
"x-appwrite": {
"method": "updateRow",
"group": "rows",
"weight": 405,
"weight": 402,
"cookies": false,
"type": "",
"demo": "tablesdb\/update-row.md",
@@ -10235,7 +10235,7 @@
"x-appwrite": {
"method": "deleteRow",
"group": "rows",
"weight": 409,
"weight": 406,
"cookies": false,
"type": "",
"demo": "tablesdb\/delete-row.md",
@@ -10341,7 +10341,7 @@
"x-appwrite": {
"method": "decrementRowColumn",
"group": "rows",
"weight": 414,
"weight": 411,
"cookies": false,
"type": "",
"demo": "tablesdb\/decrement-row-column.md",
@@ -10469,7 +10469,7 @@
"x-appwrite": {
"method": "incrementRowColumn",
"group": "rows",
"weight": 413,
"weight": 410,
"cookies": false,
"type": "",
"demo": "tablesdb\/increment-row-column.md",
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+61 -61
View File
@@ -555,7 +555,7 @@
"x-appwrite": {
"method": "updateMFA",
"group": "mfa",
"weight": 253,
"weight": 250,
"cookies": false,
"type": "",
"demo": "account\/update-mfa.md",
@@ -627,7 +627,7 @@
"x-appwrite": {
"method": "createMfaAuthenticator",
"group": "mfa",
"weight": 255,
"weight": 252,
"cookies": false,
"type": "",
"demo": "account\/create-mfa-authenticator.md",
@@ -751,7 +751,7 @@
"x-appwrite": {
"method": "updateMfaAuthenticator",
"group": "mfa",
"weight": 256,
"weight": 253,
"cookies": false,
"type": "",
"demo": "account\/update-mfa-authenticator.md",
@@ -891,7 +891,7 @@
"x-appwrite": {
"method": "deleteMfaAuthenticator",
"group": "mfa",
"weight": 257,
"weight": 254,
"cookies": false,
"type": "",
"demo": "account\/delete-mfa-authenticator.md",
@@ -1015,7 +1015,7 @@
"x-appwrite": {
"method": "createMfaChallenge",
"group": "mfa",
"weight": 261,
"weight": 258,
"cookies": false,
"type": "",
"demo": "account\/create-mfa-challenge.md",
@@ -1149,7 +1149,7 @@
"x-appwrite": {
"method": "updateMfaChallenge",
"group": "mfa",
"weight": 262,
"weight": 259,
"cookies": false,
"type": "",
"demo": "account\/update-mfa-challenge.md",
@@ -1287,7 +1287,7 @@
"x-appwrite": {
"method": "listMfaFactors",
"group": "mfa",
"weight": 254,
"weight": 251,
"cookies": false,
"type": "",
"demo": "account\/list-mfa-factors.md",
@@ -1388,7 +1388,7 @@
"x-appwrite": {
"method": "getMfaRecoveryCodes",
"group": "mfa",
"weight": 260,
"weight": 257,
"cookies": false,
"type": "",
"demo": "account\/get-mfa-recovery-codes.md",
@@ -1487,7 +1487,7 @@
"x-appwrite": {
"method": "createMfaRecoveryCodes",
"group": "mfa",
"weight": 258,
"weight": 255,
"cookies": false,
"type": "",
"demo": "account\/create-mfa-recovery-codes.md",
@@ -1586,7 +1586,7 @@
"x-appwrite": {
"method": "updateMfaRecoveryCodes",
"group": "mfa",
"weight": 259,
"weight": 256,
"cookies": false,
"type": "",
"demo": "account\/update-mfa-recovery-codes.md",
@@ -4051,7 +4051,7 @@
"x-appwrite": {
"method": "getBrowser",
"group": null,
"weight": 264,
"weight": 261,
"cookies": false,
"type": "location",
"demo": "avatars\/get-browser.md",
@@ -4179,7 +4179,7 @@
"x-appwrite": {
"method": "getCreditCard",
"group": null,
"weight": 263,
"weight": 260,
"cookies": false,
"type": "location",
"demo": "avatars\/get-credit-card.md",
@@ -4313,7 +4313,7 @@
"x-appwrite": {
"method": "getFavicon",
"group": null,
"weight": 267,
"weight": 264,
"cookies": false,
"type": "location",
"demo": "avatars\/get-favicon.md",
@@ -4373,7 +4373,7 @@
"x-appwrite": {
"method": "getFlag",
"group": null,
"weight": 265,
"weight": 262,
"cookies": false,
"type": "location",
"demo": "avatars\/get-flag.md",
@@ -4863,7 +4863,7 @@
"x-appwrite": {
"method": "getImage",
"group": null,
"weight": 266,
"weight": 263,
"cookies": false,
"type": "location",
"demo": "avatars\/get-image.md",
@@ -4947,7 +4947,7 @@
"x-appwrite": {
"method": "getInitials",
"group": null,
"weight": 269,
"weight": 266,
"cookies": false,
"type": "location",
"demo": "avatars\/get-initials.md",
@@ -5041,7 +5041,7 @@
"x-appwrite": {
"method": "getQR",
"group": null,
"weight": 268,
"weight": 265,
"cookies": false,
"type": "location",
"demo": "avatars\/get-qr.md",
@@ -5135,7 +5135,7 @@
"x-appwrite": {
"method": "getScreenshot",
"group": null,
"weight": 270,
"weight": 267,
"cookies": false,
"type": "location",
"demo": "avatars\/get-screenshot.md",
@@ -5888,7 +5888,7 @@
"x-appwrite": {
"method": "listTransactions",
"group": "transactions",
"weight": 346,
"weight": 343,
"cookies": false,
"type": "",
"demo": "databases\/list-transactions.md",
@@ -5955,7 +5955,7 @@
"x-appwrite": {
"method": "createTransaction",
"group": "transactions",
"weight": 342,
"weight": 339,
"cookies": false,
"type": "",
"demo": "databases\/create-transaction.md",
@@ -6026,7 +6026,7 @@
"x-appwrite": {
"method": "getTransaction",
"group": "transactions",
"weight": 343,
"weight": 340,
"cookies": false,
"type": "",
"demo": "databases\/get-transaction.md",
@@ -6090,7 +6090,7 @@
"x-appwrite": {
"method": "updateTransaction",
"group": "transactions",
"weight": 344,
"weight": 341,
"cookies": false,
"type": "",
"demo": "databases\/update-transaction.md",
@@ -6168,7 +6168,7 @@
"x-appwrite": {
"method": "deleteTransaction",
"group": "transactions",
"weight": 345,
"weight": 342,
"cookies": false,
"type": "",
"demo": "databases\/delete-transaction.md",
@@ -6234,7 +6234,7 @@
"x-appwrite": {
"method": "createOperations",
"group": "transactions",
"weight": 347,
"weight": 344,
"cookies": false,
"type": "",
"demo": "databases\/create-operations.md",
@@ -6319,7 +6319,7 @@
"x-appwrite": {
"method": "listDocuments",
"group": "documents",
"weight": 297,
"weight": 294,
"cookies": false,
"type": "",
"demo": "databases\/list-documents.md",
@@ -6431,7 +6431,7 @@
"x-appwrite": {
"method": "createDocument",
"group": "documents",
"weight": 289,
"weight": 286,
"cookies": false,
"type": "",
"demo": "databases\/create-document.md",
@@ -6592,7 +6592,7 @@
"x-appwrite": {
"method": "getDocument",
"group": "documents",
"weight": 290,
"weight": 287,
"cookies": false,
"type": "",
"demo": "databases\/get-document.md",
@@ -6703,7 +6703,7 @@
"x-appwrite": {
"method": "upsertDocument",
"group": "documents",
"weight": 293,
"weight": 290,
"cookies": false,
"type": "",
"demo": "databases\/upsert-document.md",
@@ -6858,7 +6858,7 @@
"x-appwrite": {
"method": "updateDocument",
"group": "documents",
"weight": 291,
"weight": 288,
"cookies": false,
"type": "",
"demo": "databases\/update-document.md",
@@ -6970,7 +6970,7 @@
"x-appwrite": {
"method": "deleteDocument",
"group": "documents",
"weight": 295,
"weight": 292,
"cookies": false,
"type": "",
"demo": "databases\/delete-document.md",
@@ -7077,7 +7077,7 @@
"x-appwrite": {
"method": "decrementDocumentAttribute",
"group": "documents",
"weight": 300,
"weight": 297,
"cookies": false,
"type": "",
"demo": "databases\/decrement-document-attribute.md",
@@ -7206,7 +7206,7 @@
"x-appwrite": {
"method": "incrementDocumentAttribute",
"group": "documents",
"weight": 299,
"weight": 296,
"cookies": false,
"type": "",
"demo": "databases\/increment-document-attribute.md",
@@ -7335,7 +7335,7 @@
"x-appwrite": {
"method": "listExecutions",
"group": "executions",
"weight": 447,
"weight": 444,
"cookies": false,
"type": "",
"demo": "functions\/list-executions.md",
@@ -7422,7 +7422,7 @@
"x-appwrite": {
"method": "createExecution",
"group": "executions",
"weight": 445,
"weight": 442,
"cookies": false,
"type": "",
"demo": "functions\/create-execution.md",
@@ -7540,7 +7540,7 @@
"x-appwrite": {
"method": "getExecution",
"group": "executions",
"weight": 446,
"weight": 443,
"cookies": false,
"type": "",
"demo": "functions\/get-execution.md",
@@ -7615,7 +7615,7 @@
"x-appwrite": {
"method": "query",
"group": "graphql",
"weight": 190,
"weight": 187,
"cookies": false,
"type": "graphql",
"demo": "graphql\/query.md",
@@ -7669,7 +7669,7 @@
"x-appwrite": {
"method": "mutation",
"group": "graphql",
"weight": 189,
"weight": 186,
"cookies": false,
"type": "graphql",
"demo": "graphql\/mutation.md",
@@ -8155,7 +8155,7 @@
"x-appwrite": {
"method": "createSubscriber",
"group": "subscribers",
"weight": 237,
"weight": 234,
"cookies": false,
"type": "",
"demo": "messaging\/create-subscriber.md",
@@ -8239,7 +8239,7 @@
"x-appwrite": {
"method": "deleteSubscriber",
"group": "subscribers",
"weight": 241,
"weight": 238,
"cookies": false,
"type": "",
"demo": "messaging\/delete-subscriber.md",
@@ -8315,7 +8315,7 @@
"x-appwrite": {
"method": "listFiles",
"group": "files",
"weight": 543,
"weight": 540,
"cookies": false,
"type": "",
"demo": "storage\/list-files.md",
@@ -8414,7 +8414,7 @@
"x-appwrite": {
"method": "createFile",
"group": "files",
"weight": 541,
"weight": 538,
"cookies": false,
"type": "upload",
"demo": "storage\/create-file.md",
@@ -8516,7 +8516,7 @@
"x-appwrite": {
"method": "getFile",
"group": "files",
"weight": 542,
"weight": 539,
"cookies": false,
"type": "",
"demo": "storage\/get-file.md",
@@ -8590,7 +8590,7 @@
"x-appwrite": {
"method": "updateFile",
"group": "files",
"weight": 544,
"weight": 541,
"cookies": false,
"type": "",
"demo": "storage\/update-file.md",
@@ -8682,7 +8682,7 @@
"x-appwrite": {
"method": "deleteFile",
"group": "files",
"weight": 545,
"weight": 542,
"cookies": false,
"type": "",
"demo": "storage\/delete-file.md",
@@ -8751,7 +8751,7 @@
"x-appwrite": {
"method": "getFileDownload",
"group": "files",
"weight": 547,
"weight": 544,
"cookies": false,
"type": "location",
"demo": "storage\/get-file-download.md",
@@ -8831,7 +8831,7 @@
"x-appwrite": {
"method": "getFilePreview",
"group": "files",
"weight": 546,
"weight": 543,
"cookies": false,
"type": "location",
"demo": "storage\/get-file-preview.md",
@@ -9061,7 +9061,7 @@
"x-appwrite": {
"method": "getFileView",
"group": "files",
"weight": 548,
"weight": 545,
"cookies": false,
"type": "location",
"demo": "storage\/get-file-view.md",
@@ -9148,7 +9148,7 @@
"x-appwrite": {
"method": "listTransactions",
"group": "transactions",
"weight": 419,
"weight": 416,
"cookies": false,
"type": "",
"demo": "tablesdb\/list-transactions.md",
@@ -9218,7 +9218,7 @@
"x-appwrite": {
"method": "createTransaction",
"group": "transactions",
"weight": 415,
"weight": 412,
"cookies": false,
"type": "",
"demo": "tablesdb\/create-transaction.md",
@@ -9292,7 +9292,7 @@
"x-appwrite": {
"method": "getTransaction",
"group": "transactions",
"weight": 416,
"weight": 413,
"cookies": false,
"type": "",
"demo": "tablesdb\/get-transaction.md",
@@ -9359,7 +9359,7 @@
"x-appwrite": {
"method": "updateTransaction",
"group": "transactions",
"weight": 417,
"weight": 414,
"cookies": false,
"type": "",
"demo": "tablesdb\/update-transaction.md",
@@ -9440,7 +9440,7 @@
"x-appwrite": {
"method": "deleteTransaction",
"group": "transactions",
"weight": 418,
"weight": 415,
"cookies": false,
"type": "",
"demo": "tablesdb\/delete-transaction.md",
@@ -9509,7 +9509,7 @@
"x-appwrite": {
"method": "createOperations",
"group": "transactions",
"weight": 420,
"weight": 417,
"cookies": false,
"type": "",
"demo": "tablesdb\/create-operations.md",
@@ -9597,7 +9597,7 @@
"x-appwrite": {
"method": "listRows",
"group": "rows",
"weight": 411,
"weight": 408,
"cookies": false,
"type": "",
"demo": "tablesdb\/list-rows.md",
@@ -9708,7 +9708,7 @@
"x-appwrite": {
"method": "createRow",
"group": "rows",
"weight": 403,
"weight": 400,
"cookies": false,
"type": "",
"demo": "tablesdb\/create-row.md",
@@ -9864,7 +9864,7 @@
"x-appwrite": {
"method": "getRow",
"group": "rows",
"weight": 404,
"weight": 401,
"cookies": false,
"type": "",
"demo": "tablesdb\/get-row.md",
@@ -9974,7 +9974,7 @@
"x-appwrite": {
"method": "upsertRow",
"group": "rows",
"weight": 407,
"weight": 404,
"cookies": false,
"type": "",
"demo": "tablesdb\/upsert-row.md",
@@ -10124,7 +10124,7 @@
"x-appwrite": {
"method": "updateRow",
"group": "rows",
"weight": 405,
"weight": 402,
"cookies": false,
"type": "",
"demo": "tablesdb\/update-row.md",
@@ -10235,7 +10235,7 @@
"x-appwrite": {
"method": "deleteRow",
"group": "rows",
"weight": 409,
"weight": 406,
"cookies": false,
"type": "",
"demo": "tablesdb\/delete-row.md",
@@ -10341,7 +10341,7 @@
"x-appwrite": {
"method": "decrementRowColumn",
"group": "rows",
"weight": 414,
"weight": 411,
"cookies": false,
"type": "",
"demo": "tablesdb\/decrement-row-column.md",
@@ -10469,7 +10469,7 @@
"x-appwrite": {
"method": "incrementRowColumn",
"group": "rows",
"weight": 413,
"weight": 410,
"cookies": false,
"type": "",
"demo": "tablesdb\/increment-row-column.md",
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+61 -61
View File
@@ -612,7 +612,7 @@
"x-appwrite": {
"method": "updateMFA",
"group": "mfa",
"weight": 253,
"weight": 250,
"cookies": false,
"type": "",
"demo": "account\/update-mfa.md",
@@ -687,7 +687,7 @@
"x-appwrite": {
"method": "createMfaAuthenticator",
"group": "mfa",
"weight": 255,
"weight": 252,
"cookies": false,
"type": "",
"demo": "account\/create-mfa-authenticator.md",
@@ -811,7 +811,7 @@
"x-appwrite": {
"method": "updateMfaAuthenticator",
"group": "mfa",
"weight": 256,
"weight": 253,
"cookies": false,
"type": "",
"demo": "account\/update-mfa-authenticator.md",
@@ -952,7 +952,7 @@
"x-appwrite": {
"method": "deleteMfaAuthenticator",
"group": "mfa",
"weight": 257,
"weight": 254,
"cookies": false,
"type": "",
"demo": "account\/delete-mfa-authenticator.md",
@@ -1076,7 +1076,7 @@
"x-appwrite": {
"method": "createMfaChallenge",
"group": "mfa",
"weight": 261,
"weight": 258,
"cookies": false,
"type": "",
"demo": "account\/create-mfa-challenge.md",
@@ -1213,7 +1213,7 @@
"x-appwrite": {
"method": "updateMfaChallenge",
"group": "mfa",
"weight": 262,
"weight": 259,
"cookies": false,
"type": "",
"demo": "account\/update-mfa-challenge.md",
@@ -1353,7 +1353,7 @@
"x-appwrite": {
"method": "listMfaFactors",
"group": "mfa",
"weight": 254,
"weight": 251,
"cookies": false,
"type": "",
"demo": "account\/list-mfa-factors.md",
@@ -1454,7 +1454,7 @@
"x-appwrite": {
"method": "getMfaRecoveryCodes",
"group": "mfa",
"weight": 260,
"weight": 257,
"cookies": false,
"type": "",
"demo": "account\/get-mfa-recovery-codes.md",
@@ -1555,7 +1555,7 @@
"x-appwrite": {
"method": "createMfaRecoveryCodes",
"group": "mfa",
"weight": 258,
"weight": 255,
"cookies": false,
"type": "",
"demo": "account\/create-mfa-recovery-codes.md",
@@ -1656,7 +1656,7 @@
"x-appwrite": {
"method": "updateMfaRecoveryCodes",
"group": "mfa",
"weight": 259,
"weight": 256,
"cookies": false,
"type": "",
"demo": "account\/update-mfa-recovery-codes.md",
@@ -4203,7 +4203,7 @@
"x-appwrite": {
"method": "getBrowser",
"group": null,
"weight": 264,
"weight": 261,
"cookies": false,
"type": "location",
"demo": "avatars\/get-browser.md",
@@ -4329,7 +4329,7 @@
"x-appwrite": {
"method": "getCreditCard",
"group": null,
"weight": 263,
"weight": 260,
"cookies": false,
"type": "location",
"demo": "avatars\/get-credit-card.md",
@@ -4461,7 +4461,7 @@
"x-appwrite": {
"method": "getFavicon",
"group": null,
"weight": 267,
"weight": 264,
"cookies": false,
"type": "location",
"demo": "avatars\/get-favicon.md",
@@ -4525,7 +4525,7 @@
"x-appwrite": {
"method": "getFlag",
"group": null,
"weight": 265,
"weight": 262,
"cookies": false,
"type": "location",
"demo": "avatars\/get-flag.md",
@@ -5013,7 +5013,7 @@
"x-appwrite": {
"method": "getImage",
"group": null,
"weight": 266,
"weight": 263,
"cookies": false,
"type": "location",
"demo": "avatars\/get-image.md",
@@ -5097,7 +5097,7 @@
"x-appwrite": {
"method": "getInitials",
"group": null,
"weight": 269,
"weight": 266,
"cookies": false,
"type": "location",
"demo": "avatars\/get-initials.md",
@@ -5189,7 +5189,7 @@
"x-appwrite": {
"method": "getQR",
"group": null,
"weight": 268,
"weight": 265,
"cookies": false,
"type": "location",
"demo": "avatars\/get-qr.md",
@@ -5281,7 +5281,7 @@
"x-appwrite": {
"method": "getScreenshot",
"group": null,
"weight": 270,
"weight": 267,
"cookies": false,
"type": "location",
"demo": "avatars\/get-screenshot.md",
@@ -5994,7 +5994,7 @@
"x-appwrite": {
"method": "listTransactions",
"group": "transactions",
"weight": 346,
"weight": 343,
"cookies": false,
"type": "",
"demo": "databases\/list-transactions.md",
@@ -6061,7 +6061,7 @@
"x-appwrite": {
"method": "createTransaction",
"group": "transactions",
"weight": 342,
"weight": 339,
"cookies": false,
"type": "",
"demo": "databases\/create-transaction.md",
@@ -6132,7 +6132,7 @@
"x-appwrite": {
"method": "getTransaction",
"group": "transactions",
"weight": 343,
"weight": 340,
"cookies": false,
"type": "",
"demo": "databases\/get-transaction.md",
@@ -6195,7 +6195,7 @@
"x-appwrite": {
"method": "updateTransaction",
"group": "transactions",
"weight": 344,
"weight": 341,
"cookies": false,
"type": "",
"demo": "databases\/update-transaction.md",
@@ -6274,7 +6274,7 @@
"x-appwrite": {
"method": "deleteTransaction",
"group": "transactions",
"weight": 345,
"weight": 342,
"cookies": false,
"type": "",
"demo": "databases\/delete-transaction.md",
@@ -6339,7 +6339,7 @@
"x-appwrite": {
"method": "createOperations",
"group": "transactions",
"weight": 347,
"weight": 344,
"cookies": false,
"type": "",
"demo": "databases\/create-operations.md",
@@ -6420,7 +6420,7 @@
"x-appwrite": {
"method": "listDocuments",
"group": "documents",
"weight": 297,
"weight": 294,
"cookies": false,
"type": "",
"demo": "databases\/list-documents.md",
@@ -6524,7 +6524,7 @@
"x-appwrite": {
"method": "createDocument",
"group": "documents",
"weight": 289,
"weight": 286,
"cookies": false,
"type": "",
"demo": "databases\/create-document.md",
@@ -6683,7 +6683,7 @@
"x-appwrite": {
"method": "getDocument",
"group": "documents",
"weight": 290,
"weight": 287,
"cookies": false,
"type": "",
"demo": "databases\/get-document.md",
@@ -6786,7 +6786,7 @@
"x-appwrite": {
"method": "upsertDocument",
"group": "documents",
"weight": 293,
"weight": 290,
"cookies": false,
"type": "",
"demo": "databases\/upsert-document.md",
@@ -6937,7 +6937,7 @@
"x-appwrite": {
"method": "updateDocument",
"group": "documents",
"weight": 291,
"weight": 288,
"cookies": false,
"type": "",
"demo": "databases\/update-document.md",
@@ -7047,7 +7047,7 @@
"x-appwrite": {
"method": "deleteDocument",
"group": "documents",
"weight": 295,
"weight": 292,
"cookies": false,
"type": "",
"demo": "databases\/delete-document.md",
@@ -7148,7 +7148,7 @@
"x-appwrite": {
"method": "decrementDocumentAttribute",
"group": "documents",
"weight": 300,
"weight": 297,
"cookies": false,
"type": "",
"demo": "databases\/decrement-document-attribute.md",
@@ -7271,7 +7271,7 @@
"x-appwrite": {
"method": "incrementDocumentAttribute",
"group": "documents",
"weight": 299,
"weight": 296,
"cookies": false,
"type": "",
"demo": "databases\/increment-document-attribute.md",
@@ -7392,7 +7392,7 @@
"x-appwrite": {
"method": "listExecutions",
"group": "executions",
"weight": 447,
"weight": 444,
"cookies": false,
"type": "",
"demo": "functions\/list-executions.md",
@@ -7475,7 +7475,7 @@
"x-appwrite": {
"method": "createExecution",
"group": "executions",
"weight": 445,
"weight": 442,
"cookies": false,
"type": "",
"demo": "functions\/create-execution.md",
@@ -7594,7 +7594,7 @@
"x-appwrite": {
"method": "getExecution",
"group": "executions",
"weight": 446,
"weight": 443,
"cookies": false,
"type": "",
"demo": "functions\/get-execution.md",
@@ -7666,7 +7666,7 @@
"x-appwrite": {
"method": "query",
"group": "graphql",
"weight": 190,
"weight": 187,
"cookies": false,
"type": "graphql",
"demo": "graphql\/query.md",
@@ -7741,7 +7741,7 @@
"x-appwrite": {
"method": "mutation",
"group": "graphql",
"weight": 189,
"weight": 186,
"cookies": false,
"type": "graphql",
"demo": "graphql\/mutation.md",
@@ -8240,7 +8240,7 @@
"x-appwrite": {
"method": "createSubscriber",
"group": "subscribers",
"weight": 237,
"weight": 234,
"cookies": false,
"type": "",
"demo": "messaging\/create-subscriber.md",
@@ -8325,7 +8325,7 @@
"x-appwrite": {
"method": "deleteSubscriber",
"group": "subscribers",
"weight": 241,
"weight": 238,
"cookies": false,
"type": "",
"demo": "messaging\/delete-subscriber.md",
@@ -8396,7 +8396,7 @@
"x-appwrite": {
"method": "listFiles",
"group": "files",
"weight": 543,
"weight": 540,
"cookies": false,
"type": "",
"demo": "storage\/list-files.md",
@@ -8489,7 +8489,7 @@
"x-appwrite": {
"method": "createFile",
"group": "files",
"weight": 541,
"weight": 538,
"cookies": false,
"type": "upload",
"demo": "storage\/create-file.md",
@@ -8580,7 +8580,7 @@
"x-appwrite": {
"method": "getFile",
"group": "files",
"weight": 542,
"weight": 539,
"cookies": false,
"type": "",
"demo": "storage\/get-file.md",
@@ -8651,7 +8651,7 @@
"x-appwrite": {
"method": "updateFile",
"group": "files",
"weight": 544,
"weight": 541,
"cookies": false,
"type": "",
"demo": "storage\/update-file.md",
@@ -8742,7 +8742,7 @@
"x-appwrite": {
"method": "deleteFile",
"group": "files",
"weight": 545,
"weight": 542,
"cookies": false,
"type": "",
"demo": "storage\/delete-file.md",
@@ -8813,7 +8813,7 @@
"x-appwrite": {
"method": "getFileDownload",
"group": "files",
"weight": 547,
"weight": 544,
"cookies": false,
"type": "location",
"demo": "storage\/get-file-download.md",
@@ -8893,7 +8893,7 @@
"x-appwrite": {
"method": "getFilePreview",
"group": "files",
"weight": 546,
"weight": 543,
"cookies": false,
"type": "location",
"demo": "storage\/get-file-preview.md",
@@ -9101,7 +9101,7 @@
"x-appwrite": {
"method": "getFileView",
"group": "files",
"weight": 548,
"weight": 545,
"cookies": false,
"type": "location",
"demo": "storage\/get-file-view.md",
@@ -9181,7 +9181,7 @@
"x-appwrite": {
"method": "listTransactions",
"group": "transactions",
"weight": 419,
"weight": 416,
"cookies": false,
"type": "",
"demo": "tablesdb\/list-transactions.md",
@@ -9251,7 +9251,7 @@
"x-appwrite": {
"method": "createTransaction",
"group": "transactions",
"weight": 415,
"weight": 412,
"cookies": false,
"type": "",
"demo": "tablesdb\/create-transaction.md",
@@ -9325,7 +9325,7 @@
"x-appwrite": {
"method": "getTransaction",
"group": "transactions",
"weight": 416,
"weight": 413,
"cookies": false,
"type": "",
"demo": "tablesdb\/get-transaction.md",
@@ -9391,7 +9391,7 @@
"x-appwrite": {
"method": "updateTransaction",
"group": "transactions",
"weight": 417,
"weight": 414,
"cookies": false,
"type": "",
"demo": "tablesdb\/update-transaction.md",
@@ -9473,7 +9473,7 @@
"x-appwrite": {
"method": "deleteTransaction",
"group": "transactions",
"weight": 418,
"weight": 415,
"cookies": false,
"type": "",
"demo": "tablesdb\/delete-transaction.md",
@@ -9541,7 +9541,7 @@
"x-appwrite": {
"method": "createOperations",
"group": "transactions",
"weight": 420,
"weight": 417,
"cookies": false,
"type": "",
"demo": "tablesdb\/create-operations.md",
@@ -9625,7 +9625,7 @@
"x-appwrite": {
"method": "listRows",
"group": "rows",
"weight": 411,
"weight": 408,
"cookies": false,
"type": "",
"demo": "tablesdb\/list-rows.md",
@@ -9728,7 +9728,7 @@
"x-appwrite": {
"method": "createRow",
"group": "rows",
"weight": 403,
"weight": 400,
"cookies": false,
"type": "",
"demo": "tablesdb\/create-row.md",
@@ -9882,7 +9882,7 @@
"x-appwrite": {
"method": "getRow",
"group": "rows",
"weight": 404,
"weight": 401,
"cookies": false,
"type": "",
"demo": "tablesdb\/get-row.md",
@@ -9984,7 +9984,7 @@
"x-appwrite": {
"method": "upsertRow",
"group": "rows",
"weight": 407,
"weight": 404,
"cookies": false,
"type": "",
"demo": "tablesdb\/upsert-row.md",
@@ -10130,7 +10130,7 @@
"x-appwrite": {
"method": "updateRow",
"group": "rows",
"weight": 405,
"weight": 402,
"cookies": false,
"type": "",
"demo": "tablesdb\/update-row.md",
@@ -10239,7 +10239,7 @@
"x-appwrite": {
"method": "deleteRow",
"group": "rows",
"weight": 409,
"weight": 406,
"cookies": false,
"type": "",
"demo": "tablesdb\/delete-row.md",
@@ -10339,7 +10339,7 @@
"x-appwrite": {
"method": "decrementRowColumn",
"group": "rows",
"weight": 414,
"weight": 411,
"cookies": false,
"type": "",
"demo": "tablesdb\/decrement-row-column.md",
@@ -10461,7 +10461,7 @@
"x-appwrite": {
"method": "incrementRowColumn",
"group": "rows",
"weight": 413,
"weight": 410,
"cookies": false,
"type": "",
"demo": "tablesdb\/increment-row-column.md",
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+61 -61
View File
@@ -612,7 +612,7 @@
"x-appwrite": {
"method": "updateMFA",
"group": "mfa",
"weight": 253,
"weight": 250,
"cookies": false,
"type": "",
"demo": "account\/update-mfa.md",
@@ -687,7 +687,7 @@
"x-appwrite": {
"method": "createMfaAuthenticator",
"group": "mfa",
"weight": 255,
"weight": 252,
"cookies": false,
"type": "",
"demo": "account\/create-mfa-authenticator.md",
@@ -811,7 +811,7 @@
"x-appwrite": {
"method": "updateMfaAuthenticator",
"group": "mfa",
"weight": 256,
"weight": 253,
"cookies": false,
"type": "",
"demo": "account\/update-mfa-authenticator.md",
@@ -952,7 +952,7 @@
"x-appwrite": {
"method": "deleteMfaAuthenticator",
"group": "mfa",
"weight": 257,
"weight": 254,
"cookies": false,
"type": "",
"demo": "account\/delete-mfa-authenticator.md",
@@ -1076,7 +1076,7 @@
"x-appwrite": {
"method": "createMfaChallenge",
"group": "mfa",
"weight": 261,
"weight": 258,
"cookies": false,
"type": "",
"demo": "account\/create-mfa-challenge.md",
@@ -1213,7 +1213,7 @@
"x-appwrite": {
"method": "updateMfaChallenge",
"group": "mfa",
"weight": 262,
"weight": 259,
"cookies": false,
"type": "",
"demo": "account\/update-mfa-challenge.md",
@@ -1353,7 +1353,7 @@
"x-appwrite": {
"method": "listMfaFactors",
"group": "mfa",
"weight": 254,
"weight": 251,
"cookies": false,
"type": "",
"demo": "account\/list-mfa-factors.md",
@@ -1454,7 +1454,7 @@
"x-appwrite": {
"method": "getMfaRecoveryCodes",
"group": "mfa",
"weight": 260,
"weight": 257,
"cookies": false,
"type": "",
"demo": "account\/get-mfa-recovery-codes.md",
@@ -1555,7 +1555,7 @@
"x-appwrite": {
"method": "createMfaRecoveryCodes",
"group": "mfa",
"weight": 258,
"weight": 255,
"cookies": false,
"type": "",
"demo": "account\/create-mfa-recovery-codes.md",
@@ -1656,7 +1656,7 @@
"x-appwrite": {
"method": "updateMfaRecoveryCodes",
"group": "mfa",
"weight": 259,
"weight": 256,
"cookies": false,
"type": "",
"demo": "account\/update-mfa-recovery-codes.md",
@@ -4203,7 +4203,7 @@
"x-appwrite": {
"method": "getBrowser",
"group": null,
"weight": 264,
"weight": 261,
"cookies": false,
"type": "location",
"demo": "avatars\/get-browser.md",
@@ -4329,7 +4329,7 @@
"x-appwrite": {
"method": "getCreditCard",
"group": null,
"weight": 263,
"weight": 260,
"cookies": false,
"type": "location",
"demo": "avatars\/get-credit-card.md",
@@ -4461,7 +4461,7 @@
"x-appwrite": {
"method": "getFavicon",
"group": null,
"weight": 267,
"weight": 264,
"cookies": false,
"type": "location",
"demo": "avatars\/get-favicon.md",
@@ -4525,7 +4525,7 @@
"x-appwrite": {
"method": "getFlag",
"group": null,
"weight": 265,
"weight": 262,
"cookies": false,
"type": "location",
"demo": "avatars\/get-flag.md",
@@ -5013,7 +5013,7 @@
"x-appwrite": {
"method": "getImage",
"group": null,
"weight": 266,
"weight": 263,
"cookies": false,
"type": "location",
"demo": "avatars\/get-image.md",
@@ -5097,7 +5097,7 @@
"x-appwrite": {
"method": "getInitials",
"group": null,
"weight": 269,
"weight": 266,
"cookies": false,
"type": "location",
"demo": "avatars\/get-initials.md",
@@ -5189,7 +5189,7 @@
"x-appwrite": {
"method": "getQR",
"group": null,
"weight": 268,
"weight": 265,
"cookies": false,
"type": "location",
"demo": "avatars\/get-qr.md",
@@ -5281,7 +5281,7 @@
"x-appwrite": {
"method": "getScreenshot",
"group": null,
"weight": 270,
"weight": 267,
"cookies": false,
"type": "location",
"demo": "avatars\/get-screenshot.md",
@@ -5994,7 +5994,7 @@
"x-appwrite": {
"method": "listTransactions",
"group": "transactions",
"weight": 346,
"weight": 343,
"cookies": false,
"type": "",
"demo": "databases\/list-transactions.md",
@@ -6061,7 +6061,7 @@
"x-appwrite": {
"method": "createTransaction",
"group": "transactions",
"weight": 342,
"weight": 339,
"cookies": false,
"type": "",
"demo": "databases\/create-transaction.md",
@@ -6132,7 +6132,7 @@
"x-appwrite": {
"method": "getTransaction",
"group": "transactions",
"weight": 343,
"weight": 340,
"cookies": false,
"type": "",
"demo": "databases\/get-transaction.md",
@@ -6195,7 +6195,7 @@
"x-appwrite": {
"method": "updateTransaction",
"group": "transactions",
"weight": 344,
"weight": 341,
"cookies": false,
"type": "",
"demo": "databases\/update-transaction.md",
@@ -6274,7 +6274,7 @@
"x-appwrite": {
"method": "deleteTransaction",
"group": "transactions",
"weight": 345,
"weight": 342,
"cookies": false,
"type": "",
"demo": "databases\/delete-transaction.md",
@@ -6339,7 +6339,7 @@
"x-appwrite": {
"method": "createOperations",
"group": "transactions",
"weight": 347,
"weight": 344,
"cookies": false,
"type": "",
"demo": "databases\/create-operations.md",
@@ -6420,7 +6420,7 @@
"x-appwrite": {
"method": "listDocuments",
"group": "documents",
"weight": 297,
"weight": 294,
"cookies": false,
"type": "",
"demo": "databases\/list-documents.md",
@@ -6524,7 +6524,7 @@
"x-appwrite": {
"method": "createDocument",
"group": "documents",
"weight": 289,
"weight": 286,
"cookies": false,
"type": "",
"demo": "databases\/create-document.md",
@@ -6683,7 +6683,7 @@
"x-appwrite": {
"method": "getDocument",
"group": "documents",
"weight": 290,
"weight": 287,
"cookies": false,
"type": "",
"demo": "databases\/get-document.md",
@@ -6786,7 +6786,7 @@
"x-appwrite": {
"method": "upsertDocument",
"group": "documents",
"weight": 293,
"weight": 290,
"cookies": false,
"type": "",
"demo": "databases\/upsert-document.md",
@@ -6937,7 +6937,7 @@
"x-appwrite": {
"method": "updateDocument",
"group": "documents",
"weight": 291,
"weight": 288,
"cookies": false,
"type": "",
"demo": "databases\/update-document.md",
@@ -7047,7 +7047,7 @@
"x-appwrite": {
"method": "deleteDocument",
"group": "documents",
"weight": 295,
"weight": 292,
"cookies": false,
"type": "",
"demo": "databases\/delete-document.md",
@@ -7148,7 +7148,7 @@
"x-appwrite": {
"method": "decrementDocumentAttribute",
"group": "documents",
"weight": 300,
"weight": 297,
"cookies": false,
"type": "",
"demo": "databases\/decrement-document-attribute.md",
@@ -7271,7 +7271,7 @@
"x-appwrite": {
"method": "incrementDocumentAttribute",
"group": "documents",
"weight": 299,
"weight": 296,
"cookies": false,
"type": "",
"demo": "databases\/increment-document-attribute.md",
@@ -7392,7 +7392,7 @@
"x-appwrite": {
"method": "listExecutions",
"group": "executions",
"weight": 447,
"weight": 444,
"cookies": false,
"type": "",
"demo": "functions\/list-executions.md",
@@ -7475,7 +7475,7 @@
"x-appwrite": {
"method": "createExecution",
"group": "executions",
"weight": 445,
"weight": 442,
"cookies": false,
"type": "",
"demo": "functions\/create-execution.md",
@@ -7594,7 +7594,7 @@
"x-appwrite": {
"method": "getExecution",
"group": "executions",
"weight": 446,
"weight": 443,
"cookies": false,
"type": "",
"demo": "functions\/get-execution.md",
@@ -7666,7 +7666,7 @@
"x-appwrite": {
"method": "query",
"group": "graphql",
"weight": 190,
"weight": 187,
"cookies": false,
"type": "graphql",
"demo": "graphql\/query.md",
@@ -7741,7 +7741,7 @@
"x-appwrite": {
"method": "mutation",
"group": "graphql",
"weight": 189,
"weight": 186,
"cookies": false,
"type": "graphql",
"demo": "graphql\/mutation.md",
@@ -8240,7 +8240,7 @@
"x-appwrite": {
"method": "createSubscriber",
"group": "subscribers",
"weight": 237,
"weight": 234,
"cookies": false,
"type": "",
"demo": "messaging\/create-subscriber.md",
@@ -8325,7 +8325,7 @@
"x-appwrite": {
"method": "deleteSubscriber",
"group": "subscribers",
"weight": 241,
"weight": 238,
"cookies": false,
"type": "",
"demo": "messaging\/delete-subscriber.md",
@@ -8396,7 +8396,7 @@
"x-appwrite": {
"method": "listFiles",
"group": "files",
"weight": 543,
"weight": 540,
"cookies": false,
"type": "",
"demo": "storage\/list-files.md",
@@ -8489,7 +8489,7 @@
"x-appwrite": {
"method": "createFile",
"group": "files",
"weight": 541,
"weight": 538,
"cookies": false,
"type": "upload",
"demo": "storage\/create-file.md",
@@ -8580,7 +8580,7 @@
"x-appwrite": {
"method": "getFile",
"group": "files",
"weight": 542,
"weight": 539,
"cookies": false,
"type": "",
"demo": "storage\/get-file.md",
@@ -8651,7 +8651,7 @@
"x-appwrite": {
"method": "updateFile",
"group": "files",
"weight": 544,
"weight": 541,
"cookies": false,
"type": "",
"demo": "storage\/update-file.md",
@@ -8742,7 +8742,7 @@
"x-appwrite": {
"method": "deleteFile",
"group": "files",
"weight": 545,
"weight": 542,
"cookies": false,
"type": "",
"demo": "storage\/delete-file.md",
@@ -8813,7 +8813,7 @@
"x-appwrite": {
"method": "getFileDownload",
"group": "files",
"weight": 547,
"weight": 544,
"cookies": false,
"type": "location",
"demo": "storage\/get-file-download.md",
@@ -8893,7 +8893,7 @@
"x-appwrite": {
"method": "getFilePreview",
"group": "files",
"weight": 546,
"weight": 543,
"cookies": false,
"type": "location",
"demo": "storage\/get-file-preview.md",
@@ -9101,7 +9101,7 @@
"x-appwrite": {
"method": "getFileView",
"group": "files",
"weight": 548,
"weight": 545,
"cookies": false,
"type": "location",
"demo": "storage\/get-file-view.md",
@@ -9181,7 +9181,7 @@
"x-appwrite": {
"method": "listTransactions",
"group": "transactions",
"weight": 419,
"weight": 416,
"cookies": false,
"type": "",
"demo": "tablesdb\/list-transactions.md",
@@ -9251,7 +9251,7 @@
"x-appwrite": {
"method": "createTransaction",
"group": "transactions",
"weight": 415,
"weight": 412,
"cookies": false,
"type": "",
"demo": "tablesdb\/create-transaction.md",
@@ -9325,7 +9325,7 @@
"x-appwrite": {
"method": "getTransaction",
"group": "transactions",
"weight": 416,
"weight": 413,
"cookies": false,
"type": "",
"demo": "tablesdb\/get-transaction.md",
@@ -9391,7 +9391,7 @@
"x-appwrite": {
"method": "updateTransaction",
"group": "transactions",
"weight": 417,
"weight": 414,
"cookies": false,
"type": "",
"demo": "tablesdb\/update-transaction.md",
@@ -9473,7 +9473,7 @@
"x-appwrite": {
"method": "deleteTransaction",
"group": "transactions",
"weight": 418,
"weight": 415,
"cookies": false,
"type": "",
"demo": "tablesdb\/delete-transaction.md",
@@ -9541,7 +9541,7 @@
"x-appwrite": {
"method": "createOperations",
"group": "transactions",
"weight": 420,
"weight": 417,
"cookies": false,
"type": "",
"demo": "tablesdb\/create-operations.md",
@@ -9625,7 +9625,7 @@
"x-appwrite": {
"method": "listRows",
"group": "rows",
"weight": 411,
"weight": 408,
"cookies": false,
"type": "",
"demo": "tablesdb\/list-rows.md",
@@ -9728,7 +9728,7 @@
"x-appwrite": {
"method": "createRow",
"group": "rows",
"weight": 403,
"weight": 400,
"cookies": false,
"type": "",
"demo": "tablesdb\/create-row.md",
@@ -9882,7 +9882,7 @@
"x-appwrite": {
"method": "getRow",
"group": "rows",
"weight": 404,
"weight": 401,
"cookies": false,
"type": "",
"demo": "tablesdb\/get-row.md",
@@ -9984,7 +9984,7 @@
"x-appwrite": {
"method": "upsertRow",
"group": "rows",
"weight": 407,
"weight": 404,
"cookies": false,
"type": "",
"demo": "tablesdb\/upsert-row.md",
@@ -10130,7 +10130,7 @@
"x-appwrite": {
"method": "updateRow",
"group": "rows",
"weight": 405,
"weight": 402,
"cookies": false,
"type": "",
"demo": "tablesdb\/update-row.md",
@@ -10239,7 +10239,7 @@
"x-appwrite": {
"method": "deleteRow",
"group": "rows",
"weight": 409,
"weight": 406,
"cookies": false,
"type": "",
"demo": "tablesdb\/delete-row.md",
@@ -10339,7 +10339,7 @@
"x-appwrite": {
"method": "decrementRowColumn",
"group": "rows",
"weight": 414,
"weight": 411,
"cookies": false,
"type": "",
"demo": "tablesdb\/decrement-row-column.md",
@@ -10461,7 +10461,7 @@
"x-appwrite": {
"method": "incrementRowColumn",
"group": "rows",
"weight": 413,
"weight": 410,
"cookies": false,
"type": "",
"demo": "tablesdb\/increment-row-column.md",
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+24 -10
View File
@@ -1469,13 +1469,14 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
->inject('devKey')
->inject('user')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('geodb')
->inject('queueForEvents')
->inject('store')
->inject('proofForPassword')
->inject('proofForToken')
->inject('authorization')
->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Validator $redirectValidator, Document $devKey, User $user, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, Authorization $authorization) use ($oauthDefaultSuccess) {
->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Validator $redirectValidator, Document $devKey, User $user, Database $dbForProject, Database $dbForPlatform, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, Authorization $authorization) use ($oauthDefaultSuccess) {
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$port = $request->getPort();
$callbackBase = $protocol . '://' . $request->getHostname();
@@ -1512,6 +1513,25 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
$state = $defaultState;
}
// Allow redirect to rule URL if related to project
//Check if $redirectValidator is instance of Redirect class
if ($redirectValidator instanceof Redirect) {
$rules = $authorization->skip(fn () => $dbForPlatform->find('rules', [
Query::equal('domain', [
parse_url($state['success'], PHP_URL_HOST),
parse_url($state['failure'], PHP_URL_HOST)
]),
Query::equal('projectInternalId', [$project->getSequence()]),
Query::limit(2)
]));
foreach ($rules as $rule) {
$allowedHostnames = $redirectValidator->getAllowedHostnames();
$allowedHostnames[] = $rule->getAttribute('domain', '');
$redirectValidator->setAllowedHostnames($allowedHostnames);
}
}
if ($devKey->isEmpty() && !$redirectValidator->isValid($state['success'])) {
throw new Exception(Exception::PROJECT_INVALID_SUCCESS_URL);
}
@@ -4574,16 +4594,10 @@ Http::get('/v1/account/identities')
$queries[] = Query::equal('userInternalId', [$user->getSequence()]);
/**
* 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) {
/** @var Query $cursor */
$cursor = Query::getCursorQueries($queries, false);
$cursor = \reset($cursor);
if ($cursor !== false) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
+15 -40
View File
@@ -1087,15 +1087,10 @@ Http::get('/v1/messaging/providers')
$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);
$cursor = Query::getCursorQueries($queries, false);
$cursor = \reset($cursor);
if ($cursor) {
if ($cursor !== false) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
@@ -2496,15 +2491,10 @@ Http::get('/v1/messaging/topics')
$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);
$cursor = Query::getCursorQueries($queries, false);
$cursor = \reset($cursor);
if ($cursor) {
if ($cursor !== false) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
@@ -2904,15 +2894,10 @@ Http::get('/v1/messaging/topics/:topicId/subscribers')
$queries[] = Query::equal('topicInternalId', [$topic->getSequence()]);
/**
* 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);
$cursor = Query::getCursorQueries($queries, false);
$cursor = \reset($cursor);
if ($cursor) {
if ($cursor !== false) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
@@ -3719,15 +3704,10 @@ Http::get('/v1/messaging/messages')
$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);
$cursor = Query::getCursorQueries($queries, false);
$cursor = \reset($cursor);
if ($cursor) {
if ($cursor !== false) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
@@ -3899,15 +3879,10 @@ Http::get('/v1/messaging/messages/:messageId/targets')
$queries[] = Query::equal('$id', $targetIDs);
/**
* 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);
$cursor = Query::getCursorQueries($queries, false);
$cursor = \reset($cursor);
if ($cursor) {
if ($cursor !== false) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
+3 -9
View File
@@ -629,16 +629,10 @@ Http::get('/v1/migrations')
$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) {
/** @var Query $cursor */
$cursor = Query::getCursorQueries($queries, false);
$cursor = \reset($cursor);
if ($cursor !== false) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
+54 -10
View File
@@ -14,16 +14,21 @@ use Appwrite\SDK\Deprecated;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Template\Template;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Database\Validator\Queries\Keys;
use Appwrite\Utopia\Response;
use PHPMailer\PHPMailer\PHPMailer;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Database\Validator\Datetime as DatetimeValidator;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\UID;
use Utopia\Domains\Validator\PublicDomain;
use Utopia\Http;
@@ -1094,12 +1099,15 @@ Http::post('/v1/projects/:projectId/keys')
]
))
->param('projectId', '', new UID(), 'Project unique ID.')
// TODO: When migrating to Platform API, mark keyId required for consistency
->param('keyId', 'unique()', new CustomId(), 'Key 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('name', null, new Text(128), 'Key name. Max length: 128 chars.')
->param('scopes', null, new Nullable(new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE)), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.')
->param('expire', null, new Nullable(new DatetimeValidator()), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true)
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, string $name, array $scopes, ?string $expire, Response $response, Database $dbForPlatform) {
->action(function (string $projectId, string $keyId, string $name, array $scopes, ?string $expire, Response $response, Database $dbForPlatform) {
$keyId = $keyId == 'unique()' ? ID::unique() : $keyId;
$project = $dbForPlatform->getDocument('projects', $projectId);
@@ -1108,7 +1116,7 @@ Http::post('/v1/projects/:projectId/keys')
}
$key = new Document([
'$id' => ID::unique(),
'$id' => $keyId,
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
@@ -1125,7 +1133,11 @@ Http::post('/v1/projects/:projectId/keys')
'secret' => API_KEY_STANDARD . '_' . \bin2hex(\random_bytes(128)),
]);
$key = $dbForPlatform->createDocument('keys', $key);
try {
$key = $dbForPlatform->createDocument('keys', $key);
} catch (Duplicate) {
throw new Exception(Exception::KEY_ALREADY_EXISTS);
}
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
@@ -1152,10 +1164,11 @@ Http::get('/v1/projects/:projectId/keys')
]
))
->param('projectId', '', new UID(), 'Project unique ID.')
->param('queries', [], new Keys(), '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(', ', Keys::ALLOWED_ATTRIBUTES), true)
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, bool $includeTotal, Response $response, Database $dbForPlatform) {
->action(function (string $projectId, array $queries, bool $includeTotal, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
@@ -1163,15 +1176,46 @@ Http::get('/v1/projects/:projectId/keys')
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$keys = $dbForPlatform->find('keys', [
Query::equal('resourceType', ['projects']),
Query::equal('resourceInternalId', [$project->getSequence()]),
Query::limit(5000),
]);
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
// Backwards compatibility
if (\count(Query::getByType($queries, [Query::TYPE_LIMIT])) === 0) {
$queries[] = Query::limit(5000);
}
$queries[] = Query::equal('resourceType', ['projects']);
$queries[] = Query::equal('resourceInternalId', [$project->getSequence()]);
$cursor = Query::getCursorQueries($queries, false);
$cursor = \reset($cursor);
if ($cursor !== false) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
}
$keyId = $cursor->getValue();
$cursorDocument = $dbForPlatform->getDocument('keys', $keyId);
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Key '{$keyId}' for the 'cursor' value not found.");
}
$cursor->setValue($cursorDocument);
}
$filterQueries = Query::groupByType($queries)['filters'];
$keys = $dbForPlatform->find('keys', $queries);
$response->dynamic(new Document([
'keys' => $keys,
'total' => $includeTotal ? count($keys) : 0,
'total' => $includeTotal ? $dbForPlatform->count('keys', $filterQueries, APP_LIMIT_COUNT) : 0,
]), Response::MODEL_KEY_LIST);
});
+6 -19
View File
@@ -190,16 +190,10 @@ Http::get('/v1/teams')
$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) {
/** @var Query $cursor */
$cursor = Query::getCursorQueries($queries, false);
$cursor = \reset($cursor);
if ($cursor !== false) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
@@ -879,22 +873,15 @@ Http::get('/v1/teams/:teamId/memberships')
// Set internal queries
$queries[] = Query::equal('teamInternalId', [$team->getSequence()]);
/**
* 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) {
/** @var Query $cursor */
$cursor = Query::getCursorQueries($queries, false);
$cursor = \reset($cursor);
if ($cursor !== false) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
}
$membershipId = $cursor->getValue();
$cursorDocument = $dbForProject->getDocument('memberships', $membershipId);
+13 -26
View File
@@ -675,16 +675,10 @@ Http::get('/v1/users')
$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) {
/** @var Query $cursor */
$cursor = Query::getCursorQueries($queries, false);
$cursor = \reset($cursor);
if ($cursor !== false) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
@@ -1042,14 +1036,11 @@ Http::get('/v1/users/:userId/targets')
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$queries[] = Query::equal('userId', [$userId]);
/**
* 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) {
$cursor = Query::getCursorQueries($queries, false);
$cursor = \reset($cursor);
if ($cursor !== false) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
@@ -1106,15 +1097,11 @@ Http::get('/v1/users/identities')
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) {
/** @var Query $cursor */
$cursor = Query::getCursorQueries($queries, false);
$cursor = \reset($cursor);
if ($cursor !== false) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
File diff suppressed because it is too large Load Diff
+26 -21
View File
@@ -8,7 +8,7 @@ use Appwrite\Auth\Key;
use Appwrite\Event\Certificate;
use Appwrite\Event\Delete as DeleteEvent;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Execution;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Network\Cors;
@@ -60,7 +60,7 @@ Config::setParam('domainVerification', false);
Config::setParam('cookieDomain', 'localhost');
Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE);
function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Authorization $authorization, ?Key $apiKey, DeleteEvent $queueForDeletes, int $executionsRetentionCount)
function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, StatsUsage $queueForStatsUsage, Execution $queueForExecutions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Authorization $authorization, ?Key $apiKey, DeleteEvent $queueForDeletes, int $executionsRetentionCount)
{
$host = $request->getHostname() ?? '';
if (!empty($previewHostname)) {
@@ -696,14 +696,11 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
throw $th;
}
} finally {
if ($type === 'function') {
$queueForFunctions
->setType(Func::TYPE_ASYNC_WRITE)
if ($type === 'function' || $type === 'site') {
$queueForExecutions
->setExecution($execution)
->setProject($project)
->trigger();
} elseif ($type === 'site') { // TODO: Move it to logs worker later
$dbForProject->createDocument('executions', $execution);
}
}
@@ -877,7 +874,7 @@ Http::init()
->inject('geodb')
->inject('queueForStatsUsage')
->inject('queueForEvents')
->inject('queueForFunctions')
->inject('queueForExecutions')
->inject('executor')
->inject('platform')
->inject('isResourceBlocked')
@@ -888,7 +885,7 @@ Http::init()
->inject('authorization')
->inject('queueForDeletes')
->inject('executionsRetentionCount')
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, Reader $geodb, StatsUsage $queueForStatsUsage, Event $queueForEvents, Func $queueForFunctions, Executor $executor, array $platform, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, Reader $geodb, StatsUsage $queueForStatsUsage, Event $queueForEvents, Execution $queueForExecutions, Executor $executor, array $platform, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
/*
* Appwrite Router
*/
@@ -896,7 +893,7 @@ Http::init()
$platformHostnames = $platform['hostnames'] ?? [];
// Only run Router when external domain
if (!\in_array($hostname, $platformHostnames) || !empty($previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForExecutions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -950,7 +947,15 @@ Http::init()
$endDomain->getRegisterable() !== ''
);
$isLocalHost = $request->getHostname() === 'localhost' || $request->getHostname() === 'localhost:' . $request->getPort();
$localHosts = ['localhost','localhost:'.$request->getPort()];
$migrationHost = System::getEnv('_APP_MIGRATION_HOST');
if (!empty($migrationHost)) {
$localHosts[] = $migrationHost;
$localHosts[] = $migrationHost.':'.$request->getPort();
}
$isLocalHost = in_array($request->getHostname(), $localHosts);
$isIpAddress = filter_var($request->getHostname(), FILTER_VALIDATE_IP) !== false;
$isConsoleProject = $project->getAttribute('$id', '') === 'console';
@@ -997,7 +1002,7 @@ Http::init()
}
if (System::getEnv('_APP_OPTIONS_FORCE_HTTPS', 'disabled') === 'enabled') { // Force HTTPS
if ($request->getProtocol() !== 'https' && ($swooleRequest->header['host'] ?? '') !== 'localhost') { // localhost allowed for proxy, APP_HOSTNAME_INTERNAL allowed for migrations
if ($request->getProtocol() !== 'https' && !in_array(($swooleRequest->header['host'] ?? ''), $localHosts)) { // localhost allowed for proxy
if ($request->getMethod() !== Request::METHOD_GET) {
throw new AppwriteException(AppwriteException::GENERAL_PROTOCOL_UNSUPPORTED, 'Method unsupported over HTTP. Please use HTTPS instead.');
}
@@ -1167,7 +1172,7 @@ Http::options()
->inject('getProjectDB')
->inject('queueForEvents')
->inject('queueForStatsUsage')
->inject('queueForFunctions')
->inject('queueForExecutions')
->inject('executor')
->inject('geodb')
->inject('isResourceBlocked')
@@ -1180,14 +1185,14 @@ Http::options()
->inject('authorization')
->inject('queueForDeletes')
->inject('executionsRetentionCount')
->action(function (Http $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, array $platform, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Execution $queueForExecutions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
/*
* Appwrite Router
*/
$platformHostnames = $platform['hostnames'] ?? [];
// Only run Router when external domain
if (!in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForExecutions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -1563,7 +1568,7 @@ Http::get('/robots.txt')
->inject('getProjectDB')
->inject('queueForEvents')
->inject('queueForStatsUsage')
->inject('queueForFunctions')
->inject('queueForExecutions')
->inject('executor')
->inject('geodb')
->inject('isResourceBlocked')
@@ -1573,13 +1578,13 @@ Http::get('/robots.txt')
->inject('authorization')
->inject('queueForDeletes')
->inject('executionsRetentionCount')
->action(function (Http $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, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Execution $queueForExecutions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
$platformHostnames = $platform['hostnames'] ?? [];
if (in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
$template = new View(__DIR__ . '/../views/general/robots.phtml');
$response->text($template->render(false));
} else {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForExecutions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -1598,7 +1603,7 @@ Http::get('/humans.txt')
->inject('getProjectDB')
->inject('queueForEvents')
->inject('queueForStatsUsage')
->inject('queueForFunctions')
->inject('queueForExecutions')
->inject('executor')
->inject('geodb')
->inject('isResourceBlocked')
@@ -1608,13 +1613,13 @@ Http::get('/humans.txt')
->inject('authorization')
->inject('queueForDeletes')
->inject('executionsRetentionCount')
->action(function (Http $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, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Execution $queueForExecutions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
$platformHostnames = $platform['hostnames'] ?? [];
if (in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
$template = new View(__DIR__ . '/../views/general/humans.phtml');
$response->text($template->render(false));
} else {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForExecutions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
$utopia->getRoute()?->label('router', true);
}
}
+1 -1
View File
@@ -181,7 +181,7 @@ $http->on(Constant::EVENT_AFTER_RELOAD, function ($server) {
include __DIR__ . '/controllers/general.php';
function createDatabase(Http $app, string $resourceKey, string $dbName, array $collections, mixed $pools, callable $extraSetup = null): void
function createDatabase(Http $app, string $resourceKey, string $dbName, array $collections, mixed $pools, ?callable $extraSetup = null): void
{
$max = 10;
$sleep = 1;
+24 -4
View File
@@ -10,6 +10,7 @@ use Appwrite\Event\Certificate;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Execution;
use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
@@ -159,6 +160,9 @@ Http::setResource('queueForAudits', function (Publisher $publisher) {
Http::setResource('queueForFunctions', function (Publisher $publisher) {
return new Func($publisher);
}, ['publisher']);
Http::setResource('queueForExecutions', function (Publisher $publisher) {
return new Execution($publisher);
}, ['publisher']);
Http::setResource('eventProcessor', function () {
return new EventProcessor();
}, []);
@@ -198,15 +202,26 @@ Http::setResource('allowedHostnames', function (array $platform, Document $proje
}
$originHostname = parse_url($request->getOrigin(), PHP_URL_HOST);
$refererHostname = parse_url($request->getReferer(), PHP_URL_HOST);
$hostname = $originHostname;
if (empty($hostname)) {
$hostname = $refererHostname;
}
/* Add request hostname for preflight requests */
if ($request->getMethod() === 'OPTIONS') {
$allowed[] = $originHostname;
$allowed[] = $hostname;
}
/* Allow the request origin if a dev key or rule is found */
if ((!$rule->isEmpty() || !$devKey->isEmpty()) && !empty($originHostname)) {
$allowed[] = $originHostname;
/* Allow the request origin of rule */
if (!$rule->isEmpty() && !empty($rule->getAttribute('domain', ''))) {
$allowed[] = $rule->getAttribute('domain', '');
}
/* Allow the request origin if a dev key is found */
if (!$devKey->isEmpty() && !empty($hostname)) {
$allowed[] = $hostname;
}
return array_unique($allowed);
@@ -237,6 +252,11 @@ Http::setResource('allowedSchemes', function (Document $project) {
*/
Http::setResource('rule', function (Request $request, Database $dbForPlatform, Document $project, Authorization $authorization) {
$domain = \parse_url($request->getOrigin(), PHP_URL_HOST);
if (empty($domain)) {
$domain = \parse_url($request->getReferer(), PHP_URL_HOST);
}
if (empty($domain)) {
return new Document();
}
+52 -49
View File
@@ -431,15 +431,20 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
]
];
$subscribers = $realtime->getSubscribers($event); // [connectionId => [subId => queries]]
$subscribers = $realtime->getSubscribers($event);
// For test events, send to all connections with their matched subscription queries
foreach ($subscribers as $connectionId => $matchedSubscriptions) {
$groups = [];
foreach ($subscribers as $id => $matched) {
$key = implode(',', array_keys($matched));
$groups[$key]['ids'][] = $id;
$groups[$key]['subscriptions'] = array_keys($matched);
}
foreach ($groups as $group) {
$data = $event['data'];
// Send matched subscription IDs
$data['subscriptions'] = array_keys($matchedSubscriptions);
$data['subscriptions'] = $group['subscriptions'];
$server->send([$connectionId], json_encode([
$server->send($group['ids'], json_encode([
'type' => 'event',
'data' => $data
]));
@@ -484,18 +489,18 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
$roles = $user->getRoles($database->getAuthorization());
$authorization = $realtime->connections[$connection]['authorization'] ?? null;
$subscriptionMetadata = $realtime->getSubscriptionMetadata($connection);
$meta = $realtime->getSubscriptionMetadata($connection);
$realtime->unsubscribe($connection);
foreach ($subscriptionMetadata as $subscriptionId => $metadata) {
$queries = Query::parseQueries($metadata['queries'] ?? []);
foreach ($meta as $subscriptionId => $subscription) {
$queries = Query::parseQueries($subscription['queries'] ?? []);
$realtime->subscribe(
$projectId,
$connection,
$subscriptionId,
$roles,
$metadata['channels'] ?? [],
$subscription['channels'] ?? [],
$queries
);
}
@@ -507,35 +512,38 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
}
}
$receivers = $realtime->getSubscribers($event); // [connectionId => [subId => queries]]
$receivers = $realtime->getSubscribers($event);
if (Http::isDevelopment() && !empty($receivers)) {
Console::log("[Debug][Worker {$workerId}] Receivers: " . count($receivers));
Console::log("[Debug][Worker {$workerId}] Receivers Connection IDs: " . json_encode(array_keys($receivers)));
Console::log("[Debug][Worker {$workerId}] Event Query: " . json_encode(array_values($receivers)));
Console::log("[Debug][Worker {$workerId}] Connection IDs: " . json_encode(array_keys($receivers)));
Console::log("[Debug][Worker {$workerId}] Matched: " . json_encode(array_values($receivers)));
Console::log("[Debug][Worker {$workerId}] Event: " . $payload);
}
$totalMessages = 0;
foreach ($receivers as $connectionId => $matchedSubscriptions) {
$data = $event['data'];
// Send matched subscription IDs
$data['subscriptions'] = array_keys($matchedSubscriptions);
$server->send(
[$connectionId],
json_encode([
'type' => 'event',
'data' => $data
])
);
$totalMessages++;
// Group connections by matched subscription IDs for batch sending
$groups = [];
foreach ($receivers as $id => $matched) {
$key = implode(',', array_keys($matched));
$groups[$key]['ids'][] = $id;
$groups[$key]['subscriptions'] = array_keys($matched);
}
if ($totalMessages > 0) {
$register->get('telemetry.messageSentCounter')->add($totalMessages);
$stats->incr($event['project'], 'messages', $totalMessages);
$total = 0;
foreach ($groups as $group) {
$data = $event['data'];
$data['subscriptions'] = $group['subscriptions'];
$server->send($group['ids'], json_encode([
'type' => 'event',
'data' => $data
]));
$total += count($group['ids']);
}
if ($total > 0) {
$register->get('telemetry.messageSentCounter')->add($total);
$stats->incr($event['project'], 'messages', $total);
}
});
} catch (Throwable $th) {
@@ -624,21 +632,19 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, 'Missing channels');
}
// Reconstruct subscriptions from query params using helper method
$channelNames = array_keys($channels);
$names = array_keys($channels);
try {
$subscriptionsByIndex = Realtime::constructSubscriptions(
$channelNames,
$subscriptions = Realtime::constructSubscriptions(
$names,
fn ($channel) => $request->getQuery($channel, null)
);
} catch (QueryException $e) {
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, $e->getMessage());
}
// Generate subscription IDs and subscribe
$subscriptionMapping = [];
foreach ($subscriptionsByIndex as $index => $subscription) {
$mapping = [];
foreach ($subscriptions as $index => $subscription) {
$subscriptionId = ID::unique();
$realtime->subscribe(
@@ -647,10 +653,10 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
$subscriptionId,
$roles,
$subscription['channels'],
$subscription['queries'] // Query objects
$subscription['queries']
);
$subscriptionMapping[$index] = $subscriptionId;
$mapping[$index] = $subscriptionId;
}
$realtime->connections[$connection]['authorization'] = $authorization;
@@ -660,8 +666,8 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
$server->send([$connection], json_encode([
'type' => 'connected',
'data' => [
'channels' => $channelNames,
'subscriptions' => $subscriptionMapping,
'channels' => $names,
'subscriptions' => $mapping,
'user' => $user
]
]));
@@ -791,32 +797,29 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
}
$roles = $user->getRoles($database->getAuthorization());
$channelNames = $realtime->connections[$connection]['channels'] ?? [];
$channels = Realtime::convertChannels(array_flip($channelNames), $user->getId());
$authorization = $realtime->connections[$connection]['authorization'] ?? null;
$projectId = $realtime->connections[$connection]['projectId'] ?? null;
$subscriptionMetadata = $realtime->getSubscriptionMetadata($connection);
$meta = $realtime->getSubscriptionMetadata($connection);
$realtime->unsubscribe($connection);
if (!empty($projectId)) {
foreach ($subscriptionMetadata as $subscriptionId => $metadata) {
$queries = Query::parseQueries($metadata['queries'] ?? []);
foreach ($meta as $subscriptionId => $subscription) {
$queries = Query::parseQueries($subscription['queries'] ?? []);
$realtime->subscribe(
$projectId,
$connection,
$subscriptionId,
$roles,
$metadata['channels'] ?? [],
$subscription['channels'] ?? [],
$queries
);
}
}
// Restore authorization after subscribe
if ($authorization !== null) {
$realtime->connections[$connection]['authorization'] = $authorization;
}
+1 -1
View File
@@ -14,7 +14,7 @@ $image = $this->getParam('image', '');
$enableAssistant = $this->getParam('enableAssistant', false);
?>services:
traefik:
image: traefik:2.11
image: traefik:3.6
container_name: appwrite-traefik
<<: *x-logging
command:
+5
View File
@@ -9,6 +9,7 @@ use Appwrite\Event\Certificate;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Execution;
use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
@@ -358,6 +359,10 @@ Server::setResource('queueForFunctions', function (Publisher $publisher) {
return new Func($publisher);
}, ['publisher']);
Server::setResource('queueForExecutions', function (Publisher $publisher) {
return new Execution($publisher);
}, ['publisher']);
Server::setResource('queueForRealtime', function () {
return new Realtime();
}, []);
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
exec php /usr/src/code/app/worker.php executions "$@"
+2 -2
View File
@@ -73,7 +73,7 @@
"utopia-php/registry": "0.5.*",
"utopia-php/storage": "0.18.*",
"utopia-php/swoole": "1.*",
"utopia-php/system": "0.9.*",
"utopia-php/system": "0.10.*",
"utopia-php/telemetry": "0.2.*",
"utopia-php/vcs": "1.*",
"utopia-php/websocket": "0.3.*",
@@ -82,7 +82,7 @@
"phpmailer/phpmailer": "6.9.*",
"chillerlan/php-qrcode": "4.4.*",
"adhocore/jwt": "1.1.*",
"spomky-labs/otphp": "10.0.*",
"spomky-labs/otphp": "^11.4.2",
"webonyx/graphql-php": "14.11.*",
"league/csv": "9.24.*",
"enshrined/svg-sanitize": "0.22.*"
Generated
+164 -330
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "78db79de4948322d4e6307f9eb9a74a6",
"content-hash": "917f9050c673379c91e46814c7c07f64",
"packages": [
{
"name": "adhocore/jwt",
@@ -161,21 +161,21 @@
},
{
"name": "appwrite/php-runtimes",
"version": "0.19.2",
"version": "0.19.3",
"source": {
"type": "git",
"url": "https://github.com/appwrite/runtimes.git",
"reference": "e5c142519df5aced37de9c302971c29c079ce3d9"
"reference": "bfe9f3c730c8c203c5e2d80bd5a81c9b1579714e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/runtimes/zipball/e5c142519df5aced37de9c302971c29c079ce3d9",
"reference": "e5c142519df5aced37de9c302971c29c079ce3d9",
"url": "https://api.github.com/repos/appwrite/runtimes/zipball/bfe9f3c730c8c203c5e2d80bd5a81c9b1579714e",
"reference": "bfe9f3c730c8c203c5e2d80bd5a81c9b1579714e",
"shasum": ""
},
"require": {
"php": ">=8.0",
"utopia-php/system": "0.9.*"
"utopia-php/system": "0.10.*"
},
"require-dev": {
"laravel/pint": "^1.15",
@@ -210,89 +210,22 @@
],
"support": {
"issues": "https://github.com/appwrite/runtimes/issues",
"source": "https://github.com/appwrite/runtimes/tree/0.19.2"
"source": "https://github.com/appwrite/runtimes/tree/0.19.3"
},
"time": "2025-11-11T13:44:44+00:00"
},
{
"name": "beberlei/assert",
"version": "v3.3.3",
"source": {
"type": "git",
"url": "https://github.com/beberlei/assert.git",
"reference": "b5fd8eacd8915a1b627b8bfc027803f1939734dd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/beberlei/assert/zipball/b5fd8eacd8915a1b627b8bfc027803f1939734dd",
"reference": "b5fd8eacd8915a1b627b8bfc027803f1939734dd",
"shasum": ""
},
"require": {
"ext-ctype": "*",
"ext-json": "*",
"ext-mbstring": "*",
"ext-simplexml": "*",
"php": "^7.1 || ^8.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "*",
"phpstan/phpstan": "*",
"phpunit/phpunit": ">=6.0.0",
"yoast/phpunit-polyfills": "^0.1.0"
},
"suggest": {
"ext-intl": "Needed to allow Assertion::count(), Assertion::isCountable(), Assertion::minCount(), and Assertion::maxCount() to operate on ResourceBundles"
},
"type": "library",
"autoload": {
"files": [
"lib/Assert/functions.php"
],
"psr-4": {
"Assert\\": "lib/Assert"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-2-Clause"
],
"authors": [
{
"name": "Benjamin Eberlei",
"email": "kontakt@beberlei.de",
"role": "Lead Developer"
},
{
"name": "Richard Quadling",
"email": "rquadling@gmail.com",
"role": "Collaborator"
}
],
"description": "Thin assertion library for input validation in business models.",
"keywords": [
"assert",
"assertion",
"validation"
],
"support": {
"issues": "https://github.com/beberlei/assert/issues",
"source": "https://github.com/beberlei/assert/tree/v3.3.3"
},
"time": "2024-07-15T13:18:35+00:00"
"time": "2026-02-06T10:08:18+00:00"
},
{
"name": "brick/math",
"version": "0.14.6",
"version": "0.14.7",
"source": {
"type": "git",
"url": "https://github.com/brick/math.git",
"reference": "32498d5e1897e7642c0b961ace2df6d7dc9a3bc3"
"reference": "07ff363b16ef8aca9692bba3be9e73fe63f34e50"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/brick/math/zipball/32498d5e1897e7642c0b961ace2df6d7dc9a3bc3",
"reference": "32498d5e1897e7642c0b961ace2df6d7dc9a3bc3",
"url": "https://api.github.com/repos/brick/math/zipball/07ff363b16ef8aca9692bba3be9e73fe63f34e50",
"reference": "07ff363b16ef8aca9692bba3be9e73fe63f34e50",
"shasum": ""
},
"require": {
@@ -331,7 +264,7 @@
],
"support": {
"issues": "https://github.com/brick/math/issues",
"source": "https://github.com/brick/math/tree/0.14.6"
"source": "https://github.com/brick/math/tree/0.14.7"
},
"funding": [
{
@@ -339,7 +272,7 @@
"type": "github"
}
],
"time": "2026-02-05T07:59:58+00:00"
"time": "2026-02-07T10:57:35+00:00"
},
{
"name": "chillerlan/php-qrcode",
@@ -673,16 +606,16 @@
},
{
"name": "giggsey/libphonenumber-for-php-lite",
"version": "8.13.36",
"version": "9.0.23",
"source": {
"type": "git",
"url": "https://github.com/giggsey/libphonenumber-for-php-lite.git",
"reference": "144bbe70d67664b5245910a475c7190ff140ab4b"
"reference": "e8f6f896c1bef398136849934a934904bd9bf072"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/giggsey/libphonenumber-for-php-lite/zipball/144bbe70d67664b5245910a475c7190ff140ab4b",
"reference": "144bbe70d67664b5245910a475c7190ff140ab4b",
"url": "https://api.github.com/repos/giggsey/libphonenumber-for-php-lite/zipball/e8f6f896c1bef398136849934a934904bd9bf072",
"reference": "e8f6f896c1bef398136849934a934904bd9bf072",
"shasum": ""
},
"require": {
@@ -694,18 +627,19 @@
},
"require-dev": {
"ext-dom": "*",
"friendsofphp/php-cs-fixer": "^3.12",
"infection/infection": "^0.28",
"pear/pear-core-minimal": "^1.10.11",
"pear/pear_exception": "^1.0.2",
"pear/versioncontrol_git": "^0.7",
"phing/phing": "^2.17.4",
"phpstan/extension-installer": "^1.2",
"phpstan/phpstan": "^1.8",
"phpstan/phpstan-phpunit": "^1.2",
"phpunit/phpunit": "^10.5",
"symfony/console": "^6.0",
"symfony/var-exporter": "^6.0"
"friendsofphp/php-cs-fixer": "^3.71",
"infection/infection": "^0.29|^0.31.0",
"nette/php-generator": "^4.1",
"php-coveralls/php-coveralls": "^2.7",
"phpstan/extension-installer": "^1.4.3",
"phpstan/phpstan": "^2.1.7",
"phpstan/phpstan-deprecation-rules": "^2.0.1",
"phpstan/phpstan-phpunit": "^2.0.4",
"phpstan/phpstan-strict-rules": "^2.0.3",
"phpunit/phpunit": "^10.5.45",
"symfony/console": "^6.4",
"symfony/filesystem": "^6.4",
"symfony/process": "^6.4"
},
"suggest": {
"giggsey/libphonenumber-for-php": "Use libphonenumber-for-php for geocoding, carriers, timezones and matching"
@@ -713,19 +647,13 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "8.x-dev"
"dev-master": "9.x-dev"
}
},
"autoload": {
"psr-4": {
"libphonenumber\\": "src/"
},
"exclude-from-classmap": [
"/src/data/",
"/src/carrier/data/",
"/src/geocoding/data/",
"/src/timezone/data/"
]
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
@@ -752,7 +680,7 @@
"issues": "https://github.com/giggsey/libphonenumber-for-php-lite/issues",
"source": "https://github.com/giggsey/libphonenumber-for-php-lite"
},
"time": "2024-05-03T06:31:11+00:00"
"time": "2026-01-30T12:33:03+00:00"
},
{
"name": "google/protobuf",
@@ -1708,24 +1636,26 @@
},
{
"name": "paragonie/constant_time_encoding",
"version": "v2.8.2",
"version": "v3.1.3",
"source": {
"type": "git",
"url": "https://github.com/paragonie/constant_time_encoding.git",
"reference": "e30811f7bc69e4b5b6d5783e712c06c8eabf0226"
"reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/e30811f7bc69e4b5b6d5783e712c06c8eabf0226",
"reference": "e30811f7bc69e4b5b6d5783e712c06c8eabf0226",
"url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77",
"reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77",
"shasum": ""
},
"require": {
"php": "^7|^8"
"php": "^8"
},
"require-dev": {
"phpunit/phpunit": "^6|^7|^8|^9",
"vimeo/psalm": "^1|^2|^3|^4"
"infection/infection": "^0",
"nikic/php-fuzzer": "^0",
"phpunit/phpunit": "^9|^10|^11",
"vimeo/psalm": "^4|^5|^6"
},
"type": "library",
"autoload": {
@@ -1771,7 +1701,7 @@
"issues": "https://github.com/paragonie/constant_time_encoding/issues",
"source": "https://github.com/paragonie/constant_time_encoding"
},
"time": "2025-09-24T15:12:37+00:00"
"time": "2025-09-24T15:06:41+00:00"
},
{
"name": "paragonie/random_compat",
@@ -2174,6 +2104,54 @@
],
"time": "2026-01-27T09:17:28+00:00"
},
{
"name": "psr/clock",
"version": "1.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/clock.git",
"reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d",
"reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d",
"shasum": ""
},
"require": {
"php": "^7.0 || ^8.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Psr\\Clock\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for reading the clock.",
"homepage": "https://github.com/php-fig/clock",
"keywords": [
"clock",
"now",
"psr",
"psr-20",
"time"
],
"support": {
"issues": "https://github.com/php-fig/clock/issues",
"source": "https://github.com/php-fig/clock/tree/1.0.0"
},
"time": "2022-11-25T14:36:26+00:00"
},
{
"name": "psr/container",
"version": "2.0.2",
@@ -2593,43 +2571,28 @@
},
{
"name": "spomky-labs/otphp",
"version": "v10.0.3",
"version": "11.4.2",
"source": {
"type": "git",
"url": "https://github.com/Spomky-Labs/otphp.git",
"reference": "9784d9f7c790eed26e102d6c78f12c754036c366"
"reference": "2a1b503fd1c1a5c751ab3c5cd37f2d2d26ab74ad"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Spomky-Labs/otphp/zipball/9784d9f7c790eed26e102d6c78f12c754036c366",
"reference": "9784d9f7c790eed26e102d6c78f12c754036c366",
"url": "https://api.github.com/repos/Spomky-Labs/otphp/zipball/2a1b503fd1c1a5c751ab3c5cd37f2d2d26ab74ad",
"reference": "2a1b503fd1c1a5c751ab3c5cd37f2d2d26ab74ad",
"shasum": ""
},
"require": {
"beberlei/assert": "^3.0",
"ext-mbstring": "*",
"paragonie/constant_time_encoding": "^2.0",
"php": "^7.2|^8.0",
"thecodingmachine/safe": "^0.1.14|^1.0|^2.0"
"paragonie/constant_time_encoding": "^2.0 || ^3.0",
"php": ">=8.1",
"psr/clock": "^1.0",
"symfony/deprecation-contracts": "^3.2"
},
"require-dev": {
"php-coveralls/php-coveralls": "^2.0",
"phpstan/phpstan": "^0.12",
"phpstan/phpstan-beberlei-assert": "^0.12",
"phpstan/phpstan-deprecation-rules": "^0.12",
"phpstan/phpstan-phpunit": "^0.12",
"phpstan/phpstan-strict-rules": "^0.12",
"phpunit/phpunit": "^8.0",
"thecodingmachine/phpstan-safe-rule": "^1.0 || ^2.0"
"symfony/error-handler": "^6.4|^7.0|^8.0"
},
"type": "library",
"extra": {
"branch-alias": {
"v8.3": "8.3.x-dev",
"v9.0": "9.0.x-dev",
"v10.0": "10.0.x-dev"
}
},
"autoload": {
"psr-4": {
"OTPHP\\": "src/"
@@ -2662,9 +2625,19 @@
],
"support": {
"issues": "https://github.com/Spomky-Labs/otphp/issues",
"source": "https://github.com/Spomky-Labs/otphp/tree/v10.0.3"
"source": "https://github.com/Spomky-Labs/otphp/tree/11.4.2"
},
"time": "2022-03-17T08:00:35+00:00"
"funding": [
{
"url": "https://github.com/Spomky",
"type": "github"
},
{
"url": "https://www.patreon.com/FlorentMorselli",
"type": "patreon"
}
],
"time": "2026-01-23T10:53:01+00:00"
},
{
"name": "symfony/deprecation-contracts",
@@ -3376,145 +3349,6 @@
},
"time": "2025-06-29T15:42:06+00:00"
},
{
"name": "thecodingmachine/safe",
"version": "v2.5.0",
"source": {
"type": "git",
"url": "https://github.com/thecodingmachine/safe.git",
"reference": "3115ecd6b4391662b4931daac4eba6b07a2ac1f0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thecodingmachine/safe/zipball/3115ecd6b4391662b4931daac4eba6b07a2ac1f0",
"reference": "3115ecd6b4391662b4931daac4eba6b07a2ac1f0",
"shasum": ""
},
"require": {
"php": "^8.0"
},
"require-dev": {
"phpstan/phpstan": "^1.5",
"phpunit/phpunit": "^9.5",
"squizlabs/php_codesniffer": "^3.2",
"thecodingmachine/phpstan-strict-rules": "^1.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.2.x-dev"
}
},
"autoload": {
"files": [
"deprecated/apc.php",
"deprecated/array.php",
"deprecated/datetime.php",
"deprecated/libevent.php",
"deprecated/misc.php",
"deprecated/password.php",
"deprecated/mssql.php",
"deprecated/stats.php",
"deprecated/strings.php",
"lib/special_cases.php",
"deprecated/mysqli.php",
"generated/apache.php",
"generated/apcu.php",
"generated/array.php",
"generated/bzip2.php",
"generated/calendar.php",
"generated/classobj.php",
"generated/com.php",
"generated/cubrid.php",
"generated/curl.php",
"generated/datetime.php",
"generated/dir.php",
"generated/eio.php",
"generated/errorfunc.php",
"generated/exec.php",
"generated/fileinfo.php",
"generated/filesystem.php",
"generated/filter.php",
"generated/fpm.php",
"generated/ftp.php",
"generated/funchand.php",
"generated/gettext.php",
"generated/gmp.php",
"generated/gnupg.php",
"generated/hash.php",
"generated/ibase.php",
"generated/ibmDb2.php",
"generated/iconv.php",
"generated/image.php",
"generated/imap.php",
"generated/info.php",
"generated/inotify.php",
"generated/json.php",
"generated/ldap.php",
"generated/libxml.php",
"generated/lzf.php",
"generated/mailparse.php",
"generated/mbstring.php",
"generated/misc.php",
"generated/mysql.php",
"generated/network.php",
"generated/oci8.php",
"generated/opcache.php",
"generated/openssl.php",
"generated/outcontrol.php",
"generated/pcntl.php",
"generated/pcre.php",
"generated/pgsql.php",
"generated/posix.php",
"generated/ps.php",
"generated/pspell.php",
"generated/readline.php",
"generated/rpminfo.php",
"generated/rrd.php",
"generated/sem.php",
"generated/session.php",
"generated/shmop.php",
"generated/sockets.php",
"generated/sodium.php",
"generated/solr.php",
"generated/spl.php",
"generated/sqlsrv.php",
"generated/ssdeep.php",
"generated/ssh2.php",
"generated/stream.php",
"generated/strings.php",
"generated/swoole.php",
"generated/uodbc.php",
"generated/uopz.php",
"generated/url.php",
"generated/var.php",
"generated/xdiff.php",
"generated/xml.php",
"generated/xmlrpc.php",
"generated/yaml.php",
"generated/yaz.php",
"generated/zip.php",
"generated/zlib.php"
],
"classmap": [
"lib/DateTime.php",
"lib/DateTimeImmutable.php",
"lib/Exceptions/",
"deprecated/Exceptions/",
"generated/Exceptions/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "PHP core functions that throw exceptions instead of returning FALSE on error",
"support": {
"issues": "https://github.com/thecodingmachine/safe/issues",
"source": "https://github.com/thecodingmachine/safe/tree/v2.5.0"
},
"time": "2023-04-05T11:54:14+00:00"
},
{
"name": "utopia-php/abuse",
"version": "1.2.2",
@@ -3771,16 +3605,16 @@
},
{
"name": "utopia-php/cli",
"version": "0.15.2",
"version": "0.15.4",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/cli.git",
"reference": "da00ff6b8b29a826a1794002ae43442cdf3a0f5f"
"reference": "cc873548dcab4329f2d56ca35f2edf3ae9d70f51"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/cli/zipball/da00ff6b8b29a826a1794002ae43442cdf3a0f5f",
"reference": "da00ff6b8b29a826a1794002ae43442cdf3a0f5f",
"url": "https://api.github.com/repos/utopia-php/cli/zipball/cc873548dcab4329f2d56ca35f2edf3ae9d70f51",
"reference": "cc873548dcab4329f2d56ca35f2edf3ae9d70f51",
"shasum": ""
},
"require": {
@@ -3814,9 +3648,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/cli/issues",
"source": "https://github.com/utopia-php/cli/tree/0.15.2"
"source": "https://github.com/utopia-php/cli/tree/0.15.4"
},
"time": "2025-04-15T10:08:48+00:00"
"time": "2026-02-06T09:49:09+00:00"
},
{
"name": "utopia-php/compression",
@@ -3961,16 +3795,16 @@
},
{
"name": "utopia-php/database",
"version": "5.0.1",
"version": "5.0.2",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
"reference": "2783f07e74ddd86dda6e711d79212dd34158540d"
"reference": "aa80f86f5bf3f0d8c13abd3213bf1649f542d366"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/database/zipball/2783f07e74ddd86dda6e711d79212dd34158540d",
"reference": "2783f07e74ddd86dda6e711d79212dd34158540d",
"url": "https://api.github.com/repos/utopia-php/database/zipball/aa80f86f5bf3f0d8c13abd3213bf1649f542d366",
"reference": "aa80f86f5bf3f0d8c13abd3213bf1649f542d366",
"shasum": ""
},
"require": {
@@ -4013,9 +3847,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/database/issues",
"source": "https://github.com/utopia-php/database/tree/5.0.1"
"source": "https://github.com/utopia-php/database/tree/5.0.2"
},
"time": "2026-02-03T10:31:12+00:00"
"time": "2026-02-08T05:23:42+00:00"
},
{
"name": "utopia-php/detector",
@@ -4169,16 +4003,16 @@
},
{
"name": "utopia-php/domains",
"version": "1.0.0",
"version": "1.0.1",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/domains.git",
"reference": "ecac82392e83d4a8ede76c3c94258d868d82f709"
"reference": "20b6a6868234d766fef35a47814dccc66906c2af"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/domains/zipball/ecac82392e83d4a8ede76c3c94258d868d82f709",
"reference": "ecac82392e83d4a8ede76c3c94258d868d82f709",
"url": "https://api.github.com/repos/utopia-php/domains/zipball/20b6a6868234d766fef35a47814dccc66906c2af",
"reference": "20b6a6868234d766fef35a47814dccc66906c2af",
"shasum": ""
},
"require": {
@@ -4225,9 +4059,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/domains/issues",
"source": "https://github.com/utopia-php/domains/tree/1.0.0"
"source": "https://github.com/utopia-php/domains/tree/1.0.1"
},
"time": "2026-01-30T06:15:50+00:00"
"time": "2026-02-06T12:45:12+00:00"
},
{
"name": "utopia-php/dsn",
@@ -4575,22 +4409,22 @@
},
{
"name": "utopia-php/messaging",
"version": "0.20.0",
"version": "0.20.1",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/messaging.git",
"reference": "6c5be4588d97e3732a1907ecb13cd8a098eade96"
"reference": "fcb4c3c46a48008a677957690bd45ec934dd33b0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/messaging/zipball/6c5be4588d97e3732a1907ecb13cd8a098eade96",
"reference": "6c5be4588d97e3732a1907ecb13cd8a098eade96",
"url": "https://api.github.com/repos/utopia-php/messaging/zipball/fcb4c3c46a48008a677957690bd45ec934dd33b0",
"reference": "fcb4c3c46a48008a677957690bd45ec934dd33b0",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-openssl": "*",
"giggsey/libphonenumber-for-php-lite": "8.13.36",
"giggsey/libphonenumber-for-php-lite": "9.0.23",
"php": ">=8.0.0",
"phpmailer/phpmailer": "6.9.1"
},
@@ -4620,22 +4454,22 @@
],
"support": {
"issues": "https://github.com/utopia-php/messaging/issues",
"source": "https://github.com/utopia-php/messaging/tree/0.20.0"
"source": "https://github.com/utopia-php/messaging/tree/0.20.1"
},
"time": "2025-10-22T04:27:37+00:00"
"time": "2026-02-06T09:56:06+00:00"
},
{
"name": "utopia-php/migration",
"version": "1.5.0",
"version": "1.5.1",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/migration.git",
"reference": "0d6e77748ca7d9302a88953751bd89d577610afd"
"reference": "b5fe19804b41d5bdd85571e7cdb83a268b6859e2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/migration/zipball/0d6e77748ca7d9302a88953751bd89d577610afd",
"reference": "0d6e77748ca7d9302a88953751bd89d577610afd",
"url": "https://api.github.com/repos/utopia-php/migration/zipball/b5fe19804b41d5bdd85571e7cdb83a268b6859e2",
"reference": "b5fe19804b41d5bdd85571e7cdb83a268b6859e2",
"shasum": ""
},
"require": {
@@ -4676,9 +4510,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/migration/issues",
"source": "https://github.com/utopia-php/migration/tree/1.5.0"
"source": "https://github.com/utopia-php/migration/tree/1.5.1"
},
"time": "2026-02-02T10:42:04+00:00"
"time": "2026-02-05T11:32:03+00:00"
},
{
"name": "utopia-php/mongo",
@@ -5275,16 +5109,16 @@
},
{
"name": "utopia-php/system",
"version": "0.9.0",
"version": "0.10.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/system.git",
"reference": "8e4a7edaf2dfeb4c9524e9f766d27754f2c4b64d"
"reference": "6441a9c180958a373e5ddb330264dd638539dfdb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/system/zipball/8e4a7edaf2dfeb4c9524e9f766d27754f2c4b64d",
"reference": "8e4a7edaf2dfeb4c9524e9f766d27754f2c4b64d",
"url": "https://api.github.com/repos/utopia-php/system/zipball/6441a9c180958a373e5ddb330264dd638539dfdb",
"reference": "6441a9c180958a373e5ddb330264dd638539dfdb",
"shasum": ""
},
"require": {
@@ -5292,7 +5126,7 @@
},
"require-dev": {
"laravel/pint": "1.13.*",
"phpstan/phpstan": "1.10.*",
"phpstan/phpstan": "1.12.*",
"phpunit/phpunit": "9.6.*"
},
"type": "library",
@@ -5325,9 +5159,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/system/issues",
"source": "https://github.com/utopia-php/system/tree/0.9.0"
"source": "https://github.com/utopia-php/system/tree/0.10.0"
},
"time": "2024-10-09T14:44:01+00:00"
"time": "2025-10-15T19:12:00+00:00"
},
{
"name": "utopia-php/telemetry",
@@ -5431,16 +5265,16 @@
},
{
"name": "utopia-php/vcs",
"version": "1.0.0",
"version": "1.0.1",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/vcs.git",
"reference": "3a427529dae09c0794541adac19062603d441e28"
"reference": "0f942f667319bb22cd420431e4e79107b41061f3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/vcs/zipball/3a427529dae09c0794541adac19062603d441e28",
"reference": "3a427529dae09c0794541adac19062603d441e28",
"url": "https://api.github.com/repos/utopia-php/vcs/zipball/0f942f667319bb22cd420431e4e79107b41061f3",
"reference": "0f942f667319bb22cd420431e4e79107b41061f3",
"shasum": ""
},
"require": {
@@ -5448,7 +5282,7 @@
"php": ">=8.0",
"utopia-php/cache": "1.0.*",
"utopia-php/framework": "0.*.*",
"utopia-php/system": "0.9.*"
"utopia-php/system": "0.10.*"
},
"require-dev": {
"laravel/pint": "1.*.*",
@@ -5474,9 +5308,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/vcs/issues",
"source": "https://github.com/utopia-php/vcs/tree/1.0.0"
"source": "https://github.com/utopia-php/vcs/tree/1.0.1"
},
"time": "2026-01-30T06:18:24+00:00"
"time": "2026-02-06T10:11:58+00:00"
},
{
"name": "utopia-php/websocket",
@@ -5654,16 +5488,16 @@
"packages-dev": [
{
"name": "appwrite/sdk-generator",
"version": "1.8.24",
"version": "1.8.26",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-generator.git",
"reference": "6bfe521d2b6762d1da94c234f6ef4eab54e5e269"
"reference": "ce65854069a1af8ef0757650da5848168cca5f02"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/6bfe521d2b6762d1da94c234f6ef4eab54e5e269",
"reference": "6bfe521d2b6762d1da94c234f6ef4eab54e5e269",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/ce65854069a1af8ef0757650da5848168cca5f02",
"reference": "ce65854069a1af8ef0757650da5848168cca5f02",
"shasum": ""
},
"require": {
@@ -5699,9 +5533,9 @@
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
"support": {
"issues": "https://github.com/appwrite/sdk-generator/issues",
"source": "https://github.com/appwrite/sdk-generator/tree/1.8.24"
"source": "https://github.com/appwrite/sdk-generator/tree/1.8.26"
},
"time": "2026-02-04T04:42:05+00:00"
"time": "2026-02-08T07:41:27+00:00"
},
{
"name": "doctrine/annotations",
+374
View File
@@ -0,0 +1,374 @@
# Development overrides for the Appwrite stack.
# This file is automatically loaded by `docker compose up` alongside docker-compose.yml.
# It adds source-code volume mounts, build configuration, debug ports, and dev tools.
services:
traefik:
ports:
- 8080:80
- 9500:8080
appwrite:
build:
context: .
target: development
args:
DEBUG: false
TESTING: true
VERSION: dev
ports:
- 9501:80
dns:
- 172.16.238.100
volumes:
- /var/run/docker.sock:/var/run/docker.sock # Only needed for tests
- ./docker-compose.yml:/usr/src/code/docker-compose.yml # Only needed for tests
- ./.env:/usr/src/code/.env # Only needed for tests
- ./phpunit.xml:/usr/src/code/phpunit.xml
- ./tests:/usr/src/code/tests
- ./app:/usr/src/code/app
- ./docs:/usr/src/code/docs
- ./public:/usr/src/code/public
- ./src:/usr/src/code/src
- ./dev:/usr/src/code/dev
# - ./vendor/utopia-php/framework:/usr/src/code/vendor/utopia-php/framework
depends_on:
- coredns
appwrite-realtime:
ports:
- 9505:80
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
appwrite-worker-audits:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
appwrite-worker-webhooks:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- request-catcher-sms
- request-catcher-webhook
appwrite-worker-deletes:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
appwrite-worker-databases:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
appwrite-worker-builds:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
appwrite-worker-screenshots:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
appwrite-worker-certificates:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
appwrite-worker-executions:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
appwrite-worker-functions:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
appwrite-worker-mails:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- maildev
appwrite-worker-messaging:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
appwrite-worker-migrations:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
- ./tests:/usr/src/code/tests
appwrite-task-maintenance:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
appwrite-task-interval:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
appwrite-task-stats-resources:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
appwrite-worker-stats-resources:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
appwrite-worker-stats-usage:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
appwrite-task-scheduler-functions:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
appwrite-task-scheduler-executions:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
appwrite-task-scheduler-messages:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
# Testing infrastructure
coredns: # DNS server for testing purposes (Proxy APIs)
image: coredns/coredns:1.12.4
container_name: appwrite-coredns
restart: unless-stopped
logging:
driver: "json-file"
options:
max-file: "5"
max-size: "10m"
command: ["-conf", "/mnt/resources/Corefile"]
networks:
appwrite:
ipv4_address: 172.16.238.100
volumes:
- ./tests/resources/coredns:/mnt/resources:ro
# Dev Tools ------------------------------------------------------------------------------------------
#
# The Appwrite Team uses the following tools to help debug, monitor and diagnose the Appwrite stack
#
# Here is a description of the different tools and why are we using them:
#
# MailCatcher - An SMTP server. Catches all system emails and displays them in a nice UI.
# RequestCatcher - An HTTP server. Catches all system https calls and displays them using a simple HTTP API. Used to debug & tests webhooks and HTTP tasks
# Redis Insight - A nice UI for exploring Redis data
# Adminer - A nice UI for exploring MariaDB data
# GraphQl Explorer - A nice UI for exploring GraphQL API
maildev: # used mainly for dev tests
image: appwrite/mailcatcher:1.1.1
container_name: appwrite-mailcatcher
logging:
driver: "json-file"
options:
max-file: "5"
max-size: "10m"
ports:
- "9503:1080"
networks:
- appwrite
- gateway
labels:
- "traefik.enable=true"
- "traefik.constraint-label-stack=appwrite"
- "traefik.docker.network=gateway"
- "traefik.http.services.appwrite_maildev.loadbalancer.server.port=1080"
- "traefik.http.routers.appwrite_maildev_http.entrypoints=appwrite_web"
- "traefik.http.routers.appwrite_maildev_http.rule=Host(`mail.localhost`)"
- "traefik.http.routers.appwrite_maildev_http.service=appwrite_maildev"
- "traefik.http.routers.appwrite_maildev_https.entrypoints=appwrite_websecure"
- "traefik.http.routers.appwrite_maildev_https.rule=Host(`mail.localhost`)"
- "traefik.http.routers.appwrite_maildev_https.service=appwrite_maildev"
- "traefik.http.routers.appwrite_maildev_https.tls=true"
environment:
- MAILDEV_INCOMING_USER=${_APP_SMTP_USERNAME}
- MAILDEV_INCOMING_PASS=${_APP_SMTP_PASSWORD}
request-catcher-webhook: # used mainly for dev tests (mock HTTP webhook)
image: appwrite/requestcatcher:1.0.0
container_name: appwrite-requestcatcher-webhook
logging:
driver: "json-file"
options:
max-file: "5"
max-size: "10m"
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
logging:
driver: "json-file"
options:
max-file: "5"
max-size: "10m"
ports:
- "9507:5000"
networks:
- appwrite
adminer:
image: adminer
container_name: appwrite-adminer
logging:
driver: "json-file"
options:
max-file: "5"
max-size: "10m"
restart: always
ports:
- 9506:8080
networks:
- appwrite
- gateway
environment:
- ADMINER_DESIGN=pepa-linha
- ADMINER_DEFAULT_SERVER=mariadb
- ADMINER_DEFAULT_USERNAME=root
- ADMINER_DEFAULT_PASSWORD=rootsecretpassword
- ADMINER_DEFAULT_DB=appwrite
configs:
- source: adminer-index.php
target: /var/www/html/index.php
mode: 0755
labels:
- "traefik.enable=true"
- "traefik.constraint-label-stack=appwrite"
- "traefik.docker.network=gateway"
- "traefik.http.services.appwrite_adminer.loadbalancer.server.port=8080"
- "traefik.http.routers.appwrite_adminer_http.entrypoints=appwrite_web"
- "traefik.http.routers.appwrite_adminer_http.rule=Host(`mysql.localhost`)"
- "traefik.http.routers.appwrite_adminer_http.service=appwrite_adminer"
- "traefik.http.routers.appwrite_adminer_https.entrypoints=appwrite_websecure"
- "traefik.http.routers.appwrite_adminer_https.rule=Host(`mysql.localhost`)"
- "traefik.http.routers.appwrite_adminer_https.service=appwrite_adminer"
- "traefik.http.routers.appwrite_adminer_https.tls=true"
redis-insight:
image: redis/redisinsight:latest
restart: unless-stopped
networks:
- appwrite
- gateway
environment:
- RI_PRE_SETUP_DATABASES_PATH=/mnt/connections.json
configs:
- source: redisinsight-connections.json
target: /mnt/connections.json
mode: 0755
labels:
- "traefik.enable=true"
- "traefik.constraint-label-stack=appwrite"
- "traefik.docker.network=gateway"
- "traefik.http.services.appwrite_redisinsight.loadbalancer.server.port=5540"
- "traefik.http.routers.appwrite_redisinsight_http.entrypoints=appwrite_web"
- "traefik.http.routers.appwrite_redisinsight_http.rule=Host(`redis.localhost`)"
- "traefik.http.routers.appwrite_redisinsight_http.service=appwrite_redisinsight"
- "traefik.http.routers.appwrite_redisinsight_https.entrypoints=appwrite_websecure"
- "traefik.http.routers.appwrite_redisinsight_https.rule=Host(`redis.localhost`)"
- "traefik.http.routers.appwrite_redisinsight_https.service=appwrite_redisinsight"
- "traefik.http.routers.appwrite_redisinsight_https.tls=true"
ports:
- "8081:5540"
graphql-explorer:
container_name: appwrite-graphql-explorer
image: appwrite/altair:0.3.0
restart: unless-stopped
networks:
- appwrite
ports:
- "9509:3000"
environment:
- SERVER_URL=http://localhost/v1/graphql
mariadb:
ports:
- "3306:3306"
redis:
ports:
- "6379:6379"
# Dev Tools End ------------------------------------------------------------------------------------------
configs:
redisinsight-connections.json:
content: |
[
{
"compressor": "NONE",
"id": "104dc90a-21ef-4d5e-8912-b30baabb152f",
"host": "redis",
"port": 6379,
"name": "redis:6379",
"db": 0,
"username": "default",
"password": null,
"connectionType": "STANDALONE",
"nameFromProvider": null,
"provider": "REDIS",
"lastConnection": "2025-10-16T09:22:02.591Z",
"modules": [
{
"name": "ReJSON",
"version": 20808,
"semanticVersion": "2.8.8"
},
{
"name": "search",
"version": 21015,
"semanticVersion": "2.10.15"
}
],
"tls": false,
"tlsServername": null,
"verifyServerCert": null,
"caCert": null,
"clientCert": null,
"ssh": false,
"sshOptions": null,
"forceStandalone": false,
"tags": []
}
]
adminer-index.php:
content: |
<?php
if(!count($$_GET)) {
$$_POST['auth'] = [
'server' => $$_ENV['ADMINER_DEFAULT_SERVER'],
'driver' => 'server', /* seems to autodetect the driver from server settings */
'username' => $$_ENV['ADMINER_DEFAULT_USERNAME'],
'password' => $$_ENV['ADMINER_DEFAULT_PASSWORD'],
'db' => $$_ENV['ADMINER_DEFAULT_DB'],
];
}
include './adminer.php';
+31 -295
View File
@@ -1,8 +1,3 @@
# WARNING!
# This is a development version of THE Appwrite docker-compose.yml file.
# Avoid using this file in your production environment.
# We're exposing here sensitive ports and mounting code volumes for rapid development and debugging of the server stack.
x-logging: &x-logging
logging:
driver: "json-file"
@@ -12,7 +7,7 @@ x-logging: &x-logging
services:
traefik:
image: traefik:2.11
image: traefik:3.6
<<: *x-logging
container_name: appwrite-traefik
command:
@@ -28,9 +23,7 @@ services:
- --accesslog=true
ports:
- 80:80
- 8080:80
- 443:443
- 9500:8080
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- appwrite-config:/storage/config:ro
@@ -52,19 +45,8 @@ services:
container_name: appwrite
<<: *x-logging
image: appwrite-dev
build:
context: .
target: development
args:
DEBUG: false
TESTING: true
VERSION: dev
ports:
- 9501:80
networks:
- appwrite
dns:
- 172.16.238.100
labels:
- "traefik.enable=true"
- "traefik.constraint-label-stack=appwrite"
@@ -80,9 +62,6 @@ services:
- traefik.http.routers.appwrite_api_https.service=appwrite_api
- traefik.http.routers.appwrite_api_https.tls=true
volumes:
- /var/run/docker.sock:/var/run/docker.sock # Only needed for tests
- ./docker-compose.yml:/usr/src/code/docker-compose.yml # Only needed for tests
- ./.env:/usr/src/code/.env # Only needed for tests
- appwrite-uploads:/storage/uploads:rw
- appwrite-imports:/storage/imports:rw
- appwrite-cache:/storage/cache:rw
@@ -91,19 +70,9 @@ services:
- appwrite-functions:/storage/functions:rw
- appwrite-sites:/storage/sites:rw
- appwrite-builds:/storage/builds:rw
- ./phpunit.xml:/usr/src/code/phpunit.xml
- ./tests:/usr/src/code/tests
- ./app:/usr/src/code/app
- ./docs:/usr/src/code/docs
- ./public:/usr/src/code/public
- ./src:/usr/src/code/src
- ./dev:/usr/src/code/dev
# - ./vendor/utopia-php/framework:/usr/src/code/vendor/utopia-php/framework
depends_on:
- mariadb
- redis
- coredns
# - clamav
entrypoint:
- php
- -e
@@ -229,6 +198,7 @@ services:
- _APP_FUNCTIONS_CREATION_ABUSE_LIMIT
- _APP_CUSTOM_DOMAIN_DENY_LIST
- _APP_TRUSTED_HEADERS
- _APP_MIGRATION_HOST
extra_hosts:
- "host.docker.internal:host-gateway"
@@ -260,8 +230,6 @@ services:
container_name: appwrite-realtime
image: appwrite-dev
restart: unless-stopped
ports:
- 9505:80
labels:
- "traefik.enable=true"
- "traefik.constraint-label-stack=appwrite"
@@ -278,9 +246,6 @@ services:
- traefik.http.routers.appwrite_realtime_wss.tls=true
networks:
- appwrite
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- mariadb
- redis
@@ -312,9 +277,6 @@ services:
image: appwrite-dev
networks:
- appwrite
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- redis
- mariadb
@@ -342,14 +304,9 @@ services:
image: appwrite-dev
networks:
- appwrite
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- redis
- mariadb
- request-catcher-sms
- request-catcher-webhook
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -386,8 +343,6 @@ services:
- appwrite-sites:/storage/sites:rw
- appwrite-builds:/storage/builds:rw
- appwrite-certificates:/storage/certificates:rw
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -440,9 +395,6 @@ services:
image: appwrite-dev
networks:
- appwrite
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- redis
- mariadb
@@ -476,8 +428,6 @@ services:
- appwrite-functions:/storage/functions:rw
- appwrite-sites:/storage/sites:rw
- appwrite-builds:/storage/builds:rw
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- redis
- mariadb
@@ -548,8 +498,6 @@ services:
- appwrite
volumes:
- appwrite-uploads:/storage/uploads:rw
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- redis
- mariadb
@@ -612,8 +560,6 @@ services:
volumes:
- appwrite-config:/storage/config:rw
- appwrite-certificates:/storage/certificates:rw
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -640,6 +586,34 @@ services:
- _APP_LOGGING_CONFIG
- _APP_DATABASE_SHARED_TABLES
appwrite-worker-executions:
entrypoint: worker-executions
<<: *x-logging
container_name: appwrite-worker-executions
image: appwrite-dev
networks:
- appwrite
depends_on:
redis:
condition: service_started
mariadb:
condition: service_started
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
- _APP_POOL_ADAPTER
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_LOGGING_CONFIG
- _APP_DATABASE_SHARED_TABLES
appwrite-worker-functions:
entrypoint: worker-functions
<<: *x-logging
@@ -647,9 +621,6 @@ services:
image: appwrite-dev
networks:
- appwrite
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
redis:
condition: service_started
@@ -694,13 +665,8 @@ services:
image: appwrite-dev
networks:
- appwrite
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- redis
- maildev
# - smtp
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -732,8 +698,6 @@ services:
- appwrite
volumes:
- appwrite-uploads:/storage/uploads:rw
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- redis
environment:
@@ -789,9 +753,6 @@ services:
volumes:
- appwrite-imports:/storage/imports:rw
- appwrite-uploads:/storage/uploads:rw
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
- ./tests:/usr/src/code/tests
depends_on:
- mariadb
environment:
@@ -820,6 +781,7 @@ services:
- _APP_MIGRATIONS_FIREBASE_CLIENT_SECRET
- _APP_DATABASE_SHARED_TABLES
- _APP_OPTIONS_FORCE_HTTPS
- _APP_MIGRATION_HOST
appwrite-task-maintenance:
entrypoint: maintenance
@@ -828,9 +790,6 @@ services:
image: appwrite-dev
networks:
- appwrite
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- mariadb
- redis
@@ -873,9 +832,6 @@ services:
image: appwrite-dev
networks:
- appwrite
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- mariadb
- redis
@@ -912,9 +868,6 @@ services:
image: appwrite-dev
networks:
- appwrite
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- redis
- mariadb
@@ -944,9 +897,6 @@ services:
image: appwrite-dev
networks:
- appwrite
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- redis
- mariadb
@@ -976,9 +926,6 @@ services:
image: appwrite-dev
networks:
- appwrite
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- redis
- mariadb
@@ -1008,9 +955,6 @@ services:
image: appwrite-dev
networks:
- appwrite
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- mariadb
- redis
@@ -1037,9 +981,6 @@ services:
image: appwrite-dev
networks:
- appwrite
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- mariadb
- redis
@@ -1065,9 +1006,6 @@ services:
image: appwrite-dev
networks:
- appwrite
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- mariadb
- redis
@@ -1168,8 +1106,6 @@ services:
- appwrite
volumes:
- appwrite-mariadb:/var/lib/mysql:rw
ports:
- "3306:3306"
environment:
- MYSQL_ROOT_PASSWORD=${_APP_DB_ROOT_PASS}
- MYSQL_DATABASE=${_APP_DB_SCHEMA}
@@ -1187,156 +1123,11 @@ services:
--maxmemory 512mb
--maxmemory-policy allkeys-lru
--maxmemory-samples 5
ports:
- "6379:6379"
networks:
- appwrite
volumes:
- appwrite-redis:/data:rw
coredns: # DNS server for testing purposes (Proxy APIs)
image: coredns/coredns:1.12.4
container_name: appwrite-coredns
restart: unless-stopped
<<: *x-logging
command: ["-conf", "/mnt/resources/Corefile"]
# If you need to debug CoreDNS, do it from "appwrite container", or port forward:
# ports:
# - "53:53"
networks:
appwrite:
ipv4_address: 172.16.238.100
volumes:
- ./tests/resources/coredns:/mnt/resources:ro
# Dev Tools Start ------------------------------------------------------------------------------------------
#
# The Appwrite Team uses the following tools to help debug, monitor and diagnose the Appwrite stack
#
# Here is a description of the different tools and why are we using them:
#
# MailCatcher - An SMTP server. Catches all system emails and displays them in a nice UI.
# RequestCatcher - An HTTP server. Catches all system https calls and displays them using a simple HTTP API. Used to debug & tests webhooks and HTTP tasks
# Redis Insight - A nice UI for exploring Redis data
# Adminer - A nice UI for exploring MariaDB data
# GraphQl Explorer - A nice UI for exploring GraphQL API
maildev: # used mainly for dev tests
image: appwrite/mailcatcher:1.1.1
container_name: appwrite-mailcatcher
<<: *x-logging
ports:
- "9503:1080"
networks:
- appwrite
- gateway
labels:
- "traefik.enable=true"
- "traefik.constraint-label-stack=appwrite"
- "traefik.docker.network=gateway"
- "traefik.http.services.appwrite_maildev.loadbalancer.server.port=1080"
- "traefik.http.routers.appwrite_maildev_http.entrypoints=appwrite_web"
- "traefik.http.routers.appwrite_maildev_http.rule=Host(`mail.localhost`)"
- "traefik.http.routers.appwrite_maildev_http.service=appwrite_maildev"
- "traefik.http.routers.appwrite_maildev_https.entrypoints=appwrite_websecure"
- "traefik.http.routers.appwrite_maildev_https.rule=Host(`mail.localhost`)"
- "traefik.http.routers.appwrite_maildev_https.service=appwrite_maildev"
- "traefik.http.routers.appwrite_maildev_https.tls=true"
environment:
- MAILDEV_INCOMING_USER=${_APP_SMTP_USERNAME}
- MAILDEV_INCOMING_PASS=${_APP_SMTP_PASSWORD}
request-catcher-webhook: # used mainly for dev tests (mock HTTP webhook)
image: appwrite/requestcatcher:1.0.0
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
<<: *x-logging
restart: always
ports:
- 9506:8080
networks:
- appwrite
- gateway
environment:
- ADMINER_DESIGN=pepa-linha
- ADMINER_DEFAULT_SERVER=mariadb
- ADMINER_DEFAULT_USERNAME=root
- ADMINER_DEFAULT_PASSWORD=rootsecretpassword
- ADMINER_DEFAULT_DB=appwrite
configs:
- source: adminer-index.php
target: /var/www/html/index.php
mode: 0755
labels:
- "traefik.enable=true"
- "traefik.constraint-label-stack=appwrite"
- "traefik.docker.network=gateway"
- "traefik.http.services.appwrite_adminer.loadbalancer.server.port=8080"
- "traefik.http.routers.appwrite_adminer_http.entrypoints=appwrite_web"
- "traefik.http.routers.appwrite_adminer_http.rule=Host(`mysql.localhost`)"
- "traefik.http.routers.appwrite_adminer_http.service=appwrite_adminer"
- "traefik.http.routers.appwrite_adminer_https.entrypoints=appwrite_websecure"
- "traefik.http.routers.appwrite_adminer_https.rule=Host(`mysql.localhost`)"
- "traefik.http.routers.appwrite_adminer_https.service=appwrite_adminer"
- "traefik.http.routers.appwrite_adminer_https.tls=true"
redis-insight:
image: redis/redisinsight:latest
restart: unless-stopped
networks:
- appwrite
- gateway
environment:
- RI_PRE_SETUP_DATABASES_PATH=/mnt/connections.json
configs:
- source: redisinsight-connections.json
target: /mnt/connections.json
mode: 0755
labels:
- "traefik.enable=true"
- "traefik.constraint-label-stack=appwrite"
- "traefik.docker.network=gateway"
- "traefik.http.services.appwrite_redisinsight.loadbalancer.server.port=5540"
- "traefik.http.routers.appwrite_redisinsight_http.entrypoints=appwrite_web"
- "traefik.http.routers.appwrite_redisinsight_http.rule=Host(`redis.localhost`)"
- "traefik.http.routers.appwrite_redisinsight_http.service=appwrite_redisinsight"
- "traefik.http.routers.appwrite_redisinsight_https.entrypoints=appwrite_websecure"
- "traefik.http.routers.appwrite_redisinsight_https.rule=Host(`redis.localhost`)"
- "traefik.http.routers.appwrite_redisinsight_https.service=appwrite_redisinsight"
- "traefik.http.routers.appwrite_redisinsight_https.tls=true"
ports:
- "8081:5540"
graphql-explorer:
container_name: appwrite-graphql-explorer
image: appwrite/altair:0.3.0
restart: unless-stopped
networks:
- appwrite
ports:
- "9509:3000"
environment:
- SERVER_URL=http://localhost/v1/graphql
# Dev Tools End ------------------------------------------------------------------------------------------
networks:
gateway:
name: gateway
@@ -1348,61 +1139,6 @@ networks:
runtimes:
name: runtimes
configs:
redisinsight-connections.json:
content: |
[
{
"compressor": "NONE",
"id": "104dc90a-21ef-4d5e-8912-b30baabb152f",
"host": "redis",
"port": 6379,
"name": "redis:6379",
"db": 0,
"username": "default",
"password": null,
"connectionType": "STANDALONE",
"nameFromProvider": null,
"provider": "REDIS",
"lastConnection": "2025-10-16T09:22:02.591Z",
"modules": [
{
"name": "ReJSON",
"version": 20808,
"semanticVersion": "2.8.8"
},
{
"name": "search",
"version": 21015,
"semanticVersion": "2.10.15"
}
],
"tls": false,
"tlsServername": null,
"verifyServerCert": null,
"caCert": null,
"clientCert": null,
"ssh": false,
"sshOptions": null,
"forceStandalone": false,
"tags": []
}
]
adminer-index.php:
content: |
<?php
if(!count($$_GET)) {
$$_POST['auth'] = [
'server' => $$_ENV['ADMINER_DEFAULT_SERVER'],
'driver' => 'server', /* seems to autodetect the driver from server settings */
'username' => $$_ENV['ADMINER_DEFAULT_USERNAME'],
'password' => $$_ENV['ADMINER_DEFAULT_PASSWORD'],
'db' => $$_ENV['ADMINER_DEFAULT_DB'],
];
}
include './adminer.php';
volumes:
appwrite-mariadb:
appwrite-redis:
+9
View File
@@ -1,5 +1,14 @@
# Change Log
## 13.3.1
- Fix generated TS imports to auto-detect ESM vs non-ESM
## 13.3.0
- Support type generation for text/varchar/mediumtext/longtext attributes
- Improve CLI session switch and logout UX
## 13.2.1
- Fix site domain construction
+3
View File
@@ -46,6 +46,9 @@ class Event
public const MESSAGING_QUEUE_NAME = 'v1-messaging';
public const MESSAGING_CLASS_NAME = 'MessagingV1';
public const EXECUTIONS_QUEUE_NAME = 'v1-executions';
public const EXECUTIONS_CLASS_NAME = 'ExecutionsV1';
public const MIGRATIONS_QUEUE_NAME = 'v1-migrations';
public const MIGRATIONS_CLASS_NAME = 'MigrationsV1';
+56
View File
@@ -0,0 +1,56 @@
<?php
namespace Appwrite\Event;
use Utopia\Database\Document;
use Utopia\Queue\Publisher;
class Execution extends Event
{
protected ?Document $execution = null;
public function __construct(protected Publisher $publisher)
{
parent::__construct($publisher);
$this
->setQueue(Event::EXECUTIONS_QUEUE_NAME)
->setClass(Event::EXECUTIONS_CLASS_NAME);
}
/**
* Sets execution document for the execution event.
*
* @param Document $execution
* @return self
*/
public function setExecution(Document $execution): self
{
$this->execution = $execution;
return $this;
}
/**
* Returns set execution document for the execution event.
*
* @return null|Document
*/
public function getExecution(): ?Document
{
return $this->execution;
}
/**
* Prepare payload for the execution event.
*
* @return array
*/
protected function preparePayload(): array
{
return [
'project' => $this->project,
'execution' => $this->execution,
];
}
}
-2
View File
@@ -9,8 +9,6 @@ use Utopia\System\System;
class Func extends Event
{
public const TYPE_ASYNC_WRITE = 'async_write';
protected string $jwt = '';
protected string $type = '';
protected string $body = '';
+1
View File
@@ -318,6 +318,7 @@ class Exception extends \Exception
/** Keys */
public const string KEY_NOT_FOUND = 'key_not_found';
public const string KEY_ALREADY_EXISTS = 'key_already_exists';
/** Variables */
public const string VARIABLE_NOT_FOUND = 'variable_not_found';
+74 -115
View File
@@ -31,9 +31,9 @@ class Realtime extends MessagingAdapter
* [ROLE_X] ->
* [CHANNEL_NAME_X] ->
* [CONNECTION_ID] ->
* [SUB_ID] -> ['strings' => [...], 'parsed' => [...]]
* [SUB_ID] -> ['strings' => [...], 'compiled' => [...]]
*
* Each subscription ID maps to query strings (for metadata) and pre-parsed Query objects (for filtering).
* Each subscription ID maps to query strings (for metadata) and pre-compiled query filters.
* Within a subscription: AND logic (all queries must match)
* Across subscriptions: OR logic (any subscription matching = send event)
*/
@@ -73,25 +73,18 @@ class Realtime extends MessagingAdapter
$this->subscriptions[$projectId] = [];
}
// Convert Query objects to strings and store both for this subscription
$queryStrings = [];
$parsedQueries = [];
$strings = [];
if (empty($queryGroup)) {
// No queries means "listen to all events" - use select("*")
$selectAll = Query::select(['*']);
$queryStrings[] = $selectAll->toString();
$parsedQueries[] = $selectAll;
$strings[] = Query::select(['*'])->toString();
} else {
foreach ($queryGroup as $query) {
/** @var Query $query */
$queryStrings[] = $query->toString();
$parsedQueries[] = $query;
$strings[] = $query->toString();
}
}
$subscriptionData = [
'strings' => $queryStrings,
'parsed' => $parsedQueries,
$data = [
'strings' => $strings,
'compiled' => RuntimeQuery::compile($queryGroup),
];
foreach ($roles as $role) {
@@ -106,7 +99,7 @@ class Realtime extends MessagingAdapter
if (!isset($this->subscriptions[$projectId][$role][$channel][$identifier])) {
$this->subscriptions[$projectId][$role][$channel][$identifier] = [];
}
$this->subscriptions[$projectId][$role][$channel][$identifier][$subscriptionId] = $subscriptionData;
$this->subscriptions[$projectId][$role][$channel][$identifier][$subscriptionId] = $data;
}
}
@@ -148,15 +141,15 @@ class Realtime extends MessagingAdapter
continue;
}
foreach ($this->subscriptions[$projectId][$role][$channel][$connection] as $subId => $subscriptionData) {
if (!isset($subscriptions[$subId])) {
$subscriptions[$subId] = [
foreach ($this->subscriptions[$projectId][$role][$channel][$connection] as $subscriptionId => $data) {
if (!isset($subscriptions[$subscriptionId])) {
$subscriptions[$subscriptionId] = [
'channels' => [],
'queries' => $subscriptionData['strings'] ?? []
'queries' => $data['strings'] ?? []
];
}
if (!\in_array($channel, $subscriptions[$subId]['channels'])) {
$subscriptions[$subId]['channels'][] = $channel;
if (!\in_array($channel, $subscriptions[$subscriptionId]['channels'])) {
$subscriptions[$subscriptionId]['channels'][] = $channel;
}
}
}
@@ -259,12 +252,10 @@ class Realtime extends MessagingAdapter
* Identifies the receivers of all subscriptions, based on the permissions and event.
*
* Example of performance with an event with user:XXX permissions and with X users spread across 10 different channels:
* - 0.014 ms (±6.88%) | 10 Connections / 100 Subscriptions
* - 0.070 ms (±3.71%) | 100 Connections / 1,000 Subscriptions
* - 0.846 ms (±2.74%) | 1,000 Connections / 10,000 Subscriptions
* - 10.866 ms (±1.01%) | 10,000 Connections / 100,000 Subscriptions
* - 110.201 ms (±2.32%) | 100,000 Connections / 1,000,000 Subscriptions
* - 1,121.328 ms (±0.84%) | 1,000,000 Connections / 10,000,000 Subscriptions
* - 0.013 ms | 10 Connections / 100 Subscriptions
* - 0.14 ms | 100 Connections / 1,000 Subscriptions
* - 1.5 ms | 1,000 Connections / 10,000 Subscriptions
* - 15 ms | 10,000 Connections / 100,000 Subscriptions
*
* @param array $event
* @return array<int|string, array> Map of connection IDs to matched query groups
@@ -272,55 +263,39 @@ class Realtime extends MessagingAdapter
public function getSubscribers(array $event): array
{
$receivers = [];
/**
* Check if project has subscriber.
*/
if (isset($this->subscriptions[$event['project']])) {
/**
* Iterate through each role.
*/
foreach ($this->subscriptions[$event['project']] as $role => $subscription) {
/**
* Iterate through each channel.
*/
foreach ($event['data']['channels'] as $channel) {
/**
* Check if channel has subscriber. Also taking care of the role in the event and the wildcard role.
*/
if (
\array_key_exists($channel, $this->subscriptions[$event['project']][$role])
&& (\in_array($role, $event['roles']) || \in_array(Role::any()->toString(), $event['roles']))
) {
/**
* Saving all connections that are allowed to receive this event.
*/
$payload = $event['data']['payload'] ?? [];
foreach ($this->subscriptions[$event['project']][$role][$channel] as $id => $subscriptions) {
$matchedSubscriptions = [];
// Process each subscription (OR logic across subscriptions)
foreach ($subscriptions as $subId => $subscriptionData) {
// Use pre-parsed queries instead of re-parsing on every event
$parsedQueries = $subscriptionData['parsed'] ?? [];
$queryStrings = $subscriptionData['strings'] ?? [];
if (!isset($this->subscriptions[$event['project']])) {
return $receivers;
}
// Check if this subscription matches (AND logic within subscription)
// Or if empty payload and select all as filter will return empty payload out of it even if it passed
$isEmptyPayloadAndSelectAll = !empty($parsedQueries) && RuntimeQuery::isSelectAll($parsedQueries[0]) && empty($payload);
if ($isEmptyPayloadAndSelectAll || !empty(RuntimeQuery::filter($parsedQueries, $payload))) {
$matchedSubscriptions[$subId] = $queryStrings;
}
}
$payload = $event['data']['payload'] ?? [];
// Only add connection to receivers if at least one subscription matched
if (!empty($matchedSubscriptions)) {
if (!isset($receivers[$id])) {
$receivers[$id] = [];
}
$receivers[$id] += $matchedSubscriptions;
}
foreach ($this->subscriptions[$event['project']] as $role => $subscriptionsByChannel) {
foreach ($event['data']['channels'] as $channel) {
if (
!\array_key_exists($channel, $subscriptionsByChannel)
|| (!\in_array($role, $event['roles']) && !\in_array(Role::any()->toString(), $event['roles']))
) {
continue;
}
foreach ($subscriptionsByChannel[$channel] as $id => $subscriptions) {
$matched = [];
foreach ($subscriptions as $subscriptionId => $data) {
$compiled = $data['compiled'] ?? ['type' => 'selectAll'];
$strings = $data['strings'] ?? [];
if (RuntimeQuery::filter($compiled, $payload) !== null) {
$matched[$subscriptionId] = $strings;
}
break;
}
if (!empty($matched)) {
if (!isset($receivers[$id])) {
$receivers[$id] = [];
}
$receivers[$id] += $matched;
}
}
}
@@ -360,65 +335,50 @@ class Realtime extends MessagingAdapter
/**
* Constructs subscriptions from query parameters.
*
* Reconstructs subscription structure from query params where subscription indices can span multiple channels.
* Format: {channel}[subscriptionIndex][]=query1&{channel}[subscriptionIndex][]=query2
*
* Example:
* - tests[0][]=select(*) subscription 0: channels=["tests"]
* - tests[1][]=equal(...) & prod[1][]=equal(...) subscription 1: channels=["tests", "prod"]
*
* @param array $channelNames Array of channel names
* @param callable $getQueryParam Callable that takes a channel name and returns its query param value (null if not present)
* @return array Array indexed by subscription index: [index => ['channels' => string[], 'queries' => Query[]]]
* @param array $channelNames
* @param callable $getQueryParam
* @return array [index => ['channels' => string[], 'queries' => Query[]]]
* @throws QueryException
*/
public static function constructSubscriptions(array $channelNames, callable $getQueryParam): array
{
$subscriptionsByIndex = [];
$subscriptions = [];
foreach ($channelNames as $channel) {
$channelSubscriptions = $getQueryParam($channel);
$params = $getQueryParam(\str_replace('.', '_', $channel));
// Backward compatibility: if no channel-specific query params, treat as subscription 0 with select("*")
if ($channelSubscriptions === null) {
if (!isset($subscriptionsByIndex[0])) {
$subscriptionsByIndex[0] = [
'channels' => [],
'queries' => []
];
if ($params === null) {
if (!isset($subscriptions[0])) {
$subscriptions[0] = ['channels' => [], 'queries' => []];
}
$subscriptionsByIndex[0]['channels'][] = $channel;
if (empty($subscriptionsByIndex[0]['queries'])) {
$subscriptionsByIndex[0]['queries'] = [Query::select(['*'])];
$subscriptions[0]['channels'][] = $channel;
if (empty($subscriptions[0]['queries'])) {
$subscriptions[0]['queries'] = [Query::select(['*'])];
}
continue;
}
if (!is_array($channelSubscriptions)) {
$channelSubscriptions = [$channelSubscriptions];
if (!is_array($params)) {
$params = [$params];
}
foreach ($channelSubscriptions as $subscriptionIndex => $subscription) {
if (!isset($subscriptionsByIndex[$subscriptionIndex])) {
$subscriptionsByIndex[$subscriptionIndex] = [
'channels' => [],
'queries' => []
];
foreach ($params as $index => $slot) {
if (!isset($subscriptions[$index])) {
$subscriptions[$index] = ['channels' => [], 'queries' => []];
}
if (!in_array($channel, $subscriptionsByIndex[$subscriptionIndex]['channels'])) {
$subscriptionsByIndex[$subscriptionIndex]['channels'][] = $channel;
if (!in_array($channel, $subscriptions[$index]['channels'])) {
$subscriptions[$index]['channels'][] = $channel;
}
if (empty($subscriptionsByIndex[$subscriptionIndex]['queries'])) {
$queriesToParse = is_array($subscription) ? $subscription : [$subscription];
$parsedQueries = self::convertQueries($queriesToParse);
$subscriptionsByIndex[$subscriptionIndex]['queries'] = $parsedQueries;
if (empty($subscriptions[$index]['queries'])) {
$raw = is_array($slot) ? $slot : [$slot];
$subscriptions[$index]['queries'] = self::convertQueries($raw);
}
}
}
return $subscriptionsByIndex;
return $subscriptions;
}
/**
@@ -431,19 +391,18 @@ class Realtime extends MessagingAdapter
{
$queries = Query::parseQueries($queries);
$stack = $queries;
$allowedMethods = implode(', ', RuntimeQuery::ALLOWED_QUERIES);
$allowed = implode(', ', RuntimeQuery::ALLOWED_QUERIES);
while (!empty($stack)) {
/** @var Query $query */
$query = array_pop($stack);
$method = $query->getMethod();
if (!in_array($method, RuntimeQuery::ALLOWED_QUERIES, true)) {
$unsupportedMethod = $method;
throw new QueryException(
"Query method '{$unsupportedMethod}' is not supported in Realtime queries. Allowed query methods are: {$allowedMethods}"
"Query method '{$method}' is not supported in Realtime queries. Allowed: {$allowed}"
);
}
// Validate select queries - only select("*") is allowed
if ($method === Query::TYPE_SELECT) {
RuntimeQuery::validateSelectQuery($query);
}
+21
View File
@@ -22,6 +22,27 @@ class Origin extends Validator
{
}
public function setAllowedHostnames(array $allowedHostnames): self
{
$this->allowedHostnames = $allowedHostnames;
return $this;
}
public function setAllowedSchemes(array $allowedSchemes): self
{
$this->allowedSchemes = $allowedSchemes;
return $this;
}
public function getAllowedHostnames(): array
{
return $this->allowedHostnames;
}
public function getAllowedSchemes(): array
{
return $this->allowedSchemes;
}
/**
* Check if Origin is valid.
@@ -109,6 +109,12 @@ class Base extends Action
} catch (\Throwable $error) {
// Ignore; deployment can continue
}
} else {
// Fallback till we have tag support here
// Goal is to set providerBranch, so build worker knows what to clone as base
// Without this, clone command would be cloning empty branch, and failing
$providerBranch = $function->getAttribute('providerBranch', 'main');
$branchUrl = "https://github.com/$owner/$repositoryName/tree/$providerBranch";
}
$repositoryUrl = "https://github.com/$owner/$repositoryName";
@@ -199,6 +205,12 @@ class Base extends Action
} catch (\Throwable $error) {
// Ignore; deployment can continue
}
} else {
// Fallback till we have tag support here
// Goal is to set providerBranch, so build worker knows what to clone as base
// Without this, clone command would be cloning empty branch, and failing
$providerBranch = $site->getAttribute('providerBranch', 'main');
$branchUrl = "https://github.com/$owner/$repositoryName/tree/$providerBranch";
}
$repositoryUrl = "https://github.com/$owner/$repositoryName";
@@ -3,6 +3,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections;
use Appwrite\Extend\Exception;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Platform\Action as UtopiaAction;
use Utopia\Platform\Scope\HTTP;
@@ -165,4 +167,15 @@ abstract class Action extends UtopiaAction
? Exception::ATTRIBUTE_TYPE_INVALID
: Exception::COLUMN_TYPE_INVALID;
}
/**
* Add row bytes information to a collection/table document.
*/
protected function addRowBytesInfo(Document $document, Database $dbForProject): Document
{
$adapter = $dbForProject->getAdapter();
$document->setAttribute('bytesMax', $adapter->getDocumentSizeLimit());
$document->setAttribute('bytesUsed', $adapter->getAttributeWidth($document));
return $document;
}
}
@@ -248,6 +248,8 @@ class Create extends Action
->setParam('databaseId', $databaseId)
->setParam($this->getEventsParamKey(), $collection->getId());
$this->addRowBytesInfo($collection, $dbForProject);
$response
->setStatusCode(SwooleResponse::STATUS_CODE_CREATED)
->dynamic($collection, $this->getResponseModel());
@@ -99,16 +99,10 @@ class XList extends Action
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 = Query::getCursorQueries($queries, false);
$cursor = \reset($cursor);
if ($cursor) {
if ($cursor !== false) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
@@ -75,6 +75,8 @@ class Get extends Action
throw new Exception($this->getNotFoundException(), params: [$collectionId]);
}
$this->addRowBytesInfo($collection, $dbForProject);
$response->dynamic($collection, $this->getResponseModel());
}
}
@@ -98,15 +98,10 @@ class XList extends Action
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);
$cursor = Query::getCursorQueries($queries, false);
$cursor = \reset($cursor);
if ($cursor) {
if ($cursor !== false) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
@@ -117,6 +117,8 @@ class Update extends Action
->setParam('databaseId', $databaseId)
->setParam($this->getEventsParamKey(), $collection->getId());
$this->addRowBytesInfo($collection, $dbForProject);
$response->dynamic($collection, $this->getResponseModel());
}
}
@@ -89,15 +89,10 @@ class XList extends Action
$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);
$cursor = Query::getCursorQueries($queries, false);
$cursor = \reset($cursor);
if ($cursor) {
if ($cursor !== false) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
@@ -122,6 +117,10 @@ class XList extends Action
throw new Exception(Exception::GENERAL_QUERY_INVALID);
}
foreach ($collections as $collection) {
$this->addRowBytesInfo($collection, $dbForProject);
}
$response->dynamic(new Document([
'total' => $total,
$this->getSDKGroup() => $collections,
@@ -73,16 +73,10 @@ class XList extends Action
$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) {
/** @var Query $cursor */
$cursor = Query::getCursorQueries($queries, false);
$cursor = \reset($cursor);
if ($cursor !== false) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
@@ -95,16 +95,10 @@ class XList extends Base
$queries[] = Query::equal('resourceInternalId', [$function->getSequence()]);
$queries[] = Query::equal('resourceType', ['functions']);
/**
* 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) {
/** @var Query $cursor */
$cursor = Query::getCursorQueries($queries, false);
$cursor = \reset($cursor);
if ($cursor !== false) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
@@ -301,7 +301,9 @@ class Create extends Base
if ($async) {
if (is_null($scheduledAt)) {
$execution = $authorization->skip(fn () => $dbForProject->createDocument('executions', $execution));
if (System::getEnv('_APP_REGION') !== 'nyc') { // TODO: Remove region check
$execution = $authorization->skip(fn () => $dbForProject->createDocument('executions', $execution));
}
$queueForFunctions
->setType('http')
->setExecution($execution)
@@ -342,7 +344,9 @@ class Create extends Base
->setAttribute('scheduleInternalId', $schedule->getSequence())
->setAttribute('scheduledAt', $scheduledAt);
$execution = $authorization->skip(fn () => $dbForProject->createDocument('executions', $execution));
if (System::getEnv('_APP_REGION') !== 'nyc') { // TODO: Remove region check
$execution = $authorization->skip(fn () => $dbForProject->createDocument('executions', $execution));
}
}
$this->enqueueDeletes(
@@ -501,7 +505,9 @@ class Create extends Base
->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [RESOURCE_TYPE_FUNCTIONS, $function->getSequence()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS_MB_SECONDS), (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT)))
;
$execution = $authorization->skip(fn () => $dbForProject->createDocument('executions', $execution));
if (System::getEnv('_APP_REGION') !== 'nyc') { // TODO: Remove region check
$execution = $authorization->skip(fn () => $dbForProject->createDocument('executions', $execution));
}
}
$executionResponse['headers']['x-appwrite-execution-id'] = $execution->getId();
@@ -91,16 +91,10 @@ class XList extends Base
$queries[] = Query::equal('resourceInternalId', [$function->getSequence()]);
$queries[] = Query::equal('resourceType', ['functions']);
/**
* 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) {
/** @var Query $cursor */
$cursor = Query::getCursorQueries($queries, false);
$cursor = \reset($cursor);
if ($cursor !== false) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
@@ -78,16 +78,10 @@ class XList extends Base
$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) {
/** @var Query $cursor */
$cursor = Query::getCursorQueries($queries, false);
$cursor = \reset($cursor);
if ($cursor !== false) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
@@ -86,16 +86,10 @@ class XList extends Action
$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) {
/** @var Query $cursor */
$cursor = Query::getCursorQueries($queries, false);
$cursor = \reset($cursor);
if ($cursor !== false) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
@@ -81,16 +81,10 @@ class XList extends Action
$queries[] = Query::equal('projectInternalId', [$project->getSequence()]);
/**
* 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) {
/** @var Query $cursor */
$cursor = Query::getCursorQueries($queries, false);
$cursor = \reset($cursor);
if ($cursor !== false) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
@@ -95,16 +95,10 @@ class XList extends Base
$queries[] = Query::equal('resourceInternalId', [$site->getSequence()]);
$queries[] = Query::equal('resourceType', ['sites']);
/**
* 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) {
/** @var Query $cursor */
$cursor = Query::getCursorQueries($queries, false);
$cursor = \reset($cursor);
if ($cursor !== false) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
@@ -80,16 +80,10 @@ class XList extends Base
$queries[] = Query::equal('resourceInternalId', [$site->getSequence()]);
$queries[] = Query::equal('resourceType', ['sites']);
/**
* 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) {
/** @var Query $cursor */
$cursor = Query::getCursorQueries($queries, false);
$cursor = \reset($cursor);
if ($cursor !== false) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
@@ -73,16 +73,10 @@ class XList extends Base
$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) {
/** @var Query $cursor */
$cursor = Query::getCursorQueries($queries, false);
$cursor = \reset($cursor);
if ($cursor !== false) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
@@ -101,16 +101,10 @@ class XList extends Action
$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) {
/** @var Query $cursor */
$cursor = Query::getCursorQueries($queries, false);
$cursor = \reset($cursor);
if ($cursor !== false) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
@@ -82,16 +82,10 @@ class XList extends Action
$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) {
/** @var Query $cursor */
$cursor = Query::getCursorQueries($queries, false);
$cursor = \reset($cursor);
if ($cursor !== false) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
@@ -2,18 +2,18 @@
namespace Appwrite\Platform\Modules\Tokens\Http\Tokens\Buckets\Files;
use Appwrite\Extend\Exception as ExtendException;
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\FileTokens;
use Appwrite\Utopia\Response;
use Exception;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
@@ -69,18 +69,21 @@ class XList extends Action
$queries = Query::parseQueries($queries);
$queries[] = Query::equal('resourceType', [TOKENS_RESOURCE_TYPE_FILES]);
$queries[] = Query::equal('resourceInternalId', [$bucket->getSequence() . ':' . $file->getSequence()]);
// Get cursor document if there was a cursor query
$cursor = \array_filter($queries, function ($query) {
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
});
$cursor = reset($cursor);
if ($cursor) {
/** @var Query $cursor */
$cursor = Query::getCursorQueries($queries, false);
$cursor = \reset($cursor);
if ($cursor !== false) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
}
$tokenId = $cursor->getValue();
$cursorDocument = $dbForProject->getDocument('resourceTokens', $tokenId);
if ($cursorDocument->isEmpty()) {
throw new Exception(ExtendException::GENERAL_CURSOR_NOT_FOUND, "File token '{$tokenId}' for the 'cursor' value not found.");
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "File token '{$tokenId}' for the 'cursor' value not found.");
}
$cursor->setValue($cursorDocument);
@@ -0,0 +1,90 @@
<?php
namespace Appwrite\Platform\Modules\VCS\Http\GitHub\Authorize;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\MethodType;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Document;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
class Get extends Action
{
use HTTP;
public static function getName()
{
return 'getVCSGitHubAuthorize';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/vcs/github/authorize')
->desc('Create GitHub app installation')
->groups(['api', 'vcs'])
->label('scope', 'vcs.read')
->label('error', __DIR__ . '/../../views/general/error.phtml')
->label('sdk', new Method(
namespace: 'vcs',
group: 'installations',
name: 'createGitHubInstallation',
description: '/docs/references/vcs/create-github-installation.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_MOVED_PERMANENTLY,
model: Response::MODEL_NONE,
)
],
contentType: ContentType::HTML,
type: MethodType::WEBAUTH,
hide: true,
))
->param('success', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect back to console after a successful installation attempt.', true, ['redirectValidator'])
->param('failure', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect back to console after a failed installation attempt.', true, ['redirectValidator'])
->inject('response')
->inject('project')
->inject('platform')
->callback($this->action(...));
}
public function action(
string $success,
string $failure,
Response $response,
Document $project,
array $platform
) {
$state = \json_encode([
'projectId' => $project->getId(),
'success' => $success,
'failure' => $failure,
]);
$appName = System::getEnv('_APP_VCS_GITHUB_APP_NAME');
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$hostname = $platform['consoleHostname'] ?? '';
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.');
}
$url = "https://github.com/apps/$appName/installations/new?" . \http_build_query([
'state' => $state,
'redirect_uri' => $protocol . '://' . $hostname . "/v1/vcs/github/callback"
]);
$response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
->redirect($url);
}
}
@@ -0,0 +1,182 @@
<?php
namespace Appwrite\Platform\Modules\VCS\Http\GitHub\Callback;
use Appwrite\Auth\OAuth2\Github as OAuth2Github;
use Appwrite\Extend\Exception;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
use Utopia\Validator\Text;
use Utopia\VCS\Adapter\Git\GitHub;
class Get extends Action
{
use HTTP;
public static function getName()
{
return 'getVCSGitHubCallback';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/vcs/github/callback')
->desc('Get installation and authorization from GitHub app')
->groups(['api', 'vcs'])
->label('scope', 'public')
->label('error', __DIR__ . '/../../views/general/error.phtml')
->param('installation_id', '', new Text(256, 0), 'GitHub installation ID', true)
->param('setup_action', '', new Text(256, 0), 'GitHub setup action type', true)
->param('state', '', new Text(2048), 'GitHub state. Contains info sent when starting authorization flow.', true)
->param('code', '', new Text(2048, 0), 'OAuth2 code. This is a temporary code that the will be later exchanged for an access token.', true)
->inject('gitHub')
->inject('project')
->inject('response')
->inject('dbForPlatform')
->inject('platform')
->callback($this->action(...));
}
public function action(
string $providerInstallationId,
string $setupAction,
string $state,
string $code,
GitHub $github,
Document $project,
Response $response,
Database $dbForPlatform,
array $platform
) {
if (empty($state)) {
$error = 'Installation requests from organisation members for the Appwrite GitHub App are currently unsupported. To proceed with the installation, login to the Appwrite Console and install the GitHub App.';
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, $error);
}
$state = \json_decode($state, true);
$projectId = $state['projectId'] ?? '';
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
$error = 'Project with the ID from state could not be found.';
if (!empty($redirectFailure)) {
$separator = \str_contains($redirectFailure, '?') ? '&' : ':';
return $response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
->redirect($redirectFailure . $separator . \http_build_query(['error' => $error]));
}
throw new Exception(Exception::PROJECT_NOT_FOUND, $error);
}
$region = $project->getAttribute('region', 'default');
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$hostname = $platform['consoleHostname'] ?? '';
$defaultState = [
'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 ?? []);
$redirectSuccess = $state['success'] ?? '';
$redirectFailure = $state['failure'] ?? '';
// Create / Update installation
if (!empty($providerInstallationId)) {
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
$owner = $github->getOwnerName($providerInstallationId) ?? '';
$projectInternalId = $project->getSequence();
$installation = $dbForPlatform->findOne('installations', [
Query::equal('providerInstallationId', [$providerInstallationId]),
Query::equal('projectInternalId', [$projectInternalId])
]);
$personal = false;
$refreshToken = null;
$accessToken = null;
$accessTokenExpiry = null;
if (!empty($code)) {
$oauth2 = new OAuth2Github(System::getEnv('_APP_VCS_GITHUB_CLIENT_ID', ''), System::getEnv('_APP_VCS_GITHUB_CLIENT_SECRET', ''), "");
$accessToken = $oauth2->getAccessToken($code) ?? '';
$refreshToken = $oauth2->getRefreshToken($code) ?? '';
$accessTokenExpiry = DateTime::addSeconds(new \DateTime(), \intval($oauth2->getAccessTokenExpiry($code)));
$personalSlug = $oauth2->getUserSlug($accessToken) ?? '';
$personal = $personalSlug === $owner;
}
if ($installation->isEmpty()) {
$teamId = $project->getAttribute('teamId', '');
$installation = new Document([
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::team(ID::custom($teamId))),
Permission::update(Role::team(ID::custom($teamId), 'owner')),
Permission::update(Role::team(ID::custom($teamId), 'developer')),
Permission::delete(Role::team(ID::custom($teamId), 'owner')),
Permission::delete(Role::team(ID::custom($teamId), 'developer')),
],
'providerInstallationId' => $providerInstallationId,
'projectId' => $projectId,
'projectInternalId' => $projectInternalId,
'provider' => 'github',
'organization' => $owner,
'personal' => $personal,
'personalRefreshToken' => $refreshToken,
'personalAccessToken' => $accessToken,
'personalAccessTokenExpiry' => $accessTokenExpiry,
]);
$installation = $dbForPlatform->createDocument('installations', $installation);
} else {
$installation = $installation
->setAttribute('organization', $owner)
->setAttribute('personal', $personal)
->setAttribute('personalRefreshToken', $refreshToken)
->setAttribute('personalAccessToken', $accessToken)
->setAttribute('personalAccessTokenExpiry', $accessTokenExpiry);
$installation = $dbForPlatform->updateDocument('installations', $installation->getId(), $installation);
}
} else {
$error = 'Installation of the Appwrite GitHub App on organization accounts is restricted to organization owners. As a member of the organization, you do not have the necessary permissions to install this GitHub App. Please contact the organization owner to create the installation from the Appwrite console.';
if (!empty($redirectFailure)) {
$separator = \str_contains($redirectFailure, '?') ? '&' : ':';
return $response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
->redirect($redirectFailure . $separator . \http_build_query(['error' => $error]));
}
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, $error);
}
$response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
->redirect($redirectSuccess);
}
}
@@ -0,0 +1,95 @@
<?php
namespace Appwrite\Platform\Modules\VCS\Http\Installations\Repositories\Branches;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
use Utopia\Validator\Text;
use Utopia\VCS\Adapter\Git\GitHub;
use Utopia\VCS\Exception\RepositoryNotFound;
class XList extends Action
{
use HTTP;
public static function getName()
{
return 'listRepositoryBranches';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/branches')
->desc('List repository branches')
->groups(['api', 'vcs'])
->label('scope', 'vcs.read')
->label('resourceType', RESOURCE_TYPE_VCS)
->label('sdk', new Method(
namespace: 'vcs',
group: 'repositories',
name: 'listRepositoryBranches',
description: '/docs/references/vcs/list-repository-branches.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_BRANCH_LIST,
)
]
))
->param('installationId', '', new Text(256), 'Installation Id')
->param('providerRepositoryId', '', new Text(256), 'Repository Id')
->inject('gitHub')
->inject('response')
->inject('dbForPlatform')
->callback($this->action(...));
}
public function action(
string $installationId,
string $providerRepositoryId,
GitHub $github,
Response $response,
Database $dbForPlatform
) {
$installation = $dbForPlatform->getDocument('installations', $installationId);
if ($installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
}
$providerInstallationId = $installation->getAttribute('providerInstallationId');
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
$owner = $github->getOwnerName($providerInstallationId) ?? '';
try {
$repositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
if (empty($repositoryName)) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
} catch (RepositoryNotFound $e) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
$branches = $github->listBranches($owner, $repositoryName) ?? [];
$response->dynamic(new Document([
'branches' => \array_map(function ($branch) {
return new Document(['name' => $branch]);
}, $branches),
'total' => \count($branches),
]), Response::MODEL_BRANCH_LIST);
}
}
@@ -0,0 +1,110 @@
<?php
namespace Appwrite\Platform\Modules\VCS\Http\Installations\Repositories\Contents;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
use Utopia\Validator\Text;
use Utopia\VCS\Adapter\Git\GitHub;
use Utopia\VCS\Exception\RepositoryNotFound;
class Get extends Action
{
use HTTP;
public static function getName()
{
return 'getRepositoryContents';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/contents')
->desc('Get files and directories of a VCS repository')
->groups(['api', 'vcs'])
->label('scope', 'vcs.read')
->label('resourceType', RESOURCE_TYPE_VCS)
->label('sdk', new Method(
namespace: 'vcs',
group: 'repositories',
name: 'getRepositoryContents',
description: '/docs/references/vcs/get-repository-contents.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_VCS_CONTENT_LIST,
)
]
))
->param('installationId', '', new Text(256), 'Installation Id')
->param('providerRepositoryId', '', new Text(256), 'Repository Id')
->param('providerRootDirectory', '', new Text(256, 0), 'Path to get contents of nested directory', true)
->param('providerReference', '', new Text(256, 0), 'Git reference (branch, tag, commit) to get contents from', true)
->inject('gitHub')
->inject('response')
->inject('dbForPlatform')
->callback($this->action(...));
}
public function action(
string $installationId,
string $providerRepositoryId,
string $providerRootDirectory,
string $providerReference,
GitHub $github,
Response $response,
Database $dbForPlatform
) {
$installation = $dbForPlatform->getDocument('installations', $installationId);
if ($installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
}
$providerInstallationId = $installation->getAttribute('providerInstallationId');
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
$owner = $github->getOwnerName($providerInstallationId);
try {
$repositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
if (empty($repositoryName)) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
} catch (RepositoryNotFound $e) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
$contents = $github->listRepositoryContents($owner, $repositoryName, $providerRootDirectory, $providerReference);
$vcsContents = [];
foreach ($contents as $content) {
$isDirectory = false;
if ($content['type'] === GitHub::CONTENTS_DIRECTORY) {
$isDirectory = true;
}
$vcsContents[] = new Document([
'isDirectory' => $isDirectory,
'name' => $content['name'] ?? '',
'size' => $content['size'] ?? 0
]);
}
$response->dynamic(new Document([
'contents' => $vcsContents
]), Response::MODEL_VCS_CONTENT_LIST);
}
}
@@ -0,0 +1,158 @@
<?php
namespace Appwrite\Platform\Modules\VCS\Http\Installations\Repositories;
use Appwrite\Auth\OAuth2\Github as OAuth2Github;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
use Utopia\VCS\Adapter\Git\GitHub;
class Create extends Action
{
use HTTP;
public static function getName()
{
return 'createRepository';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/vcs/github/installations/:installationId/providerRepositories')
->desc('Create repository')
->groups(['api', 'vcs'])
->label('scope', 'vcs.write')
->label('resourceType', RESOURCE_TYPE_VCS)
->label('sdk', new Method(
namespace: 'vcs',
group: 'repositories',
name: 'createRepository',
description: '/docs/references/vcs/create-repository.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROVIDER_REPOSITORY,
)
]
))
->param('installationId', '', new Text(256), 'Installation Id')
->param('name', '', new Text(256), 'Repository name (slug)')
->param('private', '', new Boolean(false), 'Mark repository public or private')
->inject('gitHub')
->inject('user')
->inject('response')
->inject('dbForPlatform')
->callback($this->action(...));
}
public function action(
string $installationId,
string $name,
bool $private,
GitHub $github,
Document $user,
Response $response,
Database $dbForPlatform
) {
$installation = $dbForPlatform->getDocument('installations', $installationId);
if ($installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
}
if ($installation->getAttribute('personal', false) === true) {
$oauth2 = new OAuth2Github(System::getEnv('_APP_VCS_GITHUB_CLIENT_ID', ''), System::getEnv('_APP_VCS_GITHUB_CLIENT_SECRET', ''), "");
$accessToken = $installation->getAttribute('personalAccessToken');
$refreshToken = $installation->getAttribute('personalRefreshToken');
$accessTokenExpiry = $installation->getAttribute('personalAccessTokenExpiry');
if (empty($accessToken) || empty($refreshToken) || empty($accessTokenExpiry)) {
$identity = $dbForPlatform->findOne('identities', [
Query::equal('provider', ['github']),
Query::equal('userInternalId', [$user->getSequence()]),
]);
if ($identity->isEmpty()) {
throw new Exception(Exception::USER_IDENTITY_NOT_FOUND);
}
$accessToken = $accessToken ?? $identity->getAttribute('providerAccessToken');
$refreshToken = $refreshToken ?? $identity->getAttribute('providerRefreshToken');
$accessTokenExpiry = $accessTokenExpiry ?? $identity->getAttribute('providerAccessTokenExpiry');
}
$isExpired = new \DateTime($accessTokenExpiry) < new \DateTime('now');
if ($isExpired) {
$oauth2->refreshTokens($refreshToken);
$accessToken = $oauth2->getAccessToken('');
$refreshToken = $oauth2->getRefreshToken('');
$verificationId = $oauth2->getUserID($accessToken);
if (empty($verificationId)) {
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, "Another request is currently refreshing OAuth token. Please try again.");
}
$installation = $installation
->setAttribute('personalAccessToken', $accessToken)
->setAttribute('personalRefreshToken', $refreshToken)
->setAttribute('personalAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$oauth2->getAccessTokenExpiry('')));
$dbForPlatform->updateDocument('installations', $installation->getId(), $installation);
}
try {
$repository = $oauth2->createRepository($accessToken, $name, $private);
} catch (Exception $exception) {
throw new Exception(Exception::GENERAL_PROVIDER_FAILURE, "GitHub failed to process the request: " . $exception->getMessage());
}
} else {
$providerInstallationId = $installation->getAttribute('providerInstallationId');
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
$owner = $github->getOwnerName($providerInstallationId);
try {
$repository = $github->createRepository($owner, $name, $private);
} catch (Exception $exception) {
throw new Exception(Exception::GENERAL_PROVIDER_FAILURE, "GitHub failed to process the request: " . $exception->getMessage());
}
}
if (isset($repository['errors'])) {
$message = $repository['message'] ?? 'Unknown error.';
if (isset($repository['errors'][0])) {
$message .= ' ' . $repository['errors'][0]['message'];
}
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Provider Error: ' . $message);
}
if (isset($repository['message'])) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Provider Error: ' . $repository['message']);
}
$repository['id'] = \strval($repository['id']) ?? '';
$repository['pushedAt'] = $repository['pushed_at'] ?? '';
$repository['organization'] = $installation->getAttribute('organization', '');
$repository['provider'] = $installation->getAttribute('provider', '');
$response->dynamic(new Document($repository), Response::MODEL_PROVIDER_REPOSITORY);
}
}
@@ -0,0 +1,315 @@
<?php
namespace Appwrite\Platform\Modules\VCS\Http\Installations\Repositories\Detections;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Swoole\Coroutine\WaitGroup;
use Utopia\Config\Adapters\Dotenv as ConfigDotenv;
use Utopia\Config\Config;
use Utopia\Config\Exceptions\Parse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Detector\Detection\Framework\Analog;
use Utopia\Detector\Detection\Framework\Angular;
use Utopia\Detector\Detection\Framework\Astro;
use Utopia\Detector\Detection\Framework\Flutter;
use Utopia\Detector\Detection\Framework\Lynx;
use Utopia\Detector\Detection\Framework\NextJs;
use Utopia\Detector\Detection\Framework\Nuxt;
use Utopia\Detector\Detection\Framework\React;
use Utopia\Detector\Detection\Framework\ReactNative;
use Utopia\Detector\Detection\Framework\Remix;
use Utopia\Detector\Detection\Framework\Svelte;
use Utopia\Detector\Detection\Framework\SvelteKit;
use Utopia\Detector\Detection\Framework\TanStackStart;
use Utopia\Detector\Detection\Framework\Vue;
use Utopia\Detector\Detection\Packager\NPM;
use Utopia\Detector\Detection\Packager\PNPM;
use Utopia\Detector\Detection\Packager\Yarn;
use Utopia\Detector\Detection\Runtime\Bun;
use Utopia\Detector\Detection\Runtime\CPP;
use Utopia\Detector\Detection\Runtime\Dart;
use Utopia\Detector\Detection\Runtime\Deno;
use Utopia\Detector\Detection\Runtime\Dotnet;
use Utopia\Detector\Detection\Runtime\Java;
use Utopia\Detector\Detection\Runtime\Node;
use Utopia\Detector\Detection\Runtime\PHP;
use Utopia\Detector\Detection\Runtime\Python;
use Utopia\Detector\Detection\Runtime\Ruby;
use Utopia\Detector\Detection\Runtime\Swift;
use Utopia\Detector\Detector\Framework;
use Utopia\Detector\Detector\Packager;
use Utopia\Detector\Detector\Runtime;
use Utopia\Detector\Detector\Strategy;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
use Utopia\VCS\Adapter\Git\GitHub;
use Utopia\VCS\Exception\FileNotFound;
use Utopia\VCS\Exception\RepositoryNotFound;
class Create extends Action
{
use HTTP;
public static function getName()
{
return 'createRepositoryDetection';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/vcs/github/installations/:installationId/detections')
->httpAlias('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/detection')
->desc('Create repository detection')
->groups(['api', 'vcs'])
->label('scope', 'vcs.write')
->label('resourceType', RESOURCE_TYPE_VCS)
->label('sdk', new Method(
namespace: 'vcs',
group: 'repositories',
name: 'createRepositoryDetection',
description: '/docs/references/vcs/create-repository-detection.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_DETECTION_RUNTIME,
),
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_DETECTION_FRAMEWORK,
)
]
))
->param('installationId', '', new Text(256), 'Installation Id')
->param('providerRepositoryId', '', new Text(256), 'Repository Id')
->param('type', '', new WhiteList(['runtime', 'framework']), 'Detector type. Must be one of the following: runtime, framework')
->param('providerRootDirectory', '', new Text(256, 0), 'Path to Root Directory', true)
->inject('gitHub')
->inject('response')
->inject('dbForPlatform')
->callback($this->action(...));
}
public function action(
string $installationId,
string $providerRepositoryId,
string $type,
string $providerRootDirectory,
GitHub $github,
Response $response,
Database $dbForPlatform
) {
$installation = $dbForPlatform->getDocument('installations', $installationId);
if ($installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
}
$providerInstallationId = $installation->getAttribute('providerInstallationId');
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
$owner = $github->getOwnerName($providerInstallationId);
try {
$repositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
if (empty($repositoryName)) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
} catch (RepositoryNotFound $e) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
$files = $github->listRepositoryContents($owner, $repositoryName, $providerRootDirectory);
$files = \array_column($files, 'name');
$languages = $github->listRepositoryLanguages($owner, $repositoryName);
$detector = new Packager();
foreach ($files as $file) {
$detector->addInput($file);
}
$detector
->addOption(new Yarn())
->addOption(new PNPM())
->addOption(new NPM());
$detection = $detector->detect();
$packager = !\is_null($detection) ? $detection->getName() : 'npm';
if ($type === 'framework') {
$packages = '';
try {
$contentResponse = $github->getRepositoryContent($owner, $repositoryName, \rtrim($providerRootDirectory, '/') . '/package.json');
$packages = $contentResponse['content'] ?? '';
} catch (FileNotFound $e) {
// Continue detection without package.json
}
$output = new Document([
'framework' => '',
'installCommand' => '',
'buildCommand' => '',
'outputDirectory' => '',
]);
$detector = new Framework($packager);
$detector->addInput($packages, Framework::INPUT_PACKAGES);
foreach ($files as $file) {
$detector->addInput($file, Framework::INPUT_FILE);
}
$detector
->addOption(new Analog())
->addOption(new Angular())
->addOption(new Astro())
->addOption(new Flutter())
->addOption(new Lynx())
->addOption(new NextJs())
->addOption(new Nuxt())
->addOption(new React())
->addOption(new ReactNative())
->addOption(new Remix())
->addOption(new Svelte())
->addOption(new SvelteKit())
->addOption(new TanStackStart())
->addOption(new Vue());
$framework = $detector->detect();
if (!\is_null($framework)) {
$output->setAttribute('installCommand', $framework->getInstallCommand());
$output->setAttribute('buildCommand', $framework->getBuildCommand());
$output->setAttribute('outputDirectory', $framework->getOutputDirectory());
$framework = $framework->getName();
} else {
$framework = 'other';
$output->setAttribute('installCommand', '');
$output->setAttribute('buildCommand', '');
$output->setAttribute('outputDirectory', '');
}
$frameworks = Config::getParam('frameworks');
if (!\in_array($framework, \array_keys($frameworks), true)) {
$framework = 'other';
}
$output->setAttribute('framework', $framework);
} else {
$output = new Document([
'runtime' => '',
'commands' => '',
'entrypoint' => '',
]);
$strategies = [
new Strategy(Strategy::FILEMATCH),
new Strategy(Strategy::LANGUAGES),
new Strategy(Strategy::EXTENSION),
];
foreach ($strategies as $strategy) {
$detector = new Runtime($strategy, $packager);
if ($strategy->getValue() === Strategy::LANGUAGES) {
foreach ($languages as $language) {
$detector->addInput($language);
}
} else {
foreach ($files as $file) {
$detector->addInput($file);
}
}
$detector
->addOption(new Node())
->addOption(new Bun())
->addOption(new Deno())
->addOption(new PHP())
->addOption(new Python())
->addOption(new Dart())
->addOption(new Swift())
->addOption(new Ruby())
->addOption(new Java())
->addOption(new CPP())
->addOption(new Dotnet());
$runtime = $detector->detect();
if (!\is_null($runtime)) {
$output->setAttribute('commands', $runtime->getCommands());
$output->setAttribute('entrypoint', $runtime->getEntrypoint());
$runtime = $runtime->getName();
break;
}
}
if (!empty($runtime)) {
$runtimes = Config::getParam('runtimes');
$runtimeWithVersion = '';
foreach ($runtimes as $runtimeKey => $runtimeConfig) {
if ($runtimeConfig['key'] === $runtime) {
$runtimeWithVersion = $runtimeKey;
}
}
if (empty($runtimeWithVersion)) {
throw new Exception(Exception::FUNCTION_RUNTIME_NOT_DETECTED);
}
$output->setAttribute('runtime', $runtimeWithVersion);
} else {
throw new Exception(Exception::FUNCTION_RUNTIME_NOT_DETECTED);
}
}
$wg = new WaitGroup();
$envs = [];
foreach ($files as $file) {
if (!(\str_starts_with($file, '.env'))) {
continue;
}
$wg->add();
go(function () use ($github, $owner, $repositoryName, $providerRootDirectory, $file, $wg, &$envs) {
try {
$contentResponse = $github->getRepositoryContent($owner, $repositoryName, \rtrim($providerRootDirectory, '/') . '/' . $file);
$envFile = $contentResponse['content'] ?? '';
$configAdapter = new ConfigDotenv();
try {
$envObject = $configAdapter->parse($envFile);
foreach ($envObject as $envName => $envValue) {
$envs[$envName] = $envValue;
}
} catch (Parse $err) {
// Silence error, so rest of endpoint can return
}
} finally {
$wg->done();
}
});
}
$wg->wait();
$variables = [];
foreach ($envs as $key => $value) {
$variables[] = [
'name' => $key,
'value' => $value,
];
}
$output->setAttribute('variables', $variables);
$response->dynamic($output, $type === 'framework' ? Response::MODEL_DETECTION_FRAMEWORK : Response::MODEL_DETECTION_RUNTIME);
}
}
@@ -0,0 +1,96 @@
<?php
namespace Appwrite\Platform\Modules\VCS\Http\Installations\Repositories;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
use Utopia\Validator\Text;
use Utopia\VCS\Adapter\Git\GitHub;
use Utopia\VCS\Exception\RepositoryNotFound;
class Get extends Action
{
use HTTP;
public static function getName()
{
return 'getRepository';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId')
->desc('Get repository')
->groups(['api', 'vcs'])
->label('scope', 'vcs.read')
->label('resourceType', RESOURCE_TYPE_VCS)
->label('sdk', new Method(
namespace: 'vcs',
group: 'repositories',
name: 'getRepository',
description: '/docs/references/vcs/get-repository.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROVIDER_REPOSITORY,
)
]
))
->param('installationId', '', new Text(256), 'Installation Id')
->param('providerRepositoryId', '', new Text(256), 'Repository Id')
->inject('gitHub')
->inject('response')
->inject('dbForPlatform')
->callback($this->action(...));
}
public function action(
string $installationId,
string $providerRepositoryId,
GitHub $github,
Response $response,
Database $dbForPlatform
) {
$installation = $dbForPlatform->getDocument('installations', $installationId);
if ($installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
}
$providerInstallationId = $installation->getAttribute('providerInstallationId');
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
$owner = $github->getOwnerName($providerInstallationId) ?? '';
try {
$repositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
if (empty($repositoryName)) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
} catch (RepositoryNotFound $e) {
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
}
$repository = $github->getRepository($owner, $repositoryName);
$repository['id'] = \strval($repository['id']) ?? '';
$repository['pushedAt'] = $repository['pushed_at'] ?? '';
$repository['organization'] = $installation->getAttribute('organization', '');
$repository['provider'] = $installation->getAttribute('provider', '');
$repository['defaultBranch'] = $repository['default_branch'] ?? '';
$response->dynamic(new Document($repository), Response::MODEL_PROVIDER_REPOSITORY);
}
}
@@ -0,0 +1,319 @@
<?php
namespace Appwrite\Platform\Modules\VCS\Http\Installations\Repositories;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Swoole\Coroutine\WaitGroup;
use Utopia\Config\Adapters\Dotenv as ConfigDotenv;
use Utopia\Config\Config;
use Utopia\Config\Exceptions\Parse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Database\Validator\Queries;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Detector\Detection\Framework\Analog;
use Utopia\Detector\Detection\Framework\Angular;
use Utopia\Detector\Detection\Framework\Astro;
use Utopia\Detector\Detection\Framework\Flutter;
use Utopia\Detector\Detection\Framework\Lynx;
use Utopia\Detector\Detection\Framework\NextJs;
use Utopia\Detector\Detection\Framework\Nuxt;
use Utopia\Detector\Detection\Framework\React;
use Utopia\Detector\Detection\Framework\ReactNative;
use Utopia\Detector\Detection\Framework\Remix;
use Utopia\Detector\Detection\Framework\Svelte;
use Utopia\Detector\Detection\Framework\SvelteKit;
use Utopia\Detector\Detection\Framework\TanStackStart;
use Utopia\Detector\Detection\Framework\Vue;
use Utopia\Detector\Detection\Packager\NPM;
use Utopia\Detector\Detection\Packager\PNPM;
use Utopia\Detector\Detection\Packager\Yarn;
use Utopia\Detector\Detection\Runtime\Bun;
use Utopia\Detector\Detection\Runtime\CPP;
use Utopia\Detector\Detection\Runtime\Dart;
use Utopia\Detector\Detection\Runtime\Deno;
use Utopia\Detector\Detection\Runtime\Dotnet;
use Utopia\Detector\Detection\Runtime\Java;
use Utopia\Detector\Detection\Runtime\Node;
use Utopia\Detector\Detection\Runtime\PHP;
use Utopia\Detector\Detection\Runtime\Python;
use Utopia\Detector\Detection\Runtime\Ruby;
use Utopia\Detector\Detection\Runtime\Swift;
use Utopia\Detector\Detector\Framework;
use Utopia\Detector\Detector\Packager;
use Utopia\Detector\Detector\Runtime;
use Utopia\Detector\Detector\Strategy;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
use Utopia\VCS\Adapter\Git\GitHub;
use Utopia\VCS\Exception\FileNotFound;
use function Swoole\Coroutine\batch;
class XList extends Action
{
use HTTP;
public static function getName()
{
return 'listRepositories';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/vcs/github/installations/:installationId/providerRepositories')
->desc('List repositories')
->groups(['api', 'vcs'])
->label('scope', 'vcs.read')
->label('resourceType', RESOURCE_TYPE_VCS)
->label('sdk', new Method(
namespace: 'vcs',
group: 'repositories',
name: 'listRepositories',
description: '/docs/references/vcs/list-repositories.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST,
),
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST,
)
]
))
->param('installationId', '', new Text(256), 'Installation Id')
->param('type', '', new WhiteList(['runtime', 'framework']), 'Detector type. Must be one of the following: runtime, framework')
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->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('gitHub')
->inject('response')
->inject('dbForPlatform')
->callback($this->action(...));
}
public function action(
string $installationId,
string $type,
string $search,
array $queries,
GitHub $github,
Response $response,
Database $dbForPlatform
) {
if (empty($search)) {
$search = "";
}
$installation = $dbForPlatform->getDocument('installations', $installationId);
if ($installation->isEmpty()) {
throw new Exception(Exception::INSTALLATION_NOT_FOUND);
}
$providerInstallationId = $installation->getAttribute('providerInstallationId');
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
$queries = Query::parseQueries($queries);
$limitQuery = current(array_filter($queries, fn ($query) => $query->getMethod() === Query::TYPE_LIMIT));
$offsetQuery = current(array_filter($queries, fn ($query) => $query->getMethod() === Query::TYPE_OFFSET));
$limit = !empty($limitQuery) ? $limitQuery->getValue() : 4;
$offset = !empty($offsetQuery) ? $offsetQuery->getValue() : 0;
if ($offset % $limit !== 0) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'offset must be a multiple of the limit');
}
$page = ($offset / $limit) + 1;
$owner = $github->getOwnerName($providerInstallationId);
['items' => $repos, 'total' => $total] = $github->searchRepositories($owner, $page, $limit, $search);
$repos = \array_map(function ($repo) use ($installation) {
$repo['id'] = \strval($repo['id'] ?? '');
$repo['pushedAt'] = $repo['pushed_at'] ?? null;
$repo['provider'] = $installation->getAttribute('provider', '') ?? '';
$repo['organization'] = $installation->getAttribute('organization', '') ?? '';
return $repo;
}, $repos);
$repos = batch(\array_map(function ($repo) use ($type, $github) {
return function () use ($repo, $type, $github) {
$files = $github->listRepositoryContents($repo['organization'], $repo['name'], '');
$files = \array_column($files, 'name');
$detector = new Packager();
foreach ($files as $file) {
$detector->addInput($file);
}
$detector
->addOption(new Yarn())
->addOption(new PNPM())
->addOption(new NPM());
$detection = $detector->detect();
$packager = !\is_null($detection) ? $detection->getName() : 'npm';
if ($type === 'framework') {
$packages = '';
try {
$contentResponse = $github->getRepositoryContent($repo['organization'], $repo['name'], 'package.json');
$packages = $contentResponse['content'] ?? '';
} catch (FileNotFound $e) {
// Continue detection without package.json
}
$frameworkDetector = new Framework($packager);
$frameworkDetector->addInput($packages, Framework::INPUT_PACKAGES);
foreach ($files as $file) {
$frameworkDetector->addInput($file, Framework::INPUT_FILE);
}
$frameworkDetector
->addOption(new Analog())
->addOption(new Angular())
->addOption(new Astro())
->addOption(new Flutter())
->addOption(new Lynx())
->addOption(new NextJs())
->addOption(new Nuxt())
->addOption(new React())
->addOption(new ReactNative())
->addOption(new Remix())
->addOption(new Svelte())
->addOption(new SvelteKit())
->addOption(new TanStackStart())
->addOption(new Vue());
$detectedFramework = $frameworkDetector->detect();
if (!\is_null($detectedFramework)) {
$framework = $detectedFramework->getName();
} else {
$framework = 'other';
}
$frameworks = Config::getParam('frameworks');
if (!\in_array($framework, \array_keys($frameworks), true)) {
$framework = 'other';
}
$repo['framework'] = $framework;
} else {
$languages = $github->listRepositoryLanguages($repo['organization'], $repo['name']);
$strategies = [
new Strategy(Strategy::FILEMATCH),
new Strategy(Strategy::LANGUAGES),
new Strategy(Strategy::EXTENSION),
];
foreach ($strategies as $strategy) {
$detector = new Runtime($strategy, $packager);
if ($strategy->getValue() === Strategy::LANGUAGES) {
foreach ($languages as $language) {
$detector->addInput($language);
}
} else {
foreach ($files as $file) {
$detector->addInput($file);
}
}
$detector
->addOption(new Node())
->addOption(new Bun())
->addOption(new Deno())
->addOption(new PHP())
->addOption(new Python())
->addOption(new Dart())
->addOption(new Swift())
->addOption(new Ruby())
->addOption(new Java())
->addOption(new CPP())
->addOption(new Dotnet());
$runtime = $detector->detect();
if (!\is_null($runtime)) {
$runtime = $runtime->getName();
break;
}
}
if (!empty($runtime)) {
$runtimes = Config::getParam('runtimes');
$runtimeWithVersion = '';
foreach ($runtimes as $runtimeKey => $runtimeConfig) {
if ($runtimeConfig['key'] === $runtime) {
$runtimeWithVersion = $runtimeKey;
}
}
$repo['runtime'] = $runtimeWithVersion ?? '';
}
}
$wg = new WaitGroup();
$envs = [];
foreach ($files as $file) {
if (!(\str_starts_with($file, '.env'))) {
continue;
}
$wg->add();
go(function () use ($github, $repo, $file, $wg, &$envs) {
try {
$contentResponse = $github->getRepositoryContent($repo['organization'], $repo['name'], $file);
$envFile = $contentResponse['content'] ?? '';
$configAdapter = new ConfigDotenv();
try {
$envObject = $configAdapter->parse($envFile);
foreach ($envObject as $envName => $envValue) {
$envs[$envName] = $envValue;
}
} catch (Parse) {
// Silence error, so rest of endpoint can return
}
} finally {
$wg->done();
}
});
}
$wg->wait();
$repo['variables'] = [];
foreach ($envs as $key => $value) {
$repo['variables'][] = [
'name' => $key,
'value' => $value,
];
}
return $repo;
};
}, $repos));
$repos = \array_map(function ($repo) {
return new Document($repo);
}, $repos);
$response->dynamic(new Document([
$type === 'framework' ? 'frameworkProviderRepositories' : 'runtimeProviderRepositories' => $repos,
'total' => $total,
]), ($type === 'framework') ? Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST : Response::MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST);
}
}
@@ -2,8 +2,16 @@
namespace Appwrite\Platform\Modules\VCS\Services;
use Appwrite\Platform\Modules\VCS\Http\GitHub\Authorize\Get as GetGitHubAuthorize;
use Appwrite\Platform\Modules\VCS\Http\GitHub\Callback\Get as GetGitHubCallback;
use Appwrite\Platform\Modules\VCS\Http\Installations\Delete as DeleteInstallation;
use Appwrite\Platform\Modules\VCS\Http\Installations\Get as GetInstallation;
use Appwrite\Platform\Modules\VCS\Http\Installations\Repositories\Branches\XList as ListRepositoryBranches;
use Appwrite\Platform\Modules\VCS\Http\Installations\Repositories\Contents\Get as GetRepositoryContents;
use Appwrite\Platform\Modules\VCS\Http\Installations\Repositories\Create as CreateRepository;
use Appwrite\Platform\Modules\VCS\Http\Installations\Repositories\Detections\Create as CreateRepositoryDetections;
use Appwrite\Platform\Modules\VCS\Http\Installations\Repositories\Get as GetRepository;
use Appwrite\Platform\Modules\VCS\Http\Installations\Repositories\XList as ListRepositories;
use Appwrite\Platform\Modules\VCS\Http\Installations\XList as ListInstallations;
use Utopia\Platform\Service;
@@ -13,9 +21,21 @@ class Http extends Service
{
$this->type = Service::TYPE_HTTP;
// GitHub Authorization & Callback
$this->addAction(GetGitHubAuthorize::getName(), new GetGitHubAuthorize());
$this->addAction(GetGitHubCallback::getName(), new GetGitHubCallback());
// Installations
$this->addAction(GetInstallation::getName(), new GetInstallation());
$this->addAction(ListInstallations::getName(), new ListInstallations());
$this->addAction(DeleteInstallation::getName(), new DeleteInstallation());
// Repositories
$this->addAction(CreateRepository::getName(), new CreateRepository());
$this->addAction(GetRepository::getName(), new GetRepository());
$this->addAction(ListRepositories::getName(), new ListRepositories());
$this->addAction(ListRepositoryBranches::getName(), new ListRepositoryBranches());
$this->addAction(GetRepositoryContents::getName(), new GetRepositoryContents());
$this->addAction(CreateRepositoryDetections::getName(), new CreateRepositoryDetections());
}
}
@@ -5,6 +5,7 @@ namespace Appwrite\Platform\Services;
use Appwrite\Platform\Workers\Audits;
use Appwrite\Platform\Workers\Certificates;
use Appwrite\Platform\Workers\Deletes;
use Appwrite\Platform\Workers\Executions;
use Appwrite\Platform\Workers\Functions;
use Appwrite\Platform\Workers\Mails;
use Appwrite\Platform\Workers\Messaging;
@@ -23,6 +24,7 @@ class Workers extends Service
->addAction(Audits::getName(), new Audits())
->addAction(Certificates::getName(), new Certificates())
->addAction(Deletes::getName(), new Deletes())
->addAction(Executions::getName(), new Executions())
->addAction(Functions::getName(), new Functions())
->addAction(Mails::getName(), new Mails())
->addAction(Messaging::getName(), new Messaging())
+72 -5
View File
@@ -4,7 +4,11 @@ namespace Appwrite\Platform\Tasks;
use Appwrite\Event\Certificate;
use Utopia\CLI\Console;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Query;
use Utopia\Domains\Domain;
use Utopia\Platform\Action;
use Utopia\System\System;
use Utopia\Validator\Boolean;
@@ -22,22 +26,85 @@ class SSL extends Action
$this
->desc('Validate server certificates')
->param('domain', System::getEnv('_APP_DOMAIN', ''), new Hostname(), 'Domain to generate certificate for. If empty, main domain will be used.', true)
->param('skip-check', true, new Boolean(true), 'If DNS and renew check should be skipped. Defaults to true, and when true, all jobs will result in certificate generation attempt.', true)
->param('skip-check', 'true', new Boolean(true), 'If DNS and renew check should be skipped. Defaults to true, and when true, all jobs will result in certificate generation attempt.', true)
->inject('console')
->inject('dbForPlatform')
->inject('queueForCertificates')
->callback($this->action(...));
}
public function action(string $domain, bool|string $skipCheck, Certificate $queueForCertificates): void
public function action(string $domain, bool|string $skipCheck, Document $console, Database $dbForPlatform, Certificate $queueForCertificates): void
{
$skipCheck = \strval($skipCheck) === 'true';
$domain = new Domain(!empty($domain) ? $domain : '');
if (!$domain->isKnown() || $domain->isTest()) {
Console::error('Domain is not known or is a test domain: ' . $domain->get());
return;
}
Console::success('Scheduling a job to issue a TLS certificate for domain: ' . $domain);
$skipCheck = \strval($skipCheck) === 'true';
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
$rule = $isMd5
? $dbForPlatform->getDocument('rules', md5($domain->get()))
: $dbForPlatform->findOne('rules', [
Query::equal('domain', [$domain->get()]),
]);
if ($rule->isEmpty()) {
$owner = '';
// Mark owner as Appwrite if its appwrite-owned domain
$appwriteDomains = [];
$appwriteDomainEnvs = [
System::getEnv('_APP_DOMAIN_FUNCTIONS_FALLBACK', ''),
System::getEnv('_APP_DOMAIN_FUNCTIONS', ''),
System::getEnv('_APP_DOMAIN_SITES', ''),
];
foreach ($appwriteDomainEnvs as $appwriteDomainEnv) {
foreach (\explode(',', $appwriteDomainEnv) as $appwriteDomain) {
if (empty($appwriteDomain)) {
continue;
}
$appwriteDomains[] = $appwriteDomain;
}
}
foreach ($appwriteDomains as $appwriteDomain) {
if (\str_ends_with($domain->get(), $appwriteDomain)) {
$owner = 'Appwrite';
break;
}
}
$ruleId = $isMd5 ? md5($domain->get()) : ID::unique();
$rule = $dbForPlatform->createDocument('rules', new Document([
'$id' => $ruleId,
'domain' => $domain->get(),
'type' => 'api',
'status' => RULE_STATUS_CERTIFICATE_GENERATING,
'projectId' => $console->getId(),
'projectInternalId' => $console->getSequence(),
'search' => implode(' ', [$ruleId, $domain->get()]),
'owner' => $owner,
'region' => $console->getAttribute('region')
]));
Console::info('Rule ' . $rule->getId() . ' created for domain: ' . $domain->get());
} else {
$rule = $dbForPlatform->updateDocument('rules', $rule->getId(), new Document([
'status' => RULE_STATUS_CERTIFICATE_GENERATING,
]));
Console::info('Updated existing rule ' . $rule->getId() . ' for domain: ' . $domain->get());
}
$queueForCertificates
->setDomain(new Document([
'domain' => $domain
'domain' => $domain->get()
]))
->setSkipRenewCheck($skipCheck)
->trigger();
Console::success('Scheduled a job to issue a TLS certificate for domain: ' . $domain->get());
}
}
+22 -10
View File
@@ -153,11 +153,11 @@ class Certificates extends Action
): void {
// Get rule
$rule = System::getEnv('_APP_RULES_FORMAT') === 'md5'
? $authorization->skip(fn () => $dbForPlatform->getDocument('rules', md5($domain->get())))
: $authorization->skip(fn () => $dbForPlatform->findOne('rules', [
? $dbForPlatform->getDocument('rules', md5($domain->get()))
: $dbForPlatform->findOne('rules', [
Query::equal('domain', [$domain->get()]),
Query::limit(1),
]));
]);
// Skip if rule is not desired state (created but not verified yet).
if ($rule->getAttribute('status', '') !== RULE_STATUS_CREATED) {
@@ -177,7 +177,10 @@ class Certificates extends Action
Console::success('Domain verification succeeded.');
} catch (AppwriteException $err) {
Console::warning('Domain verification failed: ' . $err->getMessage());
$rule->setAttribute('logs', $err->getMessage());
$date = \date('H:i:s');
$logs = "\033[90m[{$date}] \033[31mDNS verification failed: \033[0m\n";
$logs .= \mb_strcut($err->getMessage(), 0, 500000); // Limit to 500kb
$rule->setAttribute('logs', $logs);
} finally {
// Update rule and emit events
$this->updateRuleAndSendEvents($rule, $dbForPlatform, $queueForEvents, $queueForWebhooks, $queueForFunctions, $queueForRealtime);
@@ -269,11 +272,11 @@ class Certificates extends Action
// Get rule document for domain
// TODO: (@Meldiron) Remove after 1.7.x migration
$rule = System::getEnv('_APP_RULES_FORMAT') === 'md5'
? $authorization->skip(fn () => $dbForPlatform->getDocument('rules', md5($domain->get())))
: $authorization->skip(fn () => $dbForPlatform->findOne('rules', [
? $dbForPlatform->getDocument('rules', md5($domain->get()))
: $dbForPlatform->findOne('rules', [
Query::equal('domain', [$domain->get()]),
Query::limit(1),
]));
]);
// Rule not found (or) not in the expected state
if ($rule->isEmpty() || !\in_array($rule->getAttribute('status'), [RULE_STATUS_CERTIFICATE_GENERATING, RULE_STATUS_VERIFIED])) {
@@ -474,11 +477,20 @@ class Certificates extends Action
{
$mainDomain = $validationDomain ?? $this->getMainDomain();
$isMainDomain = !isset($mainDomain) || $domain->get() === $mainDomain;
if (!$isMainDomain) {
$this->verifyRule($rule, $log);
} else {
if ($isMainDomain) {
// Main domain validation
// TODO: Would be awesome to check A/AAAA record here. Maybe dry run?
return;
}
try {
$this->verifyRule($rule, $log);
} catch (AppwriteException $err) {
$msg = $err->getMessage() . "\n";
$msg .= "Verify your DNS records are correctly configured and try again.\n";
$msg .= "If they're correct and it still fails, please retry after sometime. DNS records can take up to 48 hours to propagate.\n";
throw new AppwriteException($err->getType(), $msg);
}
}
@@ -0,0 +1,52 @@
<?php
namespace Appwrite\Platform\Workers;
use Exception;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Platform\Action;
use Utopia\Queue\Message;
use Utopia\System\System;
class Executions extends Action
{
public static function getName(): string
{
return 'executions';
}
/**
* @throws Exception
*/
public function __construct()
{
$this
->desc('Executions worker')
->groups(['executions'])
->inject('message')
->inject('dbForProject')
->callback($this->action(...));
}
public function action(
Message $message,
Database $dbForProject,
): void {
$payload = $message->getPayload() ?? [];
if (empty($payload)) {
throw new Exception('Missing payload');
}
$execution = new Document($payload['execution'] ?? []);
if ($execution->isEmpty()) {
throw new Exception('Missing execution');
}
if (System::getEnv('_APP_REGION') !== 'nyc') { // TODO: Remove region check
$dbForProject->upsertDocument('executions', $execution);
}
}
}
+48 -72
View File
@@ -4,6 +4,7 @@ namespace Appwrite\Platform\Workers;
use Ahc\Jwt\JWT;
use Appwrite\Event\Event;
use Appwrite\Event\Execution as ExecutionEvent;
use Appwrite\Event\Func;
use Appwrite\Event\Realtime;
use Appwrite\Event\StatsUsage;
@@ -16,8 +17,6 @@ use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Conflict;
use Utopia\Database\Exception\Structure;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
@@ -50,6 +49,7 @@ class Functions extends Action
->inject('queueForRealtime')
->inject('queueForEvents')
->inject('queueForStatsUsage')
->inject('queueForExecutions')
->inject('log')
->inject('executor')
->inject('isResourceBlocked')
@@ -65,6 +65,7 @@ class Functions extends Action
Realtime $queueForRealtime,
Event $queueForEvents,
StatsUsage $queueForStatsUsage,
ExecutionEvent $queueForExecutions,
Log $log,
Executor $executor,
callable $isResourceBlocked
@@ -77,13 +78,6 @@ class Functions extends Action
$type = $payload['type'] ?? '';
// Short-term solution to offhand write operation from API container
if ($type === Func::TYPE_ASYNC_WRITE) {
$execution = new Document($payload['execution'] ?? []);
$dbForProject->createDocument('executions', $execution);
return;
}
$events = $payload['events'] ?? [];
$data = $payload['body'] ?? '';
$eventData = $payload['payload'] ?? '';
@@ -164,6 +158,7 @@ class Functions extends Action
queueForRealtime: $queueForRealtime,
queueForStatsUsage: $queueForStatsUsage,
queueForEvents: $queueForEvents,
queueForExecutions: $queueForExecutions,
project: $project,
function: $function,
executor: $executor,
@@ -208,6 +203,7 @@ class Functions extends Action
queueForRealtime: $queueForRealtime,
queueForStatsUsage: $queueForStatsUsage,
queueForEvents: $queueForEvents,
queueForExecutions: $queueForExecutions,
project: $project,
function: $function,
executor: $executor,
@@ -234,6 +230,7 @@ class Functions extends Action
queueForRealtime: $queueForRealtime,
queueForStatsUsage: $queueForStatsUsage,
queueForEvents: $queueForEvents,
queueForExecutions: $queueForExecutions,
project: $project,
function: $function,
executor: $executor,
@@ -266,7 +263,8 @@ class Functions extends Action
*/
private function fail(
string $message,
Database $dbForProject,
Document $project,
ExecutionEvent $queueForExecutions,
Document $function,
string $trigger,
string $path,
@@ -309,11 +307,10 @@ class Functions extends Action
'duration' => 0.0,
]);
$execution = $dbForProject->createDocument('executions', $execution);
if ($execution->isEmpty()) {
throw new Exception('Failed to create execution');
}
$queueForExecutions
->setExecution($execution)
->setProject($project)
->trigger();
}
/**
@@ -337,9 +334,6 @@ class Functions extends Action
* @param string|null $eventData
* @param string|null $executionId
* @return void
* @throws Structure
* @throws \Utopia\Database\Exception
* @throws Conflict
*/
private function execute(
Log $log,
@@ -349,6 +343,7 @@ class Functions extends Action
Realtime $queueForRealtime,
StatsUsage $queueForStatsUsage,
Event $queueForEvents,
ExecutionEvent $queueForExecutions,
Document $project,
Document $function,
Executor $executor,
@@ -376,19 +371,19 @@ class Functions extends Action
if ($deployment->getAttribute('resourceId') !== $functionId) {
$errorMessage = 'The execution could not be completed because a corresponding deployment was not found. A function deployment needs to be created before it can be executed. Please create a deployment for your function and try again.';
$this->fail($errorMessage, $dbForProject, $function, $trigger, $path, $method, $user, $jwt, $event);
$this->fail($errorMessage, $project, $queueForExecutions, $function, $trigger, $path, $method, $user, $jwt, $event);
return;
}
if ($deployment->isEmpty()) {
$errorMessage = 'The execution could not be completed because a corresponding deployment was not found. A function deployment needs to be created before it can be executed. Please create a deployment for your function and try again.';
$this->fail($errorMessage, $dbForProject, $function, $trigger, $path, $method, $user, $jwt, $event);
$this->fail($errorMessage, $project, $queueForExecutions, $function, $trigger, $path, $method, $user, $jwt, $event);
return;
}
if ($deployment->getAttribute('status') !== 'ready') {
$errorMessage = 'The execution could not be completed because the build is not ready. Please wait for the build to complete and try again.';
$this->fail($errorMessage, $dbForProject, $function, $trigger, $path, $method, $user, $jwt, $event);
$this->fail($errorMessage, $project, $queueForExecutions, $function, $trigger, $path, $method, $user, $jwt, $event);
return;
}
@@ -419,56 +414,38 @@ class Functions extends Action
$headers['x-appwrite-continent-code'] = '';
$headers['x-appwrite-continent-eu'] = 'false';
/** Create execution or update execution status */
$execution = $dbForProject->getDocument('executions', $executionId ?? '');
if ($execution->isEmpty()) {
/** Create or update execution to processing status */
if (empty($executionId)) {
$executionId = ID::unique();
$headers['x-appwrite-execution-id'] = $executionId;
$headersFiltered = [];
foreach ($headers as $key => $value) {
if (\in_array(\strtolower($key), FUNCTION_ALLOWLIST_HEADERS_REQUEST)) {
$headersFiltered[] = [ 'name' => $key, 'value' => $value ];
}
}
}
$headers['x-appwrite-execution-id'] = $executionId;
$execution = new Document([
'$id' => $executionId,
'$permissions' => $user->isEmpty() ? [] : [Permission::read(Role::user($user->getId()))],
'resourceInternalId' => $function->getSequence(),
'resourceId' => $function->getId(),
'resourceType' => 'functions',
'deploymentInternalId' => $deployment->getSequence(),
'deploymentId' => $deployment->getId(),
'trigger' => $trigger,
'status' => 'processing',
'responseStatusCode' => 0,
'responseHeaders' => [],
'requestPath' => $path,
'requestMethod' => $method,
'requestHeaders' => $headersFiltered,
'errors' => '',
'logs' => '',
'duration' => 0.0,
]);
$execution = $dbForProject->createDocument('executions', $execution);
// TODO: @Meldiron Trigger executions.create event here
if ($execution->isEmpty()) {
throw new Exception('Failed to create or read execution');
$headersFiltered = [];
foreach ($headers as $key => $value) {
if (\in_array(\strtolower($key), FUNCTION_ALLOWLIST_HEADERS_REQUEST)) {
$headersFiltered[] = [ 'name' => $key, 'value' => $value ];
}
}
if ($execution->getAttribute('status') !== 'processing') {
$execution->setAttribute('status', 'processing');
try {
$execution = $dbForProject->updateDocument('executions', $executionId, $execution);
} catch (\Throwable $e) {
$log->addExtra('updateError', $e->getMessage());
}
}
$execution = new Document([
'$id' => $executionId,
'$permissions' => $user->isEmpty() ? [] : [Permission::read(Role::user($user->getId()))],
'resourceInternalId' => $function->getSequence(),
'resourceId' => $function->getId(),
'resourceType' => 'functions',
'deploymentInternalId' => $deployment->getSequence(),
'deploymentId' => $deployment->getId(),
'trigger' => $trigger,
'status' => 'processing',
'responseStatusCode' => 0,
'responseHeaders' => [],
'requestPath' => $path,
'requestMethod' => $method,
'requestHeaders' => $headersFiltered,
'errors' => '',
'logs' => '',
'duration' => 0.0,
]);
$durationStart = \microtime(true);
@@ -610,12 +587,11 @@ class Functions extends Action
$error = $th->getMessage();
$errorCode = $th->getCode();
} finally {
/** Update execution status */
try {
$execution = $dbForProject->updateDocument('executions', $executionId, $execution);
} catch (\Throwable $e) {
$log->addExtra('updateError', $e->getMessage());
}
/** Persist final execution status */
$queueForExecutions
->setExecution($execution)
->setProject($project)
->trigger();
/** Trigger usage queue */
$queueForStatsUsage
+11 -9
View File
@@ -333,8 +333,12 @@ class Migrations extends Action
$transfer = $source = $destination = null;
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$endpoint = $protocol . '://' . $platform['apiHostname'] . '/v1';
$host = System::getEnv('_APP_MIGRATION_HOST');
if (empty($host)) {
throw new \Exception('_APP_MIGRATION_HOST is not set');
}
$endpoint = 'http://'.$host.'/v1';
try {
$credentials = $migration->getAttribute('credentials', []);
@@ -350,10 +354,6 @@ class Migrations extends Action
$credentials['destinationEndpoint'] = $endpoint;
}
if (($credentials['endpoint'] ?? '') === 'http://localhost/v1') {
$credentials['endpoint'] = $endpoint;
}
$migration->setAttribute('credentials', $credentials);
$migration->setAttribute('stage', 'processing');
@@ -481,7 +481,6 @@ class Migrations extends Action
array $platform,
Authorization $authorization,
): void {
$credentials = $migration->getAttribute('credentials', []);
$options = $migration->getAttribute('options', []);
$bucketId = 'default'; // Always use platform default bucket
$filename = $options['filename'] ?? 'export_' . \time();
@@ -494,7 +493,7 @@ class Migrations extends Action
throw new \Exception('User ' . $userInternalId . ' not found');
}
$bucket = $authorization->skip(fn () => $this->dbForPlatform->getDocument('buckets', $bucketId));
$bucket = $this->dbForPlatform->getDocument('buckets', $bucketId);
if ($bucket->isEmpty()) {
throw new \Exception('Bucket not found');
}
@@ -577,8 +576,11 @@ class Migrations extends Action
]);
// Generate download URL with JWT
$endpoint = System::getEnv('_APP_DOMAIN', '');
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS', 'disabled') === 'enabled' ? 'https' : 'http';
$downloadUrl = "{$protocol}://{$endpoint}/v1/storage/buckets/{$bucketId}/files/{$fileId}/push?project={$project->getId()}&jwt={$jwt}";
$downloadUrl = "{$credentials['endpoint']}/storage/buckets/{$bucketId}/files/{$fileId}/push?project={$project->getId()}&jwt={$jwt}";
$options['downloadUrl'] = $downloadUrl;
$migration->setAttribute('options', $options);
$this->updateMigrationDocument($migration, $project, $queueForRealtime);
+190 -65
View File
@@ -4,6 +4,11 @@ namespace Appwrite\Utopia\Database;
use Utopia\Database\Query;
/**
* RuntimeQuery handles real-time query filtering for Appwrite's Realtime subscriptions.
*
* Queries are pre-compiled at subscription time for fast evaluation during message delivery.
*/
class RuntimeQuery extends Query
{
public const ALLOWED_QUERIES = [
@@ -28,16 +33,19 @@ class RuntimeQuery extends Query
];
/**
* Checks if a query is select("*") which means "listen to all events"
* Checks if a query is a select("*") query.
*
* @param Query $query
* @return bool
*/
public static function isSelectAll(Query $query): bool
{
return $query->getMethod() === Query::TYPE_SELECT
&& count($query->getValues()) === 1
&& $query->getValues()[0] === '*';
if ($query->getMethod() !== Query::TYPE_SELECT) {
return false;
}
$values = $query->getValues();
return count($values) === 1 && $values[0] === '*';
}
/**
@@ -60,105 +68,222 @@ class RuntimeQuery extends Query
}
/**
* Pre-compile queries into an optimized format for fast evaluation.
* Call this once when subscription is created, store the result.
*
* @param array<Query> $queries
* @param array<string, mixed> $payload
* @return array Compiled query structure with 'type' key
*/
public static function filter(array $queries, array $payload): array
public static function compile(array $queries): array
{
if (empty($queries)) {
return $payload;
return ['type' => 'selectAll'];
}
// Check if select("*") is present - if so, return payload (match all)
// Check for select("*") upfront
foreach ($queries as $query) {
if (self::isSelectAll($query)) {
return $payload;
if ($query->getMethod() === Query::TYPE_SELECT) {
$values = $query->getValues();
if (count($values) === 1 && $values[0] === '*') {
return ['type' => 'selectAll'];
}
}
}
// multiple queries follows and condition
// Compile queries into flat structure
$compiled = [
'type' => 'filter',
'conditions' => [],
'attributes' => [],
'hasOr' => false,
];
foreach ($queries as $query) {
if (!self::evaluateFilter($query, $payload)) {
return [];
};
$condition = self::compileCondition($query);
$compiled['conditions'][] = $condition;
self::extractAttributes($condition, $compiled['attributes'], $compiled['hasOr']);
}
$compiled['attributes'] = array_unique($compiled['attributes']);
return $compiled;
}
/**
* Compile a single query condition into an optimized array format.
*/
private static function compileCondition(Query $query): array
{
$method = $query->getMethod();
if ($method === Query::TYPE_AND) {
return [
'op' => 'AND',
'conditions' => array_map([self::class, 'compileCondition'], $query->getValues()),
];
}
if ($method === Query::TYPE_OR) {
return [
'op' => 'OR',
'conditions' => array_map([self::class, 'compileCondition'], $query->getValues()),
];
}
return [
'op' => $method,
'attr' => $query->getAttribute(),
'values' => $query->getValues(),
];
}
/**
* Extract all attribute names from a compiled condition tree.
* Also tracks whether any OR conditions exist.
*/
private static function extractAttributes(array $condition, array &$attributes, bool &$hasOr): void
{
if (isset($condition['op']) && $condition['op'] === 'OR') {
$hasOr = true;
}
if (isset($condition['attr'])) {
$attributes[] = $condition['attr'];
}
if (isset($condition['conditions'])) {
foreach ($condition['conditions'] as $sub) {
self::extractAttributes($sub, $attributes, $hasOr);
}
}
}
/**
* Fast filter using pre-compiled query structure.
*
* @param array $compiled Result from compile()
* @param array $payload Event payload
* @return array|null Null if no match, payload if match
*/
public static function filter(array $compiled, array $payload): ?array
{
// Fast path for select("*") subscriptions
if ($compiled['type'] === 'selectAll') {
return $payload;
}
// Quick rejection: if payload is missing any required attribute, fail fast
// Skip this optimization when OR conditions exist (OR can match with partial attributes)
if (empty($compiled['hasOr'])) {
foreach ($compiled['attributes'] as $attr) {
if (!isset($payload[$attr]) && !\array_key_exists($attr, $payload)) {
return null;
}
}
}
// Evaluate all conditions (AND logic at top level)
foreach ($compiled['conditions'] as $condition) {
if (!self::evaluateCondition($condition, $payload)) {
return null;
}
}
return $payload;
}
private static function evaluateFilter(Query $query, array $payload): bool
/**
* Evaluate a single compiled condition against a payload.
*/
private static function evaluateCondition(array $condition, array $payload): bool
{
$attribute = $query->getAttribute();
$method = $query->getMethod();
$values = $query->getValues();
$op = $condition['op'];
// during 'and' and 'or' attribute will not be present
switch ($method) {
case Query::TYPE_AND:
// All subqueries must evaluate to true
foreach ($query->getValues() as $subquery) {
if (!self::evaluateFilter($subquery, $payload)) {
// Handle AND/OR
if ($op === 'AND') {
foreach ($condition['conditions'] as $sub) {
if (!self::evaluateCondition($sub, $payload)) {
return false;
}
}
return true;
}
if ($op === 'OR') {
foreach ($condition['conditions'] as $sub) {
if (self::evaluateCondition($sub, $payload)) {
return true;
}
}
return false;
}
// Leaf condition - direct comparison
$attr = $condition['attr'];
if (!\array_key_exists($attr, $payload)) {
return false;
}
$value = $payload[$attr];
$targets = $condition['values'];
// Inlined comparisons - no closures, no method calls
switch ($op) {
case Query::TYPE_EQUAL:
foreach ($targets as $target) {
if ($value === $target) {
return true;
}
}
return false;
case Query::TYPE_NOT_EQUAL:
foreach ($targets as $target) {
if ($value === $target) {
return false;
}
}
return true;
case Query::TYPE_OR:
// At least one subquery must evaluate to true
foreach ($query->getValues() as $subquery) {
if (self::evaluateFilter($subquery, $payload)) {
case Query::TYPE_LESSER:
foreach ($targets as $target) {
if ($value < $target) {
return true;
}
}
return false;
}
$hasAttribute = \array_key_exists($attribute, $payload);
if (!$hasAttribute) {
return false;
}
// null can be a value as well
$payloadAttributeValue = $payload[$attribute];
switch ($method) {
case Query::TYPE_EQUAL:
return self::anyMatch($values, fn ($value) => $payloadAttributeValue === $value);
case Query::TYPE_NOT_EQUAL:
return !self::anyMatch($values, fn ($value) => $payloadAttributeValue === $value);
case Query::TYPE_LESSER:
return self::anyMatch($values, fn ($value) => $payloadAttributeValue < $value);
case Query::TYPE_LESSER_EQUAL:
return self::anyMatch($values, fn ($value) => $payloadAttributeValue <= $value);
foreach ($targets as $target) {
if ($value <= $target) {
return true;
}
}
return false;
case Query::TYPE_GREATER:
return self::anyMatch($values, fn ($value) => $payloadAttributeValue > $value);
foreach ($targets as $target) {
if ($value > $target) {
return true;
}
}
return false;
case Query::TYPE_GREATER_EQUAL:
return self::anyMatch($values, fn ($value) => $payloadAttributeValue >= $value);
foreach ($targets as $target) {
if ($value >= $target) {
return true;
}
}
return false;
// attribute must be present and should be explicitly null
case Query::TYPE_IS_NULL:
return $payloadAttributeValue === null;
return $value === null;
case Query::TYPE_IS_NOT_NULL:
return $payloadAttributeValue !== null;
return $value !== null;
default:
throw new \InvalidArgumentException(
"Unsupported query method: {$method}"
);
return false;
}
}
private static function anyMatch(array $values, callable $fn): bool
{
foreach ($values as $value) {
if ($fn($value)) {
return true;
}
}
return false;
}
}
@@ -0,0 +1,18 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Queries;
class Keys extends Base
{
public const ALLOWED_ATTRIBUTES = [
'expire',
'accessedAt',
'name',
'scopes',
];
public function __construct()
{
parent::__construct('keys', self::ALLOWED_ATTRIBUTES);
}
}
@@ -87,6 +87,18 @@ class Collection extends Model
'example' => new \stdClass(),
'array' => true
])
->addRule('bytesMax', [
'type' => self::TYPE_INTEGER,
'description' => 'Maximum document size in bytes. Returns 0 when no limit applies.',
'default' => 0,
'example' => 65535,
])
->addRule('bytesUsed', [
'type' => self::TYPE_INTEGER,
'description' => 'Currently used document size in bytes based on defined attributes.',
'default' => 0,
'example' => 1500,
])
;
}
@@ -92,6 +92,18 @@ class Table extends Model
'example' => new \stdClass(),
'array' => true
])
->addRule('bytesMax', [
'type' => self::TYPE_INTEGER,
'description' => 'Maximum row size in bytes. Returns 0 when no limit applies.',
'default' => 0,
'example' => 65535,
])
->addRule('bytesUsed', [
'type' => self::TYPE_INTEGER,
'description' => 'Currently used row size in bytes based on defined columns.',
'default' => 0,
'example' => 1500,
])
;
}
+5 -2
View File
@@ -86,8 +86,11 @@ class CompressionTest extends Scope
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertArrayNotHasKey('content-encoding', $response['headers']);
$this->assertEquals('chunked', $response['headers']['transfer-encoding'] ?? null, 'Uncompressed response should use chunked transfer, headers received: ' . json_encode($response['headers'], JSON_PRETTY_PRINT));
$this->assertArrayNotHasKey('content-length', $response['headers'], 'Uncompressed response should not send content length when chunked.');
if ($this->endpoint !== 'http://appwrite/v1') {
$this->assertEquals('chunked', $response['headers']['transfer-encoding'] ?? null, 'Uncompressed response should use chunked transfer, headers received: ' . json_encode($response['headers'], JSON_PRETTY_PRINT));
$this->assertArrayNotHasKey('content-length', $response['headers'], 'Uncompressed response should not send content length when chunked.');
}
$this->assertArrayHasKey('longValue', $response['body'], 'Prefs payload should expose longValue at the top level, body received: ' . json_encode($response['body'], JSON_PRETTY_PRINT));
+2
View File
@@ -60,6 +60,7 @@ trait ProjectCustom
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
'x-appwrite-project' => 'console',
], [
'keyId' => ID::unique(),
'name' => 'Demo Project Key',
'scopes' => [
'users.read',
@@ -194,6 +195,7 @@ trait ProjectCustom
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
'x-appwrite-project' => 'console',
], [
'keyId' => ID::unique(),
'name' => 'Demo Project Key',
'scopes' => $scopes,
]);
+2 -1
View File
@@ -18,7 +18,8 @@ abstract class Scope extends TestCase
public const REQUEST_TYPE_SMS = 'sms';
protected ?Client $client = null;
protected string $endpoint = 'http://appwrite.test/v1';
protected string $endpoint = 'http://appwrite/v1';
protected string $webEndpoint = 'http://appwrite.test/v1';
protected function setUp(): void
{
@@ -62,6 +62,12 @@ trait DatabasesBase
$this->assertEquals(201, $movies['headers']['status-code']);
$this->assertEquals($movies['body']['name'], 'Movies');
$this->assertArrayHasKey('bytesMax', $movies['body']);
$this->assertArrayHasKey('bytesUsed', $movies['body']);
$this->assertIsInt($movies['body']['bytesMax']);
$this->assertIsInt($movies['body']['bytesUsed']);
$this->assertGreaterThanOrEqual(0, $movies['body']['bytesMax']);
$this->assertGreaterThanOrEqual(0, $movies['body']['bytesUsed']);
$actors = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([
'content-type' => 'application/json',
@@ -143,6 +149,8 @@ trait DatabasesBase
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertFalse($response['body']['enabled']);
$this->assertArrayHasKey('bytesMax', $response['body']);
$this->assertArrayHasKey('bytesUsed', $response['body']);
if ($this->getSide() === 'client') {
$responseCreateDocument = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/documents', array_merge([
@@ -363,6 +371,9 @@ trait DatabasesBase
$this->assertIsArray($movies['body']['attributes']);
$this->assertCount(9, $movies['body']['attributes']);
$this->assertArrayHasKey('bytesMax', $movies['body']);
$this->assertArrayHasKey('bytesUsed', $movies['body']);
$this->assertGreaterThanOrEqual(0, $movies['body']['bytesUsed']);
$this->assertEquals($movies['body']['attributes'][0]['key'], $title['body']['key']);
$this->assertEquals($movies['body']['attributes'][1]['key'], $description['body']['key']);
$this->assertEquals($movies['body']['attributes'][2]['key'], $tagline['body']['key']);
@@ -62,6 +62,12 @@ trait DatabasesBase
$this->assertEquals(201, $movies['headers']['status-code']);
$this->assertEquals($movies['body']['name'], 'Movies');
$this->assertArrayHasKey('bytesMax', $movies['body']);
$this->assertArrayHasKey('bytesUsed', $movies['body']);
$this->assertIsInt($movies['body']['bytesMax']);
$this->assertIsInt($movies['body']['bytesUsed']);
$this->assertGreaterThanOrEqual(0, $movies['body']['bytesMax']);
$this->assertGreaterThanOrEqual(0, $movies['body']['bytesUsed']);
$actors = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([
'content-type' => 'application/json',
@@ -143,6 +149,8 @@ trait DatabasesBase
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertFalse($response['body']['enabled']);
$this->assertArrayHasKey('bytesMax', $response['body']);
$this->assertArrayHasKey('bytesUsed', $response['body']);
if ($this->getSide() === 'client') {
$responseCreateRow = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $data['moviesId'] . '/rows', array_merge([
@@ -363,6 +371,9 @@ trait DatabasesBase
$this->assertIsArray($movies['body']['columns']);
$this->assertCount(9, $movies['body']['columns']);
$this->assertArrayHasKey('bytesMax', $movies['body']);
$this->assertArrayHasKey('bytesUsed', $movies['body']);
$this->assertGreaterThanOrEqual(0, $movies['body']['bytesUsed']);
$this->assertEquals($movies['body']['columns'][0]['key'], $title['body']['key']);
$this->assertEquals($movies['body']['columns'][1]['key'], $description['body']['key']);
$this->assertEquals($movies['body']['columns'][2]['key'], $tagline['body']['key']);
@@ -1751,7 +1751,10 @@ class FunctionsCustomServerTest extends Scope
$this->assertEventually(function () use ($functionId, $userId) {
$executions = $this->listExecutions($functionId);
$lastExecution = $executions['body']['executions'][0];
$this->assertEquals(200, $executions['headers']['status-code']);
$executionsList = $executions['body']['executions'] ?? [];
$this->assertNotEmpty($executionsList);
$lastExecution = $executionsList[0];
$this->assertEquals('completed', $lastExecution['status']);
$this->assertEquals(204, $lastExecution['responseStatusCode']);
+1 -1
View File
@@ -33,7 +33,7 @@ class ScopeTest extends Scope
'x-appwrite-key' => $apiKey,
], $gqlPayload);
$message = "app.{$projectId}@service.appwrite.test (role: applications) missing scopes ([\"databases.write\"])";
$message = "app.{$projectId}@service.appwrite (role: applications) missing scopes ([\"databases.write\"])";
$this->assertArrayHasKey('errors', $database['body']);
$this->assertEquals($message, $database['body']['errors'][0]['message']);
}
@@ -89,7 +89,7 @@ trait MigrationsBase
{
$response = $this->performMigrationSync([
'resources' => Appwrite::getSupportedResources(),
'endpoint' => $this->endpoint,
'endpoint' => $this->webEndpoint,
'projectId' => $this->getProject()['$id'],
'apiKey' => $this->getProject()['apiKey'],
]);
@@ -126,7 +126,7 @@ trait MigrationsBase
'resources' => [
Resource::TYPE_USER,
],
'endpoint' => $this->endpoint,
'endpoint' => $this->webEndpoint,
'projectId' => $this->getProject()['$id'],
'apiKey' => $this->getProject()['apiKey'],
]);
@@ -188,7 +188,7 @@ trait MigrationsBase
'resources' => [
Resource::TYPE_USER,
],
'endpoint' => $this->endpoint,
'endpoint' => $this->webEndpoint,
'projectId' => $this->getProject()['$id'],
'apiKey' => $this->getProject()['apiKey'],
]);
@@ -277,7 +277,7 @@ trait MigrationsBase
Resource::TYPE_TEAM,
Resource::TYPE_MEMBERSHIP,
],
'endpoint' => $this->endpoint,
'endpoint' => $this->webEndpoint,
'projectId' => $this->getProject()['$id'],
'apiKey' => $this->getProject()['apiKey'],
]);
@@ -393,7 +393,7 @@ trait MigrationsBase
'resources' => [
Resource::TYPE_DATABASE,
],
'endpoint' => $this->endpoint,
'endpoint' => $this->webEndpoint,
'projectId' => $this->getProject()['$id'],
'apiKey' => $this->getProject()['apiKey'],
]);
@@ -484,7 +484,7 @@ trait MigrationsBase
Resource::TYPE_TABLE,
Resource::TYPE_COLUMN,
],
'endpoint' => $this->endpoint,
'endpoint' => $this->webEndpoint,
'projectId' => $this->getProject()['$id'],
'apiKey' => $this->getProject()['apiKey'],
]);
@@ -571,7 +571,7 @@ trait MigrationsBase
Resource::TYPE_COLUMN,
Resource::TYPE_ROW,
],
'endpoint' => $this->endpoint,
'endpoint' => $this->webEndpoint,
'projectId' => $this->getProject()['$id'],
'apiKey' => $this->getProject()['apiKey'],
]);
@@ -651,7 +651,7 @@ trait MigrationsBase
'resources' => [
Resource::TYPE_BUCKET
],
'endpoint' => $this->endpoint,
'endpoint' => $this->webEndpoint,
'projectId' => $this->getProject()['$id'],
'apiKey' => $this->getProject()['apiKey'],
]);
@@ -747,7 +747,7 @@ trait MigrationsBase
Resource::TYPE_BUCKET,
Resource::TYPE_FILE
],
'endpoint' => $this->endpoint,
'endpoint' => $this->webEndpoint,
'projectId' => $this->getProject()['$id'],
'apiKey' => $this->getProject()['apiKey'],
]);
@@ -818,7 +818,7 @@ trait MigrationsBase
Resource::TYPE_FUNCTION,
Resource::TYPE_DEPLOYMENT
],
'endpoint' => $this->endpoint,
'endpoint' => $this->webEndpoint,
'projectId' => $this->getProject()['$id'],
'apiKey' => $this->getProject()['apiKey'],
]);
@@ -1119,7 +1119,7 @@ trait MigrationsBase
// all data exists, pass.
$migration = $this->performCsvMigration(
[
'endpoint' => $this->endpoint,
'endpoint' => $this->webEndpoint,
'fileId' => $fileIds['default'],
'bucketId' => $bucketIds['default'],
'resourceId' => $databaseId . ':' . $tableId,
@@ -1161,7 +1161,7 @@ trait MigrationsBase
// all data exists and includes internals, pass.
$migration = $this->performCsvMigration(
[
'endpoint' => $this->endpoint,
'endpoint' => $this->webEndpoint,
'fileId' => $fileIds['documents-internals'],
'bucketId' => $bucketIds['documents-internals'],
'resourceId' => $databaseId . ':' . $tableId,
@@ -1168,7 +1168,7 @@ class ProjectsConsoleClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'duration' => 60, // Set session duration to 1 minute
'duration' => 10, // Set session duration to 10 seconds
]);
$this->assertEquals(200, $response['headers']['status-code']);
@@ -1177,7 +1177,7 @@ class ProjectsConsoleClientTest extends Scope
$this->assertArrayHasKey('platforms', $response['body']);
$this->assertArrayHasKey('webhooks', $response['body']);
$this->assertArrayHasKey('keys', $response['body']);
$this->assertEquals(60, $response['body']['authDuration']);
$this->assertEquals(10, $response['body']['authDuration']);
$projectId = $response['body']['$id'];
@@ -1218,44 +1218,30 @@ class ProjectsConsoleClientTest extends Scope
$this->assertEquals(200, $response['headers']['status-code']);
// Check session doesn't expire too soon.
sleep(30);
// Eventually session expires, within 15 seconds (10+variance)
$this->assertEventually(function () use ($projectId, $sessionCookie) {
// Get User
$response = $this->client->call(Client::METHOD_GET, '/account', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'Cookie' => $sessionCookie,
]));
// Get User
$response = $this->client->call(Client::METHOD_GET, '/account', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'Cookie' => $sessionCookie,
]));
$this->assertEquals(401, $response['headers']['status-code']);
}, timeoutMs: 15 * 1000);
$this->assertEquals(200, $response['headers']['status-code']);
// Wait just over a minute
sleep(35);
// Get User
$response = $this->client->call(Client::METHOD_GET, '/account', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'Cookie' => $sessionCookie,
]));
$this->assertEquals(401, $response['headers']['status-code']);
// Set session duration to 15s
// Set session duration to 10min
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/duration', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'duration' => 15, // seconds
'duration' => 600, // seconds
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(15, $response['body']['authDuration']);
// Wait 20 seconds, ensure non-valid session
\sleep(20);
$this->assertEquals(600, $response['body']['authDuration']);
// Ensure session is still expired (new duration only affects new sessions)
$response = $this->client->call(Client::METHOD_GET, '/account', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
@@ -2774,6 +2760,7 @@ class ProjectsConsoleClientTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'keyId' => ID::unique(),
'name' => 'Key Test',
'scopes' => ['functions.read', 'teams.write'],
]);
@@ -3123,6 +3110,7 @@ class ProjectsConsoleClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'keyId' => ID::unique(),
'name' => 'Key Test',
'scopes' => ['teams.read', 'teams.write'],
]);
@@ -3138,6 +3126,66 @@ class ProjectsConsoleClientTest extends Scope
$this->assertArrayHasKey('accessedAt', $response['body']);
$this->assertEmpty($response['body']['accessedAt']);
/**
* Test for SUCCESS without key ID
*/
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'name' => 'Key Custom',
'scopes' => ['teams.read', 'teams.write'],
]);
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
/**
* Test for SUCCESS with custom ID
*/
$customKeyId = \uniqid() . 'custom-id';
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'keyId' => $customKeyId,
'name' => 'Key Custom',
'scopes' => ['teams.read', 'teams.write'],
]);
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertSame($customKeyId, $response['body']['$id']);
/**
* Test for FAILURE with custom ID
*/
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'keyId' => $customKeyId,
'name' => 'Key Custom',
'scopes' => ['teams.read', 'teams.write'],
]);
$this->assertEquals(409, $response['headers']['status-code']);
/**
* Test for SUCCESS with magic string ID
*/
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'keyId' => 'unique()',
'name' => 'Key Custom',
'scopes' => ['teams.read', 'teams.write'],
]);
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertNotSame('unique()', $response['body']['$id']);
$data = array_merge($data, [
'keyId' => $response['body']['$id'],
'secret' => $response['body']['secret']
@@ -3150,6 +3198,7 @@ class ProjectsConsoleClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'keyId' => ID::unique(),
'name' => 'Key Test',
'scopes' => ['unknown'],
]);
@@ -3167,19 +3216,258 @@ class ProjectsConsoleClientTest extends Scope
{
$id = $data['projectId'] ?? '';
/** Create a second key with an expiry for query testing */
$expireDate = DateTime::addSeconds(new \DateTime(), 3600);
$response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'name' => 'Key Test 2',
'scopes' => ['users.read'],
'expire' => $expireDate,
]);
$this->assertEquals(201, $response['headers']['status-code']);
$key2Id = $response['body']['$id'];
/** List all keys (no queries) */
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(5, $response['body']['total']);
$this->assertCount(5, $response['body']['keys']);
/** List keys with limit */
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::limit(1)->toString(),
]
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertCount(1, $response['body']['keys']);
$this->assertEquals(5, $response['body']['total']);
/** List keys with offset */
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::offset(1)->toString(),
]
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertCount(4, $response['body']['keys']);
$this->assertEquals(5, $response['body']['total']);
/** List keys with cursor after */
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::cursorAfter(new Document(['$id' => $data['keyId']]))->toString(),
]
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertCount(1, $response['body']['keys']);
$this->assertEquals(5, $response['body']['total']);
$this->assertEquals($key2Id, $response['body']['keys'][0]['$id']);
/** List keys filtering by expire (lessThan now — should match none) */
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::lessThan('expire', (new \DateTime())->format('Y-m-d H:i:s'))->toString(),
]
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(0, $response['body']['total']);
/** List keys filtering by expire (greaterThan now — should match the key with expiry) */
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::greaterThan('expire', (new \DateTime())->format('Y-m-d H:i:s'))->toString(),
]
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(1, $response['body']['total']);
$this->assertCount(1, $response['body']['keys']);
/** List keys filtering by name (equal — exact match) */
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::equal('name', ['Key Test'])->toString(),
]
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(1, $response['body']['total']);
$this->assertCount(1, $response['body']['keys']);
$this->assertEquals('Key Test', $response['body']['keys'][0]['name']);
/** List keys filtering by name (equal — multiple values) */
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::equal('name', ['Key Test', 'Key Test 2'])->toString(),
]
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(2, $response['body']['total']);
$this->assertCount(2, $response['body']['keys']);
/** List keys filtering by name (equal — no match) */
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::equal('name', ['Non Existent Key'])->toString(),
]
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(0, $response['body']['total']);
$this->assertCount(0, $response['body']['keys']);
/** List keys filtering by scopes (contains — match key with teams.read) */
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::contains('scopes', ['teams.read'])->toString(),
]
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(4, $response['body']['total']);
$this->assertCount(4, $response['body']['keys']);
/** List keys filtering by scopes (contains — match key with users.read) */
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::contains('scopes', ['users.read'])->toString(),
]
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(1, $response['body']['total']);
$this->assertCount(1, $response['body']['keys']);
/** List keys filtering by scopes (contains — no match) */
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::contains('scopes', ['databases.read'])->toString(),
]
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(0, $response['body']['total']);
$this->assertCount(0, $response['body']['keys']);
/** List keys filtering by name and scopes combined */
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::equal('name', ['Key Test'])->toString(),
Query::contains('scopes', ['teams.read'])->toString(),
]
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(1, $response['body']['total']);
$this->assertCount(1, $response['body']['keys']);
$this->assertEquals('Key Test', $response['body']['keys'][0]['name']);
/** List keys with orderDesc */
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::orderDesc('$createdAt')->toString(),
]
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertCount(5, $response['body']['keys']);
$this->assertGreaterThan($response['body']['keys'][1]['$createdAt'], $response['body']['keys'][0]['$createdAt']);
/** List keys with total disabled */
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'total' => false,
'queries' => [
Query::limit(1)->toString()
]
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertCount(1, $response['body']['keys']);
$this->assertEquals(0, $response['body']['total']);
/**
* Test for FAILURE
*/
/** Test invalid query attribute */
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::equal('secret', ['test'])->toString(),
]
]);
$this->assertEquals(400, $response['headers']['status-code']);
/** Test invalid cursor */
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id . '/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::cursorAfter(new Document(['$id' => 'invalid']))->toString(),
]
]);
$this->assertEquals(400, $response['headers']['status-code']);
return $data;
}
@@ -3200,7 +3488,7 @@ class ProjectsConsoleClientTest extends Scope
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertEquals($keyId, $response['body']['$id']);
$this->assertEquals('Key Test', $response['body']['name']);
$this->assertEquals('Key Custom', $response['body']['name']);
$this->assertContains('teams.read', $response['body']['scopes']);
$this->assertContains('teams.write', $response['body']['scopes']);
$this->assertCount(2, $response['body']['scopes']);
@@ -3240,6 +3528,7 @@ class ProjectsConsoleClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'keyId' => ID::unique(),
'name' => 'Key Test',
'scopes' => ['users.write'],
'expire' => DateTime::addSeconds(new \DateTime(), 3600),
@@ -3260,6 +3549,7 @@ class ProjectsConsoleClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'keyId' => ID::unique(),
'name' => 'Key Test',
'scopes' => ['health.read'],
'expire' => null,
@@ -3282,6 +3572,7 @@ class ProjectsConsoleClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'keyId' => ID::unique(),
'name' => 'Key Test',
'scopes' => ['health.read'],
'expire' => DateTime::addSeconds(new \DateTime(), -3600),
@@ -3323,6 +3614,7 @@ class ProjectsConsoleClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'keyId' => ID::unique(),
'name' => 'Key Test',
'scopes' => ['teams.read'],
'expire' => DateTime::addSeconds(new \DateTime(), 3600),
@@ -3355,6 +3647,7 @@ class ProjectsConsoleClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'keyId' => ID::unique(),
'name' => 'Key Test',
'scopes' => ['health.read'],
'expire' => DateTime::addSeconds(new \DateTime(), 3600),
@@ -4364,6 +4657,7 @@ class ProjectsConsoleClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'keyId' => ID::unique(),
'name' => 'Key Test',
'scopes' => ['users.read', 'users.write'],
]);
@@ -4384,6 +4678,7 @@ class ProjectsConsoleClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'keyId' => ID::unique(),
'name' => 'Key Test',
'scopes' => ['users.read', 'users.write'],
]);
@@ -5092,6 +5387,31 @@ class ProjectsConsoleClientTest extends Scope
]);
$this->assertEquals(200, $response['headers']['status-code']);
/** Ensure any hostname is allowed */
$response = $this->client->call(Client::METHOD_GET, '/account/sessions/oauth2/' . $provider, [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-dev-key' => $devKey['secret'],
'origin' => '',
'referer' => 'https://domain-without-rule.com'
], [
'success' => 'https://domain-without-rule.com',
'failure' => 'https://domain-without-rule.com'
], followRedirects: false);
$this->assertEquals(301, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_GET, '/account/sessions/oauth2/' . $provider, [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-dev-key' => $devKey['secret'],
'referer' => '',
'origin' => 'https://domain-without-rule.com'
], [
'success' => 'https://domain-without-rule.com',
'failure' => 'https://domain-without-rule.com'
], followRedirects: false);
$this->assertEquals(301, $response['headers']['status-code']);
/** Test hostname in Magic URL */
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/magic-url', [
'content-type' => 'application/json',
@@ -5116,6 +5436,131 @@ class ProjectsConsoleClientTest extends Scope
$this->assertEquals(201, $response['headers']['status-code']);
}
public function testRuleOAuthRedirect(): void
{
// Prepare project
$projectId = $this->setupProject([
'projectId' => ID::unique(),
'name' => 'testRuleOAuthRedirect',
'region' => System::getEnv('_APP_REGION', 'default')
]);
$provider = 'mock';
$appId = '1';
$secret = '123456';
// Prepare OAuth provider
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $projectId . '/oauth2', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'provider' => $provider,
'appId' => $appId,
'secret' => $secret,
'enabled' => true,
]);
$this->assertEquals(200, $response['headers']['status-code']);
// Prepare rule. In reality this is site rule, but for testing, API rule is enough, and faster to prepare
$domain = \uniqid() . '-with-rule.custom.localhost';
$rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/api', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-mode' => 'admin',
], $this->getHeaders()), [
'domain' => $domain
]);
$this->assertEquals(201, $rule['headers']['status-code']);
// Ensure unknown domain cannot be redirect URL
$response = $this->client->call(Client::METHOD_GET, '/account/sessions/oauth2/' . $provider, [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'referer' => 'https://' . $domain,
'origin' => '',
], [
'success' => 'https://domain-without-rule.com',
'failure' => 'https://domain-without-rule.com'
], followRedirects: false);
$this->assertEquals(400, $response['headers']['status-code']);
// Also ensure final step blocks unknown redirect URL
$response = $this->client->call(Client::METHOD_GET, '/account/sessions/oauth2/' . $provider . '/redirect', [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'origin' => '',
'referer' => 'https://mockserver.com',
], [
'code' => 'any-code',
'state' => \json_encode([
'success' => 'https://domain-without-rule.com',
'failure' => 'https://domain-without-rule.com'
]),
'error' => '',
'error_description' => '',
], followRedirects: false);
$this->assertEquals(400, $response['headers']['status-code']);
$this->assertStringContainsString('project_invalid_success_url', $response['body']);
// Ensure rule's domain can be redirect URL
$response = $this->client->call(Client::METHOD_GET, '/account/sessions/oauth2/' . $provider, [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'referer' => 'https://' . $domain,
'origin' => '',
], [
'success' => 'https://' . $domain,
'failure' => 'https://' . $domain
], followRedirects: false);
$this->assertEquals(301, $response['headers']['status-code']);
// Also ensure final step allows redirect URL
$response = $this->client->call(Client::METHOD_GET, '/account/sessions/oauth2/' . $provider . '/redirect', [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'origin' => '',
'referer' => 'https://mockserver.com',
], [
'code' => 'any-code',
'state' => \json_encode([
'success' => 'https://' . $domain,
'failure' => 'https://' . $domain
]),
'error' => '',
'error_deescription' => '',
], followRedirects: false);
$this->assertEquals(301, $response['headers']['status-code']);
$this->assertStringContainsString('https://' . $domain, $response['headers']['location']);
// Ensure unknown domain cannot be redirect URL
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/magic-url', [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'referer' => 'https://' . $domain,
'origin' => '',
], [
'userId' => ID::unique(),
'email' => 'user@appwrite.io',
'url' => 'https://domain-without-rule.com',
]);
$this->assertEquals(400, $response['headers']['status-code']);
// Ensure rule's domain can be redirect URL
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/magic-url', [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'referer' => 'https://' . $domain,
'origin' => '',
], [
'userId' => ID::unique(),
'email' => 'user@appwrite.io',
'url' => 'https://' . $domain,
]);
$this->assertEquals(201, $response['headers']['status-code']);
}
/**
* @group abuseEnabled
*/
@@ -1234,6 +1234,183 @@ class RealtimeCustomClientQueryTest extends Scope
$client->close();
}
public function testCollectionScopedDocumentsChannelReceivesEvents()
{
$user = $this->getUser();
$session = $user['session'] ?? '';
$projectId = $this->getProject()['$id'];
// Setup database and collection
$database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'databaseId' => ID::unique(),
'name' => 'Scoped Channel DB',
]);
$databaseId = $database['body']['$id'];
$collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'collectionId' => ID::unique(),
'name' => 'Scoped Channel Collection',
'permissions' => [
Permission::create(Role::user($user['$id'])),
],
'documentSecurity' => true,
]);
$collectionId = $collection['body']['$id'];
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'key' => 'status',
'size' => 256,
'required' => false,
]);
sleep(2);
// Subscribe only to the fully-qualified documents channel for this collection
$scopedChannel = 'databases.' . $databaseId . '.collections.' . $collectionId . '.documents';
$client = $this->getWebsocket([$scopedChannel], [
'origin' => 'http://localhost',
'cookie' => 'a_session_' . $projectId . '=' . $session,
]);
$response = json_decode($client->receive(), true);
$this->assertEquals('connected', $response['type']);
$this->assertContains($scopedChannel, $response['data']['channels']);
// Create document in that collection - should receive event on the scoped channel
$documentId = ID::unique();
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $this->getHeaders()), [
'documentId' => $documentId,
'data' => [
'status' => 'active'
],
'permissions' => [
Permission::read(Role::any()),
],
]);
$event = json_decode($client->receive(), true);
$this->assertEquals('event', $event['type']);
$this->assertEquals($documentId, $event['data']['payload']['$id']);
$client->close();
}
public function testCollectionScopedDocumentsChannelWithQuery()
{
$user = $this->getUser();
$session = $user['session'] ?? '';
$projectId = $this->getProject()['$id'];
// Setup database and collection
$database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'databaseId' => ID::unique(),
'name' => 'Scoped Channel Query DB',
]);
$databaseId = $database['body']['$id'];
$collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'collectionId' => ID::unique(),
'name' => 'Scoped Channel Query Collection',
'permissions' => [
Permission::create(Role::user($user['$id'])),
],
'documentSecurity' => true,
]);
$collectionId = $collection['body']['$id'];
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'key' => 'status',
'size' => 256,
'required' => false,
]);
sleep(2);
$targetDocumentId = ID::unique();
// Subscribe with query for specific document ID on the fully-qualified documents channel
$scopedChannel = 'databases.' . $databaseId . '.collections.' . $collectionId . '.documents';
$client = $this->getWebsocket([$scopedChannel], [
'origin' => 'http://localhost',
'cookie' => 'a_session_' . $projectId . '=' . $session,
], null, [
Query::equal('$id', [$targetDocumentId])->toString(),
]);
$response = json_decode($client->receive(), true);
$this->assertEquals('connected', $response['type']);
$this->assertContains($scopedChannel, $response['data']['channels']);
// Create document with matching ID - should receive event
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $this->getHeaders()), [
'documentId' => $targetDocumentId,
'data' => [
'status' => 'active'
],
'permissions' => [
Permission::read(Role::any()),
],
]);
$event = json_decode($client->receive(), true);
$this->assertEquals('event', $event['type']);
$this->assertEquals($targetDocumentId, $event['data']['payload']['$id']);
// Create document with different ID - should NOT receive event
$otherDocumentId = ID::unique();
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $this->getHeaders()), [
'documentId' => $otherDocumentId,
'data' => [
'status' => 'inactive'
],
'permissions' => [
Permission::read(Role::any()),
],
]);
try {
$client->receive();
$this->fail('Expected TimeoutException - event should be filtered for scoped channel query');
} catch (TimeoutException $e) {
$this->assertTrue(true);
}
$client->close();
}
public function testFilesChannelWithQuery()
{
$user = $this->getUser();
+22 -1
View File
@@ -1,6 +1,6 @@
services:
traefik:
image: traefik:2.2
image: traefik:3.6
container_name: appwrite-traefik
command:
- --log.level=DEBUG
@@ -212,6 +212,27 @@ services:
- _APP_DB_USER
- _APP_DB_PASS
appwrite-worker-executions:
entrypoint: worker-executions
container_name: appwrite-worker-executions
build:
context: .
restart: unless-stopped
networks:
- appwrite
depends_on:
- redis
- mariadb
environment:
- _APP_ENV
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
appwrite-worker-functions:
entrypoint: worker-functions
container_name: appwrite-worker-functions
+2 -2
View File
@@ -23,10 +23,10 @@ class ComposeTest extends TestCase
public function testServices(): void
{
$this->assertCount(15, $this->object->getServices());
$this->assertCount(16, $this->object->getServices());
$this->assertEquals('appwrite', $this->object->getService('appwrite')->getContainerName());
$this->assertEquals('', $this->object->getService('appwrite')->getImageVersion());
$this->assertEquals('2.2', $this->object->getService('traefik')->getImageVersion());
$this->assertEquals('3.6', $this->object->getService('traefik')->getImageVersion());
$this->assertEquals(['2080' => '80', '2443' => '443', '8080' => '8080'], $this->object->getService('traefik')->getPorts());
}
@@ -74,4 +74,60 @@ class OriginTest extends TestCase
$this->assertEquals(false, $validator->isValid('random-scheme://localhost'));
$this->assertEquals('Invalid Scheme. The scheme used (random-scheme) in the Origin (random-scheme://localhost) is not supported. If you are using a custom scheme, please change it to `appwrite-callback-<PROJECT_ID>`', $validator->getDescription());
}
public function testGetAllowedHostnames(): void
{
$validator = new Origin(
allowedHostnames: ['appwrite.io', 'localhost'],
allowedSchemes: ['exp']
);
$this->assertEquals(['appwrite.io', 'localhost'], $validator->getAllowedHostnames());
}
public function testGetAllowedSchemes(): void
{
$validator = new Origin(
allowedHostnames: ['appwrite.io'],
allowedSchemes: ['exp', 'appwrite-callback-123']
);
$this->assertEquals(['exp', 'appwrite-callback-123'], $validator->getAllowedSchemes());
}
public function testSetAllowedHostnames(): void
{
$validator = new Origin(
allowedHostnames: ['appwrite.io'],
allowedSchemes: ['exp']
);
$this->assertEquals(true, $validator->isValid('https://appwrite.io'));
$this->assertEquals(false, $validator->isValid('https://example.com'));
$result = $validator->setAllowedHostnames(['example.com']);
$this->assertSame($validator, $result);
$this->assertEquals(['example.com'], $validator->getAllowedHostnames());
$this->assertEquals(true, $validator->isValid('https://example.com'));
$this->assertEquals(false, $validator->isValid('https://appwrite.io'));
}
public function testSetAllowedSchemes(): void
{
$validator = new Origin(
allowedHostnames: ['appwrite.io'],
allowedSchemes: ['exp']
);
$this->assertEquals(true, $validator->isValid('exp://'));
$this->assertEquals(false, $validator->isValid('appwrite-callback-456://'));
$result = $validator->setAllowedSchemes(['appwrite-callback-456']);
$this->assertSame($validator, $result);
$this->assertEquals(['appwrite-callback-456'], $validator->getAllowedSchemes());
$this->assertEquals(true, $validator->isValid('appwrite-callback-456://'));
$this->assertEquals(false, $validator->isValid('exp://'));
}
}
@@ -16,10 +16,19 @@ class RuntimeQueryTest extends TestCase
{
}
/**
* Helper to compile and filter queries in one step for tests.
*/
private function compileAndFilter(array $queries, array $payload): ?array
{
$compiled = RuntimeQuery::compile($queries);
return RuntimeQuery::filter($compiled, $payload);
}
public function testFilterEmptyQueries(): void
{
$payload = ['name' => 'John', 'age' => 30];
$result = RuntimeQuery::filter([], $payload);
$result = $this->compileAndFilter([], $payload);
$this->assertEquals($payload, $result);
}
@@ -27,15 +36,15 @@ class RuntimeQueryTest extends TestCase
{
$queries = [Query::equal('name', ['Jane'])];
$payload = ['name' => 'John', 'age' => 30];
$result = RuntimeQuery::filter($queries, $payload);
$this->assertEquals([], $result);
$result = $this->compileAndFilter($queries, $payload);
$this->assertNull($result);
}
public function testFilterWithMatchingQuery(): void
{
$queries = [Query::equal('name', ['John'])];
$payload = ['name' => 'John', 'age' => 30];
$result = RuntimeQuery::filter($queries, $payload);
$result = $this->compileAndFilter($queries, $payload);
$this->assertEquals($payload, $result);
}
@@ -44,7 +53,7 @@ class RuntimeQueryTest extends TestCase
{
$query = Query::equal('name', ['John']);
$payload = ['name' => 'John'];
$result = RuntimeQuery::filter([$query], $payload);
$result = $this->compileAndFilter([$query], $payload);
$this->assertEquals($payload, $result);
}
@@ -52,15 +61,15 @@ class RuntimeQueryTest extends TestCase
{
$query = Query::equal('name', ['Jane']);
$payload = ['name' => 'John'];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals([], $result);
$result = $this->compileAndFilter([$query], $payload);
$this->assertNull($result);
}
public function testEqualMultipleValuesMatch(): void
{
$query = Query::equal('status', ['active', 'pending', 'approved']);
$payload = ['status' => 'active'];
$result = RuntimeQuery::filter([$query], $payload);
$result = $this->compileAndFilter([$query], $payload);
$this->assertEquals($payload, $result);
}
@@ -68,15 +77,15 @@ class RuntimeQueryTest extends TestCase
{
$query = Query::equal('status', ['active', 'pending', 'approved']);
$payload = ['status' => 'rejected'];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals([], $result);
$result = $this->compileAndFilter([$query], $payload);
$this->assertNull($result);
}
public function testEqualNumericValues(): void
{
$query = Query::equal('age', [30, 25, 35]);
$payload = ['age' => 30];
$result = RuntimeQuery::filter([$query], $payload);
$result = $this->compileAndFilter([$query], $payload);
$this->assertEquals($payload, $result);
}
@@ -84,7 +93,7 @@ class RuntimeQueryTest extends TestCase
{
$query = Query::equal('active', [true]);
$payload = ['active' => true];
$result = RuntimeQuery::filter([$query], $payload);
$result = $this->compileAndFilter([$query], $payload);
$this->assertEquals($payload, $result);
}
@@ -92,8 +101,8 @@ class RuntimeQueryTest extends TestCase
{
$query = Query::equal('missing', ['value']);
$payload = ['name' => 'John'];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals([], $result);
$result = $this->compileAndFilter([$query], $payload);
$this->assertNull($result);
}
// TYPE_NOT_EQUAL tests
@@ -101,7 +110,7 @@ class RuntimeQueryTest extends TestCase
{
$query = Query::notEqual('name', ['Jane']);
$payload = ['name' => 'John'];
$result = RuntimeQuery::filter([$query], $payload);
$result = $this->compileAndFilter([$query], $payload);
$this->assertEquals($payload, $result);
}
@@ -109,8 +118,8 @@ class RuntimeQueryTest extends TestCase
{
$query = Query::notEqual('name', ['John']);
$payload = ['name' => 'John'];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals([], $result);
$result = $this->compileAndFilter([$query], $payload);
$this->assertNull($result);
}
public function testNotEqualMultipleValues(): void
@@ -119,13 +128,13 @@ class RuntimeQueryTest extends TestCase
// and Query::parse will be done first and parse doesn't allow multiple notEqual values
$query = Query::notEqual('status', ['rejected', 'cancelled']);
$payload = ['status' => 'active'];
$result = RuntimeQuery::filter([$query], $payload);
$result = $this->compileAndFilter([$query], $payload);
$this->assertEquals($payload, $result);
$query = Query::notEqual('status', ['active', 'pending']);
$payload = ['status' => 'active'];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals([], $result);
$result = $this->compileAndFilter([$query], $payload);
$this->assertNull($result);
}
// TYPE_LESSER tests
@@ -133,7 +142,7 @@ class RuntimeQueryTest extends TestCase
{
$query = Query::lessThan('age', 30);
$payload = ['age' => 25];
$result = RuntimeQuery::filter([$query], $payload);
$result = $this->compileAndFilter([$query], $payload);
$this->assertEquals($payload, $result);
}
@@ -141,16 +150,16 @@ class RuntimeQueryTest extends TestCase
{
$query = Query::lessThan('age', 30);
$payload = ['age' => 35];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals([], $result);
$result = $this->compileAndFilter([$query], $payload);
$this->assertNull($result);
}
public function testLesserEqualValue(): void
{
$query = Query::lessThan('age', 30);
$payload = ['age' => 30];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals([], $result);
$result = $this->compileAndFilter([$query], $payload);
$this->assertNull($result);
}
public function testLesserMultipleValues(): void
@@ -159,7 +168,7 @@ class RuntimeQueryTest extends TestCase
// This test uses a single value as Query class requires
$query = Query::lessThan('age', 30);
$payload = ['age' => 25];
$result = RuntimeQuery::filter([$query], $payload);
$result = $this->compileAndFilter([$query], $payload);
$this->assertEquals($payload, $result);
}
@@ -167,7 +176,7 @@ class RuntimeQueryTest extends TestCase
{
$query = Query::lessThan('name', 'M');
$payload = ['name' => 'A'];
$result = RuntimeQuery::filter([$query], $payload);
$result = $this->compileAndFilter([$query], $payload);
$this->assertEquals($payload, $result);
}
@@ -176,7 +185,7 @@ class RuntimeQueryTest extends TestCase
{
$query = Query::lessThanEqual('age', 30);
$payload = ['age' => 25];
$result = RuntimeQuery::filter([$query], $payload);
$result = $this->compileAndFilter([$query], $payload);
$this->assertEquals($payload, $result);
}
@@ -184,7 +193,7 @@ class RuntimeQueryTest extends TestCase
{
$query = Query::lessThanEqual('age', 30);
$payload = ['age' => 30];
$result = RuntimeQuery::filter([$query], $payload);
$result = $this->compileAndFilter([$query], $payload);
$this->assertEquals($payload, $result);
}
@@ -192,8 +201,8 @@ class RuntimeQueryTest extends TestCase
{
$query = Query::lessThanEqual('age', 30);
$payload = ['age' => 35];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals([], $result);
$result = $this->compileAndFilter([$query], $payload);
$this->assertNull($result);
}
public function testLesserEqualMultipleValues(): void
@@ -201,7 +210,7 @@ class RuntimeQueryTest extends TestCase
// Note: Query::lessThanEqual only accepts single value
$query = Query::lessThanEqual('age', 30);
$payload = ['age' => 30];
$result = RuntimeQuery::filter([$query], $payload);
$result = $this->compileAndFilter([$query], $payload);
$this->assertEquals($payload, $result);
}
@@ -210,7 +219,7 @@ class RuntimeQueryTest extends TestCase
{
$query = Query::greaterThan('age', 30);
$payload = ['age' => 35];
$result = RuntimeQuery::filter([$query], $payload);
$result = $this->compileAndFilter([$query], $payload);
$this->assertEquals($payload, $result);
}
@@ -218,16 +227,16 @@ class RuntimeQueryTest extends TestCase
{
$query = Query::greaterThan('age', 30);
$payload = ['age' => 25];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals([], $result);
$result = $this->compileAndFilter([$query], $payload);
$this->assertNull($result);
}
public function testGreaterEqualValue(): void
{
$query = Query::greaterThan('age', 30);
$payload = ['age' => 30];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals([], $result);
$result = $this->compileAndFilter([$query], $payload);
$this->assertNull($result);
}
public function testGreaterMultipleValues(): void
@@ -235,7 +244,7 @@ class RuntimeQueryTest extends TestCase
// Note: Query::greaterThan only accepts single value
$query = Query::greaterThan('age', 20);
$payload = ['age' => 35];
$result = RuntimeQuery::filter([$query], $payload);
$result = $this->compileAndFilter([$query], $payload);
$this->assertEquals($payload, $result);
}
@@ -244,7 +253,7 @@ class RuntimeQueryTest extends TestCase
{
$query = Query::greaterThanEqual('age', 30);
$payload = ['age' => 35];
$result = RuntimeQuery::filter([$query], $payload);
$result = $this->compileAndFilter([$query], $payload);
$this->assertEquals($payload, $result);
}
@@ -252,7 +261,7 @@ class RuntimeQueryTest extends TestCase
{
$query = Query::greaterThanEqual('age', 30);
$payload = ['age' => 30];
$result = RuntimeQuery::filter([$query], $payload);
$result = $this->compileAndFilter([$query], $payload);
$this->assertEquals($payload, $result);
}
@@ -260,8 +269,8 @@ class RuntimeQueryTest extends TestCase
{
$query = Query::greaterThanEqual('age', 30);
$payload = ['age' => 25];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals([], $result);
$result = $this->compileAndFilter([$query], $payload);
$this->assertNull($result);
}
public function testGreaterEqualMultipleValues(): void
@@ -269,7 +278,7 @@ class RuntimeQueryTest extends TestCase
// Note: Query::greaterThanEqual only accepts single value
$query = Query::greaterThanEqual('age', 20);
$payload = ['age' => 30];
$result = RuntimeQuery::filter([$query], $payload);
$result = $this->compileAndFilter([$query], $payload);
$this->assertEquals($payload, $result);
}
@@ -278,7 +287,7 @@ class RuntimeQueryTest extends TestCase
{
$query = Query::isNull('description');
$payload = ['description' => null];
$result = RuntimeQuery::filter([$query], $payload);
$result = $this->compileAndFilter([$query], $payload);
$this->assertEquals($payload, $result);
}
@@ -286,16 +295,16 @@ class RuntimeQueryTest extends TestCase
{
$query = Query::isNull('description');
$payload = ['description' => 'Some text'];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals([], $result);
$result = $this->compileAndFilter([$query], $payload);
$this->assertNull($result);
}
public function testIsNullMissingAttribute(): void
{
$query = Query::isNull('missing');
$payload = ['name' => 'John'];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals([], $result);
$result = $this->compileAndFilter([$query], $payload);
$this->assertNull($result);
}
// TYPE_IS_NOT_NULL tests
@@ -303,7 +312,7 @@ class RuntimeQueryTest extends TestCase
{
$query = Query::isNotNull('description');
$payload = ['description' => 'Some text'];
$result = RuntimeQuery::filter([$query], $payload);
$result = $this->compileAndFilter([$query], $payload);
$this->assertEquals($payload, $result);
}
@@ -311,16 +320,16 @@ class RuntimeQueryTest extends TestCase
{
$query = Query::isNotNull('description');
$payload = ['description' => null];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals([], $result);
$result = $this->compileAndFilter([$query], $payload);
$this->assertNull($result);
}
public function testIsNotNullMissingAttribute(): void
{
$query = Query::isNotNull('missing');
$payload = ['name' => 'John'];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals([], $result);
$result = $this->compileAndFilter([$query], $payload);
$this->assertNull($result);
}
// TYPE_AND tests
@@ -331,7 +340,7 @@ class RuntimeQueryTest extends TestCase
Query::equal('age', [30])
]);
$payload = ['name' => 'John', 'age' => 30];
$result = RuntimeQuery::filter([$query], $payload);
$result = $this->compileAndFilter([$query], $payload);
$this->assertEquals($payload, $result);
}
@@ -342,8 +351,8 @@ class RuntimeQueryTest extends TestCase
Query::equal('age', [25])
]);
$payload = ['name' => 'John', 'age' => 30];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals([], $result);
$result = $this->compileAndFilter([$query], $payload);
$this->assertNull($result);
}
public function testAndAllFail(): void
@@ -353,8 +362,8 @@ class RuntimeQueryTest extends TestCase
Query::equal('age', [25])
]);
$payload = ['name' => 'John', 'age' => 30];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals([], $result);
$result = $this->compileAndFilter([$query], $payload);
$this->assertNull($result);
}
public function testAndMultipleConditions(): void
@@ -365,7 +374,7 @@ class RuntimeQueryTest extends TestCase
Query::isNotNull('email')
]);
$payload = ['status' => 'active', 'age' => 25, 'email' => 'test@example.com'];
$result = RuntimeQuery::filter([$query], $payload);
$result = $this->compileAndFilter([$query], $payload);
$this->assertEquals($payload, $result);
}
@@ -379,7 +388,7 @@ class RuntimeQueryTest extends TestCase
])
]);
$payload = ['name' => 'John', 'age' => 30, 'status' => 'active'];
$result = RuntimeQuery::filter([$query], $payload);
$result = $this->compileAndFilter([$query], $payload);
$this->assertEquals($payload, $result);
}
@@ -391,7 +400,7 @@ class RuntimeQueryTest extends TestCase
Query::equal('name', ['Jane'])
]);
$payload = ['name' => 'John'];
$result = RuntimeQuery::filter([$query], $payload);
$result = $this->compileAndFilter([$query], $payload);
$this->assertEquals($payload, $result);
}
@@ -402,7 +411,7 @@ class RuntimeQueryTest extends TestCase
Query::equal('status', ['pending'])
]);
$payload = ['status' => 'active'];
$result = RuntimeQuery::filter([$query], $payload);
$result = $this->compileAndFilter([$query], $payload);
$this->assertEquals($payload, $result);
}
@@ -413,8 +422,8 @@ class RuntimeQueryTest extends TestCase
Query::equal('age', [25])
]);
$payload = ['name' => 'John', 'age' => 30];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals([], $result);
$result = $this->compileAndFilter([$query], $payload);
$this->assertNull($result);
}
public function testOrMultipleConditions(): void
@@ -425,7 +434,7 @@ class RuntimeQueryTest extends TestCase
Query::equal('status', ['approved'])
]);
$payload = ['status' => 'pending'];
$result = RuntimeQuery::filter([$query], $payload);
$result = $this->compileAndFilter([$query], $payload);
$this->assertEquals($payload, $result);
}
@@ -439,7 +448,7 @@ class RuntimeQueryTest extends TestCase
])
]);
$payload = ['name' => 'Bob'];
$result = RuntimeQuery::filter([$query], $payload);
$result = $this->compileAndFilter([$query], $payload);
$this->assertEquals($payload, $result);
}
@@ -450,10 +459,36 @@ class RuntimeQueryTest extends TestCase
Query::equal('email', ['john@example.com'])
]);
$payload = ['name' => 'Jane', 'email' => 'john@example.com'];
$result = RuntimeQuery::filter([$query], $payload);
$result = $this->compileAndFilter([$query], $payload);
$this->assertEquals($payload, $result);
}
public function testOrWithMissingAttributeInOneBranch(): void
{
// OR should match when one branch's attribute is missing but another branch matches
$query = Query::or([
Query::equal('name', ['John']),
Query::equal('email', ['john@example.com'])
]);
// Payload only has email, not name - should still match via email branch
$payload = ['email' => 'john@example.com'];
$result = $this->compileAndFilter([$query], $payload);
$this->assertEquals($payload, $result);
}
public function testOrWithMissingAttributeNoMatch(): void
{
// OR should not match when the only matching branch has missing attribute
$query = Query::or([
Query::equal('name', ['John']),
Query::equal('email', ['john@example.com'])
]);
// Payload only has name but it doesn't match - should not match
$payload = ['name' => 'Jane'];
$result = $this->compileAndFilter([$query], $payload);
$this->assertNull($result);
}
// Complex combinations
public function testAndOrCombination(): void
{
@@ -465,7 +500,7 @@ class RuntimeQueryTest extends TestCase
])
]);
$payload = ['type' => 'user', 'status' => 'active'];
$result = RuntimeQuery::filter([$query], $payload);
$result = $this->compileAndFilter([$query], $payload);
$this->assertEquals($payload, $result);
}
@@ -482,7 +517,7 @@ class RuntimeQueryTest extends TestCase
])
]);
$payload = ['name' => 'John', 'age' => 30];
$result = RuntimeQuery::filter([$query], $payload);
$result = $this->compileAndFilter([$query], $payload);
$this->assertEquals($payload, $result);
}
@@ -494,7 +529,7 @@ class RuntimeQueryTest extends TestCase
Query::equal('age', [30])
];
$payload = ['name' => 'John', 'age' => 30];
$result = RuntimeQuery::filter($queries, $payload);
$result = $this->compileAndFilter($queries, $payload);
$this->assertEquals($payload, $result);
}
@@ -505,9 +540,9 @@ class RuntimeQueryTest extends TestCase
Query::equal('age', [25])
];
$payload = ['name' => 'John', 'age' => 30];
$result = RuntimeQuery::filter($queries, $payload);
$result = $this->compileAndFilter($queries, $payload);
// With AND logic, if first matches but second doesn't, should return empty
$this->assertEquals([], $result);
$this->assertNull($result);
}
public function testMultipleQueriesSecondMatches(): void
@@ -517,9 +552,9 @@ class RuntimeQueryTest extends TestCase
Query::equal('age', [30])
];
$payload = ['name' => 'John', 'age' => 30];
$result = RuntimeQuery::filter($queries, $payload);
$result = $this->compileAndFilter($queries, $payload);
// With AND logic, if second matches but first doesn't, should return empty
$this->assertEquals([], $result);
$this->assertNull($result);
}
public function testMultipleQueriesNoneMatch(): void
@@ -529,23 +564,23 @@ class RuntimeQueryTest extends TestCase
Query::equal('age', [25])
];
$payload = ['name' => 'John', 'age' => 30];
$result = RuntimeQuery::filter($queries, $payload);
$this->assertEquals([], $result);
$result = $this->compileAndFilter($queries, $payload);
$this->assertNull($result);
}
public function testEmptyPayload(): void
{
$query = Query::equal('name', ['John']);
$payload = [];
$result = RuntimeQuery::filter([$query], $payload);
$this->assertEquals([], $result);
$result = $this->compileAndFilter([$query], $payload);
$this->assertNull($result);
}
public function testEmptyAndQuery(): void
{
$query = Query::and([]);
$payload = ['name' => 'John'];
$result = RuntimeQuery::filter([$query], $payload);
$result = $this->compileAndFilter([$query], $payload);
// Empty AND should return true (all conditions pass vacuously)
$this->assertEquals($payload, $result);
}
@@ -554,9 +589,9 @@ class RuntimeQueryTest extends TestCase
{
$query = Query::or([]);
$payload = ['name' => 'John'];
$result = RuntimeQuery::filter([$query], $payload);
$result = $this->compileAndFilter([$query], $payload);
// Empty OR should return false (no conditions match)
$this->assertEquals([], $result);
$this->assertNull($result);
}
// Type-specific edge cases
@@ -564,7 +599,7 @@ class RuntimeQueryTest extends TestCase
{
$query = Query::equal('count', [0]);
$payload = ['count' => 0];
$result = RuntimeQuery::filter([$query], $payload);
$result = $this->compileAndFilter([$query], $payload);
$this->assertEquals($payload, $result);
}
@@ -572,7 +607,7 @@ class RuntimeQueryTest extends TestCase
{
$query = Query::equal('name', ['']);
$payload = ['name' => ''];
$result = RuntimeQuery::filter([$query], $payload);
$result = $this->compileAndFilter([$query], $payload);
$this->assertEquals($payload, $result);
}
@@ -580,7 +615,7 @@ class RuntimeQueryTest extends TestCase
{
$query = Query::equal('active', [false]);
$payload = ['active' => false];
$result = RuntimeQuery::filter([$query], $payload);
$result = $this->compileAndFilter([$query], $payload);
$this->assertEquals($payload, $result);
}
@@ -588,7 +623,7 @@ class RuntimeQueryTest extends TestCase
{
$query = Query::greaterThan('score', 8.5);
$payload = ['score' => 9.2];
$result = RuntimeQuery::filter([$query], $payload);
$result = $this->compileAndFilter([$query], $payload);
$this->assertEquals($payload, $result);
}
@@ -596,7 +631,7 @@ class RuntimeQueryTest extends TestCase
{
$query = Query::lessThan('version', '10');
$payload = ['version' => '9'];
$result = RuntimeQuery::filter([$query], $payload);
$result = $this->compileAndFilter([$query], $payload);
$this->assertEquals($payload, $result);
}
@@ -667,7 +702,7 @@ class RuntimeQueryTest extends TestCase
{
$query = Query::select(['*']);
$payload = ['name' => 'John', 'age' => 30];
$result = RuntimeQuery::filter([$query], $payload);
$result = $this->compileAndFilter([$query], $payload);
$this->assertEquals($payload, $result);
}
@@ -679,7 +714,7 @@ class RuntimeQueryTest extends TestCase
Query::equal('name', ['Jane']), // This would normally fail
];
$payload = ['name' => 'John', 'age' => 30];
$result = RuntimeQuery::filter($queries, $payload);
$result = $this->compileAndFilter($queries, $payload);
// select("*") takes precedence - returns payload
$this->assertEquals($payload, $result);
}
@@ -688,7 +723,7 @@ class RuntimeQueryTest extends TestCase
{
$query = Query::select(['*']);
$payload = [];
$result = RuntimeQuery::filter([$query], $payload);
$result = $this->compileAndFilter([$query], $payload);
$this->assertEquals($payload, $result);
}
}