Compare commits

..
Author SHA1 Message Date
ArnabChatterjee20kandGitHub ebc3febc38 Merge branch '1.9.x' into docsdb-fixes 2026-04-15 09:42:05 +05:30
Chirag AggarwalandGitHub 8fcba8b5c9 Merge pull request #11893 from appwrite/codex/disable-graphql-functional 2026-04-14 19:19:14 +05:30
Jake BarnbyandGitHub 6c8b34c230 Merge pull request #11885 from appwrite/fix-gql-route-reset
(fix): reset route to avoid clobbering otel
2026-04-15 01:26:02 +12:00
Shimon NewmanandGitHub 377ade3c44 Merge pull request #11892 from appwrite/schedule-functions-debug-2
Schedule functions debug
2026-04-14 15:32:29 +03:00
Shimon NewmanGitHubgreptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
71533aaaf3 Update app/cli.php
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-04-14 15:17:37 +03:00
shimon ccbe7e5d3f Merge branch '1.9.x' of github.com:appwrite/appwrite into schedule-functions-debug-2 2026-04-14 15:11:56 +03:00
Chirag AggarwalandGitHub bea7739d7e Merge branch '1.9.x' into codex/disable-graphql-functional 2026-04-14 17:21:55 +05:30
Jake BarnbyandGitHub 76320d652a Merge pull request #11887 from appwrite/fix/composer-audit-graphql-php
Upgrade graphql-php to v15.31.5
2026-04-14 23:46:45 +12:00
Chirag Aggarwal 6d5968e2ea ci: disable functional GraphQL e2e 2026-04-14 16:51:46 +05:30
shimon 70c380fa36 feat: add tracing support for execution events in log and worker classes 2026-04-14 13:36:29 +03:00
shimon 512a7ae2bd feat: enhance function scheduling with tracing support 2026-04-14 13:36:19 +03:00
Chirag AggarwalandGitHub 49d3cd980f Merge branch '1.9.x' into fix/composer-audit-graphql-php 2026-04-14 15:07:22 +05:30
Luke B. SilverandGitHub 39a396810a Merge pull request #11881 from appwrite/feat/45-min-build-timeout
feat: increase default build timeout to 45 minutes
2026-04-14 09:28:29 +01:00
Jake BarnbyandGitHub 1d307178bb Merge pull request #11691 from appwrite/delete-project-shared-table-v1
Safe delete shared tables v1
2026-04-14 18:00:00 +12:00
fogelito c3ed2ff6ce Merge branch '1.9.x' of https://github.com/appwrite/appwrite into delete-project-shared-table-v1 2026-04-14 08:05:44 +03:00
Chirag Aggarwal efadf17bfe Fix GraphQL 15 static analysis 2026-04-14 10:26:59 +05:30
Chirag Aggarwal bcfec8d5de Align graphql version pinning style 2026-04-14 09:35:25 +05:30
Chirag Aggarwal 4b2e22d9da Fix graphql-php audit vulnerability 2026-04-14 09:27:14 +05:30
ArnabChatterjee20k 45e22e2243 Merge remote-tracking branch 'origin/1.9.x' into docsdb-fixes 2026-04-13 22:44:07 +05:30
Matej BačoandGitHub 0a21f3b139 Merge pull request #11884 from appwrite/fix-webhook-api-security
Fix: Webhook API secret rotation and security
2026-04-13 15:52:55 +02:00
Jake Barnby cc8eb62c83 (chore): rename 2026-04-14 01:15:06 +12:00
Luke B. SilverandGitHub b1ad6ea87c Merge pull request #11883 from appwrite/fix/large-execution-payload
fix: trim execution queue payload to project ID only
2026-04-13 14:13:43 +01:00
Jake Barnby d52d6c0bf0 (fix): reset route to avoid clobbering otel 2026-04-14 01:13:32 +12:00
Matej Bačo db406b0a27 Fix tests 2026-04-13 15:08:20 +02:00
Matej Bačo 2585518e33 Fix bugs 2026-04-13 15:08:13 +02:00
Matej Bačo c9fceb870c Fix folder structure 2026-04-13 14:57:24 +02:00
Matej Bačo 9f1ec356d1 Formatting fix 2026-04-13 14:53:25 +02:00
Matej Bačo 28d285d5c5 Improved tests for webhook edge cases 2026-04-13 14:53:06 +02:00
loks0nandClaude Sonnet 4.6 a941d8b855 fix: trim execution queue payload to project ID only
The v1-executions queue was serialising the full project document
(OAuth providers, webhooks, keys, auths config, permissions, etc.)
into every message. The worker DI system already re-fetches the
complete project from the platform database using only the project ID,
so the full document was wasted bytes.

Override trimPayload() in the Execution event class to include only
$id in the project stub, reducing message size significantly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 13:52:09 +01:00
Matej Bačo 3263133e5f Implement secure webhook interfaces 2026-04-13 14:50:06 +02:00
fogelito a3ba66c90a Merge branch '1.9.x' of https://github.com/appwrite/appwrite into delete-project-shared-table-v1
# Conflicts:
#	composer.lock
2026-04-13 13:29:03 +03:00
ArnabChatterjee20kandGitHub c24d724000 Merge pull request #11749 from appwrite/db-workers-memory
added reset in db worker for queue for realtime
2026-04-13 15:47:54 +05:30
Luke B. SilverGitHubgreptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
df5ccc10ad Apply suggestion from @greptile-apps[bot]
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-04-13 10:48:20 +01:00
loks0nandClaude Sonnet 4.6 2807d6cd9a feat: increase default build timeout to 45 minutes
Raises _APP_COMPUTE_BUILD_TIMEOUT default from 900s (15 min) to
2700s (45 min) to support longer-running builds.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 09:57:27 +01:00
ArnabChatterjee20kandGitHub 20e2f2284f Merge branch '1.9.x' into db-workers-memory 2026-04-13 13:43:14 +05:30
Matej BačoandGitHub 7a0d69c826 Merge pull request #11839 from appwrite/feat-services-protocols-apis
Feat: services protocols public apis
2026-04-13 09:38:17 +02:00
Matej Bačo feedec80f2 Merge branch '1.9.x' into feat-services-protocols-apis 2026-04-13 09:17:09 +02:00
Damodar LohaniandGitHub f4d40a1289 Merge pull request #11879 from appwrite/fix/graphql-batch-sent-reset
fix: reset response sent state between batched GraphQL queries
2026-04-13 11:35:22 +05:45
Damodar LohaniandGitHub 1e65f075e6 Merge branch '1.9.x' into fix/graphql-batch-sent-reset 2026-04-13 11:23:23 +05:45
Jake BarnbyandGitHub 68725d9262 Merge pull request #11860 from appwrite/fix-cache-fallback
(fix): cache fallback
2026-04-13 17:34:51 +12:00
Damodar LohaniandClaude Opus 4.6 5b805d686b fix: reset response sent state between batched GraphQL queries
utopia-php/http 0.34.20 added a guard that skips the action if
$response->isSent() is true. In batched GraphQL requests the resolver
reuses a single Response across all queries — after the first query's
action calls send(), subsequent queries hit the guard, their actions
are skipped, and stale/null payloads are returned.

Add Response::clearSent() to the Appwrite Response subclass (which can
access the protected $sent property from the parent) and call it in
Resolvers::resolve() before each execute(). This ensures each batched
query gets a fresh sent state while keeping the guard active for normal
request paths.

