Compare commits

..
Author SHA1 Message Date
fogelito c6a8e5710e Merge branch '1.9.x' of https://github.com/appwrite/appwrite into update-migration-processing 2026-05-26 09:57:42 +03:00
Chirag AggarwalandGitHub 4efe1e6890 Merge pull request #12392 from appwrite/fix/preserve-sdk-enum-metadata
Update SDK spec metadata generation
2026-05-26 11:53:00 +05:30
Chirag Aggarwal c7caf8d23d fix: map numeric spec enum keys 2026-05-26 11:33:25 +05:30
Chirag Aggarwal a4d889145b test: expect client web version 2026-05-26 11:28:30 +05:30
Chirag Aggarwal fe1a2f6364 fix: preserve organization project region enum 2026-05-26 11:19:48 +05:30
Chirag Aggarwal 3f0d1d2a27 fix: preserve remaining spec enum metadata 2026-05-26 11:14:52 +05:30
Chirag Aggarwal 08b18f0774 fix: name preserved enum metadata 2026-05-26 11:06:34 +05:30
Chirag Aggarwal 6f1841681f fix: preserve runtime and migration enum metadata 2026-05-26 11:02:52 +05:30
Chirag Aggarwal 29d2a99e92 fix: remove weight from spec metadata 2026-05-26 10:53:50 +05:30
Chirag Aggarwal 17bae22b24 Revert "Merge pull request #12356 from appwrite/fix-account-server-specs-auth"
This reverts commit ff76f3efa0, reversing
changes made to ed14acfc75.
2026-05-26 10:51:52 +05:30
Chirag Aggarwal 6bc5d0dd06 fix: remove endpoint docs appwrite metadata 2026-05-26 10:51:10 +05:30
Chirag Aggarwal 7f97d91f66 fix: order spec defaults before examples 2026-05-26 10:48:13 +05:30
Chirag Aggarwal 6266dfb7ab fix: preserve remaining SDK enum metadata 2026-05-26 10:45:48 +05:30
Chirag Aggarwal 88648b78f0 fix: preserve SDK enum metadata 2026-05-26 10:30:50 +05:30
Chirag AggarwalandGitHub e7f720b8e7 Merge pull request #12358 from appwrite/feat/platform-param-enums
Use platform enum metadata for SDK specs
2026-05-26 10:10:43 +05:30
fogelito 8685266e97 Merge branch '1.9.x' of https://github.com/appwrite/appwrite into update-migration-processing 2026-05-25 19:11:35 +03:00
Chirag Aggarwal e19ca24d5d Update enum metadata tests 2026-05-25 20:36:14 +05:30
Chirag Aggarwal ee97d4ef4d Use compatible HTTP release for platform 2026-05-25 19:27:04 +05:30
Chirag Aggarwal 764fb0d156 Merge remote-tracking branch 'origin/1.9.x' into feat/platform-param-enums
# Conflicts:
#	composer.lock
2026-05-25 19:23:54 +05:30
Chirag Aggarwal a659b0b187 Use stable Utopia dependency releases 2026-05-25 19:21:35 +05:30
Shmuel FogelandGitHub 22be6514fb Merge pull request #12293 from appwrite/users-skip-targets
skip subQueryTargets in users route
2026-05-25 12:11:58 +03:00
Jake BarnbyandGitHub 7e4645ffbd Merge branch '1.9.x' into users-skip-targets 2026-05-25 20:09:58 +12:00
Damodar LohaniandGitHub 4b801273c0 Merge pull request #12384 from appwrite/CLO-4385-fix-event-params-on-chunked-retry
fix(uploads): reset queueForEvents on idempotent chunked-upload retry
2026-05-25 13:04:08 +05:45
Damodar LohaniandClaude Opus 4.7 d6ca57d008 fix(uploads): reset queueForEvents on idempotent chunked-upload retry
The early-return branches added in #12138 for already-complete chunked
uploads bypass the trailing $queueForEvents->setParam('bucketId', ...)
calls. The action returns cleanly, the shutdown hook then calls
Event::generateEvents with empty params, throws \InvalidArgumentException,
and the error handler maps it to HTTP 500.

Reset the queue on each early-return path so the shutdown short-circuits
on the empty event string -- the resource was created on a previous
request and that request already fired the event; replaying it would
double-trigger webhooks/functions.

Applied to Storage Files Create, Functions Deployments Create, and
Sites Deployments Create -- all three endpoints share the same pattern
introduced by #12138.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 06:36:25 +00:00
Chirag AggarwalandGitHub 03555fa365 Merge pull request #12376 from appwrite/codex/builds-worker-span-logs
Replace builds worker logs with span attributes
2026-05-25 09:00:38 +05:30
fogelito b07046eaba Merge branch '1.9.x' of https://github.com/appwrite/appwrite into update-migration-processing 2026-05-24 10:19:42 +03:00
Damodar LohaniandGitHub 9cbd5be707 Merge pull request #12329 from appwrite/feat/actor-type-constants-CLO-4357
feat!: rename ACTIVITY_TYPE_*, ROLE_APPS, isApp() to ACTOR_TYPE_*, ROLE_KEYS, isKey() + bump utopia-php/audit to ^2.4
2026-05-24 10:02:05 +05:45
Damodar LohaniandGitHub e9aaea19a4 Merge pull request #12378 from appwrite/CLO-4364-bump-php-runtimes-0.20.1
chore: bump appwrite/php-runtimes to 0.20.1
2026-05-24 09:44:45 +05:45
Damodar Lohani 3fc5fd8fcf chore: bump appwrite/php-runtimes to 0.20.1 2026-05-24 03:08:25 +00:00
Damodar Lohani 5f63100f4c fix(presences): rename isApp() callers to isKey() (new files from upstream 1.9.x merge) 2026-05-24 02:20:47 +00:00
Damodar Lohani 39aeecc58c fix(audits): drop removed 'location' field from event payload (utopia-php/audit 2.4 schema) 2026-05-24 02:20:47 +00:00
Damodar Lohani ce31a4f336 feat!: rename User::ROLE_APPS to ROLE_KEYS and isApp() to isKey() 2026-05-24 02:20:47 +00:00
Damodar Lohani 5f1e537e53 chore: bump utopia-php/audit to ^2.4 2026-05-24 02:20:47 +00:00
Damodar LohaniandClaude Opus 4.7 27370b6acb feat!: rename ACTIVITY_TYPE_* constants to ACTOR_TYPE_*
Renames the actor identity constants used by the audit/auth flow:

- ACTIVITY_TYPE_USER             -> ACTOR_TYPE_USER             ('user')
- ACTIVITY_TYPE_ADMIN            -> ACTOR_TYPE_ADMIN            ('admin')
- ACTIVITY_TYPE_GUEST            -> ACTOR_TYPE_GUEST            ('guest')
- ACTIVITY_TYPE_KEY_PROJECT      -> ACTOR_TYPE_KEY_PROJECT      ('keyProject')
- ACTIVITY_TYPE_KEY_ACCOUNT      -> ACTOR_TYPE_KEY_ACCOUNT      ('keyAccount')
- ACTIVITY_TYPE_KEY_ORGANIZATION -> ACTOR_TYPE_KEY_ORGANIZATION ('keyOrganization')

Values are unchanged. Call sites updated in:
- app/controllers/shared/api.php
- src/Appwrite/Platform/Workers/Audits.php

Audit payload key rename (userType -> actorType) and utopia-php/audit
bump will land in a follow-up PR.

BREAKING CHANGE: ACTIVITY_TYPE_* global constants are removed. Any
downstream extension or plugin importing those names must be updated
to the ACTOR_TYPE_* equivalents.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 02:20:17 +00:00
Chirag Aggarwal f1abdde9cf Replace builds worker logs with span attributes 2026-05-23 20:37:34 +05:30
Harsh MahajanandGitHub 8436fb0175 Merge pull request #12375 from appwrite/fix/organization-projects-api
Fix: organization project API headers and queries
2026-05-22 16:46:45 +05:30
harsh mahajan 6a4dcd76a7 Keep organization projects schema unchanged 2026-05-22 15:45:32 +05:30
harsh mahajan e831b3e952 Add project status index 2026-05-22 15:39:16 +05:30
harsh mahajan 4dec4f2ff2 Fix organization project API headers and queries 2026-05-22 15:32:44 +05:30
Harsh MahajanandGitHub aa4c6b6e8f Merge pull request #12374 from appwrite/fix/smtp-test-email-missing-platform-vars
Fix missing platform variables in SMTP test email template
2026-05-22 13:39:59 +05:30
harsh mahajan 2628cc31fc Remove unused $plan injection 2026-05-22 13:12:50 +05:30
harsh mahajanandHarsh Mahajan e3768ce8ee Fix missing platform variables in SMTP test email template
The SMTP test email uses email-base-styled.tpl as its base template,
which contains {{platform}}, {{logoUrl}}, {{accentColor}}, and social/
legal link placeholders. These were never passed as template variables,
causing them to render as literal strings (e.g. "{{platform}} logo").

Inject the platform config and pass the variables to MailMessage,
matching the pattern used by OTP and magic-url email flows.

Co-Authored-By: Harsh Mahajan <harsh@appwrite.io>
2026-05-22 13:03:48 +05:30
Jake BarnbyandGitHub dfc5f1c8dc Merge pull request #12371 from appwrite/fix-project-smtp-port-type
fix(project): cast smtpPort to int in response model
2026-05-22 19:09:07 +12:00
Harsh MahajanandGitHub e375009a49 Merge pull request #12373 from appwrite/feat-ser-401-expose-build-trigger-fields
Expose providerBranches and providerPaths in Function and Site response
2026-05-22 12:36:59 +05:30
harsh mahajanandHarsh Mahajan cf1623bc91 Expose providerBranches and providerPaths in Function and Site response models
These fields were already persisted on update but omitted from the response
model, causing them to disappear after a page refresh in the console.

Co-Authored-By: Harsh Mahajan <harsh@appwrite.io>
2026-05-22 12:27:05 +05:30
Prem Palanisamy e364082577 fix(project): cast smtpPort to int in response model 2026-05-22 07:52:25 +01:00
Luke B. SilverandGitHub 9951ccdca3 Merge pull request #12338 from appwrite/fix/coroutines-correctness
chore: migrate to utopia-php/http feat-safe-wildcards
2026-05-21 21:58:55 +01:00
loks0nandClaude Opus 4.7 035cbacbfb Use 'params' injection in V20 filter
Drops the redundant Route::resolveParams() call. The matched path
params are now provided directly via the new frame-local 'params'
injection from utopia-php/http, avoiding a second URL parse.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 21:09:55 +01:00
loks0nandClaude Opus 4.7 690c72b789 Use 'route' injection in remaining api/auth init hooks
The 'route' injection is frame-local and non-nullable inside a
matched action's hooks. Replaces \$utopia->match() lookups in api.php
and auth.php init hooks, drops the dead \$utopia inject from the
session shutdown hook, and removes the now-redundant null guards.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 21:09:41 +01:00
loks0nandClaude Opus 4.7 dd0c8af160 Use 'route' injection in shutdown hooks
The 'route' injection introduced in utopia-php/http feat-safe-wildcards
is frame-local and non-nullable inside a matched action's hooks, so
the shutdown handlers in api.php and mock.php no longer need to call
match() and dereference a nullable result.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 21:09:41 +01:00
loks0nandClaude Opus 4.7 933fffd763 Use route template instead of request URI for documentsdb path check
The documentsdb/vectorsdb routes are registered with
setHttpPath('/v1/documentsdb/...') with no aliases, so getPath()
returns a template containing the substring we're matching against
— and matches the prior getMatchedPath() semantics without depending
on the raw request URI.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 21:09:41 +01:00
loks0nandClaude Opus 4.7 1538927486 chore: migrate to utopia-php/http feat-safe-wildcards
Adopts the new safe-wildcard dispatch primitive from
utopia-php/http#feat-safe-wildcards. Http::execute() is now the
re-entrant dispatch entry point and Http::match() is pure (returns
?RouteMatch). The removed Http::getRoute()/setRoute(),
Route::getMatchedPath(), Route::getPathValues() callsites are migrated
to the new API.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 21:09:41 +01:00
Harsh MahajanandGitHub 49bf24d388 Merge pull request #11955 from appwrite/feat-ser-401-custom-triggers
Feat: custom triggers for VCS builds
2026-05-21 19:08:38 +05:30
harsh mahajan f62d765bbf format 2026-05-21 18:46:18 +05:30
harsh mahajanandClaude Sonnet 4.6 268dd50106 Use Globstar validator directly, remove BuildTrigger wrapper
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 18:43:34 +05:30
Harsh MahajanandGitHub 4eb956b820 Merge branch '1.9.x' into feat-ser-401-custom-triggers 2026-05-21 16:37:24 +05:30
harsh mahajan 01a6d6e4d6 Inline VCS build trigger checks 2026-05-21 15:35:44 +05:30
harsh mahajan c1e902c79c Restore deployments on branch creation 2026-05-21 15:32:35 +05:30
harsh mahajan 9bcb4d7ca7 Clarify renamed PR file filtering 2026-05-21 15:31:05 +05:30
Matej BačoandGitHub f77205295b Merge pull request #12309 from appwrite/feat-public-organization-api
Feat: Public organization API
2026-05-21 10:25:59 +02:00
Matej BačoandGitHub 781df9d4cf Merge branch '1.9.x' into feat-public-organization-api 2026-05-21 09:42:32 +02:00
Jake Barnby 238b4e447d test: stabilize function logging executions 2026-05-21 18:38:59 +12:00
Jake Barnby 349ac6bbc1 test: stabilize account log pagination 2026-05-21 16:47:45 +12:00
Jake BarnbyandGitHub dca75b059b Merge pull request #12361 from appwrite/fix-smtp-template-regressions
Fix SMTP empty sender regressions
2026-05-21 12:42:51 +12:00
Jake Barnby fd145e988a test: align versions endpoint expectations 2026-05-21 12:18:11 +12:00
Jake Barnby c6ac0f28e4 chore: restore twig security update 2026-05-21 12:16:06 +12:00
Jake Barnby 52d7ebd41e chore: revert to 2.0.0-rc.1 2026-05-21 10:59:37 +12:00
Jake Barnby 41ff2ed48e chore: update lock 2026-05-21 10:37:36 +12:00
harsh mahajan 436064e7cc Revert test fixture change 2026-05-20 23:44:11 +05:30
harsh mahajan a3ae9f39ec Fix SMTP empty sender regressions 2026-05-20 23:40:41 +05:30
fogelito 19ab27bf57 Merge branch '1.9.x' of https://github.com/appwrite/appwrite into update-migration-processing 2026-05-20 17:39:45 +03:00
fogelito 9ad625da36 formatting 2026-05-20 16:38:17 +03:00
fogelito 26e8792b63 Use $updatedAt 2026-05-20 16:37:50 +03:00
fogelito c2df9409d6 Use $updatedAt 2026-05-20 16:00:35 +03:00
fogelito fd6146888b Merge branch '1.9.x' of https://github.com/appwrite/appwrite into users-skip-targets 2026-05-20 15:54:15 +03:00
Chirag Aggarwal 91227249b8 chore: use enum metadata server branch 2026-05-20 17:47:45 +05:30
Matej Bačo 4400eedbae Add proper test coverage 2026-05-20 13:56:47 +02:00
Matej Bačo cbcf86e362 Add missing delete endpoint 2026-05-20 13:49:40 +02:00
Chirag Aggarwal 83dc606c14 Use platform enum metadata for SDK specs 2026-05-20 17:19:35 +05:30
Matej Bačo e69b9eb428 PR cleanup 2026-05-20 13:49:29 +02:00
Harsh MahajanandGitHub 8cfc28f08c Merge pull request #12348 from appwrite/fix-smtp-template-empty-string-params
fix: allow empty string to clear optional SMTP and email template fields
2026-05-20 17:00:14 +05:30
harsh mahajan 5473be5756 chore: revert twig override, restore sdk-generator to 1.31.0 2026-05-20 16:56:56 +05:30
harsh mahajan a3fca62818 chore: bump twig/twig to ^3.26 to fix security advisories 2026-05-20 16:45:17 +05:30
harsh mahajan d397ab9b40 chore: update composer dependencies 2026-05-20 16:34:15 +05:30
Harsh MahajanandGitHub e59e726d38 Merge branch '1.9.x' into fix-smtp-template-empty-string-params 2026-05-20 16:28:06 +05:30
harsh mahajan 8272aa6fa2 fix: remove unnecessary null coalescing on senderEmail 2026-05-20 16:27:52 +05:30
harsh mahajan 43b9bf8152 fix: remove redundant senderEmail empty check before setFrom 2026-05-20 16:25:04 +05:30
fogelito 7312f4bfe0 Merge branch '1.9.x' of https://github.com/appwrite/appwrite into users-skip-targets 2026-05-20 13:50:31 +03:00
fogelito a83c01db65 Created at 2026-05-20 13:46:29 +03:00
fogelito d50dcad24a Update message 2026-05-20 13:15:54 +03:00
Chirag AggarwalandGitHub 5df3280131 Merge pull request #12357 from appwrite/fix/spec-regional-endpoint-validation
Fix generated spec endpoint and enum defaults
2026-05-20 15:21:12 +05:30
fogelito 151ef8ab7c console info 2026-05-20 12:41:38 +03:00
Chirag Aggarwal 515696dd36 Fix generated spec endpoint and enum defaults 2026-05-20 14:58:56 +05:30
fogelito 227e47f121 return presents 2026-05-20 12:27:42 +03:00
fogelito 940fa701da return on transactions 2026-05-20 12:02:34 +03:00
Chirag AggarwalandGitHub ff76f3efa0 Merge pull request #12356 from appwrite/fix-account-server-specs-auth
Fix account server specs auth
2026-05-20 14:26:14 +05:30
fogelito 2ef2d4d498 return for stats 2026-05-20 11:53:07 +03:00
Chirag Aggarwal 0d19894331 Move Web SDK to server platform 2026-05-20 14:09:07 +05:30
Chirag Aggarwal 1d9acf5f29 Fix account SDK specs auth 2026-05-20 14:03:20 +05:30
Harsh MahajanandGitHub d3994b436d Merge branch '1.9.x' into fix-smtp-template-empty-string-params 2026-05-20 11:40:47 +05:30
harsh mahajan cc51f42430 fix: allow SMTP connection test without senderEmail 2026-05-20 11:40:23 +05:30
Matej Bačo 8cf0438a98 Merge branch '1.9.x' into feat-public-organization-api 2026-05-19 17:20:33 +02:00
Matej Bačo b2ebfa9bb9 Revert unwanted tests 2026-05-19 17:20:15 +02:00
Matej BačoandGitHub ed14acfc75 Merge pull request #12353 from appwrite/fix-trace-ending-too-soon
Fix: subspan ending main request span
2026-05-19 16:57:59 +02:00
Matej Bačo 90df8efe0a Formatting fix 2026-05-19 15:50:48 +02:00
fogelito c1aeaf9bb9 Merge remote-tracking branch 'origin/update-migration-processing' into update-migration-processing 2026-05-19 16:29:57 +03:00
fogelito 2b8d035545 use constant for processing stuck 2026-05-19 16:29:45 +03:00
Matej Bačo f7b09c090b Fix subspan ending main request span 2026-05-19 15:23:34 +02:00
Shmuel FogelGitHubgreptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
d9e4318ab9 Update src/Appwrite/Platform/Workers/Deletes.php
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-05-19 16:01:27 +03:00
Shmuel FogelGitHubgreptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
4523ad5e1f Update src/Appwrite/Platform/Workers/Deletes.php
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-05-19 16:00:55 +03:00
fogelito a67930a18a Remove use 2026-05-19 15:56:19 +03:00
fogelito 24900ee08e retention 2026-05-19 15:54:56 +03:00
fogelito aa3a63c87b Merge branch '1.9.x' of https://github.com/appwrite/appwrite into users-skip-targets 2026-05-19 15:51:12 +03:00
fogelito 586eb3ca19 updateProcessingMigrations 2026-05-19 15:49:55 +03:00
harsh mahajan 8c903ab687 fix: allow empty string to clear optional SMTP and email template fields 2026-05-19 16:29:55 +05:30
Jake BarnbyandGitHub c5b8535a7f Merge pull request #12233 from appwrite/feat-dedicated-db
feat: dedicated database
2026-05-19 19:44:13 +12:00
Harsh MahajanandGitHub cf101b6a8e Merge branch '1.9.x' into feat-ser-401-custom-triggers 2026-05-19 12:35:41 +05:30
Jake BarnbyGitHubgreptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
218cc13b20 Update src/Appwrite/Platform/Workers/StatsResources.php
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-05-19 18:32:54 +12:00
282b5b2a6c fix(databases): map cursor lookup NotFound to collection 404
Agent-Logs-Url: https://github.com/appwrite/appwrite/sessions/546b5830-100b-4a48-9d71-f19c204e269f

Co-authored-by: abnegate <5857008+abnegate@users.noreply.github.com>
2026-05-19 06:32:06 +00:00
948c702ad4 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-19 18:28:20 +12:00
c28f476462 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-19 18:24:06 +12:00
Jake BarnbyandGitHub 3078cc5941 Merge pull request #12345 from appwrite/codex/advisor-admin-key-auth
fix: restrict advisor auth types
2026-05-19 18:23:06 +12:00
harsh mahajanandClaude Sonnet 4.6 74a4ae19ee refactor: remove unnecessary comments
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 11:28:04 +05:30
harsh mahajanandClaude Sonnet 4.6 3b3a04877f feat: remove CommitSkipPatterns - already available in 1.9.x
Keep only branch and path skip trigger logic. Commit message skip
is handled by the existing Contains(VCS_DEPLOYMENT_SKIP_PATTERNS) check.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 11:21:04 +05:30
harsh mahajanandClaude Sonnet 4.6 a4a57bc64d Merge branch '1.9.x' into feat-ser-401-custom-triggers
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 11:20:08 +05:30
Damodar LohaniandGitHub fd91490ae9 Merge pull request #12344 from appwrite/feat/file-size-actual
feat(storage): expose sizeActual in File response model
2026-05-19 09:02:12 +05:45
Jake BarnbyandGitHub 81d78ceb96 Merge branch '1.9.x' into codex/advisor-admin-key-auth 2026-05-19 14:53:56 +12:00
Jake Barnby 9233fe5b42 fix: restrict advisor auth types 2026-05-19 14:47:54 +12:00
Jake BarnbyandGitHub 3c1e07fc92 Merge pull request #12343 from appwrite/presence-api
added category to the presences scopes
2026-05-19 14:47:28 +12:00
Damodar LohaniandClaude Opus 4.7 27534133bb feat(storage): expose sizeActual in File response model
Adds the stored byte size (after compression and/or encryption) alongside
sizeOriginal so clients can see how much space a file actually occupies.
The sizeActual attribute is already persisted on file documents.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 02:11:49 +00:00
Jake Barnby 52b8e1b94d chore: sync 1.9.x into feat-dedicated-db 2026-05-19 13:32:46 +12:00
ArnabChatterjee20k 4d3c60b36c updated 2026-05-18 21:53:06 +05:30
Chirag AggarwalandGitHub 8d93922a52 Merge pull request #12339 from appwrite/chore/remove-router-deployment-fallback
Remove router deployment fallback
2026-05-18 21:50:49 +05:30
ArnabChatterjee20k 89d8c9285a Merge remote-tracking branch 'origin/presence-api' into presence-api 2026-05-18 21:49:59 +05:30
ArnabChatterjee20k ffe06180bc Merge remote-tracking branch 'origin/1.9.x' into presence-api 2026-05-18 21:49:37 +05:30
ArnabChatterjee20k 349db8d370 fix: improve error handling for Utopia DB exceptions in realtime API 2026-05-18 21:49:13 +05:30
ArnabChatterjee20kandGitHub 246ff8900b Merge branch '1.9.x' into presence-api 2026-05-18 21:31:56 +05:30
ArnabChatterjee20k 6f28f2e4f7 linting 2026-05-18 21:26:21 +05:30
ArnabChatterjee20k 42144ab9a0 added category to the presences scopes 2026-05-18 21:23:21 +05:30
Luke B. SilverandGitHub 836a89714d Merge pull request #12341 from appwrite/fix/cache-healthcheck
fix(health): use injected cache resource for healthcheck
2026-05-18 14:31:16 +01:00
Chirag AggarwalandGitHub f1d40a8e92 Merge pull request #12332 from appwrite/refactor/shared-di-resources
Consolidate shared DI resources
2026-05-18 18:38:38 +05:30
Chirag Aggarwal 05eead1857 Remove router deployment fallback 2026-05-18 18:34:12 +05:30
loks0nandClaude Opus 4.7 2cd422f4c2 fix(health): use injected cache resource for healthcheck
Inject the live cache resource and ping it directly instead of
rebuilding pool adapters from pools-cache. The container already
constructs the right adapter (pool, multiplexing, none), so the
healthcheck now exercises the actual configured cache.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 13:58:24 +01:00
Chirag Aggarwal b756a38252 Consolidate shared DI resources 2026-05-18 16:16:23 +05:30
Atharva DeosthaleandGitHub 4f676aac15 Merge pull request #12337 from appwrite/feat/sites-bun-deno-runtimes
feat(sites): allow bun and deno as build runtimes for JS frameworks
2026-05-18 16:12:18 +05:30
ArnabChatterjee20kandGitHub 7a711b7295 Merge pull request #11886 from appwrite/presence-api
Presence api
2026-05-18 15:25:03 +05:30
Atharva Deosthale 03e62dde94 feat(sites): allow bun and deno as build runtimes for JS frameworks
Merge BUN and DENO template runtimes into the runtimes list for every NODE-based Sites framework (analog, angular, nextjs, react, nuxt, vue, sveltekit, astro, tanstack-start, remix, lynx, react-native, vite, other). The Console's build-runtime dropdown reads framework.runtimes, so this is what surfaces bun-1.3 and deno-2.6 in the UI now that openruntimes/open-runtimes#485 has shipped SSR support for both runtimes.

The server-side WhiteList validator on POST /v1/sites already accepts any runtime in the global runtimes config, so CLI and REST clients have been able to pick bun/deno all along; this change only flips the UI gate.