Also bumps utopia-php/http from 0.34.19 to 0.34.20 so CE CI tests
against the same version used by downstream consumers (cloud).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 05:32:04 +00:00
Chirag AggarwalandGitHub 584acafb1d Merge branch '1.9.x' into feat-services-protocols-apis 2026-04-13 10:45:42 +05:30
Chirag AggarwalandGitHub dce7856b77 Merge pull request #11848 from appwrite/fix/spec-generator-console-pr82 2026-04-13 10:40:47 +05:30
Chirag Aggarwal a6af609317 Remove scopes spec override, now fixed at source in #11839 2026-04-13 10:33:46 +05:30
Chirag Aggarwal 035f6244e1 Revert "fix: require scopes for project keys"
This reverts commit 8deafcaf4d52a59cc2e1b27c7a128e8b7843afa4.
2026-04-13 10:33:46 +05:30
Chirag Aggarwal 723cb1a488 fix: require scopes for project keys 2026-04-13 10:33:46 +05:30
Chirag Aggarwal 815209ebb0 fix: address sdk spec review feedback 2026-04-13 10:33:46 +05:30
Chirag Aggarwal 53c74582fc refactor: simplify request parameter spec overrides 2026-04-13 10:33:46 +05:30
Chirag Aggarwal 78bbe77580 fix: align project sdk spec generation 2026-04-13 10:33:45 +05:30
0c3871a681 fix: pass response to Http::execute() in GraphQL resolver (#11876)
Http::execute() now requires a Response parameter as of utopia-php/http
0.34.20. The GraphQL resolver was only passing route and request,
causing all GraphQL queries to fail with "Internal server error".

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 16:33:32 +12:00
Chirag AggarwalandGitHub b59eba4ec6 Merge pull request #11874 from appwrite/fix/11763-installer-compose-executor 2026-04-13 08:52:39 +05:30
Chirag Aggarwal cb4c97f2ee chore: remove installer compose regression test 2026-04-12 14:11:51 +05:30
Chirag Aggarwal 2ee2ea09a0 fix(installer): sync compose template executor image 2026-04-12 13:56:49 +05:30
Chirag AggarwalandGitHub 4ec84a147f Merge pull request #11861 from appwrite/fix-edge-pzj-rule-deployment-resource-type-optional
Make rule deploymentResourceType optional for non-deployment rules
2026-04-11 22:08:17 +05:30
Chirag AggarwalandGitHub e583de4650 Merge pull request #11858 from appwrite/fix-cve-2026-40194-phpseclib-bump
Bump phpseclib to 3.0.51 for CVE-2026-40194
2026-04-11 22:07:47 +05:30
Chirag Aggarwal 98af2a5eb3 fix: make rule deploymentResourceType optional 2026-04-11 22:05:01 +05:30
Jake BarnbyandClaude Opus 4.6 e3ad0f85de fix: narrow cache try-catch to avoid swallowing query exceptions
Wrap only cache load/save calls in try-catch instead of the entire
cache block. This prevents OrderException, QueryException, and Timeout
from $find() being caught and retried, which would double DB calls and
hide real query errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 03:42:47 +12:00
Jake Barnby a26382ac51 (chore): lockfile 2026-04-12 03:25:07 +12:00
Jake Barnby 9ba182d8a0 (fix): cache fallback 2026-04-12 03:22:21 +12:00
Matej Bačo 27fc8058b9 Fix failing tests 2026-04-11 14:19:05 +02:00
Matej Bačo c5bd8c712f Upgrade libs 2026-04-11 11:31:35 +02:00
Matej Bačo a1267b1bff Backwards compatibiltiy tests 2026-04-11 11:16:43 +02:00
Matej Bačo 18d17ea945 Webhook endpoints quality improvements 2026-04-11 11:00:56 +02:00
Matej Bačo fabd9559c4 Tests for backwards compatibility 2026-04-11 10:22:03 +02:00
Matej Bačo ec637d4417 Mark key scopes required 2026-04-11 10:19:14 +02:00
Chirag Aggarwal 4523e86b91 fix: bump phpseclib to 3.0.51 2026-04-11 09:01:42 +05:30
Luke B. SilverandGitHub 1ea108c2ce Merge pull request #11854 from appwrite/feat/remove-err
feat: remove error logs
2026-04-10 14:18:33 +01:00
loks0n 0a864e51b8 feat: remove error logs 2026-04-10 14:17:24 +01:00
ArnabChatterjee20k 0cce480592 typo 2026-04-10 17:23:43 +05:30
ArnabChatterjee20k b622b092a8 updated 2026-04-10 17:22:22 +05:30
ArnabChatterjee20k 96fe989f6d update composer dependencies and remove obsolete log classes 2026-04-10 17:15:31 +05:30
Chirag AggarwalandGitHub c6dd7de216 Merge pull request #11850 from appwrite/chore-migrate-selected-queues-to-publishers
Migrate executions, migrations, and stats resources to publishers
2026-04-10 15:13:15 +05:30
Chirag Aggarwal f77a64bff9 chore: address publisher PR nits 2026-04-10 14:00:57 +05:30
ArnabChatterjee20k d13dbae0fe added allowance of empty payload for documentsdb 2026-04-10 13:59:45 +05:30
Luke B. SilverandGitHub f552a1ba15 Merge pull request #11844 from appwrite/feat/storage-cache-spans
feat: add tracing spans for storage file preview timing and cache state
2026-04-10 09:18:42 +01:00
Chirag Aggarwal 7282c5d51f chore: remove unused execution exclusion 2026-04-10 13:25:32 +05:30
Chirag Aggarwal 82ec75d582 chore: address PR review feedback 2026-04-10 13:12:08 +05:30
Chirag Aggarwal 6bf6142667 refactor: migrate selected queues to publishers 2026-04-10 13:02:00 +05:30
Chirag AggarwalandGitHub 938e65cb02 Merge pull request #11831 from appwrite/codex/remove-realtime-http-dependency
Use dedicated connection resources in realtime
2026-04-10 12:54:45 +05:30
ArnabChatterjee20kandGitHub 114de91f48 Merge pull request #11767 from appwrite/realtime-query-message-payload
Realtime query message payload
2026-04-10 12:06:00 +05:30
ArnabChatterjee20k 2e6f3f5c14 typo 2026-04-10 11:13:03 +05:30
ArnabChatterjee20k 7b3d9bae03 updated authorization 2026-04-10 11:04:44 +05:30
Chirag Aggarwal d81a1154e3 refactor: isolate realtime connection resources 2026-04-10 10:19:41 +05:30
Chirag Aggarwal a944c65660 refactor: move worker message resources 2026-04-10 09:43:32 +05:30
Chirag Aggarwal 856046dc82 shrink the size 2026-04-10 09:28:17 +05:30
Chirag Aggarwal 2ca551123d use connection container 2026-04-10 09:25:00 +05:30
Chirag Aggarwal c861d45749 Merge branch '1.9.x' into codex/remove-realtime-http-dependency 2026-04-10 09:02:23 +05:30
loks0nandClaude Sonnet 4.6 4a43969da9 fix: use consistent dot notation for all storage span attribute names
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 19:53:20 +01:00
loks0nandClaude Sonnet 4.6 6fa4122910 fix: rename storage span attributes to use dot notation for ids
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 19:52:25 +01:00
loks0nandClaude Sonnet 4.6 1d27101770 feat: add tracing spans for storage file preview timing and cache state
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 19:49:57 +01:00
Luke B. SilverandGitHub 9214decc8d Merge pull request #11843 from appwrite/fix/session-mails
fix: set project on mail queue in session mails listener
2026-04-09 18:30:24 +01:00
Matej Bačo d69726487e PR review fixes 2026-04-09 16:58:42 +02:00
Matej Bačo 5fccb8cc28 Improve tests 2026-04-09 16:57:44 +02:00
Matej Bačo 21a0d60c98 Fix tests 2026-04-09 16:13:54 +02:00
Matej Bačo 4eb8534294 Fix tests 2026-04-09 16:08:11 +02:00
Matej Bačo c95f905bce New services and protocols tests 2026-04-09 15:58:28 +02:00
Matej Bačo 0293da1e22 Improve test for backwards compatibility 2026-04-09 15:54:00 +02:00
Matej Bačo a4a0c4175d Implement new endpoints in /v1/project for services and protocols 2026-04-09 15:45:06 +02:00
Matej Bačo d3c73fbb49 Add endpoints to control protocols and services 2026-04-09 15:34:50 +02:00
Matej Bačo 8818187740 Introduce req&res filters for 1.9.1 2026-04-09 15:21:58 +02:00
Matej Bačo 75324b24fc Improve skill 2026-04-09 15:21:24 +02:00
Matej Bačo d6d118f4ab Bump version to 1.9.1 2026-04-09 15:19:58 +02:00
Matej Bačo e998739998 Add agent skill to increase patch version 2026-04-09 15:18:26 +02:00
ArnabChatterjee20k 912dbda159 updated type 2026-04-09 18:16:09 +05:30
ArnabChatterjee20k 410a050244 updated 2026-04-09 18:04:01 +05:30
ArnabChatterjee20k 9cf45816c2 added triggering stats for messaging based subscription during the start 2026-04-09 17:38:25 +05:30
ArnabChatterjee20kandGitHub 920ddd18e6 Merge branch '1.9.x' into realtime-query-message-payload 2026-04-09 17:24:57 +05:30
Chirag Aggarwal bf489ce13b Fix requestRoutePath fallback 2026-04-09 11:09:21 +05:30
Chirag Aggarwal 357d6482f9 Remove realtime HTTP app dependency 2026-04-09 10:52:31 +05:30
ArnabChatterjee20kandGitHub da2444ede2 Merge branch '1.9.x' into realtime-query-message-payload 2026-04-07 17:36:29 +05:30
ArnabChatterjee20k bc224de751 Add userId to connection info in Realtime adapter and simplify userId fetching 2026-04-07 17:35:48 +05:30
ArnabChatterjee20k ca62504b5a Enhance realtime message handling to support initial connection payload and improve query subscription logic 2026-04-07 16:55:10 +05:30
ArnabChatterjee20k d5fe5c34af Validate subscribe payload format in realtime message handling 2026-04-06 17:14:12 +05:30
ArnabChatterjee20k 6bc9adece8 Refactor realtime message handling to send subscriber keys and add comprehensive tests for subscription message upsert behavior 2026-04-06 17:10:57 +05:30
ArnabChatterjee20k 97d46c6273 Remove redundant subscription removal call in realtime message handling 2026-04-06 16:58:08 +05:30
ArnabChatterjee20k 9d78a8e6b6 Add stats tracking for outbound subscription messages in realtime 2026-04-06 16:57:06 +05:30
ArnabChatterjee20k d12a6f5168 Refactor realtime message payload handling for improved validation and parsing 2026-04-06 16:41:42 +05:30
ArnabChatterjee20k 0f47e6ea28 Enhance subscription message documentation for clarity on upsertion behavior 2026-04-06 15:59:22 +05:30
ArnabChatterjee20k 592629587d Remove unused query assertion methods and improve comment clarity in RealtimeQueryBase 2026-04-06 14:07:25 +05:30
ArnabChatterjee20k 187fde4a4e Refactor realtime subscription handling and enhance query validation in tests 2026-04-06 14:05:42 +05:30
fogelito 31728c9b72 Update lock 2026-04-06 09:29:58 +03:00
fogelito 9d0fc9e5b1 Merge branch '1.9.x' of https://github.com/appwrite/appwrite into delete-project-shared-table-v1 2026-04-06 09:19:38 +03:00
fogelito a332eb5f32 Merge branch '1.9.x' of https://github.com/appwrite/appwrite into delete-project-shared-table-v1 2026-04-05 11:42:25 +03:00
fogelito 775d21e5ee lock 2026-04-05 10:08:29 +03:00
ArnabChatterjee20k bfbf180aee Refactor realtime message handling and enhance query validation tests 2026-04-02 18:32:27 +05:30
ArnabChatterjee20k d8a3b53641 Refactor code structure for improved readability and maintainability 2026-04-02 17:35:55 +05:30
ArnabChatterjee20k df4dbcf607 updated user roles 2026-04-02 17:02:46 +05:30
ArnabChatterjee20k 29b0ebb3bd updated query subscription 2026-04-02 16:28:00 +05:30
ArnabChatterjee20k f0ccd1f586 added message based query payload to realtime 2026-04-02 15:56:56 +05:30
ArnabChatterjee20k cf99269bc5 Add reset method to Realtime class for clearing event state
This new method resets the event state for long-lived worker processes by clearing subscribers, context, and user-related fields, ensuring no stale state affects subsequent triggers.
2026-04-01 18:08:45 +05:30
ArnabChatterjee20k 2a184288f4 removed throw 2026-04-01 18:05:56 +05:30
ArnabChatterjee20k 9237f387fc added reset in db worker for queue for realtime 2026-04-01 17:59:28 +05:30
fogelito e42a26fdad comment 2026-03-31 19:20:13 +03:00
fogelito 2c4770d29b Database 5.3.19 2026-03-31 19:18:32 +03:00
fogelito 2a9e423cb1 Temporary disabling deletes from internal collections 2026-03-31 12:54:33 +03:00
fogelito 14869cc6d6 Safe delete shared tables v1 2026-03-31 09:59:53 +03:00
106 changed files with 6656 additions and 3476 deletions
@@ -0,0 +1,29 @@
# Patch Release Checklist for Appwrite
When bumping a patch version (e.g., `1.9.0` -> `1.9.1`), follow this checklist.
## Checklist
### Bump console image
Update the console Docker image tag in both files:
- [ ] `docker-compose.yml` -- update `image: appwrite/console:X.Y.Z`
- [ ] `app/views/install/compose.phtml` -- update `image: <?php echo $organization; ?>/console:X.Y.Z`
### Bump Appwrite version
- [ ] **`app/init/constants.php`** -- update `APP_VERSION_STABLE` to the new version (e.g., `'1.9.1'`). In same file, increment `APP_CACHE_BUSTER` by 1.
- [ ] **`README.md`** -- update the Docker image tag `appwrite/appwrite:X.Y.Z` in all 3 install code blocks (Unix, Windows CMD, PowerShell).
- [ ] **`README-CN.md`** -- same Docker image tag update in all 3 install code blocks.
- [ ] **`src/Appwrite/Migration/Migration.php`** -- add the new version to the `$versions` array, mapping it to a migration class. If new class exists, use that, otherwise use sle same class as previous version
### Update CHANGES.md
- [ ] Add a new `# Version X.Y.Z` section at the top of `CHANGES.md` with subsections: `### Notable changes`, `### Fixes`, `### Miscellaneous`
## Final review
- [ ] Ask user to review changes before commiting
- [ ] Ask user to update `CHANGES.md` with PRs
- [ ] Ask user to generate specs, if needed
- [ ] Ask user to add request and response filters, if needed
+1 -1
View File
@@ -512,7 +512,7 @@ jobs:
# Services that rely on sequential test method execution (shared static state)
FUNCTIONAL_FLAG="--functional"
case "${{ matrix.service }}" in
Databases|TablesDB|Functions|Realtime) FUNCTIONAL_FLAG="" ;;
Databases|TablesDB|Functions|Realtime|GraphQL) FUNCTIONAL_FLAG="" ;;
esac
docker compose exec -T \
+4
View File
@@ -115,6 +115,10 @@ Common injections: `$response`, `$request`, `$dbForProject`, `$dbForPlatform`, `
- Never hardcode credentials -- use environment variables.
- Code changes may require container restart. No central log location -- check relevant containers.
## Patch release process
For bumping patch versions (e.g., `1.9.0` -> `1.9.1`), follow the checklist in `.claude/skills/patch-release-checklist/SKILL.md`. It covers the 4 files that must be updated, console image bumps, CHANGES.md updates, and common pitfalls to avoid.
## Cross-repo context
Appwrite is the base server for `appwrite/cloud`. Changes to the Action pattern, module structure, DI system, or response models affect cloud. The `feat-dedicated-db` feature spans cloud, edge, and console.
+3 -3
View File
@@ -72,7 +72,7 @@ docker run -it --rm \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
--entrypoint="install" \
appwrite/appwrite:1.9.0
appwrite/appwrite:1.9.1
```
### Windows
@@ -84,7 +84,7 @@ docker run -it --rm ^
--volume //var/run/docker.sock:/var/run/docker.sock ^
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
--entrypoint="install" ^
appwrite/appwrite:1.9.0
appwrite/appwrite:1.9.1
```
#### PowerShell
@@ -94,7 +94,7 @@ docker run -it --rm `
--volume /var/run/docker.sock:/var/run/docker.sock `
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw `
--entrypoint="install" `
appwrite/appwrite:1.9.0
appwrite/appwrite:1.9.1
```
运行后,可以在浏览器上访问 http://localhost 找到 Appwrite 控制台。在非 Linux 的本机主机上完成安装后,服务器可能需要几分钟才能启动。
+3 -3
View File
@@ -75,7 +75,7 @@ docker run -it --rm \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
--entrypoint="install" \
appwrite/appwrite:1.9.0
appwrite/appwrite:1.9.1
```
### Windows
@@ -88,7 +88,7 @@ docker run -it --rm ^
--volume //var/run/docker.sock:/var/run/docker.sock ^
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
--entrypoint="install" ^
appwrite/appwrite:1.9.0
appwrite/appwrite:1.9.1
```
#### PowerShell
@@ -99,7 +99,7 @@ docker run -it --rm `
--volume /var/run/docker.sock:/var/run/docker.sock `
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw `
--entrypoint="install" `
appwrite/appwrite:1.9.0
appwrite/appwrite:1.9.1
```
Once the Docker installation is complete, go to http://localhost to access the Appwrite console from your browser. Please note that on non-Linux native hosts, the server might take a few minutes to start after completing the installation.
+5 -4
View File
@@ -6,8 +6,8 @@ use Appwrite\Event\Certificate;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Publisher\StatsResources as StatsResourcesPublisher;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Event\StatsResources;
use Appwrite\Platform\Appwrite;
use Appwrite\Runtimes\Runtimes;
use Appwrite\Usage\Context as UsageContext;
@@ -253,9 +253,10 @@ $container->set('publisherForUsage', fn (Publisher $publisher) => new UsagePubli
$publisher,
new Queue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME))
), ['publisher']);
$container->set('queueForStatsResources', function (Publisher $publisher) {
return new StatsResources($publisher);
}, ['publisher']);
$container->set('publisherForStatsResources', fn (Publisher $publisher) => new StatsResourcesPublisher(
$publisher,
new Queue(System::getEnv('_APP_STATS_RESOURCES_QUEUE_NAME', Event::STATS_RESOURCES_QUEUE_NAME))
), ['publisher']);
$container->set('queueForFunctions', function (Publisher $publisher) {
return new Func($publisher);
}, ['publisher']);
@@ -9,8 +9,8 @@ return [
'key' => 'graphql',
'name' => 'GraphQL',
],
'realtime' => [
'key' => 'realtime',
'name' => 'Realtime',
'websocket' => [
'key' => 'websocket',
'name' => 'Websocket',
],
];
+4 -4
View File
@@ -137,7 +137,7 @@ return [
'docs' => true,
'docsUrl' => '',
'tests' => false,
'optional' => false,
'optional' => true,
'icon' => '',
'platforms' => ['client', 'server', 'console'],
],
@@ -193,7 +193,7 @@ return [
'docs' => false,
'docsUrl' => '',
'tests' => false,
'optional' => false,
'optional' => true,
'icon' => '',
'platforms' => ['client', 'server', 'console'],
],
@@ -235,7 +235,7 @@ return [
'docs' => true,
'docsUrl' => 'https://appwrite.io/docs/proxy',
'tests' => false,
'optional' => false,
'optional' => true,
'icon' => '/images/services/proxy.png',
'platforms' => ['client', 'server', 'console'],
],
@@ -291,7 +291,7 @@ return [
'docs' => true,
'docsUrl' => 'https://appwrite.io/docs/migrations',
'tests' => true,
'optional' => false,
'optional' => true,
'icon' => '/images/services/migrations.png',
'platforms' => ['client', 'server', 'console'],
],
+4 -4
View File
@@ -872,18 +872,18 @@ return [
],
[
'name' => '_APP_FUNCTIONS_BUILD_TIMEOUT',
'description' => 'Deprecated since 1.7.0. The maximum number of seconds allowed as a timeout value when building a new function. The default value is 900 seconds.',
'description' => 'Deprecated since 1.7.0. The maximum number of seconds allowed as a timeout value when building a new function. The default value is 2700 seconds.',
'introduction' => '0.13.0',
'default' => '900',
'default' => '2700',
'required' => false,
'question' => '',
'filter' => ''
],
[
'name' => '_APP_COMPUTE_BUILD_TIMEOUT',
'description' => 'The maximum number of seconds allowed as a timeout value when building a new function or site. The default value is 900 seconds.',
'description' => 'The maximum number of seconds allowed as a timeout value when building a new function or site. The default value is 2700 seconds.',
'introduction' => '1.7.0',
'default' => '900',
'default' => '2700',
'required' => false,
'question' => '',
'filter' => ''
+1 -1
View File
@@ -231,7 +231,7 @@ function execute(
$validations = GraphQL::getStandardValidationRules();
if (System::getEnv('_APP_GRAPHQL_INTROSPECTION', 'enabled') === 'disabled') {
$validations[] = new DisableIntrospection();
$validations[] = new DisableIntrospection(DisableIntrospection::ENABLED);
}
if (System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled') {
+64 -74
View File
@@ -1,7 +1,8 @@
<?php
use Appwrite\Event\Event;
use Appwrite\Event\Migration;
use Appwrite\Event\Message\Migration as MigrationMessage;
use Appwrite\Event\Publisher\Migration as MigrationPublisher;
use Appwrite\Extend\Exception;
use Appwrite\OpenSSL\OpenSSL;
use Appwrite\SDK\AuthType;
@@ -90,10 +91,9 @@ Http::post('/v1/migrations/appwrite')
->inject('dbForProject')
->inject('project')
->inject('platform')
->inject('user')
->inject('queueForEvents')
->inject('queueForMigrations')
->action(function (array $resources, string $endpoint, string $projectId, string $apiKey, Response $response, Database $dbForProject, Document $project, array $platform, Document $user, Event $queueForEvents, Migration $queueForMigrations) {
->inject('publisherForMigrations')
->action(function (array $resources, string $endpoint, string $projectId, string $apiKey, Response $response, Database $dbForProject, Document $project, array $platform, Event $queueForEvents, MigrationPublisher $publisherForMigrations) {
$migration = $dbForProject->createDocument('migrations', new Document([
'$id' => ID::unique(),
'status' => 'pending',
@@ -114,12 +114,11 @@ Http::post('/v1/migrations/appwrite')
$queueForEvents->setParam('migrationId', $migration->getId());
// Trigger Transfer
$queueForMigrations
->setMigration($migration)
->setProject($project)
->setPlatform($platform)
->setUser($user)
->trigger();
$publisherForMigrations->enqueue(new MigrationMessage(
project: $project,
migration: $migration,
platform: $platform,
));
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
@@ -151,10 +150,9 @@ Http::post('/v1/migrations/firebase')
->inject('dbForProject')
->inject('project')
->inject('platform')
->inject('user')
->inject('queueForEvents')
->inject('queueForMigrations')
->action(function (array $resources, string $serviceAccount, Response $response, Database $dbForProject, Document $project, array $platform, Document $user, Event $queueForEvents, Migration $queueForMigrations) {
->inject('publisherForMigrations')
->action(function (array $resources, string $serviceAccount, Response $response, Database $dbForProject, Document $project, array $platform, Event $queueForEvents, MigrationPublisher $publisherForMigrations) {
$serviceAccountData = json_decode($serviceAccount, true);
if (empty($serviceAccountData)) {
@@ -183,12 +181,11 @@ Http::post('/v1/migrations/firebase')
$queueForEvents->setParam('migrationId', $migration->getId());
// Trigger Transfer
$queueForMigrations
->setMigration($migration)
->setProject($project)
->setPlatform($platform)
->setUser($user)
->trigger();
$publisherForMigrations->enqueue(new MigrationMessage(
project: $project,
migration: $migration,
platform: $platform,
));
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
@@ -225,10 +222,9 @@ Http::post('/v1/migrations/supabase')
->inject('dbForProject')
->inject('project')
->inject('platform')
->inject('user')
->inject('queueForEvents')
->inject('queueForMigrations')
->action(function (array $resources, string $endpoint, string $apiKey, string $databaseHost, string $username, string $password, int $port, Response $response, Database $dbForProject, Document $project, array $platform, Document $user, Event $queueForEvents, Migration $queueForMigrations) {
->inject('publisherForMigrations')
->action(function (array $resources, string $endpoint, string $apiKey, string $databaseHost, string $username, string $password, int $port, Response $response, Database $dbForProject, Document $project, array $platform, Event $queueForEvents, MigrationPublisher $publisherForMigrations) {
$migration = $dbForProject->createDocument('migrations', new Document([
'$id' => ID::unique(),
'status' => 'pending',
@@ -252,12 +248,11 @@ Http::post('/v1/migrations/supabase')
$queueForEvents->setParam('migrationId', $migration->getId());
// Trigger Transfer
$queueForMigrations
->setMigration($migration)
->setProject($project)
->setPlatform($platform)
->setUser($user)
->trigger();
$publisherForMigrations->enqueue(new MigrationMessage(
project: $project,
migration: $migration,
platform: $platform,
));
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
@@ -295,10 +290,9 @@ Http::post('/v1/migrations/nhost')
->inject('dbForProject')
->inject('project')
->inject('platform')
->inject('user')
->inject('queueForEvents')
->inject('queueForMigrations')
->action(function (array $resources, string $subdomain, string $region, string $adminSecret, string $database, string $username, string $password, int $port, Response $response, Database $dbForProject, Document $project, array $platform, Document $user, Event $queueForEvents, Migration $queueForMigrations) {
->inject('publisherForMigrations')
->action(function (array $resources, string $subdomain, string $region, string $adminSecret, string $database, string $username, string $password, int $port, Response $response, Database $dbForProject, Document $project, array $platform, Event $queueForEvents, MigrationPublisher $publisherForMigrations) {
$migration = $dbForProject->createDocument('migrations', new Document([
'$id' => ID::unique(),
'status' => 'pending',
@@ -323,12 +317,11 @@ Http::post('/v1/migrations/nhost')
$queueForEvents->setParam('migrationId', $migration->getId());
// Trigger Transfer
$queueForMigrations
->setMigration($migration)
->setProject($project)
->setPlatform($platform)
->setUser($user)
->trigger();
$publisherForMigrations->enqueue(new MigrationMessage(
project: $project,
migration: $migration,
platform: $platform,
));
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
@@ -368,7 +361,7 @@ Http::post('/v1/migrations/csv/imports')
->inject('deviceForFiles')
->inject('deviceForMigrations')
->inject('queueForEvents')
->inject('queueForMigrations')
->inject('publisherForMigrations')
->action(function (
string $bucketId,
string $fileId,
@@ -383,7 +376,7 @@ Http::post('/v1/migrations/csv/imports')
Device $deviceForFiles,
Device $deviceForMigrations,
Event $queueForEvents,
Migration $queueForMigrations
MigrationPublisher $publisherForMigrations
) {
$bucket = $authorization->skip(function () use ($internalFile, $dbForPlatform, $dbForProject, $bucketId) {
if ($internalFile) {
@@ -479,11 +472,10 @@ Http::post('/v1/migrations/csv/imports')
$queueForEvents->setParam('migrationId', $migration->getId());
$queueForMigrations
->setMigration($migration)
->setProject($project)
->setProject($project)
->trigger();
$publisherForMigrations->enqueue(new MigrationMessage(
project: $project,
migration: $migration,
));
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
@@ -526,7 +518,7 @@ Http::post('/v1/migrations/csv/exports')
->inject('project')
->inject('platform')
->inject('queueForEvents')
->inject('queueForMigrations')
->inject('publisherForMigrations')
->action(function (
string $resourceId,
string $filename,
@@ -545,7 +537,7 @@ Http::post('/v1/migrations/csv/exports')
Document $project,
array $platform,
Event $queueForEvents,
Migration $queueForMigrations
MigrationPublisher $publisherForMigrations
) {
try {
$parsedQueries = Query::parseQueries($queries);
@@ -630,11 +622,11 @@ Http::post('/v1/migrations/csv/exports')
$queueForEvents->setParam('migrationId', $migration->getId());
$queueForMigrations
->setMigration($migration)
->setProject($project)
->setPlatform($platform)
->trigger();
$publisherForMigrations->enqueue(new MigrationMessage(
project: $project,
migration: $migration,
platform: $platform,
));
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
@@ -673,7 +665,7 @@ Http::post('/v1/migrations/json/imports')
->inject('deviceForFiles')
->inject('deviceForMigrations')
->inject('queueForEvents')
->inject('queueForMigrations')
->inject('publisherForMigrations')
->action(function (
string $bucketId,
string $fileId,
@@ -688,7 +680,7 @@ Http::post('/v1/migrations/json/imports')
Device $deviceForFiles,
Device $deviceForMigrations,
Event $queueForEvents,
Migration $queueForMigrations
MigrationPublisher $publisherForMigrations
) {
$bucket = $authorization->skip(function () use ($internalFile, $dbForPlatform, $dbForProject, $bucketId) {
if ($internalFile) {
@@ -783,11 +775,11 @@ Http::post('/v1/migrations/json/imports')
$queueForEvents->setParam('migrationId', $migration->getId());
$queueForMigrations
->setMigration($migration)
->setProject($project)
->setPlatform($platform)
->trigger();
$publisherForMigrations->enqueue(new MigrationMessage(
project: $project,
migration: $migration,
platform: $platform,
));
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
@@ -826,7 +818,7 @@ Http::post('/v1/migrations/json/exports')
->inject('project')
->inject('platform')
->inject('queueForEvents')
->inject('queueForMigrations')
->inject('publisherForMigrations')
->action(function (
string $resourceId,
string $filename,
@@ -841,7 +833,7 @@ Http::post('/v1/migrations/json/exports')
Document $project,
array $platform,
Event $queueForEvents,
Migration $queueForMigrations
MigrationPublisher $publisherForMigrations
) {
try {
$parsedQueries = Query::parseQueries($queries);
@@ -915,11 +907,11 @@ Http::post('/v1/migrations/json/exports')
$queueForEvents->setParam('migrationId', $migration->getId());
$queueForMigrations
->setMigration($migration)
->setProject($project)
->setPlatform($platform)
->trigger();
$publisherForMigrations->enqueue(new MigrationMessage(
project: $project,
migration: $migration,
platform: $platform,
));
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
@@ -1216,9 +1208,8 @@ Http::patch('/v1/migrations/:migrationId')
->inject('dbForProject')
->inject('project')
->inject('platform')
->inject('user')
->inject('queueForMigrations')
->action(function (string $migrationId, Response $response, Database $dbForProject, Document $project, array $platform, Document $user, Migration $queueForMigrations) {
->inject('publisherForMigrations')
->action(function (string $migrationId, Response $response, Database $dbForProject, Document $project, array $platform, MigrationPublisher $publisherForMigrations) {
$migration = $dbForProject->getDocument('migrations', $migrationId);
if ($migration->isEmpty()) {
@@ -1234,12 +1225,11 @@ Http::patch('/v1/migrations/:migrationId')
->setAttribute('dateUpdated', \time());
// Trigger Migration
$queueForMigrations
->setMigration($migration)
->setProject($project)
->setPlatform($platform)
->setUser($user)
->trigger();
$publisherForMigrations->enqueue(new MigrationMessage(
project: $project,
migration: $migration,
platform: $platform,
));
$response->noContent();
});
+4 -186
View File
@@ -71,202 +71,20 @@ Http::get('/v1/projects/:projectId')
$response->dynamic($project, Response::MODEL_PROJECT);
});
Http::patch('/v1/projects/:projectId/service')
->desc('Update service status')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'projects',
name: 'updateServiceStatus',
description: '/docs/references/projects/update-service-status.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROJECT,
)
]
))
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param('service', '', new WhiteList(array_keys(array_filter(Config::getParam('services'), fn ($element) => $element['optional'])), true), 'Service name.')
->param('status', null, new Boolean(), 'Service status.')
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, string $service, bool $status, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$services = $project->getAttribute('services', []);
$services[$service] = $status;
$project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('services', $services));
$response->dynamic($project, Response::MODEL_PROJECT);
});
Http::patch('/v1/projects/:projectId/service/all')
->desc('Update all service status')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'projects',
name: 'updateServiceStatusAll',
description: '/docs/references/projects/update-service-status-all.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROJECT,
)
]
))
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param('status', null, new Boolean(), 'Service status.')
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, bool $status, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$allServices = array_keys(array_filter(Config::getParam('services'), fn ($element) => $element['optional']));
$services = [];
foreach ($allServices as $service) {
$services[$service] = $status;
}
$project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('services', $services));
$response->dynamic($project, Response::MODEL_PROJECT);
});
Http::patch('/v1/projects/:projectId/api')
->desc('Update API status')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk', [
new Method(
namespace: 'projects',
group: 'projects',
name: 'updateApiStatus',
description: '/docs/references/projects/update-api-status.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROJECT,
)
],
deprecated: new Deprecated(
since: '1.8.0',
replaceWith: 'projects.updateAPIStatus',
),
public: false,
),
new Method(
namespace: 'projects',
group: 'projects',
name: 'updateAPIStatus',
description: '/docs/references/projects/update-api-status.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROJECT,
)
]
)
])
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param('api', '', new WhiteList(array_keys(Config::getParam('apis')), true), 'API name.')
->param('status', null, new Boolean(), 'API status.')
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, string $api, bool $status, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$apis = $project->getAttribute('apis', []);
$apis[$api] = $status;
$project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('apis', $apis));
$response->dynamic($project, Response::MODEL_PROJECT);
->action(function () {
throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED, 'Bulk API no longer exists for services. Please change status individually.');
});
Http::patch('/v1/projects/:projectId/api/all')
->desc('Update all API status')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk', [
new Method(
namespace: 'projects',
group: 'projects',
name: 'updateApiStatusAll',
description: '/docs/references/projects/update-api-status-all.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROJECT,
)
],
deprecated: new Deprecated(
since: '1.8.0',
replaceWith: 'projects.updateAPIStatusAll',
),
public: false,
),
new Method(
namespace: 'projects',
group: 'projects',
name: 'updateAPIStatusAll',
description: '/docs/references/projects/update-api-status-all.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROJECT,
)
]
)
])
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
->param('status', null, new Boolean(), 'API status.')
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, bool $status, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$allApis = array_keys(Config::getParam('apis'));
$apis = [];
foreach ($allApis as $api) {
$apis[$api] = $status;
}
$project = $dbForPlatform->updateDocument('projects', $project->getId(), $project->setAttribute('apis', $apis));
$response->dynamic($project, Response::MODEL_PROJECT);
->action(function () {
throw new Exception(Exception::GENERAL_NOT_IMPLEMENTED, 'Bulk API no longer exists for services. Please change status individually.');
});
Http::patch('/v1/projects/:projectId/oauth2')
+10 -12
View File
@@ -25,6 +25,7 @@ use Appwrite\Utopia\Request\Filters\V18 as RequestV18;
use Appwrite\Utopia\Request\Filters\V19 as RequestV19;
use Appwrite\Utopia\Request\Filters\V20 as RequestV20;
use Appwrite\Utopia\Request\Filters\V21 as RequestV21;
use Appwrite\Utopia\Request\Filters\V22 as RequestV22;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Filters\V16 as ResponseV16;
use Appwrite\Utopia\Response\Filters\V17 as ResponseV17;
@@ -32,6 +33,7 @@ use Appwrite\Utopia\Response\Filters\V18 as ResponseV18;
use Appwrite\Utopia\Response\Filters\V19 as ResponseV19;
use Appwrite\Utopia\Response\Filters\V20 as ResponseV20;
use Appwrite\Utopia\Response\Filters\V21 as ResponseV21;
use Appwrite\Utopia\Response\Filters\V22 as ResponseV22;
use Appwrite\Utopia\View;
use Executor\Executor;
use MaxMind\Db\Reader;
@@ -892,6 +894,9 @@ Http::init()
if (version_compare($requestFormat, '1.9.0', '<')) {
$request->addFilter(new RequestV21());
}
if (version_compare($requestFormat, '1.9.1', '<')) {
$request->addFilter(new RequestV22());
}
}
$localeParam = (string) $request->getParam('locale', $request->getHeader('x-appwrite-locale', ''));
@@ -916,6 +921,9 @@ Http::init()
*/
$responseFormat = $request->getHeader('x-appwrite-response-format', System::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
if ($responseFormat) {
if (version_compare($responseFormat, '1.9.1', '<')) {
$response->addFilter(new ResponseV22());
}
if (version_compare($responseFormat, '1.9.0', '<')) {
$response->addFilter(new ResponseV21());
}
@@ -1168,15 +1176,6 @@ Http::error()
->inject('devKey')
->inject('authorization')
->action(function (Throwable $error, Http $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, Bus $bus, Document $devKey, Authorization $authorization) {
$trace = $error->getTrace();
foreach (array_slice($trace, 0, 100) as $index => $traceEntry) {
$file = isset($traceEntry['file']) ? $traceEntry['file'] : '[internal function]';
$line = isset($traceEntry['line']) ? $traceEntry['line'] : '';
$function = isset($traceEntry['function']) ? $traceEntry['function'] : '';
Console::error("[$index] $file : $line -> $function()");
}
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
$route = $utopia->getRoute();
$class = \get_class($error);
@@ -1186,9 +1185,7 @@ Http::error()
$line = $error->getLine();
$trace = $error->getTrace();
if (php_sapi_name() === 'cli') {
Span::error($error);
}
Span::error($error);
switch ($class) {
case Utopia\Http\Exception::class:
@@ -1430,6 +1427,7 @@ Http::error()
case 402: // Error allowed publicly
case 403: // Error allowed publicly
case 404: // Error allowed publicly
case 405: // Error allowed publicly
case 408: // Error allowed publicly
case 409: // Error allowed publicly
case 412: // Error allowed publicly
+16 -9
View File
@@ -37,6 +37,7 @@ use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Authorization\Input;
use Utopia\Database\Validator\Roles;
use Utopia\Http\Http;
use Utopia\Span\Span;
use Utopia\System\System;
use Utopia\Telemetry\Adapter as Telemetry;
use Utopia\Validator\WhiteList;
@@ -424,7 +425,7 @@ Http::init()
}
if (! empty($method)) {
$namespace = $method->getNamespace();
$namespace = \strtolower($method->getNamespace());
if (
array_key_exists($namespace, $project->getAttribute('services', []))
@@ -435,6 +436,15 @@ Http::init()
}
}
// Step 8b: Check REST protocol status
if (
array_key_exists('rest', $project->getAttribute('apis', []))
&& ! $project->getAttribute('apis', [])['rest']
&& ! ($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles()))
) {
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
}
// Step 9: Validate scope permissions
$allowed = (array) $route->getLabel('scope', 'none');
if (empty(\array_intersect($allowed, $scopes))) {
@@ -510,14 +520,6 @@ Http::init()
default => '',
};
if (
array_key_exists('rest', $project->getAttribute('apis', []))
&& ! $project->getAttribute('apis', [])['rest']
&& ! ($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles()))
) {
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
}
/*
* Abuse Check
*/
@@ -633,6 +635,7 @@ Http::init()
$isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && ! $user->isPrivileged($authorization->getRoles());
$key = $request->cacheIdentifier();
Span::add('storage.cache.key', $key);
$cacheLog = $authorization->skip(fn () => $dbForProject->getDocument('cache', $key));
$cache = new Cache(
new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId())
@@ -681,6 +684,8 @@ Http::init()
if ($file->isEmpty()) {
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
}
Span::add('storage.bucket.id', $bucketId);
Span::add('storage.file.id', $fileId);
// Do not update transformedAt if it's a console user
if (! $user->isPrivileged($authorization->getRoles())) {
$transformedAt = $file->getAttribute('transformedAt', '');
@@ -708,10 +713,12 @@ Http::init()
->setContentType($cacheLog->getAttribute('mimeType'));
$storageCacheOperationsCounter->add(1, ['result' => 'hit']);
if (! $isImageTransformation || ! $isDisabled) {
Span::add('storage.cache.hit', true);
$response->send($data);
}
} else {
$storageCacheOperationsCounter->add(1, ['result' => 'miss']);
Span::add('storage.cache.hit', false);
$response
->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
->addHeader('Pragma', 'no-cache')
+1 -1
View File
@@ -12,7 +12,7 @@ Config::load('runtimes-v2', __DIR__ . '/../config/runtimes-v2.php', $configAdapt
Config::load('template-runtimes', __DIR__ . '/../config/template-runtimes.php', $configAdapter);
Config::load('events', __DIR__ . '/../config/events.php', $configAdapter);
Config::load('auth', __DIR__ . '/../config/auth.php', $configAdapter);
Config::load('apis', __DIR__ . '/../config/apis.php', $configAdapter); // List of APIs
Config::load('protocols', __DIR__ . '/../config/protocols.php', $configAdapter);
Config::load('errors', __DIR__ . '/../config/errors.php', $configAdapter);
Config::load('oAuthProviders', __DIR__ . '/../config/oAuthProviders.php', $configAdapter);
Config::load('sdks', __DIR__ . '/../config/sdks.php', $configAdapter);
+2 -2
View File
@@ -46,8 +46,8 @@ const APP_PROJECT_ACCESS = 24 * 60 * 60; // 24 hours
const APP_RESOURCE_TOKEN_ACCESS = 24 * 60 * 60; // 24 hours
const APP_FILE_ACCESS = 24 * 60 * 60; // 24 hours
const APP_CACHE_UPDATE = 24 * 60 * 60; // 24 hours
const APP_CACHE_BUSTER = 4321;
const APP_VERSION_STABLE = '1.9.0';
const APP_CACHE_BUSTER = 4322;
const APP_VERSION_STABLE = '1.9.1';
const APP_DATABASE_ATTRIBUTE_EMAIL = 'email';
const APP_DATABASE_ATTRIBUTE_ENUM = 'enum';
const APP_DATABASE_ATTRIBUTE_IP = 'ip';
+366
View File
@@ -0,0 +1,366 @@
<?php
use Ahc\Jwt\JWT;
use Ahc\Jwt\JWTException;
use Appwrite\Extend\Exception;
use Appwrite\Network\Platform;
use Appwrite\Network\Validator\Origin;
use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Request;
use Utopia\Auth\Hashes\Sha;
use Utopia\Auth\Proofs\Token;
use Utopia\Auth\Store;
use Utopia\Database\DateTime as DatabaseDateTime;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\DI\Container;
use Utopia\System\System;
use Utopia\Validator\URL;
use Utopia\Validator\WhiteList;
/**
* Register the minimal per-connection resources required by realtime.
*/
return function (Container $container): void {
$getProjectId = static function (Request $request): string {
$projectId = $request->getHeader('x-appwrite-project', '');
if (!empty($projectId)) {
return $projectId;
}
$projectId = $request->getParam('project', '');
return \is_string($projectId) ? $projectId : '';
};
$getMode = static function (Request $request, Document $project) use ($getProjectId): string {
$mode = $request->getParam('mode', $request->getHeader('x-appwrite-mode', APP_MODE_DEFAULT));
$projectId = $getProjectId($request);
if (!empty($projectId) && $project->getId() !== $projectId) {
$mode = APP_MODE_ADMIN;
}
return $mode;
};
$getDbForPlatform = static function (Authorization $authorization) {
$database = getConsoleDB();
$database->setAuthorization($authorization);
return $database;
};
$getDbForProject = static function (Document $project, Authorization $authorization) use ($getDbForPlatform) {
if ($project->isEmpty() || $project->getId() === 'console') {
return $getDbForPlatform($authorization);
}
$database = getProjectDB($project);
$database->setAuthorization($authorization);
return $database;
};
$findRule = static function (Request $request, Document $project, Authorization $authorization) use ($getDbForPlatform): Document {
$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();
}
$dbForPlatform = $getDbForPlatform($authorization);
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
$rule = $authorization->skip(function () use ($dbForPlatform, $domain, $isMd5) {
if ($isMd5) {
return $dbForPlatform->getDocument('rules', md5($domain));
}
return $dbForPlatform->findOne('rules', [
Query::equal('domain', [$domain]),
]) ?? new Document();
});
$permitsCurrentProject = $rule->getAttribute('projectInternalId', '') === $project->getSequence();
if (!$permitsCurrentProject && !$rule->isEmpty() && !empty($rule->getAttribute('projectId', ''))) {
$trustedProjects = [];
foreach (\explode(',', System::getEnv('_APP_CONSOLE_TRUSTED_PROJECTS', '')) as $trustedProject) {
if (empty($trustedProject)) {
continue;
}
$trustedProjects[] = $trustedProject;
}
if (\in_array($rule->getAttribute('projectId', ''), $trustedProjects, true)) {
$permitsCurrentProject = true;
}
}
if (!$permitsCurrentProject) {
return new Document();
}
return $rule;
};
$findDevKey = static function (Request $request, Document $project, array $servers, Authorization $authorization) use ($getDbForPlatform): Document {
$devKey = $request->getHeader('x-appwrite-dev-key', $request->getParam('devKey', ''));
$key = $project->find('secret', $devKey, 'devKeys');
if (!$key) {
return new Document([]);
}
$expire = $key->getAttribute('expire');
if (!empty($expire) && $expire < DatabaseDateTime::formatTz(DatabaseDateTime::now())) {
return new Document([]);
}
$dbForPlatform = $getDbForPlatform($authorization);
$accessedAt = $key->getAttribute('accessedAt', 0);
if (empty($accessedAt) || DatabaseDateTime::formatTz(DatabaseDateTime::addSeconds(new \DateTime(), -APP_KEY_ACCESS)) > $accessedAt) {
$key->setAttribute('accessedAt', DatabaseDateTime::now());
$authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), new Document([
'accessedAt' => $key->getAttribute('accessedAt'),
])));
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
}
$sdkValidator = new WhiteList($servers, true);
$sdk = \strtolower($request->getHeader('x-sdk-name', 'UNKNOWN'));
if ($sdk !== 'UNKNOWN' && $sdkValidator->isValid($sdk)) {
$sdks = $key->getAttribute('sdks', []);
if (!\in_array($sdk, $sdks, true)) {
$sdks[] = $sdk;
$key->setAttribute('sdks', $sdks);
$key->setAttribute('accessedAt', DatabaseDateTime::now());
$key = $authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), new Document([
'sdks' => $key->getAttribute('sdks'),
'accessedAt' => $key->getAttribute('accessedAt'),
])));
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
}
}
return $key;
};
$container->set('authorization', function () {
return new Authorization();
}, []);
$container->set('project', function (Request $request, Document $console, Authorization $authorization) use ($getProjectId, $getDbForPlatform) {
$projectId = $getProjectId($request);
if (empty($projectId) || $projectId === 'console') {
return $console;
}
$dbForPlatform = $getDbForPlatform($authorization);
return $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
}, ['request', 'console', 'authorization']);
$container->set('originValidator', function (array $platform, Request $request, Document $project, array $servers, Authorization $authorization) use ($findDevKey, $findRule) {
$devKey = $findDevKey($request, $project, $servers, $authorization);
if (!$devKey->isEmpty()) {
return new URL();
}
$allowedHostnames = [...($platform['hostnames'] ?? [])];
if (!$project->isEmpty() && $project->getId() !== 'console') {
$allowedHostnames = [...$allowedHostnames, ...Platform::getHostnames($project->getAttribute('platforms', []))];
}
$rule = $findRule($request, $project, $authorization);
if (!$rule->isEmpty() && !empty($rule->getAttribute('domain', ''))) {
$allowedHostnames[] = $rule->getAttribute('domain', '');
}
$originHostname = \parse_url($request->getOrigin(), PHP_URL_HOST);
$refererHostname = \parse_url($request->getReferer(), PHP_URL_HOST);
$hostname = $originHostname ?: $refererHostname;
if ($request->getMethod() === 'OPTIONS' && !empty($hostname)) {
$allowedHostnames[] = $hostname;
}
$allowedSchemes = [...($platform['schemas'] ?? [])];
if (!$project->isEmpty() && $project->getId() !== 'console') {
$allowedSchemes[] = 'exp';
$allowedSchemes[] = 'appwrite-callback-' . $project->getId();
$allowedSchemes = [...$allowedSchemes, ...Platform::getSchemes($project->getAttribute('platforms', []))];
}
return new Origin(\array_unique($allowedHostnames), \array_unique($allowedSchemes));
}, ['platform', 'request', 'project', 'servers', 'authorization']);
$container->set('user', function (Request $request, Document $project, Document $console, Authorization $authorization) use ($getMode, $getDbForPlatform, $getDbForProject) {
$mode = $getMode($request, $project);
$store = new Store();
$proofForToken = new Token();
$proofForToken->setHash(new Sha());
$authorization->setDefaultStatus(true);
$dbForPlatform = $getDbForPlatform($authorization);
$dbForProject = $getDbForProject($project, $authorization);
$store->setKey('a_session_' . $project->getId());
if ($mode === APP_MODE_ADMIN) {
$store->setKey('a_session_' . $console->getId());
}
$store->decode(
$request->getCookie(
$store->getKey(),
$request->getCookie($store->getKey() . '_legacy', '')
)
);
if (empty($store->getProperty('id', '')) && empty($store->getProperty('secret', ''))) {
$sessionHeader = $request->getHeader('x-appwrite-session', '');
if (!empty($sessionHeader)) {
$store->decode($sessionHeader);
}
}
if (empty($store->getProperty('id', '')) && empty($store->getProperty('secret', ''))) {
$fallback = \json_decode($request->getHeader('x-fallback-cookies', ''), true);
$store->decode((\is_array($fallback) && isset($fallback[$store->getKey()])) ? $fallback[$store->getKey()] : '');
}
$user = null;
if ($mode === APP_MODE_ADMIN) {
/** @var User $user */
$user = $dbForPlatform->getDocument('users', $store->getProperty('id', ''));
} else {
if ($project->isEmpty()) {
$user = new User([]);
} elseif (!empty($store->getProperty('id', ''))) {
if ($project->getId() === 'console') {
/** @var User $user */
$user = $dbForPlatform->getDocument('users', $store->getProperty('id', ''));
} else {
/** @var User $user */
$user = $dbForProject->getDocument('users', $store->getProperty('id', ''));
}
}
}
if (
!$user
|| $user->isEmpty()
|| !$user->sessionVerify($store->getProperty('secret', ''), $proofForToken)
) {
$user = new User([]);
}
$authJWT = $request->getHeader('x-appwrite-jwt', '');
if (!empty($authJWT) && !$project->isEmpty()) {
if (!$user->isEmpty()) {
throw new Exception(Exception::USER_JWT_AND_COOKIE_SET);
}
$jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0);
try {
$payload = $jwt->decode($authJWT);
} catch (JWTException $error) {
throw new Exception(Exception::USER_JWT_INVALID, 'Failed to verify JWT. ' . $error->getMessage());
}
$jwtUserId = $payload['userId'] ?? '';
if (!empty($jwtUserId)) {
if ($mode === APP_MODE_ADMIN) {
$user = $dbForPlatform->getDocument('users', $jwtUserId);
} else {
$user = $dbForProject->getDocument('users', $jwtUserId);
}
}
$jwtSessionId = $payload['sessionId'] ?? '';
if (!empty($jwtSessionId) && empty($user->find('$id', $jwtSessionId, 'sessions'))) {
$user = new User([]);
}
}
$accountKey = $request->getHeader('x-appwrite-key', '');
$accountKeyUserId = $request->getHeader('x-appwrite-user', '');
if (!empty($accountKeyUserId) && !empty($accountKey)) {
if (!$user->isEmpty()) {
throw new Exception(Exception::USER_API_KEY_AND_SESSION_SET);
}
$accountKeyUser = $authorization->skip(fn () => $dbForPlatform->getDocument('users', $accountKeyUserId));
if (!$accountKeyUser->isEmpty()) {
$key = $accountKeyUser->find(
key: 'secret',
find: $accountKey,
subject: 'keys'
);
if (!empty($key)) {
$expire = $key->getAttribute('expire');
if (!empty($expire) && $expire < DatabaseDateTime::formatTz(DatabaseDateTime::now())) {
throw new Exception(Exception::ACCOUNT_KEY_EXPIRED);
}
$user = $accountKeyUser;
}
}
}
$impersonateUserId = $request->getHeader('x-appwrite-impersonate-user-id', '');
$impersonateEmail = $request->getHeader('x-appwrite-impersonate-user-email', '');
$impersonatePhone = $request->getHeader('x-appwrite-impersonate-user-phone', '');
if (!$user->isEmpty() && $user->getAttribute('impersonator', false)) {
$userDb = ($mode === APP_MODE_ADMIN || $project->getId() === 'console') ? $dbForPlatform : $dbForProject;
$targetUser = null;
if (!empty($impersonateUserId)) {
$targetUser = $authorization->skip(fn () => $userDb->getDocument('users', $impersonateUserId));
} elseif (!empty($impersonateEmail)) {
$targetUser = $authorization->skip(fn () => $userDb->findOne('users', [
Query::equal('email', [\strtolower($impersonateEmail)]),
]));
} elseif (!empty($impersonatePhone)) {
$targetUser = $authorization->skip(fn () => $userDb->findOne('users', [
Query::equal('phone', [$impersonatePhone]),
]));
}
if ($targetUser !== null && !$targetUser->isEmpty()) {
$impersonator = clone $user;
$user = clone $targetUser;
$user->setAttribute('impersonatorUserId', $impersonator->getId());
$user->setAttribute('impersonatorUserInternalId', $impersonator->getSequence());
$user->setAttribute('impersonatorUserName', $impersonator->getAttribute('name', ''));
$user->setAttribute('impersonatorUserEmail', $impersonator->getAttribute('email', ''));
$user->setAttribute('impersonatorAccessedAt', $impersonator->getAttribute('accessedAt', 0));
}
}
$dbForPlatform->setMetadata('user', $user->getId());
$dbForProject->setMetadata('user', $user->getId());
return $user;
}, ['request', 'project', 'console', 'authorization']);
};
+15
View File
@@ -1,6 +1,9 @@
<?php
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Execution as ExecutionPublisher;
use Appwrite\Event\Publisher\Migration as MigrationPublisher;
use Appwrite\Event\Publisher\StatsResources as StatsResourcesPublisher;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Utopia\Database\Documents\User;
use Executor\Executor;
@@ -82,6 +85,18 @@ $container->set('publisherForUsage', fn (Publisher $publisher) => new UsagePubli
$publisher,
new Queue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForExecutions', fn (Publisher $publisher) => new ExecutionPublisher(
$publisher,
new Queue(System::getEnv('_APP_EXECUTIONS_QUEUE_NAME', Event::EXECUTIONS_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForMigrations', fn (Publisher $publisher) => new MigrationPublisher(
$publisher,
new Queue(System::getEnv('_APP_MIGRATIONS_QUEUE_NAME', Event::MIGRATIONS_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForStatsResources', fn (Publisher $publisher) => new StatsResourcesPublisher(
$publisher,
new Queue(System::getEnv('_APP_STATS_RESOURCES_QUEUE_NAME', Event::STATS_RESOURCES_QUEUE_NAME))
), ['publisher']);
/**
* Platform configuration
-9
View File
@@ -13,10 +13,8 @@ use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\Migration;
use Appwrite\Event\Realtime;
use Appwrite\Event\Screenshot;
use Appwrite\Event\StatsResources;
use Appwrite\Event\Webhook;
use Appwrite\Extend\Exception;
use Appwrite\Functions\EventProcessor;
@@ -163,13 +161,6 @@ return function (Container $container): void {
$container->set('queueForCertificates', function (Publisher $publisher) {
return new Certificate($publisher);
}, ['publisher']);
$container->set('queueForMigrations', function (Publisher $publisher) {
return new Migration($publisher);
}, ['publisher']);
$container->set('queueForStatsResources', function (Publisher $publisher) {
return new StatsResources($publisher);
}, ['publisher']);
$container->set('dbForPlatform', function (Group $pools, Cache $cache, Authorization $authorization) {
$adapter = new DatabasePool($pools->get('console'));
$database = new Database($adapter, $cache);
-5
View File
@@ -9,7 +9,6 @@ use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\Migration;
use Appwrite\Event\Realtime;
use Appwrite\Event\Screenshot;
use Appwrite\Event\Webhook;
@@ -344,10 +343,6 @@ return function (Container $container): void {
return new Certificate($publisher);
}, ['publisher']);
$container->set('queueForMigrations', function (Publisher $publisher) {
return new Migration($publisher);
}, ['publisher']);
$container->set('deviceForSites', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
+151 -43
View File
@@ -35,8 +35,6 @@ use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\DI\Container;
use Utopia\DSN\DSN;
use Utopia\Http\Adapter\FPM\Server as HttpServer;
use Utopia\Http\Http;
use Utopia\Logger\Log;
use Utopia\Pools\Group;
use Utopia\Registry\Registry;
@@ -45,12 +43,12 @@ use Utopia\Telemetry\Adapter\None as NoTelemetry;
use Utopia\WebSocket\Adapter;
use Utopia\WebSocket\Server;
/**
* @var Registry $register
*/
require_once __DIR__ . '/init.php';
$registerRequestResources ??= require __DIR__ . '/init/resources/request.php';
/** @var Registry $register */
$register = $GLOBALS['register'] ?? throw new \RuntimeException('Registry not initialized');
$registerConnectionResources ??= require __DIR__ . '/init/realtime/connection.php';
Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
@@ -450,7 +448,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
]
];
$server->send($realtime->getSubscribers($event), json_encode([
$server->send(array_keys($realtime->getSubscribers($event)), json_encode([
'type' => 'event',
'data' => $event['data']
]));
@@ -557,7 +555,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
$receivers = $realtime->getSubscribers($event);
if (Http::isDevelopment() && !empty($receivers)) {
if (System::getEnv('_APP_ENV', 'production') === 'development' && !empty($receivers)) {
Console::log("[Debug][Worker {$workerId}] Receivers: " . count($receivers));
Console::log("[Debug][Worker {$workerId}] Connection IDs: " . json_encode(array_keys($receivers)));
Console::log("[Debug][Worker {$workerId}] Matched: " . json_encode(array_values($receivers)));
@@ -623,7 +621,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
Console::error('Failed to restart pub/sub...');
});
$server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $register, $stats, &$realtime, $registerRequestResources) {
$server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $register, $stats, &$realtime, $registerConnectionResources) {
global $container;
$request = new Request($request);
$response = new Response(new SwooleResponse());
@@ -631,14 +629,9 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
Console::info("Connection open (user: {$connection})");
$connectionContainer = new Container($container);
$adapter = new HttpServer($connectionContainer);
$app = new Http($adapter, 'UTC');
$connectionContainer->set('utopia', fn () => $app);
$connectionContainer->set('request', fn () => $request);
$connectionContainer->set('response', fn () => $response);
$registerRequestResources($connectionContainer);
$registerConnectionResources($connectionContainer);
$project = null;
$logUser = null;
@@ -646,8 +639,8 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
try {
/** @var Document $project */
$project = $app->getResource('project');
$authorization = $app->getResource('authorization');
$project = $connectionContainer->get('project');
$authorization = $connectionContainer->get('authorization');
/*
* Project Check
@@ -656,13 +649,15 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, 'Missing or unknown project ID');
}
$timelimit = $app->getResource('timelimit');
$user = $app->getResource('user'); /** @var User $user */
$timelimit = $connectionContainer->get('timelimit');
$user = $connectionContainer->get('user'); /** @var User $user */
$logUser = $user;
$apis = $project->getAttribute('apis', []);
// Websocket is what to check, but realtime is checked too for backwards compatibility
$websocketEnabled = $apis['websocket'] ?? $apis['realtime'] ?? true;
if (
array_key_exists('realtime', $project->getAttribute('apis', []))
&& !$project->getAttribute('apis', [])['realtime']
!$websocketEnabled
&& !($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles()))
) {
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
@@ -702,7 +697,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
* Skip this check for non-web platforms which are not required to send an origin header.
*/
$origin = $request->getOrigin();
$originValidator = $app->getResource('originValidator');
$originValidator = $connectionContainer->get('originValidator');
if (!empty($origin) && !$originValidator->isValid($origin) && $project->getId() !== 'console') {
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, $originValidator->getDescription());
@@ -712,11 +707,43 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
$channels = Realtime::convertChannels($request->getQuery('channels', []), $user->getId());
$updateStats = static function (string $projectId, ?string $teamId, string $payloadJson) use ($register, $stats): void {
$register->get('telemetry.connectionCounter')->add(1);
$register->get('telemetry.connectionCreatedCounter')->add(1);
$stats->set($projectId, [
'projectId' => $projectId,
'teamId' => $teamId
]);
$stats->incr($projectId, 'connections');
$stats->incr($projectId, 'connectionsTotal');
triggerStats([
METRIC_REALTIME_CONNECTIONS => 1,
METRIC_REALTIME_OUTBOUND => \strlen($payloadJson),
], $projectId);
};
/**
* Channels Check
*/
if (empty($channels)) {
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, 'Missing channels');
// in case of message based 'subscribe' channels will be empty at first and only projectId and roles will be available
$sanitizedUser = empty($user->getId()) ? null : $response->output($user, Response::MODEL_ACCOUNT);
$connectedPayloadJson = json_encode([
'type' => 'connected',
'data' => [
'channels' => [],
'subscriptions' => [],
'user' => $sanitizedUser
]
]);
$realtime->subscribe($project->getId(), $connection, '', $roles, [], [], $user->getId());
$realtime->connections[$connection]['authorization'] = $authorization;
$server->send([$connection], $connectedPayloadJson);
$updateStats($project->getId(), $project->getAttribute('teamId'), $connectedPayloadJson);
return;
}
$names = array_keys($channels);
@@ -740,7 +767,8 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
$subscriptionId,
$roles,
$subscription['channels'],
$subscription['queries']
$subscription['queries'],
$user->getId()
);
$mapping[$index] = $subscriptionId;
@@ -760,20 +788,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
]);
$server->send([$connection], $connectedPayloadJson);
$register->get('telemetry.connectionCounter')->add(1);
$register->get('telemetry.connectionCreatedCounter')->add(1);
$stats->set($project->getId(), [
'projectId' => $project->getId(),
'teamId' => $project->getAttribute('teamId')
]);
$stats->incr($project->getId(), 'connections');
$stats->incr($project->getId(), 'connectionsTotal');
$connectedOutboundBytes = \strlen($connectedPayloadJson);
triggerStats([METRIC_REALTIME_CONNECTIONS => 1, METRIC_REALTIME_OUTBOUND => $connectedOutboundBytes], $project->getId());
$updateStats($project->getId(), $project->getAttribute('teamId'), $connectedPayloadJson);
} catch (Throwable $th) {
@@ -789,7 +804,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
// sanitize 0 && 5xx errors
$realtimeViolation = $th instanceof AppwriteException && $th->getType() === AppwriteException::REALTIME_POLICY_VIOLATION;
if (($code === 0 || $code >= 500) && !$realtimeViolation && !Http::isDevelopment()) {
if (($code === 0 || $code >= 500) && !$realtimeViolation && System::getEnv('_APP_ENV', 'production') !== 'development') {
$message = 'Error: Server Error';
}
@@ -804,7 +819,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
$server->send([$connection], json_encode($response));
$server->close($connection, $code);
if (Http::isDevelopment()) {
if (System::getEnv('_APP_ENV', 'production') === 'development') {
Console::error('[Error] Connection Error');
Console::error('[Error] Code: ' . $response['data']['code']);
Console::error('[Error] Message: ' . $response['data']['message']);
@@ -815,7 +830,6 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
$server->onMessage(function (int $connection, string $message) use ($server, $realtime, $containerId) {
$project = null;
$authorization = null;
try {
$rawSize = \strlen($message);
$response = new Response(new SwooleResponse());
@@ -941,7 +955,8 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
$subscriptionId,
$roles,
$subscription['channels'] ?? [],
$queries
$queries,
$user->getId()
);
}
}
@@ -975,6 +990,99 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
break;
case 'subscribe':
/**
* Message based upsertion of a subscription
* If subscriptionId is given then it will match subId of the connection and update the subscription with channels and queries
* If non-existing subid is given or not given a new subid will be generated
* Similar to what we have now -> two subscribe() block with same channels and queries still two different subscriptions
*
* structure of the payload -> array of maps
* 'data' : [subscriptionId:"" , channels:[] , queries:[]]
*/
if (!is_array($message['data']) || !array_is_list($message['data'])) {
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Payload is not valid.');
}
$roles = $realtime->connections[$connection]['roles'] ?? [Role::guests()->toString()];
$userId = $realtime->connections[$connection]['userId'] ?? '';
// bulk validation + parsing before subscribing
$parsedPayloads = [];
foreach ($message['data'] as $payload) {
if (!\is_array($payload)) {
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Each subscribe payload must be an object.');
}
if (!array_key_exists('channels', $payload)) {
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'channels is not present in payload.');
}
if (!is_array($payload['channels']) || !array_is_list($payload['channels'])) {
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'channels is not a valid array.');
}
// registering the queries if not present and check in the same payload later on
if (!array_key_exists('queries', $payload)) {
$payload['queries'] = [];
}
if (!is_array($payload['queries']) || !array_is_list($payload['queries'])) {
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'queries is not a valid array.');
}
$subscriptionId = \array_key_exists('subscriptionId', $payload)
? $payload['subscriptionId']
: ID::unique();
try {
$convertedQueries = Realtime::convertQueries($payload['queries']);
} catch (QueryException $e) {
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Invalid query: ' . $e->getMessage());
}
$parsedPayloads[] = [
'subscriptionId' => $subscriptionId,
'channels' => $payload['channels'],
'queries' => $convertedQueries,
];
}
foreach ($parsedPayloads as $parsedPayload) {
$subscriptionId = $parsedPayload['subscriptionId'];
$channels = \array_keys(Realtime::convertChannels($parsedPayload['channels'], $userId));
$queries = $parsedPayload['queries'];
$realtime->subscribe($projectId, $connection, $subscriptionId, $roles, $channels, $queries);
}
// subscribe() overwrites the connection entry; restore auth so later onMessage uses the same context.
$realtime->connections[$connection]['authorization'] = $authorization;
$responsePayload = json_encode([
'type' => 'response',
'data' => [
'to' => 'subscribe',
'success' => true,
'subscriptions' => \array_map(function (array $parsedPayload) {
return [
'subscriptionId' => $parsedPayload['subscriptionId'],
'channels' => $parsedPayload['channels'],
'queries' => \array_map(fn ($q) => $q->toString(), $parsedPayload['queries']),
];
}, $parsedPayloads),
]
]);
$server->send([$connection], $responsePayload);
if ($project !== null && !$project->isEmpty()) {
$subscribeOutboundBytes = \strlen($responsePayload);
if ($subscribeOutboundBytes > 0) {
triggerStats([
METRIC_REALTIME_OUTBOUND => $subscribeOutboundBytes,
], $project->getId());
}
}
break;
default:
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Message type is not valid.');
}
@@ -988,7 +1096,7 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
$message = $th->getMessage();
// sanitize 0 && 5xx errors
if (($code === 0 || $code >= 500) && !Http::isDevelopment()) {
if (($code === 0 || $code >= 500) && System::getEnv('_APP_ENV', 'production') !== 'development') {
$message = 'Error: Server Error';
}
+1 -1
View File
@@ -993,7 +993,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
<<: *x-logging
restart: unless-stopped
stop_signal: SIGINT
image: openruntimes/executor:0.7.22
image: openruntimes/executor:0.11.4
networks:
- appwrite
- runtimes
+1 -1
View File
@@ -1,8 +1,8 @@
<?php
require_once __DIR__ . '/init.php';
$registerWorkerMessageResources = require __DIR__ . '/init/worker/message.php';
use Appwrite\Certificates\LetsEncrypt;
use Appwrite\Platform\Appwrite;
use Swoole\Runtime;
+1 -1
View File
@@ -92,7 +92,7 @@
"chillerlan/php-qrcode": "4.3.*",
"adhocore/jwt": "1.1.*",
"spomky-labs/otphp": "11.*",
"webonyx/graphql-php": "14.11.*",
"webonyx/graphql-php": "15.31.*",
"league/csv": "9.14.*",
"enshrined/svg-sanitize": "0.22.*"
},
Generated
+103 -89
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": "4fb974e9843f6104e40396e7cad4a833",
"content-hash": "f6a87c1012b316e614258f8f57a28e48",
"packages": [
{
"name": "adhocore/jwt",
@@ -1996,16 +1996,16 @@
},
{
"name": "phpseclib/phpseclib",
"version": "3.0.50",
"version": "3.0.51",
"source": {
"type": "git",
"url": "https://github.com/phpseclib/phpseclib.git",
"reference": "aa6ad8321ed103dc3624fb600a25b66ebf78ec7b"
"reference": "d59c94077f9c9915abb51ddb52ce85188ece1748"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/aa6ad8321ed103dc3624fb600a25b66ebf78ec7b",
"reference": "aa6ad8321ed103dc3624fb600a25b66ebf78ec7b",
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/d59c94077f9c9915abb51ddb52ce85188ece1748",
"reference": "d59c94077f9c9915abb51ddb52ce85188ece1748",
"shasum": ""
},
"require": {
@@ -2086,7 +2086,7 @@
],
"support": {
"issues": "https://github.com/phpseclib/phpseclib/issues",
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.50"
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.51"
},
"funding": [
{
@@ -2102,7 +2102,7 @@
"type": "tidelift"
}
],
"time": "2026-03-19T02:57:58+00:00"
"time": "2026-04-10T01:33:53+00:00"
},
{
"name": "psr/clock",
@@ -2887,16 +2887,16 @@
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.33.0",
"version": "v1.34.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493"
"reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493",
"reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6a21eb99c6973357967f6ce3708cd55a6bec6315",
"reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315",
"shasum": ""
},
"require": {
@@ -2948,7 +2948,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0"
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.34.0"
},
"funding": [
{
@@ -2968,20 +2968,20 @@
"type": "tidelift"
}
],
"time": "2024-12-23T08:48:59+00:00"
"time": "2026-04-10T17:25:58+00:00"
},
{
"name": "symfony/polyfill-php82",
"version": "v1.33.0",
"version": "v1.34.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php82.git",
"reference": "5d2ed36f7734637dacc025f179698031951b1692"
"reference": "34808efe3e68f69685796f7c253a2f1d8ea9df59"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php82/zipball/5d2ed36f7734637dacc025f179698031951b1692",
"reference": "5d2ed36f7734637dacc025f179698031951b1692",
"url": "https://api.github.com/repos/symfony/polyfill-php82/zipball/34808efe3e68f69685796f7c253a2f1d8ea9df59",
"reference": "34808efe3e68f69685796f7c253a2f1d8ea9df59",
"shasum": ""
},
"require": {
@@ -3028,7 +3028,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php82/tree/v1.33.0"
"source": "https://github.com/symfony/polyfill-php82/tree/v1.34.0"
},
"funding": [
{
@@ -3048,20 +3048,20 @@
"type": "tidelift"
}
],
"time": "2024-09-09T11:45:10+00:00"
"time": "2026-04-10T16:19:22+00:00"
},
{
"name": "symfony/polyfill-php83",
"version": "v1.33.0",
"version": "v1.34.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php83.git",
"reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5"
"reference": "3600c2cb22399e25bb226e4a135ce91eeb2a6149"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5",
"reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5",
"url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/3600c2cb22399e25bb226e4a135ce91eeb2a6149",
"reference": "3600c2cb22399e25bb226e4a135ce91eeb2a6149",
"shasum": ""
},
"require": {
@@ -3108,7 +3108,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0"
"source": "https://github.com/symfony/polyfill-php83/tree/v1.34.0"
},
"funding": [
{
@@ -3128,20 +3128,20 @@
"type": "tidelift"
}
],
"time": "2025-07-08T02:45:35+00:00"
"time": "2026-04-10T17:25:58+00:00"
},
{
"name": "symfony/polyfill-php85",
"version": "v1.33.0",
"version": "v1.34.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php85.git",
"reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91"
"reference": "2c408a6bb0313e6001a83628dc5506100474254e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91",
"reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91",
"url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/2c408a6bb0313e6001a83628dc5506100474254e",
"reference": "2c408a6bb0313e6001a83628dc5506100474254e",
"shasum": ""
},
"require": {
@@ -3188,7 +3188,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php85/tree/v1.33.0"
"source": "https://github.com/symfony/polyfill-php85/tree/v1.34.0"
},
"funding": [
{
@@ -3208,7 +3208,7 @@
"type": "tidelift"
}
],
"time": "2025-06-23T16:12:55+00:00"
"time": "2026-04-10T16:50:15+00:00"
},
{
"name": "symfony/service-contracts",
@@ -3850,16 +3850,16 @@
},
{
"name": "utopia-php/database",
"version": "5.3.19",
"version": "5.3.21",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
"reference": "72ee1614c37e37c7fdd9d4dc87f1f7cdfa1ca691"
"reference": "ee2d7d4c87b3a3fae954089ad7494ceb454f619d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/database/zipball/72ee1614c37e37c7fdd9d4dc87f1f7cdfa1ca691",
"reference": "72ee1614c37e37c7fdd9d4dc87f1f7cdfa1ca691",
"url": "https://api.github.com/repos/utopia-php/database/zipball/ee2d7d4c87b3a3fae954089ad7494ceb454f619d",
"reference": "ee2d7d4c87b3a3fae954089ad7494ceb454f619d",
"shasum": ""
},
"require": {
@@ -3903,9 +3903,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/database/issues",
"source": "https://github.com/utopia-php/database/tree/5.3.19"
"source": "https://github.com/utopia-php/database/tree/5.3.21"
},
"time": "2026-03-31T15:52:08+00:00"
"time": "2026-04-10T12:38:57+00:00"
},
{
"name": "utopia-php/detector",
@@ -4271,16 +4271,16 @@
},
{
"name": "utopia-php/http",
"version": "0.34.19",
"version": "0.34.20",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/http.git",
"reference": "995c119f31866cacd42d63b1f922bf86eabb396c"
"reference": "d6b360d555022d16c16d40be51f86180364819f8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/http/zipball/995c119f31866cacd42d63b1f922bf86eabb396c",
"reference": "995c119f31866cacd42d63b1f922bf86eabb396c",
"url": "https://api.github.com/repos/utopia-php/http/zipball/d6b360d555022d16c16d40be51f86180364819f8",
"reference": "d6b360d555022d16c16d40be51f86180364819f8",
"shasum": ""
},
"require": {
@@ -4319,9 +4319,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/http/issues",
"source": "https://github.com/utopia-php/http/tree/0.34.19"
"source": "https://github.com/utopia-php/http/tree/0.34.20"
},
"time": "2026-04-08T10:23:17+00:00"
"time": "2026-04-12T14:25:22+00:00"
},
{
"name": "utopia-php/image",
@@ -5225,16 +5225,16 @@
},
{
"name": "utopia-php/vcs",
"version": "3.1.0",
"version": "3.2.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/vcs.git",
"reference": "03b76ad5fd01bc50f809915bca6ff0745ea913af"
"reference": "44a84ab52b42fc12f812b4d7331286b519d39db3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/vcs/zipball/03b76ad5fd01bc50f809915bca6ff0745ea913af",
"reference": "03b76ad5fd01bc50f809915bca6ff0745ea913af",
"url": "https://api.github.com/repos/utopia-php/vcs/zipball/44a84ab52b42fc12f812b4d7331286b519d39db3",
"reference": "44a84ab52b42fc12f812b4d7331286b519d39db3",
"shasum": ""
},
"require": {
@@ -5268,9 +5268,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/vcs/issues",
"source": "https://github.com/utopia-php/vcs/tree/3.1.0"
"source": "https://github.com/utopia-php/vcs/tree/3.2.0"
},
"time": "2026-03-24T08:49:14+00:00"
"time": "2026-04-08T16:00:31+00:00"
},
{
"name": "utopia-php/websocket",
@@ -5381,38 +5381,48 @@
},
{
"name": "webonyx/graphql-php",
"version": "v14.11.10",
"version": "v15.31.5",
"source": {
"type": "git",
"url": "https://github.com/webonyx/graphql-php.git",
"reference": "d9c2fdebc6aa01d831bc2969da00e8588cffef19"
"reference": "089c4ef7e112df85788cfe06596278a8f99f4aa9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/webonyx/graphql-php/zipball/d9c2fdebc6aa01d831bc2969da00e8588cffef19",
"reference": "d9c2fdebc6aa01d831bc2969da00e8588cffef19",
"url": "https://api.github.com/repos/webonyx/graphql-php/zipball/089c4ef7e112df85788cfe06596278a8f99f4aa9",
"reference": "089c4ef7e112df85788cfe06596278a8f99f4aa9",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-mbstring": "*",
"php": "^7.1 || ^8"
"php": "^7.4 || ^8"
},
"require-dev": {
"amphp/amp": "^2.3",
"doctrine/coding-standard": "^6.0",
"nyholm/psr7": "^1.2",
"amphp/amp": "^2.6",
"amphp/http-server": "^2.1",
"dms/phpunit-arraysubset-asserts": "dev-master",
"ergebnis/composer-normalize": "^2.28",
"friendsofphp/php-cs-fixer": "3.94.2",
"mll-lab/php-cs-fixer-config": "5.13.0",
"nyholm/psr7": "^1.5",
"phpbench/phpbench": "^1.2",
"phpstan/extension-installer": "^1.0",
"phpstan/phpstan": "0.12.82",
"phpstan/phpstan-phpunit": "0.12.18",
"phpstan/phpstan-strict-rules": "0.12.9",
"phpunit/phpunit": "^7.2 || ^8.5",
"psr/http-message": "^1.0",
"react/promise": "2.*",
"simpod/php-coveralls-mirror": "^3.0"
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan": "2.1.46",
"phpstan/phpstan-phpunit": "2.0.16",
"phpstan/phpstan-strict-rules": "2.0.10",
"phpunit/phpunit": "^9.5 || ^10.5.21 || ^11",
"psr/http-message": "^1 || ^2",
"react/http": "^1.6",
"react/promise": "^2.0 || ^3.0",
"rector/rector": "^2.0",
"symfony/polyfill-php81": "^1.23",
"symfony/var-exporter": "^5 || ^6 || ^7 || ^8",
"thecodingmachine/safe": "^1.3 || ^2 || ^3",
"ticketswap/phpstan-error-formatter": "1.3.0"
},
"suggest": {
"amphp/http-server": "To leverage async resolving with webserver on AMPHP platform",
"psr/http-message": "To use standard GraphQL server",
"react/promise": "To leverage async resolving on React PHP platform"
},
@@ -5434,30 +5444,34 @@
],
"support": {
"issues": "https://github.com/webonyx/graphql-php/issues",
"source": "https://github.com/webonyx/graphql-php/tree/v14.11.10"
"source": "https://github.com/webonyx/graphql-php/tree/v15.31.5"
},
"funding": [
{
"url": "https://github.com/spawnia",
"type": "github"
},
{
"url": "https://opencollective.com/webonyx-graphql-php",
"type": "open_collective"
}
],
"time": "2023-07-05T14:23:37+00:00"
"time": "2026-04-11T18:06:15+00:00"
}
],
"packages-dev": [
{
"name": "appwrite/sdk-generator",
"version": "1.17.7",
"version": "1.17.11",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-generator.git",
"reference": "291471d04c3f0e7b9fcc46668a6255a4c0f2947e"
"reference": "c714ee52659ef5968b3372ff4da0e407140a6250"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/291471d04c3f0e7b9fcc46668a6255a4c0f2947e",
"reference": "291471d04c3f0e7b9fcc46668a6255a4c0f2947e",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/c714ee52659ef5968b3372ff4da0e407140a6250",
"reference": "c714ee52659ef5968b3372ff4da0e407140a6250",
"shasum": ""
},
"require": {
@@ -5493,9 +5507,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.17.7"
"source": "https://github.com/appwrite/sdk-generator/tree/1.17.11"
},
"time": "2026-04-08T08:51:05+00:00"
"time": "2026-04-11T02:42:32+00:00"
},
{
"name": "brianium/paratest",
@@ -7764,16 +7778,16 @@
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.33.0",
"version": "v1.34.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638"
"reference": "141046a8f9477948ff284fa65be2095baafb94f2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638",
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2",
"reference": "141046a8f9477948ff284fa65be2095baafb94f2",
"shasum": ""
},
"require": {
@@ -7823,7 +7837,7 @@
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0"
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.34.0"
},
"funding": [
{
@@ -7843,20 +7857,20 @@
"type": "tidelift"
}
],
"time": "2024-09-09T11:45:10+00:00"
"time": "2026-04-10T16:19:22+00:00"
},
{
"name": "symfony/polyfill-intl-grapheme",
"version": "v1.33.0",
"version": "v1.34.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
"reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70"
"reference": "ad1b7b9092976d6c948b8a187cec9faaea9ec1df"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70",
"reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70",
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/ad1b7b9092976d6c948b8a187cec9faaea9ec1df",
"reference": "ad1b7b9092976d6c948b8a187cec9faaea9ec1df",
"shasum": ""
},
"require": {
@@ -7905,7 +7919,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0"
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.34.0"
},
"funding": [
{
@@ -7925,11 +7939,11 @@
"type": "tidelift"
}
],
"time": "2025-06-27T09:58:17+00:00"
"time": "2026-04-10T16:19:22+00:00"
},
{
"name": "symfony/polyfill-intl-normalizer",
"version": "v1.33.0",
"version": "v1.34.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
@@ -7990,7 +8004,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0"
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.34.0"
},
"funding": [
{
@@ -8014,7 +8028,7 @@
},
{
"name": "symfony/polyfill-php81",
"version": "v1.33.0",
"version": "v1.34.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php81.git",
@@ -8070,7 +8084,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php81/tree/v1.33.0"
"source": "https://github.com/symfony/polyfill-php81/tree/v1.34.0"
},
"funding": [
{
+28 -9
View File
@@ -3,10 +3,12 @@
namespace Appwrite\Bus\Listeners;
use Appwrite\Bus\Events\ExecutionCompleted;
use Appwrite\Event\Execution;
use Appwrite\Event\Message\Execution as ExecutionMessage;
use Appwrite\Event\Publisher\Execution as ExecutionPublisher;
use Utopia\Bus\Listener;
use Utopia\Database\Document;
use Utopia\Queue\Publisher;
use Utopia\Span\Span;
use Utopia\System\System;
class Log extends Listener
{
@@ -24,16 +26,33 @@ class Log extends Listener
{
$this
->desc('Persists execution logs to database via queue')
->inject('publisher')
->inject('publisherForExecutions')
->callback($this->handle(...));
}
public function handle(ExecutionCompleted $event, Publisher $publisher): void
public function handle(ExecutionCompleted $event, ExecutionPublisher $publisherForExecutions): void
{
$queueForExecutions = new Execution($publisher);
$queueForExecutions
->setExecution(new Document($event->execution))
->setProject(new Document($event->project))
->trigger();
$project = new Document($event->project);
$execution = new Document($event->execution);
if ($execution->getAttribute('resourceType', '') === 'functions') {
$traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
$traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
$resourceId = $execution->getAttribute('resourceId', '');
if ($traceProjectId !== '' && $traceFunctionId !== '' && $project->getId() === $traceProjectId && $resourceId === $traceFunctionId) {
Span::init('execution.trace.v1_executions_enqueue');
Span::add('datetime', gmdate('c'));
Span::add('projectId', $project->getId());
Span::add('functionId', $resourceId);
Span::add('executionId', $execution->getId());
Span::add('deploymentId', $execution->getAttribute('deploymentId', ''));
Span::add('status', $execution->getAttribute('status', ''));
Span::current()?->finish();
}
}
$publisherForExecutions->enqueue(new ExecutionMessage(
project: $project,
execution: $execution,
));
}
}
+19
View File
@@ -53,4 +53,23 @@ class Execution extends Event
'execution' => $this->execution,
];
}
/**
* Trim payload for the execution event.
* Only the project ID is needed — the worker DI fetches the full project from the platform database.
*
* @return array
*/
protected function trimPayload(): array
{
$trimmed = [];
if ($this->project) {
$trimmed['project'] = new Document([
'$id' => $this->project->getId(),
]);
}
return $trimmed;
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace Appwrite\Event\Message;
use Utopia\Database\Document;
final class Execution extends Base
{
public function __construct(
public readonly Document $project,
public readonly Document $execution,
) {
}
public function toArray(): array
{
return [
'project' => $this->project->getArrayCopy(),
'execution' => $this->execution->getArrayCopy(),
];
}
public static function fromArray(array $data): static
{
return new self(
project: new Document($data['project'] ?? []),
execution: new Document($data['execution'] ?? []),
);
}
}
+33
View File
@@ -0,0 +1,33 @@
<?php
namespace Appwrite\Event\Message;
use Utopia\Database\Document;
final class Migration extends Base
{
public function __construct(
public readonly Document $project,
public readonly Document $migration,
public readonly array $platform = [],
) {
}
public function toArray(): array
{
return [
'project' => $this->project->getArrayCopy(),
'migration' => $this->migration->getArrayCopy(),
'platform' => $this->platform,
];
}
public static function fromArray(array $data): static
{
return new self(
project: new Document($data['project'] ?? []),
migration: new Document($data['migration'] ?? []),
platform: $data['platform'] ?? [],
);
}
}
@@ -0,0 +1,27 @@
<?php
namespace Appwrite\Event\Message;
use Utopia\Database\Document;
final class StatsResources extends Base
{
public function __construct(
public readonly Document $project,
) {
}
public function toArray(): array
{
return [
'project' => $this->project->getArrayCopy(),
];
}
public static function fromArray(array $data): static
{
return new self(
project: new Document($data['project'] ?? []),
);
}
}
@@ -0,0 +1,27 @@
<?php
namespace Appwrite\Event\Publisher;
use Appwrite\Event\Message\Execution as ExecutionMessage;
use Utopia\Queue\Publisher;
use Utopia\Queue\Queue;
readonly class Execution extends Base
{
public function __construct(
Publisher $publisher,
protected Queue $queue
) {
parent::__construct($publisher);
}
public function enqueue(ExecutionMessage $message): string|bool
{
return $this->publish($this->queue, $message);
}
public function getSize(bool $failed = false): int
{
return $this->getQueueSize($this->queue, $failed);
}
}
@@ -0,0 +1,27 @@
<?php
namespace Appwrite\Event\Publisher;
use Appwrite\Event\Message\Migration as MigrationMessage;
use Utopia\Queue\Publisher;
use Utopia\Queue\Queue;
readonly class Migration extends Base
{
public function __construct(
Publisher $publisher,
protected Queue $queue
) {
parent::__construct($publisher);
}
public function enqueue(MigrationMessage $message): string|bool
{
return $this->publish($this->queue, $message);
}
public function getSize(bool $failed = false): int
{
return $this->getQueueSize($this->queue, $failed);
}
}
@@ -0,0 +1,34 @@
<?php
namespace Appwrite\Event\Publisher;
use Appwrite\Event\Message\StatsResources as StatsResourcesMessage;
use Utopia\Console;
use Utopia\Queue\Publisher;
use Utopia\Queue\Queue;
readonly class StatsResources extends Base
{
public function __construct(
Publisher $publisher,
protected Queue $queue
) {
parent::__construct($publisher);
}
public function enqueue(StatsResourcesMessage $message): string|bool
{
// Resource stats are best-effort; publishing failures should not interrupt the scheduler loop.
try {
return $this->publish($this->queue, $message);
} catch (\Throwable $th) {
Console::error('[StatsResources] Failed to publish stats resources message: ' . $th->getMessage());
return false;
}
}
public function getSize(bool $failed = false): int
{
return $this->getQueueSize($this->queue, $failed);
}
}
+20
View File
@@ -61,6 +61,26 @@ class Realtime extends Event
return $this->subscribers;
}
/**
* Reset the event state for long-lived worker processes.
*
* `Event::reset()` clears params/sensitive/event/payload only. Realtime routing also
* depends on `context`, `subscribers`, and `project`/`user` fields, so we clear them too
* to prevent stale state from affecting subsequent triggers.
*/
public function reset(): self
{
parent::reset();
$this->subscribers = [];
$this->context = [];
$this->project = null;
$this->user = null;
$this->userId = null;
return $this;
}
/**
* Execute Event.
*
+2 -2
View File
@@ -81,8 +81,8 @@ abstract class Adapter implements PromiseAdapter
/**
* Create a new promise that resolves when all passed in promises resolve.
*
* @param array $promisesOrValues
* @param iterable $promisesOrValues
* @return GQLPromise
*/
abstract public function all(array $promisesOrValues): GQLPromise;
abstract public function all(iterable $promisesOrValues): GQLPromise;
}
@@ -35,8 +35,12 @@ class Swoole extends Adapter
return new GQLPromise($promise, $this);
}
public function all(array $promisesOrValues): GQLPromise
public function all(iterable $promisesOrValues): GQLPromise
{
if ($promisesOrValues instanceof \Traversable) {
$promisesOrValues = \iterator_to_array($promisesOrValues);
}
return new GQLPromise(SwoolePromise::all($promisesOrValues), $this);
}
}
+8 -1
View File
@@ -262,17 +262,24 @@ class Resolvers
$request = clone $request;
$utopia->setResource('request', static fn () => $request);
$response->setContentType(Response::CONTENT_TYPE_NULL);
$response->clearSent();
$original = $utopia->getRoute();
try {
$route = $utopia->match($request, fresh: true);
$utopia->execute($route, $request);
$utopia->execute($route, $request, $response);
} catch (\Throwable $e) {
if ($beforeReject) {
$e = $beforeReject($e);
}
$reject($e);
return;
} finally {
if ($original !== null) {
$utopia->setRoute($original);
}
}
$payload = $response->getPayload();
+8 -3
View File
@@ -3,12 +3,13 @@
namespace Appwrite\GraphQL\Types;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\StringValueNode;
// https://github.com/webonyx/graphql-php/issues/129#issuecomment-309366803
class Assoc extends Json
{
public $name = 'Assoc';
public $description = 'The `Assoc` scalar type represents associative array values.';
public string $name = 'Assoc';
public ?string $description = 'The `Assoc` scalar type represents associative array values.';
public function serialize($value)
{
@@ -30,6 +31,10 @@ class Assoc extends Json
public function parseLiteral(Node $valueNode, ?array $variables = null)
{
return \json_decode($valueNode->value, true);
if ($valueNode instanceof StringValueNode) {
return \json_decode($valueNode->value, true);
}
return parent::parseLiteral($valueNode, $variables);
}
}
+2 -2
View File
@@ -8,8 +8,8 @@ use GraphQL\Type\Definition\ScalarType;
class InputFile extends ScalarType
{
public $name = 'InputFile';
public $description = 'The `InputFile` special type represents a file to be uploaded in the same HTTP request as specified by
public string $name = 'InputFile';
public ?string $description = 'The `InputFile` special type represents a file to be uploaded in the same HTTP request as specified by
[graphql-multipart-request-spec](https://github.com/jaydenseric/graphql-multipart-request-spec).';
public function serialize($value)
+2 -2
View File
@@ -14,8 +14,8 @@ use GraphQL\Type\Definition\ScalarType;
// https://github.com/webonyx/graphql-php/issues/129#issuecomment-309366803
class Json extends ScalarType
{
public $name = 'Json';
public $description = 'The `JSON` scalar type represents JSON values as specified by
public string $name = 'Json';
public ?string $description = 'The `JSON` scalar type represents JSON values as specified by
[ECMA-404](https://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).';
public function serialize($value)
+27 -14
View File
@@ -20,6 +20,7 @@ class Realtime extends MessagingAdapter
* [CONNECTION_ID] ->
* 'projectId' -> [PROJECT_ID]
* 'roles' -> [ROLE_x, ROLE_Y]
* 'userId' -> [USER_ID]
* 'channels' -> [CHANNEL_NAME_X, CHANNEL_NAME_Y, CHANNEL_NAME_Z]
*/
public array $connections = [];
@@ -67,25 +68,35 @@ class Realtime extends MessagingAdapter
* @param array $queryGroup Array of Query objects for this subscription (AND logic within subscription)
* @return void
*/
public function subscribe(string $projectId, mixed $identifier, string $subscriptionId, array $roles, array $channels, array $queryGroup = []): void
{
public function subscribe(
string $projectId,
mixed $identifier,
string $subscriptionId,
array $roles,
array $channels,
array $queryGroup = [],
?string $userId = null
): void {
if (!isset($this->subscriptions[$projectId])) { // Init Project
$this->subscriptions[$projectId] = [];
}
$strings = [];
if (empty($queryGroup)) {
$strings[] = Query::select(['*'])->toString();
} else {
foreach ($queryGroup as $query) {
$strings[] = $query->toString();
}
}
$data = [];
$data = [
'strings' => $strings,
'compiled' => RuntimeQuery::compile($queryGroup),
];
if (!empty($channels)) {
if (empty($queryGroup)) {
$strings[] = Query::select(['*'])->toString();
} else {
foreach ($queryGroup as $query) {
$strings[] = $query->toString();
}
}
$data = [
'strings' => $strings,
'compiled' => RuntimeQuery::compile($queryGroup),
];
}
foreach ($roles as $role) {
if (!isset($this->subscriptions[$projectId][$role])) {
@@ -103,10 +114,12 @@ class Realtime extends MessagingAdapter
}
}
// Update connection info
// Keep userId from onOpen/authentication when provided.
// Fallback to existing stored value for subsequent subscribe upserts.
$this->connections[$identifier] = [
'projectId' => $projectId,
'roles' => $roles,
'userId' => $userId ?? ($this->connections[$identifier]['userId'] ?? ''),
'channels' => $channels
];
}
+1
View File
@@ -93,6 +93,7 @@ abstract class Migration
'1.8.0' => 'V23',
'1.8.1' => 'V23',
'1.9.0' => 'V24',
'1.9.1' => 'V24',
];
/**
@@ -50,6 +50,11 @@ class Create extends Action
return UtopiaResponse::MODEL_DOCUMENT_LIST;
}
protected function getSupportForEmptyDocument()
{
return false;
}
public function __construct()
{
$this
@@ -139,30 +144,42 @@ class Create extends Action
->inject('eventProcessor')
->callback($this->action(...));
}
public function action(string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, ?array $documents, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, User $user, Event $queueForEvents, Context $usage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan, Authorization $authorization, EventProcessor $eventProcessor): void
{
$data = \is_string($data)
? \json_decode($data, true)
: $data;
$supportsEmptyDocument = $this->getSupportForEmptyDocument();
$hasData = !empty($data);
$hasDocuments = !empty($documents);
/**
* Determine which internal path to call, single or bulk
*/
if (empty($data) && empty($documents)) {
if (!$supportsEmptyDocument && !$hasData && !$hasDocuments) {
// No single or bulk documents provided
throw new Exception($this->getMissingDataException());
}
if (!empty($data) && !empty($documents)) {
// When empty documents are supported, an empty payload should still be treated as single create.
if ($supportsEmptyDocument && !$hasData && !$hasDocuments) {
$data = [];
$hasData = true;
}
if ($hasData && $hasDocuments) {
// Both single and bulk documents provided
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'You can only send one of the following parameters: data, ' . $this->getSDKGroup());
}
if (!empty($data) && empty($documentId)) {
if ($hasData && empty($documentId)) {
// Single document provided without document ID
$document = $this->isCollectionsAPI() ? 'Document' : 'Row';
$message = "$document ID is required when creating a single " . strtolower($document) . '.';
throw new Exception($this->getMissingDataException(), $message);
}
if (!empty($documents) && !empty($documentId)) {
if ($hasDocuments && !empty($documentId)) {
// Bulk documents provided with document ID
$documentId = $this->isCollectionsAPI() ? 'documentId' : 'rowId';
throw new Exception(
@@ -170,13 +187,13 @@ class Create extends Action
"Param \"$documentId\" is not allowed when creating multiple " . $this->getSDKGroup() . ', set "$id" on each instead.'
);
}
if (!empty($documents) && !empty($permissions)) {
if ($hasDocuments && !empty($permissions)) {
// Bulk documents provided with permissions
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Param "permissions" is disallowed when creating multiple ' . $this->getSDKGroup() . ', set "$permissions" on each instead');
}
$isBulk = true;
if (!empty($data)) {
$isBulk = $hasDocuments;
if ($hasData) {
// Single document provided, convert to single item array
// But remember that it was single to respond with a single document
$isBulk = false;
@@ -145,7 +145,11 @@ class XList extends Action
$documentsField = $this->getListCacheField($collection, $roles, $queries, self::LIST_CACHE_FIELD_DOCUMENTS);
$documentsCacheHit = false;
$cachedDocuments = $dbForProject->getCache()->load($cacheKey, $ttl, $documentsField);
try {
$cachedDocuments = $dbForProject->getCache()->load($cacheKey, $ttl, $documentsField);
} catch (\Throwable) {
$cachedDocuments = null;
}
if ($cachedDocuments !== null &&
$cachedDocuments !== false &&
@@ -157,21 +161,30 @@ class XList extends Action
} else {
$documents = $find();
// Convert Document objects to arrays for caching
$documentsArray = \array_map(function ($doc) {
return $doc->getArrayCopy();
}, $documents);
$dbForProject->getCache()->save($cacheKey, $documentsArray, $documentsField);
try {
$dbForProject->getCache()->save($cacheKey, $documentsArray, $documentsField);
} catch (\Throwable) {
}
}
if ($includeTotal) {
$totalField = $this->getListCacheField($collection, $roles, $queries, self::LIST_CACHE_FIELD_TOTAL);
$cachedTotal = $dbForProject->getCache()->load($cacheKey, $ttl, $totalField);
try {
$cachedTotal = $dbForProject->getCache()->load($cacheKey, $ttl, $totalField);
} catch (\Throwable) {
$cachedTotal = null;
}
if ($cachedTotal !== null && $cachedTotal !== false) {
$total = $cachedTotal;
} else {
$total = $dbForDatabases->count($collectionTableId, $queries, APP_LIMIT_COUNT);
$dbForProject->getCache()->save($cacheKey, $total, $totalField);
try {
$dbForProject->getCache()->save($cacheKey, $total, $totalField);
} catch (\Throwable) {
}
}
} else {
$total = 0;
@@ -34,6 +34,12 @@ class Create extends DocumentCreate
return UtopiaResponse::MODEL_DOCUMENT_LIST;
}
protected function getSupportForEmptyDocument()
{
return true;
}
public function __construct()
{
$this
@@ -1,59 +0,0 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Logs;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Logs\XList as DocumentLogXList;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Utopia\Database\Validator\Queries;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\UID;
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
class XList extends DocumentLogXList
{
public static function getName(): string
{
return 'listDocumentsDBDocumentLogs';
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/documentsdb/:databaseId/collections/:collectionId/documents/:documentId/logs')
->desc('List document logs')
->groups(['api', 'database'])
->label('scope', 'documents.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: 'documentsDB',
group: 'logs',
name: 'listDocumentLogs',
description: '/docs/references/documentsdb/get-document-logs.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON,
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('documentId', '', new UID(), 'Document ID.')
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->inject('response')
->inject('dbForProject')
->inject('getDatabasesDB')
->inject('locale')
->inject('geodb')
->inject('authorization')
->inject('audit')
->callback($this->action(...));
}
}
@@ -1,58 +0,0 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Logs;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Logs\XList as CollectionLogXList;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Queries;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\UID;
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
class XList extends CollectionLogXList
{
public static function getName(): string
{
return 'listDocumentsDBCollectionLogs';
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/documentsdb/:databaseId/collections/:collectionId/logs')
->desc('List collection logs')
->groups(['api', 'database'])
->label('scope', 'collections.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: 'documentsDB',
group: $this->getSdkGroup(),
name: 'listCollectionLogs',
description: '/docs/references/documentsdb/get-collection-logs.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject'])
->param('collectionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Collection ID.', false, ['dbForProject'])
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->inject('response')
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->inject('authorization')
->inject('audit')
->callback($this->action(...));
}
}
@@ -1,59 +0,0 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Logs;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Logs\XList as DocumentLogXList;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Utopia\Database\Validator\Queries;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\UID;
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
class XList extends DocumentLogXList
{
public static function getName(): string
{
return 'listVectorsDBDocumentLogs';
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/vectorsdb/:databaseId/collections/:collectionId/documents/:documentId/logs')
->desc('List document logs')
->groups(['api', 'database'])
->label('scope', 'documents.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: 'vectorsDB',
group: 'logs',
name: 'listDocumentLogs',
description: '/docs/references/vectorsdb/get-document-logs.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON,
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('documentId', '', new UID(), 'Document ID.')
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->inject('response')
->inject('dbForProject')
->inject('getDatabasesDB')
->inject('locale')
->inject('geodb')
->inject('authorization')
->inject('audit')
->callback($this->action(...));
}
}
@@ -1,57 +0,0 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Logs;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Logs\XList as CollectionLogXList;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Utopia\Database\Validator\Queries;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\UID;
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
class XList extends CollectionLogXList
{
public static function getName(): string
{
return 'listVectorsDBCollectionLogs';
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/vectorsdb/:databaseId/collections/:collectionId/logs')
->desc('List collection logs')
->groups(['api', 'database'])
->label('scope', 'collections.read')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: 'vectorsDB',
group: $this->getSdkGroup(),
name: 'listCollectionLogs',
description: '/docs/references/vectorsdb/get-collection-logs.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->inject('response')
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->inject('authorization')
->inject('audit')
->callback($this->action(...));
}
}
@@ -12,7 +12,6 @@ use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\B
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Create as CreateRow;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Delete as DeleteRow;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Get as GetRow;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Logs\XList as ListRowLogs;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Update as UpdateRow;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\Upsert as UpsertRow;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Documents\XList as ListRows;
@@ -21,7 +20,6 @@ use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Indexes\Cre
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Indexes\Delete as DeleteColumnIndex;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Indexes\Get as GetColumnIndex;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Indexes\XList as ListColumnIndexes;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Logs\XList as ListTableLogs;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Update as UpdateTable;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\Usage\Get as GetTableUsage;
use Appwrite\Platform\Modules\Databases\Http\DocumentsDB\Collections\XList as ListTables;
@@ -69,7 +67,6 @@ class DocumentsDB extends Base
$service->addAction(UpdateTable::getName(), new UpdateTable());
$service->addAction(DeleteTable::getName(), new DeleteTable());
$service->addAction(ListTables::getName(), new ListTables());
$service->addAction(ListTableLogs::getName(), new ListTableLogs());
$service->addAction(GetTableUsage::getName(), new GetTableUsage());
}
@@ -92,7 +89,6 @@ class DocumentsDB extends Base
$service->addAction(DeleteRow::getName(), new DeleteRow());
$service->addAction(DeleteRows::getName(), new DeleteRows());
$service->addAction(ListRows::getName(), new ListRows());
$service->addAction(ListRowLogs::getName(), new ListRowLogs());
$service->addAction(IncrementRowColumn::getName(), new IncrementRowColumn());
$service->addAction(DecrementRowColumn::getName(), new DecrementRowColumn());
}
@@ -10,7 +10,6 @@ use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Bul
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Create as CreateDocument;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Delete as DeleteDocument;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Get as GetDocument;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Logs\XList as ListDocumentLogs;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Update as UpdateDocument;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\Upsert as UpsertDocument;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Documents\XList as ListDocuments;
@@ -19,7 +18,6 @@ use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Indexes\Creat
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Indexes\Delete as DeleteIndex;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Indexes\Get as GetIndex;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Indexes\XList as ListIndexes;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Logs\XList as ListCollectionLogs;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Update as UpdateCollection;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\Usage\Get as GetCollectionUsage;
use Appwrite\Platform\Modules\Databases\Http\VectorsDB\Collections\XList as ListCollections;
@@ -69,7 +67,6 @@ class VectorsDB extends Base
$service->addAction(UpdateCollection::getName(), new UpdateCollection());
$service->addAction(DeleteCollection::getName(), new DeleteCollection());
$service->addAction(ListCollections::getName(), new ListCollections());
$service->addAction(ListCollectionLogs::getName(), new ListCollectionLogs());
$service->addAction(GetCollectionUsage::getName(), new GetCollectionUsage());
}
@@ -92,7 +89,6 @@ class VectorsDB extends Base
$service->addAction(UpdateDocuments::getName(), new UpdateDocuments());
$service->addAction(UpsertDocuments::getName(), new UpsertDocuments());
$service->addAction(DeleteDocuments::getName(), new DeleteDocuments());
$service->addAction(ListDocumentLogs::getName(), new ListDocumentLogs());
}
private function registerTransactionActions(Service $service): void
@@ -650,26 +650,30 @@ class Databases extends Action
Document|null $attribute = null,
Document|null $index = null,
): void {
$queueForRealtime
->setProject($project)
->setSubscribers(['console'])
->setEvent($event)
->setParam('databaseId', $database->getId())
->setParam('tableId', $collection->getId())
->setParam('collectionId', $collection->getId());
try {
$queueForRealtime
->setProject($project)
->setSubscribers(['console'])
->setEvent($event)
->setParam('databaseId', $database->getId())
->setParam('tableId', $collection->getId())
->setParam('collectionId', $collection->getId());
if (! empty($attribute)) {
$queueForRealtime
->setParam('columnId', $attribute->getId())
->setParam('attributeId', $attribute->getId())
->setPayload($attribute->getArrayCopy());
}
if (! empty($index)) {
$queueForRealtime
->setParam('indexId', $index->getId())
->setPayload($index->getArrayCopy());
if (! empty($attribute)) {
$queueForRealtime
->setParam('columnId', $attribute->getId())
->setParam('attributeId', $attribute->getId())
->setPayload($attribute->getArrayCopy());
}
if (! empty($index)) {
$queueForRealtime
->setParam('indexId', $index->getId())
->setPayload($index->getArrayCopy());
}
$queueForRealtime->trigger();
} finally {
$queueForRealtime->reset();
}
$queueForRealtime->trigger();
}
}
@@ -305,9 +305,7 @@ class Create extends Base
if ($async) {
if (is_null($scheduledAt)) {
if ($project->getId() != '6862e6a6000cce69f9da') {
$execution = $authorization->skip(fn () => $dbForProject->createDocument('executions', $execution));
}
$execution = $authorization->skip(fn () => $dbForProject->createDocument('executions', $execution));
$queueForFunctions
->setType('http')
->setExecution($execution)
@@ -348,9 +346,7 @@ class Create extends Base
->setAttribute('scheduleInternalId', $schedule->getSequence())
->setAttribute('scheduledAt', $scheduledAt);
if ($project->getId() != '6862e6a6000cce69f9da') {
$execution = $authorization->skip(fn () => $dbForProject->createDocument('executions', $execution));
}
$execution = $authorization->skip(fn () => $dbForProject->createDocument('executions', $execution));
}
if ($executionsRetentionCount > 0 && ENABLE_EXECUTIONS_LIMIT_ON_ROUTE) {
@@ -516,9 +512,7 @@ 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)))
;
if ($project->getId() != '6862e6a6000cce69f9da') {
$execution = $authorization->skip(fn () => $dbForProject->createDocument('executions', $execution));
}
$execution = $authorization->skip(fn () => $dbForProject->createDocument('executions', $execution));
}
$executionResponse['headers']['x-appwrite-execution-id'] = $execution->getId();
@@ -11,10 +11,10 @@ use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\Migration;
use Appwrite\Event\Publisher\Migration as MigrationPublisher;
use Appwrite\Event\Publisher\StatsResources as StatsResourcesPublisher;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Event\Screenshot;
use Appwrite\Event\StatsResources;
use Appwrite\Event\Webhook;
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Base;
use Appwrite\SDK\AuthType;
@@ -78,13 +78,13 @@ class Get extends Base
->inject('queueForAudits')
->inject('queueForMails')
->inject('queueForFunctions')
->inject('queueForStatsResources')
->inject('publisherForStatsResources')
->inject('publisherForUsage')
->inject('queueForWebhooks')
->inject('queueForCertificates')
->inject('queueForBuilds')
->inject('queueForMessaging')
->inject('queueForMigrations')
->inject('publisherForMigrations')
->inject('queueForScreenshots')
->callback($this->action(...));
}
@@ -98,13 +98,13 @@ class Get extends Base
Audit $queueForAudits,
Mail $queueForMails,
Func $queueForFunctions,
StatsResources $queueForStatsResources,
StatsResourcesPublisher $publisherForStatsResources,
UsagePublisher $publisherForUsage,
Webhook $queueForWebhooks,
Certificate $queueForCertificates,
Build $queueForBuilds,
Messaging $queueForMessaging,
Migration $queueForMigrations,
MigrationPublisher $publisherForMigrations,
Screenshot $queueForScreenshots,
): void {
$threshold = (int) $threshold;
@@ -115,14 +115,14 @@ class Get extends Base
System::getEnv('_APP_AUDITS_QUEUE_NAME', Event::AUDITS_QUEUE_NAME) => $queueForAudits,
System::getEnv('_APP_MAILS_QUEUE_NAME', Event::MAILS_QUEUE_NAME) => $queueForMails,
System::getEnv('_APP_FUNCTIONS_QUEUE_NAME', Event::FUNCTIONS_QUEUE_NAME) => $queueForFunctions,
System::getEnv('_APP_STATS_RESOURCES_QUEUE_NAME', Event::STATS_RESOURCES_QUEUE_NAME) => $queueForStatsResources,
System::getEnv('_APP_STATS_RESOURCES_QUEUE_NAME', Event::STATS_RESOURCES_QUEUE_NAME) => $publisherForStatsResources,
System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME) => $publisherForUsage,
System::getEnv('_APP_WEBHOOK_QUEUE_NAME', Event::WEBHOOK_QUEUE_NAME) => $queueForWebhooks,
System::getEnv('_APP_CERTIFICATES_QUEUE_NAME', Event::CERTIFICATES_QUEUE_NAME) => $queueForCertificates,
System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::BUILDS_QUEUE_NAME) => $queueForBuilds,
System::getEnv('_APP_SCREENSHOTS_QUEUE_NAME', Event::SCREENSHOTS_QUEUE_NAME) => $queueForScreenshots,
System::getEnv('_APP_MESSAGING_QUEUE_NAME', Event::MESSAGING_QUEUE_NAME) => $queueForMessaging,
System::getEnv('_APP_MIGRATIONS_QUEUE_NAME', Event::MIGRATIONS_QUEUE_NAME) => $queueForMigrations,
System::getEnv('_APP_MIGRATIONS_QUEUE_NAME', Event::MIGRATIONS_QUEUE_NAME) => $publisherForMigrations,
};
$failed = $queue->getSize(failed: true);
@@ -2,7 +2,7 @@
namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\Migrations;
use Appwrite\Event\Migration;
use Appwrite\Event\Publisher\Migration as MigrationPublisher;
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@@ -42,16 +42,16 @@ class Get extends Base
contentType: ContentType::JSON
))
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
->inject('queueForMigrations')
->inject('publisherForMigrations')
->inject('response')
->callback($this->action(...));
}
public function action(int|string $threshold, Migration $queueForMigrations, Response $response): void
public function action(int|string $threshold, MigrationPublisher $publisherForMigrations, Response $response): void
{
$threshold = (int) $threshold;
$size = $queueForMigrations->getSize();
$size = $publisherForMigrations->getSize();
$this->assertQueueThreshold($size, $threshold);
@@ -2,7 +2,7 @@
namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\StatsResources;
use Appwrite\Event\StatsResources;
use Appwrite\Event\Publisher\StatsResources as StatsResourcesPublisher;
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@@ -42,16 +42,16 @@ class Get extends Base
contentType: ContentType::JSON
))
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
->inject('queueForStatsResources')
->inject('publisherForStatsResources')
->inject('response')
->callback($this->action(...));
}
public function action(int|string $threshold, StatsResources $queueForStatsResources, Response $response): void
public function action(int|string $threshold, StatsResourcesPublisher $publisherForStatsResources, Response $response): void
{
$threshold = (int) $threshold;
$size = $queueForStatsResources->getSize();
$size = $publisherForStatsResources->getSize();
$this->assertQueueThreshold($size, $threshold);
@@ -62,7 +62,7 @@ class Create extends Base
))
->param('keyId', '', fn (Database $dbForPlatform) => new CustomId(false, $dbForPlatform->getAdapter()->getMaxUIDLength()), '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.', false, ['dbForPlatform'])
->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('scopes', [], 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.', optional: false)
->param('expire', null, new Nullable(new Datetime()), '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('queueForEvents')
@@ -72,13 +72,10 @@ class Create extends Base
->callback($this->action(...));
}
/**
* @param array<string>|null $scopes
*/
public function action(
string $keyId,
string $name,
?array $scopes,
array $scopes,
?string $expire,
Response $response,
QueueEvent $queueForEvents,
@@ -95,7 +92,7 @@ class Create extends Base
'resourceId' => $project->getId(),
'resourceType' => 'projects',
'name' => $name,
'scopes' => $scopes ?? [],
'scopes' => $scopes,
'expire' => $expire,
'sdks' => [],
'accessedAt' => null,
@@ -60,7 +60,7 @@ class Update extends Base
))
->param('keyId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Key ID.', false, ['dbForPlatform'])
->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('scopes', [], 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.', optional: false)
->param('expire', null, new Nullable(new Datetime()), '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('queueForEvents')
@@ -70,13 +70,10 @@ class Update extends Base
->callback($this->action(...));
}
/**
* @param array<string>|null $scopes
*/
public function action(
string $keyId,
string $name,
?array $scopes,
array $scopes,
?string $expire,
Response $response,
QueueEvent $queueForEvents,
@@ -92,7 +89,7 @@ class Update extends Base
$updates = new Document([
'name' => $name,
'scopes' => $scopes ?? [],
'scopes' => $scopes,
'expire' => $expire,
]);
@@ -64,7 +64,7 @@ class Create extends Action
))
->param('platformId', '', fn (Database $dbForPlatform) => new CustomId(false, $dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForPlatform'])
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
->param('hostname', '', new Hostname(), 'Platform web hostname. Max length: 256 chars.', optional: true) // Optional for backwards compatibility
->param('hostname', '', new Hostname(), 'Platform web hostname. Max length: 256 chars.', optional: true, example: 'app.example.com') // Optional for backwards compatibility
->param('key', '', new Text(256), 'Deprecated: Package name for Android or bundle ID for iOS or macOS. Max length: 256 chars.', optional: true, deprecated: true) // Exists for backwards compatibility
->param('type', '', new Text(256), 'Deprecated: Platform type. Max length: 256 chars.', optional: true, deprecated: true) // Exists for backwards compatibility
->inject('request')
@@ -56,7 +56,7 @@ class Update extends Action
))
->param('platformId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Platform ID.', false, ['dbForPlatform'])
->param('name', null, new Text(128), 'Platform name. Max length: 128 chars.')
->param('hostname', '', new Hostname(), 'Platform web hostname. Max length: 256 chars.', optional: true) // Optional for backwards compatibility
->param('hostname', '', new Hostname(), 'Platform web hostname. Max length: 256 chars.', optional: true, example: 'app.example.com') // Optional for backwards compatibility
->param('key', '', new Text(256), 'Package name for Android or bundle ID for iOS or macOS. Max length: 256 chars.', optional: true, deprecated: true) // Exists for backwards compatibility
->inject('response')
->inject('queueForEvents')
@@ -0,0 +1,80 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\Protocols\Status;
use Appwrite\Platform\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
use Utopia\Validator\WhiteList;
class Update extends Action
{
use HTTP;
public static function getName()
{
return 'updateProjectProtocolStatus';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/project/protocols/:protocolId/status')
->httpAlias('/v1/projects/:projectId/api')
->desc('Update project protocol status')
->groups(['api', 'project'])
->label('scope', 'project.write')
->label('event', 'protocols.[protocol].update')
->label('audits.event', 'project.protocols.[protocol].update')
->label('audits.resource', 'project.protocols/{response.$id}')
->label('sdk', new Method(
namespace: 'project',
group: null,
name: 'updateProtocolStatus',
description: <<<EOT
Update the status of a specific protocol. Use this endpoint to enable or disable a protocol in your project.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROJECT,
)
],
))
->param('protocolId', '', new WhiteList(array_keys(Config::getParam('protocols')), true), 'Protocol name. Can be one of: ' . \implode(', ', array_keys(Config::getParam('protocols'))))
->param('enabled', null, new Boolean(), 'Protocol status.')
->inject('response')
->inject('dbForPlatform')
->inject('project')
->inject('authorization')
->callback($this->action(...));
}
public function action(
string $protocolId,
bool $enabled,
Response $response,
Database $dbForPlatform,
Document $project,
Authorization $authorization
): void {
$protocols = $project->getAttribute('apis', []);
$protocols[$protocolId] = $enabled;
$project = $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), new Document([
'apis' => $protocols,
])));
$response->dynamic($project, Response::MODEL_PROJECT);
}
}
@@ -0,0 +1,80 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\Services\Status;
use Appwrite\Platform\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
use Utopia\Validator\WhiteList;
class Update extends Action
{
use HTTP;
public static function getName()
{
return 'updateProjectServiceStatus';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/project/services/:serviceId/status')
->httpAlias('/v1/projects/:projectId/service')
->desc('Update project service status')
->groups(['api', 'project'])
->label('scope', 'project.write')
->label('event', 'services.[service].update')
->label('audits.event', 'project.services.[service].update')
->label('audits.resource', 'project.services/{response.$id}')
->label('sdk', new Method(
namespace: 'project',
group: null,
name: 'updateServiceStatus',
description: <<<EOT
Update the status of a specific service. Use this endpoint to enable or disable a service in your project.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROJECT,
)
],
))
->param('serviceId', '', new WhiteList(array_keys(array_filter(Config::getParam('services'), fn ($element) => $element['optional'])), true), 'Service name. Can be one of: '.\implode(', ', array_keys(array_filter(Config::getParam('services'), fn ($element) => $element['optional']))))
->param('enabled', null, new Boolean(), 'Service status.')
->inject('response')
->inject('dbForPlatform')
->inject('project')
->inject('authorization')
->callback($this->action(...));
}
public function action(
string $serviceId,
bool $enabled,
Response $response,
Database $dbForPlatform,
Document $project,
Authorization $authorization
): void {
$services = $project->getAttribute('services', []);
$services[$serviceId] = $enabled;
$project = $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), new Document([
'services' => $services,
])));
$response->dynamic($project, Response::MODEL_PROJECT);
}
}
@@ -22,6 +22,8 @@ use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Web\Update as Updat
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Windows\Create as CreateWindowsPlatform;
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Windows\Update as UpdateWindowsPlatform;
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\XList as ListPlatforms;
use Appwrite\Platform\Modules\Project\Http\Project\Protocols\Status\Update as UpdateProjectProtocolStatus;
use Appwrite\Platform\Modules\Project\Http\Project\Services\Status\Update as UpdateProjectServiceStatus;
use Appwrite\Platform\Modules\Project\Http\Project\Variables\Create as CreateVariable;
use Appwrite\Platform\Modules\Project\Http\Project\Variables\Delete as DeleteVariable;
use Appwrite\Platform\Modules\Project\Http\Project\Variables\Get as GetVariable;
@@ -40,6 +42,8 @@ class Http extends Service
// Project
$this->addAction(UpdateProjectLabels::getName(), new UpdateProjectLabels());
$this->addAction(UpdateProjectProtocolStatus::getName(), new UpdateProjectProtocolStatus());
$this->addAction(UpdateProjectServiceStatus::getName(), new UpdateProjectServiceStatus());
// Variables
$this->addAction(CreateVariable::getName(), new CreateVariable());
@@ -15,7 +15,6 @@ use Utopia\Compression\Algorithms\GZIP;
use Utopia\Compression\Algorithms\Zstd;
use Utopia\Compression\Compression;
use Utopia\Config\Config;
use Utopia\Console;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
@@ -26,6 +25,7 @@ use Utopia\Http\Adapter\Swoole\Request;
use Utopia\Image\Image;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Span\Span;
use Utopia\Storage\Device;
use Utopia\System\System;
use Utopia\Validator\HexColor;
@@ -269,7 +269,17 @@ class Get extends Action
$totalTime = \microtime(true) - $startTime;
Console::info("File preview rendered,project=" . $project->getId() . ",bucket=" . $bucketId . ",file=" . $file->getId() . ",uri=" . $request->getURI() . ",total=" . $totalTime . ",rendering=" . $renderingTime . ",decryption=" . $decryptionTime . ",decompression=" . $decompressionTime . ",download=" . $downloadTime);
Span::add('storage.file.id', $file->getId());
Span::add('storage.bucket.id', $bucketId);
Span::add('storage.file.size_bytes', $file->getAttribute('sizeActual'));
if (!empty($type)) {
Span::add('storage.file.extension', $type);
}
Span::add('storage.timing.download_seconds', $downloadTime);
Span::add('storage.timing.decryption_seconds', $decryptionTime);
Span::add('storage.timing.decompression_seconds', $decompressionTime);
Span::add('storage.timing.rendering_seconds', $renderingTime);
Span::add('storage.timing.total_seconds', $totalTime);
$contentType = (\array_key_exists($output, $outputs)) ? $outputs[$output] : $outputs['jpg'];
@@ -21,6 +21,7 @@ use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
use Utopia\Validator\Multiple;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
use Utopia\Validator\URL;
@@ -65,9 +66,10 @@ class Create extends Action
->param('name', null, new Text(128), 'Webhook name. Max length: 128 chars.')
->param('events', null, new ArrayList(new Event(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Events list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.')
->param('enabled', true, new Boolean(), 'Enable or disable a webhook.', true)
->param('security', false, new Boolean(), 'Certificate verification, false for disabled or true for enabled.', true)
->param('httpUser', '', new Text(256), 'Webhook HTTP user. Max length: 256 chars.', true)
->param('httpPass', '', new Text(256), 'Webhook HTTP password. Max length: 256 chars.', true)
->param('tls', false, new Boolean(), 'Certificate verification, false for disabled or true for enabled.', true)
->param('authUsername', '', new Text(256), 'Webhook HTTP user. Max length: 256 chars.', true)
->param('authPassword', '', new Text(256), 'Webhook HTTP password. Max length: 256 chars.', true)
->param('secret', null, new Nullable(new Text(256, 8)), 'Webhook secret key. If not provided, a new key will be generated automatically. Key must be at least 8 characters long, and at max 256 characters.', optional: true)
->inject('response')
->inject('project')
->inject('queueForEvents')
@@ -85,9 +87,10 @@ class Create extends Action
string $name,
array $events,
bool $enabled,
bool $security,
string $httpUser,
string $httpPass,
bool $tls,
string $authUsername,
string $authPassword,
?string $secret,
Response $response,
Document $project,
QueueEvent $queueForEvents,
@@ -104,10 +107,10 @@ class Create extends Action
'name' => $name,
'events' => $events,
'url' => $url,
'security' => $security,
'httpUser' => $httpUser,
'httpPass' => $httpPass,
'signatureKey' => \bin2hex(\random_bytes(64)),
'security' => $tls,
'httpUser' => $authUsername,
'httpPass' => $authPassword,
'signatureKey' => $secret ?? \bin2hex(\random_bytes(64)),
'enabled' => $enabled,
]);
@@ -72,6 +72,8 @@ class Get extends Action
throw new Exception(Exception::WEBHOOK_NOT_FOUND);
}
$webhook->removeAttribute('signatureKey');
$response->dynamic($webhook, Response::MODEL_WEBHOOK);
}
}
@@ -1,6 +1,6 @@
<?php
namespace Appwrite\Platform\Modules\Webhooks\Http\Webhooks\Signature;
namespace Appwrite\Platform\Modules\Webhooks\Http\Webhooks\Secret;
use Appwrite\Event\Event as QueueEvent;
use Appwrite\Extend\Exception;
@@ -15,6 +15,8 @@ use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
class Update extends Action
{
@@ -22,15 +24,15 @@ class Update extends Action
public static function getName()
{
return 'updateWebhookSignature';
return 'updateWebhookSecret';
}
public function __construct()
{
$this->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/webhooks/:webhookId/signature')
->setHttpPath('/v1/webhooks/:webhookId/secret')
->httpAlias('/v1/projects/:projectId/webhooks/:webhookId/signature')
->desc('Update webhook signature key')
->desc('Update webhook secret key')
->groups(['api', 'webhooks'])
->label('scope', 'webhooks.write')
->label('event', 'webhooks.[webhookId].update')
@@ -39,9 +41,9 @@ class Update extends Action
->label('sdk', new Method(
namespace: 'webhooks',
group: null,
name: 'updateSignature',
name: 'updateSecret',
description: <<<EOT
Update the webhook signature key. This endpoint can be used to regenerate the signature key used to sign and validate payload deliveries for a specific webhook.
Update the webhook signing key. This endpoint can be used to regenerate the signing key used to sign and validate payload deliveries for a specific webhook.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
@@ -52,6 +54,7 @@ class Update extends Action
]
))
->param('webhookId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Webhook ID.', false, ['dbForPlatform'])
->param('secret', null, new Nullable(new Text(256, 8)), 'Webhook secret key. If not provided, a new key will be generated automatically. Key must be at least 8 characters long, and at max 256 characters.', optional: true)
->inject('response')
->inject('project')
->inject('queueForEvents')
@@ -62,6 +65,7 @@ class Update extends Action
public function action(
string $webhookId,
?string $secret,
Response $response,
Document $project,
QueueEvent $queueForEvents,
@@ -78,7 +82,7 @@ class Update extends Action
}
$updates = new Document([
'signatureKey' => \bin2hex(\random_bytes(64)),
'signatureKey' => $secret ?? \bin2hex(\random_bytes(64)),
]);
$webhook = $authorization->skip(fn () => $dbForPlatform->updateDocument('webhooks', $webhook->getId(), $updates));
@@ -63,9 +63,9 @@ class Update extends Action
->param('url', '', fn () => new Multiple([new URL(['http', 'https']), new PublicDomain()], Multiple::TYPE_STRING), 'Webhook URL.')
->param('events', null, new ArrayList(new Event(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Events list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.')
->param('enabled', true, new Boolean(), 'Enable or disable a webhook.', true)
->param('security', false, new Boolean(), 'Certificate verification, false for disabled or true for enabled.', true)
->param('httpUser', '', new Text(256), 'Webhook HTTP user. Max length: 256 chars.', true)
->param('httpPass', '', new Text(256), 'Webhook HTTP password. Max length: 256 chars.', true)
->param('tls', false, new Boolean(), 'Certificate verification, false for disabled or true for enabled.', true)
->param('authUsername', '', new Text(256), 'Webhook HTTP user. Max length: 256 chars.', true)
->param('authPassword', '', new Text(256), 'Webhook HTTP password. Max length: 256 chars.', true)
->inject('response')
->inject('project')
->inject('queueForEvents')
@@ -80,9 +80,9 @@ class Update extends Action
string $url,
array $events,
bool $enabled,
bool $security,
string $httpUser,
string $httpPass,
bool $tls,
string $authUsername,
string $authPassword,
Response $response,
Document $project,
QueueEvent $queueForEvents,
@@ -102,9 +102,9 @@ class Update extends Action
'name' => $name,
'events' => $events,
'url' => $url,
'security' => $security,
'httpUser' => $httpUser,
'httpPass' => $httpPass,
'security' => $tls,
'httpUser' => $authUsername,
'httpPass' => $authPassword,
'enabled' => $enabled,
]);
@@ -118,6 +118,8 @@ class Update extends Action
$queueForEvents->setParam('webhookId', $webhook->getId());
$webhook->removeAttribute('signatureKey');
$response->dynamic($webhook, Response::MODEL_WEBHOOK);
}
}
@@ -78,6 +78,15 @@ class XList extends Action
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
foreach ($queries as $query) {
$attribute = $query->getAttribute();
if ($attribute === 'authUsername') {
$query->setAttribute('httpUser');
} elseif ($attribute === 'tls') {
$query->setAttribute('security');
}
}
$queries[] = Query::equal('projectInternalId', [$project->getSequence()]);
$cursor = Query::getCursorQueries($queries, false);
@@ -111,6 +120,10 @@ class XList extends Action
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
}
foreach ($webhooks as $webhook) {
$webhook->removeAttribute('signatureKey');
}
$response->dynamic(new Document([
'webhooks' => $webhooks,
'total' => $total,
@@ -6,7 +6,7 @@ use Appwrite\Platform\Modules\Webhooks\Http\Init;
use Appwrite\Platform\Modules\Webhooks\Http\Webhooks\Create as CreateWebhook;
use Appwrite\Platform\Modules\Webhooks\Http\Webhooks\Delete as DeleteWebhook;
use Appwrite\Platform\Modules\Webhooks\Http\Webhooks\Get as GetWebhook;
use Appwrite\Platform\Modules\Webhooks\Http\Webhooks\Signature\Update as UpdateWebhookSignature;
use Appwrite\Platform\Modules\Webhooks\Http\Webhooks\Secret\Update as UpdateWebhookSecret;
use Appwrite\Platform\Modules\Webhooks\Http\Webhooks\Update as UpdateWebhook;
use Appwrite\Platform\Modules\Webhooks\Http\Webhooks\XList as ListWebhooks;
use Utopia\Platform\Service;
@@ -26,6 +26,6 @@ class Http extends Service
$this->addAction(GetWebhook::getName(), new GetWebhook());
$this->addAction(DeleteWebhook::getName(), new DeleteWebhook());
$this->addAction(UpdateWebhook::getName(), new UpdateWebhook());
$this->addAction(UpdateWebhookSignature::getName(), new UpdateWebhookSignature());
$this->addAction(UpdateWebhookSecret::getName(), new UpdateWebhookSecret());
}
}
@@ -7,6 +7,8 @@ use Cron\CronExpression;
use Utopia\Console;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Span\Span;
use Utopia\System\System;
/**
* ScheduleFunctions
@@ -88,7 +90,7 @@ class ScheduleFunctions extends ScheduleBase
$scheduleKey = $delayConfig['key'];
// Ensure schedule was not deleted
if (!\array_key_exists($scheduleKey, $this->schedules)) {
return;
continue;
}
$schedule = $this->schedules[$scheduleKey];
@@ -102,8 +104,22 @@ class ScheduleFunctions extends ScheduleBase
->setFunction($schedule['resource'])
->setMethod('POST')
->setPath('/')
->setProject($schedule['project'])
->trigger();
->setProject($schedule['project']);
$projectDoc = $schedule['project'];
$functionDoc = $schedule['resource'];
$traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
$traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
if ($traceProjectId !== '' && $traceFunctionId !== '' && $projectDoc->getId() === $traceProjectId && $functionDoc->getId() === $traceFunctionId) {
Span::init('execution.trace.v1_functions_enqueue');
Span::add('datetime', gmdate('c'));
Span::add('projectId', $projectDoc->getId());
Span::add('functionId', $functionDoc->getId());
Span::add('scheduleId', $schedule['$id'] ?? '');
Span::current()?->finish();
}
$queueForFunctions->trigger();
$this->recordEnqueueDelay($delayConfig['nextDate']);
}
@@ -2,7 +2,7 @@
namespace Appwrite\Platform\Tasks;
use Appwrite\Event\StatsResources as EventStatsResources;
use Appwrite\Event\Publisher\StatsResources as StatsResourcesPublisher;
use Appwrite\Platform\Action;
use Utopia\Console;
use Utopia\Database\Database;
@@ -43,11 +43,11 @@ class StatsResources extends Action
->desc('Schedules projects for usage count')
->inject('dbForPlatform')
->inject('logError')
->inject('queueForStatsResources')
->inject('publisherForStatsResources')
->callback($this->action(...));
}
public function action(Database $dbForPlatform, callable $logError, EventStatsResources $queueForStatsResources): void
public function action(Database $dbForPlatform, callable $logError, StatsResourcesPublisher $publisherForStatsResources): void
{
$this->logError = $logError;
$this->dbForPlatform = $dbForPlatform;
@@ -60,7 +60,7 @@ class StatsResources extends Action
$interval = (int) System::getEnv('_APP_STATS_RESOURCES_INTERVAL', '3600');
Console::loop(function () use ($queueForStatsResources) {
Console::loop(function () use ($publisherForStatsResources) {
$last24Hours = (new \DateTime())->sub(\DateInterval::createFromDateString('24 hours'));
/**
@@ -69,10 +69,10 @@ class StatsResources extends Action
$this->foreachDocument($this->dbForPlatform, 'projects', [
Query::greaterThanEqual('accessedAt', DateTime::format($last24Hours)),
Query::equal('region', [System::getEnv('_APP_REGION', 'default')])
], function ($project) use ($queueForStatsResources) {
$queueForStatsResources
->setProject($project)
->trigger();
], function ($project) use ($publisherForStatsResources) {
$publisherForStatsResources->enqueue(new \Appwrite\Event\Message\StatsResources(
project: $project,
));
Console::success('project: ' . $project->getId() . '(' . $project->getSequence() . ')' . ' queued');
});
}, $interval);
+11 -3
View File
@@ -759,11 +759,19 @@ class Deletes extends Action
$databasesToClean
));
} elseif ($sharedTablesV1) {
/**
* Temporary disabling deletes for internal collections
*/
$queries = \array_map(
fn ($id) => Query::notEqual('$id', $id),
$projectCollectionIds
);
$queries[] = Query::orderAsc();
$this->deleteByGroup(
Database::METADATA,
[
Query::orderAsc()
],
$queries,
$dbForProject
);
} elseif ($sharedTablesV2) {
+19 -11
View File
@@ -2,11 +2,13 @@
namespace Appwrite\Platform\Workers;
use Appwrite\Event\Message\Execution;
use Exception;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Platform\Action;
use Utopia\Queue\Message;
use Utopia\Span\Span;
use Utopia\System\System;
class Executions extends Action
{
@@ -32,21 +34,27 @@ class Executions extends Action
Message $message,
Database $dbForProject,
): void {
$payload = $message->getPayload() ?? [];
if (empty($payload)) {
throw new Exception('Missing payload');
}
$execution = new Document($payload['execution'] ?? []);
$executionMessage = Execution::fromArray($message->getPayload() ?? []);
$execution = $executionMessage->execution;
if ($execution->isEmpty()) {
throw new Exception('Missing execution');
}
$project = new Document($payload['project'] ?? []);
if ($project->getId() != '6862e6a6000cce69f9da') {
$dbForProject->upsertDocument('executions', $execution);
$traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
$traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
$resourceId = $execution->getAttribute('resourceId', '');
if ($traceProjectId !== '' && $traceFunctionId !== '' && $executionMessage->project->getId() === $traceProjectId && $resourceId === $traceFunctionId) {
Span::init('execution.trace.executions_worker_upsert');
Span::add('datetime', gmdate('c'));
Span::add('projectId', $executionMessage->project->getId());
Span::add('functionId', $resourceId);
Span::add('executionId', $execution->getId());
Span::add('deploymentId', $execution->getAttribute('deploymentId', ''));
Span::add('resourceType', $execution->getAttribute('resourceType', ''));
Span::current()?->finish();
}
$dbForProject->upsertDocument('executions', $execution);
}
}
@@ -23,6 +23,7 @@ use Utopia\Database\Query;
use Utopia\Logger\Log;
use Utopia\Platform\Action;
use Utopia\Queue\Message;
use Utopia\Span\Span;
use Utopia\System\System;
class Functions extends Action
@@ -115,6 +116,22 @@ class Functions extends Action
$log->addTag('projectId', $project->getId());
$log->addTag('type', $type);
if (empty($events) && !$function->isEmpty()) {
$traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
$traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
if ($traceProjectId !== '' && $traceFunctionId !== '' && $project->getId() === $traceProjectId && $function->getId() === $traceFunctionId) {
Span::init('execution.trace.functions_worker_dequeue');
Span::add('datetime', gmdate('c'));
Span::add('projectId', $project->getId());
Span::add('functionId', $function->getId());
Span::add('payloadType', $type);
Span::add('queuePid', $message->getPid());
Span::add('queueName', $message->getQueue());
Span::add('messageTimestamp', (string) $message->getTimestamp());
Span::current()?->finish();
}
}
if (!empty($events)) {
$limit = 100;
$sum = 100;
@@ -304,6 +321,20 @@ class Functions extends Action
'duration' => 0.0,
]);
$traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
$traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
if ($traceProjectId !== '' && $traceFunctionId !== '' && $project->getId() === $traceProjectId && $function->getId() === $traceFunctionId) {
Span::init('execution.trace.functions_worker_before_execution_completed_bus_fail');
Span::add('datetime', gmdate('c'));
Span::add('projectId', $project->getId());
Span::add('functionId', $function->getId());
Span::add('executionId', $execution->getId());
Span::add('deploymentId', $execution->getAttribute('deploymentId', ''));
Span::add('trigger', $trigger);
Span::add('status', $execution->getAttribute('status', ''));
Span::current()?->finish();
}
$bus->dispatch(new ExecutionCompleted(
execution: $execution->getArrayCopy(),
project: $project->getArrayCopy(),
@@ -522,6 +553,18 @@ class Functions extends Action
$source = $deployment->getAttribute('buildPath', '');
$extension = str_ends_with($source, '.tar') ? 'tar' : 'tar.gz';
$command = $version === 'v2' ? '' : "cp /tmp/code.$extension /mnt/code/code.$extension && nohup helpers/start.sh \"$command\"";
$traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
$traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
if ($traceProjectId !== '' && $traceFunctionId !== '' && $project->getId() === $traceProjectId && $functionId === $traceFunctionId) {
Span::init('execution.trace.functions_worker_before_executor');
Span::add('datetime', gmdate('c'));
Span::add('projectId', $project->getId());
Span::add('functionId', $functionId);
Span::add('executionId', $executionId);
Span::add('deploymentId', $deployment->getId());
Span::add('trigger', $trigger);
Span::current()?->finish();
}
$executionResponse = $executor->createExecution(
projectId: $project->getId(),
deploymentId: $deploymentId,
@@ -594,6 +637,19 @@ class Functions extends Action
$errorCode = $th->getCode();
} finally {
/** Persist final execution status and record usage */
$traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
$traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
if ($traceProjectId !== '' && $traceFunctionId !== '' && $project->getId() === $traceProjectId && $functionId === $traceFunctionId) {
Span::init('execution.trace.functions_worker_before_execution_completed_bus');
Span::add('datetime', gmdate('c'));
Span::add('projectId', $project->getId());
Span::add('functionId', $functionId);
Span::add('executionId', $execution->getId());
Span::add('deploymentId', $execution->getAttribute('deploymentId', ''));
Span::add('status', $execution->getAttribute('status', ''));
Span::add('trigger', $trigger);
Span::current()?->finish();
}
$bus->dispatch(new ExecutionCompleted(
execution: $execution->getArrayCopy(),
project: $project->getArrayCopy(),
+4 -12
View File
@@ -4,6 +4,7 @@ namespace Appwrite\Platform\Workers;
use Ahc\Jwt\JWT;
use Appwrite\Event\Mail;
use Appwrite\Event\Message\Migration;
use Appwrite\Event\Message\Usage as UsageMessage;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Event\Realtime;
@@ -129,7 +130,7 @@ class Migrations extends Action
array $plan,
Authorization $authorization,
): void {
$payload = $message->getPayload() ?? [];
$migrationMessage = Migration::fromArray($message->getPayload() ?? []);
$this->getDatabasesDB = $getDatabasesDB;
$this->getProjectDB = $getProjectDB;
@@ -137,12 +138,7 @@ class Migrations extends Action
$this->deviceForFiles = $deviceForFiles;
$this->plan = $plan;
if (empty($payload)) {
throw new Exception('Missing payload');
}
$events = $payload['events'] ?? [];
$migration = new Document($payload['migration'] ?? []);
$migration = $migrationMessage->migration;
if ($migration->isEmpty()) {
throw new \Exception('Migration not found');
@@ -161,11 +157,7 @@ class Migrations extends Action
$this->project = $project;
$this->logError = $logError;
$platform = $payload['platform'] ?? Config::getParam('platform', []);
if (!empty($events)) {
return;
}
$platform = $migrationMessage->platform ?: Config::getParam('platform', []);
try {
$this->processMigration(
@@ -2,6 +2,7 @@
namespace Appwrite\Platform\Workers;
use Appwrite\Event\Message\StatsResources as StatsResourcesMessage;
use Appwrite\Platform\Action;
use Exception;
use Throwable;
@@ -67,8 +68,8 @@ class StatsResources extends Action
{
$this->logError = $logError;
$payload = $message->getPayload() ?? [];
if (empty($payload)) {
$statsResources = StatsResourcesMessage::fromArray($message->getPayload() ?? []);
if ($statsResources->project->isEmpty()) {
throw new Exception('Missing payload');
}
+42
View File
@@ -78,6 +78,18 @@ abstract class Format
],
];
private const array REQUEST_PARAMETER_OVERRIDES = [
[
'namespace' => 'project',
'methods' => [
'createWebPlatform',
'updateWebPlatform',
],
'parameter' => 'hostname',
'required' => true,
],
];
protected array $enumBlacklist = [];
public function __construct(Container $container, array $services, array $routes, array $models, array $keys, int $authCount, string $platform)
@@ -774,8 +786,38 @@ abstract class Format
return $values;
}
protected function getRequestParameterConfig(string $service, string $method, string $param, bool $optional, bool $nullable, mixed $default): array
{
$config = [
'required' => !$optional,
'nullable' => $nullable,
];
foreach (self::REQUEST_PARAMETER_OVERRIDES as $override) {
if (
$override['namespace'] !== $service
|| !\in_array($method, $override['methods'], true)
|| $override['parameter'] !== $param
) {
continue;
}
$config['required'] = $override['required'] ?? $config['required'];
$config['nullable'] = $override['nullable'] ?? $config['nullable'];
break;
}
$config['emitDefault'] = !$config['required'] && !\is_null($default);
return $config;
}
public function getResponseEnumName(string $model, string $param): ?string
{
if ($param === 'type' && \str_starts_with($model, 'platform') && $model !== 'platformList') {
return 'PlatformType';
}
if ($param !== 'status') {
return null;
}
@@ -381,14 +381,23 @@ class OpenAPI3 extends Format
*/
$validator = $this->getValidator($param);
$isNullable = $validator instanceof Nullable;
$parameter = $this->getRequestParameterConfig(
$sdk->getNamespace() ?? '',
$methodName,
$name,
$param['optional'],
$isNullable,
$param['default'],
);
$node = [
'name' => $name,
'description' => $param['description'],
'required' => !$param['optional'],
'required' => $parameter['required'],
];
$isNullable = $validator instanceof Nullable;
if ($isNullable) {
/** @var Nullable $validator */
$validator = $validator->getValidator();
@@ -735,7 +744,7 @@ class OpenAPI3 extends Format
break;
}
if ($param['optional'] && !\is_null($param['default'])) { // Param has default value
if ($parameter['emitDefault']) { // Param has default value
$node['schema']['default'] = $param['default'];
}
@@ -746,7 +755,7 @@ class OpenAPI3 extends Format
$node['in'] = 'query';
$temp['parameters'][] = $node;
} else { // Param is in payload
if (!$param['optional']) {
if ($node['required']) {
$bodyRequired[] = $name;
}
@@ -783,7 +792,7 @@ class OpenAPI3 extends Format
$body['content'][$consumes[0]]['schema']['properties'][$name]['x-global'] = true;
}
if ($isNullable) {
if ($parameter['nullable']) {
$body['content'][$consumes[0]]['schema']['properties'][$name]['x-nullable'] = true;
}
}
@@ -383,14 +383,23 @@ class Swagger2 extends Format
/** @var Validator $validator */
$validator = $this->getValidator($param);
$isNullable = $validator instanceof Nullable;
$parameter = $this->getRequestParameterConfig(
$sdk->getNamespace() ?? '',
$methodName,
$name,
$param['optional'],
$isNullable,
$param['default'],
);
$node = [
'name' => $name,
'description' => $param['description'],
'required' => !$param['optional'],
'required' => $parameter['required'],
];
$isNullable = $validator instanceof Nullable;
if ($isNullable) {
/** @var Nullable $validator */
$validator = $validator->getValidator();
@@ -711,7 +720,7 @@ class Swagger2 extends Format
break;
}
if ($param['optional'] && !\is_null($param['default'])) { // Param has default value
if ($parameter['emitDefault']) { // Param has default value
$node['default'] = $param['default'];
}
@@ -729,7 +738,7 @@ class Swagger2 extends Format
continue;
}
if (!$param['optional']) {
if ($node['required']) {
$bodyRequired[] = $name;
}
@@ -755,7 +764,7 @@ class Swagger2 extends Format
$body['schema']['properties'][$name]['x-global'] = true;
}
if ($isNullable) {
if ($parameter['nullable']) {
$body['schema']['properties'][$name]['x-nullable'] = true;
}
@@ -5,6 +5,28 @@ namespace Appwrite\Utopia\Database\Validator\Queries;
class Webhooks extends Base
{
public const ALLOWED_ATTRIBUTES = [
'name',
'url',
'authUsername',
'tls',
'events',
'enabled',
'logs',
'attempts',
];
/**
* Map API attribute names to DB column names.
*/
private const ATTRIBUTE_ALIASES = [
'tls' => 'security',
'authUsername' => 'httpUser',
];
/**
* DB column names used for schema validation.
*/
private const DB_ATTRIBUTES = [
'name',
'url',
'httpUser',
@@ -21,6 +43,26 @@ class Webhooks extends Base
*/
public function __construct()
{
parent::__construct('webhooks', self::ALLOWED_ATTRIBUTES);
parent::__construct('webhooks', self::DB_ATTRIBUTES);
}
/**
* Convert API attribute names to DB column names in query strings before validation.
*/
public function isValid($value): bool
{
if (\is_array($value)) {
foreach ($value as &$queryString) {
if (!\is_string($queryString)) {
continue;
}
foreach (self::ATTRIBUTE_ALIASES as $alias => $dbName) {
$queryString = \str_replace('"' . $alias . '"', '"' . $dbName . '"', $queryString);
}
}
unset($queryString);
}
return parent::isValid($value);
}
}
@@ -0,0 +1,93 @@
<?php
namespace Appwrite\Utopia\Request\Filters;
use Appwrite\Utopia\Request\Filter;
class V22 extends Filter
{
// Convert 1.9.0 params to 1.9.1
protected function parseUpdateProtocolStatus(array $content): array
{
if (isset($content['api'])) {
$content['protocolId'] = $content['api'];
unset($content['api']);
}
if (isset($content['status'])) {
$content['enabled'] = $content['status'];
unset($content['status']);
}
if (($content['protocolId'] ?? '') === 'realtime') {
$content['protocolId'] = 'websocket';
}
return $content;
}
protected function parseUpdateServiceStatus(array $content): array
{
if (isset($content['service'])) {
$content['serviceId'] = $content['service'];
unset($content['service']);
}
if (isset($content['status'])) {
$content['enabled'] = $content['status'];
unset($content['status']);
}
return $content;
}
protected function parseKeyScopes(array $content): array
{
if (!\is_array($content['scopes'] ?? null)) {
$content['scopes'] = [];
}
return $content;
}
protected function parseWebhook(array $content): array
{
if (isset($content['security'])) {
$content['tls'] = $content['security'];
unset($content['security']);
}
if (isset($content['httpUser'])) {
$content['authUsername'] = $content['httpUser'];
unset($content['httpUser']);
}
if (isset($content['httpPass'])) {
$content['authPassword'] = $content['httpPass'];
unset($content['httpPass']);
}
return $content;
}
public function parse(array $content, string $model): array
{
switch ($model) {
case 'project.updateServiceStatus':
$content = $this->parseUpdateServiceStatus($content);
break;
case 'project.updateProtocolStatus':
$content = $this->parseUpdateProtocolStatus($content);
break;
case 'project.createKey':
case 'project.updateKey':
$content = $this->parseKeyScopes($content);
break;
case 'webhooks.create':
case 'webhooks.update':
$content = $this->parseWebhook($content);
break;
}
return $content;
}
}
+11
View File
@@ -626,6 +626,17 @@ class Response extends SwooleResponse
return $this->payload;
}
/**
* Reset the sent flag so the response can be reused for another
* action execution (e.g. batched GraphQL queries that share one
* Response instance).
*/
public function clearSent(): static
{
$this->sent = false;
return $this;
}
/**
* Function to add a response filter, the order of filters are first in - first out.
*
@@ -0,0 +1,45 @@
<?php
namespace Appwrite\Utopia\Response\Filters;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Filter;
// Convert 1.9.1 Data format to 1.9.0 format
class V22 extends Filter
{
public function parse(array $content, string $model): array
{
return match ($model) {
Response::MODEL_PROJECT => $this->parseProject($content),
Response::MODEL_WEBHOOK => $this->parseWebhook($content),
Response::MODEL_WEBHOOK_LIST => $this->handleList($content, 'webhooks', fn ($item) => $this->parseWebhook($item)),
default => $content,
};
}
private function parseProject(array $content): array
{
foreach (['protocolStatusForRest', 'protocolStatusForGraphql', 'protocolStatusForWebsocket'] as $field) {
unset($content[$field]);
}
return $content;
}
private function parseWebhook(array $content): array
{
$content['security'] = $content['tls'] ?? true;
unset($content['tls']);
$content['httpUser'] = $content['authUsername'] ?? '';
unset($content['authUsername']);
$content['httpPass'] = $content['authPassword'] ?? '';
unset($content['authPassword']);
$content['signatureKey'] = $content['secret'] ?? '';
unset($content['secret']);
return $content;
}
}
@@ -344,6 +344,22 @@ class Project extends Model
])
;
}
$apis = Config::getParam('protocols', []);
foreach ($apis as $api) {
$name = $api['name'] ?? '';
$key = $api['key'] ?? '';
$this
->addRule('protocolStatusFor' . ucfirst($key), [
'type' => self::TYPE_BOOLEAN,
'description' => $name . ' protocol status',
'example' => true,
'default' => true,
])
;
}
}
/**
@@ -375,6 +391,7 @@ class Project extends Model
{
$this->expandSmtpFields($document);
$this->expandServiceFields($document);
$this->expandApiFields($document);
$this->expandAuthFields($document);
$this->expandOAuthProviders($document);
@@ -419,6 +436,22 @@ class Project extends Model
}
}
private function expandApiFields(Document $document): void
{
if (!$document->isSet('apis')) {
return;
}
$values = $document->getAttribute('apis', []);
$apis = Config::getParam('protocols', []);
foreach ($apis as $api) {
$key = $api['key'] ?? '';
$value = $values[$key] ?? true;
$document->setAttribute('protocolStatusFor' . ucfirst($key), $value);
}
}
private function expandAuthFields(Document $document): void
{
if (!$document->isSet('auths')) {
+2 -1
View File
@@ -66,8 +66,9 @@ class Rule extends Model
])
->addRule('deploymentResourceType', [
'type' => self::TYPE_ENUM,
'required' => false,
'description' => 'Type of deployment. Possible values are "function", "site". Used if rule\'s type is "deployment".',
'default' => '',
'default' => null,
'example' => 'function',
'enum' => ['function', 'site'],
])
+24 -6
View File
@@ -4,6 +4,7 @@ namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
use Utopia\Database\Document;
class Webhook extends Model
{
@@ -50,27 +51,27 @@ class Webhook extends Model
],
'array' => true,
])
->addRule('security', [
->addRule('tls', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Indicated if SSL / TLS Certificate verification is enabled.',
'description' => 'Indicates if SSL / TLS certificate verification is enabled.',
'default' => true,
'example' => true,
])
->addRule('httpUser', [
->addRule('authUsername', [
'type' => self::TYPE_STRING,
'description' => 'HTTP basic authentication username.',
'default' => '',
'example' => 'username',
])
->addRule('httpPass', [
->addRule('authPassword', [
'type' => self::TYPE_STRING,
'description' => 'HTTP basic authentication password.',
'default' => '',
'example' => 'password',
])
->addRule('signatureKey', [
->addRule('secret', [
'type' => self::TYPE_STRING,
'description' => 'Signature key which can be used to validated incoming',
'description' => 'Signature key which can be used to validate incoming webhook payloads. Only returned on creation and secret rotation.',
'default' => '',
'example' => 'ad3d581ca230e2b7059c545e5a',
])
@@ -94,6 +95,23 @@ class Webhook extends Model
]);
}
public function filter(Document $document): Document
{
$document->setAttribute('tls', $document->getAttribute('security'));
$document->removeAttribute('security');
$document->setAttribute('authUsername', $document->getAttribute('httpUser'));
$document->removeAttribute('httpUser');
$document->setAttribute('authPassword', $document->getAttribute('httpPass'));
$document->removeAttribute('httpPass');
$document->setAttribute('secret', $document->getAttribute('signatureKey'));
$document->removeAttribute('signatureKey');
return $document;
}
/**
* Get Name
*
+1 -1
View File
@@ -22,7 +22,7 @@ class Comment
'Every Git commit and branch gets its own deployment URL automatically',
'Custom domains work with both CNAME for subdomains and NS records for apex domains',
'HTTPS and SSL certificates are handled automatically for all your Sites',
'Functions can run for up to 15 minutes before timing out',
'Function builds can take up to 45 minutes before timing out',
'Schedule functions to run as often as every minute with cron expressions',
'Environment variables can be scoped per function or shared across your project',
'Function scopes give you fine-grained control over API permissions',
+2 -2
View File
@@ -216,7 +216,7 @@ trait ProjectCustom
'users.*'
],
'url' => 'http://request-catcher-webhook:5000/',
'security' => false,
'tls' => false,
]);
$this->assertEquals(201, $webhook['headers']['status-code']);
@@ -243,7 +243,7 @@ trait ProjectCustom
'apiKey' => $key['body']['secret'],
'devKey' => $devKey['body']['secret'],
'webhookId' => $webhook['body']['$id'],
'signatureKey' => $webhook['body']['signatureKey'],
'signatureKey' => $webhook['body']['secret'],
];
}
@@ -11539,4 +11539,162 @@ trait DatabasesBase
$this->assertEquals('Product B', $rows['body'][$this->getRecordResource()][0]['name']);
$this->assertEquals(139.99, $rows['body'][$this->getRecordResource()][0]['price']);
}
public function testDocumentWithEmptyPayload(): void
{
$data = $this->setupCollection();
$databaseId = $data['databaseId'];
$document = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $data['moviesId']), array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
$this->getRecordIdParam() => ID::unique(),
'data' => [],
'permissions' => [
Permission::read(Role::user($this->getUser()['$id'])),
Permission::update(Role::user($this->getUser()['$id'])),
Permission::delete(Role::user($this->getUser()['$id'])),
]
]);
if ($this->getSupportForAttributes()) {
$this->assertEquals(400, $document['headers']['status-code']);
} else {
$this->assertEquals(201, $document['headers']['status-code']);
$this->assertEquals($data['moviesId'], $document['body'][$this->getContainerIdResponseKey()]);
$this->assertArrayNotHasKey('$collection', $document['body']);
$this->assertEquals($databaseId, $document['body']['$databaseId']);
$this->assertTrue(array_key_exists('$sequence', $document['body']));
$this->assertIsString($document['body']['$sequence']);
$documentId = $document['body']['$id'];
$fetched = $this->client->call(
Client::METHOD_GET,
$this->getRecordUrl($databaseId, $data['moviesId'], $documentId),
array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders())
);
$this->assertEquals(200, $fetched['headers']['status-code']);
$this->assertEqualsCanonicalizing([
'$id',
'$databaseId',
'$createdAt',
'$updatedAt',
'$permissions',
'$sequence',
$this->getContainerIdResponseKey(),
], \array_keys($fetched['body']));
$this->assertFalse(array_key_exists('$tenant', $fetched['body']));
$updated = $this->client->call(
Client::METHOD_PATCH,
$this->getRecordUrl($databaseId, $data['moviesId'], $documentId),
array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()),
[
'data' => [
'status' => 'draft',
],
]
);
$this->assertEquals(200, $updated['headers']['status-code']);
$this->assertEquals('draft', $updated['body']['status']);
$refetched = $this->client->call(
Client::METHOD_GET,
$this->getRecordUrl($databaseId, $data['moviesId'], $documentId),
array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders())
);
$this->assertEquals(200, $refetched['headers']['status-code']);
$this->assertEquals('draft', $refetched['body']['status']);
}
}
/**
* API keys may set $createdAt / $updatedAt; invalid strings must return 400, not 500.
* Assertions are HTTP status codes only (no error body matching).
*/
public function testInvalidDate(): void
{
$data = $this->setupAttributes();
$databaseId = $data['databaseId'];
$invalidDatetime = '1dfs:12:55+sdf:00';
$validUpdatedAt = '2024-01-01T00:00:00Z';
$apiKeyHeaders = [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
];
$documentPayload = [
'title' => 'Captain America',
'releaseYear' => 1944,
'actors' => [
'Chris Evans',
'Samuel Jackson',
],
];
$permissions = [
Permission::read(Role::user($this->getUser()['$id'])),
Permission::update(Role::user($this->getUser()['$id'])),
Permission::delete(Role::user($this->getUser()['$id'])),
];
$invalidCreate = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $data['moviesId']), $apiKeyHeaders, [
$this->getRecordIdParam() => ID::unique(),
'data' => \array_merge($documentPayload, ['$updatedAt' => $invalidDatetime]),
'permissions' => $permissions,
]);
$this->assertEquals(400, $invalidCreate['headers']['status-code']);
$document = $this->client->call(Client::METHOD_POST, $this->getRecordUrl($databaseId, $data['moviesId']), $apiKeyHeaders, [
$this->getRecordIdParam() => ID::unique(),
'data' => $documentPayload,
'permissions' => $permissions,
]);
$this->assertEquals(201, $document['headers']['status-code']);
$documentId = $document['body']['$id'];
$this->assertNotEmpty($documentId);
$invalidPatch = $this->client->call(
Client::METHOD_PATCH,
$this->getRecordUrl($databaseId, $data['moviesId'], $documentId),
$apiKeyHeaders,
[
'data' => [
'$updatedAt' => $invalidDatetime,
],
]
);
$this->assertEquals(400, $invalidPatch['headers']['status-code']);
$updated = $this->client->call(
Client::METHOD_PATCH,
$this->getRecordUrl($databaseId, $data['moviesId'], $documentId),
$apiKeyHeaders,
[
'data' => [
'$updatedAt' => $validUpdatedAt,
],
]
);
$this->assertEquals(200, $updated['headers']['status-code']);
$refetched = $this->client->call(
Client::METHOD_GET,
$this->getRecordUrl($databaseId, $data['moviesId'], $documentId),
$apiKeyHeaders
);
$this->assertEquals(200, $refetched['headers']['status-code']);
}
}
+55 -3
View File
@@ -78,12 +78,12 @@ trait KeysBase
$this->deleteKey($key['body']['$id']);
}
public function testCreateKeyWithNullScopes(): void
public function testCreateKeyWithEmptyScopes(): void
{
$key = $this->createKey(
ID::unique(),
'Null Scopes Key',
null,
'Empty Scopes Key',
[],
);
$this->assertSame(201, $key['headers']['status-code']);
@@ -93,6 +93,58 @@ trait KeysBase
$this->deleteKey($key['body']['$id']);
}
public function testCreateKeyWithNullScopesV22BackwardCompat(): void
{
$headers = [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.9.0',
];
$headers = array_merge($headers, $this->getHeaders());
$key = $this->client->call(Client::METHOD_POST, '/project/keys', $headers, [
'keyId' => ID::unique(),
'name' => 'V22 Compat Key',
'scopes' => null,
]);
$this->assertSame(201, $key['headers']['status-code']);
$this->assertSame([], $key['body']['scopes']);
// Cleanup
$this->deleteKey($key['body']['$id']);
}
public function testUpdateKeyWithNullScopesV22BackwardCompat(): void
{
$key = $this->createKey(
ID::unique(),
'V22 Update Compat Key',
['users.read'],
);
$this->assertSame(201, $key['headers']['status-code']);
$keyId = $key['body']['$id'];
$headers = [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.9.0',
];
$headers = array_merge($headers, $this->getHeaders());
$updated = $this->client->call(Client::METHOD_PUT, '/project/keys/' . $keyId, $headers, [
'name' => 'V22 Update Compat Key',
'scopes' => null,
]);
$this->assertSame(200, $updated['headers']['status-code']);
$this->assertSame([], $updated['body']['scopes']);
// Cleanup
$this->deleteKey($keyId);
}
public function testCreateKeyWithoutAuthentication(): void
{
$response = $this->createKey(
@@ -0,0 +1,261 @@
<?php
namespace Tests\E2E\Services\Project;
use Tests\E2E\Client;
trait ProtocolsBase
{
protected static array $protocols = ['rest', 'graphql', 'websocket'];
// Success flow
public function testDisableProtocol(): void
{
foreach (self::$protocols as $protocol) {
$response = $this->updateProtocolStatus($protocol, false);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertSame(false, $response['body']['protocolStatusFor' . ucfirst($protocol)]);
}
// Cleanup
foreach (self::$protocols as $protocol) {
$this->updateProtocolStatus($protocol, true);
}
}
public function testEnableProtocol(): void
{
// Disable first
foreach (self::$protocols as $protocol) {
$this->updateProtocolStatus($protocol, false);
}
// Re-enable
foreach (self::$protocols as $protocol) {
$response = $this->updateProtocolStatus($protocol, true);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertSame(true, $response['body']['protocolStatusFor' . ucfirst($protocol)]);
}
}
public function testDisableProtocolIdempotent(): void
{
$first = $this->updateProtocolStatus('rest', false);
$this->assertSame(200, $first['headers']['status-code']);
$this->assertSame(false, $first['body']['protocolStatusForRest']);
$second = $this->updateProtocolStatus('rest', false);
$this->assertSame(200, $second['headers']['status-code']);
$this->assertSame(false, $second['body']['protocolStatusForRest']);
// Cleanup
$this->updateProtocolStatus('rest', true);
}
public function testEnableProtocolIdempotent(): void
{
$first = $this->updateProtocolStatus('rest', true);
$this->assertSame(200, $first['headers']['status-code']);
$this->assertSame(true, $first['body']['protocolStatusForRest']);
$second = $this->updateProtocolStatus('rest', true);
$this->assertSame(200, $second['headers']['status-code']);
$this->assertSame(true, $second['body']['protocolStatusForRest']);
}
public function testDisabledRestBlocksClientRequest(): void
{
$this->updateProtocolStatus('rest', false);
$response = $this->client->call(Client::METHOD_GET, '/locale/countries', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]);
$this->assertSame(403, $response['headers']['status-code']);
$this->assertSame('general_api_disabled', $response['body']['type']);
// Cleanup
$this->updateProtocolStatus('rest', true);
}
public function testEnabledRestAllowsClientRequest(): void
{
$this->updateProtocolStatus('rest', false);
$this->updateProtocolStatus('rest', true);
$response = $this->client->call(Client::METHOD_GET, '/locale/countries', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]);
$this->assertSame(200, $response['headers']['status-code']);
}
public function testDisabledGraphqlBlocksClientRequest(): void
{
$this->updateProtocolStatus('graphql', false);
$response = $this->client->call(Client::METHOD_POST, '/graphql', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], [
'query' => '{ localeListCountries { total } }',
]);
$this->assertSame(403, $response['headers']['status-code']);
$this->assertSame('general_api_disabled', $response['body']['type']);
// Cleanup
$this->updateProtocolStatus('graphql', true);
}
public function testDisableOneProtocolDoesNotAffectOther(): void
{
$this->updateProtocolStatus('graphql', false);
// REST should still work
$response = $this->client->call(Client::METHOD_GET, '/locale/countries', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]);
$this->assertSame(200, $response['headers']['status-code']);
// Cleanup
$this->updateProtocolStatus('graphql', true);
}
public function testDisabledRestBlocksAllServiceEndpoints(): void
{
$endpoints = [
'account' => '/account',
'teams' => '/teams',
'databases' => '/databases',
'storage' => '/storage/buckets',
'functions' => '/functions',
'sites' => '/sites',
'locale' => '/locale',
'health' => '/health',
'users' => '/users',
'messaging' => '/messaging/providers',
'migrations' => '/migrations',
];
$this->updateProtocolStatus('rest', false);
foreach ($endpoints as $service => $path) {
$response = $this->client->call(Client::METHOD_GET, $path, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]);
$this->assertSame(403, $response['headers']['status-code'], 'Disabled REST protocol should block ' . $service . ' endpoint (got ' . $response['headers']['status-code'] . ')');
$this->assertSame('general_api_disabled', $response['body']['type'], 'Disabled REST protocol should return general_api_disabled for ' . $service);
}
// Cleanup
$this->updateProtocolStatus('rest', true);
}
public function testReenabledRestAllowsAllServiceEndpoints(): void
{
$endpoints = [
'teams' => '/teams',
'databases' => '/databases',
'functions' => '/functions',
'locale' => '/locale',
];
$this->updateProtocolStatus('rest', false);
$this->updateProtocolStatus('rest', true);
foreach ($endpoints as $service => $path) {
$response = $this->client->call(Client::METHOD_GET, $path, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertNotEquals(403, $response['headers']['status-code'], 'Re-enabled REST protocol should not block ' . $service . ' endpoint');
}
}
public function testDisabledGraphqlBlocksMutationRequest(): void
{
$this->updateProtocolStatus('graphql', false);
$response = $this->client->call(Client::METHOD_POST, '/graphql', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], [
'query' => 'mutation { teamsCreate(teamId: "unique()", name: "Test") { _id } }',
]);
$this->assertSame(403, $response['headers']['status-code']);
$this->assertSame('general_api_disabled', $response['body']['type']);
// Cleanup
$this->updateProtocolStatus('graphql', true);
}
public function testResponseModel(): void
{
$response = $this->updateProtocolStatus('rest', false);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertArrayHasKey('$id', $response['body']);
$this->assertArrayHasKey('name', $response['body']);
$this->assertArrayHasKey('protocolStatusForRest', $response['body']);
$this->assertArrayHasKey('protocolStatusForGraphql', $response['body']);
$this->assertArrayHasKey('protocolStatusForWebsocket', $response['body']);
// Cleanup
$this->updateProtocolStatus('rest', true);
}
// Failure flow
public function testUpdateProtocolWithoutAuthentication(): void
{
$response = $this->updateProtocolStatus('rest', false, false);
$this->assertSame(401, $response['headers']['status-code']);
}
public function testUpdateProtocolInvalidProtocolId(): void
{
$response = $this->updateProtocolStatus('invalid', false);
$this->assertSame(400, $response['headers']['status-code']);
}
public function testUpdateProtocolEmptyProtocolId(): void
{
$response = $this->updateProtocolStatus('', false);
$this->assertSame(404, $response['headers']['status-code']);
}
// Helpers
protected function updateProtocolStatus(string $protocolId, bool $enabled, bool $authenticated = true): mixed
{
$headers = [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
];
if ($authenticated) {
$headers = array_merge($headers, $this->getHeaders());
}
return $this->client->call(Client::METHOD_PATCH, '/project/protocols/' . $protocolId . '/status', $headers, [
'enabled' => $enabled,
]);
}
}
@@ -0,0 +1,14 @@
<?php
namespace Tests\E2E\Services\Project;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideConsole;
class ProtocolsConsoleClientTest extends Scope
{
use ProtocolsBase;
use ProjectCustom;
use SideConsole;
}
@@ -0,0 +1,14 @@
<?php
namespace Tests\E2E\Services\Project;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideServer;
class ProtocolsCustomServerTest extends Scope
{
use ProtocolsBase;
use ProjectCustom;
use SideServer;
}
+259
View File
@@ -0,0 +1,259 @@
<?php
namespace Tests\E2E\Services\Project;
use Tests\E2E\Client;
trait ServicesBase
{
/**
* Optional services that can be toggled.
*/
protected static array $optionalServices = [
'account',
'avatars',
'databases',
'tablesdb',
'locale',
'health',
'project',
'storage',
'teams',
'users',
'vcs',
'sites',
'functions',
'proxy',
'migrations',
'messaging',
];
// Success flow
public function testDisableService(): void
{
foreach (self::$optionalServices as $service) {
$response = $this->updateServiceStatus($service, false);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertSame(false, $response['body']['serviceStatusFor' . ucfirst($service)]);
}
// Cleanup
foreach (self::$optionalServices as $service) {
$this->updateServiceStatus($service, true);
}
}
public function testEnableService(): void
{
// Disable first
foreach (self::$optionalServices as $service) {
$this->updateServiceStatus($service, false);
}
// Re-enable
foreach (self::$optionalServices as $service) {
$response = $this->updateServiceStatus($service, true);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertSame(true, $response['body']['serviceStatusFor' . ucfirst($service)]);
}
}
public function testDisableServiceIdempotent(): void
{
$first = $this->updateServiceStatus('teams', false);
$this->assertSame(200, $first['headers']['status-code']);
$this->assertSame(false, $first['body']['serviceStatusForTeams']);
$second = $this->updateServiceStatus('teams', false);
$this->assertSame(200, $second['headers']['status-code']);
$this->assertSame(false, $second['body']['serviceStatusForTeams']);
// Cleanup
$this->updateServiceStatus('teams', true);
}
public function testEnableServiceIdempotent(): void
{
$first = $this->updateServiceStatus('teams', true);
$this->assertSame(200, $first['headers']['status-code']);
$this->assertSame(true, $first['body']['serviceStatusForTeams']);
$second = $this->updateServiceStatus('teams', true);
$this->assertSame(200, $second['headers']['status-code']);
$this->assertSame(true, $second['body']['serviceStatusForTeams']);
}
public function testDisabledServiceBlocksClientRequest(): void
{
$this->updateServiceStatus('teams', false);
$response = $this->client->call(Client::METHOD_GET, '/teams', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]);
$this->assertSame(403, $response['headers']['status-code']);
$this->assertSame('general_service_disabled', $response['body']['type']);
// Cleanup
$this->updateServiceStatus('teams', true);
}
public function testEnabledServiceAllowsClientRequest(): void
{
$this->updateServiceStatus('teams', false);
$this->updateServiceStatus('teams', true);
$response = $this->client->call(Client::METHOD_GET, '/teams', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertSame(200, $response['headers']['status-code']);
}
public function testDisableOneServiceDoesNotAffectOther(): void
{
$this->updateServiceStatus('teams', false);
$response = $this->client->call(Client::METHOD_GET, '/functions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertSame(200, $response['headers']['status-code']);
// Cleanup
$this->updateServiceStatus('teams', true);
}
public function testEachDisabledServiceBlocksItsEndpoint(): void
{
$serviceEndpoints = [
'account' => ['method' => Client::METHOD_GET, 'path' => '/account'],
'avatars' => ['method' => Client::METHOD_GET, 'path' => '/avatars/initials'],
'databases' => ['method' => Client::METHOD_GET, 'path' => '/databases'],
'tablesdb' => ['method' => Client::METHOD_GET, 'path' => '/tablesdb'],
'locale' => ['method' => Client::METHOD_GET, 'path' => '/locale'],
'health' => ['method' => Client::METHOD_GET, 'path' => '/health'],
'project' => ['method' => Client::METHOD_GET, 'path' => '/project/platforms'],
'storage' => ['method' => Client::METHOD_GET, 'path' => '/storage/buckets'],
'teams' => ['method' => Client::METHOD_GET, 'path' => '/teams'],
'users' => ['method' => Client::METHOD_GET, 'path' => '/users'],
'vcs' => ['method' => Client::METHOD_GET, 'path' => '/vcs/installations'],
'sites' => ['method' => Client::METHOD_GET, 'path' => '/sites'],
'functions' => ['method' => Client::METHOD_GET, 'path' => '/functions'],
'proxy' => ['method' => Client::METHOD_GET, 'path' => '/proxy/rules'],
'migrations' => ['method' => Client::METHOD_GET, 'path' => '/migrations'],
'messaging' => ['method' => Client::METHOD_GET, 'path' => '/messaging/providers'],
];
foreach ($serviceEndpoints as $service => $endpoint) {
$this->updateServiceStatus($service, false);
$response = $this->client->call($endpoint['method'], $endpoint['path'], [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]);
$this->assertSame(403, $response['headers']['status-code'], 'Service ' . $service . ' should block requests when disabled (got ' . $response['headers']['status-code'] . ')');
$this->assertSame('general_service_disabled', $response['body']['type'], 'Service ' . $service . ' should return general_service_disabled error type');
// Cleanup
$this->updateServiceStatus($service, true);
}
}
public function testEachReenabledServiceAllowsRequest(): void
{
$serviceEndpoints = [
'account' => ['method' => Client::METHOD_GET, 'path' => '/account'],
'avatars' => ['method' => Client::METHOD_GET, 'path' => '/avatars/initials'],
'databases' => ['method' => Client::METHOD_GET, 'path' => '/databases'],
'tablesdb' => ['method' => Client::METHOD_GET, 'path' => '/tablesdb'],
'locale' => ['method' => Client::METHOD_GET, 'path' => '/locale'],
'health' => ['method' => Client::METHOD_GET, 'path' => '/health'],
'project' => ['method' => Client::METHOD_GET, 'path' => '/project/platforms'],
'storage' => ['method' => Client::METHOD_GET, 'path' => '/storage/buckets'],
'teams' => ['method' => Client::METHOD_GET, 'path' => '/teams'],
'users' => ['method' => Client::METHOD_GET, 'path' => '/users'],
'vcs' => ['method' => Client::METHOD_GET, 'path' => '/vcs/installations'],
'sites' => ['method' => Client::METHOD_GET, 'path' => '/sites'],
'functions' => ['method' => Client::METHOD_GET, 'path' => '/functions'],
'proxy' => ['method' => Client::METHOD_GET, 'path' => '/proxy/rules'],
'migrations' => ['method' => Client::METHOD_GET, 'path' => '/migrations'],
'messaging' => ['method' => Client::METHOD_GET, 'path' => '/messaging/providers'],
];
foreach ($serviceEndpoints as $service => $endpoint) {
$this->updateServiceStatus($service, false);
$this->updateServiceStatus($service, true);
$response = $this->client->call($endpoint['method'], $endpoint['path'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertNotEquals(403, $response['headers']['status-code'], 'Service ' . $service . ' should allow requests after re-enabling');
}
}
public function testResponseModel(): void
{
$response = $this->updateServiceStatus('teams', false);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertArrayHasKey('$id', $response['body']);
$this->assertArrayHasKey('name', $response['body']);
$this->assertArrayHasKey('serviceStatusForTeams', $response['body']);
// Cleanup
$this->updateServiceStatus('teams', true);
}
// Failure flow
public function testUpdateServiceWithoutAuthentication(): void
{
$response = $this->updateServiceStatus('teams', false, false);
$this->assertSame(401, $response['headers']['status-code']);
}
public function testUpdateServiceInvalidServiceId(): void
{
$response = $this->updateServiceStatus('invalid', false);
$this->assertSame(400, $response['headers']['status-code']);
}
public function testUpdateServiceEmptyServiceId(): void
{
$response = $this->updateServiceStatus('', false);
$this->assertSame(404, $response['headers']['status-code']);
}
// Helpers
protected function updateServiceStatus(string $serviceId, bool $enabled, bool $authenticated = true): mixed
{
$headers = [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
];
if ($authenticated) {
$headers = array_merge($headers, $this->getHeaders());
}
return $this->client->call(Client::METHOD_PATCH, '/project/services/' . $serviceId . '/status', $headers, [
'enabled' => $enabled,
]);
}
}
@@ -0,0 +1,14 @@
<?php
namespace Tests\E2E\Services\Project;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideConsole;
class ServicesConsoleClientTest extends Scope
{
use ServicesBase;
use ProjectCustom;
use SideConsole;
}
@@ -0,0 +1,14 @@
<?php
namespace Tests\E2E\Services\Project;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideServer;
class ServicesCustomServerTest extends Scope
{
use ServicesBase;
use ProjectCustom;
use SideServer;
}
+5 -4
View File
@@ -90,16 +90,16 @@ trait ProjectsBase
'name' => 'Webhook Test',
'events' => ['users.*.create', 'users.*.update.email'],
'url' => 'https://appwrite.io',
'security' => true,
'httpUser' => 'username',
'httpPass' => 'password',
'tls' => true,
'authUsername' => 'username',
'authPassword' => 'password',
]);
$this->assertEquals(201, $response['headers']['status-code']);
self::$cachedProjectWithWebhook = array_merge($projectData, [
'webhookId' => $response['body']['$id'],
'signatureKey' => $response['body']['signatureKey']
'signatureKey' => $response['body']['secret']
]);
return self::$cachedProjectWithWebhook;
@@ -388,6 +388,7 @@ trait ProjectsBase
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/service', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.9.0',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'service' => $key,
@@ -2497,49 +2497,222 @@ class ProjectsConsoleClientTest extends Scope
$id = $project['body']['$id'];
// Bulk disable should no longer work
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/service/all', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.9.0',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'status' => false,
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$matches = [];
$pattern = '/serviceStatusFor.*/';
foreach ($response['body'] as $key => $value) {
if (\preg_match($pattern, $key)) {
$matches[$key] = $value;
}
}
foreach ($matches as $value) {
$this->assertFalse($value);
}
$this->assertEquals(405, $response['headers']['status-code']);
$this->assertEquals('general_not_implemented', $response['body']['type']);
// Bulk enable should no longer work
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/service/all', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.9.0',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'status' => true,
]);
$this->assertEquals(405, $response['headers']['status-code']);
$this->assertEquals('general_not_implemented', $response['body']['type']);
}
public function testUpdateProjectApisAll(): void
{
$team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'teamId' => ID::unique(),
'name' => 'Project Test',
]);
$this->assertEquals(201, $team['headers']['status-code']);
$this->assertNotEmpty($team['body']['$id']);
$project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'projectId' => ID::unique(),
'name' => 'Project Test',
'teamId' => $team['body']['$id'],
'region' => System::getEnv('_APP_REGION', 'default')
]);
$this->assertEquals(201, $project['headers']['status-code']);
$this->assertNotEmpty($project['body']['$id']);
$id = $project['body']['$id'];
// Bulk disable should no longer work
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/api/all', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.9.0',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'status' => false,
]);
$this->assertEquals(405, $response['headers']['status-code']);
$this->assertEquals('general_not_implemented', $response['body']['type']);
// Bulk enable should no longer work
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/api/all', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.9.0',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'status' => true,
]);
$this->assertEquals(405, $response['headers']['status-code']);
$this->assertEquals('general_not_implemented', $response['body']['type']);
}
public function testUpdateProjectApiStatus(): void
{
$team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'teamId' => ID::unique(),
'name' => 'Project Test',
]);
$this->assertEquals(201, $team['headers']['status-code']);
$this->assertNotEmpty($team['body']['$id']);
$project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'projectId' => ID::unique(),
'name' => 'Project Test',
'teamId' => $team['body']['$id'],
'region' => System::getEnv('_APP_REGION', 'default')
]);
$this->assertEquals(201, $project['headers']['status-code']);
$this->assertNotEmpty($project['body']['$id']);
$id = $project['body']['$id'];
$protocols = ['rest', 'graphql', 'websocket'];
/**
* Test for Disabled using old format (api + status)
*/
foreach ($protocols as $key) {
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/api', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.9.0',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'api' => $key,
'status' => false,
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertEquals(false, $response['body']['protocolStatusFor' . ucfirst($key)]);
}
/**
* Test for Enabled using old format (api + status)
*/
foreach ($protocols as $key) {
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/api', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.9.0',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'api' => $key,
'status' => true,
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertEquals(true, $response['body']['protocolStatusFor' . ucfirst($key)]);
}
}
public function testUpdateProjectApiStatusRealtimeBackwardsCompat(): void
{
$team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'teamId' => ID::unique(),
'name' => 'Project Test',
]);
$this->assertEquals(201, $team['headers']['status-code']);
$project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'projectId' => ID::unique(),
'name' => 'Project Test',
'teamId' => $team['body']['$id'],
'region' => System::getEnv('_APP_REGION', 'default')
]);
$this->assertEquals(201, $project['headers']['status-code']);
$id = $project['body']['$id'];
/**
* Test that "realtime" gets renamed to "websocket" via request filter
*/
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/api', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.9.0',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'api' => 'realtime',
'status' => false,
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
@@ -2548,17 +2721,29 @@ class ProjectsConsoleClientTest extends Scope
]));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(false, $response['body']['protocolStatusForWebsocket']);
$matches = [];
foreach ($response['body'] as $key => $value) {
if (\preg_match($pattern, $key)) {
$matches[$key] = $value;
}
}
// Re-enable via old "realtime" name
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/api', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.9.0',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'api' => 'realtime',
'status' => true,
]);
foreach ($matches as $value) {
$this->assertTrue($value);
}
$this->assertEquals(200, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(true, $response['body']['protocolStatusForWebsocket']);
}
public function testUpdateProjectServiceStatusAdmin(): array
@@ -2604,6 +2789,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/service', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.9.0',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'service' => $key,
@@ -2649,6 +2835,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/service/', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.9.0',
], $this->getHeaders()), [
'service' => $key,
'status' => true,
@@ -2678,6 +2865,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/service', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.9.0',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'service' => $key,
@@ -2725,6 +2913,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/service/', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.9.0',
], $this->getHeaders()), [
'service' => $service,
'status' => true,
@@ -2752,6 +2941,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/service', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.9.0',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'service' => $key,
@@ -2841,6 +3031,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/service/', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.9.0',
], $this->getHeaders()), [
'service' => $service,
'status' => true,
@@ -2862,9 +3053,9 @@ class ProjectsConsoleClientTest extends Scope
'name' => 'Webhook Test',
'events' => ['users.*.create', 'users.*.update.email'],
'url' => 'https://appwrite.io',
'security' => true,
'httpUser' => 'username',
'httpPass' => 'password',
'tls' => true,
'authUsername' => 'username',
'authPassword' => 'password',
]);
$this->assertEquals(201, $response['headers']['status-code']);
@@ -2873,9 +3064,9 @@ class ProjectsConsoleClientTest extends Scope
$this->assertContains('users.*.update.email', $response['body']['events']);
$this->assertCount(2, $response['body']['events']);
$this->assertEquals('https://appwrite.io', $response['body']['url']);
$this->assertIsBool($response['body']['security']);
$this->assertEquals(true, $response['body']['security']);
$this->assertEquals('username', $response['body']['httpUser']);
$this->assertIsBool($response['body']['tls']);
$this->assertEquals(true, $response['body']['tls']);
$this->assertEquals('username', $response['body']['authUsername']);
/**
* Test for FAILURE
@@ -2889,9 +3080,9 @@ class ProjectsConsoleClientTest extends Scope
'name' => 'Webhook Test',
'events' => ['account.unknown', 'users.*.update.email'],
'url' => 'https://appwrite.io',
'security' => true,
'httpUser' => 'username',
'httpPass' => 'password',
'tls' => true,
'authUsername' => 'username',
'authPassword' => 'password',
]);
$this->assertEquals(400, $response['headers']['status-code']);
@@ -2949,8 +3140,8 @@ class ProjectsConsoleClientTest extends Scope
$this->assertContains('users.*.update.email', $response['body']['events']);
$this->assertCount(2, $response['body']['events']);
$this->assertEquals('https://appwrite.io', $response['body']['url']);
$this->assertEquals('username', $response['body']['httpUser']);
$this->assertEquals('password', $response['body']['httpPass']);
$this->assertEquals('username', $response['body']['authUsername']);
$this->assertEquals('password', $response['body']['authPassword']);
/**
* Test for FAILURE
@@ -2978,7 +3169,7 @@ class ProjectsConsoleClientTest extends Scope
'name' => 'Webhook Test Update',
'events' => ['users.*.delete', 'users.*.sessions.*.delete', 'buckets.*.files.*.create'],
'url' => 'https://appwrite.io/new',
'security' => false,
'tls' => false,
]);
$this->assertEquals(200, $response['headers']['status-code']);
@@ -2990,10 +3181,10 @@ class ProjectsConsoleClientTest extends Scope
$this->assertContains('buckets.*.files.*.create', $response['body']['events']);
$this->assertCount(3, $response['body']['events']);
$this->assertEquals('https://appwrite.io/new', $response['body']['url']);
$this->assertIsBool($response['body']['security']);
$this->assertEquals(false, $response['body']['security']);
$this->assertEquals('', $response['body']['httpUser']);
$this->assertEquals('', $response['body']['httpPass']);
$this->assertIsBool($response['body']['tls']);
$this->assertEquals(false, $response['body']['tls']);
$this->assertEquals('', $response['body']['authUsername']);
$this->assertEquals('', $response['body']['authPassword']);
$response = $this->client->call(Client::METHOD_GET, '/webhooks/' . $webhookId, array_merge([
'content-type' => 'application/json',
@@ -3010,10 +3201,10 @@ class ProjectsConsoleClientTest extends Scope
$this->assertContains('buckets.*.files.*.create', $response['body']['events']);
$this->assertCount(3, $response['body']['events']);
$this->assertEquals('https://appwrite.io/new', $response['body']['url']);
$this->assertIsBool($response['body']['security']);
$this->assertEquals(false, $response['body']['security']);
$this->assertEquals('', $response['body']['httpUser']);
$this->assertEquals('', $response['body']['httpPass']);
$this->assertIsBool($response['body']['tls']);
$this->assertEquals(false, $response['body']['tls']);
$this->assertEquals('', $response['body']['authUsername']);
$this->assertEquals('', $response['body']['authPassword']);
/**
* Test for FAILURE
@@ -3026,7 +3217,7 @@ class ProjectsConsoleClientTest extends Scope
'name' => 'Webhook Test Update',
'events' => ['users.*.delete', 'users.*.sessions.*.delete', 'buckets.*.files.*.unknown'],
'url' => 'https://appwrite.io/new',
'security' => false,
'tls' => false,
]);
$this->assertEquals(400, $response['headers']['status-code']);
@@ -3039,7 +3230,7 @@ class ProjectsConsoleClientTest extends Scope
'name' => 'Webhook Test Update',
'events' => ['users.*.delete', 'users.*.sessions.*.delete', 'buckets.*.files.*.create'],
'url' => 'appwrite.io/new',
'security' => false,
'tls' => false,
]);
$this->assertEquals(400, $response['headers']['status-code']);
@@ -3064,15 +3255,15 @@ class ProjectsConsoleClientTest extends Scope
$webhookId = $data['webhookId'];
$signatureKey = $data['signatureKey'];
$response = $this->client->call(Client::METHOD_PATCH, '/webhooks/' . $webhookId . '/signature', array_merge([
$response = $this->client->call(Client::METHOD_PATCH, '/webhooks/' . $webhookId . '/secret', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $id,
'x-appwrite-mode' => 'admin'
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['signatureKey']);
$this->assertNotEquals($signatureKey, $response['body']['signatureKey']);
$this->assertNotEmpty($response['body']['secret']);
$this->assertNotEquals($signatureKey, $response['body']['secret']);
}
public function testDeleteProjectWebhook(): void
@@ -3091,9 +3282,9 @@ class ProjectsConsoleClientTest extends Scope
'name' => 'Webhook To Delete',
'events' => ['users.*.create'],
'url' => 'https://appwrite.io',
'security' => true,
'httpUser' => 'username',
'httpPass' => 'password',
'tls' => true,
'authUsername' => 'username',
'authPassword' => 'password',
]);
$this->assertEquals(201, $response['headers']['status-code']);
+2 -6
View File
@@ -101,18 +101,14 @@ trait RealtimeBase
$client->close();
}
public function testConnectionFailureMissingChannels(): void
public function testConnectionSuccessMissingChannels(): void
{
$client = $this->getWebsocket([]);
$payload = json_decode($client->receive(), true);
$this->assertArrayHasKey("type", $payload);
$this->assertArrayHasKey("data", $payload);
$this->assertEquals("error", $payload["type"]);
$this->assertEquals(1008, $payload["data"]["code"]);
$this->assertEquals("Missing channels", $payload["data"]["message"]);
\usleep(250000); // 250ms
$this->expectException(ConnectionException::class); // Check if server disconnected client
$this->assertEquals("connected", $payload["type"]);
$client->close();
}

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