Flutter stays flutter-only.
2026-05-18 15:16:59 +05:30
Shmuel FogelandGitHub b81b818c47 Merge pull request #12335 from appwrite/delete-executions-order-by
Delete executions order by
2026-05-18 11:24:03 +03:00
fogelito 95789b5aec Merge branch '1.9.x' of https://github.com/appwrite/appwrite into delete-executions-order-by 2026-05-18 11:02:30 +03:00
ArnabChatterjee20k 20c0c8081c feat: add documentation for presence API endpoints and update descriptions 2026-05-18 12:30:58 +05:30
fogelito 254606c4f9 Add orderDesc 2026-05-18 10:00:24 +03:00
Chirag AggarwalandGitHub 7cef1202f1 Merge pull request #12330 from appwrite/fix-unity-sdk-getting-started
Add Unity SDK getting started docs
2026-05-18 12:29:40 +05:30
ArnabChatterjee20k ff298cb206 updated clousure scope 2026-05-18 11:48:52 +05:30
ArnabChatterjee20k 9263ea3661 Merge remote-tracking branch 'origin/presence-api' into presence-api 2026-05-18 11:32:55 +05:30
ArnabChatterjee20k 8aa09cfcc1 fix: update checkForProjectUsage function to return void instead of Document 2026-05-18 11:32:37 +05:30
Chirag Aggarwal ab208a01b6 Update Unity docs for client factories 2026-05-18 11:22:24 +05:30
ArnabChatterjee20kandGitHub 80fffa32b8 Merge branch '1.9.x' into presence-api 2026-05-18 11:09:55 +05:30
ArnabChatterjee20k 664c24173e feat: update checkForProjectUsage function to return the project document 2026-05-18 11:06:33 +05:30
Chirag Aggarwal 5b3af53bdd Address Unity SDK docs review 2026-05-18 11:04:07 +05:30
ArnabChatterjee20k b2809c911d fix: improve presence log deletion handling and ensure accurate event triggering 2026-05-18 11:02:21 +05:30
Chirag Aggarwal febe332050 Add Unity SDK getting started docs 2026-05-18 10:56:29 +05:30
ArnabChatterjee20k d18de7d1e8 feat: add checkForProjectUsage function and integrate it into message handling 2026-05-18 10:42:11 +05:30
Luke B. SilverandGitHub 4e39506bca Merge pull request #12326 from appwrite/chore/bump-cace
chore: bump utopia-php/cache to ^3.0
2026-05-17 20:56:32 +01:00
loks0nandClaude Opus 4.7 1b945bdeea chore: bump utopia-php/cache to ^3.0
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 17:11:05 +01:00
fogelito 5c9f360744 Merge branch '1.9.x' of https://github.com/appwrite/appwrite into users-skip-targets 2026-05-17 17:17:09 +03:00
Torsten DittmannandGitHub 4a44af7308 Merge pull request #12209 from appwrite/fix/parallel-storage-chunk-upload
Fix parallel chunked uploads with pooled locks
2026-05-15 22:22:41 +04:00
Torsten Dittmann 37921a5694 Remove utopia-php/lock vcs repo 2026-05-15 21:20:20 +04:00
Torsten Dittmann 27bf229524 Simplify storage test assumptions 2026-05-15 21:12:14 +04:00
Torsten DittmannandGitHub e873f02b94 Merge branch '1.9.x' into fix/parallel-storage-chunk-upload 2026-05-15 20:17:08 +04:00
Torsten Dittmann 5750155591 Add pool-backed lock resource 2026-05-15 19:54:44 +04:00
Harsh MahajanandGitHub d2dcd82efa Merge pull request #12318 from appwrite/feat-email-template-reset-default
feat: add email template reset and get-default endpoints
2026-05-15 20:27:39 +05:30
harsh mahajan cb80f0e9c8 Remove email template integration test 2026-05-15 19:44:20 +05:30
harsh mahajan 6a2ddb221c Clean up template update diff and add locale test 2026-05-15 19:34:15 +05:30
harsh mahajan 6428b73966 Fix email template update System import 2026-05-15 19:05:01 +05:30
harsh mahajan 7b718e7dcc Add email template default integration test 2026-05-15 18:57:08 +05:30
harsh mahajan 15422dc7ca Remove email template reset API changes 2026-05-15 18:51:22 +05:30
harsh mahajan 0e7c50c181 Align console email template endpoint metadata 2026-05-15 18:45:38 +05:30
harsh mahajan 8781942633 Fix console email template spec enum names 2026-05-15 18:18:10 +05:30
harsh mahajan a614afe5c7 refactor: move email template defaults to console module and add tests
- Move GetDefault endpoint to Console module as Get.php
  (GET /v1/console/templates/email/:templateId, console.getEmailTemplate)
- Remove from Project module
- Add e2e tests for console.getEmailTemplate and reset=true
2026-05-15 18:03:12 +05:30
ArnabChatterjee20k cf80efbc3c feat: add support for 'getUsage' method in presences with range parameter 2026-05-15 18:01:32 +05:30
ArnabChatterjee20k ec1fad20dd Merge remote-tracking branch 'origin/presence-api' into presence-api 2026-05-15 17:51:33 +05:30
ArnabChatterjee20k 2c2e8bcabd fix: add expiration check for presence updates to prevent outdated entries 2026-05-15 17:51:08 +05:30
Torsten Dittmann b7bf14a18a Fix storage multipart upload preparation 2026-05-15 15:58:48 +04:00
harsh mahajan 0e157efabe chore: fix import ordering in Http.php 2026-05-15 17:17:32 +05:30
harsh mahajan 69735b383b feat: add email template reset and get-default endpoints
- Add GET /v1/project/templates/email/:templateId/default that returns
  Appwrite built-in defaults, ignoring any custom project overrides
- Add reset param to PATCH /v1/project/templates/email — when reset=true,
  clears the custom template entry and returns defaults without requiring
  SMTP to be enabled
2026-05-15 17:13:35 +05:30
Matej Bačo b627a7d6ff Org key tests 2026-05-15 12:46:27 +02:00
ArnabChatterjee20kandGitHub 14381f2268 Merge branch '1.9.x' into presence-api 2026-05-15 15:53:51 +05:30
Torsten Dittmann 97d6656687 Fix deployment upload lock TTL 2026-05-15 14:17:14 +04:00
Torsten Dittmann e6f3061103 Pin utopia-php/lock to 0.2.x 2026-05-15 14:15:08 +04:00
Torsten Dittmann 9acfb2a679 Merge branch '1.9.x' of https://github.com/appwrite/appwrite into fix/parallel-storage-chunk-upload 2026-05-15 14:02:58 +04:00
Torsten Dittmann c5123529ee Update graphql-php to 15.32 and fix storage tests
- Bump graphql-php to 15.32 in composer.json and update composer.lock
- Extend Storage/Files/Create with mergeUploadMetadata and pass path and
  deviceForFiles into the lock callback
- Remove obsolete type hints in Storage/Files/Preview/Get
- Update Storage tests to tolerate S3-backed signatures
2026-05-15 13:54:24 +04:00
Matej Bačo a0c27afec8 Revert test changes 2026-05-15 11:47:52 +02:00
Matej Bačo 5a0bb57db2 linter fix 2026-05-15 11:45:39 +02:00
Matej Bačo d422b7abdc Final reverts 2026-05-15 11:44:47 +02:00
Matej Bačo 34078ad9af Revert unwanted changes 2026-05-15 11:37:04 +02:00
Matej Bačo 2d94704948 Formatting fix 2026-05-15 11:30:06 +02:00
Matej Bačo cd54e91784 Deprecate old projects endpoints 2026-05-15 11:27:19 +02:00
Matej Bačo 79f536445e Revive old endpoints 2026-05-15 11:26:35 +02:00
Matej Bačo 11839c6dcc Remove old endpoint aliases 2026-05-15 11:26:22 +02:00
Matej Bačo a9eb9d47a5 Merge branch '1.9.x' into feat-public-organization-api 2026-05-15 11:21:17 +02:00
Matej BačoandGitHub 246f487687 Merge pull request #12317 from appwrite/feat-org-scopes-console-api
Feat: Org scopes Console API
2026-05-15 11:17:13 +02:00
Matej Bačo 88e7fdbd9e Improve tests 2026-05-15 10:55:13 +02:00
Chirag AggarwalandGitHub aada60bdb1 Merge pull request #12316 from appwrite/chore/queue-database-publisher-migration
Refactor database queue publisher
2026-05-15 14:18:46 +05:30
Chirag AggarwalandGitHub f43e55620b Merge pull request #12315 from appwrite/chore/update-platform-rc2
Update Utopia platform to rc2
2026-05-15 14:13:13 +05:30
Chirag Aggarwal bf3ac31157 chore: retrigger ci 2026-05-15 13:59:10 +05:30
Chirag Aggarwal e9b026647a chore: retrigger ci 2026-05-15 13:57:10 +05:30
Matej Bačo e93a2c7101 Add tests for org scopes console get endpoint 2026-05-15 10:23:40 +02:00
Matej Bačo 8d8a0e883e Add org scopes get console endpoint 2026-05-15 10:23:32 +02:00
Matej Bačo 75587e629e Improve quality of org scopes 2026-05-15 10:23:22 +02:00
Chirag Aggarwal ad88b827e6 Refactor database queue publisher 2026-05-15 13:22:35 +05:30
Chirag Aggarwal 4d1dabac66 Update Utopia platform to rc2 2026-05-15 13:21:35 +05:30
ArnabChatterjee20k a8670ee8fb fix: update authorization handling in message processing to prevent state leakage 2026-05-15 13:17:14 +05:30
Matej Bačo 5b109f2f8d Fix org key usecase 2026-05-14 19:52:14 +02:00
Matej Bačo 3e35b888a7 Fix bug 2026-05-14 19:47:59 +02:00
Matej Bačo bcd2bfe5b2 Fix security 2026-05-14 19:34:49 +02:00
Matej Bačo d7e29a4dae fix more tests 2026-05-14 19:03:14 +02:00
Matej Bačo 3596662fec Update more tests 2026-05-14 18:42:30 +02:00
Matej Bačo f40c7d415c Prioritize org id header 2026-05-14 18:26:47 +02:00
Matej Bačo 176d59e7c9 Fix broken test after breaking change
List ALL projects no longer exists - we now have list project per organization
2026-05-14 18:02:20 +02:00
Matej Bačo bb25a36cf5 Fix project creation 401 expectation tests 2026-05-14 17:53:03 +02:00
Matej Bačo f37e091b77 Fix specs generation 2026-05-14 17:50:36 +02:00
Matej Bačo 18380a679d Fix backwards compatibility 2026-05-14 17:46:13 +02:00
Matej Bačo 2a82604a2b Better backwards compatibility for project creation 2026-05-14 17:36:03 +02:00
Matej Bačo b618bf1353 Fix inability to CRUD projects 2026-05-14 17:26:08 +02:00
ArnabChatterjee20k 0e1cabe47a Merge remote-tracking branch 'origin/1.9.x' into presence-api 2026-05-14 18:25:16 +05:30
Harsh MahajanandGitHub 54693d9417 Merge pull request #12278 from appwrite/feat/skip-deployment-commit-patterns
Feat/skip deployment commit patterns
2026-05-14 18:22:06 +05:30
ArnabChatterjee20k 2cbd8bbb47 linting 2026-05-14 17:33:53 +05:30
ArnabChatterjee20k 12b2acd7f7 Merge remote-tracking branch 'origin/1.9.x' into presence-api 2026-05-14 17:31:24 +05:30
fogelito 3684fee2a2 Merge branch '1.9.x' of https://github.com/appwrite/appwrite into users-skip-targets 2026-05-14 14:58:50 +03:00
Matej Bačo aaa870e656 Formatting fixes 2026-05-14 13:52:34 +02:00
Matej Bačo 94274381ff agentic migration fixes 2026-05-14 13:52:25 +02:00
ArnabChatterjee20k 3703da188e refactor: enhance presence management to prevent cross-user overwrites 2026-05-14 17:17:27 +05:30
fogelito d9446fa892 address 2026-05-14 14:45:00 +03:00
Matej Bačo 8a4a6d83d4 Manual migration fixes 2026-05-14 13:06:39 +02:00
Matej Bačo b524b9f934 Fix scopes 2026-05-14 12:18:09 +02:00
Matej Bačo 275a6fe078 merge endpoints as aliases 2026-05-14 12:01:51 +02:00
Matej Bačo 8b4c90e603 Introduce organization endpoints 2026-05-14 11:47:46 +02:00
ArnabChatterjee20k a4acd000c4 linting 2026-05-14 13:57:35 +05:30
ArnabChatterjee20k f7e5e825f2 Merge remote-tracking branch 'origin/presence-api' into presence-api 2026-05-14 13:29:19 +05:30
ArnabChatterjee20k 7dd85f9143 add ownership guard to prevent cross-user presence overwrite 2026-05-14 13:28:47 +05:30
ArnabChatterjee20kandGitHub 7da3591d2b Merge branch '1.9.x' into presence-api 2026-05-14 13:12:42 +05:30
ArnabChatterjee20k faf13c502b reordered set in the realtime 2026-05-14 12:46:58 +05:30
ArnabChatterjee20k 43da03ef63 removed redundant container set 2026-05-14 12:38:11 +05:30
ArnabChatterjee20k 5e4de3931a added presences leaf names 2026-05-14 12:17:45 +05:30
ArnabChatterjee20k 2b7ed2aa24 removed stale 2026-05-14 12:15:15 +05:30
ArnabChatterjee20k 5123956790 reverted schema 2026-05-14 11:57:23 +05:30
ArnabChatterjee20k 28d0348ef5 fixed case 2026-05-14 11:55:25 +05:30
ArnabChatterjee20k ad0b2ebb06 changed to sentence case for the dispatcher messages 2026-05-14 11:53:40 +05:30
ArnabChatterjee20k a3542ed7fa feat: enhance presence handling by refactoring usage and event triggering logic 2026-05-14 11:48:41 +05:30
ArnabChatterjee20k 0aa8d402ae feat: streamline presence cleanup logic and enhance stale presence deletion 2026-05-14 11:36:55 +05:30
ArnabChatterjee20k 0466c82a42 fix: update error message for invalid permissions in PresenceRealtimeClientTest 2026-05-13 19:22:43 +05:30
ArnabChatterjee20k 995f844254 feat: refine permissions parameter handling in Presence action for clarity and consistency 2026-05-13 19:19:00 +05:30
ArnabChatterjee20k 18a26809af feat: improve user resolution during disconnect event for accurate actor context 2026-05-13 19:13:59 +05:30
ArnabChatterjee20k 42d3240d05 feat: refactor presence state usage and telemetry span naming for consistency 2026-05-13 19:12:11 +05:30
ArnabChatterjee20k 52c072489b Merge remote-tracking branch 'origin/1.9.x' into presence-api 2026-05-13 18:58:44 +05:30
ArnabChatterjee20k 602ceb55ef feat: refactor presence state usage across multiple files for consistency 2026-05-13 18:57:09 +05:30
ArnabChatterjee20k f09aec7651 feat: update presence handling and refactor related attributes for consistency 2026-05-13 18:52:14 +05:30
ArnabChatterjee20k f818f16ebe feat: simplify presence parameter validation by removing Nullable wrapper 2026-05-13 18:25:06 +05:30
ArnabChatterjee20k 20162a69be feat: ensure unique ID assignment for new presence documents 2026-05-13 14:26:23 +05:30
ArnabChatterjee20k f1e0ef2991 feat: refactor queue handling for events and realtime to use coroutine context 2026-05-13 14:04:41 +05:30
ArnabChatterjee20k a1f321d92e updated presence model 2026-05-13 12:38:47 +05:30
ArnabChatterjee20k 4e2bd9c307 feat: add presence already exists error handling and update exception messages 2026-05-13 12:30:54 +05:30
ArnabChatterjee20k e799589d11 feat: implement deduplication for presence delete events and enhance presence management 2026-05-13 12:20:12 +05:30
ArnabChatterjee20k efadf1d74b removed redundant 2026-05-13 11:18:09 +05:30
ArnabChatterjee20k 7f6482a7d2 Merge remote-tracking branch 'origin/1.9.x' into presence-api 2026-05-13 11:12:23 +05:30
fogelito 1435e2c526 use max 2026-05-13 08:41:14 +03:00
ArnabChatterjee20k 84f18341ed feat: add setOwnerPermissions method and update presence ownership logic 2026-05-13 11:09:56 +05:30
fogelito 2ea66a0d8b APP_LIMIT_SUBQUERY 2026-05-13 08:36:57 +03:00
fogelito 2d5289c383 Merge branch '1.9.x' of https://github.com/appwrite/appwrite into users-skip-targets 2026-05-12 20:55:54 +03:00
fogelito 06068c0406 skip subQueryTargets 2026-05-12 20:51:57 +03:00
ArnabChatterjee20k 5827f2ece6 fix: correct presence usage decrement in triggerPresenceUsage calls 2026-05-12 18:05:51 +05:30
ArnabChatterjee20k 62de39b858 removed todo comment 2026-05-12 17:44:28 +05:30
ArnabChatterjee20k c1cab2e41a removed redundant 2026-05-12 17:33:21 +05:30
ArnabChatterjee20k 95dd44115d refactor: update Dockerfile to remove composer stage and adjust presence handling in tests 2026-05-12 15:49:59 +05:30
ArnabChatterjee20kandCopilot cad35c7d5c refactor: enhance presence deletion logic and add expiration check in Get action
Co-authored-by: Copilot <copilot@github.com>
2026-05-11 16:47:36 +05:30
ArnabChatterjee20k 9f65c6ce41 updated namespace 2026-05-11 15:27:54 +05:30
ArnabChatterjee20k 28621678dd changed presence to presences for the e2e tests 2026-05-11 14:35:01 +05:30
ArnabChatterjee20k 4b98579adf refactor: simplify user retrieval in Presence handler by directly instantiating User object 2026-05-11 14:29:47 +05:30
ArnabChatterjee20k 6b7fb918cc refactor: streamline presence upsert logic and remove redundant presenceId parameter 2026-05-08 19:09:50 +05:30
ArnabChatterjee20k ffeb27e5a3 updated presence test 2026-05-08 14:36:59 +05:30
ArnabChatterjee20k 46e86ce854 fix: drop expired presences at fetch time and clean up Presence model attributes 2026-05-08 12:28:33 +05:30
ArnabChatterjee20k 5767ab8ac7 refactor: update triggerPresenceUsage to accept Document and streamline presence cleanup logic 2026-05-08 12:09:12 +05:30
ArnabChatterjee20k 5002afde09 optimizations
fix: update presence handling to include hostname and correct usage trigger parameter
improvement: removed an external findOne call to the db and used the timestamps for knowing upsert did an update or a create
2026-05-08 12:05:03 +05:30
ArnabChatterjee20k c13a0d1010 updated test 2026-05-08 11:27:11 +05:30
ArnabChatterjee20k f526fc8577 roles updated 2026-05-08 11:14:00 +05:30
ArnabChatterjee20k 63333a0cbf updated delete endpoint exceptions 2026-05-08 11:13:38 +05:30
ArnabChatterjee20k 0048d3f911 updated presence worker deletion to cap the 30 days limit 2026-05-08 11:10:36 +05:30
ArnabChatterjee20k 31adbe6189 fix: update presence permissions from read to write in tests and implementation 2026-05-08 11:02:25 +05:30
ArnabChatterjee20k a1811570b3 fix: update presence permissions from read to write in roles and HTTP handlers 2026-05-08 10:49:38 +05:30
harsh mahajanandClaude Sonnet 4.6 90f5f9b30b refactor: standalone regex matching for commit skip patterns
Replace directive-extraction approach with word-boundary regex matching
so plain-word patterns like "skip appwrite" and "appwrite skip" work
alongside bracket directives. Use \s+ between word tokens (required
space) and \s* only after ":" tokens (git trailer flexibility).
Add tests for "skip appwrite" and "appwrite skip" with case insensitivity.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 18:25:39 +05:30
Harsh MahajanandGitHub 408a12322a Merge branch '1.9.x' into feat-ser-401-custom-triggers 2026-05-07 18:17:27 +05:30
Jake Barnby 97477686fd chore: sync 1.9.x into feat-dedicated-db 2026-05-08 00:29:51 +12:00
Jake BarnbyandClaude Opus 4.7 9de9e5bdf0 test: stabilise ProjectWebhooks and Proxy e2e suites
ProjectWebhooks: bump assertEventually timeouts from 15s to 30/60s. Under
ParaTest with 4 parallel processes the column/attribute worker can take
longer than the original 15s budget to flip status to 'available', which
manifested as a flaky 'available' vs 'processing' assertion in
testUpdateRow on retry. The 60s budget covers attribute and column
provisioning; the 30s budget covers webhook delivery and auto-disable
polling. The function deployment build keeps its existing 120s budget.

Proxy: testCreateRedirectRule was deleting the site before its rules and
only tracking one of two created rule IDs, so cleanupRule hit a 404 because
the site cascade had already removed the rule. Track both rule IDs
distinctly (301 and 307) and clean rules up before the site so cleanup is
deterministic regardless of cascade behaviour.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 21:37:15 +12:00
Jake BarnbyandClaude Opus 4.7 de361573f2 fix: thread gauges through StatsResources message envelope
PHPStan flagged $payload as undefined in StatsResources::action — the gauges
fast path was reading from a variable that never existed, so the optimization
silently no-op'd and always fell through to the full count. Add a typed
$gauges field to the StatsResources message DTO so the worker reads the
pre-computed metrics from the parsed envelope, and drop redundant null
coalescing on the array{metric, value} shape since PHPStan now proves the
keys exist.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 21:30:13 +12:00
Jake Barnby 917ce627bb chore: merge 1.9.x into feat-dedicated-db 2026-05-07 20:30:08 +12:00
Jake BarnbyandGitHub a8f33ab964 Merge branch '1.9.x' into presence-api 2026-05-07 19:13:57 +12:00
Jake BarnbyandGitHub 0f67fd7618 Merge pull request #12210 from appwrite/realtime-messsage-dispatchers
Realtime messsage dispatchers (presence api)
2026-05-07 19:12:56 +12:00
ArnabChatterjee20k ec8d48327c reverted type error as query exception only thown 2026-05-07 12:34:34 +05:30
ArnabChatterjee20k ce07c6ffd5 added type error to the subscribe handler 2026-05-07 12:24:42 +05:30
ArnabChatterjee20k 6679d02a35 refactor: rename connection to connectionId for consistency across handlers 2026-05-07 11:48:45 +05:30
ArnabChatterjee20kandCopilot 63e226c761 updated
Co-authored-by: Copilot <copilot@github.com>
2026-05-06 14:50:40 +05:30
ArnabChatterjee20k 6290a664e9 refactor: rename presence methods for consistency and clarity 2026-05-06 14:16:59 +05:30
ArnabChatterjee20k ac80ba270b updated queries 2026-05-06 13:24:34 +05:30
ArnabChatterjee20k f9c18a6462 updated validator subscribe 2026-05-06 13:20:19 +05:30
ArnabChatterjee20k d2ea6ef2ca removed redundant dumps 2026-05-06 12:33:42 +05:30
ArnabChatterjee20k 1946ee86b2 feat: add realtime message handlers for authentication, ping, presence, subscribe, and unsubscribe 2026-05-06 12:20:06 +05:30
ArnabChatterjee20k 5eabeeea20 refactor: update payload validators to use closures for instantiation and improve error handling 2026-05-06 12:10:50 +05:30
ArnabChatterjee20k d043b4740e updated 2026-05-05 19:11:05 +05:30
ArnabChatterjee20k 6eb84c8235 Merge remote-tracking branch 'origin/presence-api' into realtime-messsage-dispatchers 2026-05-05 18:48:05 +05:30
harsh mahajan 61cee5892a fix: tighten commit skip directive matching 2026-05-05 18:23:14 +05:30
Harsh MahajanandGitHub a77558807c Merge branch '1.9.x' into feat-ser-401-custom-triggers 2026-05-05 17:58:54 +05:30
ArnabChatterjee20k f455bb7a58 refactor: add checks for queueForEvents and queueForRealtime in container setup; enhance presence resolution in tests 2026-05-05 17:11:49 +05:30
ArnabChatterjee20k bdf6c660d9 Merge remote-tracking branch 'origin/1.9.x' into presence-api 2026-05-05 11:53:43 +05:30
ArnabChatterjee20k b0a2ef271a updated the queue for event propagation 2026-05-05 11:35:49 +05:30
Torsten Dittmann 5c7e08fdff Fix deployment upload analyze warning 2026-05-04 17:59:27 +04:00
Torsten Dittmann fd83090215 Fix parallel deployment chunk uploads 2026-05-04 17:34:53 +04:00
ArnabChatterjee20k 2ed8247fba fix graphql tests 2026-05-04 17:39:34 +05:30
ArnabChatterjee20k b2b6fb6b69 Enhance validation logic in Realtime message validators. Update error handling to return specific validation error messages and streamline the validation process in SubscribePayloadValidator and UnsubscribePayloadValidator. 2026-05-04 17:25:39 +05:30
ArnabChatterjee20k 4822164e32 Inject global helpers into PresenceHandler for improved testability and maintainability. 2026-05-04 17:14:09 +05:30
ArnabChatterjee20k 42542aec61 Enhance error handling and validation in Realtime message dispatchers. Update error messages for clarity and enforce required parameters in AuthenticationHandler. 2026-05-04 17:11:35 +05:30
Torsten Dittmann 6e19db130e Fix storage chunk upload events 2026-05-04 15:09:52 +04:00
ArnabChatterjee20k ec0f7cf683 Update error message in PresenceRealtimeClientTest for clarity on permissions validation 2026-05-04 16:39:47 +05:30
ArnabChatterjee20k ced5b88b57 Refactor message dispatching and handler parameters for improved clarity and consistency. Update AuthenticationHandler and PresenceHandler to enforce required parameters. 2026-05-04 16:37:35 +05:30
Torsten Dittmann d58929b621 Restore storage upload lock TTL 2026-05-04 15:00:16 +04:00
ArnabChatterjee20k 29092b277b Update AuthenticationHandler to make session parameter optional and add validation for empty session tokens. Update error message in PresenceRealtimeClientTest for clarity. 2026-05-04 16:22:54 +05:30
Torsten Dittmann 9927e4bd8e Fix storage upload lock contention handling 2026-05-04 14:34:42 +04:00
ArnabChatterjee20k 0e0e40f991 fixed the container inheritance bug 2026-05-04 15:59:25 +05:30
Torsten Dittmann 3db776e2e5 Update parallel upload test concurrency 2026-05-04 14:24:01 +04:00
Torsten Dittmann 61ca4e3969 Fix parallel storage chunk upload state 2026-05-04 14:15:01 +04:00
harsh mahajanandClaude Sonnet 4.6 40c2e1a1fa feat: skip deployment on commit message pattern match
Add providerCommitSkipPatterns array field to functions and sites.
Any commit message containing one of the patterns (case-insensitive
substring) skips the VCS-triggered deployment.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 13:09:38 +05:30
ArnabChatterjee20k 429454ca0e added handlers 2026-05-04 11:18:22 +05:30
ArnabChatterjee20k 8cef822e8f Refactor presence management to use userInternalId instead of userId for improved consistency and clarity. Updated relevant methods in PresenceState and Upsert classes to reflect this change, ensuring proper handling of user identifiers across the application. 2026-04-30 18:50:46 +05:30
ArnabChatterjee20k 6dae78a206 linting 2026-04-30 18:48:54 +05:30
ArnabChatterjee20k 18ae81bbb0 Refactor presence management by introducing caching mechanisms for list responses. Added methods for loading, saving, and purging cache fields in the PresenceState class. Updated API endpoints to utilize caching, including purge functionality in Update and Delete actions. Enhanced tests to verify cache behavior during presence updates and deletions. 2026-04-30 18:33:20 +05:30
ArnabChatterjee20k e03218d8fa reverted last change 2026-04-30 18:24:38 +05:30
ArnabChatterjee20k 4f030619da temp gitignore 2026-04-30 18:03:25 +05:30
ArnabChatterjee20k e267dcf9ab updated 2026-04-30 18:01:51 +05:30
ArnabChatterjee20k 9c1cbecda4 bypassed sdk example? 2026-04-30 17:58:39 +05:30
ArnabChatterjee20k 8597d48e41 added additonal property key to rename on the sdk generator 2026-04-30 17:47:31 +05:30
ArnabChatterjee20k 971858078c updated 2026-04-30 16:50:04 +05:30
ArnabChatterjee20k 88bbb965f8 Merge remote-tracking branch 'origin/1.9.x' into presence-api 2026-04-30 16:41:48 +05:30
ArnabChatterjee20k 5510bc4f6a Update userInternalId attribute type to VAR_ID and remove lengths from index definition for consistency. 2026-04-30 16:38:26 +05:30
ArnabChatterjee20k d802c57cb9 Refactor presence management by introducing a new 'presenceLogs' collection and updating related API methods. Added error handling for presence not found scenarios and standardized parameter names across the Update and XList actions. Removed obsolete migration version 25 and adjusted migration mapping for version 1.9.4. 2026-04-30 16:22:28 +05:30
ArnabChatterjee20k 32dc870eaf Refactor presence API functions and update expiry test to improve clarity and maintainability. Renamed 'triggerPresenceEvent' to 'getQueueForEventsForProject' and adjusted expiry handling in tests for better validation. 2026-04-30 15:56:31 +05:30
ArnabChatterjee20k 2c06e4185f Update presence expiry datetime validation to enforce a 30-day maximum limit. This change standardizes the 'expiresAt' parameter across the Update and Upsert actions, ensuring consistent handling of presence expiry across the API. 2026-04-30 13:30:25 +05:30
ArnabChatterjee20k 869c35416d Refactor presence API to standardize scope and attribute names. Updated 'presence' to 'presences' across roles, scopes, and HTTP methods for consistency. Renamed 'expiry' to 'expiresAt' in various locations to improve clarity and maintainability. 2026-04-30 13:30:15 +05:30
ArnabChatterjee20k e6c3acb21c Refactor presence API by removing the Action class and updating related classes to extend PlatformAction. This change enhances consistency across the API methods and improves code maintainability. 2026-04-30 13:04:28 +05:30
harsh mahajan e7405b4393 addressed comments 2026-04-30 13:01:00 +05:30
Harsh MahajanandGitHub 3eabca1d7a Merge branch '1.9.x' into feat-ser-401-custom-triggers 2026-04-30 12:10:16 +05:30
Harsh MahajanandGitHub ec984de510 Merge branch '1.9.x' into feat-ser-401-custom-triggers 2026-04-29 17:23:08 +05:30
ArnabChatterjee20k 9d6be1eef4 updated 2026-04-29 15:04:28 +05:30
ArnabChatterjee20k 06a05311a4 Merge remote-tracking branch 'origin/1.9.x' into presence-api 2026-04-29 15:04:20 +05:30
ArnabChatterjee20k 58c248f4f2 Add migration for version 25 to ensure presenceLogs collection exists; update migration mapping for version 1.9.3. Remove redundant error assertion in PresenceTest. 2026-04-29 14:32:57 +05:30
ArnabChatterjee20k 59ee0901c9 Enhance presence API methods by adding detailed parameter specifications for update and upsert actions. Separate client-side and server-side SDK method definitions to clarify authentication requirements and improve usability. This update includes new parameters for presence management, ensuring better handling of user presence logs. 2026-04-29 14:20:38 +05:30
harsh mahajan 2d3c1086ae add endpoints 2026-04-29 13:50:40 +05:30
ArnabChatterjee20k e624040e57 Refactor presence API methods for clarity and consistency. Updated method names to include 'Presence' suffix for better identification. Enhanced presence state logic to support unique index-based upserts and improved test coverage for presence functionalities, including custom permissions and expiry handling. 2026-04-29 13:49:57 +05:30
ArnabChatterjee20k 496b91480b updated 2026-04-29 11:33:48 +05:30
ArnabChatterjee20k b08f3bdc52 Merge remote-tracking branch 'origin/1.9.x' into presence-api 2026-04-29 11:29:59 +05:30
Harsh MahajanandGitHub 9c646c2893 Merge branch '1.9.x' into feat-ser-401-custom-triggers 2026-04-29 11:25:05 +05:30
Jake Barnby 77942af4ea Merge remote-tracking branch 'origin/1.9.x' into feat-dedicated-db 2026-04-29 13:53:52 +12:00
ArnabChatterjee20k 41b2962e64 updated usage name 2026-04-28 18:03:26 +05:30
ArnabChatterjee20k 25ae2e7314 Implement triggerStats function, add GraphQL source header, and enhance presence upsert logic with source detection. Also, introduce PresenceTest for GraphQL presence upsert validation. 2026-04-28 17:48:23 +05:30
ArnabChatterjee20k 8d378720b0 add presence API metrics and usage tracking 2026-04-28 17:19:24 +05:30
ArnabChatterjee20k 34f782d986 updated roles for the admin and members users 2026-04-28 17:16:24 +05:30
ArnabChatterjee20k 587a039493 fixed syntax 2026-04-28 13:57:31 +05:30
ArnabChatterjee20k 93ee8f45ea updated scopes 2026-04-28 13:50:11 +05:30
ArnabChatterjee20k 5157da870f add presence specific read and write scope instead of depending on the users read and write scope 2026-04-28 12:19:03 +05:30
ArnabChatterjee20k 6fabfe08ce Merge remote-tracking branch 'origin/1.9.x' into presence-api 2026-04-28 11:40:58 +05:30
Harsh MahajanandGitHub 1eb7bf7619 Merge branch '1.9.x' into feat-ser-401-custom-triggers 2026-04-27 15:14:58 +05:30
ArnabChatterjee20k a17f9ce989 removed comments 2026-04-24 17:30:19 +05:30
ArnabChatterjee20k 053d25c64e Merge remote-tracking branch 'origin/1.9.x' into presence-api 2026-04-24 17:29:39 +05:30
Jake Barnby 8ca59f22c9 Merge remote-tracking branch 'origin/1.9.x' into feat-dedicated-db 2026-04-22 11:41:42 +12:00
harsh mahajan 63f04f893d fix composer 2026-04-20 12:18:26 +05:30
Hemachandarandharsh mahajan aa5543ac0a filter out null values 2026-04-20 12:02:07 +05:30
Hemachandarandharsh mahajan 60e51548e8 more tests 2026-04-20 12:02:07 +05:30
Hemachandarandharsh mahajan e802102c8f fix exclusion sub-directory 2026-04-20 12:02:07 +05:30
Hemachandarandharsh mahajan 87f29c6889 include previous_filename 2026-04-20 12:02:07 +05:30
Hemachandarandharsh mahajan a70d241da0 fix path trigger 2026-04-20 12:02:07 +05:30
Hemachandarandharsh mahajan e17cdc2bd4 fix pattern logic 2026-04-20 12:02:07 +05:30
Hemachandarandharsh mahajan a4f174ea7e update VCS 2026-04-20 12:02:07 +05:30
Hemachandarandharsh mahajan 175fcb51a8 remove default 2026-04-20 12:00:27 +05:30
Hemachandarandharsh mahajan 772358ff51 more tests 2026-04-20 12:00:27 +05:30
Hemachandarandharsh mahajan abad809a91 get PR files in Appwrite 2026-04-20 12:00:27 +05:30
Hemachandarandharsh mahajan 076e64886e validator pattern 2026-04-20 11:59:57 +05:30
harsh mahajan 88107ee7b3 feat: implement custom triggers for VCS builds 2026-04-20 11:57:57 +05:30
Jake Barnby db720fbb22 (chore): update lock 2026-04-20 17:58:06 +12:00
Jake Barnby d92580470d Merge remote-tracking branch 'origin/1.9.x' into feat-dedicated-db 2026-04-20 17:57:01 +12:00
Jake BarnbyandClaude Opus 4.7 fc714de11d (diag): remove realtime-auth-diag / realtime-project-diag logging
Root causes are fixed: Realtime now purges both projects:$projectId
and users:$userId from the shared cache before the respective reads,
eliminating the cross-process negative-cache races that surfaced as
intermittent 'Session is not valid.' failures on
appwrite-labs/cloud#3214's Realtime (dedicated) E2E.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 07:00:39 +12:00
Jake BarnbyandClaude Opus 4.7 92e8334169 (fix): purge cached project document before Realtime auth lookup
Cross-process negative-cache race in Realtime's onMessage handler: when
the WebSocket opens against a just-created project, an earlier router
probe or subscription lookup may have cached projects:\$projectId as
empty before the project actually existed. Although the HTTP worker
purges on createDocument, a narrow window lets Realtime re-populate the
stale empty entry in its own process cache, after which getProjectDB()
sees an empty Document and falls back to getConsoleDB(), which looks
for users in the console namespace and returns nothing — sessionVerify
then fails with 'Session is not valid.'

Surfaces on cloud#3214 as a recurring Realtime (dedicated) E2E failure.
Diagnosed via [realtime-project-diag] + [realtime-auth-diag]
instrumentation — the logs show consoleDb=appwrite consoleNs=console15x
(correct), project lookup empty, and subsequent user lookup landing in
the console namespace.

Purge is bounded: one cache DEL per WS message (not per read), runs
only when a projectId is present, and only forces the single
projects:\$projectId key to be re-read from adapter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 06:13:44 +12:00
Jake Barnby 8716497d1f (diag): log project lookup result before getProjectDB fallback 2026-04-18 05:42:49 +12:00
Jake Barnby d8486612de (diag): re-add realtime-auth-diag with session ids + shared-env 2026-04-18 04:10:44 +12:00
Jake BarnbyandClaude Opus 4.7 1225aa4861 (fix): translate NotFound from find() into 404 on listDocuments
When a DocumentsDB collection's metadata document exists but the
backing store (e.g. a freshly restored dedicated shard) has no table,
Utopia\Database\Database::find() throws NotFound. Previously this
bubbled to the HTTP layer as a generic 500. Catch it in the same
try/catch that already handles OrderException, QueryException, and
Timeout, and translate into COLLECTION_NOT_FOUND/TABLE_NOT_FOUND via
getParentNotFoundException() so the caller sees the correct 404.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 00:47:33 +12:00
Jake Barnby e8006dce6e (diag): remove realtime-auth-diag logging (root cause fixed in cloud) 2026-04-18 00:37:38 +12:00
Jake Barnby 894d771a7c (diag): include db/namespace/tenant/host in realtime-auth-diag 2026-04-18 00:17:13 +12:00
Jake BarnbyandClaude Opus 4.7 4e928ee08b (diag): log Realtime session-verify failure context
Temporary instrumentation to diagnose the 'Session is not valid.' failure
on appwrite-labs/cloud#3214's Realtime (dedicated) E2E. Emits:
  - project ID in scope
  - user ID from the session payload
  - whether the user document was found in the project DB
  - session count on the returned user document
  - session-secret prefix being verified
  - sessionVerify result

This will reveal whether the race is (a) user doc missing in project DB
(routing/provisioning issue) or (b) user doc found but sessions array
empty or mismatched (session write path issue). Revert once the root
cause is identified.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 23:08:44 +12:00
Jake BarnbyandClaude Opus 4.7 4da9873b83 (fix): purge cached user document before Realtime session verification
Cross-process read-after-write race: the HTTP worker writes a new session
into the user document on /account login endpoints, then the client sends
an authentication frame over a different Swoole process (Realtime). Cache
propagation between processes is not guaranteed to be observed on the
very next read, so sessionVerify() occasionally fails with a stale user
document whose sessions array does not yet contain the just-created one.

Purge the cached user locally before the read so sessionVerify sees the
freshly-written session deterministically. Overhead is bounded: one DEL
on cache + one extra primary-key read, executed once per WebSocket
authentication frame (not per message).

Surfaces on PR appwrite-labs/cloud#3214 as an intermittent
'Session is not valid.' failure in RealtimeConsoleClientTest +
RealtimeCustomClientTest manual-authentication cases in dedicated mode.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 22:27:28 +12:00
Jake Barnby 082110bba8 Merge branch '1.9.x' into feat-dedicated-db 2026-04-17 22:25:51 +12:00
ArnabChatterjee20k 7167fe2fb4 Merge remote-tracking branch 'origin/presence-api' into presence-api 2026-04-17 12:18:13 +05:30
ArnabChatterjee20k d1c3ee0290 updated upsert logic 2026-04-17 12:17:57 +05:30
ArnabChatterjee20kandGitHub 6bd3821804 Merge branch '1.9.x' into presence-api 2026-04-17 12:16:56 +05:30
ArnabChatterjee20k 1d32faff4f updated realtime 2026-04-17 12:04:55 +05:30
ArnabChatterjee20k e67016584f updated the schema 2026-04-17 12:02:26 +05:30
ArnabChatterjee20k a4b6d9a635 updated tests and reduce the timeouts 2026-04-17 12:02:17 +05:30
ArnabChatterjee20k 1f27769cc9 added catching of the exceptions 2026-04-17 12:02:07 +05:30
ArnabChatterjee20k 0d2a4da347 updated the presence helper 2026-04-17 12:01:56 +05:30
ArnabChatterjee20k ee34e2b587 created a helper for the duplicated logic between realtime and http api 2026-04-17 12:01:34 +05:30
ArnabChatterjee20k 52dbf14d79 updated ci 2026-04-17 12:01:04 +05:30
ArnabChatterjee20k 70f42a0dff added presences to the tests 2026-04-16 18:16:09 +05:30
ArnabChatterjee20k 2ef62b309e Refactor presence upsert logic to handle duplicate entries. Added exception handling for MongoDB adapter to update existing documents instead of creating new ones when presenceId mismatches occur. 2026-04-16 18:14:13 +05:30
ArnabChatterjee20k a480d8ed73 updated realtime 2026-04-16 18:11:01 +05:30
ArnabChatterjee20k 4fea92c9cb Refactor realtime authorization handling and enhance presence event tests. Updated authorization roles synchronization in the realtime connection and added exception handling for user retrieval. Improved connection pool size logic for PubSub workers and added comprehensive tests for presence events to ensure correct message ordering and validation. 2026-04-16 17:47:34 +05:30
ArnabChatterjee20k 05add106c1 Merge remote-tracking branch 'origin/1.9.x' into presence-api 2026-04-16 12:44:29 +05:30
ArnabChatterjee20k 0e3a017d50 added realtime trigger 2026-04-16 12:35:50 +05:30
ArnabChatterjee20k d28cce761d updated to have permissions order deterministic 2026-04-16 11:20:52 +05:30
ArnabChatterjee20k abb96f650b fix analyze 2026-04-16 11:18:39 +05:30
ArnabChatterjee20k 1785a78fa1 added event validation tests 2026-04-16 11:15:04 +05:30
ArnabChatterjee20k 3f06b5cd1a Enhance presence deletion by adding payload to event queue after document removal 2026-04-16 11:13:37 +05:30
Jake Barnby 95d5d1bdd9 Merge remote-tracking branch 'origin/1.9.x' into feat-dedicated-db 2026-04-16 01:15:10 +12:00
ArnabChatterjee20k cd6a6956d6 added channels and events generation 2026-04-15 18:16:51 +05:30
ArnabChatterjee20k 7e18e6f8c5 updated realtime and tests 2026-04-15 16:30:17 +05:30
ArnabChatterjee20k 968b1c0861 updated configs 2026-04-15 16:11:05 +05:30
ArnabChatterjee20k 2ce39768c3 added delete of presence on disconnect 2026-04-15 13:38:34 +05:30
ArnabChatterjee20k 69bc935517 Add permission handling for presence updates and enhance error validation in realtime messages 2026-04-15 13:21:26 +05:30
ArnabChatterjee20k 26ff78d98a updated expiry tests 2026-04-13 22:39:56 +05:30
ArnabChatterjee20k 0935c60f3c added users.read, users.write currently for the scoping 2026-04-13 22:37:17 +05:30
ArnabChatterjee20k 54fa813bc6 Implement presence expiry management and enhance user ID handling in presence actions. Add automated tests for expired presence deletion during maintenance. 2026-04-13 22:31:46 +05:30
ArnabChatterjee20k f9d0cb7daf Refactor presence tests to handle unauthorized access for client side operations and simplify presence setup logic. 2026-04-13 21:08:23 +05:30
ArnabChatterjee20k c5d5946fd2 updated scopes 2026-04-13 21:08:17 +05:30
ArnabChatterjee20k af41c64135 Merge remote-tracking branch 'origin/1.9.x' into presence-api 2026-04-13 20:47:32 +05:30
ArnabChatterjee20k 63afcad23d tests 2026-04-13 20:47:13 +05:30
ArnabChatterjee20k eb76c31fdd updated aciton source 2026-04-13 20:46:30 +05:30
ArnabChatterjee20k 51fd96f4bf added crud operation 2026-04-13 20:28:40 +05:30
ArnabChatterjee20k 14351c7338 updated model 2026-04-13 20:28:31 +05:30
ArnabChatterjee20k 01c6cc86ce Presence api response models 2026-04-13 20:28:23 +05:30
ArnabChatterjee20k c6f16cde4a updated schema 2026-04-13 11:44:01 +05:30
Jake Barnby 8412790436 Merge remote-tracking branch 'origin/1.9.x' into feat-dedicated-db 2026-04-09 14:02:49 +12:00
Jake Barnby fdca14e659 (feat): precomputed guage write in stats resources 2026-04-07 23:45:59 +12:00
Jake Barnby e3b1eead55 Merge remote-tracking branch 'origin/1.9.x' into feat-dedicated-db
# Conflicts:
#	app/config/collections/projects.php
2026-04-07 23:24:08 +12:00
Jake Barnby 11fc170d72 (docs): Add documentsdb and vectorsdb reference docs 2026-03-25 22:53:03 +13:00
Jake Barnby 467b2a9281 (feat): Increase databases.database attribute size to 2048 2026-03-25 18:39:49 +13:00
ArnabChatterjee20k 3b2c8a4d36 added schema for presence api 2026-03-24 14:29:36 +05:30
310 changed files with 10040 additions and 2963 deletions
+2 -1
View File
@@ -442,7 +442,8 @@ jobs:
VCS,
Messaging,
Migrations,
Project
Project,
Presences
]
include:
- service: Databases
+2 -56
View File
@@ -2,17 +2,10 @@
require_once __DIR__ . '/init.php';
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Certificate as CertificatePublisher;
use Appwrite\Event\Publisher\Delete as DeletePublisher;
use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Event\Publisher\StatsResources as StatsResourcesPublisher;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Platform\Appwrite;
use Appwrite\Runtimes\Runtimes;
use Appwrite\Usage\Context as UsageContext;
use Appwrite\Utopia\Database\Documents\User;
use Executor\Executor;
use Swoole\Runtime;
use Swoole\Timer;
use Utopia\Cache\Adapter\Pool as CachePool;
@@ -26,17 +19,12 @@ use Utopia\Database\Adapter\Pool as DatabasePool;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\DI\Container;
use Utopia\DSN\DSN;
use Utopia\Logger\Log;
use Utopia\Platform\Service;
use Utopia\Pools\Group;
use Utopia\Queue\Broker\Pool as BrokerPool;
use Utopia\Queue\Publisher;
use Utopia\Queue\Queue;
use Utopia\Registry\Registry;
use Utopia\System\System;
use Utopia\Telemetry\Adapter\None as NoTelemetry;
use function Swoole\Coroutine\run;
@@ -47,6 +35,7 @@ Config::setParam('runtimes', (new Runtimes('v5'))->getAll(supported: false));
require_once __DIR__ . '/controllers/general.php';
global $register;
global $container;
$platform = new Appwrite();
$args = $_SERVER['argv'] ?? [];
@@ -58,7 +47,6 @@ if (! isset($args[0])) {
}
$taskName = $args[0];
$container = new Container();
$cli = new CLI(new Generic(), $_SERVER['argv'] ?? [], $container);
$platform->setCli($cli);
@@ -131,10 +119,6 @@ $container->set('dbForPlatform', function ($pools, $cache, $authorization) {
return $dbForPlatform;
}, ['pools', 'cache', 'authorization']);
$container->set('console', function () {
return new Document(Config::getParam('console'));
}, []);
$container->set(
'isResourceBlocked',
fn () => fn (Document $project, string $resourceType, ?string $resourceId) => false,
@@ -251,44 +235,10 @@ $container->set('getLogsDB', function (Group $pools, Cache $cache, Authorization
return $database;
};
}, ['pools', 'cache', 'authorization']);
$container->set('publisher', function (Group $pools) {
return new BrokerPool(publisher: $pools->get('publisher'));
}, ['pools']);
$container->set('publisherDatabases', function (BrokerPool $publisher) {
return $publisher;
}, ['publisher']);
$container->set('publisherFunctions', function (BrokerPool $publisher) {
return $publisher;
}, ['publisher']);
$container->set('publisherMigrations', function (BrokerPool $publisher) {
return $publisher;
}, ['publisher']);
$container->set('publisherMessaging', function (BrokerPool $publisher) {
return $publisher;
}, ['publisher']);
$container->set('usage', function () {
return new UsageContext();
}, []);
$container->set('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher(
$publisher,
new Queue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForCertificates', fn (Publisher $publisher) => new CertificatePublisher(
$publisher,
new Queue(System::getEnv('_APP_CERTIFICATES_QUEUE_NAME', Event::CERTIFICATES_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']);
$container->set('publisherForFunctions', fn (Publisher $publisher) => new FunctionPublisher(
$publisher,
new Queue(System::getEnv('_APP_FUNCTIONS_QUEUE_NAME', Event::FUNCTIONS_QUEUE_NAME), 'utopia-queue', Event::FUNCTIONS_QUEUE_TTL)
), ['publisher']);
$container->set('publisherForDeletes', fn (Publisher $publisher) => new DeletePublisher(
$publisher,
new Queue(System::getEnv('_APP_DELETE_QUEUE_NAME', Event::DELETE_QUEUE_NAME))
), ['publisher']);
$container->set('logError', function (Registry $register) {
return function (Throwable $error, string $namespace, string $action) use ($register) {
Console::error('[Error] Timestamp: ' . date('c', time()));
@@ -341,14 +291,10 @@ $container->set('logError', function (Registry $register) {
};
}, ['register']);
$container->set('executor', fn () => new Executor(), []);
$container->set('bus', function (Registry $register) use ($container) {
return $register->get('bus')->setResolver(fn (string $name) => $container->get($name));
}, ['register']);
$container->set('telemetry', fn () => new NoTelemetry(), []);
$exitCode = 0;
$cli
+186
View File
@@ -841,6 +841,28 @@ return [
'array' => true,
'filters' => [],
],
[
'$id' => ID::custom('providerBranches'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 128,
'signed' => true,
'required' => false,
'default' => [],
'array' => true,
'filters' => [],
],
[
'$id' => ID::custom('providerPaths'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 128,
'signed' => true,
'required' => false,
'default' => [],
'array' => true,
'filters' => [],
],
],
'indexes' => [
[
@@ -1320,6 +1342,28 @@ return [
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('providerBranches'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 128,
'signed' => true,
'required' => false,
'default' => [],
'array' => true,
'filters' => [],
],
[
'$id' => ID::custom('providerPaths'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 128,
'signed' => true,
'required' => false,
'default' => [],
'array' => true,
'filters' => [],
],
],
'indexes' => [
[
@@ -2754,4 +2798,146 @@ return [
],
],
],
// Naming it presenceLogs as later it might be only be used as a presence events table only and not for the actual presence
'presenceLogs' => [
'$collection' => ID::custom(Database::METADATA),
'$id' => ID::custom('presenceLogs'),
'name' => 'Presence Logs',
'attributes' => [
[
'$id' => ID::custom('userInternalId'),
'type' => Database::VAR_ID,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('userId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('expiresAt'),
'type' => Database::VAR_DATETIME,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['datetime'],
],
[
'$id' => ID::custom('status'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('source'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('hostname'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('metadata'),
'type' => Database::VAR_TEXT,
'format' => '',
'size' => 65535,
'signed' => true,
'required' => false,
'default' => new \stdClass(),
'array' => false,
'filters' => ['json'],
],
[
'$id' => ID::custom('permissionsHash'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 32,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
'$id' => ID::custom('_unique_userId'),
'type' => Database::INDEX_UNIQUE,
'attributes' => ['userId'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC]
],
[
'$id' => ID::custom('_key_userInternal'),
'type' => Database::INDEX_KEY,
'attributes' => ['userInternalId'],
'orders' => [Database::ORDER_ASC]
],
[
'$id' => ID::custom('_key_expiresAt'),
'type' => Database::INDEX_KEY,
'attributes' => ['expiresAt'],
'lengths' => [],
'orders' => [Database::ORDER_ASC]
],
[
'$id' => ID::custom('_key_status'),
'type' => Database::INDEX_KEY,
'attributes' => ['status'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC]
],
[
'$id' => ID::custom('_key_source'),
'type' => Database::INDEX_KEY,
'attributes' => ['source'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC]
],
[
'$id' => ID::custom('_key_source_status'),
'type' => Database::INDEX_KEY,
'attributes' => ['source', 'status']
],
[
'$id' => ID::custom('_key_permissionsHash'),
'type' => Database::INDEX_KEY,
'attributes' => ['permissionsHash']
]
]
]
];
+1
View File
@@ -22,6 +22,7 @@ return [
'X-Appwrite-Locale',
'X-Appwrite-Mode',
'X-Appwrite-JWT',
'X-Appwrite-Organization',
'X-Appwrite-Response-Format',
'X-Appwrite-Timeout',
'X-Appwrite-ID',
+12
View File
@@ -725,6 +725,18 @@ return [
'code' => 404,
],
/** Presence */
Exception::PRESENCE_NOT_FOUND => [
'name' => Exception::PRESENCE_NOT_FOUND,
'description' => 'Presence with the requested ID could not be found.',
'code' => 404,
],
Exception::PRESENCE_ALREADY_EXISTS => [
'name' => Exception::PRESENCE_ALREADY_EXISTS,
'description' => 'Presence with the requested ID \'%s\' already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
'code' => 409,
],
/** Databases */
Exception::DATABASE_NOT_FOUND => [
'name' => Exception::DATABASE_NOT_FOUND,
+70 -14
View File
@@ -14,7 +14,11 @@ return [
'name' => 'Analog',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
'runtimes' => $templateRuntimes['NODE'],
'runtimes' => array_merge(
$templateRuntimes['NODE'],
$templateRuntimes['BUN'],
$templateRuntimes['DENO']
),
'bundleCommand' => 'bash /usr/local/server/helpers/analog/bundle.sh',
'envCommand' => 'source /usr/local/server/helpers/analog/env.sh',
'adapters' => [
@@ -40,7 +44,11 @@ return [
'name' => 'Angular',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
'runtimes' => $templateRuntimes['NODE'],
'runtimes' => array_merge(
$templateRuntimes['NODE'],
$templateRuntimes['BUN'],
$templateRuntimes['DENO']
),
'bundleCommand' => 'bash /usr/local/server/helpers/angular/bundle.sh',
'envCommand' => 'source /usr/local/server/helpers/angular/env.sh',
'adapters' => [
@@ -66,7 +74,11 @@ return [
'name' => 'Next.js',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
'runtimes' => $templateRuntimes['NODE'],
'runtimes' => array_merge(
$templateRuntimes['NODE'],
$templateRuntimes['BUN'],
$templateRuntimes['DENO']
),
'bundleCommand' => 'bash /usr/local/server/helpers/next-js/bundle.sh',
'envCommand' => 'source /usr/local/server/helpers/next-js/env.sh',
'adapters' => [
@@ -91,7 +103,11 @@ return [
'name' => 'React',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
'runtimes' => $templateRuntimes['NODE'],
'runtimes' => array_merge(
$templateRuntimes['NODE'],
$templateRuntimes['BUN'],
$templateRuntimes['DENO']
),
'adapters' => [
'static' => [
'key' => 'static',
@@ -108,7 +124,11 @@ return [
'name' => 'Nuxt',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
'runtimes' => $templateRuntimes['NODE'],
'runtimes' => array_merge(
$templateRuntimes['NODE'],
$templateRuntimes['BUN'],
$templateRuntimes['DENO']
),
'bundleCommand' => 'bash /usr/local/server/helpers/nuxt/bundle.sh',
'envCommand' => 'source /usr/local/server/helpers/nuxt/env.sh',
'adapters' => [
@@ -133,7 +153,11 @@ return [
'name' => 'Vue.js',
'screenshotSleep' => 5000,
'buildRuntime' => 'node-22',
'runtimes' => $templateRuntimes['NODE'],
'runtimes' => array_merge(
$templateRuntimes['NODE'],
$templateRuntimes['BUN'],
$templateRuntimes['DENO']
),
'adapters' => [
'static' => [
'key' => 'static',
@@ -150,7 +174,11 @@ return [
'name' => 'SvelteKit',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
'runtimes' => $templateRuntimes['NODE'],
'runtimes' => array_merge(
$templateRuntimes['NODE'],
$templateRuntimes['BUN'],
$templateRuntimes['DENO']
),
'bundleCommand' => 'bash /usr/local/server/helpers/sveltekit/bundle.sh',
'envCommand' => 'source /usr/local/server/helpers/sveltekit/env.sh',
'adapters' => [
@@ -175,7 +203,11 @@ return [
'name' => 'Astro',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
'runtimes' => $templateRuntimes['NODE'],
'runtimes' => array_merge(
$templateRuntimes['NODE'],
$templateRuntimes['BUN'],
$templateRuntimes['DENO']
),
'bundleCommand' => 'bash /usr/local/server/helpers/astro/bundle.sh',
'envCommand' => 'source /usr/local/server/helpers/astro/env.sh',
'adapters' => [
@@ -200,7 +232,11 @@ return [
'name' => 'TanStack Start',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
'runtimes' => $templateRuntimes['NODE'],
'runtimes' => array_merge(
$templateRuntimes['NODE'],
$templateRuntimes['BUN'],
$templateRuntimes['DENO']
),
'bundleCommand' => 'bash /usr/local/server/helpers/tanstack-start/bundle.sh',
'envCommand' => 'source /usr/local/server/helpers/tanstack-start/env.sh',
'adapters' => [
@@ -225,7 +261,11 @@ return [
'name' => 'Remix',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
'runtimes' => $templateRuntimes['NODE'],
'runtimes' => array_merge(
$templateRuntimes['NODE'],
$templateRuntimes['BUN'],
$templateRuntimes['DENO']
),
'bundleCommand' => 'bash /usr/local/server/helpers/remix/bundle.sh',
'envCommand' => 'source /usr/local/server/helpers/remix/env.sh',
'adapters' => [
@@ -250,7 +290,11 @@ return [
'name' => 'Lynx',
'screenshotSleep' => 5000,
'buildRuntime' => 'node-22',
'runtimes' => $templateRuntimes['NODE'],
'runtimes' => array_merge(
$templateRuntimes['NODE'],
$templateRuntimes['BUN'],
$templateRuntimes['DENO']
),
'adapters' => [
'static' => [
'key' => 'static',
@@ -284,7 +328,11 @@ return [
'name' => 'React Native',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
'runtimes' => $templateRuntimes['NODE'],
'runtimes' => array_merge(
$templateRuntimes['NODE'],
$templateRuntimes['BUN'],
$templateRuntimes['DENO']
),
'adapters' => [
'static' => [
'key' => 'static',
@@ -301,7 +349,11 @@ return [
'name' => 'Vite',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
'runtimes' => $templateRuntimes['NODE'],
'runtimes' => array_merge(
$templateRuntimes['NODE'],
$templateRuntimes['BUN'],
$templateRuntimes['DENO']
),
'adapters' => [
'static' => [
'key' => 'static',
@@ -317,7 +369,11 @@ return [
'name' => 'Other',
'screenshotSleep' => 3000,
'buildRuntime' => 'node-22',
'runtimes' => $templateRuntimes['NODE'],
'runtimes' => array_merge(
$templateRuntimes['NODE'],
$templateRuntimes['BUN'],
$templateRuntimes['DENO']
),
'adapters' => [
'static' => [
'key' => 'static',
+5 -1
View File
@@ -12,6 +12,8 @@ $member = [
'account',
'teams.read',
'teams.write',
'presences.read',
'presences.write',
'documents.read',
'documents.write',
'rows.read',
@@ -47,6 +49,8 @@ $admins = [
'buckets.write',
'users.read',
'users.write',
'presences.read',
'presences.write',
'databases.read',
'databases.write',
'collections.read',
@@ -146,7 +150,7 @@ return [
'label' => 'Owner',
'scopes' => \array_merge($member, $admins),
],
User::ROLE_APPS => [
User::ROLE_KEYS => [
'label' => 'Applications',
'scopes' => ['global', 'health.read', 'graphql'],
],
+8 -2
View File
@@ -4,17 +4,23 @@
return [
"projects.read" => [
"description" => 'Access to read organization\'s projects',
"description" => 'Access to read organization projects',
"category" => "Projects",
],
"projects.write" => [
"description" =>
"Access to create, update, and delete projects in organization",
"Access to create, update, and delete organization projects",
"category" => "Projects",
],
"devKeys.read" => [
"description" => 'Access to read project\'s development keys',
"category" => "Other",
"deprecated" => true,
],
"devKeys.write" => [
"description" =>
"Access to create, update, and delete project\'s development keys",
"category" => "Other",
"deprecated" => true,
],
];
+8
View File
@@ -379,4 +379,12 @@ return [
'description' => 'Access to delete reports under Advisor service.',
'category' => 'Advisor',
],
'presences.read' => [
'description' => 'Access to read your project\'s presences',
'category' => 'Presences',
],
'presences.write' => [
'description' => 'Access to create, update, and delete your project\'s presences',
'category' => 'Presences',
],
];
+3 -2
View File
@@ -67,6 +67,7 @@ use Utopia\Emails\Email;
use Utopia\Emails\Validator\Email as EmailValidator;
use Utopia\Http\Http;
use Utopia\Locale\Locale;
use Utopia\Platform\Enum;
use Utopia\Storage\Validator\FileName;
use Utopia\System\System;
use Utopia\Validator;
@@ -1243,7 +1244,7 @@ Http::get('/v1/account/sessions/oauth2/:provider')
))
->label('abuse-limit', 50)
->label('abuse-key', 'ip:{ip}')
->param('provider', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders')), true), 'OAuth2 Provider. Currently, supported providers are: ' . \implode(', ', \array_keys(\array_filter(Config::getParam('oAuthProviders'), fn ($node) => (!$node['mock'])))) . '.')
->param('provider', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders')), true), 'OAuth2 Provider. Currently, supported providers are: ' . \implode(', ', \array_keys(\array_filter(Config::getParam('oAuthProviders'), fn ($node) => (!$node['mock'])))) . '.', enum: new Enum(name: 'OAuthProvider', exclude: ['mock', 'mock-unverified']))
->param('success', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['redirectValidator'])
->param('failure', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['redirectValidator'])
->param('scopes', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true)
@@ -2009,7 +2010,7 @@ Http::get('/v1/account/tokens/oauth2/:provider')
))
->label('abuse-limit', 50)
->label('abuse-key', 'ip:{ip}')
->param('provider', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders')), true), 'OAuth2 Provider. Currently, supported providers are: ' . \implode(', ', \array_keys(\array_filter(Config::getParam('oAuthProviders'), fn ($node) => (!$node['mock'])))) . '.')
->param('provider', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders')), true), 'OAuth2 Provider. Currently, supported providers are: ' . \implode(', ', \array_keys(\array_filter(Config::getParam('oAuthProviders'), fn ($node) => (!$node['mock'])))) . '.', enum: new Enum(name: 'OAuthProvider', exclude: ['mock', 'mock-unverified']))
->param('success', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['redirectValidator'])
->param('failure', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['redirectValidator'])
->param('scopes', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true)
+1 -1
View File
@@ -39,7 +39,7 @@ Http::init()
if (
array_key_exists('graphql', $project->getAttribute('apis', []))
&& !$project->getAttribute('apis', [])['graphql']
&& !($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles()))
&& !($user->isPrivileged($authorization->getRoles()) || $user->isKey($authorization->getRoles()))
) {
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
}
+5 -4
View File
@@ -47,6 +47,7 @@ use Utopia\Database\Validator\UID;
use Utopia\Emails\Validator\Email;
use Utopia\Http\Http;
use Utopia\Locale\Locale;
use Utopia\Platform\Enum;
use Utopia\System\System;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
@@ -364,7 +365,7 @@ Http::post('/v1/messaging/providers/smtp')
->param('port', 587, new Range(1, 65535), 'The default SMTP server port.', true)
->param('username', '', new Text(0), 'Authentication username.', true)
->param('password', '', new Text(0), 'Authentication password.', true)
->param('encryption', '', new WhiteList(['none', 'ssl', 'tls']), 'Encryption type. Can be omitted, \'ssl\', or \'tls\'', true)
->param('encryption', '', new WhiteList(['none', 'ssl', 'tls']), 'Encryption type. Can be omitted, \'ssl\', or \'tls\'', true, enum: new Enum(name: 'SmtpEncryption'))
->param('autoTLS', true, new Boolean(), 'Enable SMTP AutoTLS feature.', true)
->param('mailer', '', new Text(0), 'The value to use for the X-Mailer header.', true)
->param('fromName', '', new Text(128, 0), 'Sender Name.', true)
@@ -1602,7 +1603,7 @@ Http::patch('/v1/messaging/providers/smtp/:providerId')
->param('port', null, new Nullable(new Range(1, 65535)), 'SMTP port.', true)
->param('username', '', new Text(0), 'Authentication username.', true)
->param('password', '', new Text(0), 'Authentication password.', true)
->param('encryption', '', new WhiteList(['none', 'ssl', 'tls']), 'Encryption type. Can be \'ssl\' or \'tls\'', true)
->param('encryption', '', new WhiteList(['none', 'ssl', 'tls']), 'Encryption type. Can be \'ssl\' or \'tls\'', true, enum: new Enum(name: 'SmtpEncryption'))
->param('autoTLS', null, new Nullable(new Boolean()), 'Enable SMTP AutoTLS feature.', true)
->param('mailer', '', new Text(0), 'The value to use for the X-Mailer header.', true)
->param('fromName', '', new Text(128), 'Sender Name.', true)
@@ -3501,7 +3502,7 @@ Http::post('/v1/messaging/messages/push')
->param('scheduledAt', null, new Nullable(new DatetimeValidator(requireDateInFuture: true)), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->param('contentAvailable', false, new Boolean(), 'If set to true, the notification will be delivered in the background. Available only for iOS Platform.', true)
->param('critical', false, new Boolean(), 'If set to true, the notification will be marked as critical. This requires the app to have the critical notification entitlement. Available only for iOS Platform.', true)
->param('priority', 'high', new WhiteList(['normal', 'high']), 'Set the notification priority. "normal" will consider device state and may not deliver notifications immediately. "high" will always attempt to immediately deliver the notification.', true)
->param('priority', 'high', new WhiteList(['normal', 'high']), 'Set the notification priority. "normal" will consider device state and may not deliver notifications immediately. "high" will always attempt to immediately deliver the notification.', true, enum: new Enum(name: 'MessagePriority'))
->inject('queueForEvents')
->inject('dbForProject')
->inject('dbForPlatform')
@@ -4388,7 +4389,7 @@ Http::patch('/v1/messaging/messages/push/:messageId')
->param('scheduledAt', null, new Nullable(new DatetimeValidator(requireDateInFuture: true)), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true)
->param('contentAvailable', null, new Nullable(new Boolean()), 'If set to true, the notification will be delivered in the background. Available only for iOS Platform.', true)
->param('critical', null, new Nullable(new Boolean()), 'If set to true, the notification will be marked as critical. This requires the app to have the critical notification entitlement. Available only for iOS Platform.', true)
->param('priority', null, new Nullable(new WhiteList(['normal', 'high'])), 'Set the notification priority. "normal" will consider device battery state and may send notifications later. "high" will always attempt to immediately deliver the notification.', true)
->param('priority', null, new Nullable(new WhiteList(['normal', 'high'])), 'Set the notification priority. "normal" will consider device battery state and may send notifications later. "high" will always attempt to immediately deliver the notification.', true, enum: new Enum(name: 'MessagePriority'))
->inject('queueForEvents')
->inject('dbForProject')
->inject('dbForPlatform')
+8 -1
View File
@@ -10,6 +10,7 @@ use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Datetime as DateTimeValidator;
use Utopia\Http\Http;
use Utopia\Platform\Enum;
use Utopia\Validator\WhiteList;
Http::get('/v1/project/usage')
@@ -31,7 +32,13 @@ Http::get('/v1/project/usage')
))
->param('startDate', '', new DateTimeValidator(), 'Starting date for the usage')
->param('endDate', '', new DateTimeValidator(), 'End date for the usage')
->param('period', '1d', new WhiteList(['1h', '1d']), 'Period used', true)
->param('period', '1d', new WhiteList(['1h', '1d']), 'Period used', true, enum: new Enum(
name: 'ProjectUsageRange',
map: [
'1h' => 'OneHour',
'1d' => 'OneDay',
]
))
->inject('response')
->inject('project')
->inject('dbForProject')
+38 -5
View File
@@ -63,6 +63,7 @@ use Utopia\Emails\Email;
use Utopia\Emails\Validator\Email as EmailValidator;
use Utopia\Http\Http;
use Utopia\Locale\Locale;
use Utopia\Platform\Enum;
use Utopia\System\System;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Assoc;
@@ -432,7 +433,7 @@ Http::post('/v1/users/sha')
->param('userId', '', fn (Database $dbForProject) => new CustomId(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'User 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, ['dbForProject'])
->param('email', '', new EmailValidator(), 'User email.')
->param('password', '', new Password(), 'User password hashed using SHA.')
->param('passwordVersion', '', new WhiteList(['sha1', 'sha224', 'sha256', 'sha384', 'sha512/224', 'sha512/256', 'sha512', 'sha3-224', 'sha3-256', 'sha3-384', 'sha3-512']), "Optional SHA version used to hash password. Allowed values are: 'sha1', 'sha224', 'sha256', 'sha384', 'sha512/224', 'sha512/256', 'sha512', 'sha3-224', 'sha3-256', 'sha3-384', 'sha3-512'", true)
->param('passwordVersion', '', new WhiteList(['sha1', 'sha224', 'sha256', 'sha384', 'sha512/224', 'sha512/256', 'sha512', 'sha3-224', 'sha3-256', 'sha3-384', 'sha3-512']), "Optional SHA version used to hash password. Allowed values are: 'sha1', 'sha224', 'sha256', 'sha384', 'sha512/224', 'sha512/256', 'sha512', 'sha3-224', 'sha3-256', 'sha3-384', 'sha3-512'", true, enum: new Enum(name: 'PasswordHash'))
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
->inject('project')
@@ -606,7 +607,7 @@ Http::post('/v1/users/:userId/targets')
))
->param('targetId', '', fn (Database $dbForProject) => new CustomId(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Target 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, ['dbForProject'])
->param('userId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'User ID.', false, ['dbForProject'])
->param('providerType', '', new WhiteList([MESSAGE_TYPE_EMAIL, MESSAGE_TYPE_SMS, MESSAGE_TYPE_PUSH]), 'The target provider type. Can be one of the following: `email`, `sms` or `push`.')
->param('providerType', '', new WhiteList([MESSAGE_TYPE_EMAIL, MESSAGE_TYPE_SMS, MESSAGE_TYPE_PUSH]), 'The target provider type. Can be one of the following: `email`, `sms` or `push`.', enum: new Enum(name: 'MessagingProviderType'))
->param('identifier', '', new Text(Database::LENGTH_KEY), 'The target identifier (token, email, phone etc.)')
->param('providerId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Provider ID. Message will be sent to this target from the specified provider ID. If no provider ID is set the first setup provider will be used.', true, ['dbForProject'])
->param('name', '', new Text(128), 'Target name. Max length: 128 chars. For example: My Awesome App Galaxy S23.', true)
@@ -732,6 +733,13 @@ Http::get('/v1/users')
$cursor->setValue($cursorDocument);
}
$skipFilters = ['subQueryAuthenticators', 'subQuerySessions', 'subQueryTokens', 'subQueryChallenges', 'subQueryMemberships'];
$selects = Query::getByType($queries, [Query::TYPE_SELECT]);
if (empty($selects)) {
$skipFilters[] = 'subQueryTargets';
}
$users = [];
$total = 0;
@@ -744,7 +752,32 @@ Http::get('/v1/users')
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
}, ['subQueryAuthenticators', 'subQuerySessions', 'subQueryTokens', 'subQueryChallenges', 'subQueryMemberships']);
}, $skipFilters);
if (empty($selects) && !empty($users)) {
$sequences = [];
foreach ($users as $user) {
$sequences[] = $user->getSequence();
}
try {
$targets = $dbForProject->getAuthorization()->skip(fn () => $dbForProject->find('targets', [
Query::equal('userInternalId', $sequences),
Query::limit(PHP_INT_MAX),
]));
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$targetsByUser = [];
foreach ($targets as $target) {
$targetsByUser[$target->getAttribute('userInternalId')][] = $target;
}
foreach ($users as $user) {
$user->setAttribute('targets', $targetsByUser[$user->getSequence()] ?? []);
}
}
$response->dynamic(new Document([
'users' => $users,
@@ -2286,7 +2319,7 @@ Http::delete('/v1/users/:userId/mfa/authenticators/:type')
)
])
->param('userId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'User ID.', false, ['dbForProject'])
->param('type', null, new WhiteList([Type::TOTP]), 'Type of authenticator.')
->param('type', null, new WhiteList([Type::TOTP]), 'Type of authenticator.', enum: new Enum(name: 'AuthenticatorType'))
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
@@ -2799,7 +2832,7 @@ Http::get('/v1/users/usage')
)
]
))
->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true)
->param('range', '30d', new WhiteList(['24h', '30d', '90d'], true), 'Date range.', true, enum: new Enum(name: 'UsageRange'))
->inject('response')
->inject('dbForProject')
->inject('authorization')
+10 -33
View File
@@ -184,31 +184,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
/** @var Database $dbForProject */
$dbForProject = $getProjectDB($project);
if (!empty($rule->getAttribute('deploymentId', ''))) {
$deployment = $authorization->skip(fn () => $dbForProject->getDocument('deployments', $rule->getAttribute('deploymentId')));
} else {
// 1.6.x DB schema compatibility
// TODO: Make sure deploymentId is never empty, and remove this code
// Check if site or function; should never be site, but better safe than sorry
// Attempts to use attribute from both schemas (1.6 and 1.7)
$resourceType = $rule->getAttribute('deploymentResourceType', $rule->getAttribute('resourceType', ''));
// ID of site or function
$resourceId = $rule->getAttribute('deploymentResourceId', '');
// Document of site or function
$resource = $resourceType === 'function' ?
$authorization->skip(fn () => $dbForProject->getDocument('functions', $resourceId)) :
$authorization->skip(fn () => $dbForProject->getDocument('sites', $resourceId));
// ID of active deployments
// Attempts to use attribute from both schemas (1.6 and 1.7)
$activeDeploymentId = $resource->getAttribute('deploymentId', $resource->getAttribute('deployment', ''));
// Get deployment document, as intended originally
$deployment = $authorization->skip(fn () => $dbForProject->getDocument('deployments', $activeDeploymentId));
}
$deployment = $authorization->skip(fn () => $dbForProject->getDocument('deployments', $rule->getAttribute('deploymentId')));
if ($deployment->isEmpty()) {
$resourceType = $rule->getAttribute('deploymentResourceType', '');
@@ -859,7 +835,8 @@ Http::init()
->inject('authorization')
->inject('publisherForDeletes')
->inject('executionsRetentionCount')
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, Reader $geodb, Event $queueForEvents, Bus $bus, Executor $executor, array $platform, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeletePublisher $publisherForDeletes, int $executionsRetentionCount) {
->inject('params')
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, Reader $geodb, Event $queueForEvents, Bus $bus, Executor $executor, array $platform, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeletePublisher $publisherForDeletes, int $executionsRetentionCount, array $params) {
/*
* Appwrite Router
*/
@@ -868,14 +845,14 @@ Http::init()
// Only run Router when external domain
if (!\in_array($hostname, $platformHostnames) || !empty($previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $publisherForDeletes, $executionsRetentionCount)) {
$utopia->getRoute()?->label('router', true);
$utopia->match($request)?->route->label('router', true);
}
}
/*
* Request format
*/
$route = $utopia->getRoute();
$route = $utopia->match($request)?->route;
$request->setRoute($route);
if ($route === null) {
@@ -900,7 +877,7 @@ Http::init()
}
if (version_compare($requestFormat, '1.8.0', '<')) {
$dbForProject = $getProjectDB($project);
$request->addFilter(new RequestV20($dbForProject, $route->getPathValues($request)));
$request->addFilter(new RequestV20($dbForProject, $params));
}
if (version_compare($requestFormat, '1.9.0', '<')) {
$request->addFilter(new RequestV21());
@@ -1178,7 +1155,7 @@ Http::options()
// Only run Router when external domain
if (!in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $publisherForDeletes, $executionsRetentionCount)) {
$utopia->getRoute()?->label('router', true);
$utopia->match($request)?->route->label('router', true);
}
}
@@ -1213,7 +1190,7 @@ Http::error()
->inject('authorization')
->action(function (Throwable $error, Http $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, Bus $bus, Document $devKey, Authorization $authorization) {
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
$route = $utopia->getRoute();
$route = $utopia->match($request)?->route;
$class = \get_class($error);
$code = $error->getCode();
$message = $error->getMessage();
@@ -1579,7 +1556,7 @@ Http::get('/robots.txt')
$response->text($template->render(false));
} else {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $publisherForDeletes, $executionsRetentionCount)) {
$utopia->getRoute()?->label('router', true);
$utopia->match($request)?->route->label('router', true);
}
}
});
@@ -1613,7 +1590,7 @@ Http::get('/humans.txt')
$response->text($template->render(false));
} else {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $publisherForDeletes, $executionsRetentionCount)) {
$utopia->getRoute()?->label('router', true);
$utopia->match($request)?->route->label('router', true);
}
}
});
+3 -4
View File
@@ -13,6 +13,7 @@ use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Validator\UID;
use Utopia\Http\Http;
use Utopia\Http\Route;
use Utopia\Locale\Locale;
use Utopia\System\System;
use Utopia\Validator\Text;
@@ -283,13 +284,11 @@ Http::get('/v1/mock/github/callback')
Http::shutdown()
->groups(['mock'])
->inject('utopia')
->inject('response')
->inject('request')
->action(function (Http $utopia, Response $response, Request $request) {
->inject('route')
->action(function (Response $response, Route $route) {
$result = [];
$route = $utopia->getRoute();
$path = APP_STORAGE_CACHE . '/tests.json';
$tests = (\file_exists($path)) ? \json_decode(\file_get_contents($path), true) : [];
+24 -50
View File
@@ -4,7 +4,6 @@ use Appwrite\Auth\Key;
use Appwrite\Auth\MFA\Type\TOTP;
use Appwrite\Bus\Events\RequestCompleted;
use Appwrite\Event\Context\Audit as AuditContext;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Message\Audit as AuditMessage;
use Appwrite\Event\Message\Func as FunctionMessage;
@@ -38,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\Http\Route;
use Utopia\Span\Span;
use Utopia\System\System;
use Utopia\Telemetry\Adapter as Telemetry;
@@ -86,7 +86,7 @@ $parseLabel = function (string $label, array $responsePayload, array $requestPar
Http::init()
->groups(['api'])
->inject('utopia')
->inject('route')
->inject('request')
->inject('dbForPlatform')
->inject('dbForProject')
@@ -99,11 +99,7 @@ Http::init()
->inject('team')
->inject('apiKey')
->inject('authorization')
->action(function (Http $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, AuditContext $auditContext, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization) {
$route = $utopia->getRoute();
if ($route === null) {
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND);
}
->action(function (Route $route, Request $request, Database $dbForPlatform, Database $dbForProject, AuditContext $auditContext, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization) {
/**
* Handle user authentication and session validation.
@@ -179,8 +175,8 @@ Http::init()
$role = $apiKey->getRole();
$scopes = $apiKey->getScopes();
// Handle special app role case
if ($apiKey->getRole() === User::ROLE_APPS) {
// Handle special key role case
if ($apiKey->getRole() === User::ROLE_KEYS) {
// Disable authorization checks for project API keys
// Dynamic supported for backwards compatibility
if (($apiKey->getType() === API_KEY_STANDARD || $apiKey->getType() === API_KEY_EPHEMERAL || $apiKey->getType() === 'dynamic') && $apiKey->getProjectId() === $project->getId()) {
@@ -190,7 +186,7 @@ Http::init()
$user = new User([
'$id' => '',
'status' => true,
'type' => ACTIVITY_TYPE_KEY_PROJECT,
'type' => ACTOR_TYPE_KEY_PROJECT,
'email' => 'app.' . $project->getId() . '@service.' . $request->getHostname(),
'password' => '',
'name' => $apiKey->getName(),
@@ -262,9 +258,9 @@ Http::init()
$userClone = clone $user;
$userClone->setAttribute('type', match ($apiKey->getType()) {
API_KEY_STANDARD => ACTIVITY_TYPE_KEY_PROJECT,
API_KEY_ACCOUNT => ACTIVITY_TYPE_KEY_ACCOUNT,
default => ACTIVITY_TYPE_KEY_ORGANIZATION,
API_KEY_STANDARD => ACTOR_TYPE_KEY_PROJECT,
API_KEY_ACCOUNT => ACTOR_TYPE_KEY_ACCOUNT,
default => ACTOR_TYPE_KEY_ORGANIZATION,
});
$auditContext->user = $userClone;
}
@@ -429,7 +425,7 @@ Http::init()
if (
array_key_exists($namespace, $project->getAttribute('services', []))
&& ! $project->getAttribute('services', [])[$namespace]
&& ! ($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles()))
&& ! ($user->isPrivileged($authorization->getRoles()) || $user->isKey($authorization->getRoles()))
) {
throw new Exception(Exception::GENERAL_SERVICE_DISABLED);
}
@@ -439,7 +435,7 @@ Http::init()
if (
array_key_exists('rest', $project->getAttribute('apis', []))
&& ! $project->getAttribute('apis', [])['rest']
&& ! ($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles()))
&& ! ($user->isPrivileged($authorization->getRoles()) || $user->isKey($authorization->getRoles()))
) {
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
}
@@ -478,7 +474,7 @@ Http::init()
Http::init()
->groups(['api'])
->inject('utopia')
->inject('route')
->inject('request')
->inject('response')
->inject('project')
@@ -486,21 +482,16 @@ Http::init()
->inject('timelimit')
->inject('devKey')
->inject('authorization')
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, callable $timelimit, Document $devKey, Authorization $authorization) {
->action(function (Route $route, Request $request, Response $response, Document $project, User $user, callable $timelimit, Document $devKey, Authorization $authorization) {
$response->setUser($user);
$request->setUser($user);
$roles = $authorization->getRoles();
$shouldCheckAbuse = System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled'
&& ! $user->isApp($roles)
&& ! $user->isKey($roles)
&& ! $user->isPrivileged($roles)
&& $devKey->isEmpty();
$route = $utopia->getRoute();
if ($route === null) {
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND);
}
$abuseKeyLabel = $route->getLabel('abuse-key', 'url:{url},ip:{ip}');
$abuseKeyLabel = (! is_array($abuseKeyLabel)) ? [$abuseKeyLabel] : $abuseKeyLabel;
$closestLimit = null;
@@ -557,14 +548,13 @@ Http::init()
Http::init()
->groups(['api'])
->inject('utopia')
->inject('route')
->inject('request')
->inject('response')
->inject('project')
->inject('user')
->inject('queueForEvents')
->inject('auditContext')
->inject('queueForDatabase')
->inject('usage')
->inject('publisherForFunctions')
->inject('dbForProject')
@@ -576,17 +566,12 @@ Http::init()
->inject('platform')
->inject('authorization')
->inject('cacheControlForStorage')
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, EventDatabase $queueForDatabase, Context $usage, FunctionPublisher $publisherForFunctions, Database $dbForProject, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Telemetry $telemetry, array $platform, Authorization $authorization, callable $cacheControlForStorage) {
->action(function (Route $route, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Context $usage, FunctionPublisher $publisherForFunctions, Database $dbForProject, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Telemetry $telemetry, array $platform, Authorization $authorization, callable $cacheControlForStorage) {
$response->setUser($user);
$request->setUser($user);
$route = $utopia->getRoute();
if ($route === null) {
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND);
}
$path = $route->getMatchedPath();
$path = $route->getPath();
$databaseType = match (true) {
str_contains($path, '/documentsdb') => DATABASE_TYPE_DOCUMENTSDB,
str_contains($path, '/vectorsdb') => DATABASE_TYPE_VECTORSDB,
@@ -617,20 +602,16 @@ Http::init()
$userClone = clone $user;
// $user doesn't support `type` and can cause unintended effects.
if (empty($user->getAttribute('type'))) {
$userClone->setAttribute('type', $mode === APP_MODE_ADMIN ? ACTIVITY_TYPE_ADMIN : ACTIVITY_TYPE_USER);
$userClone->setAttribute('type', $mode === APP_MODE_ADMIN ? ACTOR_TYPE_ADMIN : ACTOR_TYPE_USER);
}
$auditContext->user = $userClone;
}
/* Auto-set projects */
$queueForDatabase->setProject($project);
$useCache = $route->getLabel('cache', false);
$storageCacheOperationsCounter = $telemetry->createCounter('storage.cache.operations.load');
if ($useCache) {
$route = $utopia->match($request);
$roles = $authorization->getRoles();
$isAppUser = $user->isApp($roles);
$isAppUser = $user->isKey($roles);
$isImageTransformation = $route->getPath() === '/v1/storage/buckets/:bucketId/files/:fileId/preview';
$isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && ! $user->isPrivileged($roles);
@@ -766,12 +747,11 @@ Http::init()
*/
Http::shutdown()
->groups(['session'])
->inject('utopia')
->inject('request')
->inject('response')
->inject('project')
->inject('dbForProject')
->action(function (Http $utopia, Request $request, Response $response, Document $project, Database $dbForProject) {
->action(function (Request $request, Response $response, Document $project, Database $dbForProject) {
$sessionLimit = $project->getAttribute('auths', [])['maxSessions'] ?? 0;
if ($sessionLimit === 0) {
@@ -805,7 +785,7 @@ Http::shutdown()
Http::shutdown()
->groups(['api'])
->inject('utopia')
->inject('route')
->inject('request')
->inject('response')
->inject('project')
@@ -815,7 +795,6 @@ Http::shutdown()
->inject('publisherForAudits')
->inject('usage')
->inject('publisherForUsage')
->inject('queueForDatabase')
->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('queueForRealtime')
@@ -826,7 +805,7 @@ Http::shutdown()
->inject('bus')
->inject('apiKey')
->inject('mode')
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Audit $publisherForAudits, Context $usage, UsagePublisher $publisherForUsage, EventDatabase $queueForDatabase, FunctionPublisher $publisherForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor, Bus $bus, ?Key $apiKey, string $mode) use ($parseLabel) {
->action(function (Route $route, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Audit $publisherForAudits, Context $usage, UsagePublisher $publisherForUsage, FunctionPublisher $publisherForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor, Bus $bus, ?Key $apiKey, string $mode) use ($parseLabel) {
$responsePayload = $response->getPayload();
@@ -882,7 +861,6 @@ Http::shutdown()
}
}
$route = $utopia->getRoute();
$requestParams = $route->getParamsValues();
/**
@@ -935,7 +913,7 @@ Http::shutdown()
$userClone = clone $user;
// $user doesn't support `type` and can cause unintended effects.
if (empty($user->getAttribute('type'))) {
$userClone->setAttribute('type', $mode === APP_MODE_ADMIN ? ACTIVITY_TYPE_ADMIN : ACTIVITY_TYPE_USER);
$userClone->setAttribute('type', $mode === APP_MODE_ADMIN ? ACTOR_TYPE_ADMIN : ACTOR_TYPE_USER);
}
$auditContext->user = $userClone;
} elseif ($auditContext->user === null || $auditContext->user->isEmpty()) {
@@ -950,7 +928,7 @@ Http::shutdown()
$user = new User([
'$id' => '',
'status' => true,
'type' => ACTIVITY_TYPE_GUEST,
'type' => ACTOR_TYPE_GUEST,
'email' => 'guest.' . $project->getId() . '@service.' . $request->getHostname(),
'password' => '',
'name' => 'Guest',
@@ -973,10 +951,6 @@ Http::shutdown()
$publisherForAudits->enqueue(AuditMessage::fromContext($auditContext));
}
if (! empty($queueForDatabase->getType())) {
$queueForDatabase->trigger();
}
// Cache label
$useCache = $route->getLabel('cache', false);
if ($useCache) {
+4 -5
View File
@@ -9,6 +9,7 @@ use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Http\Http;
use Utopia\Http\Route;
use Utopia\System\System;
Http::init()
@@ -32,13 +33,13 @@ Http::init()
Http::init()
->groups(['auth'])
->inject('utopia')
->inject('route')
->inject('request')
->inject('project')
->inject('geodb')
->inject('user')
->inject('authorization')
->action(function (Http $utopia, Request $request, Document $project, Reader $geodb, User $user, Authorization $authorization) {
->action(function (Route $route, Request $request, Document $project, Reader $geodb, User $user, Authorization $authorization) {
$denylist = System::getEnv('_APP_CONSOLE_COUNTRIES_DENYLIST', '');
if (!empty($denylist && $project->getId() === 'console')) {
$countries = explode(',', $denylist);
@@ -49,10 +50,8 @@ Http::init()
}
}
$route = $utopia->match($request);
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
$isAppUser = $user->isApp($authorization->getRoles());
$isAppUser = $user->isKey($authorization->getRoles());
if ($isAppUser || $isPrivilegedUser) { // Skip limits for app and console devs
return;
+2 -2
View File
@@ -539,7 +539,7 @@ $swoole->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files, $swoo
$app->run($request, $response);
$route = $app->getRoute();
$route = $app->match($request)?->route;
Span::add('http.path', $route?->getPath() ?? 'unknown');
} catch (\Throwable $th) {
Span::error($th);
@@ -555,7 +555,7 @@ $swoole->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files, $swoo
// All good, user is optional information for logger
}
$route = $app->getRoute();
$route = $app->match($request)?->route;
$log = $app->context()->get("log");
+8 -7
View File
@@ -159,14 +159,14 @@ const SESSION_PROVIDER_TOKEN = 'token';
const SESSION_PROVIDER_SERVER = 'server';
/**
* Activity associated with user or the app.
* Actor that performed the request (user, admin, guest, or API key).
*/
const ACTIVITY_TYPE_USER = 'user';
const ACTIVITY_TYPE_ADMIN = 'admin';
const ACTIVITY_TYPE_GUEST = 'guest';
const ACTIVITY_TYPE_KEY_PROJECT = 'keyProject';
const ACTIVITY_TYPE_KEY_ACCOUNT = 'keyAccount';
const ACTIVITY_TYPE_KEY_ORGANIZATION = 'keyOrganization';
const ACTOR_TYPE_USER = 'user';
const ACTOR_TYPE_ADMIN = 'admin';
const ACTOR_TYPE_GUEST = 'guest';
const ACTOR_TYPE_KEY_PROJECT = 'keyProject';
const ACTOR_TYPE_KEY_ACCOUNT = 'keyAccount';
const ACTOR_TYPE_KEY_ORGANIZATION = 'keyOrganization';
/**
* MFA
@@ -394,6 +394,7 @@ const METRIC_NETWORK_OUTBOUND = 'network.outbound';
const METRIC_MAU = 'users.mau';
const METRIC_DAU = 'users.dau';
const METRIC_WAU = 'users.wau';
const METRIC_USERS_PRESENCE = 'users.presence';
const METRIC_WEBHOOKS = 'webhooks';
const METRIC_PLATFORMS = 'platforms';
const METRIC_PROVIDERS = 'providers';
+6 -1
View File
@@ -174,6 +174,7 @@ use Appwrite\Utopia\Response\Model\PolicySessionInvalidation;
use Appwrite\Utopia\Response\Model\PolicySessionLimit;
use Appwrite\Utopia\Response\Model\PolicyUserLimit;
use Appwrite\Utopia\Response\Model\Preferences;
use Appwrite\Utopia\Response\Model\Presence;
use Appwrite\Utopia\Response\Model\Project;
use Appwrite\Utopia\Response\Model\ProjectAuthMethod;
use Appwrite\Utopia\Response\Model\ProjectProtocol;
@@ -214,6 +215,7 @@ use Appwrite\Utopia\Response\Model\UsageDocumentsDB;
use Appwrite\Utopia\Response\Model\UsageDocumentsDBs;
use Appwrite\Utopia\Response\Model\UsageFunction;
use Appwrite\Utopia\Response\Model\UsageFunctions;
use Appwrite\Utopia\Response\Model\UsagePresence;
use Appwrite\Utopia\Response\Model\UsageProject;
use Appwrite\Utopia\Response\Model\UsageSite;
use Appwrite\Utopia\Response\Model\UsageSites;
@@ -237,6 +239,7 @@ Response::setModel(new ErrorDev());
// Lists
Response::setModel(new BaseList('Rows List', Response::MODEL_ROW_LIST, 'rows', Response::MODEL_ROW));
Response::setModel(new BaseList('Documents List', Response::MODEL_DOCUMENT_LIST, 'documents', Response::MODEL_DOCUMENT));
Response::setModel(new BaseList('Presences List', Response::MODEL_PRESENCE_LIST, 'presences', Response::MODEL_PRESENCE));
Response::setModel(new BaseList('Tables List', Response::MODEL_TABLE_LIST, 'tables', Response::MODEL_TABLE));
Response::setModel(new BaseList('Collections List', Response::MODEL_COLLECTION_LIST, 'collections', Response::MODEL_COLLECTION));
Response::setModel(new BaseList('Databases List', Response::MODEL_DATABASE_LIST, 'databases', Response::MODEL_DATABASE));
@@ -263,7 +266,7 @@ Response::setModel(new BaseList('Frameworks List', Response::MODEL_FRAMEWORK_LIS
Response::setModel(new BaseList('Runtimes List', Response::MODEL_RUNTIME_LIST, 'runtimes', Response::MODEL_RUNTIME));
Response::setModel(new BaseList('Deployments List', Response::MODEL_DEPLOYMENT_LIST, 'deployments', Response::MODEL_DEPLOYMENT));
Response::setModel(new BaseList('Executions List', Response::MODEL_EXECUTION_LIST, 'executions', Response::MODEL_EXECUTION));
Response::setModel(new BaseList('Projects List', Response::MODEL_PROJECT_LIST, 'projects', Response::MODEL_PROJECT, true, false));
Response::setModel(new BaseList('Projects List', Response::MODEL_PROJECT_LIST, 'projects', Response::MODEL_PROJECT, true, true));
Response::setModel(new BaseList('Webhooks List', Response::MODEL_WEBHOOK_LIST, 'webhooks', Response::MODEL_WEBHOOK, true, true));
Response::setModel(new BaseList('API Keys List', Response::MODEL_KEY_LIST, 'keys', Response::MODEL_KEY, true, true));
Response::setModel(new BaseList('Dev Keys List', Response::MODEL_DEV_KEY_LIST, 'devKeys', Response::MODEL_DEV_KEY, true, false));
@@ -361,6 +364,7 @@ Response::setModel(new Index());
Response::setModel(new ColumnIndex());
Response::setModel(new Row());
Response::setModel(new ModelDocument());
Response::setModel(new Presence());
Response::setModel(new Log());
Response::setModel(new User());
Response::setModel(new AlgoMd5());
@@ -489,6 +493,7 @@ Response::setModel(new UsageDatabase());
Response::setModel(new UsageTable());
Response::setModel(new UsageCollection());
Response::setModel(new UsageUsers());
Response::setModel(new UsagePresence());
Response::setModel(new UsageStorage());
Response::setModel(new UsageBuckets());
Response::setModel(new UsageFunctions());
+15 -1
View File
@@ -240,6 +240,12 @@ $register->set('pools', function () {
'multiple' => true,
'schemes' => ['redis'],
],
'lock' => [
'type' => 'lock',
'dsns' => $fallbackForRedis,
'multiple' => false,
'schemes' => ['redis'],
],
];
$maxConnections = (int) System::getEnv('_APP_CONNECTIONS_MAX', 151);
@@ -332,7 +338,13 @@ $register->set('pools', function () {
$poolAdapter = System::getEnv('_APP_POOL_ADAPTER', default: 'stack') === 'swoole' ? new SwoolePool() : new StackPool();
$pool = new Pool($poolAdapter, $name, $poolSize, function () use ($type, $resource, $dsn) {
// PubSub workers hold one long-lived subscribed connection and also need
// spare capacity for publishes from the same process.
$connectionPoolSize = $type === 'pubsub'
? max(2, $poolSize)
: $poolSize;
$pool = new Pool($poolAdapter, $name, $connectionPoolSize, function () use ($type, $resource, $dsn) {
// Get Adapter
switch ($type) {
case 'database':
@@ -369,6 +381,8 @@ $register->set('pools', function () {
}
return $adapter;
case 'lock':
return $resource();
default:
throw new Exception(Exception::GENERAL_SERVER_ERROR, "Server error: Missing adapter implementation.");
}
+99 -81
View File
@@ -4,6 +4,7 @@ use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Audit as AuditPublisher;
use Appwrite\Event\Publisher\Build as BuildPublisher;
use Appwrite\Event\Publisher\Certificate as CertificatePublisher;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Event\Publisher\Delete as DeletePublisher;
use Appwrite\Event\Publisher\Execution as ExecutionPublisher;
use Appwrite\Event\Publisher\Func as FunctionPublisher;
@@ -28,6 +29,7 @@ use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\DI\Container;
use Utopia\DSN\DSN;
use Utopia\Lock\Distributed;
use Utopia\Pools\Group;
use Utopia\Queue\Broker\Pool as BrokerPool;
use Utopia\Queue\Publisher;
@@ -51,6 +53,93 @@ global $register;
global $container;
$container = new Container();
$container->set('console', fn () => new Document(Config::getParam('console')), []);
$container->set('executor', fn () => new Executor(), []);
$container->set('telemetry', fn () => new NoTelemetry(), []);
$container->set('publisher', fn (Group $pools) => new BrokerPool(publisher: $pools->get('publisher')), ['pools']);
$container->set('publisherDatabases', fn (Publisher $publisher) => $publisher, ['publisher']);
$container->set('publisherFunctions', fn (Publisher $publisher) => $publisher, ['publisher']);
$container->set('publisherMigrations', fn (Publisher $publisher) => $publisher, ['publisher']);
$container->set('publisherMails', fn (Publisher $publisher) => $publisher, ['publisher']);
$container->set('publisherDeletes', fn (Publisher $publisher) => $publisher, ['publisher']);
$container->set('publisherMessaging', fn (Publisher $publisher) => $publisher, ['publisher']);
$container->set('publisherWebhooks', fn (Publisher $publisher) => $publisher, ['publisher']);
$container->set('publisherForAudits', fn (Publisher $publisher) => new AuditPublisher(
$publisher,
new Queue(System::getEnv('_APP_AUDITS_QUEUE_NAME', Event::AUDITS_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForCertificates', fn (Publisher $publisher) => new CertificatePublisher(
$publisher,
new Queue(System::getEnv('_APP_CERTIFICATES_QUEUE_NAME', Event::CERTIFICATES_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForScreenshots', fn (Publisher $publisher) => new ScreenshotPublisher(
$publisher,
new Queue(System::getEnv('_APP_SCREENSHOTS_QUEUE_NAME', Event::SCREENSHOTS_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher(
$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('publisherForFunctions', fn (Publisher $publisher) => new FunctionPublisher(
$publisher,
new Queue(System::getEnv('_APP_FUNCTIONS_QUEUE_NAME', Event::FUNCTIONS_QUEUE_NAME), 'utopia-queue', Event::FUNCTIONS_QUEUE_TTL)
), ['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']);
$container->set('publisherForBuilds', fn (Publisher $publisher) => new BuildPublisher(
$publisher,
new Queue(System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::BUILDS_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForDatabase', fn (Publisher $publisherDatabases) => new DatabasePublisher(
$publisherDatabases,
new Queue(System::getEnv('_APP_DATABASE_QUEUE_NAME', Event::DATABASE_QUEUE_NAME))
), ['publisherDatabases']);
$container->set('publisherForDeletes', fn (Publisher $publisher) => new DeletePublisher(
$publisher,
new Queue(System::getEnv('_APP_DELETE_QUEUE_NAME', Event::DELETE_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForMails', fn (Publisher $publisher) => new MailPublisher(
$publisher,
new Queue(System::getEnv('_APP_MAILS_QUEUE_NAME', Event::MAILS_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForMessaging', fn (Publisher $publisher) => new MessagingPublisher(
$publisher,
new Queue(System::getEnv('_APP_MESSAGING_QUEUE_NAME', Event::MESSAGING_QUEUE_NAME))
), ['publisher']);
$container->set('logger', function ($register) {
return $register->get('logger');
}, ['register']);
@@ -65,79 +154,6 @@ $container->set('localeCodes', function () {
return array_map(fn ($locale) => $locale['code'], Config::getParam('locale-codes', []));
});
// Queues - shared infrastructure (stateless pool wrappers)
$container->set('publisher', function (Group $pools) {
return new BrokerPool(publisher: $pools->get('publisher'));
}, ['pools']);
$container->set('publisherDatabases', function (Publisher $publisher) {
return $publisher;
}, ['publisher']);
$container->set('publisherFunctions', function (Publisher $publisher) {
return $publisher;
}, ['publisher']);
$container->set('publisherMigrations', function (Publisher $publisher) {
return $publisher;
}, ['publisher']);
$container->set('publisherMails', function (Publisher $publisher) {
return $publisher;
}, ['publisher']);
$container->set('publisherDeletes', function (Publisher $publisher) {
return $publisher;
}, ['publisher']);
$container->set('publisherMessaging', function (Publisher $publisher) {
return $publisher;
}, ['publisher']);
$container->set('publisherWebhooks', function (Publisher $publisher) {
return $publisher;
}, ['publisher']);
$container->set('publisherForAudits', fn (Publisher $publisher) => new AuditPublisher(
$publisher,
new Queue(System::getEnv('_APP_AUDITS_QUEUE_NAME', Event::AUDITS_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForCertificates', fn (Publisher $publisher) => new CertificatePublisher(
$publisher,
new Queue(System::getEnv('_APP_CERTIFICATES_QUEUE_NAME', Event::CERTIFICATES_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForScreenshots', fn (Publisher $publisher) => new ScreenshotPublisher(
$publisher,
new Queue(System::getEnv('_APP_SCREENSHOTS_QUEUE_NAME', Event::SCREENSHOTS_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher(
$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('publisherForFunctions', fn (Publisher $publisher) => new FunctionPublisher(
$publisher,
new Queue(System::getEnv('_APP_FUNCTIONS_QUEUE_NAME', Event::FUNCTIONS_QUEUE_NAME), 'utopia-queue', Event::FUNCTIONS_QUEUE_TTL)
), ['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']);
$container->set('publisherForBuilds', fn (Publisher $publisher) => new BuildPublisher(
$publisher,
new Queue(System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::BUILDS_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForDeletes', fn (Publisher $publisher) => new DeletePublisher(
$publisher,
new Queue(System::getEnv('_APP_DELETE_QUEUE_NAME', Event::DELETE_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForMails', fn (Publisher $publisher) => new MailPublisher(
$publisher,
new Queue(System::getEnv('_APP_MAILS_QUEUE_NAME', Event::MAILS_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForMessaging', fn (Publisher $publisher) => new MessagingPublisher(
$publisher,
new Queue(System::getEnv('_APP_MESSAGING_QUEUE_NAME', Event::MESSAGING_QUEUE_NAME))
), ['publisher']);
/**
* Platform configuration
@@ -146,10 +162,6 @@ $container->set('platform', function () {
return Config::getParam('platform', []);
}, []);
$container->set('console', function () {
return new Document(Config::getParam('console'));
}, []);
$container->set('authorization', function () {
return new Authorization();
}, []);
@@ -208,8 +220,6 @@ $container->set('getLogsDB', function (Group $pools, Cache $cache, Authorization
};
}, ['pools', 'cache', 'authorization']);
$container->set('telemetry', fn () => new NoTelemetry());
$container->set('cache', function (Group $pools, Telemetry $telemetry) {
$list = Config::getParam('pools-cache', []);
$adapters = [];
@@ -243,6 +253,16 @@ $container->set('redis', function () {
return $redis;
});
$container->set('locks', function (Group $pools) {
return function (string $key, int $ttl, callable $callback, float $timeout = 0.0) use ($pools): mixed {
return $pools->get('lock')->use(function (\Redis $redis) use ($key, $ttl, $callback, $timeout) {
$lock = new Distributed($redis, $key, ttl: $ttl);
return $lock->withLock($callback, timeout: $timeout);
});
};
}, ['pools']);
$container->set('timelimit', function (\Redis $redis) {
return function (string $key, int $limit, int $time) use ($redis) {
return new TimeLimitRedis($key, $limit, $time, $redis);
@@ -400,5 +420,3 @@ $container->set(
'isResourceBlocked',
fn () => fn (Document $project, string $resourceType, ?string $resourceId) => false
);
$container->set('executor', fn () => new Executor());
+2 -4
View File
@@ -5,7 +5,6 @@ use Ahc\Jwt\JWTException;
use Appwrite\Auth\Key;
use Appwrite\Databases\TransactionState;
use Appwrite\Event\Context\Audit as AuditContext;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Message\Func as FunctionMessage;
use Appwrite\Event\Publisher\Func as FunctionPublisher;
@@ -107,7 +106,6 @@ return function (Container $context): void {
});
// Per-request queue resources (stateful, accumulate event data during request)
$context->set('queueForDatabase', fn (Publisher $publisher) => new EventDatabase($publisher), ['publisher']);
$context->set('queueForEvents', fn (Publisher $publisher) => new Event($publisher), ['publisher']);
$context->set('queueForWebhooks', fn (Publisher $publisher) => new Webhook($publisher), ['publisher']);
$context->set('queueForRealtime', fn () => new Realtime(), []);
@@ -598,7 +596,7 @@ return function (Container $context): void {
// These endpoints moved from /v1/projects/:projectId/<resource> to /v1/<resource>
// When accessed via the old alias path, extract projectId from the URI
$deprecatedProjectPathPrefix = '/v1/projects/';
$route = $utopia->match($request);
$route = $utopia->match($request)?->route;
if (!empty($route)) {
$isDeprecatedAlias = \str_starts_with($request->getURI(), $deprecatedProjectPathPrefix) &&
!\str_starts_with($route->getPath(), $deprecatedProjectPathPrefix);
@@ -1095,7 +1093,7 @@ return function (Container $context): void {
if ($project->getId() !== 'console') {
$teamInternalId = $project->getAttribute('teamInternalId', '');
} else {
$route = $utopia->match($request);
$route = $utopia->match($request)?->route;
$path = ! empty($route) ? $route->getPath() : $request->getURI();
$orgHeader = $request->getHeader('x-appwrite-organization', '');
if (str_starts_with($path, '/v1/projects/:projectId')) {
-11
View File
@@ -1,8 +1,6 @@
<?php
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Event\Realtime;
use Appwrite\Event\Webhook;
use Appwrite\Usage\Context;
@@ -22,7 +20,6 @@ use Utopia\DSN\DSN;
use Utopia\Logger\Log;
use Utopia\Pools\Group;
use Utopia\Queue\Publisher;
use Utopia\Queue\Queue;
use Utopia\Registry\Registry;
use Utopia\Storage\Device\Telemetry as TelemetryDevice;
use Utopia\System\System;
@@ -327,10 +324,6 @@ return function (Container $container): void {
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', 1209600)); // 14 days
}, []);
$container->set('queueForDatabase', function (Publisher $publisher) {
return new EventDatabase($publisher);
}, ['publisher']);
$container->set('queueForEvents', function (Publisher $publisher) {
return new Event($publisher);
}, ['publisher']);
@@ -339,10 +332,6 @@ return function (Container $container): void {
return new Webhook($publisher);
}, ['publisher']);
$container->set('publisherForFunctions', fn (Publisher $publisher) => new FunctionPublisher(
$publisher,
new Queue(System::getEnv('_APP_FUNCTIONS_QUEUE_NAME', Event::FUNCTIONS_QUEUE_NAME), 'utopia-queue', Event::FUNCTIONS_QUEUE_TTL)
), ['publisher']);
$container->set('queueForRealtime', function () {
return new Realtime();
}, []);
+299 -315
View File
@@ -1,10 +1,20 @@
<?php
use Appwrite\Event\Event as QueueEvent;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Event\Realtime as QueueRealtime;
use Appwrite\Extend\Exception;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Messaging\Adapter\Realtime;
use Appwrite\Network\Validator\Origin;
use Appwrite\Presences\State as PresenceState;
use Appwrite\PubSub\Adapter\Pool as PubSubPool;
use Appwrite\Realtime\Message\Dispatcher as MessageDispatcher;
use Appwrite\Realtime\Message\Handlers\Authentication as AuthenticationHandler;
use Appwrite\Realtime\Message\Handlers\Ping as PingHandler;
use Appwrite\Realtime\Message\Handlers\Presence as PresenceHandler;
use Appwrite\Realtime\Message\Handlers\Subscribe as SubscribeHandler;
use Appwrite\Realtime\Message\Handlers\Unsubscribe as UnsubscribeHandler;
use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
@@ -16,9 +26,6 @@ use Swoole\Table;
use Swoole\Timer;
use Utopia\Abuse\Abuse;
use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis;
use Utopia\Auth\Hashes\Sha;
use Utopia\Auth\Proofs\Token;
use Utopia\Auth\Store;
use Utopia\Cache\Adapter\Pool as CachePool;
use Utopia\Cache\Adapter\Sharding;
use Utopia\Cache\Cache;
@@ -28,7 +35,9 @@ use Utopia\Database\Adapter\Pool as DatabasePool;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Exception\Authorization as AuthorizationException;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Exception\Timeout as TimeoutException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
@@ -37,6 +46,8 @@ use Utopia\DI\Container;
use Utopia\DSN\DSN;
use Utopia\Logger\Log;
use Utopia\Pools\Group;
use Utopia\Queue\Broker\Pool as BrokerPool;
use Utopia\Queue\Queue;
use Utopia\Registry\Registry;
use Utopia\Span\Span;
use Utopia\System\System;
@@ -67,6 +78,38 @@ set_exception_handler(function (\Throwable $e) {
));
});
global $container;
if (!$container->has('pools')) {
$container->set('pools', function ($register) {
return $register->get('pools');
}, ['register']);
}
if (!$container->has('publisherForUsage')) {
$container->set('publisherForUsage', function (Group $pools): UsagePublisher {
$statsUsageConnection = System::getEnv('_APP_CONNECTIONS_QUEUE_STATS_USAGE', '');
$publisherPoolName = 'publisher';
if (!empty($statsUsageConnection)) {
try {
$pools->get('publisher_' . $statsUsageConnection);
$publisherPoolName = 'publisher_' . $statsUsageConnection;
} catch (Throwable) {
// Fallback to default publisher pool when custom one is unavailable.
}
}
return new UsagePublisher(
new BrokerPool(publisher: $pools->get($publisherPoolName)),
new Queue(System::getEnv(
'_APP_STATS_USAGE_QUEUE_NAME',
QueueEvent::STATS_USAGE_QUEUE_NAME
))
);
}, ['pools']);
}
// Allows overriding
if (!function_exists('getConsoleDB')) {
function getConsoleDB(): Database
@@ -234,6 +277,7 @@ if (!function_exists('getRealtime')) {
}
}
if (!function_exists('getTelemetry')) {
function getTelemetry(int $workerId): Utopia\Telemetry\Adapter
{
@@ -247,18 +291,58 @@ if (!function_exists('getTelemetry')) {
}
}
if (!function_exists('getQueueForEvents')) {
function getQueueForEvents(): QueueEvent
{
$ctx = Coroutine::getContext();
if (!isset($ctx['queueForEvents'])) {
global $register;
/** @var Group $pools */
$pools = $register->get('pools');
$ctx['queueForEvents'] = new QueueEvent(new BrokerPool(
publisher: $pools->get('publisher')
));
}
return $ctx['queueForEvents'];
}
}
if (!function_exists('getQueueForRealtime')) {
function getQueueForRealtime(): QueueRealtime
{
$ctx = Coroutine::getContext();
if (!isset($ctx['queueForRealtime'])) {
$ctx['queueForRealtime'] = new QueueRealtime();
}
return $ctx['queueForRealtime'];
}
}
if (!function_exists('triggerStats')) {
function triggerStats(array $event, string $projectId): void
{
}
}
global $container;
$container->set('pools', function ($register) {
return $register->get('pools');
}, ['register']);
if (!function_exists('checkForProjectUsage')) {
function checkForProjectUsage(Document $project): void
{
}
}
$realtime = getRealtime();
$presenceState = new PresenceState();
$messageDispatcher = (new MessageDispatcher())
->addHandler(new PingHandler())
->addHandler(new AuthenticationHandler())
->addHandler(new SubscribeHandler())
->addHandler(new UnsubscribeHandler())
->addHandler(new PresenceHandler());
/**
* Table for statistics across all workers.
@@ -292,7 +376,16 @@ if (!function_exists('logError')) {
$logger = $register->get('realtimeLogger');
if ($logger && !$error instanceof Exception) {
// Match HTTP semantics (app/controllers/general.php): AppwriteException uses its
// configured publish flag; everything else publishes only for code 0 or >= 500.
// Without this, expected client errors (e.g. Utopia DB Authorization) hit Sentry.
if ($error instanceof AppwriteException) {
$publish = $error->isPublishable();
} else {
$publish = $error->getCode() === 0 || $error->getCode() >= 500;
}
if ($logger && $publish) {
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
$log = new Log();
@@ -612,6 +705,16 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
}
}
// Strip deleted presences from in-memory connection state so onClose doesn't
// re-fire delete events for rows already removed via HTTP DELETE.
$deletedPresenceId = Realtime::extractDeletedPresenceId($event);
if ($deletedPresenceId !== null) {
$realtime->removePresenceFromConnections(
(string) ($event['project'] ?? ''),
$deletedPresenceId,
);
}
$receivers = $realtime->getSubscribers($event);
if (System::getEnv('_APP_ENV', 'production') === 'development' && !empty($receivers)) {
@@ -755,7 +858,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
$websocketEnabled = $apis['websocket'] ?? $apis['realtime'] ?? true;
if (
!$websocketEnabled
&& !($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles()))
&& !($user->isPrivileged($authorization->getRoles()) || $user->isKey($authorization->getRoles()))
) {
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
}
@@ -898,6 +1001,16 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
$success = true;
} catch (Throwable $th) {
Span::error($th);
// Convert known Utopia DB exceptions to AppwriteException so isPublishable()
// suppresses expected client errors (permission denied, query timeout) from Sentry.
if ($th instanceof AuthorizationException) {
$th = new AppwriteException(AppwriteException::USER_UNAUTHORIZED, previous: $th);
} elseif ($th instanceof TimeoutException) {
$th = new AppwriteException(AppwriteException::DATABASE_TIMEOUT, previous: $th);
}
logError($th, 'realtime', project: $project, user: $logUser, authorization: $authorization);
// Handle SQL error code is 'HY000'
@@ -933,7 +1046,6 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
Console::error('[Error] Code: ' . $response['data']['code']);
Console::error('[Error] Message: ' . $response['data']['message']);
}
Span::error($th);
} finally {
Span::add('realtime.success', $success);
Span::add('realtime.response_code', $responseCode);
@@ -951,15 +1063,12 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
}
});
$server->onMessage(function (int $connection, string $message) use ($server, $realtime, $containerId, $register) {
$server->onMessage(function (int $connection, string $message) use ($container, $server, $realtime, $containerId, $register, $presenceState, $messageDispatcher) {
$project = null;
$authorization = null;
$projectId = $realtime->connections[$connection]['projectId'] ?? null;
$rawSize = \strlen($message);
$messageType = 'invalid';
$subscriptionDelta = 0;
$subscriptionsRequested = 0;
$subscriptionsRemoved = 0;
$outboundBytes = 0;
$responseCode = 200;
$success = false;
@@ -972,17 +1081,44 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
try {
$response = new Response(new SwooleResponse());
// Get authorization from connection (stored during onOpen)
$authorization = $realtime->connections[$connection]['authorization'] ?? null;
if ($authorization === null) {
$authorization = new Authorization();
// Build a fresh Authorization per message. The connection-scoped instance is shared
// across coroutines, and `Authorization::skip()` toggles instance state — concurrent
// messages on the same connection (e.g. `authentication` + `presence` sent back-to-back)
// would interleave skip/restore and leak permission checks into supposedly-skipped lookups.
$authorization = new Authorization();
$connectionAuthorization = $realtime->connections[$connection]['authorization'] ?? null;
if ($connectionAuthorization !== null) {
foreach ($connectionAuthorization->getRoles() as $role) {
$authorization->addRole($role);
}
}
$connectionRoles = $realtime->connections[$connection]['roles'] ?? [];
foreach ($connectionRoles as $role) {
if ($authorization->hasRole($role)) {
continue;
}
$authorization->addRole($role);
}
$database = getConsoleDB();
$database->setAuthorization($authorization);
if (!empty($projectId) && $projectId !== 'console') {
$project = $authorization->skip(fn () => $database->getDocument('projects', $projectId));
// Negative-cache race: if any prior code path queried projects:$projectId
// before this project existed (e.g. a router probe during connection
// setup), the Database's shared cache may hold an empty result. Try the
// cached read first, and only purge/retry when the first lookup reports
// not-found so the shared cache remains effective for normal traffic.
try {
$project = $authorization->skip(fn () => $database->getDocument('projects', $projectId));
} catch (AppwriteException $e) {
if ($e->getCode() !== 404) {
throw $e;
}
$database->purgeCachedDocument('projects', $projectId);
$project = $authorization->skip(fn () => $database->getDocument('projects', $projectId));
}
$database = getProjectDB($project);
$database->setAuthorization($authorization);
@@ -990,6 +1126,10 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
$project = null;
}
if ($project !== null) {
checkForProjectUsage($project);
}
/*
* Abuse Check
*
@@ -1008,6 +1148,7 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
}
// Record realtime inbound bytes for this project
// not making this a part of the dispatcher as we need to get the inbound bytes as well even if we dont enter the dispatcher
if ($project !== null && !$project->isEmpty()) {
triggerStats([
METRIC_REALTIME_INBOUND => $rawSize,
@@ -1026,300 +1167,54 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Message type is not valid.');
}
// Ping does not require project context; other messages do (e.g. after unsubscribe during auth)
if (empty($projectId) && ($message['type'] ?? '') !== 'ping') {
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, 'Missing project context. Reconnect to the project first.');
// Child of the global container: per-message values like $connection and $project
// live on this scope so concurrent message coroutines don't clobber each other,
// while globally-registered services (pools, ...) remain reachable via the parent.
$messageContainer = new Container($container);
$messageContainer->set('connectionId', fn () => $connection);
$messageContainer->set('server', fn () => $server);
$messageContainer->set('realtime', fn () => $realtime);
$messageContainer->set('register', fn () => $register);
$messageContainer->set('response', fn () => $response);
$messageContainer->set('presenceState', fn () => $presenceState);
$messageContainer->set('database', fn () => $database);
$messageContainer->set('authorization', fn () => $authorization);
$messageContainer->set('project', fn () => $project);
$messageContainer->set('projectId', fn () => $projectId);
$messageContainer->set('queueForEvents', fn () => getQueueForEvents());
$messageContainer->set('queueForRealtime', fn () => getQueueForRealtime());
$responsePayload = $messageDispatcher->dispatch($messageContainer, $message);
if ($responsePayload !== null) {
$responseJson = json_encode($responsePayload);
if ($responseJson === false) {
throw new \RuntimeException(
'Failed to encode realtime response payload: ' . json_last_error_msg()
);
}
$server->send([$connection], $responseJson);
$bytes = \strlen($responseJson);
$outboundBytes += $bytes;
if ($project !== null && !$project->isEmpty()) {
triggerStats([METRIC_REALTIME_OUTBOUND => $bytes], $project->getId());
}
}
switch ($message['type']) {
case 'ping':
$pongPayloadJson = json_encode([
'type' => 'pong'
]);
$server->send([$connection], $pongPayloadJson);
$outboundBytes += \strlen($pongPayloadJson);
if ($project !== null && !$project->isEmpty()) {
$pongOutboundBytes = \strlen($pongPayloadJson);
if ($pongOutboundBytes > 0) {
triggerStats([
METRIC_REALTIME_OUTBOUND => $pongOutboundBytes,
], $project->getId());
}
}
break;
case 'authentication':
if (!array_key_exists('session', $message['data'])) {
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Payload is not valid.');
}
$store = new Store();
$store->decode($message['data']['session']);
/** @var User $user */
$user = $database->getDocument('users', $store->getProperty('id', ''));
/**
* TODO:
* Moving forward, we should try to use our dependency injection container
* to inject the proof for token.
* This way we will have one source of truth for the proof for token.
*/
$proofForToken = new Token();
$proofForToken->setHash(new Sha());
if (
empty($user->getId()) // Check a document has been found in the DB
|| !$user->sessionVerify($store->getProperty('secret', ''), $proofForToken) // Validate user has valid login token
) {
// cookie not valid
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Session is not valid.');
}
$roles = $user->getRoles($database->getAuthorization());
$authorization = $realtime->connections[$connection]['authorization'] ?? null;
$projectId = $realtime->connections[$connection]['projectId'] ?? null;
// Capture the pre-auth userId so we can rebind any account channels
// that were stored under it (e.g. guest who subscribed to `account`
// and now authenticates). unsubscribe() below clears the connection
// entry, so we must read it first.
$previousUserId = $realtime->connections[$connection]['userId'] ?? '';
$subscriptionsBefore = \count($realtime->getSubscriptionMetadata($connection));
$meta = $realtime->getSubscriptionMetadata($connection);
$realtime->unsubscribe($connection);
if (!empty($projectId)) {
foreach ($meta as $subscriptionId => $subscription) {
$queries = Query::parseQueries($subscription['queries'] ?? []);
$channels = Realtime::rebindAccountChannels(
$subscription['channels'] ?? [],
$previousUserId,
$user->getId()
);
$realtime->subscribe(
$projectId,
$connection,
$subscriptionId,
$roles,
$channels,
$queries,
$user->getId()
);
}
}
if ($authorization !== null) {
$realtime->connections[$connection]['authorization'] = $authorization;
}
$subscriptionsAfter = \count($realtime->getSubscriptionMetadata($connection));
$subscriptionDelta = $subscriptionsAfter - $subscriptionsBefore;
if ($subscriptionDelta !== 0) {
$register->get('telemetry.workerSubscriptionCounter')->add($subscriptionDelta, $register->get('telemetry.workerAttributes'));
}
$user = $response->output($user, Response::MODEL_ACCOUNT);
$authResponsePayloadJson = json_encode([
'type' => 'response',
'data' => [
'to' => 'authentication',
'success' => true,
'user' => $user
]
]);
$server->send([$connection], $authResponsePayloadJson);
$outboundBytes += \strlen($authResponsePayloadJson);
if ($project !== null && !$project->isEmpty()) {
$authOutboundBytes = \strlen($authResponsePayloadJson);
if ($authOutboundBytes > 0) {
triggerStats([
METRIC_REALTIME_OUTBOUND => $authOutboundBytes,
], $project->getId());
}
}
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 = [];
$subscriptionsBefore = \count($realtime->getSubscriptionMetadata($connection));
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());
}
$convertedChannels = \array_keys(Realtime::convertChannels($payload['channels'], $userId));
$parsedPayloads[] = [
'subscriptionId' => $subscriptionId,
'channels' => $payload['channels'],
'convertedChannels' => $convertedChannels,
'queries' => $convertedQueries,
];
}
foreach ($parsedPayloads as $parsedPayload) {
$subscriptionId = $parsedPayload['subscriptionId'];
$channels = $parsedPayload['convertedChannels'];
$queries = $parsedPayload['queries'];
$realtime->subscribe($projectId, $connection, $subscriptionId, $roles, $channels, $queries);
}
$subscriptionsAfter = \count($realtime->getSubscriptionMetadata($connection));
$subscriptionDelta = $subscriptionsAfter - $subscriptionsBefore;
$subscriptionsRequested = \count($parsedPayloads);
if ($subscriptionDelta !== 0) {
$register->get('telemetry.workerSubscriptionCounter')->add($subscriptionDelta, $register->get('telemetry.workerAttributes'));
}
$responsePayload = json_encode([
'type' => 'response',
'data' => [
'to' => 'subscribe',
'success' => true,
'subscriptions' => \array_map(function (array $parsedPayload) {
return [
'subscriptionId' => $parsedPayload['subscriptionId'],
'channels' => $parsedPayload['convertedChannels'],
'queries' => \array_map(fn ($q) => $q->toString(), $parsedPayload['queries']),
];
}, $parsedPayloads),
]
]);
$server->send([$connection], $responsePayload);
$outboundBytes += \strlen($responsePayload);
if ($project !== null && !$project->isEmpty()) {
$subscribeOutboundBytes = \strlen($responsePayload);
if ($subscribeOutboundBytes > 0) {
triggerStats([
METRIC_REALTIME_OUTBOUND => $subscribeOutboundBytes,
], $project->getId());
}
}
break;
case 'unsubscribe':
if (!\is_array($message['data']) || !\array_is_list($message['data'])) {
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Payload is not valid.');
}
$subscriptionsBefore = \count($realtime->getSubscriptionMetadata($connection));
// Validate every payload before executing any removal so an invalid entry
// later in the batch does not leave earlier entries half-applied on the server.
$validatedIds = [];
foreach ($message['data'] as $payload) {
if (
!\is_array($payload)
|| !\array_key_exists('subscriptionId', $payload)
|| !\is_string($payload['subscriptionId'])
|| $payload['subscriptionId'] === ''
) {
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Each unsubscribe payload must include a non-empty subscriptionId.');
}
$validatedIds[] = $payload['subscriptionId'];
}
$unsubscribeResults = [];
foreach ($validatedIds as $subscriptionId) {
$wasRemoved = $realtime->unsubscribeSubscription($connection, $subscriptionId);
$unsubscribeResults[] = [
'subscriptionId' => $subscriptionId,
'removed' => $wasRemoved,
];
}
$subscriptionsAfter = \count($realtime->getSubscriptionMetadata($connection));
$subscriptionDelta = $subscriptionsAfter - $subscriptionsBefore;
$subscriptionsRequested = \count($validatedIds);
$subscriptionsRemoved = \count(\array_filter($unsubscribeResults, fn (array $item) => $item['removed']));
if ($subscriptionDelta !== 0) {
$register->get('telemetry.workerSubscriptionCounter')->add($subscriptionDelta, $register->get('telemetry.workerAttributes'));
}
$unsubscribeResponsePayload = json_encode([
'type' => 'response',
'data' => [
'to' => 'unsubscribe',
'success' => true,
'subscriptions' => $unsubscribeResults,
],
]);
$server->send([$connection], $unsubscribeResponsePayload);
$outboundBytes += \strlen($unsubscribeResponsePayload);
if ($project !== null && !$project->isEmpty()) {
$unsubscribeOutboundBytes = \strlen($unsubscribeResponsePayload);
if ($unsubscribeOutboundBytes > 0) {
triggerStats([
METRIC_REALTIME_OUTBOUND => $unsubscribeOutboundBytes,
], $project->getId());
}
}
break;
default:
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Message type is not valid.');
}
$success = true;
} catch (Throwable $th) {
Span::error($th);
// Convert known Utopia DB exceptions to AppwriteException so isPublishable()
// suppresses expected client errors (permission denied, query timeout) from Sentry.
if ($th instanceof AuthorizationException) {
$th = new AppwriteException(AppwriteException::USER_UNAUTHORIZED, previous: $th);
} elseif ($th instanceof TimeoutException) {
$th = new AppwriteException(AppwriteException::DATABASE_TIMEOUT, previous: $th);
}
logError($th, 'realtimeMessage', project: $project, authorization: $authorization);
$code = $th->getCode();
if (!is_int($code)) {
@@ -1349,14 +1244,9 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
if ($th->getCode() === 1008) {
$server->close($connection, $th->getCode());
}
Span::error($th);
} finally {
Span::add('realtime.success', $success);
Span::add('realtime.response_code', $responseCode);
Span::add('realtime.subscription_delta', $subscriptionDelta);
Span::add('realtime.subscriptions_requested', $subscriptionsRequested);
Span::add('realtime.subscriptions_removed', $subscriptionsRemoved);
Span::add('realtime.subscribe.subscriptions_count', $subscriptionsRequested);
Span::add('realtime.outbound_bytes', $outboundBytes);
Span::add('project.id', $project?->getId() ?? $projectId);
Span::add('user.id', $realtime->connections[$connection]['userId'] ?? null);
@@ -1365,7 +1255,7 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
}
});
$server->onClose(function (int $connection) use ($realtime, $stats, $register) {
$server->onClose(function (int $connection) use ($realtime, $stats, $register, $container, $presenceState) {
$projectId = null;
$userId = null;
$subscriptionsBeforeClose = 0;
@@ -1390,6 +1280,100 @@ $server->onClose(function (int $connection) use ($realtime, $stats, $register) {
}
$projectId = $realtime->connections[$connection]['projectId'];
/** @var array<string, Document> $presencesById */
$presencesById = $realtime->connections[$connection]['presences'] ?? [];
if (
!empty($presencesById)
&& $projectId !== 'console'
) {
go(function () use ($presencesById, $projectId, $userId, $container, $presenceState): void {
// Fresh span: the parent realtime.close span finishes before this coroutine
Span::init('realtime.close.presenceCleanup');
Span::add('realtime.projectId', $projectId);
Span::add('realtime.presenceCount', \count($presencesById));
try {
$dbForPlatform = getConsoleDB();
$project = $dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
if ($project->isEmpty()) {
return;
}
$presenceIds = \array_keys($presencesById);
$presences = \array_values($presencesById);
$dbForProject = getProjectDB($project);
$user = new User([]);
if (!empty($userId)) {
try {
$fetched = $dbForProject->getAuthorization()->skip(
fn () => $dbForProject->getDocument('users', $userId)
);
if (!$fetched->isEmpty()) {
$user = new User($fetched->getArrayCopy());
}
} catch (Throwable) {
// Fall back to empty User if lookup fails.
}
}
/** @var UsagePublisher $publisherForUsage */
$publisherForUsage = $container->get('publisherForUsage');
/** @var array<string, true> $deletedIds */
$deletedIds = [];
try {
$deletionCount = $dbForProject->getAuthorization()->skip(
function () use ($dbForProject, $presenceIds, &$deletedIds): int {
return $dbForProject->deleteDocuments(
'presenceLogs',
[Query::equal('$id', $presenceIds)],
onNext: function (Document $deleted) use (&$deletedIds): void {
$deletedIds[$deleted->getId()] = true;
},
);
}
);
$presenceState->triggerUsage($publisherForUsage, $project, -$deletionCount);
} catch (Throwable $th) {
Span::error($th);
logError($th, 'realtimeOnClosePresenceDeletion', tags: [
'projectId' => $projectId,
'presences' => \count($presences)
]);
}
$queueForEvents = getQueueForEvents();
$queueForRealtime = getQueueForRealtime();
foreach ($presences as $presence) {
if (!isset($deletedIds[$presence->getId()])) {
continue;
}
try {
$presenceState->triggerEvent(
$queueForEvents,
$queueForRealtime,
$project,
$user,
'presences.[presenceId].delete',
$presence,
);
} catch (Throwable) {
// Swallow errors to avoid breaking disconnect cleanup
}
}
} catch (Throwable $th) {
Span::error($th);
logError($th, 'realtimeOnClosePresenceCleanup', tags: [
'projectId' => $projectId,
]);
} finally {
Span::current()?->finish();
}
});
}
triggerStats([
METRIC_REALTIME_CONNECTIONS => -1,
+6 -5
View File
@@ -54,28 +54,29 @@
"utopia-php/abuse": "1.3.*",
"utopia-php/agents": "1.2.*",
"utopia-php/analytics": "0.15.*",
"utopia-php/audit": "2.3.*",
"utopia-php/audit": "^2.4",
"utopia-php/auth": "0.5.*",
"utopia-php/cache": "^2.1",
"utopia-php/cache": "^3.0",
"utopia-php/cli": "0.23.*",
"utopia-php/compression": "0.1.*",
"utopia-php/config": "1.*",
"utopia-php/console": "0.1.*",
"utopia-php/database": "5.*",
"utopia-php/detector": "0.2.*",
"utopia-php/domains": "2.*",
"utopia-php/domains": "^2.1",
"utopia-php/emails": "0.7.*",
"utopia-php/dns": "1.7.*",
"utopia-php/dsn": "0.2.1",
"utopia-php/http": "^2.0@RC",
"utopia-php/http": "2.0.0-rc3",
"utopia-php/fetch": "^1.1",
"utopia-php/validators": "0.2.*",
"utopia-php/image": "0.8.*",
"utopia-php/locale": "0.8.*",
"utopia-php/lock": "0.2.*",
"utopia-php/logger": "0.8.*",
"utopia-php/messaging": "0.22.*",
"utopia-php/migration": "1.*",
"utopia-php/platform": "^1.0@RC",
"utopia-php/platform": "1.0.0-rc3",
"utopia-php/pools": "1.*",
"utopia-php/span": "1.1.*",
"utopia-php/preloader": "0.2.*",
Generated
+224 -243
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
Delete a presence log by its unique ID.
+1
View File
@@ -0,0 +1 @@
Get presence usage metrics, including the current total of online users and historical online user counts for the selected time range.
+1
View File
@@ -0,0 +1 @@
Get a presence log by its unique ID. Entries whose `expiresAt` is in the past are treated as not found.
+1
View File
@@ -0,0 +1 @@
List presence logs. Expired entries are filtered out automatically.
+1
View File
@@ -0,0 +1 @@
Update a presence log by its unique ID. Using the patch method you can pass only specific fields that will get updated.
+1
View File
@@ -0,0 +1 @@
Create or update a presence log by its user ID.
-1
View File
@@ -1 +0,0 @@
Create a new JWT token. This token can be used to authenticate users with custom scopes and expiration time.
@@ -1 +0,0 @@
Send a test email to verify SMTP configuration.
-1
View File
@@ -1 +0,0 @@
Create a new project. You can create a maximum of 100 projects per account.
@@ -1 +0,0 @@
Reset a custom email template to its default value. This endpoint removes any custom content and restores the template to its original state.
-1
View File
@@ -1 +0,0 @@
Delete a project by its unique ID.
@@ -1 +0,0 @@
Get a custom email template for the specified locale and type. This endpoint returns the template content, subject, and other configuration details.
-1
View File
@@ -1 +0,0 @@
Get a project by its unique ID. This endpoint allows you to retrieve the project's details, including its name, description, team, region, and other metadata.
@@ -1 +0,0 @@
Update the status of a specific authentication method. Use this endpoint to enable or disable different authentication methods such as email, magic urls or sms in your project.
@@ -1 +0,0 @@
Update a custom email template for the specified locale and type. Use this endpoint to modify the content of your email templates.
@@ -1 +0,0 @@
Update the list of mock phone numbers for testing. Use these numbers to bypass SMS verification in development.
@@ -1 +0,0 @@
Update the OAuth2 provider configurations. Use this endpoint to set up or update the OAuth2 provider credentials or enable/disable providers.
-1
View File
@@ -1 +0,0 @@
Update the SMTP configuration for your project. Use this endpoint to configure your project's SMTP provider with your custom settings for sending transactional emails.
-1
View File
@@ -1 +0,0 @@
Update a project by its unique ID.
+116
View File
@@ -0,0 +1,116 @@
## Getting Started
Before you begin, create an Appwrite project and add a Unity platform in your Appwrite Console.
This SDK requires the following Unity packages and libraries:
- [**UniTask**](https://github.com/Cysharp/UniTask): For async/await support in Unity.
- [**NativeWebSocket**](https://github.com/endel/NativeWebSocket): For WebSocket realtime subscriptions.
- **System.Text.Json**: For JSON serialization, provided as a DLL in the project.
After installing the SDK, open **Appwrite → Setup Assistant** in Unity and install the required dependencies.
### Configure the SDK
Create an Appwrite configuration using the **QuickStart** window in the **Appwrite Setup Assistant**, or through **Appwrite → Create Configuration**.
### Using AppwriteManager
```csharp
[SerializeField] private AppwriteConfig config;
private AppwriteManager _manager;
private async UniTask ExampleWithManager()
{
_manager = AppwriteManager.Instance ?? new GameObject("AppwriteManager").AddComponent<AppwriteManager>();
_manager.SetConfig(config);
var success = await _manager.Initialize(needRealtime: true);
if (!success)
{
Debug.LogError("Failed to initialize AppwriteManager");
return;
}
var client = _manager.Client;
var pingResult = await client.Ping();
Debug.Log($"Ping result: {pingResult}");
var realtime = _manager.Realtime;
var subscription = realtime.Subscribe(
new[] { "databases.*.collections.*.documents" },
response =>
{
var eventName = response.Events != null && response.Events.Length > 0
? response.Events[0]
: "unknown";
Debug.Log($"Realtime event: {eventName}");
}
);
// Keep a reference to close the subscription when your MonoBehaviour is destroyed.
// subscription.Close();
}
```
### Using Client directly
```csharp
private async UniTask ExampleWithDirectClient()
{
var client = Client.From(
projectId: "<PROJECT_ID>",
endpoint: "https://<REGION>.cloud.appwrite.io/v1",
endpointRealtime: "wss://<REGION>.cloud.appwrite.io/v1");
var pingResult = await client.Ping();
Debug.Log($"Direct client ping: {pingResult}");
}
```
You can also create authenticated clients with `Client.FromSession`, `Client.FromDevKey`, or `Client.FromImpersonation` when those authentication flows are needed.
### Error handling
```csharp
try
{
var result = await client.Ping();
}
catch (AppwriteException ex)
{
Debug.LogError($"Appwrite Error: {ex.Message}");
Debug.LogError($"Status Code: {ex.Code}");
Debug.LogError($"Response: {ex.Response}");
}
```
## Preparing Models for Databases API
When working with the Databases API in Unity, models should be prepared for serialization using the System.Text.Json library. System.Text.Json uses CLR property names by default unless a naming policy is configured. If your project or SDK configuration serializes property names differently from your Appwrite collection attributes, this can cause errors due to mismatches between serialized property names and actual attribute names in your collection.
To avoid this, add the `JsonPropertyName` attribute to each property in your model class to match the attribute name in Appwrite:
```csharp
using System.Text.Json.Serialization;
public class TestModel
{
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("release_date")]
public System.DateTime ReleaseDate { get; set; }
}
```
The `JsonPropertyName` attribute ensures your data object is serialized with the correct attribute names for Appwrite databases.
### Learn more
You can use the following resources to learn more and get help:
- 🚀 [Getting Started Tutorial](https://appwrite.io/docs/getting-started-for-client)
- 📜 [Appwrite Docs](https://appwrite.io/docs)
- 💬 [Discord Community](https://appwrite.io/discord)
- 🧰 [Appwrite SDK Generator](https://github.com/appwrite/sdk-generator)
+3 -3
View File
@@ -122,9 +122,9 @@ class Key
$secret = $key;
}
$role = User::ROLE_APPS;
$role = User::ROLE_KEYS;
$roles = Config::getParam('roles', []);
$scopes = $roles[User::ROLE_APPS]['scopes'] ?? [];
$scopes = $roles[User::ROLE_KEYS]['scopes'] ?? [];
$expired = false;
$guestKey = new Key(
@@ -270,7 +270,7 @@ class Key
$name = $key->getAttribute('name', 'UNKNOWN');
$role = User::ROLE_APPS;
$role = User::ROLE_KEYS;
$scopes = $key->getAttribute('scopes', []);
+51
View File
@@ -0,0 +1,51 @@
<?php
namespace Appwrite\Event\Message;
use Utopia\Database\Document;
final class Database extends Base
{
public function __construct(
public readonly ?Document $project = null,
public readonly ?Document $user = null,
public readonly string $type = '',
public readonly ?Document $table = null,
public readonly ?Document $row = null,
public readonly ?Document $collection = null,
public readonly ?Document $document = null,
public readonly ?Document $database = null,
public readonly array $events = [],
) {
}
public function toArray(): array
{
return [
'project' => $this->project?->getArrayCopy(),
'user' => $this->user?->getArrayCopy(),
'type' => $this->type,
'table' => $this->table?->getArrayCopy(),
'row' => $this->row?->getArrayCopy(),
'collection' => $this->collection?->getArrayCopy(),
'document' => $this->document?->getArrayCopy(),
'database' => $this->database?->getArrayCopy(),
'events' => $this->events,
];
}
public static function fromArray(array $data): static
{
return new self(
project: !empty($data['project']) ? new Document($data['project']) : null,
user: !empty($data['user']) ? new Document($data['user']) : null,
type: $data['type'] ?? '',
table: !empty($data['table']) ? new Document($data['table']) : null,
row: !empty($data['row']) ? new Document($data['row']) : null,
collection: !empty($data['collection']) ? new Document($data['collection']) : null,
document: !empty($data['document']) ? new Document($data['document']) : null,
database: !empty($data['database']) ? new Document($data['database']) : null,
events: $data['events'] ?? [],
);
}
}
@@ -6,8 +6,13 @@ use Utopia\Database\Document;
final class StatsResources extends Base
{
/**
* @param Document $project
* @param array<int, array{metric: string, value: int}> $gauges
*/
public function __construct(
public readonly Document $project,
public readonly array $gauges = [],
) {
}
@@ -15,6 +20,7 @@ final class StatsResources extends Base
{
return [
'project' => $this->project->getArrayCopy(),
'gauges' => $this->gauges,
];
}
@@ -22,6 +28,7 @@ final class StatsResources extends Base
{
return new self(
project: new Document($data['project'] ?? []),
gauges: $data['gauges'] ?? [],
);
}
}
+45
View File
@@ -0,0 +1,45 @@
<?php
namespace Appwrite\Event\Publisher;
use Appwrite\Event\Message\Database as DatabaseMessage;
use Utopia\Database\Document;
use Utopia\DSN\DSN;
use Utopia\Queue\Publisher;
use Utopia\Queue\Queue;
readonly class Database extends Base
{
public function __construct(
Publisher $publisher,
protected Queue $queue,
) {
parent::__construct($publisher);
}
public function enqueue(DatabaseMessage $message, ?Queue $queue = null): string|bool
{
return $this->publish($queue ?? $this->getQueueFromProject($message->project), $message);
}
public function getSize(bool $failed = false, ?Queue $queue = null): int
{
return $this->getQueueSize($queue ?? $this->queue, $failed);
}
private function getQueueFromProject(?Document $project): Queue
{
$database = $project?->getAttribute('database', '');
if (empty($database)) {
return $this->queue;
}
try {
$dsn = new DSN($database);
} catch (\InvalidArgumentException) {
$dsn = new DSN('mysql://' . $database);
}
return new Queue($dsn->getHost());
}
}
+43 -1
View File
@@ -9,6 +9,18 @@ class StatsResources extends Event
{
protected bool $critical = false;
/**
* Pre-computed gauge metric snapshots to write to the stats collection. When non-empty,
* the StatsResources worker takes the fast path: it writes these directly via
* upsertDocuments (replace semantics) and skips the standard counting work.
*
* Each entry is a tuple of (metric key, value). The worker writes one stats document per
* (metric, period) tuple using the project's region.
*
* @var array<int, array{metric: string, value: int}>
*/
protected array $gauges = [];
public function __construct(protected Publisher $publisher)
{
parent::__construct($publisher);
@@ -18,6 +30,35 @@ class StatsResources extends Event
->setClass(System::getEnv('_APP_STATS_RESOURCES_CLASS_NAME', Event::STATS_RESOURCES_CLASS_NAME));
}
/**
* Set the full set of pre-computed gauge metrics for this message. Replaces any
* previously-set gauges.
*
* @param array<int, array{metric: string, value: int}> $gauges
*/
public function setGauges(array $gauges): self
{
$this->gauges = $gauges;
return $this;
}
/**
* Append a single pre-computed gauge metric to this message.
*/
public function addGauge(string $metric, int $value): self
{
$this->gauges[] = ['metric' => $metric, 'value' => $value];
return $this;
}
/**
* @return array<int, array{metric: string, value: int}>
*/
public function getGauges(): array
{
return $this->gauges;
}
/**
* Prepare the payload for the usage event.
*
@@ -26,7 +67,8 @@ class StatsResources extends Event
protected function preparePayload(): array
{
return [
'project' => $this->project
'project' => $this->project,
'gauges' => $this->gauges,
];
}
}
+4
View File
@@ -202,6 +202,10 @@ class Exception extends \Exception
/** Log */
public const string LOG_NOT_FOUND = 'log_not_found';
/** Presence */
public const string PRESENCE_NOT_FOUND = 'presence_not_found';
public const string PRESENCE_ALREADY_EXISTS = 'presence_already_exists';
/** Databases */
public const string DATABASE_NOT_FOUND = 'database_not_found';
public const string DATABASE_ALREADY_EXISTS = 'database_already_exists';
+3 -8
View File
@@ -342,9 +342,9 @@ class Resolvers
$lock->acquire();
$original = $utopia->getRoute();
try {
$request = clone $request;
$request->addHeader('x-appwrite-source', 'graphql');
// Drop json content type so post args are used directly.
if (\str_starts_with($request->getHeader('content-type'), 'application/json')) {
@@ -362,10 +362,9 @@ class Resolvers
$resolverResponse->setContentType(Response::CONTENT_TYPE_NULL);
$resolverResponse->setSent(false);
$route = $utopia->match($request, fresh: true);
$request->setRoute($route);
$request->setRoute($utopia->match($request)?->route);
$utopia->execute($route, $request, $resolverResponse);
$utopia->execute($request, $resolverResponse);
self::mergeResponseSideEffects($resolverResponse, $response);
@@ -384,10 +383,6 @@ class Resolvers
$reject($e);
return;
} finally {
if ($original !== null) {
$utopia->setRoute($original);
}
$lock->release();
unset(self::$locks[\spl_object_hash($utopia)]);
}
@@ -34,6 +34,7 @@ class Realtime extends MessagingAdapter
'account',
'teams',
'memberships',
'presences'
];
/**
@@ -44,6 +45,7 @@ class Realtime extends MessagingAdapter
* 'roles' -> [ROLE_x, ROLE_Y]
* 'userId' -> [USER_ID]
* 'channels' -> [CHANNEL_NAME_X, CHANNEL_NAME_Y, CHANNEL_NAME_Z]
* 'presences' -> [PRESENCE_ID_1, PRESENCE_ID_2, ...]
*/
public array $connections = [];
@@ -146,6 +148,7 @@ class Realtime extends MessagingAdapter
'roles' => \array_values(\array_unique(\array_merge($existingRoles, $roles))),
'userId' => $userId ?? ($existing['userId'] ?? ''),
'channels' => \array_values(\array_unique(\array_merge($existingChannels, $channels))),
'presences' => $this->connections[$identifier]['presences'] ?? []
];
if (\array_key_exists('authorization', $existing)) {
@@ -202,6 +205,74 @@ class Realtime extends MessagingAdapter
return $subscriptions;
}
/**
* Dedup delete presence triggers.
* Scenario: when client is connected to realtime and a delete call is made throught rest.
* If not dedupe then two delete events will get triggered. So remove the presenceIds
*
* @param string $projectId
* @param string $presenceId
* @return int Number of connections whose presences map was updated.
*/
public function removePresenceFromConnections(string $projectId, string $presenceId): int
{
if ($projectId === '' || $presenceId === '') {
return 0;
}
$removed = 0;
foreach ($this->connections as $connectionId => $connection) {
if (($connection['projectId'] ?? null) !== $projectId) {
continue;
}
if (!isset($connection['presences'][$presenceId])) {
continue;
}
unset($this->connections[$connectionId]['presences'][$presenceId]);
$removed++;
}
return $removed;
}
/**
* Returns the presence ID carried by a `presences.{id}.delete` event payload,
* or null when the event is not a presence delete.
*
* @param array $event Decoded pubsub payload produced by self::send().
* @return string|null
*/
public static function extractDeletedPresenceId(array $event): ?string
{
$events = $event['data']['events'] ?? [];
if (!\is_array($events)) {
return null;
}
$isPresenceDelete = false;
foreach ($events as $eventName) {
if (
\is_string($eventName)
&& \str_starts_with($eventName, 'presences.')
&& \str_ends_with($eventName, '.delete')
) {
$isPresenceDelete = true;
break;
}
}
if (!$isPresenceDelete) {
return null;
}
$presenceId = $event['data']['payload']['$id'] ?? null;
if (!\is_string($presenceId) || $presenceId === '') {
return null;
}
return $presenceId;
}
/**
* Removes all subscriptions for a connection.
*
@@ -789,6 +860,11 @@ class Realtime extends MessagingAdapter
}
$roles = [Role::team($project->getAttribute('teamId'))->toString()];
break;
case 'presences':
$channels[] = 'presences';
$channels[] = 'presences.' . $parts[1];
$roles = $payload->getRead();
break;
}
// Action is the last segment for plain CRUD events (e.g. `documents.X.create`),
+27
View File
@@ -55,6 +55,9 @@ class V24 extends Migration
if ($this->project->getSequence() != 'console') {
Console::info('Migrating Databases');
$this->migrateDatabases();
Console::info('Creating presence logs collection');
$this->createPresenceLogsCollection();
}
Console::info('Migrating Buckets');
@@ -330,6 +333,30 @@ class V24 extends Migration
});
}
/**
* Ensure the presenceLogs collection exists for project databases.
*
* @return void
* @throws Throwable
*/
private function createPresenceLogsCollection(): void
{
$collectionId = 'presenceLogs';
try {
Console::info("Ensuring collection \"{$collectionId}\" exists for project \"{$this->project->getId()}\".");
$this->dbForProject->purgeCachedCollection($collectionId);
$this->dbForProject->purgeCachedDocument(Database::METADATA, $collectionId);
$this->createCollection($collectionId);
} catch (Throwable $th) {
Console::warning("Failed to create collection \"{$collectionId}\": {$th->getMessage()}");
// Re-throw so the migration fails fast and doesn't leave the system in a partially migrated state.
throw $th;
}
}
/**
* Migrate all Bucket tables
*
+4
View File
@@ -11,6 +11,8 @@ use Appwrite\Platform\Modules\Databases;
use Appwrite\Platform\Modules\Functions;
use Appwrite\Platform\Modules\Health;
use Appwrite\Platform\Modules\Migrations;
use Appwrite\Platform\Modules\Organization;
use Appwrite\Platform\Modules\Presences;
use Appwrite\Platform\Modules\Project;
use Appwrite\Platform\Modules\Projects;
use Appwrite\Platform\Modules\Proxy;
@@ -31,6 +33,7 @@ class Appwrite extends Platform
$this->addModule(new Avatars\Module());
$this->addModule(new Databases\Module());
$this->addModule(new Projects\Module());
$this->addModule(new Presences\Module());
$this->addModule(new Functions\Module());
$this->addModule(new Health\Module());
$this->addModule(new Sites\Module());
@@ -42,6 +45,7 @@ class Appwrite extends Platform
$this->addModule(new VCS\Module());
$this->addModule(new Webhooks\Module());
$this->addModule(new Migrations\Module());
$this->addModule(new Organization\Module());
$this->addModule(new Project\Module());
$this->addModule(new Advisor\Module());
}
@@ -6,6 +6,7 @@ use Appwrite\Auth\MFA\Type;
use Appwrite\Auth\MFA\Type\TOTP;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Deprecated;
@@ -17,7 +18,7 @@ use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Platform\Action;
use Utopia\Platform\Enum;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\WhiteList;
@@ -77,7 +78,7 @@ class Create extends Action
contentType: ContentType::JSON
)
])
->param('type', null, new WhiteList([Type::TOTP]), 'Type of authenticator. Must be `' . Type::TOTP . '`')
->param('type', null, new WhiteList([Type::TOTP]), 'Type of authenticator. Must be `' . Type::TOTP . '`', enum: new Enum(name: 'AuthenticatorType'))
->inject('response')
->inject('project')
->inject('user')
@@ -6,6 +6,7 @@ use Appwrite\Auth\MFA\Type;
use Appwrite\Auth\MFA\Type\TOTP;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Deprecated;
@@ -14,7 +15,7 @@ use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Platform\Action;
use Utopia\Platform\Enum;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\WhiteList;
@@ -74,7 +75,7 @@ class Delete extends Action
contentType: ContentType::NONE
)
])
->param('type', null, new WhiteList([Type::TOTP]), 'Type of authenticator.')
->param('type', null, new WhiteList([Type::TOTP]), 'Type of authenticator.', enum: new Enum(name: 'AuthenticatorType'))
->inject('response')
->inject('user')
->inject('dbForProject')
@@ -7,6 +7,7 @@ use Appwrite\Auth\MFA\Type;
use Appwrite\Auth\MFA\Type\TOTP;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Deprecated;
@@ -15,7 +16,7 @@ use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Platform\Action;
use Utopia\Platform\Enum;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
@@ -76,7 +77,7 @@ class Update extends Action
contentType: ContentType::JSON
)
])
->param('type', null, new WhiteList([Type::TOTP]), 'Type of authenticator.')
->param('type', null, new WhiteList([Type::TOTP]), 'Type of authenticator.', enum: new Enum(name: 'AuthenticatorType'))
->param('otp', '', new Text(256), 'Valid verification token.')
->inject('response')
->inject('user')
@@ -10,6 +10,7 @@ use Appwrite\Event\Message\Messaging as MessagingMessage;
use Appwrite\Event\Publisher\Mail as MailPublisher;
use Appwrite\Event\Publisher\Messaging as MessagingPublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Deprecated;
@@ -29,7 +30,7 @@ use Utopia\Database\Document;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Locale\Locale;
use Utopia\Platform\Action;
use Utopia\Platform\Enum;
use Utopia\Platform\Scope\HTTP;
use Utopia\Storage\Validator\FileName;
use Utopia\System\System;
@@ -94,7 +95,7 @@ class Create extends Action
])
->label('abuse-limit', 10)
->label('abuse-key', 'url:{url},userId:{userId}')
->param('factor', '', new WhiteList([Type::EMAIL, Type::PHONE, Type::TOTP, Type::RECOVERY_CODE]), 'Factor used for verification. Must be one of following: `' . Type::EMAIL . '`, `' . Type::PHONE . '`, `' . Type::TOTP . '`, `' . Type::RECOVERY_CODE . '`.')
->param('factor', '', new WhiteList([Type::EMAIL, Type::PHONE, Type::TOTP, Type::RECOVERY_CODE]), 'Factor used for verification. Must be one of following: `' . Type::EMAIL . '`, `' . Type::PHONE . '`, `' . Type::TOTP . '`, `' . Type::RECOVERY_CODE . '`.', enum: new Enum(name: 'AuthenticationFactor'))
->inject('response')
->inject('dbForProject')
->inject('user')
@@ -36,7 +36,7 @@ class Get extends Action
group: 'insights',
name: 'getInsight',
description: '/docs/references/advisor/get-insight.md',
auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -42,7 +42,7 @@ class XList extends Action
group: 'insights',
name: 'listInsights',
description: '/docs/references/advisor/list-insights.md',
auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -37,7 +37,7 @@ class Get extends Action
group: 'reports',
name: 'getReport',
description: '/docs/references/advisor/get-report.md',
auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -41,7 +41,7 @@ class XList extends Action
group: 'reports',
name: 'listReports',
description: '/docs/references/advisor/list-reports.md',
auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -11,6 +11,7 @@ use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Platform\Action as UtopiaAction;
use Utopia\Platform\Enum;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Range;
use Utopia\Validator\WhiteList;
@@ -49,7 +50,10 @@ class Get extends Action
],
contentType: ContentType::IMAGE_PNG
))
->param('code', '', new WhiteList(\array_keys(Config::getParam('avatar-browsers'))), 'Browser Code.')
->param('code', '', new WhiteList(\array_keys(Config::getParam('avatar-browsers'))), 'Browser Code.', enum: new Enum(
name: 'Browser',
map: \array_map(fn (array $browser) => $browser['name'], Config::getParam('avatar-browsers')),
))
->param('width', 100, new Range(0, 2000), 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true)
->param('height', 100, new Range(0, 2000), 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true)
->param('quality', -1, new Range(-1, 100), 'Image quality. Pass an integer between 0 to 100. Defaults to keep existing image quality.', true)
@@ -11,6 +11,7 @@ use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Platform\Action as UtopiaAction;
use Utopia\Platform\Enum;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Range;
use Utopia\Validator\WhiteList;
@@ -49,7 +50,10 @@ class Get extends Action
],
contentType: ContentType::IMAGE_PNG
))
->param('code', '', new WhiteList(\array_keys(Config::getParam('avatar-credit-cards'))), 'Credit Card Code. Possible values: ' . \implode(', ', \array_keys(Config::getParam('avatar-credit-cards'))) . '.')
->param('code', '', new WhiteList(\array_keys(Config::getParam('avatar-credit-cards'))), 'Credit Card Code. Possible values: ' . \implode(', ', \array_keys(Config::getParam('avatar-credit-cards'))) . '.', enum: new Enum(
name: 'CreditCard',
map: \array_map(fn (array $creditCard) => $creditCard['name'], Config::getParam('avatar-credit-cards')),
))
->param('width', 100, new Range(0, 2000), 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true)
->param('height', 100, new Range(0, 2000), 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true)
->param('quality', -1, new Range(-1, 100), 'Image quality. Pass an integer between 0 to 100. Defaults to keep existing image quality.', true)
@@ -11,6 +11,7 @@ use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Platform\Action as UtopiaAction;
use Utopia\Platform\Enum;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Range;
use Utopia\Validator\WhiteList;
@@ -49,7 +50,10 @@ class Get extends Action
],
contentType: ContentType::IMAGE_PNG
))
->param('code', '', new WhiteList(\array_keys(Config::getParam('avatar-flags'))), 'Country Code. ISO Alpha-2 country code format.')
->param('code', '', new WhiteList(\array_keys(Config::getParam('avatar-flags'))), 'Country Code. ISO Alpha-2 country code format.', enum: new Enum(
name: 'Flag',
map: \array_map(fn (array $flag) => $flag['name'], Config::getParam('avatar-flags')),
))
->param('width', 100, new Range(0, 2000), 'Image width. Pass an integer between 0 to 2000. Defaults to 100.', true)
->param('height', 100, new Range(0, 2000), 'Image height. Pass an integer between 0 to 2000. Defaults to 100.', true)
->param('quality', -1, new Range(-1, 100), 'Image quality. Pass an integer between 0 to 100. Defaults to keep existing image quality.', true)
@@ -16,6 +16,7 @@ use Utopia\Domains\Domain;
use Utopia\Fetch\Client;
use Utopia\Image\Image;
use Utopia\Platform\Action as UtopiaAction;
use Utopia\Platform\Enum;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
use Utopia\Validator\ArrayList;
@@ -68,21 +69,21 @@ class Get extends Action
->param('viewportWidth', 1280, new Range(1, 1920), 'Browser viewport width. Pass an integer between 1 to 1920. Defaults to 1280.', true, example: '1920')
->param('viewportHeight', 720, new Range(1, 1080), 'Browser viewport height. Pass an integer between 1 to 1080. Defaults to 720.', true, example: '1080')
->param('scale', 1, new Range(0.1, 3, Range::TYPE_FLOAT), 'Browser scale factor. Pass a number between 0.1 to 3. Defaults to 1.', true, example: '2')
->param('theme', 'light', new WhiteList(['light', 'dark']), 'Browser theme. Pass "light" or "dark". Defaults to "light".', true, example: 'dark')
->param('theme', 'light', new WhiteList(['light', 'dark']), 'Browser theme. Pass "light" or "dark". Defaults to "light".', true, example: 'dark', enum: new Enum(name: 'BrowserTheme'))
->param('userAgent', '', new Text(512), 'Custom user agent string. Defaults to browser default.', true, example: 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15')
->param('fullpage', false, new Boolean(true), 'Capture full page scroll. Pass 0 for viewport only, or 1 for full page. Defaults to 0.', true, example: 'true')
->param('locale', '', new Text(10), 'Browser locale (e.g., "en-US", "fr-FR"). Defaults to browser default.', true, example: 'en-US')
->param('timezone', '', new WhiteList(timezone_identifiers_list()), 'IANA timezone identifier (e.g., "America/New_York", "Europe/London"). Defaults to browser default.', true, example: 'america/new_york')
->param('timezone', '', new WhiteList(timezone_identifiers_list()), 'IANA timezone identifier (e.g., "America/New_York", "Europe/London"). Defaults to browser default.', true, example: 'America/New_York', enum: new Enum(name: 'Timezone'))
->param('latitude', 0, new Range(-90, 90, Range::TYPE_FLOAT), 'Geolocation latitude. Pass a number between -90 to 90. Defaults to 0.', true, example: '37.7749')
->param('longitude', 0, new Range(-180, 180, Range::TYPE_FLOAT), 'Geolocation longitude. Pass a number between -180 to 180. Defaults to 0.', true, example: '-122.4194')
->param('accuracy', 0, new Range(0, 100000, Range::TYPE_FLOAT), 'Geolocation accuracy in meters. Pass a number between 0 to 100000. Defaults to 0.', true, example: '100')
->param('touch', false, new Boolean(true), 'Enable touch support. Pass 0 for no touch, or 1 for touch enabled. Defaults to 0.', true, example: 'true')
->param('permissions', [], new ArrayList(new WhiteList(['geolocation', 'camera', 'microphone', 'notifications', 'midi', 'push', 'clipboard-read', 'clipboard-write', 'payment-handler', 'usb', 'bluetooth', 'accelerometer', 'gyroscope', 'magnetometer', 'ambient-light-sensor', 'background-sync', 'persistent-storage', 'screen-wake-lock', 'web-share', 'xr-spatial-tracking'])), 'Browser permissions to grant. Pass an array of permission names like ["geolocation", "camera", "microphone"]. Defaults to empty.', true, example: '["geolocation","notifications"]')
->param('permissions', [], new ArrayList(new WhiteList(['geolocation', 'camera', 'microphone', 'notifications', 'midi', 'push', 'clipboard-read', 'clipboard-write', 'payment-handler', 'usb', 'bluetooth', 'accelerometer', 'gyroscope', 'magnetometer', 'ambient-light-sensor', 'background-sync', 'persistent-storage', 'screen-wake-lock', 'web-share', 'xr-spatial-tracking'])), 'Browser permissions to grant. Pass an array of permission names like ["geolocation", "camera", "microphone"]. Defaults to empty.', true, example: '["geolocation","notifications"]', enum: new Enum(name: 'BrowserPermission'))
->param('sleep', 0, new Range(0, 10), 'Wait time in seconds before taking the screenshot. Pass an integer between 0 to 10. Defaults to 0.', true, example: '3')
->param('width', 0, new Range(0, 2000), 'Output image width. Pass 0 to use original width, or an integer between 1 to 2000. Defaults to 0 (original width).', true, example: '800')
->param('height', 0, new Range(0, 2000), 'Output image height. Pass 0 to use original height, or an integer between 1 to 2000. Defaults to 0 (original height).', true, example: '600')
->param('quality', -1, new Range(-1, 100), 'Screenshot quality. Pass an integer between 0 to 100. Defaults to keep existing image quality.', true, example: '85')
->param('output', '', new WhiteList(\array_keys(Config::getParam('storage-outputs')), true), 'Output format type (jpeg, jpg, png, gif and webp).', true, example: 'jpeg')
->param('output', '', new WhiteList(\array_keys(Config::getParam('storage-outputs')), true), 'Output format type (jpeg, jpg, png, gif and webp).', true, example: 'jpeg', enum: new Enum(name: 'ImageFormat'))
->inject('response')
->inject('usage')
->callback($this->action(...));
@@ -3,6 +3,7 @@
namespace Appwrite\Platform\Modules\Console\Http\Resources;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
@@ -12,7 +13,7 @@ use Utopia\Database\Database;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Domains\Domain as Domain;
use Utopia\Platform\Action;
use Utopia\Platform\Enum;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
use Utopia\Validator\Domain as DomainValidator;
@@ -56,7 +57,7 @@ class Get extends Action
->label('abuse-key', 'userId:{userId}, url:{url}')
->label('abuse-time', 60)
->param('value', '', new Text(256), 'Resource value.')
->param('type', '', new WhiteList(['rules']), 'Resource type.')
->param('type', '', new WhiteList(['rules']), 'Resource type.', enum: new Enum(name: 'ConsoleResourceType'))
->inject('response')
->inject('dbForPlatform')
->inject('platform')
@@ -0,0 +1,69 @@
<?php
namespace Appwrite\Platform\Modules\Console\Http\Scopes\Organization;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Document;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class XList extends Action
{
use HTTP;
public static function getName(): string
{
return 'listConsoleOrganizationScopes';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/console/scopes/organization')
->desc('List organization scopes')
->groups(['api'])
->label('scope', 'public')
->label('sdk', new Method(
namespace: 'console',
group: 'console',
name: 'listOrganizationScopes',
description: 'List all scopes available for organization API keys, along with a description for each scope.',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_CONSOLE_KEY_SCOPE_LIST,
)
],
contentType: ContentType::JSON
))
->inject('response')
->callback($this->action(...));
}
public function action(Response $response): void
{
$scopesConfig = Config::getParam('organizationScopes', []);
$scopes = [];
foreach ($scopesConfig as $scopeId => $scope) {
$scopes[] = new Document([
'$id' => $scopeId,
'description' => $scope['description'] ?? '',
'category' => $scope['category'] ?? '',
'deprecated' => $scope['deprecated'] ?? false,
]);
}
$response->dynamic(new Document([
'total' => \count($scopes),
'scopes' => $scopes,
]), Response::MODEL_CONSOLE_KEY_SCOPE_LIST);
}
}
@@ -1,6 +1,6 @@
<?php
namespace Appwrite\Platform\Modules\Console\Http\Scopes\Key;
namespace Appwrite\Platform\Modules\Console\Http\Scopes\Project;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@@ -0,0 +1,124 @@
<?php
namespace Appwrite\Platform\Modules\Console\Http\Templates\Email;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Template\Template;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Document;
use Utopia\Locale\Locale;
use Utopia\Platform\Action;
use Utopia\Platform\Enum;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
use Utopia\Validator\WhiteList;
class Get extends Action
{
use HTTP;
public static function getName(): string
{
return 'getConsoleEmailTemplate';
}
public function __construct()
{
$this->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/console/templates/email/:templateId')
->desc('Get email template')
->groups(['api'])
->label('scope', 'public')
->label('sdk', new Method(
namespace: 'console',
group: null,
name: 'getEmailTemplate',
description: <<<EOT
Get the Appwrite built-in default email template for the specified type and locale. Always returns the unmodified default, ignoring any custom project overrides.
EOT,
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_EMAIL_TEMPLATE,
)
]
))
->param('templateId', '', new WhiteList(Config::getParam('locale-templates')['email'] ?? [], true), 'Email template type. Can be one of: ' . \implode(', ', Config::getParam('locale-templates')['email'] ?? []), enum: new Enum(name: 'ProjectEmailTemplateId'))
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale. If left empty, the fallback locale (en) will be used.', optional: true, injections: ['localeCodes'], enum: new Enum(name: 'ProjectEmailTemplateLocale'))
->inject('response')
->callback($this->action(...));
}
public function action(
string $templateId,
string $locale,
Response $response,
): void {
$locale = $locale ?: System::getEnv('_APP_LOCALE', 'en');
$localeObj = new Locale($locale);
$localeObj->setFallback(System::getEnv('_APP_LOCALE', 'en'));
$response->dynamic(new Document([
'templateId' => $templateId,
'locale' => $locale,
'subject' => $localeObj->getText('emails.' . $templateId . '.subject'),
'message' => $this->getDefaultMessage($templateId, $localeObj),
'senderName' => '',
'senderEmail' => '',
'replyToEmail' => '',
'replyToName' => '',
]), Response::MODEL_EMAIL_TEMPLATE);
}
private function getDefaultMessage(string $templateId, Locale $localeObj): string
{
$templateConfigs = [
'magicSession' => [
'file' => 'email-magic-url.tpl',
'placeholders' => ['optionButton', 'buttonText', 'optionUrl', 'clientInfo', 'securityPhrase']
],
'mfaChallenge' => [
'file' => 'email-mfa-challenge.tpl',
'placeholders' => ['description', 'clientInfo']
],
'otpSession' => [
'file' => 'email-otp.tpl',
'placeholders' => ['description', 'clientInfo', 'securityPhrase']
],
'sessionAlert' => [
'file' => 'email-session-alert.tpl',
'placeholders' => ['body', 'listDevice', 'listIpAddress', 'listCountry', 'footer']
],
];
$config = $templateConfigs[$templateId] ?? [
'file' => 'email-inner-base.tpl',
'placeholders' => ['buttonText', 'body', 'footer']
];
$templateString = file_get_contents(APP_CE_CONFIG_DIR . '/locale/templates/' . $config['file']);
$message = Template::fromString($templateString);
foreach ($config['placeholders'] as $param) {
$escapeHtml = !in_array($param, ['clientInfo', 'body', 'footer', 'description']);
if ($templateId === 'magicSession' && $param === 'securityPhrase') {
$message->setParam('{{securityPhrase}}', '');
continue;
}
$message->setParam("{{{$param}}}", $localeObj->getText("emails.{$templateId}.{$param}"), escapeHtml: $escapeHtml);
}
$message
->setParam('{{hello}}', $localeObj->getText("emails.{$templateId}.hello"))
->setParam('{{thanks}}', $localeObj->getText("emails.{$templateId}.thanks"))
->setParam('{{signature}}', $localeObj->getText("emails.{$templateId}.signature"));
return $message->render(useContent: true);
}
}
@@ -15,7 +15,9 @@ use Appwrite\Platform\Modules\Console\Http\Redirects\Recover\Get as RedirectReco
use Appwrite\Platform\Modules\Console\Http\Redirects\Register\Get as RedirectRegister;
use Appwrite\Platform\Modules\Console\Http\Redirects\Root\Get as RedirectRoot;
use Appwrite\Platform\Modules\Console\Http\Resources\Get as GetResourceAvailability;
use Appwrite\Platform\Modules\Console\Http\Scopes\Key\XList as ListKeyScopes;
use Appwrite\Platform\Modules\Console\Http\Scopes\Organization\XList as ListOrganizationScopes;
use Appwrite\Platform\Modules\Console\Http\Scopes\Project\XList as ListKeyScopes;
use Appwrite\Platform\Modules\Console\Http\Templates\Email\Get as GetEmailTemplate;
use Appwrite\Platform\Modules\Console\Http\Variables\Get as GetVariables;
use Utopia\Platform\Service;
@@ -30,8 +32,10 @@ class Http extends Service
$this->addAction(Web::getName(), new Web());
$this->addAction(GetVariables::getName(), new GetVariables());
$this->addAction(GetEmailTemplate::getName(), new GetEmailTemplate());
$this->addAction(ListOAuth2Providers::getName(), new ListOAuth2Providers());
$this->addAction(ListKeyScopes::getName(), new ListKeyScopes());
$this->addAction(ListOrganizationScopes::getName(), new ListOrganizationScopes());
$this->addAction(CreateAssistantQuery::getName(), new CreateAssistantQuery());
$this->addAction(GetResourceAvailability::getName(), new GetResourceAvailability());
@@ -2,8 +2,9 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Message\Database as DatabaseMessage;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response as UtopiaResponse;
@@ -312,7 +313,7 @@ abstract class Action extends UtopiaAction
};
}
protected function createAttribute(string $databaseId, string $collectionId, Document $attribute, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): Document
protected function createAttribute(string $databaseId, string $collectionId, Document $attribute, Response $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): Document
{
$key = $attribute->getAttribute('key');
$type = $attribute->getAttribute('type', '');
@@ -464,20 +465,6 @@ abstract class Action extends UtopiaAction
$dbForProject->purgeCachedCollection('database_' . $db->getSequence() . '_collection_' . $relatedCollection->getSequence());
}
$queueForDatabase
->setType(DATABASE_TYPE_CREATE_ATTRIBUTE)
->setDatabase($db);
if ($this->isCollectionsAPI()) {
$queueForDatabase
->setDocument($attribute)
->setCollection($collection);
} else {
$queueForDatabase
->setRow($attribute)
->setTable($collection);
}
$queueForEvents
->setContext('database', $db)
->setParam('databaseId', $databaseId)
@@ -487,6 +474,18 @@ abstract class Action extends UtopiaAction
->setParam('columnId', $attribute->getId())
->setContext($this->getCollectionsEventsContext(), $collection);
$publisherForDatabase->enqueue(new DatabaseMessage(
project: $queueForEvents->getProject(),
user: $queueForEvents->getUser(),
type: DATABASE_TYPE_CREATE_ATTRIBUTE,
database: $db,
collection: $this->isCollectionsAPI() ? $collection : null,
document: $this->isCollectionsAPI() ? $attribute : null,
table: $this->isCollectionsAPI() ? null : $collection,
row: $this->isCollectionsAPI() ? null : $attribute,
events: Event::generateEvents($queueForEvents->getEvent(), $queueForEvents->getParams()),
));
$response->setStatusCode(SwooleResponse::STATUS_CODE_CREATED);
return $attribute;
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\BigInt;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
@@ -73,13 +73,13 @@ class Create extends Action
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, bool $array, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void
{
$min ??= \PHP_INT_MIN;
$max ??= \PHP_INT_MAX;
@@ -102,7 +102,7 @@ class Create extends Action
'array' => $array,
'format' => APP_DATABASE_ATTRIBUTE_BIGINT_RANGE,
'formatOptions' => ['min' => $min, 'max' => $max],
]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization);
]), $response, $dbForProject, $publisherForDatabase, $queueForEvents, $authorization);
$formatOptions = $attribute->getAttribute('formatOptions', []);
if (!empty($formatOptions)) {
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Boolean;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Deprecated;
@@ -68,13 +68,13 @@ class Create extends Action
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?bool $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?bool $default, bool $array, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void
{
$attribute = $this->createAttribute($databaseId, $collectionId, new Document([
'key' => $key,
@@ -83,7 +83,7 @@ class Create extends Action
'required' => $required,
'default' => $default,
'array' => $array,
]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization);
]), $response, $dbForProject, $publisherForDatabase, $queueForEvents, $authorization);
$response
->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED)
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Datetime;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Deprecated;
@@ -69,13 +69,13 @@ class Create extends Action
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void
{
$attribute = $this->createAttribute(
$databaseId,
@@ -91,7 +91,7 @@ class Create extends Action
]),
$response,
$dbForProject,
$queueForDatabase,
$publisherForDatabase,
$queueForEvents,
$authorization
);
@@ -2,8 +2,9 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Message\Database as DatabaseMessage;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@@ -66,13 +67,13 @@ class Delete extends Action
->param('key', '', fn (Database $dbForProject) => new Key(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Attribute Key.', false, ['dbForProject'])
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
public function action(string $databaseId, string $collectionId, string $key, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void
{
$db = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($db->isEmpty()) {
@@ -129,20 +130,6 @@ class Delete extends Action
}
}
$queueForDatabase
->setDatabase($db)
->setType(DATABASE_TYPE_DELETE_ATTRIBUTE);
if ($this->isCollectionsAPI()) {
$queueForDatabase
->setRow($attribute)
->setTable($collection);
} else {
$queueForDatabase
->setDocument($attribute)
->setCollection($collection);
}
$type = $attribute->getAttribute('type');
$format = $attribute->getAttribute('format');
@@ -158,6 +145,18 @@ class Delete extends Action
->setPayload($response->output($attribute, $model))
->setContext($this->getCollectionsEventsContext(), $collection);
$publisherForDatabase->enqueue(new DatabaseMessage(
project: $queueForEvents->getProject(),
user: $queueForEvents->getUser(),
type: DATABASE_TYPE_DELETE_ATTRIBUTE,
database: $db,
collection: $this->isCollectionsAPI() ? null : $collection,
document: $this->isCollectionsAPI() ? null : $attribute,
table: $this->isCollectionsAPI() ? $collection : null,
row: $this->isCollectionsAPI() ? $attribute : null,
events: Event::generateEvents($queueForEvents->getEvent(), $queueForEvents->getParams()),
));
$response->noContent();
}
}
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Email;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Deprecated;
@@ -69,13 +69,13 @@ class Create extends Action
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void
{
$attribute = $this->createAttribute(
$databaseId,
@@ -91,7 +91,7 @@ class Create extends Action
]),
$response,
$dbForProject,
$queueForDatabase,
$publisherForDatabase,
$queueForEvents,
$authorization
);
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Enum;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
@@ -72,13 +72,13 @@ class Create extends Action
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, array $elements, ?bool $required, ?string $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
public function action(string $databaseId, string $collectionId, string $key, array $elements, ?bool $required, ?string $default, bool $array, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void
{
if (!is_null($default) && !\in_array($default, $elements, true)) {
throw new Exception($this->getInvalidValueException(), 'Default value not found in elements');
@@ -99,7 +99,7 @@ class Create extends Action
]),
$response,
$dbForProject,
$queueForDatabase,
$publisherForDatabase,
$queueForEvents,
$authorization
);
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Float;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
@@ -73,13 +73,13 @@ class Create extends Action
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?float $min, ?float $max, ?float $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?float $min, ?float $max, ?float $default, bool $array, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void
{
$min ??= -PHP_FLOAT_MAX;
$max ??= PHP_FLOAT_MAX;
@@ -102,7 +102,7 @@ class Create extends Action
'array' => $array,
'format' => APP_DATABASE_ATTRIBUTE_FLOAT_RANGE,
'formatOptions' => ['min' => $min, 'max' => $max],
]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization);
]), $response, $dbForProject, $publisherForDatabase, $queueForEvents, $authorization);
$formatOptions = $attribute->getAttribute('formatOptions', []);
if (!empty($formatOptions)) {
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\IP;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Deprecated;
@@ -69,13 +69,13 @@ class Create extends Action
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void
{
$attribute = $this->createAttribute(
$databaseId,
@@ -91,7 +91,7 @@ class Create extends Action
]),
$response,
$dbForProject,
$queueForDatabase,
$publisherForDatabase,
$queueForEvents,
$authorization
);
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Integer;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
@@ -73,13 +73,13 @@ class Create extends Action
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, bool $array, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void
{
$min ??= \PHP_INT_MIN;
$max ??= \PHP_INT_MAX;
@@ -104,7 +104,7 @@ class Create extends Action
'array' => $array,
'format' => APP_DATABASE_ATTRIBUTE_INT_RANGE,
'formatOptions' => ['min' => $min, 'max' => $max],
]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization);
]), $response, $dbForProject, $publisherForDatabase, $queueForEvents, $authorization);
$formatOptions = $attribute->getAttribute('formatOptions', []);
if (!empty($formatOptions)) {
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Line;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
@@ -69,13 +69,13 @@ class Create extends Action
->param('default', null, new Nullable(new Spatial(Database::VAR_LINESTRING)), 'Default value for attribute when not provided, two-dimensional array of coordinate pairs, [[longitude, latitude], [longitude, latitude], …], listing the vertices of the line in order. Cannot be set when attribute is required.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?array $default, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?array $default, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void
{
if (!$dbForProject->getAdapter()->getSupportForSpatialAttributes()) {
throw new Exception(Exception::GENERAL_FEATURE_UNSUPPORTED, 'Spatial columns are not supported by this database.');
@@ -86,7 +86,7 @@ class Create extends Action
'type' => Database::VAR_LINESTRING,
'required' => $required,
'default' => $default
]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization);
]), $response, $dbForProject, $publisherForDatabase, $queueForEvents, $authorization);
$response
->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED)
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Longtext;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
@@ -67,7 +67,7 @@ class Create extends Action
->param('encrypt', false, new Boolean(), 'Toggle encryption for the attribute. Encryption enhances security by not storing any plain text values in the database. However, encrypted attributes cannot be queried.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('plan')
->inject('authorization')
@@ -84,7 +84,7 @@ class Create extends Action
bool $encrypt,
UtopiaResponse $response,
Database $dbForProject,
EventDatabase $queueForDatabase,
DatabasePublisher $publisherForDatabase,
Event $queueForEvents,
array $plan,
Authorization $authorization
@@ -112,7 +112,7 @@ class Create extends Action
]),
$response,
$dbForProject,
$queueForDatabase,
$publisherForDatabase,
$queueForEvents,
$authorization
);
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Mediumtext;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
@@ -67,7 +67,7 @@ class Create extends Action
->param('encrypt', false, new Boolean(), 'Toggle encryption for the attribute. Encryption enhances security by not storing any plain text values in the database. However, encrypted attributes cannot be queried.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('plan')
->inject('authorization')
@@ -84,7 +84,7 @@ class Create extends Action
bool $encrypt,
UtopiaResponse $response,
Database $dbForProject,
EventDatabase $queueForDatabase,
DatabasePublisher $publisherForDatabase,
Event $queueForEvents,
array $plan,
Authorization $authorization
@@ -112,7 +112,7 @@ class Create extends Action
]),
$response,
$dbForProject,
$queueForDatabase,
$publisherForDatabase,
$queueForEvents,
$authorization
);
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Point;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
@@ -69,13 +69,13 @@ class Create extends Action
->param('default', null, new Nullable(new Spatial(Database::VAR_POINT)), 'Default value for attribute when not provided, array of two numbers [longitude, latitude], representing a single coordinate. Cannot be set when attribute is required.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?array $default, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?array $default, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void
{
if (!$dbForProject->getAdapter()->getSupportForSpatialAttributes()) {
throw new Exception(Exception::GENERAL_FEATURE_UNSUPPORTED, 'Spatial columns are not supported by this database.');
@@ -86,7 +86,7 @@ class Create extends Action
'type' => Database::VAR_POINT,
'required' => $required,
'default' => $default,
]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization);
]), $response, $dbForProject, $publisherForDatabase, $queueForEvents, $authorization);
$response
->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED)
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Polygon;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
@@ -69,13 +69,13 @@ class Create extends Action
->param('default', null, new Nullable(new Spatial(Database::VAR_POLYGON)), 'Default value for attribute when not provided, three-dimensional array where the outer array holds one or more linear rings, [[[longitude, latitude], …], …], the first ring is the exterior boundary, any additional rings are interior holes, and each ring must start and end with the same coordinate pair. Cannot be set when attribute is required.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?array $default, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?array $default, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void
{
if (!$dbForProject->getAdapter()->getSupportForSpatialAttributes()) {
throw new Exception(Exception::GENERAL_FEATURE_UNSUPPORTED, 'Spatial columns are not supported by this database.');
@@ -86,7 +86,7 @@ class Create extends Action
'type' => Database::VAR_POLYGON,
'required' => $required,
'default' => $default,
]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization);
]), $response, $dbForProject, $publisherForDatabase, $queueForEvents, $authorization);
$response
->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED)
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Relationship;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
@@ -17,6 +17,7 @@ use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
use Utopia\Platform\Enum;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\WhiteList;
@@ -70,7 +71,7 @@ class Create extends Action
Database::RELATION_MANY_TO_ONE,
Database::RELATION_MANY_TO_MANY,
Database::RELATION_ONE_TO_MANY
], true), 'Relation type')
], true), 'Relation type', enum: new Enum(name: 'RelationshipType'))
->param('twoWay', false, new Boolean(), 'Is Two Way?', true)
->param('key', null, fn (Database $dbForProject) => new Nullable(new Key(false, $dbForProject->getAdapter()->getMaxUIDLength())), 'Attribute Key.', true, ['dbForProject'])
->param('twoWayKey', null, fn (Database $dbForProject) => new Nullable(new Key(false, $dbForProject->getAdapter()->getMaxUIDLength())), 'Two Way Attribute Key.', true, ['dbForProject'])
@@ -78,16 +79,16 @@ class Create extends Action
Database::RELATION_MUTATE_CASCADE,
Database::RELATION_MUTATE_RESTRICT,
Database::RELATION_MUTATE_SET_NULL
], true), 'Constraints option', true)
], true), 'Constraints option', true, enum: new Enum(name: 'RelationMutate'))
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $relatedCollectionId, string $type, bool $twoWay, ?string $key, ?string $twoWayKey, string $onDelete, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
public function action(string $databaseId, string $collectionId, string $relatedCollectionId, string $type, bool $twoWay, ?string $key, ?string $twoWayKey, string $onDelete, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void
{
if (!$dbForProject->getAdapter()->getSupportForRelationships()) {
throw new Exception(Exception::GENERAL_FEATURE_UNSUPPORTED, 'Relationships are not supported by this database.');
@@ -159,7 +160,7 @@ class Create extends Action
'twoWayKey' => $twoWayKey,
'onDelete' => $onDelete,
]
]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization);
]), $response, $dbForProject, $publisherForDatabase, $queueForEvents, $authorization);
foreach ($attribute->getAttribute('options', []) as $k => $option) {
$attribute->setAttribute($k, $option);
@@ -16,6 +16,7 @@ use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
use Utopia\Platform\Enum;
use Utopia\Validator\Nullable;
use Utopia\Validator\WhiteList;
@@ -69,7 +70,7 @@ class Update extends Action
Database::RELATION_MUTATE_CASCADE,
Database::RELATION_MUTATE_RESTRICT,
Database::RELATION_MUTATE_SET_NULL
], true), 'Constraints option', true)
], true), 'Constraints option', true, enum: new Enum(name: 'RelationMutate'))
->param('newKey', null, fn (Database $dbForProject) => new Nullable(new Key(false, $dbForProject->getAdapter()->getMaxUIDLength())), 'New Attribute Key.', true, ['dbForProject'])
->inject('response')
->inject('dbForProject')
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\String;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
@@ -75,7 +75,7 @@ class Create extends Action
->param('encrypt', false, new Boolean(), 'Toggle encryption for the attribute. Encryption enhances security by not storing any plain text values in the database. However, encrypted attributes cannot be queried.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('plan')
->inject('authorization')
@@ -93,7 +93,7 @@ class Create extends Action
bool $encrypt,
UtopiaResponse $response,
Database $dbForProject,
EventDatabase $queueForDatabase,
DatabasePublisher $publisherForDatabase,
Event $queueForEvents,
array $plan,
Authorization $authorization
@@ -134,7 +134,7 @@ class Create extends Action
]),
$response,
$dbForProject,
$queueForDatabase,
$publisherForDatabase,
$queueForEvents,
$authorization
);
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Text;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
@@ -67,7 +67,7 @@ class Create extends Action
->param('encrypt', false, new Boolean(), 'Toggle encryption for the attribute. Encryption enhances security by not storing any plain text values in the database. However, encrypted attributes cannot be queried.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('plan')
->inject('authorization')
@@ -84,7 +84,7 @@ class Create extends Action
bool $encrypt,
UtopiaResponse $response,
Database $dbForProject,
EventDatabase $queueForDatabase,
DatabasePublisher $publisherForDatabase,
Event $queueForEvents,
array $plan,
Authorization $authorization
@@ -112,7 +112,7 @@ class Create extends Action
]),
$response,
$dbForProject,
$queueForDatabase,
$publisherForDatabase,
$queueForEvents,
$authorization
);
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\URL;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Deprecated;
@@ -69,7 +69,7 @@ class Create extends Action
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
@@ -84,7 +84,7 @@ class Create extends Action
bool $array,
UtopiaResponse $response,
Database $dbForProject,
EventDatabase $queueForDatabase,
DatabasePublisher $publisherForDatabase,
Event $queueForEvents,
Authorization $authorization
): void {
@@ -96,7 +96,7 @@ class Create extends Action
'default' => $default,
'array' => $array,
'format' => APP_DATABASE_ATTRIBUTE_URL,
]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization);
]), $response, $dbForProject, $publisherForDatabase, $queueForEvents, $authorization);
$response
->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED)
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Varchar;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
@@ -70,7 +70,7 @@ class Create extends Action
->param('encrypt', false, new Boolean(), 'Toggle encryption for the attribute. Encryption enhances security by not storing any plain text values in the database. However, encrypted attributes cannot be queried.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('plan')
->inject('authorization')
@@ -88,7 +88,7 @@ class Create extends Action
bool $encrypt,
UtopiaResponse $response,
Database $dbForProject,
EventDatabase $queueForDatabase,
DatabasePublisher $publisherForDatabase,
Event $queueForEvents,
array $plan,
Authorization $authorization
@@ -129,7 +129,7 @@ class Create extends Action
]),
$response,
$dbForProject,
$queueForDatabase,
$publisherForDatabase,
$queueForEvents,
$authorization
);
@@ -2,8 +2,9 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Message\Database as DatabaseMessage;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@@ -63,13 +64,13 @@ class Delete extends Action
->inject('response')
->inject('dbForProject')
->inject('getDatabasesDB')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
public function action(string $databaseId, string $collectionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void
{
$database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
@@ -89,22 +90,22 @@ class Delete extends Action
$dbForDatabases = $getDatabasesDB($database);
$dbForDatabases->purgeCachedCollection('database_' . $database->getSequence() . '_collection_' . $collection->getSequence());
$queueForDatabase
->setType(DATABASE_TYPE_DELETE_COLLECTION)
->setDatabase($database);
if ($this->isCollectionsAPI()) {
$queueForDatabase->setCollection($collection);
} else {
$queueForDatabase->setTable($collection);
}
$queueForEvents
->setParam('databaseId', $databaseId)
->setContext('database', $database)
->setParam($this->getEventsParamKey(), $collection->getId())
->setPayload($response->output($collection, $this->getResponseModel()));
$publisherForDatabase->enqueue(new DatabaseMessage(
project: $queueForEvents->getProject(),
user: $queueForEvents->getUser(),
type: DATABASE_TYPE_DELETE_COLLECTION,
database: $database,
collection: $this->isCollectionsAPI() ? $collection : null,
table: $this->isCollectionsAPI() ? null : $collection,
events: Event::generateEvents($queueForEvents->getEvent(), $queueForEvents->getParams()),
));
$response->noContent();
}
}
@@ -93,7 +93,7 @@ class Decrement extends Action
public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $min, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Event $queueForEvents, Context $usage, array $plan, Authorization $authorization, User $user): void
{
$isAPIKey = $user->isApp($authorization->getRoles());
$isAPIKey = $user->isKey($authorization->getRoles());
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
$database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
@@ -93,7 +93,7 @@ class Increment extends Action
public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $max, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, Event $queueForEvents, Context $usage, array $plan, Authorization $authorization, User $user): void
{
$isAPIKey = $user->isApp($authorization->getRoles());
$isAPIKey = $user->isKey($authorization->getRoles());
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
$database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
@@ -201,7 +201,7 @@ class Create extends Action
$documents = [$data];
}
$isAPIKey = $user->isApp($authorization->getRoles());
$isAPIKey = $user->isKey($authorization->getRoles());
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
if ($isBulk && !$isAPIKey && !$isPrivilegedUser) {

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