Compare commits

...
Author SHA1 Message Date
Prem Palanisamy e0ec28f02a revert: drop $log clone and sdks in-lock re-read
Both were defensive against rare edge cases the reviewer flagged but
which don't justify the complexity:

- $log clone: protects against tag pollution on the per-request Log
  if reportError fires AND the request later errors. http.php's
  request-end handler overwrites the core fields (namespace, message,
  action, etc.) anyway; only addTag/addExtra accumulate. Aligns with
  Embeddings/Text/Create.php precedent which mutates $log directly.

- sdks in-lock re-read: closes a sequential-acquire stale-read race on
  the sdks append-list. The race exists but the impact is bounded —
  one SDK registration delayed until the next request from that SDK
  fires. Self-healing on retry. The codebase already accepts this
  exact race for auths, oAuthProviders, services, identities, sessions,
  factors, etc. Special-casing this one site is precision the analytics
  use case doesn't need.
2026-04-30 08:24:56 +01:00
Prem Palanisamy 18f3280bad refactor(lock): extract magic numbers and outcome strings, fix target parse
- TTL defaults (5s skip / 10s fail), 3s wait timeout, and 60s sentry
  rate-limit window are now class constants.
- Telemetry outcome labels (acquired/skipped/contended/backend_error/
  release_error) are class constants — typo-safe across the 9 use sites.
- Fix: inferTargetFromKey was indexing into segment 2 with limit 4,
  which returned the project sequence instead of the collection name
  ever since project-id was added to the key in c2a249c48b. Telemetry
  was tagging 'target' with project sequences instead of 'projects' /
  'users' / etc. Now indexes segment 3 with limit 5 to correctly pick
  the collection segment from lock:platform:{project}:{target}:...
2026-04-30 07:48:04 +01:00
Prem Palanisamy bae328efa1 revert: keep narrow RedisException catch on lock acquire
Reverts the catch widening from earlier — the underlying
Utopia\Lock\Distributed::tryAcquire only calls ext-redis methods,
which throw RedisException exclusively. Catching Throwable was
over-broad and would silently swallow real bugs (TypeError, Error)
into a fail-open path.
2026-04-30 07:42:59 +01:00
Prem Palanisamyandgreptile-apps[bot] 03575e68c4 fix(lock): re-read keys inside lock body to avoid sdks append loss
The sdks attribute is an append-only list, not idempotent. With the
read happening outside the lock, two sequential acquirers could each
read the same stale list and overwrite each other's appends.

Now the lock body re-reads the keys document and re-derives the sdks
array from the fresh state. Skip-on-contention still drops the update
when the lock is held, but a same-SDK retry on the next request picks
the registration up.

Bounded loss only affects the rare 'first-seen' SDK request that
happens to land while the lock is held; sequential traffic from the
same SDK (or any later request from any SDK) re-attempts and writes.

Co-authored-by: greptile-apps[bot]
2026-04-30 07:31:15 +01:00
Prem Palanisamy 2762e84974 fix(lock): broaden acquire catch + clone Log before reportError
Two P1s from review:

- Acquire path caught only \RedisException; any other Throwable from the
  lock library would escape and skip the fail-open callback. Widened to
  \Throwable so any backend failure falls back to running unlocked.
- reportError mutated the per-request $log directly, leaving it in a
  'lock.{action}' state that the request-end handler would then submit
  as the request log. Clones $log into a dedicated $lockLog so the
  shared instance is untouched.
2026-04-30 07:26:45 +01:00
Prem Palanisamy 7b2f6ac692 fix: add explicit parens to new ClassName calls in LockTest (PSR-12) 2026-04-30 07:13:51 +01:00
Prem Palanisamy 542aac7fda Merge remote-tracking branch 'origin/1.9.x' into distributed-lock
# Conflicts:
#	composer.lock
2026-04-30 06:53:31 +01:00
Prem Palanisamy 7430de293e test: unit tests for Lock facade
Covers the four public methods (set, run, runOrFail, withKey) plus the
kill switch and the project-fallback behavior.

Tests against a real Redis (the appwrite container has it always-on);
no markTestSkipped fallback — the suite fails loudly if Redis is
unreachable rather than silently passing.
2026-04-30 06:05:50 +01:00
Chirag AggarwalandGitHub 8ffe48d948 Merge pull request #12179 from appwrite/fix/project-delete-platform-cleanup 2026-04-30 09:43:37 +05:30
Chirag Aggarwal 4050b9ded1 Continue project cleanup after resource failures 2026-04-30 09:28:22 +05:30
Prem Palanisamy 2f2a124a06 revert: redis resource cluster support + _APP_CONNECTIONS_CACHE fallback
Cloud production runs four separate single-master+replica Dragonfly
deployments (cache, queue-dragonfly, queue-usage, pubsub-dragonfly),
not sharded Redis Cluster topology — confirmed by deploy/cloud/values
+ environments/production/*.values.yaml (Dragonfly Operator with
replicas=2 = 1 primary + 1 read replica), and by the dev DSN scheme
'redis://' (not 'redis-cluster://').

So a standard \Redis client suffices for the direct redis resource
(timelimit, Lock). Cloud just needs to pass _APP_REDIS_HOST/PORT/USER/
PASS through to the appwrite container — handled in the cloud PR's
docker-compose.yml change.

This reverts the resource to its original pre-PR shape. The
utopia-php/lock cluster-support PR (utopia-php/lock#1) stays open at
upstream as a future-ready option if cloud ever moves to actual
Redis Cluster mode.
2026-04-29 16:39:36 +01:00
Torsten DittmannandGitHub 6088fd55c8 Merge pull request #12138 from appwrite/feat-out-of-order-chunk-uploads 2026-04-29 18:04:57 +04:00
Matej BačoandGitHub 1aeee8c407 Merge pull request #12178 from appwrite/fix-developer-experience
Fix: Developer experiene with recent chagnes
2026-04-29 15:56:06 +02:00
Matej Bačo 36486ccc93 Fix tests 2026-04-29 14:41:19 +02:00
Chirag Aggarwal 794d8eac5b Fix project delete platform cleanup ordering 2026-04-29 17:55:06 +05:30
Matej Bačo 32ebfc6cb8 Fix backwards compatibility 2026-04-29 14:14:49 +02:00
Matej Bačo e1b8f5bf98 review improvements 2026-04-29 14:04:54 +02:00
Matej Bačo 4d86e67006 Fix missing scopes for tables 2026-04-29 14:03:44 +02:00
Matej Bačo b3e3b2a330 Fix missing index scopes 2026-04-29 14:00:14 +02:00
Matej Bačo e010bf25d5 Fix formatting 2026-04-29 13:57:16 +02:00
Matej Bačo aaf91f3816 Improve scopes quality 2026-04-29 13:52:13 +02:00
Matej Bačo bae61e8a05 Improve developer experience of keys endpoints 2026-04-29 13:13:13 +02:00
Torsten DittmannandGitHub dfbf45f4cc Merge branch '1.9.x' into feat-out-of-order-chunk-uploads 2026-04-29 15:03:33 +04:00
Chirag AggarwalandGitHub c264db6146 Merge pull request #12177 from appwrite/codex/fix-1-9-trivy-sarif-categories 2026-04-29 16:13:30 +05:30
Chirag Aggarwal cccafeff0c Add nightly SARIF upload guards 2026-04-29 16:02:41 +05:30
Chirag Aggarwal 360d08f087 Preserve CI image for job retries 2026-04-29 16:01:15 +05:30
Chirag AggarwalandGitHub d8e97ae47d Merge pull request #12174 from appwrite/bump-docker-base-1.2.0 2026-04-29 15:59:24 +05:30
Chirag Aggarwal d13e6d75f0 Fix Trivy SARIF categories on nightly scan 2026-04-29 15:58:59 +05:30
Prem Palanisamy c2a249c48b feat(lock): include project internal id in lock key + telemetry
Per-manager request, lock keys are now prefixed with the project's
internal id (sequence) so that:
  - Locks are partitioned by project — Redis cluster slot affinity
    if/when sharded.
  - Cross-project requests can't compete on the same key for
    collection-scoped resources.
  - Telemetry (counter + Sentry tags) carries 'project' alongside
    'target', so dashboards can filter contention by project.

Key shapes:
  set:        lock:platform:{project}:{collection}:{id}:{attribute}
  run/orFail: lock:platform:{project}:{collection}:{id}
  withKey:    raw (caller-provided)

Lock now requires a project document at construction. All existing
call sites (4 in CE + 2 in cloud) run inside Http::init()-resolved
request scope where the project document is set, so no migration
needed. Workers/CLI without project context can use withKey directly.
2026-04-29 11:26:18 +01:00
Chirag Aggarwal 4447396859 Update base image to 1.2.1 2026-04-29 15:37:59 +05:30
Chirag AggarwalandGitHub 4d1f229ec7 Merge branch '1.9.x' into bump-docker-base-1.2.0 2026-04-29 15:32:32 +05:30
Chirag AggarwalandGitHub 4f733f5984 Merge pull request #12176 from appwrite/ci-ghcr-image-share 2026-04-29 15:32:22 +05:30
Chirag Aggarwal 701f557755 ci: clean up GHCR CI image after pipeline finishes
Every CI run pushes ghcr.io/<repo>/appwrite-dev:<sha> and nothing
removes it. On an active repo with many PRs the GHCR storage grows
without bound. Add a cleanup job that runs after all consumer jobs
complete (always, even if some fail) and deletes the SHA-tagged
package version via the Packages API.

Addresses Greptile feedback on appwrite/appwrite#12176.
2026-04-29 15:23:05 +05:30
Jake BarnbyandGitHub 8ab26aab44 Merge pull request #12171 from appwrite/migration-refractor
Refactor migrations API to module style
2026-04-29 21:44:19 +12:00
Chirag Aggarwal ec3aa2b54f ci: share docker image via GHCR instead of upload-artifact
The build job uploads the appwrite-dev image as an actions artifact
(~hundreds of MB), and 30+ E2E test jobs all pull it concurrently with
actions/download-artifact. GitHub Actions' artifact storage struggles
with that many parallel downloads and intermittently fails with
BlobNotFound or 'Artifact download failed after 5 retries'.

Push the built image to ghcr.io/<repo>/appwrite-dev:<sha> in the build
job and pull from GHCR in each test job. GHCR handles parallel image
fetches without throttling.

Mirrors appwrite-labs/cloud#3906.
2026-04-29 15:09:39 +05:30
Chirag AggarwalandGitHub 2d636ff7ec Merge branch '1.9.x' into bump-docker-base-1.2.0 2026-04-29 15:06:03 +05:30
Matej BačoandGitHub fd42b8fa64 Merge pull request #12175 from appwrite/feat-console-key-scopes-endpoint
Feat: Console key scopes endpoint
2026-04-29 11:17:49 +02:00
Chirag Aggarwal 9d7df34590 fix: clean up php 8.5 runtime deprecations 2026-04-29 14:47:05 +05:30
Prem Palanisamy b2b9ac5b4d fix: redis resource reads _APP_CONNECTIONS_CACHE with _APP_REDIS_* fallback
The dedicated \Redis DI resource (used by timelimit and the new Lock
class) was reading _APP_REDIS_HOST/PORT/PASS exclusively. Cloud
deployments configure cache via _APP_CONNECTIONS_CACHE URI form
(e.g. cache=redis://dragonfly:6379) and don't pass the legacy
_APP_REDIS_* vars to the appwrite container locally, so timelimit and
Lock both fail to connect outside production where Helm separately
injects the legacy vars.

Now prefers _APP_CONNECTIONS_CACHE when set (matching the cache pool
backend), falls back to _APP_REDIS_* for CE-style configs. No new env
vars introduced; both timelimit and Lock work in CE, cloud-local, and
cloud-production without compose changes.
2026-04-29 10:16:17 +01:00
Matej Bačo e75fc5b859 Add list scopes endpoint for Console 2026-04-29 10:08:31 +02:00
Jake BarnbyandGitHub 57b8305144 Merge pull request #12134 from appwrite/fix-realtime-span-exporter
added a guard to skip double import
2026-04-29 20:02:04 +12:00
Chirag AggarwalandGitHub a429fb5860 Merge branch '1.9.x' into bump-docker-base-1.2.0 2026-04-29 13:30:50 +05:30
Matej BačoandGitHub aca11ed073 Merge pull request #12170 from appwrite/feat-create-dynamic-keys
Feat: create dynamic keys
2026-04-29 09:58:22 +02:00
Chirag Aggarwal 86123c9e93 fix: update PHP extension path for xdebug cleanup in production 2026-04-29 13:27:04 +05:30
Chirag Aggarwal a58ea1123b chore: bump docker-base to 1.2.0 2026-04-29 13:21:17 +05:30
Prem Palanisamy 18a67e00d3 fix: refresh composer.lock content-hash 2026-04-29 07:55:38 +01:00
Prem Palanisamy e634145612 refactor: consolidate lock implementation into Lock class
Lock now uses Utopia\Lock\Distributed directly and owns the full
acquire/release/telemetry/error-reporting/fail-open/kill-switch logic
that previously lived in two inline DI factory closures.

Adds withKey($key, $fn, $ttl, $orFail, $waitTimeout) as a generic
escape hatch for non-platform key shapes (cache, queue, edge) and
unusual TTL/timeout requirements.

Per-attribute lock keys for set() so that an accessedAt bump and a
mcpAccessedAt bump on the same projects:{id} document don't compete.
Whole-document operations (run, runOrFail) keep document-level keys.

Removes the standalone distributedLock and distributedLockOrFail DI
factories — Lock is the single API.

request.php shrinks ~150 LOC; Lock.php grows to ~190 LOC.
2026-04-29 07:41:54 +01:00
Prem Palanisamy ce15eeb722 refactor: introduce Lock facade for platform-DB lock sites
Extracts the lock-key format and the lock+auth-skip+sparse-update pattern
into Appwrite\Locking\Lock with three methods:
  - set(collection, id, attribute=accessedAt, value=null) — throttled
    single-attribute write
  - run(collection, id, fn) — generic skip-on-contention
  - runOrFail(collection, id, fn) — block-then-409 for the deferred
    lost-update follow-up

Migrates the 4 call sites (router projects accessedAt + 3 in shared/api)
off the raw $distributedLock callable. Raw factories stay as escape
hatches for non-platform key shapes.
2026-04-29 07:17:04 +01:00
ArnabChatterjee20kandGitHub dae9cbcf45 Merge pull request #12070 from appwrite/realtime-action-channels
Realtime action channels
2026-04-29 10:49:13 +05:30
Prem Palanisamy b15457bcca style: trim verbose comments on lock factories and call sites 2026-04-29 05:50:37 +01:00
Prem Palanisamy fce2abfd4c revert: scope distributed-lock PR to thundering-herd sites only
Drop the 18 lost-update endpoint locks (Project/* settings + Projects
team/update). Those address a different bug class (read-modify-write
races) than the manager-flagged production problem (regions slow queries
to platform from thundering herd on accessedAt writes).

Kept:
- distributedLock + distributedLockOrFail factories on the per-request
  container, GENERAL_RESOURCE_LOCKED exception, 409 mapping
- 4 thundering-herd sites: cache-invalidation in shared/api.php (3) +
  router projects.accessedAt in general.php (1)

Dropped:
- 18 endpoint OrFail wires
- testConcurrentTogglesAllPersist + Swoole-cURL test client patches
- dev/test-distributed-lock.sh smoke script
2026-04-29 05:31:18 +01:00
premtsd-codeandGitHub da5382d58a Merge branch '1.9.x' into distributed-lock 2026-04-29 06:34:56 +05:30
Prem Palanisamy 380cc3eb27 refactor: drop log/logger boilerplate from lock call sites
The previous shape required every caller to thread `log: $log, logger: $logger`
as named args into each `distributedLock(...)` invocation, plus inject `log`
and `logger` into the surrounding action just to forward them to the lock.
Across 21 call sites this added ~100 LOC of pure plumbing.

The cause: the lock factory was registered on the global container in
`app/init/resources.php`, where per-request resources like `log` aren't
visible. That forced the factory to expose its inner closure with optional
`?Log $log = null, ?Logger $logger = null` params, which every caller had
to satisfy.

Move the lock factory + its `lockErrorReporter`/`lockTargetOf` helpers from
the global container to the per-request container (`resources/request.php`),
and add `'log'` + `'logger'` to the factory's dep list. The factory closure
now runs per-request and closes over the per-request `Log`/`Logger`. Inner
closure returned to callers no longer needs the optional params, and call
sites drop the named args entirely.

Knock-on cleanup:
- Drop `->inject('log')`, `->inject('logger')`, the corresponding action
  params, and `use Utopia\Logger\{Log,Logger}` imports from 19 endpoint
  files where they were only there for the lock
- Drop the same plumbing from `app/controllers/shared/api.php` (3 lock call
  sites)
- Drop just the Logger plumbing from `app/controllers/general.php` (router
  function + 3 callbacks); `Log` is kept because it's used elsewhere in
  that file
- Net 120 LOC removed across 23 files

No behavior change: the lock factories still produce the same closures
(skip-on-contention `distributedLock`, blocking-with-409 `distributedLockOrFail`).
The static lockErrorReporter rate limiter (1 push per 60s per
`(action, target)` bucket) continues to work — it lives on a closure-static
in the helper, which is independent of where the helper is constructed.

Verified end-to-end: testConcurrentTogglesAllPersist passes 4/5 (the cold-
start race flake is the same one we've consistently seen and is orthogonal
to lock changes).
2026-04-29 02:02:28 +01:00
Prem Palanisamy b29f9f4a45 feat: distributed lock on router projects.accessedAt RMW
Every request that arrives via a custom-domain rule (router path) reads
the project's `accessedAt` timestamp and, if the throttle window
(`APP_PROJECT_ACCESS`) has elapsed, writes a fresh value. With concurrent
traffic across multiple pods, this is a per-row hot RMW that loses
updates silently — the surviving timestamp depends on which pod's write
landed last.

Wrap the read-modify-write in `distributedLock('lock:platform:projects:{id}')`
(skip-on-contention variant). Every concurrent pod would write the same
throttled value, so losing the race is correct: the winner's update covers
ours.

Wires `distributedLock` and `?Logger` through:
  - `router()` function signature (app/controllers/general.php:70)
  - the three Http::init / Http::get callbacks that invoke router():
    `*` catch-all (init), `/robots.txt`, `/humans.txt`

Two related cloud-only RMW sites (`teams.accessedAt`,
`projects.mcpAccessedAt`) live in `appwrite-labs/cloud` and need a
follow-up PR there. They depend on this branch reaching 1.9.x so the
`distributedLock` DI resource is available downstream.
2026-04-29 01:36:38 +01:00
Matej Bačo 05f2d2b9cf Fix tests 2026-04-28 19:29:37 +02:00
Matej Bačo c1f61b22aa Merge branch '1.9.x' into feat-create-dynamic-keys 2026-04-28 17:18:36 +02:00
Matej Bačo 980762fc3e Rename from dynamic key to ephemeral key (api keys) 2026-04-28 17:18:06 +02:00
Matej Bačo c96836b1c0 Improve code quality of folder decoding project ID 2026-04-28 17:10:58 +02:00
Matej Bačo 15917ac7ba Fix failing tests 2026-04-28 17:05:30 +02:00
premtsd-codeandGitHub cd851bff24 Merge branch '1.9.x' into migration-refractor 2026-04-28 20:32:54 +05:30
Prem Palanisamy 3f5dcc81fd Refactor migrations API to module style 2026-04-28 15:57:41 +01:00
Matej Bačo f5a732d231 Add dynami key integration test 2026-04-28 16:47:39 +02:00
Matej Bačo 72dfd8a7bc Add E2E tests for dynamic keys 2026-04-28 16:45:00 +02:00
Matej Bačo 11f80fc2ed Solve key projectId backwards compatibility 2026-04-28 16:35:40 +02:00
Harsh MahajanandGitHub 547709a1d8 Merge pull request #12167 from appwrite/feat/impersonation-query-params
feat: add query param fallback for impersonation headers
2026-04-28 19:51:23 +05:30
Matej Bačo ccb0ddd578 Bug&test fixing 2026-04-28 16:18:36 +02:00
Matej Bačo b2ce95a0cd Dynamic key backwards compatibility 2026-04-28 16:14:10 +02:00
Matej Bačo ed9b47f6ce Migrate project jwt to dynamic api key 2026-04-28 15:57:37 +02:00
harsh mahajan 2a357511ea fix: use unique emails and phone in query param impersonation test 2026-04-28 19:17:25 +05:30
Harsh MahajanandGitHub 67d24d3ef1 Merge branch '1.9.x' into feat/impersonation-query-params 2026-04-28 19:11:14 +05:30
harsh mahajan 87ed7c3817 feat: add query param fallback for all impersonation params and simplify tests 2026-04-28 19:10:55 +05:30
Matej Bačo 8f176166c9 Re-introduce project JWT endpoint 2026-04-28 15:31:10 +02:00
Prem Palanisamy 29a0d6c2bf feat: distributed locks on all platform-projects-doc writes
The pilot on `Project/Services/Update.php` (commit fb0d43daf3) fixed the
silent lost-update on the `services` JSON map. The same read-modify-write
pattern exists on 17 other endpoints — all writing to the SAME `projects`
document, mostly via sparse `updateDocument`. Concurrent writes silently
overwrite each other, both within an endpoint (two toggles to different
auth methods) and across endpoints (parallel SMTP update + service toggle).

The worst offender is `Projects/Update.php` (`PATCH /v1/projects/:projectId`):
it calls `setAttribute` 11 times then writes the WHOLE document back, so a
concurrent write to *any* attribute on the project doc — services, auths,
smtp, templates, mockNumbers — gets clobbered.

Wrap every read-modify-write window with `distributedLockOrFail` keyed on
`lock:platform:projects:{$projectId}`. The single shared key serializes
both same-endpoint and cross-endpoint races on the same project.

Endpoints now under the lock:

  Projects module (console-managed, plural):
    - Projects/Update                  (full doc setAttribute chain)
    - Projects/Team/Update             (sparse + computed permissions; cascade
                                        to installations/repositories/vcsComments
                                        runs after release — separate collections)

  Project module (current-project, singular):
    - AuthMethods/Update               (auths[authKey])
    - Protocols/Update                 (apis[protocolId])
    - SMTP/Update                      (smtp map; PHPMailer probe inside lock —
                                        10s default TTL covers Timeout=5)
    - Templates/Email/Update           (templates['email.X-locale'])
    - MockPhone/{Create,Delete,Update} (auths['mockNumbers'][])
    - Policies/MembershipPrivacy       (auths['memberships*'])
    - Policies/PasswordDictionary      (auths['passwordDictionary'])
    - Policies/PasswordHistory         (auths['passwordHistory'])
    - Policies/PasswordPersonalData    (auths['personalDataCheck'])
    - Policies/SessionAlert            (auths['sessionAlerts'])
    - Policies/SessionDuration         (auths['duration'])
    - Policies/SessionInvalidation     (auths['invalidateSessions'])
    - Policies/SessionLimit            (auths['maxSessions'])
    - Policies/UserLimit               (auths['limit'])

Each endpoint:
  1. Injects `distributedLockOrFail`, `log`, `logger`
  2. Re-reads the project document inside the lock so the baseline reflects
     any update that landed between request init and lock acquisition
  3. Throws `GENERAL_RESOURCE_LOCKED` (HTTP 409) on contention timeout

SMTP/Update and Templates/Email/Update additionally refactor variable-
variables (`${$key}`) to an explicit input array — PHPStan can't trace
dynamic references through closure `use` clauses.
2026-04-28 14:13:31 +01:00
Torsten Dittmann a0ef145b92 Merge branch '1.9.x' of https://github.com/appwrite/appwrite into feat-out-of-order-chunk-uploads 2026-04-28 17:10:56 +04:00
Matej BačoandGitHub 3d3f5934c6 Merge pull request #11993 from appwrite/feat-public-oauth2-endpoints
Feat: Public project OAuth2 configuration API
2026-04-28 12:41:50 +02:00
Torsten Dittmann 9e1f8af103 fix: persist sourceChunksTotal/Uploaded in finalization createDocument paths
Greptile review: Functions and Sites finalization branches reached via
single-chunk uploads or out-of-order last-chunk assembly omitted
sourceChunksTotal and sourceChunksUploaded in createDocument. This caused
the retry guard to evaluate 0 === 1 on retry, missing and queuing duplicate
builds.
2026-04-28 13:44:41 +04:00
Torsten DittmannandGitHub b055ff1066 Merge branch '1.9.x' into feat-out-of-order-chunk-uploads 2026-04-28 13:19:08 +04:00
harsh mahajan f0cbfbbbe4 fix: use assertEmpty for impersonatorUserId to match response model 2026-04-28 14:31:49 +05:30
Matej Bačo cb4cff120b Add Keycloak oauth support 2026-04-28 10:54:13 +02:00
Matej Bačo 49e6a38e7f Add fusionauth oauth 2026-04-28 10:43:16 +02:00
Prem Palanisamy 752df21007 refactor: switch distributed-lock backend to utopia-php/lock
`utopia-php/lock` v0.2.0 was published this week and provides the same
Redis SET-NX-EX + Lua-compare-and-delete primitive we built locally as
`premtsd-code/lock`. Drop the dev-preview package in favor of the
official Utopia PHP library.

- composer: replace `premtsd-code/lock` with `utopia-php/lock` 0.2.*
  (still via VCS — not on Packagist yet)
- resources.php: rewire both factory variants
  - `Lock + Adapter\Redis` → `Distributed`
  - `acquire()` → `tryAcquire()` for skip variant
  - `acquire(blocking: true, waitTimeout)` → `acquire($waitTimeout)` for
    OrFail variant
  - `LockAcquireException` → `\RedisException`
  - `(int) $ttl` cast — utopia-php/lock takes seconds as int
- docker-compose: thread `_APP_LOCKING_ENABLED` into the appwrite
  service environment so the kill switch documented in
  `app/config/variables.php` is actually usable from `.env`

Verified end-to-end on local stack:
- positive case (locking enabled): 5/5 testConcurrentTogglesAllPersist
  pass, lock keys observed in `redis-cli MONITOR` with concurrent SET
  NX contention
- negative case (locking disabled): 1/3 detect lost updates as before
2026-04-28 09:38:08 +01:00
Matej Bačo dfa3ae5274 Fix tests 2026-04-28 10:19:36 +02:00
Matej Bačo 543765a22a Improve copy 2026-04-28 10:15:45 +02:00
Matej Bačo e2bb9a9161 Simplify oauth endpoints 2026-04-28 10:08:39 +02:00
harsh mahajan bda823ac0e chore: format 2026-04-28 13:38:00 +05:30
harsh mahajan 3dd5a51ba4 style: fix method argument spacing (Pint PSR-12) 2026-04-28 13:34:01 +05:30
harsh mahajan 5afc8f462d fix: allow same-site in CSRF guard to support Console on subdomains 2026-04-28 13:26:13 +05:30
harsh mahajan ed0c7b4e12 test: add CSRF attack prevention test for impersonateUserId query param 2026-04-28 13:24:15 +05:30
Matej Bačo d25707346f Add console oauth endpoint 2026-04-28 09:47:27 +02:00
harsh mahajan a3f6cf4645 fix: restrict CSRF guard to same-origin only, drop same-site 2026-04-28 13:00:18 +05:30
harsh mahajan 9a175c5098 test: add E2E tests for impersonateUserId query param and CSRF guards 2026-04-28 12:56:17 +05:30
harsh mahajan 5465be6301 fix: make CSRF guard fail-closed by requiring explicit same-origin Sec-Fetch-Site 2026-04-28 12:27:57 +05:30
harsh mahajan 46a457bfa3 fix: block impersonateUserId query param on cross-site requests to prevent CSRF 2026-04-28 12:10:51 +05:30
harsh mahajan 4c989f99c3 fix: cast impersonateUserId query param to string to prevent array injection 2026-04-28 12:05:02 +05:30
harsh mahajan 8f1d73a6cb chore: clarify intentional header-only restriction for email/phone impersonation 2026-04-28 12:02:00 +05:30
harsh mahajan 01b5fa8ecb fix: restrict impersonation query param fallback to userId only
Remove query param fallback for impersonateEmail and impersonatePhone
to avoid PII exposure in server logs, browser history, and Referer
headers. Only impersonateUserId (an opaque internal ID) is safe to
pass via URL query param.
2026-04-28 11:58:25 +05:30
harsh mahajan d73b7a70d8 feat: add query param fallback for impersonation headers
Allow impersonation to be specified via URL query params
(?impersonateUserId, ?impersonateEmail, ?impersonatePhone) as a
fallback to the existing headers, enabling Console to embed
impersonation in direct file/image URLs where headers cannot be set.
2026-04-28 11:44:39 +05:30
ArnabChatterjee20k f71a2dfddc changed the condition to app edition for the loading of the span 2026-04-28 11:07:16 +05:30
Damodar LohaniandGitHub cefd063c55 Merge pull request #12165 from appwrite/fix/CLO-4280-getheader-string-coerce
fix: coerce non-string header values in Request::getHeader
2026-04-28 10:43:40 +05:45
Damodar LohaniandGitHub c924cbcc59 Merge pull request #12166 from appwrite/fix/CLO-4279-favicon-empty-body
fix: guard DOMDocument::loadHTML against empty body in favicon endpoint
2026-04-28 10:32:32 +05:45
Damodar LohaniGitHubgreptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
81321e82d1 Update src/Appwrite/Platform/Modules/Avatars/Http/Favicon/Get.php
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-04-28 10:05:01 +05:45
Damodar Lohani 30a511692b test: add unit coverage for Request::getHeader non-string coercion
Refs CLO-4280
2026-04-28 04:15:00 +00:00
Damodar Lohani 9637409831 fix: coerce non-string header values in Request::getHeader
Closes CLO-4280
2026-04-28 03:54:35 +00:00
Damodar Lohani c4f6b11706 fix: guard DOMDocument::loadHTML against empty body in favicon endpoint
Closes CLO-4279
2026-04-28 03:54:34 +00:00
Matej Bačo ad4178aa42 Fix missing lib params for domain 2026-04-27 18:33:30 +02:00
Prem Palanisamy 92b5f0dcd6 feat: report lock backend/release errors to logger (Sentry/Raygun/etc.)
Lock backend errors (Redis/Dragonfly unreachable) and release errors
(TTL expired or backend dropped while held) were previously visible only
in the lock.attempts counter and Console::warning lines. They now also
push a structured Log entry through the configured logger adapter, so
operators using Sentry/Raygun/AppSignal/LogOwl get first-class events
for these specific failure modes.

Pattern matches Embeddings/Text/Create.php exactly:

  - Action injects 'log' (per-request Log object) and 'logger'
    (?Logger, nullable when _APP_LOGGING_CONFIG unset).
  - Helper mutates the per-request $log instead of constructing a
    fresh one — preserves the per-request context Embeddings expects.
  - Same field set: namespace='http', server, version, type,
    setMessage, setAction, setEnvironment, addTag('code', ...),
    addExtra('file' / 'line' / 'trace').
  - Defensive try/catch around addLog() so logging failures don't
    break fail-open.

Lock-specific tags added for slicing in Sentry:

  - lock.target — collection name (projects, keys, users, ...).
    Bounded set, safe for high-cardinality stores.
  - lock.key_pattern — full key with the trailing document ID
    stripped (lock:platform:projects:* not lock:platform:projects:abc).
    Prevents unbounded log cardinality from per-document IDs.

Rate limiting via per-pod static buckets, 60s window per
(action, target) combo. During a 5-minute Dragonfly outage, a fleet
of N pods produces at most N events/min, well within Sentry's dedup
tolerance. Static state is per-Swoole-worker; coroutines may race
on the bucket boundary but the worst case is one duplicate report.

Type level set to Log::TYPE_WARNING (not ERROR): fail-open means the
request still succeeds, so this is degraded operation, not a failed
request.

Deliberately NOT reported to Sentry:

  - 409 GENERAL_RESOURCE_LOCKED (normal user-facing concurrency)
  - skip-on-contention events (idempotent fan-out by design)
  - acquire retry conflicts (internal loop)
  - destructor cleanups (have an expected baseline rate; the
    lock.attempts counter aggregates them better than Sentry would)

Factory signature change: distributedLock and distributedLockOrFail
now accept ?Log and ?Logger as optional named args at call time
(rather than capturing Logger at factory-build time). The factory
closure runs once at boot but the per-request Log resource is
fresh per request — capturing at boot would have given stale state.
Existing call sites threaded log: $log, logger: $logger. Sites that
don't (workers, CLI tasks) get null and just log to Console as
before.
2026-04-27 17:25:31 +01:00
Prem Palanisamy 77982f4b90 test: concurrency proof for distributedLockOrFail pilot
Two complementary tests for the lost-update bug the lock fixes, plus
the test-Client patches needed to make Swoole-coroutine HTTP work.

- tests/e2e/Services/Project/ServicesBase.php :: testConcurrentTogglesAllPersist
  Fires N parallel PATCHes via Swoole\Coroutine\run + SWOOLE_HOOK_CURL,
  then refetches the project and asserts (successCount == enabledCount).
  With the lock disabled (_APP_LOCKING_ENABLED=disabled) sparse
  updateDocument() calls overwrite each other and the assertion fails —
  proving the test detects the bug.

- dev/test-distributed-lock.sh
  Same proof via curl + bash background jobs. Runnable outside the
  PHPUnit suite for manual verification or load-style sweeps. Reads
  APPWRITE_ENDPOINT / APPWRITE_PROJECT_ID / APPWRITE_API_KEY.

- tests/e2e/Client.php (test util):
    * Skip CURLOPT_PATH_AS_IS (option 234) when SWOOLE_HOOK_CURL is
      active. Swoole's emulated cURL doesn't support it; setting it
      fatal-errors as soon as any test enables the cURL hook.
    * Don't redundantly set CURLOPT_NOBODY=false on non-HEAD requests
      (false is cURL's default). Under Swoole's hooked cURL this
      strips the body of PATCH/PUT requests, hitting the framework's
      404 fallback instead of the intended route.
  Both changes preserve native (non-hooked) cURL behavior unchanged.
  They unblock any future test that wants real parallel HTTP via
  Swoole\Coroutine\run + the cURL hook.

Both follow the same proof-of-bug pattern: run with locking enabled
(must pass) AND with it disabled (must fail). Verified locally against
the running stack:
  _APP_LOCKING_ENABLED=enabled  -> PASS  (15 assertions)
  _APP_LOCKING_ENABLED=disabled -> FAIL  (successCount=5 enabledCount=4)
2026-04-27 17:25:15 +01:00
Prem Palanisamy 784babcf45 fix: address Greptile review on PR #12062
Three P1 issues flagged on the initial commit:

1. Lock key in updateProjectService used "platform:project:{id}" —
   missing the "lock:" namespace prefix and using singular "project"
   instead of the conventional plural collection name. The factory's
   `lockTargetOf` extracts segment [2] as the telemetry target, so
   the broken key was emitting the project ID itself as the target
   attribute (cardinality blowup, broken dashboards). Fixed to
   "lock:platform:projects:{id}" matching the convention used in
   shared/api.php.

2. The 409 contention exception embedded the raw Redis lock key in
   its user-facing message, leaking internal collection names and
   the locking namespace to API clients. Removed the custom message
   so the catalog default ("The requested resource is currently
   being modified...") is used. Telemetry already carries the
   target collection for operator-side observability.

3. _APP_LOCKING_ENABLED variable doc had `introduction: '1.10.0'`
   on a 1.9.x-targeted PR. Corrected to '1.9.3' (next 1.9.x patch).
2026-04-27 17:24:50 +01:00
Matej Bačo 1f16b0d9e7 Fix failing startup 2026-04-27 18:21:21 +02:00
Matej Bačo 015aee087a Fix write only security 2026-04-27 18:04:22 +02:00
Matej Bačo 50d86c5b5d Update ci.yml 2026-04-27 17:45:52 +02:00
Matej Bačo 3d43530225 Fix failing test 2026-04-27 17:41:13 +02:00
Matej Bačo d0d536a2dd Improve test coverage 2026-04-27 17:40:49 +02:00
Matej Bačo 4b620bb31a Improve test coverage 2026-04-27 17:27:23 +02:00
Matej Bačo ca7f36a9b8 Fix bugs by improving tests 2026-04-27 17:17:57 +02:00
Matej Bačo ec3c7f1ad6 Fix failing oauth tests 2026-04-27 17:02:53 +02:00
Matej Bačo ecba11eba5 Brin back removed tests 2026-04-27 16:54:53 +02:00
Matej Bačo 7a96b024b3 Fix tests 2026-04-27 16:51:01 +02:00
Matej Bačo 4ba413fcc0 Fix bugs when implementing tests 2026-04-27 16:50:14 +02:00
Matej Bačo af95e71244 Add OAUth update tests 2026-04-27 16:02:19 +02:00
Matej Bačo ee1eea5c0c oauth tests setup 2026-04-27 15:51:54 +02:00
Matej Bačo b28b851bb2 microsoft oauth endpoint 2026-04-27 15:49:44 +02:00
Torsten Dittmann 2f2da98cca fix: adjust out-of-order test expectations and chunk sizes
- Functions/Sites: lower minimum chunk requirement from 3 to 2
- Sites: use random_bytes instead of str_repeat for non-compressible test data
- Remove assertions on sourceChunksTotal/Uploaded from response body (not in response model)
2026-04-27 17:46:20 +04:00
Torsten Dittmann 54997638e8 fix: persist sourceChunksUploaded on finalization and avoid variable shadowing
- Functions/Sites: include sourceChunksUploaded in updateDocument when finalizing existing deployments, fixing the retry guard
- Functions test: rename loop variable to avoid shadowing setup result
2026-04-27 17:24:51 +04:00
ArnabChatterjee20k 70b9c60e2c test(Messaging): validate that bare functions channel is not emitted in published channels 2026-04-27 18:46:04 +05:30
Torsten Dittmann 49d2db65e6 feat: support out-of-order chunked uploads
- Add APP_LIMIT_UPLOAD_CHUNK_SIZE constant (5MB) matching official SDKs
- Replace dynamic chunk calculation with fixed 5MB chunk math in all upload endpoints
- Remove -1 last-chunk sentinel that broke when last chunk arrived first
- Fix duplicate-retry guards: return existing resource instead of erroring for chunked uploads
- Add out-of-order e2e tests for Storage, Functions, and Sites
- Upgrade utopia-php/storage to 2.0.0 for device-level out-of-order assembly support
2026-04-27 17:15:00 +04:00
ArnabChatterjee20k 1fdcca9592 added a guard to skip double import 2026-04-27 18:33:47 +05:30
ArnabChatterjee20k cb8640b56f feat(Realtime): enhance channel management for user authentication and account actions 2026-04-27 18:24:52 +05:30
Matej Bačo a781325679 Add oauth read operations 2026-04-27 14:47:47 +02:00
ArnabChatterjee20k 3f12062259 updated 2026-04-27 17:54:48 +05:30
Matej Bačo 2e57500d7e WIP: Read endpoints for oauth 2026-04-27 14:16:43 +02:00
Matej Bačo a1a88ae57e Make oauth secret write only 2026-04-27 14:09:24 +02:00
ArnabChatterjee20k 9553f8a9f8 refactor(MessagingTest): update method visibility and naming conventions for consistency 2026-04-27 17:35:56 +05:30
Matej Bačo 15f94d99ca Add Kick OAuth adapter 2026-04-27 14:02:30 +02:00
ArnabChatterjee20k ca105ff9bc feat(Realtime): implement rebindAccountChannels method for userId changes and add corresponding tests 2026-04-27 17:31:31 +05:30
ArnabChatterjee20k 7e3114d733 linting 2026-04-27 17:26:27 +05:30
ArnabChatterjee20k ef4b9c4934 updated 2026-04-27 17:16:17 +05:30
Matej Bačo 2e960b90df Fix unused env variable 2026-04-27 13:38:26 +02:00
ArnabChatterjee20k 1928605bd9 linting 2026-04-27 16:42:45 +05:30
ArnabChatterjee20k 340ce9d56b Add tests for channel conversion and event handling in Messaging
- Implement `test_convert_channels_rewrites_account_action_suffixes` to ensure
  that account action suffixes are correctly rewritten to user-scoped channels.
- Add `test_convert_channels_drops_account_actions_for_guest` to verify that
  account actions are dropped for guests without a user ID.
- Introduce `test_from_payload_does_not_suffix_account_for_nested_user_events`
  to confirm that nested user events do not leak action suffixes onto account channels.
2026-04-27 16:40:15 +05:30
ArnabChatterjee20k e6d5c216eb refactor(Realtime): update action extraction logic and enhance test method naming conventions 2026-04-27 16:06:00 +05:30
ArnabChatterjee20k d25ccb784d refactor(Realtime): remove SUPPORTED_ACTIONS constant and simplify action extraction logic 2026-04-27 15:59:34 +05:30
Matej Bačo 8ce7aa2abe Fix crashing http 2026-04-27 12:27:52 +02:00
ArnabChatterjee20k 78715e4a1a refactor(tests): rename test methods to snake_case and update assertions for action channels
- Changed test method names from camelCase to snake_case for consistency.
- Updated assertions to ensure action channels are correctly emitted and filtered.
- Improved readability and maintainability of the test suite by restructuring test cases.
2026-04-27 15:46:02 +05:30
ArnabChatterjee20k df57ee2a32 added unit test 2026-04-27 13:43:23 +05:30
ArnabChatterjee20k 6d4a66fbb3 Enhance Realtime adapter to support action-channel awareness in subscriber checks and add corresponding tests 2026-04-27 13:30:18 +05:30
ArnabChatterjee20k 3aee54747c Enhance Realtime adapter to support delete action and add corresponding tests 2026-04-27 13:15:04 +05:30
ArnabChatterjee20k d2423a5bb5 added tests 2026-04-27 12:57:35 +05:30
ArnabChatterjee20k c0c053ff20 Enhance Realtime adapter with action channel support and tests
- Introduced ACTION_ALL and SUPPORTED_ACTIONS constants for better action handling.
- Updated channel subscription logic to support action suffixes.
- Added tests for action channel parsing and filtering in MessagingTest.
2026-04-27 12:52:52 +05:30
Prem Palanisamy fb0d43daf3 feat: distributed locking for platform-database writes
Adds two DI factories and wires them where coordination is needed:

  - distributedLock — skip on contention, void return. For idempotent
    fan-out where N pods doing the same write is wasteful but losing
    the race is correct.
  - distributedLockOrFail — blocking acquire (3s default) then throws
    GENERAL_RESOURCE_LOCKED (HTTP 409) on contention. For
    read-modify-write on shared mutable state where a silent skip
    would drop a user's change.

Both factories: _APP_LOCKING_ENABLED kill switch (set 'disabled' for
fail-open), fail-open on Redis-unreachable, and a lock.attempts
telemetry counter sliced by outcome and target collection.

Wired sites:
  - shared/api.php × 3 (distributedLock): keys.accessedAt + sdks,
    projects.accessedAt, users.accessedAt. Reduces redundant writes
    and cache-purge fan-out under request bursts on the same project.
  - Project/Services/Update.php × 1 (distributedLockOrFail): the
    services map toggle. Re-reads inside the lock so the baseline
    reflects concurrent updates. Two simultaneous toggles to
    different services no longer lose one of them.

Lock key namespace: lock:platform:{collection}:{id}.

Dep: premtsd-code/lock pinned to a specific commit as a development
preview. Migration to utopia-php/lock is a follow-up once that
package is published.
2026-04-27 07:54:56 +01:00
Matej Bačo e4bfb38a57 add okta provider 2026-04-26 11:14:50 +02:00
Matej Bačo 0a7b7de197 Revert changes - default works as fallback for optional serverID 2026-04-26 10:59:29 +02:00
Matej Bačo 51c0767be2 Make okta server ID optional 2026-04-26 10:56:41 +02:00
Matej Bačo d25dac7d60 Manual quality improvmenets 2026-04-26 10:29:41 +02:00
Matej BačoandGitHub 1f18e16310 Merge branch '1.9.x' into feat-public-oauth2-endpoints 2026-04-25 12:45:34 +02:00
Matej Bačo d0f6daa67a Fix integration test 2026-04-25 12:05:35 +02:00
Matej Bačo 184399023c Add github integration test 2026-04-25 11:58:09 +02:00
Matej Bačo a588a62277 Prepare env for cicd integration with github oauth 2026-04-25 11:57:40 +02:00
Matej Bačo ffd0dbd406 Add OIDC endpoint 2026-04-25 10:20:00 +02:00
Matej Bačo 8200d079c6 Simplify specs 2026-04-24 16:37:27 +02:00
Matej Bačo d9d87f813f apple oauth endpoints 2026-04-24 16:31:21 +02:00
Matej Bačo db7acd4b8b More OAuth endpoints 2026-04-24 15:02:36 +02:00
Matej Bačo a62ca8612d More OAuth endpoints 2026-04-24 14:31:38 +02:00
Matej Bačo 975da667f5 Remove leftover 2026-04-24 14:23:19 +02:00
Matej Bačo 6cfb12c48b Improve OAuth SDK quality 2026-04-24 14:23:04 +02:00
Matej Bačo 8cdcd379c8 Add more oauth endpoints 2026-04-24 14:15:34 +02:00
ArnabChatterjee20kandGitHub 1b8123bf62 Merge pull request #11992 from appwrite/realtime-logs
added missing include for exporter in the realtime
2026-04-24 16:45:28 +05:30
ArnabChatterjee20kandGitHub 1ca75c73df Merge branch '1.9.x' into realtime-logs 2026-04-24 16:35:25 +05:30
Matej Bačo fe08978851 More OAuth provider endpoints 2026-04-24 12:58:32 +02:00
ArnabChatterjee20k 0633662695 removed dispatch experiment 2026-04-24 16:22:57 +05:30
ArnabChatterjee20k 89819db775 added exporter 2026-04-24 16:12:42 +05:30
Matej Bačo faf09ed7c5 Abstrated oauth response model 2026-04-24 12:38:12 +02:00
Matej Bačo c097d9fcdd Dropbox adapter 2026-04-24 12:20:48 +02:00
Matej Bačo dac184b281 abstract oauth adapters 2026-04-24 12:06:58 +02:00
Matej Bačo 335b1c2f6c Figma OAuth endpoint 2026-04-24 11:45:59 +02:00
Matej Bačo 5fbe6cba79 Improve github samples 2026-04-24 11:39:14 +02:00
Matej Bačo 36435d940d Add Discord OAuth endpoint 2026-04-24 11:35:30 +02:00
Matej Bačo 93f7a0d902 GitHub oauth endpoint 2026-04-24 11:17:18 +02:00
Matej Bačo 7fbfb6266b GitHub oauth response model 2026-04-24 10:56:39 +02:00
Matej BačoandGitHub 29b700d1ec Merge pull request #11981 from appwrite/feat-public-list-endpoints
Feat: Project public list endpoints
2026-04-24 10:08:23 +02:00
Matej Bačo e3231393b9 Fix anayser 2026-04-23 16:06:45 +02:00
Matej Bačo 5beeca5a99 Placeholder test 2026-04-23 15:57:09 +02:00
Matej Bačo 4de3009f67 Fix analyser 2026-04-23 15:36:16 +02:00
Matej Bačo 4b3963512c Linter fix 2026-04-23 15:28:20 +02:00
Matej Bačo 8c634a95e4 Fix failing tests 2026-04-23 15:28:10 +02:00
Matej Bačo 7a3c001452 Re-add project removal tests 2026-04-23 15:22:40 +02:00
Matej Bačo a48fd13ced Add getPolicy + tests + move wrongly placed project tests 2026-04-23 15:19:49 +02:00
Matej Bačo 9c6ed9565e Remove tests of removed endpoints 2026-04-23 14:07:58 +02:00
Matej Bačo bdbc5b92df Fix after code review 2026-04-23 13:47:31 +02:00
Matej Bačo c246fb0f83 Project deletion tests 2026-04-23 13:41:11 +02:00
Matej Bačo a0a3849b16 Remove unsupported bulk endpoints 2026-04-23 13:37:32 +02:00
Matej Bačo b99139661e Migrate delete project endpoint 2026-04-23 13:37:19 +02:00
Matej Bačo 6d86b8fd0d Removal of project JWTs 2026-04-23 13:25:21 +02:00
Matej Bačo cef7a5197f List policies API 2026-04-23 13:24:39 +02:00
Matej Bačo c1dfeae323 Add queries to email tempaltes list 2026-04-23 13:06:05 +02:00
Matej Bačo 51fa0770a6 Add queries to mock numbers list 2026-04-23 12:43:45 +02:00
Matej BačoandGitHub d46403507c Merge pull request #11979 from appwrite/fix-membership-privacy
Fix: membership privacy bug on production
2026-04-23 10:47:54 +02:00
Matej Bačo 83724ce96f Console membership privacy test coverage 2026-04-23 10:37:35 +02:00
Matej Bačo 34930e6d67 Merge branch '1.9.x' into fix-membership-privacy 2026-04-23 10:18:32 +02:00
Matej BačoandGitHub 096f8041fd Merge pull request #11970 from appwrite/feat-mocks-public-api
Feat: Public mock phone APIs
2026-04-23 10:18:14 +02:00
Matej Bačo 9dad7cef9e Merge branch '1.9.x' into feat-mocks-public-api 2026-04-23 10:17:32 +02:00
Matej BačoandGitHub 9e23867f0a Merge pull request #11976 from appwrite/feat-auth-methods-api
Feat: Auth methods public API
2026-04-23 10:14:34 +02:00
Matej BačoGitHubgreptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
48353faa9b Apply suggestions from code review
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-04-23 10:13:01 +02:00
Matej Bačo c36b8fbabf Fix membershiip privacy bug on production 2026-04-23 10:07:32 +02:00
Luke B. SilverandGitHub 5a5cb1e74e Merge pull request #11977 from appwrite/perf/v20-investigation
perf: memoize request filter chain and V20 schema lookups
2026-04-23 07:38:03 +01:00
Chirag AggarwalandGitHub a8e6b1b683 Merge pull request #11963 from appwrite/chore/http-benchmark-comparison 2026-04-23 09:48:37 +05:30
Matej Bačo b0939b92c3 Fix failing account tests 2026-04-22 17:02:22 +02:00
Chirag Aggarwal 3d66078fe9 Increase benchmark iterations 2026-04-22 19:46:27 +05:30
Chirag Aggarwal 9a6a597710 Address benchmark hardening review 2026-04-22 19:38:48 +05:30
Chirag Aggarwal c15e8d0126 Harden benchmark failure guard 2026-04-22 19:30:01 +05:30
Chirag Aggarwal 7b25d778d4 Trim benchmark scenarios 2026-04-22 19:21:51 +05:30
loks0n 3283b0bec0 perf: memoize request filter chain and V20 schema lookups
A phpspy profile of a production databases worker showed the V20
backwards-compat request filter accounting for ~40% of in-request
samples on `databases.listDocuments` traffic. Two compounding causes:

1. `Request::getParams()` re-ran the entire filter chain on every
   invocation. The framework and app call `getParams()` several times
   per request (route param binding, `cacheIdentifier()`, action
   injection, logging), so V20's recursive schema walk executed N times
   with identical inputs.
2. Inside `V20::getRelatedCollectionKeys`, the `databases/$databaseId`
   document was fetched at every recursion frame (up to
   RELATION_MAX_DEPTH = 3), and sibling relationships pointing at the
   same related collection each did their own `getDocument` call.

This commit:

- Memoizes the post-filter params on `Request`. The cache is
  invalidated by `addFilter`, `resetFilters`, and `setRoute`. `Request`
  is constructed per HTTP request (app/http.php), so the memo is
  naturally request-scoped. Helps every request filter version, not
  just V20.
- Splits V20's walk into an entry point that resolves the database
  namespace once and a pure recursive helper.
- Caches the collection `attributes` array per
  `(databaseNamespace, collectionId)` on the filter instance, so shared
  related collections collapse to one `getDocument` call. Missing or
  errored lookups are cached as `null` to avoid retry storms.
2026-04-22 14:49:13 +01:00
ArnabChatterjee20kandGitHub 49f8d6e89c Merge pull request #11971 from appwrite/realtime-logs
Structured Logging
2026-04-22 19:10:36 +05:30
Matej Bačo a0274a7b6f Fix failing tests 2026-04-22 15:12:58 +02:00
Chirag AggarwalandGitHub 277a09847e Merge pull request #11975 from appwrite/t3code/database-tables-retry-fix
[codex] Stabilize database e2e CI retries
2026-04-22 18:34:29 +05:30
Chirag AggarwalandGitHub cdc5654c26 Merge branch '1.9.x' into chore/http-benchmark-comparison 2026-04-22 18:24:50 +05:30
Matej Bačo bb4fdefee7 New tests for auth methods (base + integration) 2026-04-22 14:51:10 +02:00
ArnabChatterjee20k c2e5bbe0f7 updated 2026-04-22 18:11:32 +05:30
Chirag Aggarwal b2d24080b9 Stabilize database e2e CI retries 2026-04-22 18:08:45 +05:30
ArnabChatterjee20k b006858d0c dedupe 2026-04-22 17:52:45 +05:30
Chirag AggarwalandGitHub a0f30f608d Merge pull request #11974 from appwrite/fix/no-rule-without-deployment 2026-04-22 17:52:02 +05:30
Matej Bačo a85c5e582c Add auth method APIs (public) 2026-04-22 14:19:04 +02:00
ArnabChatterjee20k 6d1def7716 removed redundant span attributes 2026-04-22 17:45:58 +05:30
ArnabChatterjee20k f0f1e1c412 updated 2026-04-22 17:44:18 +05:30
Chirag AggarwalandGitHub 11f85e2477 Merge branch '1.9.x' into chore/http-benchmark-comparison 2026-04-22 17:24:19 +05:30
Chirag AggarwalandGitHub dbb1d1139e Merge branch '1.9.x' into fix/no-rule-without-deployment 2026-04-22 17:20:31 +05:30
Chirag AggarwalandGitHub f0e2a8b2c4 Merge pull request #11972 from appwrite/t3code/sentry-bug-fix 2026-04-22 17:18:15 +05:30
ArnabChatterjee20k 46e778ea90 updated 2026-04-22 17:13:35 +05:30
ArnabChatterjee20k fd9fe5d9ce corrected the position 2026-04-22 17:07:53 +05:30
ArnabChatterjee20k 59e0383264 updated 2026-04-22 17:01:08 +05:30
ArnabChatterjee20k 0f81bc2da9 Refactor telemetry logging in realtime events for consistency and clarity
- Updated span logging keys to use camelCase for uniformity across connection and message events.
- Added checks to ensure project and user IDs are only logged if they are not empty, enhancing data integrity.
- Improved error handling and logging structure to maintain consistency in telemetry data.
2026-04-22 16:57:51 +05:30
ArnabChatterjee20k 17e3d03b40 Add telemetry logging for subscribed channels and queries in realtime events
- Introduced new arrays to capture subscribed channels and passed queries during connection and message events.
- Enhanced span logging to include details about channels and queries for better monitoring and analysis.
- Updated telemetry data structure to reflect the new metrics, improving traceability of realtime interactions.
2026-04-22 16:55:52 +05:30
Chirag Aggarwal c97435d95c Stabilize benchmark wait metric tags 2026-04-22 16:53:35 +05:30
ArnabChatterjee20k b2ad7237ab Add detailed telemetry logging for realtime connection events
- Introduced span logging for connection open and close events, capturing metrics such as inbound and outbound bytes, subscription counts, and response codes.
- Enhanced error handling with logging of exceptions during connection lifecycle.
- Updated the structure of the telemetry data to include project and user IDs for better traceability.
2026-04-22 16:52:54 +05:30
ArnabChatterjee20k 57d777f80a revert format 2026-04-22 16:46:18 +05:30
Chirag Aggarwal d1962dbc62 Shorten local benchmark command 2026-04-22 16:45:16 +05:30
Matej Bačo 6648a1987b Fix tests 2026-04-22 13:14:43 +02:00
ArnabChatterjee20k c60e85ca84 Merge remote-tracking branch 'origin/1.9.x' into realtime-logs 2026-04-22 16:44:34 +05:30
ArnabChatterjee20k ca1cf1982f added wide events inside the structured one logging per message instead of a discrete logs 2026-04-22 16:44:20 +05:30
Chirag Aggarwal f50ca0281b Drop dead ternary guards now that outer check ensures deployment 2026-04-22 16:43:28 +05:30
Chirag AggarwalandGitHub 2f12717f30 Merge pull request #11969 from appwrite/t3code/spec-overlap-verification 2026-04-22 16:28:34 +05:30
Chirag Aggarwal f934259c31 Skip preview rule when no deployment exists on function create 2026-04-22 16:26:41 +05:30
Chirag AggarwalandGitHub 838dbc52e3 Merge branch '1.9.x' into t3code/sentry-bug-fix 2026-04-22 16:23:25 +05:30
Chirag Aggarwal e7d9ef74c4 Fix deployment single chunk content range
Fixes CLOUD-3JN6
2026-04-22 16:05:12 +05:30
Chirag Aggarwal d106e1d5bb Fix session alert test payload 2026-04-22 15:57:32 +05:30
Matej Bačo d1ade3872e Fix failing tests 2026-04-22 12:22:00 +02:00
Chirag Aggarwal 0f64f54221 Harden benchmark rerun metrics 2026-04-22 15:49:27 +05:30
Chirag Aggarwal 00512df4ca Clean up spec enum validation reporting 2026-04-22 15:45:16 +05:30
Matej Bačo 9065d9ada4 Add mocks scopes 2026-04-22 12:13:10 +02:00
ArnabChatterjee20k e0fec8f550 updated 2026-04-22 15:42:17 +05:30
Chirag Aggarwal 205c283935 Remove unused JWT benchmark setup 2026-04-22 15:33:52 +05:30
Chirag Aggarwal 038be90969 Preserve nested items enum validation 2026-04-22 15:33:18 +05:30
Matej Bačo 355d4323fc Fix tests not running 2026-04-22 12:01:42 +02:00
Matej Bačo 7578b5644c AI review fixes 2026-04-22 12:00:15 +02:00
Chirag Aggarwal 240cdf43e5 Simplify local benchmark command 2026-04-22 15:24:47 +05:30
Chirag Aggarwal 481eaf7530 Merge remote-tracking branch 'origin/1.9.x' into t3code/spec-overlap-verification 2026-04-22 15:24:34 +05:30
Chirag Aggarwal 2390d40731 Remove specs task unit test 2026-04-22 15:24:13 +05:30
Chirag Aggarwal f75a7269c9 Address benchmark review simplifications 2026-04-22 15:23:35 +05:30
Chirag Aggarwal 4f74394e8f Fix spec enum name validation 2026-04-22 15:21:41 +05:30
Matej Bačo f770277ea5 New mock phones tests 2026-04-22 11:42:39 +02:00
Matej Bačo eeadba3b59 Add missing endpoint in email templates 2026-04-22 11:36:54 +02:00
Matej Bačo 2e42633e12 Add public mocks API for phones 2026-04-22 11:30:39 +02:00
Chirag Aggarwal affd5876ab Add spec enum service overlap validation 2026-04-22 14:49:35 +05:30
Chirag Aggarwal dfd39d3946 Tolerate benchmark cleanup failures 2026-04-22 14:25:59 +05:30
Chirag Aggarwal 7d7fcea8c0 Ensure benchmark failures fail CI 2026-04-22 14:16:12 +05:30
Chirag Aggarwal 33d3f82a58 Merge remote-tracking branch 'origin/1.9.x' into chore/http-benchmark-comparison
# Conflicts:
#	.github/workflows/ci.yml
2026-04-22 14:05:45 +05:30
Matej BačoandGitHub af531ee4f9 Merge pull request #11964 from appwrite/feat-public-project-policies
Feat: public project policies
2026-04-22 10:23:10 +02:00
Chirag Aggarwal 73a77b8dcc Show benchmark throughput 2026-04-22 13:45:50 +05:30
Chirag Aggarwal 4b1b2972e9 Merge remote-tracking branch 'origin/1.9.x' into chore/http-benchmark-comparison
# Conflicts:
#	.github/workflows/ci.yml
2026-04-22 13:34:45 +05:30
Matej Bačo 72bb6378c2 Leftover 2026-04-22 10:00:19 +02:00
Matej Bačo bfa1960d8a Remove unneeded const 2026-04-22 10:00:10 +02:00
Matej Bačo e530bf41f7 Post-merge fix 2026-04-22 09:59:00 +02:00
Matej Bačo 0d27c59cb8 Merge branch '1.9.x' into feat-public-project-policies 2026-04-22 09:57:48 +02:00
Matej BačoandGitHub 97e611029e Merge pull request #11900 from appwrite/feat-project-smtp-endpoints
Feat: Public API for project SMTP endpoints
2026-04-22 09:52:09 +02:00
Matej Bačo efc37c68ec Merge branch '1.9.x' into feat-project-smtp-endpoints 2026-04-22 09:50:08 +02:00
Chirag Aggarwal b3f305f9a8 Record storage upload wait metric 2026-04-22 13:02:16 +05:30
Chirag Aggarwal 32508e7251 Avoid reserved TablesDB benchmark column name 2026-04-22 12:53:13 +05:30
Chirag Aggarwal a98b9f2319 Handle malformed optional benchmark summaries 2026-04-22 12:06:32 +05:30
Chirag Aggarwal a0ef5968fb Document local HTTP benchmark command 2026-04-22 11:58:30 +05:30
Chirag Aggarwal bc637ad25f Merge remote-tracking branch 'origin/1.9.x' into chore/http-benchmark-comparison
# Conflicts:
#	.github/workflows/ci.yml
2026-04-22 11:38:04 +05:30
Chirag AggarwalandGitHub 5b6dd5f75a Merge pull request #11949 from appwrite/chore/phpstan-level-4 2026-04-22 11:17:21 +05:30
Jake BarnbyandGitHub 2c973387c5 Merge pull request #11967 from appwrite/fix/listrows-total-int-cast
fix: cast cached total to int in listDocuments/listRows
2026-04-22 17:02:43 +12:00
Chirag Aggarwal 3b9c604eb8 Harden benchmark comparison run 2026-04-22 09:45:51 +05:30
Jake BarnbyandClaude Opus 4.7 8a841a6971 fix: cast cached total to int in listDocuments/listRows
Redis stringifies scalars on save, so on a cache hit the `total` field
was served as a string. Flutter SDK (and any strictly-typed client) then
failed with `TypeError: "37": type 'String' is not a subtype of type 'int'`.
The cache-miss path returned an int from `count()`, which is why only
repeat requests with `ttl > 0` tripped the bug.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 16:14:30 +12:00
Chirag Aggarwal c973ca0a5d Address PHPStan level 4 review feedback 2026-04-22 09:34:03 +05:30
Chirag Aggarwal 7c486ddcef Keep benchmark comment on missing summary 2026-04-22 09:26:36 +05:30
Chirag Aggarwal cb7f2ec693 Show top benchmark request waits 2026-04-22 09:19:26 +05:30
Chirag Aggarwal 3cc7b833db Fix k6 benchmark diagnostics 2026-04-22 09:06:54 +05:30
Chirag Aggarwal 9ca84a56c9 Switch HTTP benchmark back to k6 2026-04-22 08:51:51 +05:30
Damodar LohaniandGitHub af05ad5784 Merge pull request #11953 from appwrite/CLO-4204-slow-query-hook
feat: add afterQuery hook to listRows (tablesdb) action
2026-04-22 07:25:24 +05:45
Matej Bačo 39af9c544d Remove unnessessary backwards compatibility 2026-04-21 18:22:44 +02:00
Luke B. SilverandGitHub 10b6955e98 Merge pull request #11965 from appwrite/fix/support-worker-num
fix: honor _APP_WORKERS_NUM in realtime
2026-04-21 17:14:08 +01:00
loks0nandClaude Opus 4.7 7e963e8439 fix: honor _APP_WORKERS_NUM in realtime
Realtime was ignoring _APP_WORKERS_NUM and always computing workers as
CPU × _APP_WORKER_PER_CORE, making it impossible to cap the worker count
without also changing the per-core multiplier. Prefer _APP_WORKERS_NUM
when set, falling back to the CPU × per-core calculation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 16:55:56 +01:00
Matej Bačo d9d5a81337 Fix 0->null backwrds compatibility 2026-04-21 17:51:31 +02:00
Matej Bačo 193891a7c9 Fix analyzer 2026-04-21 17:38:32 +02:00
Matej Bačo 70cb5ca68a linter fix 2026-04-21 17:34:59 +02:00
Matej Bačo c8a1746119 Fix existing tests 2026-04-21 17:34:44 +02:00
Matej Bačo 06eb550e98 Finalize tests 2026-04-21 16:56:00 +02:00
Matej Bačo 6c89a05a60 Fix 0 session to mean unlimited 2026-04-21 16:38:53 +02:00
Matej BačoandGitHub 1b993ede0f Merge branch '1.9.x' into feat-project-smtp-endpoints 2026-04-21 16:15:00 +02:00
Matej Bačo 648ffcdcfa Fix event labels 2026-04-21 16:07:52 +02:00
Matej Bačo 0be94a7aff Fix tests 2026-04-21 15:57:30 +02:00
Matej Bačo eba145ee2d More integration tests 2026-04-21 15:13:27 +02:00
Matej Bačo 99d230c70d Integration tests 2026-04-21 14:39:02 +02:00
Chirag Aggarwal 4ac1b68bbc Fix OpenAPI enum keys analysis 2026-04-21 18:03:18 +05:30
Chirag Aggarwal 211ac32080 Guard benchmark account-dependent flows 2026-04-21 17:59:09 +05:30
Chirag Aggarwal 30bf9deae0 Remove benchmark failure rows from output 2026-04-21 17:54:36 +05:30
Chirag Aggarwal 196b04a39c Polish benchmark comment details 2026-04-21 17:49:50 +05:30
Matej Bačo 6adabae620 Add policy tests 2026-04-21 14:05:46 +02:00
Matej Bačo 5f9dc0fcd8 Req & res filters, review fixes 2026-04-21 13:58:36 +02:00
Chirag Aggarwal b9d01617a4 Address benchmark review hardening 2026-04-21 17:24:26 +05:30
Chirag Aggarwal 9c65609d73 Check benchmark summary writes 2026-04-21 17:13:00 +05:30
Chirag AggarwalandGitHub da4dcd8505 Merge branch '1.9.x' into chore/phpstan-level-4 2026-04-21 17:08:46 +05:30
Matej Bačo d0f853d4cd Add more project policies 2026-04-21 13:38:27 +02:00
Chirag Aggarwal 63b2a1fb7f Harden benchmark baseline reporting 2026-04-21 17:07:05 +05:30
Chirag Aggarwal 30cfbb2d99 Polish benchmark reporting 2026-04-21 16:58:31 +05:30
Matej Bačo 2f272f0480 Cleanup unused markdowns descriptions 2026-04-21 13:27:08 +02:00
Matej Bačo 4317ee5617 Move some of auth settings to project policies 2026-04-21 13:11:42 +02:00
Chirag Aggarwal ef08d5a04c Run before benchmark with base image 2026-04-21 16:33:09 +05:30
Chirag Aggarwal 566eebfaec Fix benchmark PNG fixture 2026-04-21 16:31:57 +05:30
Chirag Aggarwal 51bc3dc1d5 Migrate HTTP benchmark to PHP 2026-04-21 16:22:04 +05:30
Chirag Aggarwal 83f182b444 Address benchmark review feedback 2026-04-21 16:10:08 +05:30
Chirag Aggarwal dcd01a8fb0 Tidy benchmark PR comment 2026-04-21 16:06:30 +05:30
Chirag Aggarwal 6aeb2d2be0 Fix benchmark before branch comparison 2026-04-21 15:51:30 +05:30
Chirag Aggarwal 2cfe40e98e Compare benchmark against base branch 2026-04-21 15:11:24 +05:30
Chirag Aggarwal 15e45df81e Address HTTP benchmark review feedback 2026-04-21 15:07:20 +05:30
Chirag Aggarwal e4f74a3fb1 Run curated HTTP benchmark in CI 2026-04-21 14:56:33 +05:30
Chirag Aggarwal 774a0d7022 Improve HTTP benchmark coverage 2026-04-21 14:48:12 +05:30
Damodar LohaniandGitHub 1dc9217c65 Merge branch '1.9.x' into CLO-4204-slow-query-hook 2026-04-21 14:31:45 +05:45
Damodar Lohani 50bd2877f4 chore: shorten afterQuery docblock 2026-04-21 08:46:21 +00:00
Damodar Lohani f465e2267a chore: tighten afterQuery docblock 2026-04-21 08:43:20 +00:00
Damodar Lohani b4f1652286 refactor: simplify afterQuery DB timing to single wall-clock bracket
Replaces the per-call $measure closure with a single $dbStart
timestamp taken right before the fetch block and a single subtraction
right after it. Drops 6 lines of HOF indirection plus the $measure
variable, at the cost of including cache GET/SET time (~0.5–5ms) in
measurements when ttl > 0. For slow-query logging at a 100ms+
threshold that noise is negligible, and the default ttl=0 path has
no cache ops at all so the measurement is pure DB engine time.

The bracket captures the cursor lookup, find/count, and transaction
state calls — everything between "query parsed" and "fetch done",
as intended. processDocument's post-fetch relationship work is still
outside the bracket, matching the original design.
2026-04-21 08:41:11 +00:00
Damodar Lohani 4e4860e7f8 refactor: move afterQuery hook into base listDocuments action
Moves the DB-duration measurement and afterQuery() hook from the
tablesDB-specific Rows/XList into the shared
Databases/Collections/Documents/XList base. Because TablesDB Rows and
DocumentsDB Documents both extend the legacy listDocuments base, a
single override now covers all three endpoints: legacy
listDocuments, listDocumentsDBDocuments, and tablesDB listRows.

TablesDB Rows drops the ~200-line action() duplicate and keeps only
the path/params/SDK overrides it needs, plus the extra
->inject('utopia') so its injection chain matches the new base action
signature. DocumentsDB Documents gets the same one-line inject
addition. Net -165 lines of duplication removed.

Behaviour is unchanged for CE (afterQuery() is a no-op); downstream
distributions overriding afterQuery() now observe every list-documents
/ list-rows call site for free.
2026-04-21 08:28:12 +00:00
Matej Bačo cce04b3bd1 Improve test coverage 2026-04-21 10:28:11 +02:00
Harsh MahajanandGitHub 7568964b7c Merge pull request #11905 from appwrite/feat-add-telemetry-for-ss-success-rates
feat(sites): add telemetry to ss rates
2026-04-21 13:54:05 +05:30
Damodar LohaniandGitHub 82af29c0cf Merge branch '1.9.x' into CLO-4204-slow-query-hook 2026-04-21 14:04:29 +05:45
Harsh MahajanandGitHub c6672e93cf Merge branch '1.9.x' into feat-add-telemetry-for-ss-success-rates 2026-04-21 13:07:40 +05:30
ArnabChatterjee20kandGitHub ac905a80b5 Merge pull request #11954 from appwrite/realtime-time-metric
Realtime time metric
2026-04-21 11:14:27 +05:30
ArnabChatterjee20k 5df65d5417 updated 2026-04-21 10:58:34 +05:30
ArnabChatterjee20k abd6f0add2 updated 2026-04-21 10:52:21 +05:30
ArnabChatterjee20k 25854c84f5 Merge remote-tracking branch 'origin/1.9.x' into realtime-time-metric 2026-04-21 10:44:30 +05:30
Damodar Lohani 5724d19426 Merge remote-tracking branch 'origin/CLO-4204-slow-query-hook' into CLO-4204-slow-query-hook 2026-04-21 04:34:38 +00:00
Damodar Lohani 7c84337c5f Merge remote-tracking branch 'origin/1.9.x' into CLO-4204-slow-query-hook 2026-04-21 04:32:28 +00:00
Matej Bačo b1d37bc4be Fix remaining test failures 2026-04-20 23:03:16 +02:00
Matej Bačo 4808cad081 Fix most of Project tests 2026-04-20 23:01:53 +02:00
Matej Bačo 72cd7671c4 Fix projects tests 2026-04-20 23:01:00 +02:00
Matej Bačo c3e411fcaa Fix tests 2026-04-20 22:47:47 +02:00
Matej Bačo 8c31e9f206 Server error fixes 2026-04-20 22:36:47 +02:00
Matej Bačo a4ad1b6df3 Code quality improvs 2026-04-20 22:35:35 +02:00
Matej Bačo 9e94f15f02 Finalize tests 2026-04-20 22:23:34 +02:00
Matej Bačo e27719108b Add tests 2026-04-20 22:15:03 +02:00
Matej Bačo 8bf2f54a51 Leftovers 2026-04-20 22:04:46 +02:00
Matej Bačo 8c03db70e9 Finalize templates design 2026-04-20 22:04:20 +02:00
Matej Bačo 8ea69e0321 Fix password visibility test 2026-04-20 16:57:23 +02:00
Matej Bačo dfec2b3cb7 Improve test coverage 2026-04-20 16:11:22 +02:00
ArnabChatterjee20kandGitHub 69d778ab05 Merge pull request #11946 from appwrite/migration-via-api
added project region
2026-04-20 19:40:23 +05:30
Matej Bačo 848f09956e Improve backwards compatibility test coverage 2026-04-20 15:36:49 +02:00
Matej Bačo 51f50b161c Improved backwards compatibility 2026-04-20 15:28:05 +02:00
ArnabChatterjee20kandGitHub 53d3713bfe Merge pull request #11957 from appwrite/realtime-add-new-messages
Realtime add new messages
2026-04-20 18:46:25 +05:30
ArnabChatterjee20k b8385fe927 updated 2026-04-20 18:27:48 +05:30
Matej Bačo 52e3319a86 Linter fix 2026-04-20 14:50:12 +02:00
Matej Bačo 8b41aed919 Post-merge removal 2026-04-20 14:49:43 +02:00
ArnabChatterjee20k b2233193d5 updated 2026-04-20 18:19:12 +05:30
Matej Bačo ba4430801d Merge branch 'feat-project-templates-api' into feat-project-smtp-endpoints 2026-04-20 14:49:04 +02:00
Matej Bačo 1c1ec43150 Removeal post-merge 2026-04-20 14:47:01 +02:00
Matej Bačo 2f62cced0a Merge branch '1.9.x' into feat-project-smtp-endpoints 2026-04-20 14:46:42 +02:00
Matej Bačo afb8f70316 Fix tests 2026-04-20 14:42:52 +02:00
ArnabChatterjee20k 78eeac6d14 Add unsubscribe functionality and enhance subscription handling in Realtime tests 2026-04-20 17:38:01 +05:30
ArnabChatterjee20k 9f65177649 Add unsubscribe functionality to Realtime adapter 2026-04-20 17:37:54 +05:30
ArnabChatterjee20k e9ea39a822 Enhance Realtime adapter: support union of channels/roles on subscription and add unsubscribeSubscription method 2026-04-20 17:37:45 +05:30
Matej Bačo 2097b0a0b0 Better support for post-smtp changes 2026-04-20 14:04:53 +02:00
Matej Bačo 78ef52cc9e Manual QA fixes 2026-04-20 13:11:11 +02:00
Matej Bačo f040a4dc31 More backwards compatibility 2026-04-20 11:58:55 +02:00
Matej Bačo 56385ce167 Backwards compatibility 2026-04-20 11:48:45 +02:00
Matej Bačo bc592903db Support reply to name 2026-04-20 11:47:06 +02:00
Matej Bačo 06a2d48deb Linter fix 2026-04-20 11:29:13 +02:00
Matej Bačo c4d9c3dc4f Improve endpoint quality 2026-04-20 11:28:21 +02:00
Damodar LohaniandGitHub b36f1edeb0 Merge branch '1.9.x' into CLO-4204-slow-query-hook 2026-04-20 14:04:10 +05:45
Damodar Lohani eef443f07e chore: bump utopia-php/database to 5.3.22 for Query::fingerprint 2026-04-20 07:53:36 +00:00
ArnabChatterjee20k 62f7f25cb5 updated 2026-04-20 12:18:38 +05:30
ArnabChatterjee20k 4b94d14f1e updated time metric 2026-04-20 12:13:53 +05:30
ArnabChatterjee20k 12f76d74b1 added subscriptions telemetry and worker labelling to connections and subscriptions 2026-04-20 11:52:43 +05:30
Chirag AggarwalandGitHub 9f504cd065 Merge pull request #11950 from appwrite/add-claude-plugin 2026-04-20 11:48:40 +05:30
Jake BarnbyandGitHub 9dc27e9ca5 Merge pull request #11894 from appwrite/chore-remove-shared-v1
refactor: Remove shared tables V1/V2 versioning
2026-04-20 18:16:12 +12:00
ArnabChatterjee20k 2bff4192ee added telemetry for the connected clients 2026-04-20 11:45:05 +05:30
ArnabChatterjee20k a9fb17b067 added telemetry for message arriaval delay 2026-04-20 11:39:50 +05:30
Jake Barnby db3d00b1da Merge remote-tracking branch 'origin/1.9.x' into chore-remove-shared-v1 2026-04-20 18:04:26 +12:00
Atharva Deosthale 5ab42b8d32 update composer lock 2026-04-20 11:26:31 +05:30
ArnabChatterjee20k 8de7b41929 updated 2026-04-20 11:06:07 +05:30
Damodar Lohani a5c0a920ba feat: add afterQuery hook to list-documents/rows action
Wrap each database call (find, count, transaction list/count) with a
measuring closure so the actual DB duration is known — cache hits
report near-zero, cache misses report only the DB time, not cache
save / response serialization.

After the response is sent, invoke a protected afterQuery() hook with
the measured duration, the database/collection documents, and both
parsed + raw query arrays. CE impl is a no-op; downstreams (e.g.,
cloud) can override it to log slow queries without relying on HTTP
shutdown hooks or route-path matching.

Exceptions from afterQuery are swallowed so observability never
breaks the response.
2026-04-20 05:32:49 +00:00
Chirag Aggarwal 37a2b1cbd9 fix: restore executions limit cleanup behind a runtime env flag
Per review feedback on the PHPStan cleanup, the two `if
($executionsRetentionCount > 0 && ENABLE_EXECUTIONS_LIMIT_ON_ROUTE)`
blocks in `app/controllers/general.php` and
`src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php`
were load-bearing feature flags, not dead code. Removing them silently
dropped the ability to turn the cleanup on later.

Changes:

- Convert `ENABLE_EXECUTIONS_LIMIT_ON_ROUTE` from
  `const ... = false;` to a `define()` backed by the new
  `_APP_EXECUTIONS_LIMIT_ON_ROUTE` env var (defaults to `disabled`).
  PHPStan can no longer fold the `&&` away since the value is now
  runtime-resolved, so the guarded blocks are live again.
- Restore the `/* cleanup */` block in the `router()` helper in
  `app/controllers/general.php`.
- Restore the two cleanup blocks in `Functions/Http/Executions/Create.php`
  (one on the async-scheduled return path, one on the sync-response
  path), and re-add the `DeleteEvent $queueForDeletes` /
  `int $executionsRetentionCount` injections plus the
  `Appwrite\Event\Delete` import.

Runtime behavior is identical to main (flag off by default); operators
can now flip it via env without a code change.
2026-04-20 08:54:31 +05:30
Matej Bačo 6b66923f18 Fix delete response placeholder audit label 2026-04-19 19:36:24 +02:00
Chirag Aggarwal adb4e4ef36 ci: fix benchmark by pulling compose from GitHub raw for the latest tag
`https://appwrite.io/install/compose` now returns a 308 redirect to the
HTML install docs (`/docs/advanced/self-hosting/installation`) instead
of serving the compose file, so the Benchmark job's "Installing latest
version" step was downloading 0 bytes and `docker compose up -d` died
with "empty compose file". This has been failing the Benchmark job on
every recent PR, not just this one.

Resolve the latest release tag via the GitHub API, then fetch the
compose file and `.env` from `raw.githubusercontent.com` at that tag.
Switched both curl calls to `-fsSL` so they fail loudly on non-2xx
responses or redirect loss instead of silently writing empty files.
2026-04-19 20:34:51 +05:30
Atharva Deosthale 56165ee3d9 add claude plugin to static sdk 2026-04-19 18:39:19 +05:30
Chirag Aggarwal d86258a6f6 fix: restore runtime guards and widen types missed by PHPStan cleanup
Three follow-ups from CI that the level-4 pass got wrong:

1. `account.php` / `users.php`: `Document::find()` returns `mixed`
   (specifically `Document|false` in practice), not `Document`. The
   earlier `@var Document $oldTarget` docblocks were lies, and the
   runtime `instanceof Document` guards were load-bearing — removing
   them caused `Call to a member function isEmpty() on false` 500s
   on the `PATCH /v1/users/:id/email` and `/phone` endpoints (and the
   analogous `/v1/account/email`, `/v1/account/phone` flows). Dropped
   the misleading `@var` docblocks and restored
   `$oldTarget instanceof Document && !$oldTarget->isEmpty()`.

2. `Installer/Runtime/Config::setEnabledDatabases()` is a boundary
   that actually takes arbitrary user/compose input — not a trusted
   `string[]`. The `is_string($v)` filter was covering for that, and
   `ConfigTest::testSetEnabledDatabasesFiltersInvalid` explicitly
   asserts it. Widened the PHPDoc to `array<mixed>` and restored
   `is_string($v) && $v !== ''` in the filter.

3. `OAuth2/Apple::getAppSecret()` wrapped `json_decode` in a
   `try/catch (\Throwable)` — but `json_decode` without
   `JSON_THROW_ON_ERROR` returns `null` on failure, it doesn't throw.
   PHP 8.3's PHPStan flagged the catch as dead (PHP 8.5 didn't, which
   is why it slipped through locally). Replaced with
   `if (!\is_array($secret)) throw`, which preserves the original
   "invalid secret" guard.
2026-04-19 17:52:51 +05:30
Chirag Aggarwal d2230f8fe7 chore: bump PHPStan to level 4 and fix all new errors
Raises `phpstan.neon` level from 3 to 4 and fixes the 549 new errors
that level 4 surfaces across 157 files. Fixes are root-cause — no
`@phpstan-ignore`, no `@var` casts, no baseline entries, no widened
types. A handful of latent bugs were fixed along the way:

- `app/controllers/general.php`: path-traversal guard was negating
  `\substr(...)` before the strict comparison (`!\substr(...) === $base`
  was always `false === $base`). Rewritten as `\substr(...) !== $base`.
- `src/Appwrite/Platform/Modules/Databases/Http/Databases/Logs/XList.php`
  and `.../TablesDB/Logs/XList.php`: were importing the raw Matomo
  `DeviceDetector` (whose `getDevice()` returns `?int`) but treating the
  result as an array with `deviceName/deviceBrand/deviceModel` keys.
  Swapped to `Appwrite\Detector\Detector`, matching the wrapper already
  used a few lines below for `$os`/`$client`.
- `src/Appwrite/Platform/Modules/Functions/Workers/Builds.php`: a match
  key was checking `$resourceKey === 'functions'` when `$resourceKey`
  is `'functionId'|'siteId'` — always false. Switched to the intended
  `$resource->getCollection() === 'functions'` check.
- `src/Appwrite/OpenSSL/OpenSSL.php`: `encrypt()` return type tightened
  to `string|false` to match `openssl_encrypt`; this lets callers'
  `=== false` error handling remain meaningful.
- `app/controllers/api/messaging.php`: removed a dead
  `array_key_exists('from', [])` branch in the Msg91 provider (empty
  array literal; branch was unreachable).

Large cleanup categories across the 549 fixes:
- Removed redundant `?? default` on array offsets and expressions that
  PHPStan now knows are non-nullable.
- Removed unreachable statements (mostly `return;` after `throw` or
  `markTestSkipped()`).
- Removed redundant `is_array`/`is_string`/`is_bool`/`instanceof` checks
  on already-narrowed types.
- Added `default =>` arms (or throwing arms) to non-exhaustive matches
  on `string`/`mixed` input.
- Removed dead `$document === false` branches where method return types
  were tightened to non-nullable `Document`.
- Removed unused properties (`$version` on Etsy/Zoom OAuth2, `$paths` on
  Installer State, `$source` on MigrationsWorker, `$account2` on two
  GraphQL auth tests), unused traits (`ApiVectorsDB`, `DatabaseFixture`),
  and an unused `cleanupStaleExecutions` task method.
- Replaced `assertTrue(true)` and redundant `assertIsArray`/`assertIsString`/
  `assertNotNull` assertions with `addToAssertionCount(1)` or
  `assertNotEmpty` where the runtime type was already known.
2026-04-19 17:31:20 +05:30
Matej BačoandClaude Opus 4.7 69d53cb2d4 Remove unused email template list response model
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 11:03:36 +02:00
Matej Bačo 447375dcbf Fix tests 2026-04-19 10:53:11 +02:00
Matej Bačo afab349a77 self review fixes 2026-04-19 10:43:57 +02:00
Matej Bačo 2a95cfd5a3 Final template API rework 2026-04-19 10:35:57 +02:00
Matej BačoandClaude Opus 4.7 8a1f8c71b2 Temporary removal of listEmailTemplates
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 10:15:48 +02:00
ArnabChatterjee20k 2793bcac38 updated 2026-04-18 23:11:04 +05:30
ArnabChatterjee20k 13f48797d4 added project region 2026-04-18 23:01:41 +05:30
Matej Bačo 0a71efc244 improve test coverage 2026-04-18 11:23:53 +02:00
Matej Bačo fcc7a56f4a Fix list endpoint 2026-04-18 11:01:26 +02:00
Luke B. SilverandGitHub 0aab3e9a43 Merge pull request #11941 from appwrite/fix/avif
fix: include project ID in storage preview cache key
2026-04-17 20:28:11 +01:00
Matej Bačo be5eeb1aba Fix failing tests 2026-04-17 19:50:34 +02:00
loks0nandClaude Sonnet 4.6 08b43dce50 fix: ksort after project injection to keep cache key order stable
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 18:45:00 +01:00
loks0nandClaude Sonnet 4.6 ad3bdee6c1 fix: include project ID in storage preview cache key
Cache key never included the project ID, so two projects with the same
bucketId, fileId, and transform params would share a cache key. On a
cache hit, Appwrite re-validates the bucket from the cached resourceType
(another project's bucket), which doesn't exist in the requesting
project's DB, throwing storage_bucket_not_found.

Fix: add 'project' to cache.params on the preview route (covers query
param case) and fall back to the X-Appwrite-Project header in
cacheIdentifier() for authenticated requests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 18:34:13 +01:00
Matej Bačo e00efaea68 Fix bug 2026-04-17 18:15:49 +02:00
Luke B. SilverandGitHub be214f7968 Merge pull request #11940 from appwrite/fix/avif
fix: do not cache error responses for storage preview, bump utopia-php/image to 0.8.5
2026-04-17 17:03:29 +01:00
Matej Bačo 64d182ac6a Add tests for templates 2026-04-17 17:49:20 +02:00
Matej Bačo bb38bf4248 Improve code quality 2026-04-17 17:42:46 +02:00
loks0nandClaude Sonnet 4.6 956285d522 fix: do not cache error responses for storage preview, bump utopia-php/image to 0.8.5
Cache write hook now checks HTTP status code before writing to prevent
failed AVIF (or any other) conversions from poisoning the cache.
Bumps utopia-php/image to 0.8.5 which fixes AVIF/HEIC output by using
native Imagick instead of the deprecated magick convert shell command.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 16:37:25 +01:00
Matej Bačo dc704fdb51 Improved xlist endpoint 2026-04-17 17:27:19 +02:00
Matej Bačo e388d2f6a3 List tempaltes endpoint 2026-04-17 17:22:55 +02:00
Aditya OberaiandGitHub fc62ef2fcc Merge pull request #11926 from appwrite/update-react-admin-template
Update React Admin template metadata
2026-04-17 20:32:45 +05:30
Matej Bačo b01ec03723 Fix analyze bug 2026-04-17 17:01:27 +02:00
Matej Bačo 489b2c4e21 Add new scopes 2026-04-17 16:45:04 +02:00
Matej Bačo 1a46fc2006 Move template APIs under project API 2026-04-17 16:43:17 +02:00
Luke B. SilverandGitHub c1b7aff2d9 Merge pull request #11934 from appwrite/feat/build-timeout
feat: use buildTimeout from message payload in build worker
2026-04-17 15:07:27 +01:00
loks0nandClaude Sonnet 4.6 7df1814203 refactor: rename buildTimeout to timeout in payload and buildDeployment param
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 14:26:38 +01:00
loks0nandClaude Sonnet 4.6 8f39783d7a refactor: remove jwtExpiry alias, use timeout directly
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 14:26:38 +01:00
loks0nandClaude Sonnet 4.6 4043153df3 fix: pass buildTimeout as parameter to buildDeployment to fix PHPStan error
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 14:26:38 +01:00
loks0nandClaude Sonnet 4.6 9765c7f0e3 feat: use buildTimeout from message payload in build worker
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 14:26:38 +01:00
Matej BačoandGitHub eddd159af8 Merge pull request #11932 from appwrite/feature/remove-/status-from-project-paths-upgrade-to-platform-0
Remove /status from project endpoint paths; upgrade to platform 0.13
2026-04-17 15:14:58 +02:00
ArnabChatterjee20kandGitHub 17de886296 Merge pull request #11936 from appwrite/realtime-time-metric
Realtime time metric
2026-04-17 18:31:51 +05:30
Matej Bačo 27b0e48296 Remove Status suffix from project event names
- project.updateServiceStatus → project.updateService
- project.updateProtocolStatus → project.updateProtocol
2026-04-17 14:53:59 +02:00
ArnabChatterjee20k 6b2054d0b5 Merge remote-tracking branch 'origin/realtime-time-metric' into realtime-time-metric 2026-04-17 18:02:18 +05:30
ArnabChatterjee20k df0f7ba581 added bucket boundary 2026-04-17 18:02:04 +05:30
Matej Bačo c484c487a9 Update tests 2026-04-17 13:19:20 +02:00
Matej Bačo 47f3ab930b Remove /status from project paths; Upgrade to platform 0.13 2026-04-17 13:14:34 +02:00
Matej BačoandGitHub c8c3c68b0e Merge pull request #11912 from appwrite/feat-fallback-email-template
Feat: Fallback email custom template
2026-04-17 12:49:16 +02:00
ArnabChatterjee20kandGitHub facae65f08 Merge pull request #11927 from appwrite/realtime-time-metric
Added delay metric
2026-04-17 16:16:16 +05:30
ArnabChatterjee20kandGitHub ef6711e317 Merge branch '1.9.x' into realtime-time-metric 2026-04-17 16:00:18 +05:30
Matej BačoandGitHub e06b06a21b Merge branch '1.9.x' into feat-fallback-email-template 2026-04-17 11:53:40 +02:00
Matej Bačo c97dd78335 Fix tests 2026-04-17 11:40:05 +02:00
Matej Bačo bf9bb22ac5 New tests 2026-04-17 11:30:24 +02:00
Matej Bačo 1b826df8f9 Non-URL locale to allow optional 2026-04-17 11:24:59 +02:00
Matej Bačo 11f23fdcfa Rework email templates PR after discussions 2026-04-17 10:52:21 +02:00
ArnabChatterjee20k b5ec92964c updated telemetry 2026-04-17 14:08:42 +05:30
ArnabChatterjee20k 71b74e21a3 added delay metric 2026-04-17 13:36:48 +05:30
Aditya Oberai 1e797b3f01 Update React Admin template metadata 2026-04-16 17:00:28 +00:00
Jake BarnbyandGitHub f1b2dd7335 Merge pull request #11925 from appwrite/atharva/compose-fixes-appwrite
Self hosted installer compose fixes
2026-04-17 00:21:50 +12:00
Chirag AggarwalandGitHub 50c379c5c3 Merge pull request #11924 from appwrite/feat/specs-provider-repo-list-discriminator 2026-04-16 16:57:37 +05:30
Atharva Deosthale 463e5acf50 compose fixes 2026-04-16 16:57:19 +05:30
Chirag Aggarwal 807e8bec8b feat(specs): add discriminator for provider repository list response union
Add ProviderRepositoryFrameworkList and ProviderRepositoryRuntimeList
model classes with conditions and type field so the listRepositories
endpoint's oneOf response gets a discriminator on the type property.
2026-04-16 16:29:42 +05:30
Harsh MahajanandGitHub f167049b51 Merge branch '1.9.x' into feat-add-telemetry-for-ss-success-rates 2026-04-16 15:23:45 +05:30
harsh mahajan 93b9500a95 align it with cloud pattern 2026-04-16 15:22:45 +05:30
Chirag AggarwalandGitHub 935c1e40eb Merge pull request #11921 from appwrite/feat/specs-discriminator-unions 2026-04-16 14:03:54 +05:30
Chirag Aggarwal e472d98fe3 Revert "refactor(specs): rename x-propertyNames/x-mapping to x-discriminator-properties/x-union-typemap"
This reverts commit 05d70f8826.
2026-04-16 13:55:36 +05:30
Matej Bačo 7fe65eec57 Restruture endpoints 2026-04-16 10:23:31 +02:00
Matej Bačo 4cf375de6d Re-add removed test 2026-04-16 10:17:08 +02:00
Matej Bačo 19d0eb66c0 Fix tests 2026-04-16 10:09:38 +02:00
Chirag Aggarwal 05d70f8826 refactor(specs): rename x-propertyNames/x-mapping to x-discriminator-properties/x-union-typemap 2026-04-16 13:32:05 +05:30
Chirag Aggarwal 98ec9e45c4 fix(specs): narrow Detection type enum to each subclass's own value
Each Detection subclass now declares only its own type value in the enum
rather than sharing the full ['runtime', 'framework'] list. This prevents
SDK validators from accepting invalid values on concrete models.
2026-04-16 13:16:13 +05:30
Chirag Aggarwal 6dc17c91bc trigger greptile 2026-04-16 13:08:14 +05:30
Chirag Aggarwal 1493b7b8a6 feat(specs): unified discriminator with compound support and algo conditions
Unify getDiscriminator to produce a single discriminator object for both
single-key and compound cases. Single-key returns standard {propertyName,
mapping}. Compound falls back to extending the object with x-propertyNames
and x-mapping for multi-property discrimination.

Simplify call sites: OpenAPI3 uses 'discriminator', Swagger2 uses
'x-discriminator' — no more split keys.

Add conditions to all 7 Algo models (AlgoArgon2, AlgoBcrypt, AlgoMd5,
AlgoPhpass, AlgoScrypt, AlgoScryptModified, AlgoSha) to enable
discriminator generation for hashOptions unions.
2026-04-16 13:02:57 +05:30
Chirag Aggarwal 965836c8b4 fix(specs): use swagger discriminator extension mapping 2026-04-16 12:28:53 +05:30
Chirag Aggarwal 4545989c91 fix(specs): remove type rule from list models, keep only on specific models 2026-04-16 12:22:37 +05:30
Chirag Aggarwal b71d42d226 fix(specs): rename getDisciminator typo and extract shared model resolution
Fix misspelled method name (getDisciminator -> getDiscriminator) across
Format, OpenAPI3, and Swagger2. Extract duplicated model-resolution
lambda into Format::resolveModels(). Fix copy-pasted descriptions in
ProviderRepository list models.
2026-04-16 11:29:16 +05:30
Chirag Aggarwal 945cdb3a99 refactor(specs): inline model resolution 2026-04-16 11:16:25 +05:30
Chirag Aggarwal a0db023860 refactor(specs): simplify discriminator resolution 2026-04-16 11:15:08 +05:30
Chirag Aggarwal 6a7280e7dd refactor(specs): inline discriminator condition checks 2026-04-16 11:12:43 +05:30
Chirag Aggarwal 680cb04de7 feat(specs): add discriminators for polymorphic responses 2026-04-16 11:07:07 +05:30
Damodar LohaniandGitHub 6e50ed050d Merge pull request #11787 from appwrite/CLO-4175-allow-delete-with-memberships
Allow deleting user account with active memberships
2026-04-16 08:16:23 +05:45
Damodar Lohani f78b5c6596 Merge remote-tracking branch 'origin/1.9.x' into CLO-4175-allow-delete-with-memberships 2026-04-16 01:16:49 +00:00
Matej Bačo 55001a7daa New integration tests 2026-04-15 19:27:26 +02:00
Matej Bačo 6d2876ab26 New E2E tests 2026-04-15 19:01:35 +02:00
Matej Bačo b510194f00 Expose "worldwide" locale 2026-04-15 18:57:37 +02:00
Matej Bačo 8fd1c5d620 Remove worldwide to not be user-facing 2026-04-15 18:54:18 +02:00
Matej Bačo 590f063694 Remove remaining sms leftover 2026-04-15 18:40:29 +02:00
Matej Bačo 90e1433878 Fix agent mistake 2026-04-15 18:38:08 +02:00
Matej Bačo 2b42487198 Linter fix 2026-04-15 18:30:06 +02:00
Matej Bačo 53ed9462bd More cleanup of sms templates 2026-04-15 18:29:43 +02:00
Matej Bačo 0da185e689 Refactor fixes 2026-04-15 18:17:55 +02:00
Matej Bačo dc39af50a1 Support for worldwide fallback custom template for all project emails 2026-04-15 18:05:46 +02:00
Matej Bačo 6da132db46 Remove SMS templates and support null locale for mail templates 2026-04-15 18:05:27 +02:00
Matej BačoandGitHub b2a02e3807 Merge pull request #11909 from appwrite/fix-protocol-exception
Fix: protocol & service exception
2026-04-15 16:59:49 +02:00
Matej Bačo e7f78b3e01 Fix shutdown event errors 2026-04-15 15:29:05 +02:00
Harsh MahajanandGitHub 682a56c03c Merge branch '1.9.x' into feat-add-telemetry-for-ss-success-rates 2026-04-15 18:46:07 +05:30
Matej Bačo 7376c5b517 Fix protocol endpoint causing InvalidArgumentException 2026-04-15 15:09:40 +02:00
harsh mahajan 730c62bda6 remove usage stats 2026-04-15 18:35:09 +05:30
ArnabChatterjee20kandGitHub db84c79802 Merge pull request #11907 from appwrite/update-events-not-firing
added backward compat
2026-04-15 18:32:21 +05:30
ArnabChatterjee20k 6ba810a4da fix unit tests 2026-04-15 17:49:33 +05:30
ArnabChatterjee20k 6d9b787816 updated string replacement 2026-04-15 17:38:21 +05:30
ArnabChatterjee20k 7b8fb409b1 added database filtering 2026-04-15 17:33:57 +05:30
ArnabChatterjee20k 1fb78115e8 added backward compat 2026-04-15 17:23:18 +05:30
harsh mahajan 9567f1f4e8 Merge branch 'origin/1.9.x' into feat-add-telemetry-for-ss-success-rates 2026-04-15 17:21:32 +05:30
Chirag AggarwalandGitHub 49c93c635d Merge pull request #11851 from appwrite/chore-migrate-audits-certificates-screenshots-to-publishers 2026-04-15 15:13:18 +05:30
Chirag AggarwalandGitHub 5010cdedbb Merge pull request #11903 from appwrite/fix/ci-mongodb-retry 2026-04-15 14:33:15 +05:30
Chirag AggarwalandGitHub 13413c05c3 Merge pull request #11880 from appwrite/fix/graphql-coroutine-safe-response 2026-04-15 14:14:57 +05:30
Chirag Aggarwal dc185be836 collapse 2026-04-15 13:52:20 +05:30
Chirag AggarwalandGitHub 29be9b6019 Merge branch '1.9.x' into chore-migrate-audits-certificates-screenshots-to-publishers 2026-04-15 13:26:52 +05:30
ArnabChatterjee20kandGitHub 1247b1c719 Merge pull request #11852 from appwrite/docsdb-fixes
Docsdb fixes
2026-04-15 13:18:41 +05:30
harsh mahajan 23233d0dcb add try catch 2026-04-15 11:59:02 +05:30
harsh mahajan 37540aa4f5 addressed comments 2026-04-15 11:52:39 +05:30
harsh mahajan db0a971384 feat(sites): add screenshot success telemetry to usage stats 2026-04-15 11:43:53 +05:30
Chirag AggarwalandGitHub 445e86361c Merge pull request #11904 from appwrite/fix/sdk-push-no-force 2026-04-15 10:27:40 +05:30
Chirag Aggarwal 80197b566c fix: replace force-push with regular push in SDK release task
The SDK push task used `git push --force-with-lease` which fails on
repos with branch protection rules that disallow force pushes. Instead,
checkout the existing remote dev branch and commit on top of it so a
regular push is always a fast-forward.
2026-04-15 10:22:50 +05:30
Chirag Aggarwal 8671533878 fix: remove orphaned docblock from deleted test 2026-04-15 10:13:37 +05:30
Chirag Aggarwal f51f02375a test: remove flaky concurrent session race condition test
testEmailPasswordSessionNotCorruptedByConcurrentRequests relies on
timing-sensitive curl_multi orchestration with hardcoded delays to
reproduce a cache race window. This makes it inherently flaky in CI
where resource pressure shifts the timing unpredictably.
2026-04-15 09:52:32 +05:30
Chirag Aggarwal d40df613de fix: run ProjectWebhooks tests sequentially in CI
ProjectWebhooks tests have shared state dependencies (e.g. index
creation must complete before assertions). Running with --functional
(parallel methods) causes flaky failures where indexes are still
'processing' instead of 'available'.
2026-04-15 09:51:26 +05:30
ArnabChatterjee20kandGitHub ebc3febc38 Merge branch '1.9.x' into docsdb-fixes 2026-04-15 09:42:05 +05:30
Chirag Aggarwal e77bfae091 fix: add restart policy to MongoDB container for flaky CI starts
MongoDB's official Docker entrypoint uses a two-phase startup: a
temporary mongod for user/db init, then the real mongod. Under CI
resource pressure the port may not be released between the two
phases, causing mongod to exit with code 48 (address already in use).

Adding restart: on-failure:3 lets Docker handle the transient failure
natively. On restart the data directory already exists so the
entrypoint skips the two-phase init entirely, avoiding the race.
2026-04-15 09:19:02 +05:30
Chirag Aggarwal f9efd803c1 refactor: remove unnecessary IIFE wrappers in resolver methods 2026-04-15 08:55:00 +05:30
Chirag Aggarwal 54d6470163 refactor: move acquire/release into ResolverLock as instance methods 2026-04-15 08:51:14 +05:30
Chirag Aggarwal 50640b5bb9 refactor: inline createResolverResponse — only called once 2026-04-15 08:50:14 +05:30
Chirag Aggarwal 4d4ba508ef fix: use spl_object_hash for lock keys instead of WeakMap
Replace WeakMap with a plain array keyed by spl_object_hash($utopia)
as suggested in review. Entry is cleaned up in the finally block to
prevent leaks.
2026-04-15 08:47:41 +05:30
Chirag Aggarwal 4f2f9fedfa fix: resolve merge conflict keeping both lock and route restore
Merge conflict in Resolvers.php between the coroutine lock
(fix/graphql-coroutine-safe-response) and the otel route restore
(fix-gql-route-reset from 1.9.x). Both changes are needed:
the lock serialises concurrent resolvers while the route restore
prevents otel span clobbering.
2026-04-15 08:40:12 +05:30
Matej Bačo 732538504d Fix backwards compatibility 2026-04-14 17:42:26 +02:00
Matej Bačo d3dcbfe567 Leftover chaneges, 2026-04-14 17:20:22 +02:00
Matej Bačo 905d2a8eaa Fix tests 2026-04-14 17:20:03 +02:00
Matej Bačo b5e46c1a60 E2E integration tests 2026-04-14 17:09:37 +02:00
Matej Bačo a3a8ad88fe formatting fix 2026-04-14 17:07:06 +02:00
Matej Bačo 9d6428d5d5 Improve tests 2026-04-14 17:06:57 +02:00
Matej Bačo d7f8ca3f01 Improve endpoint quality 2026-04-14 17:04:22 +02:00
Matej Bačo 56bcc0d09f Fix tests 2026-04-14 17:01:56 +02:00
Matej Bačo 8b0c60d2f9 Fix bugs 2026-04-14 17:01:46 +02:00
Matej Bačo ffdfe4bbf8 Add new tests 2026-04-14 16:55:39 +02:00
Matej Bačo 556ca10ed9 Register endpoints 2026-04-14 16:52:38 +02:00
Matej Bačo 193beb76fe add SMTP endpoints 2026-04-14 16:50:07 +02:00
Chirag AggarwalandGitHub 8fcba8b5c9 Merge pull request #11893 from appwrite/codex/disable-graphql-functional 2026-04-14 19:19:14 +05:30
Jake BarnbyandGitHub 6c8b34c230 Merge pull request #11885 from appwrite/fix-gql-route-reset
(fix): reset route to avoid clobbering otel
2026-04-15 01:26:02 +12:00
Chirag Aggarwal b2884ddb88 Use audit message context helper 2026-04-14 18:23:24 +05:30
Chirag Aggarwal 82798fa5a3 Simplify audit message construction 2026-04-14 18:18:25 +05:30
Shimon NewmanandGitHub 377ade3c44 Merge pull request #11892 from appwrite/schedule-functions-debug-2
Schedule functions debug
2026-04-14 15:32:29 +03:00
Shimon NewmanGitHubgreptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
71533aaaf3 Update app/cli.php
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-04-14 15:17:37 +03:00
shimon ccbe7e5d3f Merge branch '1.9.x' of github.com:appwrite/appwrite into schedule-functions-debug-2 2026-04-14 15:11:56 +03:00
Chirag AggarwalandGitHub bea7739d7e Merge branch '1.9.x' into codex/disable-graphql-functional 2026-04-14 17:21:55 +05:30
Jake BarnbyandGitHub 76320d652a Merge pull request #11887 from appwrite/fix/composer-audit-graphql-php
Upgrade graphql-php to v15.31.5
2026-04-14 23:46:45 +12:00
Chirag Aggarwal 6d5968e2ea ci: disable functional GraphQL e2e 2026-04-14 16:51:46 +05:30
Jake Barnby fd8fedca18 (refactor): Remove shared tables V1/V2 versioning 2026-04-14 22:51:36 +12:00
shimon 70c380fa36 feat: add tracing support for execution events in log and worker classes 2026-04-14 13:36:29 +03:00
shimon 512a7ae2bd feat: enhance function scheduling with tracing support 2026-04-14 13:36:19 +03:00
Chirag AggarwalandGitHub 49d3cd980f Merge branch '1.9.x' into fix/composer-audit-graphql-php 2026-04-14 15:07:22 +05:30
Luke B. SilverandGitHub 39a396810a Merge pull request #11881 from appwrite/feat/45-min-build-timeout
feat: increase default build timeout to 45 minutes
2026-04-14 09:28:29 +01:00
Jake BarnbyandGitHub c52ca65905 Revert "Safe delete shared tables v1" 2026-04-14 19:15:41 +12:00
Jake BarnbyandGitHub 1d307178bb Merge pull request #11691 from appwrite/delete-project-shared-table-v1
Safe delete shared tables v1
2026-04-14 18:00:00 +12:00
fogelito c3ed2ff6ce Merge branch '1.9.x' of https://github.com/appwrite/appwrite into delete-project-shared-table-v1 2026-04-14 08:05:44 +03:00
Chirag Aggarwal efadf17bfe Fix GraphQL 15 static analysis 2026-04-14 10:26:59 +05:30
Chirag Aggarwal bcfec8d5de Align graphql version pinning style 2026-04-14 09:35:25 +05:30
Chirag Aggarwal 4b2e22d9da Fix graphql-php audit vulnerability 2026-04-14 09:27:14 +05:30
ArnabChatterjee20k 45e22e2243 Merge remote-tracking branch 'origin/1.9.x' into docsdb-fixes 2026-04-13 22:44:07 +05:30
Matej BačoandGitHub 0a21f3b139 Merge pull request #11884 from appwrite/fix-webhook-api-security
Fix: Webhook API secret rotation and security
2026-04-13 15:52:55 +02:00
Chirag AggarwalandGitHub 5d53685976 Merge branch '1.9.x' into chore-migrate-audits-certificates-screenshots-to-publishers 2026-04-13 19:11:43 +05:30
Chirag AggarwalandGitHub d869c39783 Merge branch '1.9.x' into fix/graphql-coroutine-safe-response 2026-04-13 19:02:06 +05:30
Chirag Aggarwal fe02964ebd fix: finalize graphql coroutine response isolation 2026-04-13 19:01:20 +05:30
Jake Barnby cc8eb62c83 (chore): rename 2026-04-14 01:15:06 +12:00
Luke B. SilverandGitHub b1ad6ea87c Merge pull request #11883 from appwrite/fix/large-execution-payload
fix: trim execution queue payload to project ID only
2026-04-13 14:13:43 +01:00
Jake Barnby d52d6c0bf0 (fix): reset route to avoid clobbering otel 2026-04-14 01:13:32 +12:00
Chirag AggarwalandGitHub 86cfea0edb Merge branch '1.9.x' into chore-migrate-audits-certificates-screenshots-to-publishers 2026-04-13 18:41:52 +05:30
Matej Bačo db406b0a27 Fix tests 2026-04-13 15:08:20 +02:00
Matej Bačo 2585518e33 Fix bugs 2026-04-13 15:08:13 +02:00
Chirag Aggarwal a1342b4b9d fix: update audit context usage 2026-04-13 18:32:38 +05:30
Matej Bačo c9fceb870c Fix folder structure 2026-04-13 14:57:24 +02:00
Matej Bačo 9f1ec356d1 Formatting fix 2026-04-13 14:53:25 +02:00
Matej Bačo 28d285d5c5 Improved tests for webhook edge cases 2026-04-13 14:53:06 +02:00
loks0nandClaude Sonnet 4.6 a941d8b855 fix: trim execution queue payload to project ID only
The v1-executions queue was serialising the full project document
(OAuth providers, webhooks, keys, auths config, permissions, etc.)
into every message. The worker DI system already re-fetches the
complete project from the platform database using only the project ID,
so the full document was wasted bytes.

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 13:52:09 +01:00
Chirag Aggarwal d40a355d9d refactor: simplify audit event context 2026-04-13 18:21:39 +05:30
Matej Bačo 3263133e5f Implement secure webhook interfaces 2026-04-13 14:50:06 +02:00
Chirag Aggarwal f4f5494b85 fix: isolate graphql resolver responses 2026-04-13 17:48:48 +05:30
Chirag Aggarwal fc0fd2f6ac fix: scope graphql resources to resolver coroutine 2026-04-13 17:10:38 +05:30
Chirag Aggarwal 70a75c2e7b fix: scope graphql resolver lock to request 2026-04-13 16:47:33 +05:30
Chirag Aggarwal 6bc2168e29 fix: isolate graphql resolver request state 2026-04-13 16:34:27 +05:30
fogelito a3ba66c90a Merge branch '1.9.x' of https://github.com/appwrite/appwrite into delete-project-shared-table-v1
# Conflicts:
#	composer.lock
2026-04-13 13:29:03 +03:00
ArnabChatterjee20kandGitHub c24d724000 Merge pull request #11749 from appwrite/db-workers-memory
added reset in db worker for queue for realtime
2026-04-13 15:47:54 +05:30
Luke B. SilverGitHubgreptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
df5ccc10ad Apply suggestion from @greptile-apps[bot]
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-04-13 10:48:20 +01:00
loks0nandClaude Sonnet 4.6 2807d6cd9a feat: increase default build timeout to 45 minutes
Raises _APP_COMPUTE_BUILD_TIMEOUT default from 900s (15 min) to
2700s (45 min) to support longer-running builds.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 09:57:27 +01:00
ArnabChatterjee20kandGitHub 20e2f2284f Merge branch '1.9.x' into db-workers-memory 2026-04-13 13:43:14 +05:30
Matej BačoandGitHub 7a0d69c826 Merge pull request #11839 from appwrite/feat-services-protocols-apis
Feat: services protocols public apis
2026-04-13 09:38:17 +02:00
Matej Bačo feedec80f2 Merge branch '1.9.x' into feat-services-protocols-apis 2026-04-13 09:17:09 +02:00
Damodar LohaniandClaude Opus 4.6 18b1c344e6 fix: serialize batched GraphQL queries for coroutine safety
When Swoole coroutine hooks are enabled (SWOOLE_HOOK_ALL), batched
GraphQL queries execute in parallel coroutines that share a single
Response object. Concurrent coroutines interleave writes to the
shared response payload, causing data mixing between queries.

Cloning the response is not viable because cookies/headers written
by the action (e.g. session tokens) must reach the real HTTP response.

Instead, serialize the critical section (execute → getPayload) using
a Swoole Channel as a coroutine-safe mutex. This ensures only one
batched query writes to the Response at a time while preserving
cookie/header propagation. The lock is released before resolve/reject
callbacks so downstream processing remains concurrent.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 06:54:15 +00:00
Damodar LohaniandGitHub f4d40a1289 Merge pull request #11879 from appwrite/fix/graphql-batch-sent-reset
fix: reset response sent state between batched GraphQL queries
2026-04-13 11:35:22 +05:45
Damodar LohaniandGitHub 1e65f075e6 Merge branch '1.9.x' into fix/graphql-batch-sent-reset 2026-04-13 11:23:23 +05:45
Jake BarnbyandGitHub 68725d9262 Merge pull request #11860 from appwrite/fix-cache-fallback
(fix): cache fallback
2026-04-13 17:34:51 +12:00
Damodar LohaniandClaude Opus 4.6 5b805d686b fix: reset response sent state between batched GraphQL queries
utopia-php/http 0.34.20 added a guard that skips the action if
$response->isSent() is true. In batched GraphQL requests the resolver
reuses a single Response across all queries — after the first query's
action calls send(), subsequent queries hit the guard, their actions
are skipped, and stale/null payloads are returned.

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

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

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

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

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 03:42:47 +12:00
Jake Barnby a26382ac51 (chore): lockfile 2026-04-12 03:25:07 +12:00
Jake Barnby 9ba182d8a0 (fix): cache fallback 2026-04-12 03:22:21 +12:00
Matej Bačo 27fc8058b9 Fix failing tests 2026-04-11 14:19:05 +02:00
Matej Bačo c5bd8c712f Upgrade libs 2026-04-11 11:31:35 +02:00
Matej Bačo a1267b1bff Backwards compatibiltiy tests 2026-04-11 11:16:43 +02:00
Matej Bačo 18d17ea945 Webhook endpoints quality improvements 2026-04-11 11:00:56 +02:00
Matej Bačo fabd9559c4 Tests for backwards compatibility 2026-04-11 10:22:03 +02:00
Matej Bačo ec637d4417 Mark key scopes required 2026-04-11 10:19:14 +02:00
Chirag Aggarwal 5ecd15a5f5 fix: register certificate publisher in cli 2026-04-11 09:07:51 +05:30
Chirag Aggarwal 4523e86b91 fix: bump phpseclib to 3.0.51 2026-04-11 09:01:42 +05:30
Chirag Aggarwal ec5472f1ed chore: remove unrelated queue resources 2026-04-11 08:57:06 +05:30
Chirag AggarwalandGitHub 9ae804f8ae Merge branch '1.9.x' into chore-migrate-audits-certificates-screenshots-to-publishers 2026-04-11 08:49:23 +05:30
Luke B. SilverandGitHub 1ea108c2ce Merge pull request #11854 from appwrite/feat/remove-err
feat: remove error logs
2026-04-10 14:18:33 +01:00
loks0n 0a864e51b8 feat: remove error logs 2026-04-10 14:17:24 +01:00
ArnabChatterjee20k 0cce480592 typo 2026-04-10 17:23:43 +05:30
ArnabChatterjee20k b622b092a8 updated 2026-04-10 17:22:22 +05:30
ArnabChatterjee20k 96fe989f6d update composer dependencies and remove obsolete log classes 2026-04-10 17:15:31 +05:30
Chirag Aggarwal 0de26be6e6 chore: address review feedback 2026-04-10 16:44:24 +05:30
Chirag Aggarwal dc0a5c88b7 refactor: migrate audits certificates screenshots to publishers 2026-04-10 16:44:00 +05:30
Chirag AggarwalandGitHub c6dd7de216 Merge pull request #11850 from appwrite/chore-migrate-selected-queues-to-publishers
Migrate executions, migrations, and stats resources to publishers
2026-04-10 15:13:15 +05:30
Chirag Aggarwal f77a64bff9 chore: address publisher PR nits 2026-04-10 14:00:57 +05:30
ArnabChatterjee20k d13dbae0fe added allowance of empty payload for documentsdb 2026-04-10 13:59:45 +05:30
Luke B. SilverandGitHub f552a1ba15 Merge pull request #11844 from appwrite/feat/storage-cache-spans
feat: add tracing spans for storage file preview timing and cache state
2026-04-10 09:18:42 +01:00
Chirag Aggarwal 7282c5d51f chore: remove unused execution exclusion 2026-04-10 13:25:32 +05:30
Chirag Aggarwal 82ec75d582 chore: address PR review feedback 2026-04-10 13:12:08 +05:30
Chirag Aggarwal 6bf6142667 refactor: migrate selected queues to publishers 2026-04-10 13:02:00 +05:30
Chirag AggarwalandGitHub 938e65cb02 Merge pull request #11831 from appwrite/codex/remove-realtime-http-dependency
Use dedicated connection resources in realtime
2026-04-10 12:54:45 +05:30
ArnabChatterjee20kandGitHub 114de91f48 Merge pull request #11767 from appwrite/realtime-query-message-payload
Realtime query message payload
2026-04-10 12:06:00 +05:30
ArnabChatterjee20k 2e6f3f5c14 typo 2026-04-10 11:13:03 +05:30
ArnabChatterjee20k 7b3d9bae03 updated authorization 2026-04-10 11:04:44 +05:30
Chirag Aggarwal d81a1154e3 refactor: isolate realtime connection resources 2026-04-10 10:19:41 +05:30
Chirag Aggarwal a944c65660 refactor: move worker message resources 2026-04-10 09:43:32 +05:30
Chirag Aggarwal 856046dc82 shrink the size 2026-04-10 09:28:17 +05:30
Chirag Aggarwal 2ca551123d use connection container 2026-04-10 09:25:00 +05:30
Chirag Aggarwal c861d45749 Merge branch '1.9.x' into codex/remove-realtime-http-dependency 2026-04-10 09:02:23 +05:30
loks0nandClaude Sonnet 4.6 4a43969da9 fix: use consistent dot notation for all storage span attribute names
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 19:53:20 +01:00
loks0nandClaude Sonnet 4.6 6fa4122910 fix: rename storage span attributes to use dot notation for ids
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 19:52:25 +01:00
loks0nandClaude Sonnet 4.6 1d27101770 feat: add tracing spans for storage file preview timing and cache state
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 19:49:57 +01:00
Luke B. SilverandGitHub 9214decc8d Merge pull request #11843 from appwrite/fix/session-mails
fix: set project on mail queue in session mails listener
2026-04-09 18:30:24 +01:00
loks0nandClaude Sonnet 4.6 ee4ae3bd47 fix: set project on mail queue in session mails listener
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 18:26:07 +01:00
Luke B. SilverandGitHub 53a114e7e4 Merge pull request #11842 from appwrite/fix/storage-cache
fix: storage preview cache misses and stale cache eviction
2026-04-09 17:27:27 +01:00
loks0nandClaude Sonnet 4.6 f2df9cb93a fix: storage preview cache misses and stale cache eviction
Three bugs causing storage preview cache to be ineffective:

1. Cache keys included the `token` auth parameter, so requests using
   resource tokens always generated unique keys and never hit cache.
   Introduced `cache.params` label for routes to opt-in specific params
   into the cache key; preview now declares only the transform params.

2. Cache hits never refreshed `accessedAt` in the DB or the filesystem
   file mtime, because `$response->send()` in the init hook skips the
   shutdown hook. After 30 days the maintenance job evicted still-active
   cache entries, and after the original 30-day filesystem TTL the cache
   file expired — causing periodic full re-renders. The cache-hit path
   now updates both on the APP_CACHE_UPDATE (24h) interval.

3. `updateDocument` in the preview action passed the full file document
   instead of a sparse one when updating `transformedAt`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 17:05:14 +01:00
Matej Bačo d69726487e PR review fixes 2026-04-09 16:58:42 +02:00
Matej Bačo 5fccb8cc28 Improve tests 2026-04-09 16:57:44 +02:00
Luke B. SilverandGitHub f371237fd5 Merge pull request #11642 from appwrite/feat/mails-listener
feat: extract session alert email into Mails listener
2026-04-09 15:29:16 +01:00
Matej Bačo 21a0d60c98 Fix tests 2026-04-09 16:13:54 +02:00
Matej Bačo 4eb8534294 Fix tests 2026-04-09 16:08:11 +02:00
Matej Bačo c95f905bce New services and protocols tests 2026-04-09 15:58:28 +02:00
Matej Bačo 0293da1e22 Improve test for backwards compatibility 2026-04-09 15:54:00 +02:00
Matej Bačo a4a0c4175d Implement new endpoints in /v1/project for services and protocols 2026-04-09 15:45:06 +02:00
Harsh MahajanandGitHub 9548d18a3e Merge pull request #11838 from appwrite/ix-missing-worker-executions-template
fix(installer): add missing worker-executions service to compose template
2026-04-09 19:06:42 +05:30
Matej Bačo d3c73fbb49 Add endpoints to control protocols and services 2026-04-09 15:34:50 +02:00
Harsh MahajanandGitHub 7928175387 Merge branch '1.9.x' into ix-missing-worker-executions-template 2026-04-09 18:53:39 +05:30
Matej Bačo 8818187740 Introduce req&res filters for 1.9.1 2026-04-09 15:21:58 +02:00
Matej Bačo 75324b24fc Improve skill 2026-04-09 15:21:24 +02:00
Matej Bačo d6d118f4ab Bump version to 1.9.1 2026-04-09 15:19:58 +02:00
Matej Bačo e998739998 Add agent skill to increase patch version 2026-04-09 15:18:26 +02:00
loks0nandClaude Sonnet 4.6 dd29967e99 refactor: tighten Mails listener with guard clauses and lean event
- SessionCreated event now carries only domain data (no isFirstSession)
- Mails listener uses ordered guard clauses, deferring the DB query
  until cheaper checks pass
- Drop $user Document allocation in favour of direct array access
- Inline FileName validator and $smtpEnabled into their use sites
- Extract $isBranded to eliminate duplicate APP_BRANDED_EMAIL_BASE_TEMPLATE check

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 14:01:13 +01:00
loks0nandClaude Sonnet 4.6 e7f5ae9306 fix: remove stale @param platform from SessionCreated docblock
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 14:01:13 +01:00
loks0nandClaude Sonnet 4.6 4133ec99ae feat: extract session alert email into Mails listener
Moves session alert email side effect out of the account controller
into a dedicated `Mails` listener that reacts to a new `SessionCreated`
bus event. The event is now always dispatched on session creation; the
listener owns all conditional logic (first session, sessionAlerts flag,
email-link sessions, user email presence).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 14:01:13 +01:00
Matej BačoandGitHub 6dd63ee152 Merge pull request #11655 from appwrite/copilot/add-test-for-deleting-partially-uploaded-file
Fix deletion of partially-uploaded (pending) files
2026-04-09 14:58:57 +02:00
ArnabChatterjee20k 912dbda159 updated type 2026-04-09 18:16:09 +05:30
ArnabChatterjee20k 410a050244 updated 2026-04-09 18:04:01 +05:30
ArnabChatterjee20k 9cf45816c2 added triggering stats for messaging based subscription during the start 2026-04-09 17:38:25 +05:30
Harsh MahajanandGitHub 386fc995e6 Update compose.phtml 2026-04-09 17:36:07 +05:30
Matej Bačo d6451b8fad Merge branch '1.9.x' into copilot/add-test-for-deleting-partially-uploaded-file 2026-04-09 14:02:06 +02:00
ArnabChatterjee20kandGitHub 920ddd18e6 Merge branch '1.9.x' into realtime-query-message-payload 2026-04-09 17:24:57 +05:30
Harsh MahajanGitHubgreptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
ff9334ab78 Update app/views/install/compose.phtml
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-04-09 17:20:55 +05:30
Harsh MahajanandGitHub c494e96f79 Merge branch '1.9.x' into ix-missing-worker-executions-template 2026-04-09 17:18:22 +05:30
harsh mahajan fd78f0f7df fix(installer): add missing worker-executions service to compose template 2026-04-09 17:13:54 +05:30
Chirag AggarwalandGitHub 4ff10dcacc Merge pull request #11837 from appwrite/fix-11795-revert-project-variableid
Revert "fix: make variableId optional in createProjectVariable endpoint"
2026-04-09 16:53:10 +05:30
Chirag Aggarwal 7a995fe759 Revert "Merge pull request #11795 from rathi-yash/fix-11765-global-variable-creation"
This reverts commit 597b20a6cb, reversing
changes made to 20f80ac067.
2026-04-09 16:25:59 +05:30
Damodar LohaniandGitHub 227606baef Merge pull request #11835 from appwrite/fix/map-deprecated-platform-types-in-project-response
fix: map deprecated platform types in project response
2026-04-09 14:23:31 +05:45
Damodar LohaniandClaude Opus 4.6 f315e759f3 fix: map deprecated platform types in subQueryPlatforms filter
The subQueryPlatforms database filter loads platforms as a sub-attribute
when project documents are fetched. Old platform type values stored in
the database (e.g. flutter-android, flutter-ios) were not being mapped
to the new consolidated types before being included in the project
response sent to the frontend/console.

This adds Platform::mapDeprecatedType() to the filter so all platforms
returned as part of a project document have their types mapped
consistently, complementing the existing mapping in the dedicated
platform Get and List endpoints.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 08:31:39 +00:00
Damodar LohaniandGitHub 8a6fba75a5 Merge pull request #11833 from appwrite/fix/deprecated-platform-origin-validation
fix: Add backwards-compat origin validation for deprecated platform types
2026-04-09 13:41:30 +05:45
Damodar LohaniandClaude Opus 4.6 196c76ac70 fix: map deprecated platform types in read endpoints for backwards compatibility
Old platform types stored in the database (e.g. flutter-web, apple-ios,
react-native-android) are now mapped to the new consolidated types (web,
apple, android, windows, linux) before being sent in API responses. This
ensures the response models' $conditions correctly select the right model
for each platform document.

Adds Platform::mapDeprecatedType() as a reusable static method and applies
the mapping in both Get and XList platform endpoints.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 07:47:42 +00:00
Damodar LohaniandClaude Opus 4.6 7f82484436 remove migration changes from V24.php
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 07:36:58 +00:00
Damodar LohaniandClaude Opus 4.6 db18caf739 fix: add backwards-compatible origin validation for deprecated platform types
Platform::getHostnames() and Platform::getSchemes() only handled the new
consolidated type names, causing "invalid origin" errors for projects still
using old granular types (flutter-*, apple-*, react-native-*, unity) stored
in the database. Add switch fall-through cases for all deprecated type values
and a V24 migration to convert old types to their new equivalents.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 07:31:58 +00:00
Chirag AggarwalandGitHub 6693e28c93 Merge pull request #11830 from appwrite/feat/specs-task-no-http
remove HTTP server bootstrap from specs task
2026-04-09 11:46:37 +05:30
Chirag Aggarwal bf489ce13b Fix requestRoutePath fallback 2026-04-09 11:09:21 +05:30
Chirag Aggarwal 357d6482f9 Remove realtime HTTP app dependency 2026-04-09 10:52:31 +05:30
Chirag Aggarwal eec72a915a inline specs container setup 2026-04-09 10:45:24 +05:30
Chirag Aggarwal ae29e4c424 drop specs task format test 2026-04-09 10:43:39 +05:30
Chirag Aggarwal 05dc264df9 remove specs task HTTP bootstrap 2026-04-09 10:41:04 +05:30
Chirag AggarwalandGitHub 597b20a6cb Merge pull request #11795 from rathi-yash/fix-11765-global-variable-creation
fix: make variableId optional in createProjectVariable endpoint
2026-04-09 09:34:39 +05:30
Chirag AggarwalandGitHub 6b7d9853f9 Merge branch '1.9.x' into fix-11765-global-variable-creation 2026-04-09 09:24:52 +05:30
Damodar LohaniandGitHub d6f51a96a5 Merge branch '1.9.x' into CLO-4175-allow-delete-with-memberships 2026-04-09 07:32:30 +05:45
Damodar LohaniandGitHub 20f80ac067 Merge pull request #11580 from appwrite/feat-audit-user-type-distinction
feat: distinguish user types in audit logs
2026-04-09 06:55:43 +05:45
My Name 8bfa659120 test: add test case for createProjectVariable without variableId 2026-04-08 15:18:13 -04:00
Yash RathiandGitHub 65e44d76a1 Merge branch '1.9.x' into fix-11765-global-variable-creation 2026-04-08 14:50:58 -04:00
Jake BarnbyandGitHub bb07808661 Merge pull request #11820 from appwrite/fix-ttl-message 2026-04-09 03:39:48 +12:00
Luke B. SilverandGitHub d18550ff54 Merge pull request #11826 from appwrite/fix/cookie-test-hack
fix: use cURL cookie engine for RFC 6265 compliant cookie handling in e2e client
2026-04-08 16:17:04 +01:00
loks0nandClaude Sonnet 4.6 84dc921d41 fix: replace utopia-php/framework with http, fix RFC 6265 cookie handling
utopia-php/framework was the old name for utopia-php/http. Replacing it
with utopia-php/http 0.34.19 which fixes getCookie() to use Swoole's
native cookie store (populated via php_raw_url_decode) instead of
re-parsing the raw Cookie header without URL-decoding.

This fixes a production auth bug where Swoole's setcookie() URL-encodes
base64 session values (+ → %2B, / → %2F, = → %3D) in Set-Cookie headers.
RFC 6265 clients (Dart, Swift) reflect these verbatim; the old getCookie()
returned %2B/%2F/%3D to base64_decode() which produced corrupted output,
rejecting valid sessions.

Also updates the e2e test client to use cURL's built-in RFC 6265 cookie
engine (CURLOPT_COOKIEFILE) instead of parse_str() which silently
URL-decoded values, masking the bug in tests. Adds a cookie roundtrip
assertion to testCreateAccountSession.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 15:07:46 +01:00
loks0nandClaude Sonnet 4.6 e2d7dd837d fix: use cURL cookie engine instead of parse_str for RFC 6265 compliance
parse_str() URL-decodes cookie values, causing the test client to behave
differently from real clients (Dart, Swift) which store values verbatim
per RFC 6265. This masked a production bug where base64 session values
containing %3D%3D would fail to decode on real devices.

Replaces the manual Set-Cookie header parsing with cURL's built-in cookie
engine (CURLOPT_COOKIEFILE='') and reads cookies via CURLINFO_COOKIELIST,
which stores and returns values verbatim without any decoding.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 15:07:46 +01:00
Jake BarnbyandGitHub 9feb204fd5 Merge pull request #11611 from appwrite/feat-x-oauth2-provider 2026-04-09 01:32:31 +12:00
Jake Barnby 3880f181b3 fix(databases): propagate purge parameter to documentsdb updateCollection 2026-04-09 01:19:18 +12:00
Harsh Mahajan 44a37e9e20 Use Exception for X OAuth2 PKCE encryption errors
Align with other OAuth2 adapters that throw base Exception for
configuration and crypto failures instead of RuntimeException.

Made-with: Cursor
2026-04-08 18:41:42 +05:30
Harsh Mahajan e6cfedd340 addressed greptile comment 2026-04-08 18:27:36 +05:30
Harsh Mahajan e4d1178e71 simplified code 2026-04-08 17:56:37 +05:30
Harsh Mahajan 929bdcef25 Merge branch '1.9.x' into feat-x-oauth2-provider 2026-04-08 17:55:00 +05:30
Harsh Mahajan 3f725c6be9 changes 2026-04-08 17:44:49 +05:30
Jake BarnbyGitHubgreptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
990a32dd9e Update src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-04-08 23:31:55 +12:00
Jake Barnby c5b8ed9cc1 feat(databases): cache list responses without requiring a select query 2026-04-08 23:02:34 +12:00
Matej BačoandGitHub fec573d23b Merge pull request #11821 from appwrite/chore-public-project-api
Chore: Mark project response format sub-formats as public
2026-04-08 12:32:58 +02:00
Matej Bačo a144968d70 Fix formatting 2026-04-08 12:08:32 +02:00
Matej Bačo 6fa0724404 Mark project response format sub-formats as public 2026-04-08 11:52:12 +02:00
Jake Barnby b1ce71e6b0 (chore): fmt 2026-04-08 21:35:49 +12:00
Jake Barnby 939092726c test(databases): add regression for purge=true list cache invalidation 2026-04-08 21:31:42 +12:00
Jake Barnby c6f8599c75 feat(databases): add purge parameter to updateCollection and updateTable 2026-04-08 21:31:38 +12:00
Jake Barnby 91d8519940 refactor(databases): restructure list response cache and clarify ttl description 2026-04-08 21:31:34 +12:00
Matej BačoandGitHub a90f79f1c1 Merge pull request #11650 from appwrite/feat-public-project-keys
Feat: Public keys API
2026-04-08 11:15:04 +02:00
Matej Bačo f880b6e8c3 Fix failing tests 2026-04-08 10:52:20 +02:00
Matej Bačo b8d65326e6 Fix failing tests 2026-04-08 10:34:18 +02:00
Matej Bačo a9fd82e406 New tests 2026-04-08 10:32:20 +02:00
Matej Bačo a8c2491fbb Fix platform scopes 2026-04-08 10:17:48 +02:00
Jake BarnbyandGitHub 7f6486ec80 Merge pull request #11762 from bhardwajparth51/fix-10923-realtime-atomic-payload 2026-04-08 20:16:55 +12:00
Matej Bačo 388cec1737 Merge branch '1.9.x' into feat-public-project-keys 2026-04-08 10:16:22 +02:00
Parth BhardwajandGitHub 2f5a49a37d Merge branch '1.9.x' into fix-10923-realtime-atomic-payload 2026-04-08 13:39:54 +05:30
Matej BačoandGitHub 1f93184c42 Merge pull request #11615 from appwrite/feat-public-platform-api
Feat: public platform API
2026-04-08 10:08:10 +02:00
Jake BarnbyandGitHub 7d9cf48ca3 Merge pull request #11750 from appwrite/bump-database-version2 2026-04-08 20:04:25 +12:00
Matej Bačo eef2a7abdf Fix scopes 2026-04-08 10:01:52 +02:00
Matej Bačo c7a022ba43 Simplify after discussions 2026-04-08 09:54:57 +02:00
Matej Bačo 96a84a8fd7 Merge branch '1.9.x' into feat-public-project-keys 2026-04-08 09:45:15 +02:00
Matej Bačo cea242c66f Merge branch '1.9.x' into feat-public-platform-api 2026-04-08 09:29:54 +02:00
Matej Bačo ce4eb563b3 AI review fixes 2026-04-08 09:29:12 +02:00
Matej Bačo 2307d637fb Revert new patch version 2026-04-08 09:10:02 +02:00
premtsd-codeandGitHub 0aa72aafae Merge branch '1.9.x' into bump-database-version2 2026-04-08 11:32:22 +05:30
Chirag AggarwalandGitHub 6e1f0d57af Merge pull request #11817 from appwrite/fix/specs-unresolved-model
fix: throw RuntimeException for unresolved response models in spec generation
2026-04-08 11:08:30 +05:30
Chirag Aggarwal 62b6ef06e6 fix: add swoole extension to specs CI job 2026-04-08 10:49:50 +05:30
Chirag Aggarwal f5ab593261 fix: make Project model public for server SDK spec generation
The project.updateLabels route uses AuthType::KEY which makes it
available on the server platform, but the Project model had public=false
causing it to be filtered out during spec generation.
2026-04-08 10:47:37 +05:30
Chirag Aggarwal dd4a43b78c fix: throw RuntimeException for unresolved response models in spec generation
Spec generation silently produced a fatal error when a response model
string could not be resolved to a registered model object. Now throws a
clear RuntimeException in both Swagger2 and OpenAPI3 formats, for both
single and array model responses.

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

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 21:44:24 +01:00
Chirag AggarwalandGitHub 59bf4663ca Merge pull request #11808 from appwrite/fix/sdk-dev-branch-sync
fix: reset SDK dev branch to base branch before pushing
2026-04-08 00:40:56 +05:30
premtsd-codeandGitHub 6653dbbb92 Merge branch '1.9.x' into bump-database-version2 2026-04-07 20:47:13 +05:30
Prem Palanisamy d7d20ccb29 Remove (int) cast from setTenant in getDatabasesDB same-pool branch 2026-04-07 15:35:20 +01:00
Yash RathiandGitHub 2d54986b09 Merge branch '1.9.x' into fix-11765-global-variable-creation 2026-04-07 10:25:08 -04:00
Matej Bačo 9ea0b2bc2f formatting fix 2026-04-07 15:56:54 +02:00
Matej Bačo 715727853b Fix unit test 2026-04-07 15:56:42 +02:00
Matej Bačo 43d4f709d5 Revert composer changes 2026-04-07 15:00:03 +02:00
Matej Bačo f40050fe6f Revert lockfile changes (failing tests) 2026-04-07 14:51:40 +02:00
Prem Palanisamy 308a534d98 Merge branch 'bump-database-version2' of https://github.com/appwrite/appwrite into bump-database-version2 2026-04-07 13:16:56 +01:00
Prem Palanisamy 35a72c4f08 Remove (int) cast from setTenant in separate-pool branches 2026-04-07 13:10:16 +01:00
Matej Bačo 34dfcba45c Linter fix 2026-04-07 14:08:16 +02:00
Chirag AggarwalandGitHub cb1e31bcd4 Merge pull request #11814 from appwrite/codex/bump-utopia-framework-0-34-18
[codex] Bump utopia-php/framework to 0.34.18
2026-04-07 17:38:06 +05:30
Matej Bačo 3e4e7fc0cd Merge branch '1.9.x' into feat-public-platform-api 2026-04-07 14:08:00 +02:00
ArnabChatterjee20kandGitHub da2444ede2 Merge branch '1.9.x' into realtime-query-message-payload 2026-04-07 17:36:29 +05:30
ArnabChatterjee20k bc224de751 Add userId to connection info in Realtime adapter and simplify userId fetching 2026-04-07 17:35:48 +05:30
Chirag Aggarwal 9403e4d65d Bump utopia-php/framework to 0.34.18 2026-04-07 17:35:32 +05:30
Matej BačoandGitHub 07a49835a6 Merge pull request #11812 from appwrite/chore-migrate-labels-api
Chore: Migrat elabels API
2026-04-07 14:05:31 +02:00
Matej Bačo d32de6f217 Improve tests 2026-04-07 13:30:35 +02:00
Matej Bačo 9b00ce4f1d Add new tests 2026-04-07 13:28:35 +02:00
ArnabChatterjee20k ca62504b5a Enhance realtime message handling to support initial connection payload and improve query subscription logic 2026-04-07 16:55:10 +05:30
Matej Bačo 8f6c8f9d8d Migrate to new endpoint 2026-04-07 13:21:36 +02:00
Matej Bačo 23fcb284a1 Fix backwards QA 2026-04-07 12:46:25 +02:00
premtsd-codeandGitHub ffdd0815a7 Merge branch '1.9.x' into bump-database-version2 2026-04-07 15:56:47 +05:30
Prem Palanisamy 4260324153 Merge branch '1.9.x' into bump-database-version2
Resolve merge conflicts in app/init/resources.php and app/worker.php
caused by the DI container migration (Http::setResource/Server::setResource
to $container->set). Port separate-pool shared tables logic for
getDatabasesDB to the new file locations (request.php and message.php)
with the correct $databaseDSN->getParam('namespace') fix.
2026-04-07 11:25:06 +01:00
Matej Bačo d66813d3cf Fix tests + QA fixes 2026-04-07 12:07:21 +02:00
Chirag AggarwalandGitHub 0ce92da41f Merge pull request #11810 from appwrite/codex/fix-post-merge-test-regressions
[codex] Fix post-merge e2e test regressions
2026-04-07 15:10:53 +05:30
Chirag Aggarwal e8ef4e40d7 fix post-merge e2e test regressions 2026-04-07 15:05:07 +05:30
Chirag AggarwalandGitHub d200b07466 Merge pull request #11809 from appwrite/codex/fix-console-null-route
[codex] Handle null console routes in API middleware
2026-04-07 14:42:08 +05:30
Chirag Aggarwal 92abfb31aa fix null route guard placement 2026-04-07 14:40:18 +05:30
Chirag Aggarwal 6c56eee0f4 test console route not found error type 2026-04-07 14:39:48 +05:30
Chirag Aggarwal 399c37d943 fix console null route handling 2026-04-07 14:33:43 +05:30
Matej Bačo f3acadd53c Fix linter 2026-04-07 10:43:57 +02:00
Matej Bačo 331fcee710 Merge branch '1.9.x' into feat-public-platform-api 2026-04-07 10:43:11 +02:00
Chirag AggarwalandGitHub 951a96ae01 Merge pull request #11564 from appwrite/feat/migrate-di-container
feat: migrate from static Http::setResource() to DI Container
2026-04-07 13:58:10 +05:30
Damodar LohaniandGitHub ed1680837d Merge branch '1.9.x' into feat-audit-user-type-distinction 2026-04-07 14:11:22 +05:45
Chirag Aggarwal 7864a5b9d1 fix: use --force-with-lease on SDK dev branch push
After resetting dev to the base branch, the remote dev may have
diverged history from squash merges. A plain push would be rejected
as non-fast-forward. Using --force-with-lease safely overwrites
the remote since we just fetched.
2026-04-07 12:52:09 +05:30
Chirag Aggarwal f11bd7ce0e fix: reset SDK dev branch to base branch before pushing
The dev branch was being reset to origin/dev, which retains stale
commits after squash merges. This caused recurring merge conflicts
and inflated PR diffs. Using `checkout -B dev <baseBranch>` ensures
dev always starts fresh from the default branch.
2026-04-07 12:50:18 +05:30
Parth BhardwajandGitHub fffc6795a7 Merge branch '1.9.x' into fix-10923-realtime-atomic-payload 2026-04-07 08:49:25 +05:30
Damodar LohaniandGitHub 8442a1e612 Merge branch '1.9.x' into CLO-4175-allow-delete-with-memberships 2026-04-07 06:27:57 +05:45
Shimon NewmanandGitHub ed91549681 Merge pull request #11805 from appwrite/revert-schedule-functions-updates
revert ScheduleFunctions.php updates
2026-04-06 22:38:05 +03:00
shimon 04173600ae revert ScheduleFunctions.php updates 2026-04-06 22:33:18 +03:00
ArnabChatterjee20k d5fe5c34af Validate subscribe payload format in realtime message handling 2026-04-06 17:14:12 +05:30
ArnabChatterjee20k 6bc9adece8 Refactor realtime message handling to send subscriber keys and add comprehensive tests for subscription message upsert behavior 2026-04-06 17:10:57 +05:30
ArnabChatterjee20k 97d46c6273 Remove redundant subscription removal call in realtime message handling 2026-04-06 16:58:08 +05:30
ArnabChatterjee20k 9d78a8e6b6 Add stats tracking for outbound subscription messages in realtime 2026-04-06 16:57:06 +05:30
ArnabChatterjee20k d12a6f5168 Refactor realtime message payload handling for improved validation and parsing 2026-04-06 16:41:42 +05:30
ArnabChatterjee20k 0f47e6ea28 Enhance subscription message documentation for clarity on upsertion behavior 2026-04-06 15:59:22 +05:30
premtsd-codeandGitHub 77ceb15d7d Merge branch '1.9.x' into bump-database-version2 2026-04-06 15:44:40 +05:30
ArnabChatterjee20k 592629587d Remove unused query assertion methods and improve comment clarity in RealtimeQueryBase 2026-04-06 14:07:25 +05:30
ArnabChatterjee20k 187fde4a4e Refactor realtime subscription handling and enhance query validation in tests 2026-04-06 14:05:42 +05:30
Chirag Aggarwal b74d4d45f9 Merge request-scoped cookie resources 2026-04-06 13:21:33 +05:30
Chirag AggarwalandGitHub aa9a58b44d Merge pull request #11799 from appwrite/codex/request-scoped-cookie-domain
Use request-scoped cookie domain resource
2026-04-06 13:18:27 +05:30
Chirag Aggarwal 59a773e9a0 Document migration host local-domain handling 2026-04-06 12:47:06 +05:30
Chirag Aggarwal e3053bb83d Remove dead cookie config defaults 2026-04-06 12:44:48 +05:30
Chirag Aggarwal 1f7fc4bd40 Use request-scoped domain verification 2026-04-06 12:43:05 +05:30
Chirag Aggarwal d1b59ff3f3 Remove unused cookie domain locals 2026-04-06 12:30:48 +05:30
Chirag Aggarwal 221b52bac0 Add request-scoped cookie domain resource 2026-04-06 12:30:25 +05:30
Chirag Aggarwal b8ed30db55 Fix CORS header override for analyze 2026-04-06 12:23:50 +05:30
Chirag AggarwalandGitHub be56317bf2 Merge branch '1.9.x' into feat/migrate-di-container 2026-04-06 12:13:31 +05:30
fogelito 31728c9b72 Update lock 2026-04-06 09:29:58 +03:00
fogelito 9d0fc9e5b1 Merge branch '1.9.x' of https://github.com/appwrite/appwrite into delete-project-shared-table-v1 2026-04-06 09:19:38 +03:00
Chirag AggarwalandGitHub 2dce141d17 Merge pull request #11798 from appwrite/codex/request-response-no-static-state
Remove request and response static state
2026-04-06 11:03:15 +05:30
Damodar LohaniandGitHub d421b5ff30 Merge pull request #11797 from appwrite/fix-cors-paused-project
fix: add CORS headers to error responses
2026-04-06 10:42:00 +05:45
Chirag Aggarwal b8eb0810c2 Make response sensitive mode instance-scoped 2026-04-06 10:24:32 +05:30
Chirag Aggarwal cb74a5756a Remove request and response static state 2026-04-06 10:20:18 +05:30
Chirag AggarwalandGitHub d1549b225b Merge branch '1.9.x' into feat/migrate-di-container 2026-04-06 08:35:58 +05:30
Damodar LohaniandClaude Opus 4.6 ba25849871 fix: resolve cors safely in error handler to avoid cascading failures
- Remove cors from inject chain; resolve via getResource() inside
  try-catch so DB failures don't cascade when resolving the cors
  resource dependency chain (cors -> allowedHostnames -> rule -> DB)
- Use override:true on addHeader to prevent duplicate CORS headers
  when init() already set them before the exception was thrown
- Degrades gracefully: if cors resolution fails, error response is
  sent without CORS headers (same behavior as before this PR)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 02:59:08 +00:00
Damodar LohaniandClaude Opus 4.6 44f3bbae03 fix: add CORS headers to error responses
The Http::error() handler was missing CORS headers, causing browsers to
block error responses (e.g. 403 PROJECT_PAUSED) with a generic CORS
error instead of showing the actual error message. This injects the cors
resource into the error handler and adds CORS headers before sending the
error response, matching the pattern already used in Http::init().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 01:40:07 +00:00
My Name 2cd91f509b fix: make variableId optional in createProjectVariable endpoint 2026-04-05 18:23:58 -04:00
Aditya a06ba57384 fix appwrite auth broken link in readme 2026-04-05 23:39:13 +05:30
Shimon NewmanandGitHub 9ac686ae31 Merge pull request #11790 from appwrite/schedule-functions-fix
Improve schedule function logic
2026-04-05 19:34:49 +03:00
Shimon NewmanandGitHub 7385bc1d9f Merge branch '1.9.x' into schedule-functions-fix 2026-04-05 19:22:44 +03:00
Chirag AggarwalandGitHub dffcfe8ee4 Merge branch '1.9.x' into feat/migrate-di-container 2026-04-05 21:16:30 +05:30
Chirag AggarwalandGitHub e3f373b8d2 Merge pull request #11772 from appwrite/codex/fix-vectordb-migration-flake
[codex] Fix flaky VectorsDB metadata bootstrap in migrations
2026-04-05 21:16:18 +05:30
Chirag AggarwalandGitHub 8b591f3522 Merge branch '1.9.x' into codex/fix-vectordb-migration-flake 2026-04-05 21:13:18 +05:30
Chirag Aggarwal 452440f3c0 fix: use released cli container support 2026-04-05 21:03:17 +05:30
Chirag AggarwalandGitHub 4e10cabec0 Merge pull request #11789 from appwrite/codex/fail-missing-spec-docs
[codex] Fail specs when referenced docs are missing
2026-04-05 20:58:24 +05:30
Chirag Aggarwal 5d1da00138 refactor: remove redundant desc guards 2026-04-05 20:12:25 +05:30
Chirag Aggarwal b236e2546b lock file 2026-04-05 20:10:00 +05:30
Chirag Aggarwal 412d09b801 remove unrelated changes 2026-04-05 20:06:13 +05:30
Chirag Aggarwal 5ab28ad99a docs: add missing json migration references 2026-04-05 19:52:48 +05:30
shimon 9be447aacf Update enqueue timer and improve schedule function logic
Reduced the ENQUEUE_TIMER constant from 60 seconds to 30 seconds. Modified the condition for currentTick to use less than or equal to (<=) instead of less than (<) for better accuracy in scheduling. Changed return statement to continue in case of missing schedule key to enhance flow control.
2026-04-05 17:20:31 +03:00
Chirag Aggarwal 66e68aea14 fix: fail specs when docs are missing 2026-04-05 19:37:29 +05:30
fogelito a332eb5f32 Merge branch '1.9.x' of https://github.com/appwrite/appwrite into delete-project-shared-table-v1 2026-04-05 11:42:25 +03:00
Damodar LohaniandClaude Opus 4.6 cc82b1a5cf fix: don't promote non-owners on account deletion, leave team orphaned instead
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 07:15:35 +00:00
Damodar LohaniandClaude Opus 4.6 ba32012744 fix: filter unconfirmed members from owner count, ownership transfer, and primary user transfer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 07:11:32 +00:00
fogelito 775d21e5ee lock 2026-04-05 10:08:29 +03:00
Damodar LohaniandClaude Opus 4.6 8f6530d1e7 fix: remove unused $userInternalId from closure
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 05:34:04 +00:00
Damodar LohaniandClaude Opus 4.6 4297c70f58 fix: address review feedback — safer orphan approach, veteran ordering, deduplicate transfer
- Remove team deletion for sole owner+sole member case; let orphan teams
  be cleaned up by Cloud's inactive project cleanup (safer, avoids
  accidental data loss)
- Add explicit ordering by $createdAt so the most veteran member gets
  ownership transfer, with limit(1) for clarity
- Remove confirm filter on primary user transfer in membership deletion
  so all members (including unconfirmed) are considered
- Remove redundant ownership transfer from Deletes worker since the API
  controller already handles it before queueing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 05:22:02 +00:00
Damodar LohaniandGitHub f6484294da Merge branch '1.9.x' into feat-audit-user-type-distinction 2026-04-05 08:04:38 +05:45
Damodar LohaniandClaude Opus 4.6 16ed60a5c3 Filter unconfirmed members when transferring team ownership
Prevent unconfirmed (pending invite) members from being promoted to
owner or set as the team's primary user during membership/account
deletion by adding a Query::equal('confirm', [true]) filter to the
relevant findOne queries.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 02:02:09 +00:00
Damodar LohaniandClaude Opus 4.6 d831b93934 Allow deleting user account with active memberships
Instead of blocking account deletion when the user has confirmed team
memberships, handle memberships gracefully during deletion:

- Sole owner + sole member: delete the team and queue project cleanup
- Sole owner + other members: transfer ownership to the next member
- Non-owner / multiple owners: no special handling needed (worker cleans up)

Also update the Deletes worker to transfer the team's primary user
reference when removing a deleted user's memberships.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 01:43:05 +00:00
Chirag AggarwalandGitHub f3b9121a14 Merge branch '1.9.x' into feat/migrate-di-container 2026-04-04 15:57:12 +05:30
Eldad A. FuxandGitHub cf8519bfa5 Revise Appwrite description and product details
Updated the description of Appwrite to emphasize its open-source nature and capabilities. Added detailed product descriptions for Appwrite services.
2026-04-04 08:41:23 +02:00
Eldad A. FuxandGitHub 860cf8cd57 Revise README content and structure
Updated the README to enhance clarity and remove outdated information.
2026-04-04 08:32:46 +02:00
Chirag Aggarwal c978b6f34f Stabilize function deployment activation in tests 2026-04-03 23:58:25 +05:30
Chirag Aggarwal f3f2855fe5 Remove final formatting-only diff 2026-04-03 23:44:19 +05:30
Chirag Aggarwal 3cb53f0604 Drop unrelated formatting churn from VectorsDB fix 2026-04-03 23:43:51 +05:30
Chirag Aggarwal a5f45b46e9 Handle raced VectorsDB metadata bootstrap errors 2026-04-03 23:41:44 +05:30
bhardwajparth51 2c1813198d Simplify comments in Realtime E2E test 2026-04-03 22:12:24 +05:30
Chirag Aggarwal 130c2221ec Fix VectorsDB metadata bootstrap race 2026-04-03 22:12:21 +05:30
bhardwajparth51 912ea37af6 Address review feedback: Remove redundant Realtime triggers, correctly reorder hydration, and add E2E tests 2026-04-03 22:07:24 +05:30
Parth BhardwajandGitHub ccc74dea74 Merge branch '1.9.x' into fix-10923-realtime-atomic-payload 2026-04-03 20:28:49 +05:30
Torsten DittmannandGitHub c08e5f706c Merge pull request #11745 from appwrite/fix-yahoo-oauth-scopes 2026-04-02 19:44:10 +04:00
bhardwajparth51 a6c6a5624a fix: ensure realtime event payload is populated for all atomic subclasses 2026-04-02 21:04:01 +05:30
bhardwajparth51 ee5bb6d73d fix: ensure realtime event payload is populated for atomic operations 2026-04-02 20:47:23 +05:30
ArnabChatterjee20k bfbf180aee Refactor realtime message handling and enhance query validation tests 2026-04-02 18:32:27 +05:30
ArnabChatterjee20k d8a3b53641 Refactor code structure for improved readability and maintainability 2026-04-02 17:35:55 +05:30
Chirag Aggarwal 3018b478ba Fix database transaction and vectors migration flakiness 2026-04-02 17:25:13 +05:30
ArnabChatterjee20k df4dbcf607 updated user roles 2026-04-02 17:02:46 +05:30
Chirag Aggarwal 094fe90499 Merge remote-tracking branch 'origin/1.9.x' into feat/migrate-di-container
# Conflicts:
#	app/worker.php
2026-04-02 16:37:58 +05:30
Matej BačoandGitHub 58a90cca4a Merge pull request #10643 from appwrite/feat-disposable-emails
feat: add support for blocking disposable email addresses
2026-04-02 13:03:32 +02:00
ArnabChatterjee20k 29b0ebb3bd updated query subscription 2026-04-02 16:28:00 +05:30
ArnabChatterjee20k f0ccd1f586 added message based query payload to realtime 2026-04-02 15:56:56 +05:30
Matej Bačo 90e705f8c5 Improve docs 2026-04-02 12:26:03 +02:00
Matej Bačo 17076e4a00 Fix formatting 2026-04-02 11:55:32 +02:00
Matej Bačo 7c50bbc500 Merge branch '1.9.x' into feat-disposable-emails 2026-04-02 11:05:19 +02:00
Chirag Aggarwal 4df5f4a18f fix: scope composer lock to intended package updates 2026-04-02 14:06:45 +05:30
Chirag Aggarwal e8bcc94187 fix: keep composer lock scoped to intended updates 2026-04-02 13:59:52 +05:30
Prem Palanisamy a071c715bc Merge remote-tracking branch 'origin/1.9.x' into bump-database-version2 2026-04-02 10:10:03 +02:00
Prem Palanisamy 52981e0164 fix: restore full CI matrix and revert test/endpoint changes to 1.9.x 2026-04-02 10:09:36 +02:00
Chirag Aggarwal e576cd5082 Merge branch '1.9.x' into feat/migrate-di-container 2026-04-02 13:32:31 +05:30
Chirag AggarwalandGitHub 387668bb47 Merge pull request #11748 from appwrite/feat-smtp-messaging-adapter
Replace PHPMailer with utopia-php/messaging SMTP adapter
2026-04-02 12:11:58 +05:30
Chirag Aggarwal 30befc6a60 fix: remove strict pool size exception 2026-04-02 11:41:35 +05:30
Chirag Aggarwal d13644a47a fix: make pool sizing runtime-aware 2026-04-02 11:38:19 +05:30
Chirag Aggarwal 4a905a6ac9 Merge branch '1.9.x' into feat/migrate-di-container
Resolve conflicts keeping DI container migration (container->set pattern)
while incorporating 1.9.x fixes: PHPStan unused variable cleanup in
GraphQL Resolvers, (int) casts in Builds.php, and phpstan-baseline removal.
2026-04-02 11:17:32 +05:30
Chirag Aggarwal b6e95f4502 test: remove redundant headers empty-key case 2026-04-02 11:05:03 +05:30
Chirag Aggarwal 1900492aad Merge branch '1.9.x' into feat-smtp-messaging-adapter 2026-04-02 10:58:09 +05:30
Prem Palanisamy cb9d3869f0 Merge remote-tracking branch 'origin/1.9.x' into bump-database-version2
# Conflicts:
#	src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Create.php
2026-04-02 06:53:05 +02:00
Chirag AggarwalandGitHub 96f4ef7d22 Merge pull request #11751 from appwrite/chore/remove-phpstan-baseline
chore: remove phpstan baseline
2026-04-02 10:22:21 +05:30
Chirag AggarwalandGitHub 8e02a5a227 Merge pull request #11757 from appwrite/air/fix-flaky-migration-test-580aa1bc-d
Fix flaky vectordb migration test
2026-04-02 10:19:15 +05:30
Prem Palanisamy d17ae517d3 fix: replace exists+create with try/catch for vectorsdb metadata init 2026-04-02 06:26:55 +02:00
Chirag Aggarwal c2dd8aceda use stable 2026-04-02 09:42:59 +05:30
Chirag Aggarwal 24fcf4247c Fix flaky vectordb migration test 2026-04-02 09:41:20 +05:30
Chirag Aggarwal 2dc20ef0eb Update utopia-php/messaging lock to latest 2026-04-02 09:31:48 +05:30
Chirag Aggarwal 04943b6313 Pass recipient display name in EmailMessage to field
Use associative array format ['email' => ..., 'name' => ...] for
the to field so the recipient display name appears in the To header.
2026-04-02 09:29:57 +05:30
Chirag Aggarwal eb366cf94b fix: preserve cors max age type 2026-04-02 08:25:59 +05:30
Chirag Aggarwal 77b4f8b7a0 style: apply formatter 2026-04-02 08:23:51 +05:30
Chirag Aggarwal 24bd4c3d7b fix: preserve migration resource stats 2026-04-02 08:11:57 +05:30
Prem Palanisamy 25d9891f2b fix: use email probe in JSON export test, format fixes, add requireAdapter skips to Databases VectorsDB/DocumentsDB tests 2026-04-02 04:39:56 +02:00
Damodar LohaniandGitHub a928fcd40f Merge pull request #11754 from appwrite/claude/sync-1.9.x-fix-tests-VoCBt
Fix PHPStan baseline and code quality issues
2026-04-02 08:17:54 +05:45
Claude 2d34301834 fix: add missing userType field to legacy log endpoints
The users.php and messaging.php legacy controllers were missing the
userType field in their log output, creating an inconsistency with the
new audit user type distinction feature. Also adds missing mode field
to users.php logs endpoint.

https://claude.ai/code/session_01J9gKXwbHoLggsGwJi6KUnM
2026-04-02 01:13:01 +00:00
Claude cf7259e060 Merge remote-tracking branch 'origin/1.9.x' into claude/sync-1.9.x-fix-tests-VoCBt 2026-04-02 01:05:56 +00:00
Prem Palanisamy 8c9f9c6f58 fix: inherit project shared tables for separate pools, skip cross-engine migration tests 2026-04-02 02:05:10 +02:00
Chirag Aggarwal 33f8e35b62 chore: remove phpstan baseline 2026-04-01 23:01:11 +05:30
Chirag Aggarwal 2c1060f57a Fix null array key in HeadersTest
Cast null to string to avoid invalid array key type error
while preserving the test's intent of validating empty keys.
2026-04-01 21:31:08 +05:30
Chirag Aggarwal 30e0ca81bd Fix Usage::fromArray() to use static return type with new static()
Keep covariant return type with parent Base::fromArray(). The
new static() is safe here because the cloud subclass constructor
is backwards-compatible via optional params.
2026-04-01 21:30:36 +05:30
Chirag Aggarwal 92cc382a6b Change Usage::fromArray() return type from static to self
The cloud subclass has a different constructor signature so
new static() is unsafe. Use self since subclasses that need
deserialization should override fromArray() themselves.
2026-04-01 21:25:22 +05:30
Prem Palanisamy c322cc3ffe Merge remote-tracking branch 'origin/1.9.x' into bump-database-version
# Conflicts:
#	src/Appwrite/Event/Message/Usage.php
2026-04-01 17:34:13 +02:00
Chirag Aggarwal 7d428ffe83 trigger ci 2026-04-01 21:03:07 +05:30
Chirag Aggarwal fb96aecbef Use new static() in Usage::fromArray() for late static binding
Required since the class is no longer final and the return type
is static, so subclasses get the correct type.
2026-04-01 19:52:16 +05:30
Matej Bačo 80d5d4716a Remove base errors 2026-04-01 15:54:26 +02:00
Matej Bačo c1dde09070 Merge branch '1.9.x' into feat-disposable-emails 2026-04-01 15:14:10 +02:00
ArnabChatterjee20k cf99269bc5 Add reset method to Realtime class for clearing event state
This new method resets the event state for long-lived worker processes by clearing subscribers, context, and user-related fields, ensuring no stale state affects subsequent triggers.
2026-04-01 18:08:45 +05:30
ArnabChatterjee20k 2a184288f4 removed throw 2026-04-01 18:05:56 +05:30
ArnabChatterjee20k 9237f387fc added reset in db worker for queue for realtime 2026-04-01 17:59:28 +05:30
Chirag Aggarwal d531c29fc8 Remove final from Usage class to allow cloud override 2026-04-01 17:20:34 +05:30
Matej Bačo b554243447 Merge branch '1.9.x' into feat-public-platform-api 2026-04-01 13:35:46 +02:00
Chirag Aggarwal 7044601603 lock file 2026-04-01 17:05:14 +05:30
Chirag Aggarwal 53bc126015 Remove stale PHPMailer baseline entry from PHPStan
The PHPMailer $Port type error no longer occurs since registers.php
now uses the SMTP adapter instead of PHPMailer directly.
2026-04-01 17:01:15 +05:30
Chirag Aggarwal f1dc468e50 Restore replyTo elseif logic to preserve old behavior
customMailOptions replyTo and smtp replyTo are mutually exclusive,
matching the original PHPMailer implementation.
2026-04-01 16:37:08 +05:30
Chirag Aggarwal 15b2ab321e fix: restore pool size validation to prevent silent connection exhaustion
The old guard that threw when workerCount > instanceConnections was
removed during the DI migration, causing pool size to silently floor
to 1. This can lead to connection exhaustion on multi-core hosts.
2026-04-01 16:34:58 +05:30
Chirag Aggarwal ce63b00cf4 lock file 2026-04-01 16:29:06 +05:30
Chirag Aggarwal b9aaecba25 Replace PHPMailer with utopia-php/messaging SMTP adapter
Use Utopia\Messaging\Adapter\Email\SMTP instead of raw PHPMailer
for the smtp register, Mails worker, and Doctor task. This enables
swapping email adapters (e.g. Resend) via DI override in downstream
repos by type-hinting against the EmailAdapter base class.
2026-04-01 16:25:33 +05:30
Chirag Aggarwal 789870b545 fix: preserve multi-value headers like Set-Cookie instead of comma-joining
addHeader() already accumulates multiple values for the same key into an
array internally, so calling it once per value is the correct approach.
Comma-joining violates RFC 6265 for Set-Cookie headers.
2026-04-01 15:43:14 +05:30
Chirag Aggarwal c9f7b7f0d9 fix: address PR review findings from code review
- Add Console::error() fallback in Bus::dispatch() so listener failures
  are visible even without telemetry (C1/M7)
- Remove duplicate $max/$sleep assignments in createDatabase (M1)
- Remove duplicate @param in Event::generateEvents docblock (M2)
- Remove unused $plan parameter from plan resource factory (M3)
- Fix inconsistent indentation in certificate init block (L2)
- Add explicit return null in session resource factory (M6)
2026-04-01 15:42:15 +05:30
Chirag Aggarwal cba7e53898 chore: fix composer 2026-04-01 15:37:34 +05:30
Chirag Aggarwal fb26da5df1 analyze fixes 2026-04-01 15:15:48 +05:30
Torsten Dittmann d9c606a1c2 fix(oauth): update Yahoo OAuth scopes from deprecated Social Directory API to OIDC
The Yahoo OAuth provider was using deprecated Social Directory API scopes
('sdct-r' and 'sdpp-w') which are no longer valid and causing authentication
failures with the error: invalid_scope

Changes:
- Replace deprecated scopes 'sdct-r' (Social Directory Contacts Read) and
  'sdpp-w' (Social Directory Profile Write) with standard OIDC scopes
- Add 'openid' scope for OpenID Connect authentication
- Add 'profile' scope for basic profile information
- Add 'email' scope for email address access

These new scopes align with Yahoo's OpenID Connect implementation and are
listed in their discovery document at:
https://api.login.yahoo.com/.well-known/openid-configuration

The Yahoo adapter already uses the OIDC userinfo endpoint
(https://api.login.yahoo.com/openid/v1/userinfo), so these scopes are the
correct choice for authentication.

Custom scopes passed via the API are still supported and will be merged
with these defaults via the base OAuth2 class constructor.

Fixes: Yahoo OAuth authentication returning 'invalid_scope' error
2026-04-01 13:15:22 +04:00
Chirag Aggarwal eb8455bd76 revert 2026-04-01 14:29:20 +05:30
Chirag AggarwalandGitHub a76a03d988 Merge branch '1.9.x' into feat/migrate-di-container 2026-04-01 14:22:13 +05:30
Prem Palanisamy d9eb69aa47 Merge remote-tracking branch 'origin/1.9.x' into bump-database-version 2026-04-01 10:41:39 +02:00
Chirag AggarwalandGitHub a5b0378138 Merge pull request #11737 from appwrite/codex/phpstan-baseline-part-2
[codex] Fix PHPStan baseline cleanup issues (part 2)
2026-04-01 13:23:15 +05:30
Chirag AggarwalandGitHub 320068a576 Merge pull request #11740 from appwrite/speed-up-phpstan
Speed up PHPStan analysis with result caching
2026-04-01 13:01:07 +05:30
Chirag Aggarwal 3cd90ae629 fix analyze 2026-04-01 12:59:51 +05:30
Damodar LohaniandGitHub e250b413f0 Merge branch '1.9.x' into feat-audit-user-type-distinction 2026-04-01 12:58:09 +05:45
Harsh MahajanandGitHub 8ccfb1aebb Merge branch '1.9.x' into feat-x-oauth2-provider 2026-04-01 12:17:05 +05:30
Harsh Mahajan 9da4f19d4f fix: pkce flow 2026-04-01 12:11:40 +05:30
Chirag Aggarwal 44f33473bd Use composer analyze in CI to stay in sync with local workflow 2026-04-01 12:04:31 +05:30
Chirag Aggarwal 358f1b78a8 Speed up PHPStan analysis with result caching
Configure a project-local result cache directory so PHPStan only
re-analyses files that changed. In CI, persist the cache across
runs with actions/cache and suppress progress output.
2026-04-01 12:00:32 +05:30
Chirag Aggarwal cc04c682b0 chore: use phpstan-baseline.neon from 1.9.x 2026-04-01 11:49:43 +05:30
Chirag Aggarwal 66ba483b6a chore: remove inapplicable phpstan baseline entries from 1.9.x merge
$register variable.undefined (app/http.php) and binary op (app/worker.php) suppressions don't apply to this branch's rewritten DI container code.
2026-04-01 11:48:41 +05:30
Chirag Aggarwal 908e408480 Merge remote-tracking branch 'origin/1.9.x' into feat/migrate-di-container
# Conflicts:
#	app/init/resources.php
#	composer.json
#	composer.lock
#	phpstan-baseline.neon
2026-04-01 11:46:13 +05:30
Jake BarnbyandGitHub 1f6b9d94bf Merge pull request #11739 from appwrite/readme-update-1.9.0-installation 2026-04-01 05:53:38 +00:00
Aditya OberaiandGitHub a734c8cd46 Update installation commands in readme for 1.9.0 to include self-hosted wizard 2026-04-01 11:20:45 +05:30
Chirag Aggarwal 1788e1bd6c Address PR review feedback 2026-04-01 11:15:59 +05:30
Prem Palanisamy 7f9ce1ca85 Merge remote-tracking branch 'origin/1.9.x' into bump-database-version 2026-04-01 07:37:57 +02:00
Chirag Aggarwal 983adf3ffd Fix analyze regressions in PHPStan cleanup 2026-04-01 11:00:26 +05:30
Chirag Aggarwal f2ea0b9b48 Fix PHPStan baseline cleanup issues (part 2) 2026-04-01 10:20:20 +05:30
Jake BarnbyandGitHub 44610462b3 Merge pull request #11735 from appwrite/lohanidamodar-patch-2 2026-04-01 03:57:20 +00:00
Damodar LohaniandGitHub 28ece7de02 Change Usage class from final to non-final 2026-04-01 09:36:26 +05:45
Jake BarnbyandGitHub 8c6d4d8b36 Merge pull request #11734 from appwrite/fix-defaults 2026-04-01 02:50:09 +00:00
Jake Barnby 2ebc6f70ef (fix): param default 2026-04-01 15:49:40 +13:00
Damodar LohaniandGitHub 2b7690d6db Merge pull request #11732 from appwrite/claude/add-deployment-hook-method-prkpM
Add beforeCreateGitDeployment hook for deployment validation
2026-04-01 08:27:50 +05:45
Damodar LohaniandGitHub d9af799cc7 Merge branch '1.9.x' into claude/add-deployment-hook-method-prkpM 2026-04-01 08:09:46 +05:45
Damodar LohaniandGitHub 3ed1ca736d Merge pull request #11731 from appwrite/claude/update-php-runtimes-hNh1r
Update dependencies
2026-04-01 07:57:25 +05:45
Jake BarnbyandGitHub 9fa35db838 Merge pull request #11733 from appwrite/fix-defaults 2026-04-01 02:08:45 +00:00
Claude b6e020389b Remove docblock from beforeCreateGitDeployment hook
https://claude.ai/code/session_01HP1N9hHbqMzxm5QmaoGhyZ
2026-04-01 02:07:36 +00:00
Jake Barnby ccc0cfbfdc (fix): migrate default 2026-04-01 15:04:52 +13:00
Claude b91506fc2d Rename hook to beforeCreateGitDeployment
https://claude.ai/code/session_01HP1N9hHbqMzxm5QmaoGhyZ
2026-04-01 02:03:59 +00:00
Claude 9ffc23946c Add validateGitDeployment hook method to Deployment trait
Add a no-op protected method that Cloud can override to enforce
billing/block checks before processing git deployments. The hook
is called inside the foreach loop after project validation, so any
exception it throws is caught and logged as an error.

https://claude.ai/code/session_01HP1N9hHbqMzxm5QmaoGhyZ
2026-04-01 02:02:18 +00:00
Jake BarnbyandGitHub 8bb6b5cd2a Merge pull request #11646 from appwrite/feat/import-export-json 2026-04-01 01:56:47 +00:00
Claude afea4ca57b Update appwrite/php-runtimes to 0.19.5
https://claude.ai/code/session_01KXrbPzuXNzRhn38xm9zqwJ
2026-04-01 01:44:07 +00:00
Jake BarnbyandGitHub b30b89ade9 Merge pull request #11730 from appwrite/fix-v24 2026-04-01 01:11:54 +00:00
Jake Barnby 829cf887dc (fix): missing users case 2026-04-01 14:07:59 +13:00
premtsd-codeandGitHub d862a64874 Merge branch '1.9.x' into feat/import-export-json 2026-03-31 22:54:12 +01:00
Prem Palanisamy 168166b9c1 bump utopia-php/database to 5.3.19 and fix shared-mode CI failures
- Bump utopia-php/database from 5.3.17 to 5.3.19

- Remove invalid (int) cast on tenant sequence in shared tables mode

- Fix DSN construction for documentsdb/vectorsdb: filter empty strings
  from explode(), skip pool filtering when shared tables env vars unset,
  fail fast when no pool found

- Use dedicated mode for separate database pools in getDatabasesDB
  since shared tables can't work across engines (PostgreSQL integer
  _tenant vs MongoDB UUID tenant). Auto-init schema on first use.

- Add documentsdb/vectorsdb shared tables env vars to CI workflow

- Fix testChannelTablesDBRowUpdate race condition with deterministic
  event drain loop
2026-03-31 21:24:22 +02:00
Luke B. SilverandGitHub 142e671c7d Merge pull request #11720 from appwrite/codex/phpstan-core-type-docs-1
[codex] Fix PHPStan core type and PHPDoc issues (part 1)
2026-03-31 19:42:26 +01:00
Chirag Aggarwal b4085d1083 Fix token trait PHPStan static access 2026-03-31 22:23:37 +05:30
Chirag Aggarwal 18ed6a9c59 Fix more PHPStan static access issues 2026-03-31 22:04:37 +05:30
fogelito e42a26fdad comment 2026-03-31 19:20:13 +03:00
fogelito 2c4770d29b Database 5.3.19 2026-03-31 19:18:32 +03:00
Chirag Aggarwal 3d64ccd056 Fix more PHPStan docblock issues 2026-03-31 21:48:14 +05:30
Chirag Aggarwal 4f73eb021f Fix PHPStan core type and PHPDoc issues (part 1) 2026-03-31 21:44:20 +05:30
premtsd-codeandGitHub 7dcfd3ff3d Update description for get-queue-audits endpoint 2026-03-31 12:38:48 +01:00
Prem Palanisamy 4dfdfb5e59 Merge remote-tracking branch 'origin/1.9.x' into feat/import-export-json 2026-03-31 12:37:17 +01:00
Prem Palanisamy 5d1009b324 fix: correct resourceType routing, schemaless validation, and E2E tests for migrations
- Add getDatabaseResourceType() helper to map database types to resource constants
- Use database-specific resourceType for CSV/JSON import/export instead of hardcoded TYPE_DATABASE
- Skip attribute validation for schemaless databases (DocumentsDB/VectorsDB) in exports
- Parse JSON export queries in migration worker
- Restore MigrationsBase from 1.9.x and append VectorsDB/DocumentsDB E2E tests
2026-03-31 12:35:18 +01:00
Chirag AggarwalandGitHub 4568316345 Merge pull request #11715 from appwrite/fix-specs-defaults
Fix spec generation defaults for _APP_HOME and _APP_SYSTEM_TEAM_EMAIL
2026-03-31 15:52:14 +05:30
Jake BarnbyandGitHub c444bccacc Merge pull request #11716 from appwrite/fix-installer 2026-03-31 10:09:42 +00:00
Jake Barnby f9aee4de5d (fix): clear stale install data before starting new installation 2026-03-31 23:08:31 +13:00
fogelito 2a9e423cb1 Temporary disabling deletes from internal collections 2026-03-31 12:54:33 +03:00
Chirag Aggarwal 037878cc75 fix: use correct defaults for spec generation
Use https://appwrite.io and team@appwrite.io as defaults for _APP_HOME
and _APP_SYSTEM_TEAM_EMAIL in spec generation, instead of [HOSTNAME]
and team@localhost.test placeholders.
2026-03-31 15:11:11 +05:30
Jake BarnbyandGitHub 416b26ac7d Merge pull request #11710 from appwrite/fix-installer
(fix): guard against missing Host header in dispatch
2026-03-31 08:30:23 +00:00
Jake Barnby b47ac00ca8 (refactor): rename migrate param and add --migrate flag to upgrade task 2026-03-31 21:08:29 +13:00
Jake Barnby 2f53d09c5b (feat): add database migration step to upgrade installer 2026-03-31 20:58:33 +13:00
Jake Barnby 1fa1aa8621 (fix): guard against missing Host header in dispatch 2026-03-31 20:58:22 +13:00
fogelito 14869cc6d6 Safe delete shared tables v1 2026-03-31 09:59:53 +03:00
Jake BarnbyandGitHub ec20fb59e2 Merge pull request #11689 from appwrite/fix-installer 2026-03-31 03:51:20 +00:00
Jake Barnby 7e49cf0bed Revert "(fix): increase GraphQL schema polling timeouts to match CI expectations"
This reverts commit 4e32497be1.
2026-03-31 16:48:32 +13:00
Jake BarnbyandClaude Opus 4.6 4e32497be1 (fix): increase GraphQL schema polling timeouts to match CI expectations
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 16:26:20 +13:00
Jake Barnby fe6f79c9c6 (fix): restore ip key in tracking payload 2026-03-31 16:02:22 +13:00
Jake Barnby 5c84efdd7c (fix): upgrade UI — remove min-height, hide secret key row, fix copy 2026-03-31 15:58:45 +13:00
Jake Barnby 02d234ad3a (fix): detect existing installation as upgrade for web installer 2026-03-31 15:58:41 +13:00
Jake Barnby 8b7459f634 (fix): address review comments — coroutine guard and hostIp key 2026-03-31 15:15:51 +13:00
Jake Barnby 100cbf50f0 Merge remote-tracking branch 'origin/feat-auto-detect-cli' into fix-installer
# Conflicts:
#	src/Appwrite/Platform/Tasks/Install.php
2026-03-31 15:04:56 +13:00
Jake Barnby c7c59f4e7b Fix key 2026-03-31 15:04:04 +13:00
Jake Barnby a955dff55a (fix): run install tracking in coroutine to avoid blocking worker 2026-03-31 14:59:31 +13:00
Jake Barnby 7bcd5b4ebc (fix): remove swallowed exception around tracking call 2026-03-31 14:46:04 +13:00
Jake Barnby dfb23612b0 (docs): rewrite AGENTS.md with module structure and action patterns 2026-03-31 14:44:24 +13:00
Jake Barnby 4e71a54fae (fix): send SSE done event before tracking to prevent installer hang 2026-03-31 14:44:20 +13:00
Jake BarnbyandGitHub ea032c2804 Merge pull request #11679 from appwrite/fix/remove-documentsdb-spatial 2026-03-30 23:40:03 +00:00
Prem Palanisamy a80ecd0cb6 fix: alphabetize imports, update phpstan baseline count for migrations tests 2026-03-30 17:24:58 +01:00
Prem Palanisamy 8be7c6182e fix: update composer.lock for utopia-php/database branch 2026-03-30 17:16:05 +01:00
premtsd-codeandGitHub 3bb6a8bcc8 Merge branch '1.9.x' into feat/import-export-json 2026-03-30 16:15:16 +01:00
Prem Palanisamy c72d438a10 fix: remove INDEX_SPATIAL from DocumentsDB index creation 2026-03-30 15:55:37 +01:00
Prem Palanisamy 0085d93aeb fix: add version alias for database branch 2026-03-30 15:48:05 +01:00
Prem Palanisamy 3b9b0c96c4 use utopia-php/database fix/mongo-order-case branch for order case normalization 2026-03-30 15:46:20 +01:00
Prem Palanisamy 2611bf4af1 fix: add setPlatform to JSON import trigger for consistency 2026-03-30 15:43:21 +01:00
Prem Palanisamy de219de31d fix: restore original databasetype block in CSV export 2026-03-30 15:26:09 +01:00
Prem Palanisamy d8bbd82556 fix: remove duplicate database fetch, add null-safe queries fallback, add schemaless comment 2026-03-30 14:55:05 +01:00
Harsh MahajanandGitHub 5d962e21b0 Merge pull request #11674 from appwrite/fix-vcs-sdk-models
fix: merge duplicate SDK responses in VCS repository list and detections
2026-03-30 16:49:56 +05:30
Harsh Mahajan 551c83dd2c fix: merge duplicate SDK responses in VCS repository list and detections 2026-03-30 16:34:26 +05:30
Harsh Mahajan fe99470374 revert test env change 2026-03-30 16:09:42 +05:30
Chirag AggarwalandGitHub d964c5a439 Merge pull request #11671 from appwrite/codex/reduce-specs-memory-retention
[codex] Reduce specs task memory retention
2026-03-30 14:48:27 +05:30
Chirag Aggarwal 4850fb54ed Disallow --release=yes with --mode=examples 2026-03-30 14:19:02 +05:30
Chirag Aggarwal 796bc13c19 Skip spec version prompt when creating SDK releases
When --release=yes, the Appwrite spec version is not needed since
the release flow uses the SDK version from config. This moves the
version prompt and validation behind a !$createRelease guard and
hoists the release block before the spec/SDK generation setup.
2026-03-30 14:11:36 +05:30
Chirag Aggarwal 30e080c4d3 Address review: race-safe mkdir and separate encode/write errors 2026-03-30 14:04:20 +05:30
Prem Palanisamy aaebeec61e fix: remove spatial from documentsdb indexes, parse JSON export queries, skip schema validation for schemaless exports 2026-03-30 09:09:50 +01:00
ArnabChatterjee20kandGitHub 1789d2ce89 Merge pull request #11658 from appwrite/update-db-size
updated db size
2026-03-30 13:37:18 +05:30
Chirag Aggarwal 239a9c5457 Reduce specs task memory retention 2026-03-30 13:32:57 +05:30
ArnabChatterjee20kandGitHub 995e9fdf39 Merge branch '1.9.x' into update-db-size 2026-03-30 13:07:30 +05:30
Damodar LohaniandGitHub 8ce42fe057 Merge pull request #11553 from appwrite/claude/apply-cloud-user-pattern-az7qg
Use injected user document for privilege checks
2026-03-30 10:24:58 +05:45
Claude c14ebab1a0 Fix unintentional merge artifacts in Upsert.php and XList.php
Revert auth types in Bulk/Upsert.php back to [AuthType::ADMIN, AuthType::KEY]
and remove duplicate query filter in Databases/XList.php that were accidentally
introduced during the 1.9.x merge.

https://claude.ai/code/session_01JLPDurUgyj7qViA8JqQFTH
2026-03-30 04:11:52 +00:00
Claude f1ea496764 fix: add missing inject('user') to Transactions/Operations/Create and
remove duplicate inject('user') from XList in DocumentsDB/VectorsDB

- DocumentsDB and VectorsDB Transactions/Operations/Create.php were
  missing ->inject('user') needed by parent action method
- DocumentsDB and VectorsDB Collections/Documents/XList.php had
  duplicate ->inject('user') calls - removed the extra one

https://claude.ai/code/session_01JLPDurUgyj7qViA8JqQFTH
2026-03-29 03:55:17 +00:00
Claude 95b6da085b fix: add missing ->inject('user') to DocumentsDB and VectorsDB child classes
The parent action methods in Databases/Collections/Documents require a
User $user parameter, but 10 child classes in DocumentsDB (6) and
VectorsDB (4) were missing the ->inject('user') call in their
constructor inject chains. This caused fatal errors when those
endpoints were hit during E2E tests.

Files fixed:
- DocumentsDB: Delete, Get, Update, XList, Attribute/Increment, Attribute/Decrement
- VectorsDB: Delete, Get, Update, XList

https://claude.ai/code/session_01JLPDurUgyj7qViA8JqQFTH
2026-03-29 03:26:51 +00:00
Claude 32005c0a49 fix: remove redundant new User(getArrayCopy()) wrapping
Since setDocumentType('users', User::class) is registered on all
database instances, getDocument('users', ...) already returns User
instances. The new User($doc->getArrayCopy()) pattern was redundant
and could lose internal state managed by the database layer.

https://claude.ai/code/session_01JLPDurUgyj7qViA8JqQFTH
2026-03-29 03:04:43 +00:00
Damodar LohaniandGitHub 98c63a87d1 Merge branch '1.9.x' into claude/apply-cloud-user-pattern-az7qg 2026-03-29 08:30:11 +05:45
Chirag Aggarwal ff903e7cbb lock file 2026-03-28 20:39:08 +05:30
Chirag AggarwalandGitHub 10ebf942b1 Merge branch '1.9.x' into feat/migrate-di-container 2026-03-28 20:36:08 +05:30
Chirag AggarwalandGitHub c5b9ac3dba Merge pull request #11664 from appwrite/codex/static-sdk-platform-1-9-x
[codex] Move static SDKs off platform specs
2026-03-28 20:22:14 +05:30
Chirag Aggarwal be50318e5d chore: update composer lock 2026-03-28 17:26:21 +05:30
Chirag Aggarwal f901b6e0ac feat: move static SDKs off platform specs 2026-03-28 17:24:20 +05:30
premtsd-codeandGitHub 53d0e14f97 Merge branch '1.9.x' into feat/import-export-json 2026-03-28 04:23:07 +00:00
Matej Bačo d9a75c5938 Improved backwards compatibility 2026-03-27 15:34:34 +01:00
ArnabChatterjee20kandGitHub 325b1d67b4 Merge branch '1.9.x' into update-db-size 2026-03-27 19:47:35 +05:30
ArnabChatterjee20k 3ff7cadcab updated project size 2026-03-27 18:39:26 +05:30
Matej Bačo bb80e50d01 Self review after refactor 2026-03-27 14:00:44 +01:00
Matej Bačo 644840ec66 Refactor to new platforms interfaces 2026-03-27 13:45:58 +01:00
Matej Bačo 71e714c66b Remove extra logging 2026-03-27 12:49:16 +01:00
Matej Bačo b2b187c57d review fixes 2026-03-27 12:35:48 +01:00
Chirag AggarwalandGitHub f2c8500d73 Merge pull request #11654 from appwrite/fix-specs
Fix issues in generated specs
2026-03-27 12:50:01 +05:30
copilot-swe-agent[bot]andMeldiron ecc76c7520 fix: address review feedback - log abort exception, fix test cleanup
Agent-Logs-Url: https://github.com/appwrite/appwrite/sessions/045e63f6-9a81-447b-ba79-7391ab97fb01

Co-authored-by: Meldiron <19310830+Meldiron@users.noreply.github.com>
2026-03-26 18:34:19 +00:00
copilot-swe-agent[bot]andMeldiron 898e8e214b fix: allow deletion of partially-uploaded (pending) files and add test
Agent-Logs-Url: https://github.com/appwrite/appwrite/sessions/8d14b17a-78d8-48c6-b6d8-51d9697adde0

Co-authored-by: Meldiron <19310830+Meldiron@users.noreply.github.com>
2026-03-26 18:04:41 +00:00
Aditya Oberai babb0196e4 Fix examples in MFAType model 2026-03-26 16:29:39 +00:00
Matej Bačo 43db2ddf6e Fix default null 2026-03-26 16:24:28 +01:00
Matej Bačo 7371b68418 Fix tests compatibility 2026-03-26 15:51:03 +01:00
Matej Bačo 7f4d5f692d Fix non-expiry key actions 2026-03-26 15:44:50 +01:00
Matej Bačo 8113854a89 AI review fixes 2026-03-26 15:37:57 +01:00
Matej Bačo eb097a037b Finish security todos 2026-03-26 15:35:40 +01:00
Matej Bačo cbfdd27834 Public keys Apis 2026-03-26 15:00:20 +01:00
Matej Bačo 113c266881 Fix audit resource ID missing 2026-03-26 14:43:38 +01:00
Chirag AggarwalandGitHub 9353fbfb74 Merge pull request #11614 from appwrite/feat-rust-sdk
feat: add Rust SDK support
2026-03-26 18:59:43 +05:30
Chirag Aggarwal 918f1d37d4 phpstan 2026-03-26 18:37:45 +05:30
Chirag Aggarwal 196acd45a2 lock file 2026-03-26 18:26:17 +05:30
Harsh Mahajan 614db7388e fix: push 2026-03-26 17:59:30 +05:30
Chirag Aggarwal ef6fdc6c72 lock file 2026-03-26 17:47:58 +05:30
Chirag Aggarwal 915a8bfbe0 Merge branch '1.9.x' into feat-rust-sdk 2026-03-26 17:43:47 +05:30
Prem Palanisamy 741267c6c5 fix: rename locale keys to dataExport, use {{type}} placeholder for CSV/JSON 2026-03-26 12:09:10 +00:00
Prem Palanisamy e85080499a fix: generalize export handler for CSV and JSON — download URL, email, dynamic file extension 2026-03-26 11:54:54 +00:00
Prem Palanisamy ee1ca5ace6 fix: remove email verification from vectorsdb export test (tested separately) 2026-03-26 11:36:50 +00:00
Prem Palanisamy b36472f0da add E2E tests for vectorsdb and documentsdb JSON import/export 2026-03-26 11:24:14 +00:00
Prem Palanisamy 52ae8b3880 fix: use type-specific resources for JSON endpoints, add JSON source/destination to worker 2026-03-26 09:23:45 +00:00
Prem Palanisamy 8f09e74462 fix: bump migration to 1.9.*, fix dataExportType property 2026-03-26 07:41:23 +00:00
Prem Palanisamy 30907d716f cleanup: remove duplicate setProject, remove stale spec files 2026-03-26 06:55:28 +00:00
DarshanandPrem Palanisamy eb46855e72 regen: specs. 2026-03-26 06:44:53 +00:00
DarshanandPrem Palanisamy 45557e5929 add: missing doc. 2026-03-26 06:44:36 +00:00
DarshanandPrem Palanisamy 6ad6a5dea3 specs. 2026-03-26 06:44:22 +00:00
DarshanandPrem Palanisamy 098f7aa3e3 update: comment. 2026-03-26 06:43:49 +00:00
DarshanandPrem Palanisamy f8c8c17757 add: tests;
fix: tests.
2026-03-26 06:43:49 +00:00
DarshanandPrem Palanisamy 5b1ee93927 fix: endpoint. 2026-03-26 06:43:04 +00:00
Claude 5fbaa7ab6e fix: revert unintentional changes from rebase conflict resolution
Restore docker-compose.yml, Install.php, VectorsDB.php, and
progress.js to match 1.9.x — these were accidentally modified
during the rebase.

https://claude.ai/code/session_01JLPDurUgyj7qViA8JqQFTH
2026-03-26 05:15:31 +00:00
Jake BarnbyandClaude e8a0a895ed (chore): update lock 2026-03-26 02:48:43 +00:00
Jake BarnbyandClaude 560ebeaddd (fix): installer stale resume redirect and account-setup phase delay 2026-03-26 02:48:39 +00:00
Jake BarnbyandClaude 0731cc1535 (fix): installer step ordering, initial container count, and proc_close timeout 2026-03-26 02:48:39 +00:00
ArnabChatterjee20kandClaude 983d0b8fad fix: remove unused repository entry and update sdk-generator version 2026-03-26 02:48:35 +00:00
ArnabChatterjee20kandClaude 92423acc01 Revert "Revert "Documentsdb + vectordb (latest)"" 2026-03-26 02:48:35 +00:00
Claude 42414a46b0 fix: address review comments for User class pattern
- general.php: add instanceof guard in error handler to prevent calling
  isPrivileged() on a plain Document if getResource('user') returns
  an unexpected type
- graphql.php: add setUser() calls on request/response in graphql group
  init so sensitive field filtering works correctly for GraphQL routes
- api.php: fix session group init type hint from Document to User for
  consistency with all other init blocks

https://claude.ai/code/session_01JLPDurUgyj7qViA8JqQFTH
2026-03-26 02:48:02 +00:00
Claude b7bd86d634 fix: add missing inject('user') to TablesDB child classes
TablesDB classes override __construct() from their parent Database
classes but were missing the ->inject('user') call that the parent
action() methods now require after the static-to-instance migration.

Affected: Rows Get/Update/Delete, Column Increment/Decrement,
and Transactions Operations Create.

https://claude.ai/code/session_01JLPDurUgyj7qViA8JqQFTH
2026-03-26 02:47:57 +00:00
Claude 7a39da0afd fix: remove unused Document imports from Delete.php and Get.php
https://claude.ai/code/session_01JLPDurUgyj7qViA8JqQFTH
2026-03-26 02:47:57 +00:00
Claude 9aa488c961 fix: wrap getDocument('users') results in User instances
The user resource and realtime handlers return Document objects from
getDocument(), but isPrivileged()/isApp() are now instance methods on
the User class. Wrapping results with new User() ensures the correct
type is returned for all code paths.

https://claude.ai/code/session_01JLPDurUgyj7qViA8JqQFTH
2026-03-26 02:47:57 +00:00
Claude cfc325635d fix: convert static isPrivileged() call to instance method in error handler
The error handler in general.php was calling User::isPrivileged()
statically, but the method was converted to an instance method.
This caused a fatal error on every request.

https://claude.ai/code/session_01JLPDurUgyj7qViA8JqQFTH
2026-03-26 02:47:57 +00:00
Claude 5c47d4f48b fix: remove duplicate return and update PHPStan baseline
- Remove duplicate `return false` in User::sessionVerify() (dead code)
- Update PHPStan baseline: change static method refs to instance method
  for isApp/isPrivileged, remove stale unreachable code entry

https://claude.ai/code/session_01JLPDurUgyj7qViA8JqQFTH
2026-03-26 02:47:56 +00:00
Claude 7aff75ae1c refactor: convert User::isApp() and User::isPrivileged() from static to instance methods
All call sites now use $user->isApp() and $user->isPrivileged() instance
syntax instead of static User::isApp() / $user::isPrivileged() calls.
Added setUser() to Request class for consistency with Response.

https://claude.ai/code/session_01JLPDurUgyj7qViA8JqQFTH
2026-03-26 02:47:56 +00:00
Claude 651a24a211 fix: correct import ordering in Tokens XList.php
https://claude.ai/code/session_01JLPDurUgyj7qViA8JqQFTH
2026-03-26 02:47:56 +00:00
Claude 6536463d49 fix: update PHPStan baseline and remove unused Document imports
- Remove getRoles() baseline entry (User type hint resolves it)
- Adjust foreach.nonIterable count from 5 to 4
- Adjust addRole argument.type count from 3 to 2
- Remove unused Document imports from 4 files

https://claude.ai/code/session_01JLPDurUgyj7qViA8JqQFTH
2026-03-26 02:47:56 +00:00
Claude 82d7926c4b fix: use User type hint instead of Document for $user parameter
PHPStan correctly flagged that Document::isPrivileged() doesn't exist.
Changed type hints from Document $user to User $user in all action
signatures where $user::isPrivileged() is called, since the runtime
instance is always a User (or subclass).

https://claude.ai/code/session_01JLPDurUgyj7qViA8JqQFTH
2026-03-26 02:47:38 +00:00
Claude 6041468fc4 fix: correct import ordering in Storage Delete.php
https://claude.ai/code/session_01JLPDurUgyj7qViA8JqQFTH
2026-03-26 02:46:48 +00:00
Claude 669f323156 refactor: use $user:: for isPrivileged() to make privilege checks extensible
Replace all static User::isPrivileged() calls with $user::isPrivileged()
across the codebase. Since $user is resolved via setDocumentType, this
allows subclasses to override the privilege check without CE needing to
know about downstream-specific roles.

https://claude.ai/code/session_01JLPDurUgyj7qViA8JqQFTH
2026-03-26 02:46:48 +00:00
Harsh MahajanandGitHub cbc5259db3 Merge branch '1.9.x' into feat-x-oauth2-provider 2026-03-25 23:29:04 +05:30
Matej Bačo 0d6d6a0a35 Make tests pass 2026-03-25 16:11:01 +01:00
Matej Bačo e8d4065af5 Improve tests human review 2026-03-25 15:54:10 +01:00
Matej Bačo 008110b3f7 Platform API tests 2026-03-25 15:47:11 +01:00
Matej BačoandGitHub 1ab05acc93 Merge pull request #11641 from appwrite/fix-missing-deployments
Fix: missing deployment on new branch without PR
2026-03-25 13:55:50 +01:00
Matej Bačo 2f66ae5533 AI review fixes 2026-03-25 13:28:16 +01:00
Matej Bačo 87c462ebd4 Revert local setup 2026-03-25 13:22:55 +01:00
Matej Bačo f89621cf74 Fix double deployments 2026-03-25 13:17:07 +01:00
Harsh MahajanandGitHub efb0cae4e0 Merge branch '1.9.x' into feat-x-oauth2-provider 2026-03-25 16:49:54 +05:30
Jake Barnby 4fb8d873b1 (docs): Add documentsdb and vectorsdb reference docs 2026-03-25 23:41:49 +13:00
Chirag AggarwalandGitHub c5186324ed Merge pull request #11639 from appwrite/improve-sdk-pr-summary
feat: improve SDK PR summary with platform grouping and clipboard copy
2026-03-25 15:29:11 +05:30
Matej Bačo b95b4f12f9 Fix missing deployment on new branch 2026-03-25 10:55:17 +01:00
Chirag AggarwalandGitHub 52efca0832 Merge pull request #11640 from appwrite/revert-upsert-auth
fix: revert bulk upsertDocuments auth
2026-03-25 15:03:11 +05:30
Chirag Aggarwal ee3d4dfeee fix: revert bulk upsertDocuments auth 2026-03-25 14:49:36 +05:30
Chirag Aggarwal b742f1c50d improve log 2026-03-25 12:49:17 +05:30
Chirag Aggarwal f3fcdec1be remove copy 2026-03-25 12:21:05 +05:30
Chirag Aggarwal 647099efc7 fix: address PR review feedback
- Guard clipboard prompt with posix_isatty() to avoid blocking CI/automated runs
- Add Wayland support (wl-copy) and improve error message for missing clipboard tools
- Validate comma-separated platform names against getPlatforms()
2026-03-25 12:06:19 +05:30
Chirag Aggarwal d535b6b39b feat: improve SDK PR summary with platform grouping and clipboard copy
- Group PR links by platform (Client, Console, Server) in the summary
- Add option to copy PR summary to clipboard in markdown format
- Support comma-separated platform selection (e.g. "client,server")
2026-03-25 11:13:37 +05:30
Chirag AggarwalandGitHub 2057eab918 Merge pull request #11638 from appwrite/update-sdks-script
chore: update sdks script
2026-03-25 10:45:10 +05:30
Chirag Aggarwal f89b3274de review comments 2026-03-25 10:17:25 +05:30
Chirag Aggarwal 956edae593 fix empty git commit 2026-03-25 10:05:16 +05:30
Chirag Aggarwal 617d6fe1eb update logging 2026-03-25 10:01:25 +05:30
Chirag Aggarwal a7cdfed253 chore: update sdks script 2026-03-25 09:49:49 +05:30
Jake Barnby dc6b2ce3aa (feat): auto-detect cli params to force non-interactive installer 2026-03-25 13:52:58 +13:00
Matej Bačo ae99d59aba CodeQL review 2026-03-24 14:52:58 +01:00
Jake BarnbyandGitHub 7fcc640652 Merge pull request #11634 from appwrite/fix-installer-state
Fix installer state
2026-03-24 13:09:48 +00:00
Jake Barnby 10a6e8832b (fix): auto-detect upgrade mode and database from existing config files 2026-03-25 02:07:53 +13:00
Jake Barnby 74adda8e77 (fix): redirect to step 1 when install resume fails instead of blank page 2026-03-25 02:07:47 +13:00
Matej Bačo a06aaaf9ca Remove Db schema changes 2026-03-24 13:55:54 +01:00
Jake BarnbyandGitHub a92ad3aa8b Merge pull request #11624 from appwrite/fix-installer-state 2026-03-24 12:47:48 +00:00
Chirag AggarwalandGitHub 4970d1b6f2 Merge branch '1.9.x' into feat/migrate-di-container 2026-03-24 18:15:45 +05:30
Matej Bačo 094cd180b5 Remove leftover 2026-03-24 13:41:12 +01:00
Matej Bačo a8f43f3486 Update DB schema 2026-03-24 13:36:11 +01:00
Matej Bačo 038f4b5992 More backwards compatibility fixes 2026-03-24 13:33:15 +01:00
Jake Barnby 4de9ec7fba Revert "fix: address review comments on installer state PR"
This reverts commit a659038ad2.
2026-03-25 01:08:14 +13:00
Jake BarnbyandClaude Opus 4.6 a659038ad2 fix: address review comments on installer state PR
- Restore postgresql in compose.phtml allowedDbServices for consistency
  with WhiteList validators, JS defaults, and compose template sections
- Log errors in performReset catch block instead of swallowing silently
- Move $currentStep assignment before waitForApiReady so timeout errors
  are attributed to the correct step
- Replace blocking fgets loop in execWithContainerProgress with
  non-blocking stream_select polling to prevent unbounded hangs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 01:05:47 +13:00
Jake Barnby 9564c9b065 (chore): update lock 2026-03-25 01:04:38 +13:00
Jake Barnby 4da726029c (fix): installer stale resume redirect and account-setup phase delay 2026-03-25 00:56:42 +13:00
Matej Bačo c903fb87ac Fix backwards compatibility 2026-03-24 12:53:25 +01:00
Jake Barnby f016d4b7cd (fix): auto-detect existing database type instead of blocking upgrades 2026-03-25 00:39:17 +13:00
Jake Barnby 2a7925b362 (fix): installer resume detects terminal snapshots and redirects cleanly 2026-03-25 00:39:14 +13:00
Jake Barnby cfe600c653 Merge branch 'fix-installer-state' of github.com:appwrite/appwrite into fix-installer-state
# Conflicts:
#	src/Appwrite/Platform/Tasks/Install.php
2026-03-25 00:27:47 +13:00
Jake Barnby 22e1969895 (fix): installer step ordering, initial container count, and proc_close timeout 2026-03-25 00:02:21 +13:00
Jake BarnbyandGitHub 49688a6c75 Merge pull request #11628 from appwrite/patch-list-db 2026-03-24 11:00:55 +00:00
Jake Barnby d0978d891f (fix): installer step ordering, initial container count, and proc_close timeout 2026-03-24 23:53:56 +13:00
Jake Barnby 0cf206cacf (fix): installer progress counter display and dynamic step messages 2026-03-24 23:53:27 +13:00
Jake Barnby b9b5d396b8 Update console 2026-03-24 23:43:04 +13:00
ArnabChatterjee20k 2b33dc3c72 updated merging of user and current queries 2026-03-24 16:07:31 +05:30
ArnabChatterjee20k 9e595588bc lint 2026-03-24 16:03:28 +05:30
ArnabChatterjee20k 20bd7af370 added a fallback isnulll 2026-03-24 15:59:42 +05:30
Matej Bačo e5841c4cc0 Fix syntax error 2026-03-24 11:29:24 +01:00
Matej Bačo bb7815bd57 Merge branch '1.9.x' into feat-public-platform-api 2026-03-24 11:18:18 +01:00
Jake Barnby 5ca30d37f7 (fix): tolerate console signup restriction in installer account creation 2026-03-24 21:36:32 +13:00
Jake Barnby 76684874e9 (feat): installer improvements — reset, state resilience, container progress, SSL email fallback 2026-03-24 21:25:57 +13:00
Jake Barnby 4fffeda596 (chore): bump console image to 7.8.25 and drop postgresql from allowed databases 2026-03-24 21:25:51 +13:00
Jake Barnby 1acfef5f5d (fix): set installer session cookie domain to match Appwrite convention 2026-03-24 21:25:47 +13:00
Jake BarnbyandGitHub d4f7d51df8 Merge pull request #11621 from appwrite/fix-installer-certificates 2026-03-24 06:24:40 +00:00
Jake Barnby 5b5020fda4 fix: address review — normalize hostname for cert check, add cache bypass, improve fallback copy 2026-03-24 18:18:56 +13:00
Chirag Aggarwal 0c33d981a7 fix analyze 2026-03-24 10:47:41 +05:30
Jake Barnby a1441174f2 fix: update installer module test to expect 7 actions including CertificateGet 2026-03-24 17:57:58 +13:00
Chirag Aggarwal 36d8ad6e7c lock file 2026-03-24 10:21:40 +05:30
Chirag Aggarwal fbce66d500 fix merge conflict 2026-03-24 10:19:34 +05:30
Jake Barnby e58f0b6378 fix: address review — pass HTTPS port to certificate check, use resolved protocol for console button, add hex length guard 2026-03-24 17:46:22 +13:00
Chirag Aggarwal 89db65299d Merge remote-tracking branch 'origin/1.9.x' into feat/migrate-di-container 2026-03-24 10:15:38 +05:30
Jake Barnby c5c78e0f2b Merge remote-tracking branch 'origin/1.9.x' into fix-installer-certificates 2026-03-24 16:55:54 +13:00
Jake BarnbyandGitHub 2e5d7a53eb Merge pull request #11586 from appwrite/revert-11585-revert-11402-sync-mongodb 2026-03-24 03:54:33 +00:00
Jake Barnby 60b5f4433c (feat): add SSL certificate check step to web installer redirect flow 2026-03-24 16:53:43 +13:00
Jake Barnby 56ce0d4b47 Update lock 2026-03-24 16:04:40 +13:00
Jake BarnbyandGitHub 8666bf1325 Merge branch '1.9.x' into revert-11585-revert-11402-sync-mongodb 2026-03-24 03:03:02 +00:00
Chirag Aggarwal ea6a05be4f fix analyze 2026-03-24 07:51:23 +05:30
Chirag Aggarwal 355acf6824 Merge branch '1.9.x' into feat/migrate-di-container 2026-03-23 23:01:30 +05:30
Chirag Aggarwal c2988efa08 chore: use stable 2026-03-23 22:51:49 +05:30
HemachandarandGitHub d53cad2b0f perf: simplify repository authorized checks (#11616)
* perf: simplify repository authorized checks

* search repos
2026-03-23 22:18:35 +05:30
Luke B. SilverandGitHub 9d976fdc9f Merge pull request #11607 from appwrite/fix/failed-open-realtime-publishing
fix: fail open realtime publishing
2026-03-23 17:44:49 +01:00
Matej Bačo 8ccf5094f8 fix multi-model conditions 2026-03-23 16:42:14 +01:00
Matej Bačo 39f2d24907 AI code review fixes 2026-03-23 16:10:12 +01:00
Matej Bačo d3c5a425e7 Implement public platform API 2026-03-23 15:46:11 +01:00
Harsh Mahajan 85703d29e1 addressed greptile suggestions 2026-03-23 19:08:12 +05:30
Harsh MahajanandGitHub d3c11b5c93 Merge branch '1.9.x' into feat-x-oauth2-provider 2026-03-23 18:51:10 +05:30
Chirag Aggarwal ce5d5cf6ef docs: add Rust SDK getting started guide 2026-03-23 17:58:24 +05:30
Chirag Aggarwal 2017183ecc Merge branch '1.9.x' into feat-rust-sdk 2026-03-23 17:56:22 +05:30
Matej BačoandGitHub 995deea801 Merge pull request #11577 from appwrite/feat-public-project-variables-api
Feat: Public project variables API
2026-03-23 13:24:10 +01:00
Chirag Aggarwal 1a1740ac7a feat: add Rust SDK support
Add Rust SDK entry to server platform config, wire up the Rust language
class in the SDK generation task, update sdk-generator to dev-rust branch,
and create the changelog directory.
2026-03-23 17:53:58 +05:30
Harsh MahajanandGitHub 6f177a0a7a Merge branch '1.9.x' into feat-x-oauth2-provider 2026-03-23 17:50:29 +05:30
Harsh Mahajan dc48bb35ef added pkce to base 2026-03-23 17:49:42 +05:30
Matej Bačo 0114e260f0 Fix tests 2026-03-23 12:56:23 +01:00
Matej Bačo 7ab474b963 Fix failing tests 2026-03-23 12:33:08 +01:00
Matej Bačo 10da066075 Merge branch '1.9.x' into feat-public-project-variables-api 2026-03-23 12:21:11 +01:00
Matej Bačo 01142bba2c Merge branch '1.8.x' into feat-public-project-variables-api 2026-03-23 12:21:06 +01:00
Matej BačoandGitHub 71146118d2 Merge pull request #11561 from appwrite/fix-oauth-token-flow-provider-param
Fix: OAuth2 token missing provider name
2026-03-23 12:17:29 +01:00
Matej Bačo 682105c068 Rework without schema changes 2026-03-23 11:52:40 +01:00
Matej Bačo 07ff923d38 Merge branch '1.9.x' into fix-oauth-token-flow-provider-param 2026-03-23 11:41:42 +01:00
Matej Bačo 2c5e029116 Merge branch '1.8.x' into fix-oauth-token-flow-provider-param 2026-03-23 11:41:39 +01:00
ArnabChatterjee20k 336bc3a74f Merge remote-tracking branch 'origin/revert-11585-revert-11402-sync-mongodb' into revert-11585-revert-11402-sync-mongodb 2026-03-23 15:20:22 +05:30
ArnabChatterjee20k 8a80ec12ed removed comments 2026-03-23 15:17:09 +05:30
Harsh Mahajan 8218f36d34 code rabbit comment 2026-03-23 13:32:06 +05:30
Harsh Mahajan 0fe906c538 feat: Add X OAuth 2.0 provider 2026-03-23 13:21:04 +05:30
ArnabChatterjee20kandGitHub b7f06a76aa Merge pull request #11608 from appwrite/fix/vectorsdb-console-improvements
Register missing vectorsdb listDocumentLogs endpoint
2026-03-23 13:07:31 +05:30
Prem Palanisamy a9acba916a fix: format VectorsDB registry import order 2026-03-23 06:40:48 +00:00
ArnabChatterjee20k fa97823385 Merge remote-tracking branch 'origin/revert-11585-revert-11402-sync-mongodb' into revert-11585-revert-11402-sync-mongodb 2026-03-23 10:48:04 +05:30
ArnabChatterjee20k 8ae07ac61f Merge remote-tracking branch 'origin/1.9.x' into revert-11585-revert-11402-sync-mongodb 2026-03-23 10:47:23 +05:30
ArnabChatterjee20kandGitHub ffbe17f34e Merge branch '1.9.x' into revert-11585-revert-11402-sync-mongodb 2026-03-23 10:47:02 +05:30
ArnabChatterjee20k 5466f55cfa removed logs 2026-03-23 10:46:53 +05:30
ArnabChatterjee20k 6c8338c3e9 Revert "added logging"
This reverts commit b630426893.
2026-03-23 10:44:35 +05:30
Chirag Aggarwal 4641596a6d use stable 2026-03-23 10:28:26 +05:30
Chirag Aggarwal d932527561 add null collacing 2026-03-23 10:27:10 +05:30
Chirag Aggarwal 89c072e223 fix analyze 2026-03-23 10:20:45 +05:30
Chirag Aggarwal 6421bc8689 fn name 2026-03-23 10:08:19 +05:30
Chirag Aggarwal d008d9bff0 merge conficts 2026-03-23 10:01:27 +05:30
Chirag Aggarwal f6dc6359bb Merge branch '1.9.x' into feat/migrate-di-container 2026-03-23 10:01:03 +05:30
Prem Palanisamy 28ba11c71e register vectorsdb listDocumentLogs endpoint 2026-03-22 23:02:34 +00:00
ArnabChatterjee20kandPrem Palanisamy a14d51321a refactor: remove debug output and enhance collection creation test with eventual assertion 2026-03-22 23:02:34 +00:00
ArnabChatterjee20kandPrem Palanisamy d8f3d581cc added log vectordb colleciton creation 2026-03-22 23:02:34 +00:00
loks0n be76990bb6 fix: fail open realtime publishing 2026-03-22 18:50:03 +01:00
Steven NguyenandGitHub 6e59e1307d Merge pull request #11602 from appwrite/copilot/update-trivy-in-workflow
Updating trivy in GitHub Actions workflow
2026-03-22 07:12:32 -07:00
Damodar LohaniandGitHub 31410879a7 Merge branch '1.9.x' into feat-audit-user-type-distinction 2026-03-22 08:36:13 +05:45
Damodar Lohani 343e352b17 fix: prevent overwriting user type in audit queue if already set 2026-03-22 02:31:46 +00:00
Damodar LohaniandGitHub b38ea72407 Merge branch '1.8.x' into feat-audit-user-type-distinction 2026-03-22 08:05:29 +05:45
eldadfux 34f23bdc9f Merge branch '1.8.x' into feat-disposable-emails
Made-with: Cursor

# Conflicts:
#	app/controllers/api/projects.php
2026-03-21 19:32:07 +01:00
copilot-swe-agent[bot]andstnguyen90 24848a872c chore: pin trivy-action to safe v0.35.0 SHA to fix compromised 0.20.0 tag
Co-authored-by: stnguyen90 <1477010+stnguyen90@users.noreply.github.com>
Agent-Logs-Url: https://github.com/appwrite/appwrite/sessions/ad20d09a-e80d-4611-9959-2e35c3413736
2026-03-21 16:47:45 +00:00
ArnabChatterjee20k b630426893 added logging 2026-03-20 21:05:24 +05:30
Matej Bačo 0a5a8c5b6c Merge branch '1.8.x' into 1.9.x 2026-03-20 16:28:16 +01:00
Matej BačoandGitHub 875637bf35 Merge pull request #11533 from appwrite/feat-user-impersonation
Add impersonation feature for user management
2026-03-20 16:13:51 +01:00
ArnabChatterjee20k 1aa86708f3 added error loggins to check 2026-03-20 17:59:52 +05:30
Jake BarnbyandGitHub 6410c2dcdf Merge pull request #11592 from appwrite/chore-update 2026-03-20 11:03:13 +00:00
Chirag Aggarwal 032638e896 fix 2026-03-20 15:35:28 +05:30
Jake BarnbyGitHubgreptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
3a40b6b629 Apply suggestion from @greptile-apps[bot]
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-03-20 22:18:56 +13:00
Jake BarnbyandClaude Opus 4.6 ef0954cdda fix: propagate isUpgrade flag from Upgrade to Install for CLI path
Install::action() hardcoded isUpgrade=false, so the CLI upgrade path
never rewrote compose/env files. Added a protected property that
Upgrade sets before calling parent::action().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 21:56:06 +13:00
Chirag Aggarwal 10cc6a8040 fix global pools state 2026-03-20 14:09:43 +05:30
Chirag Aggarwal 9ecdbf5950 func exists 2026-03-20 13:13:07 +05:30
Jake BarnbyandClaude Opus 4.6 d4c9af2eb2 fix: strip surrounding quotes when parsing .env file values
Env::__construct() now strips " and ' wrapping from values so that
_APP_DB_ADAPTER="mariadb" is read as mariadb, not "mariadb".
Without this, the upgrade flow rejected the existing database adapter
because the quoted value didn't match the whitelist.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 20:21:50 +13:00
Chirag Aggarwal 55b436c67b lock file 2026-03-20 12:17:28 +05:30
eldadfux f74a60dba4 Merge origin/1.8.x into feat-disposable-emails
Made-with: Cursor
2026-03-20 07:43:55 +01:00
Chirag Aggarwal 05defcc6e0 Merge branch '1.9.x' into feat/migrate-di-container 2026-03-20 12:12:29 +05:30
ArnabChatterjee20k 3baf2681df fix: remove appwrite-mongo-express service and update database type query filters in XList 2026-03-20 11:59:26 +05:30
Jake BarnbyandClaude Opus 4.6 1ad2cd68ef fix: rewrite compose/env files during upgrade so new image versions are applied
useExistingConfig was preventing the compose template from being
rewritten on non-local upgrades, leaving old image version tags in
place. Also fix Upgrade reading hardcoded .env instead of
getEnvFileName().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 19:12:47 +13:00
ArnabChatterjee20k 04680d68f5 fix: remove unused repository entry and update sdk-generator version 2026-03-20 11:39:40 +05:30
ArnabChatterjee20k 9a70445395 Merge remote-tracking branch 'origin/1.9.x' into revert-11585-revert-11402-sync-mongodb 2026-03-20 11:29:37 +05:30
Jake BarnbyandGitHub dabe98d4a1 Merge pull request #11591 from appwrite/chore-update 2026-03-20 05:54:39 +00:00
Jake Barnby c9d023991d (test): add e2e test for $sequence query type validation per adapter 2026-03-20 18:03:17 +13:00
Jake Barnby 91e252382b (test): add strict type assertions and list coverage for V21 $sequence filter 2026-03-20 17:33:15 +13:00
Jake BarnbyandClaude Opus 4.6 47408f04dc fix: always cast $sequence to string for SDK type safety
Revert the is_int preservation — SDKs declare $sequence as string,
so the API must always return a string. Updated tests to match.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 17:24:21 +13:00
Jake BarnbyandClaude Opus 4.6 827dd2d039 fix: preserve integer type for $sequence and handle nested relationship casting
- Document/Row model filters now preserve int type for $sequence instead of always casting to string
- V21 response filter recursively casts $sequence on nested relationship documents
- Added unit tests for nested document/row $sequence casting

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 16:36:04 +13:00
Jake Barnby ed1ab85356 Remove exclude for CI 2026-03-20 15:41:15 +13:00
Jake Barnby c155c88a31 Fix stan 2026-03-20 15:37:23 +13:00
Jake Barnby df58831d92 Update DB 2026-03-20 14:23:59 +13:00
Jake Barnby aef9f447f7 Update lock 2026-03-20 14:17:40 +13:00
Jake Barnby 48e99e70ac fix: remove 1.8.2 references, upgrade path is 1.8.1 to 1.9.0 2026-03-20 14:09:07 +13:00
Jake Barnby 2007fbf241 Merge remote-tracking branch 'origin/1.8.x' into chore-update
# Conflicts:
#	app/init/constants.php
#	composer.json
#	composer.lock
#	src/Appwrite/Migration/Migration.php
2026-03-20 14:04:46 +13:00
Jake Barnby 2673af7b83 fix: propagate database param in CLI install and fix upgrade conflict detection 2026-03-20 13:26:18 +13:00
Jake Barnby 21d7e2dfb5 chore: update utopia-php/database to fix metadata reconciliation 2026-03-20 13:08:34 +13:00
Jake Barnby 9235a4cc15 test: add V21 request and response filter unit tests 2026-03-20 13:08:27 +13:00
Jake Barnby fb21211ae8 fix: return \$sequence as string and add V21 backward-compat filters 2026-03-20 13:08:19 +13:00
Jake Barnby 616762f696 fix: harden migrate task for MongoDB compat and 1.8.x upgrades 2026-03-20 13:08:12 +13:00
Jake Barnby 3b5ba2a2b0 feat: add V24 migration for 1.9.0 schema changes 2026-03-20 13:08:05 +13:00
Jake Barnby 7ae1cf4db0 chore: bump version to 1.9.0 and update changelog 2026-03-20 13:07:55 +13:00
Steven NguyenandGitHub c537d09b5c Merge pull request #11589 from appwrite/copilot/update-origin-test-cases-tauri
Add tauri:// as a supported origin scheme
2026-03-19 16:06:35 -07:00
eldadfux efeb3b4a00 fixes 2026-03-19 23:02:41 +01:00
eldadfux aa89128d10 Fixes 2026-03-19 22:54:45 +01:00
eldadfux e8c0ab0cc1 Fixed email validator 2026-03-19 22:44:04 +01:00
eldadfux b81f3f8267 Merge origin/1.8.x into feat-user-impersonation 2026-03-19 22:12:38 +01:00
copilot-swe-agent[bot]andstnguyen90 b79132556c feat: add support for tauri://localhost as an allowed origin
Co-authored-by: stnguyen90 <1477010+stnguyen90@users.noreply.github.com>
2026-03-19 20:14:24 +00:00
copilot-swe-agent[bot] 99c76689ee Initial plan 2026-03-19 20:12:17 +00:00
Chirag Aggarwal 6700340ef3 fix realtime 2026-03-19 23:26:49 +05:30
Chirag Aggarwal 4224f6ea5a merge conficts 2026-03-19 22:15:31 +05:30
Chirag Aggarwal 8d925f3670 merge conficts 2026-03-19 21:35:19 +05:30
Chirag Aggarwal d2875c9bf6 Merge branch '1.8.x' into feat/migrate-di-container 2026-03-19 21:35:06 +05:30
Chirag Aggarwal c002f4afe3 fix specs generation 2026-03-19 21:23:00 +05:30
ArnabChatterjee20kandGitHub c7907932e4 Revert "Revert "Documentsdb + vectordb (latest)"" 2026-03-19 20:30:42 +05:30
Matej Bačo bdd3c2f9f5 Fix failing tests 2026-03-19 15:33:36 +01:00
Matej Bačo 1754d6cc81 Fix failing tests 2026-03-19 15:21:22 +01:00
Matej Bačo 351efa6cf2 Fix backwards compatibility 2026-03-19 15:00:20 +01:00
Jake BarnbyandGitHub 1aa2c7ff2b Merge pull request #11585 from appwrite/revert-11402-sync-mongodb 2026-03-19 13:50:22 +00:00
ArnabChatterjee20kandGitHub 9917f95dfd Revert "Documentsdb + vectordb (latest)" 2026-03-19 19:18:27 +05:30
Matej Bačo 8af0efafd4 Merge branch '1.8.x' into feat-public-project-variables-api 2026-03-19 14:17:26 +01:00
Matej BačoandGitHub 7e7cac017c Merge pull request #11582 from appwrite/fix-webhooks-duplication
Fix: webhook endpoints duplication
2026-03-19 14:16:30 +01:00
Jake BarnbyandGitHub b8cb146eb9 Merge pull request #11402 from appwrite/sync-mongodb 2026-03-19 12:30:05 +00:00
Jake BarnbyandGitHub 9ed6ac89f5 Merge pull request #11574 from appwrite/feat-installer 2026-03-19 12:19:15 +00:00
Luke B. SilverandGitHub 09313d0500 Merge pull request #11583 from appwrite/ci/blackmisth-runners
ci: use blacksmith runners for slowest e2e services
2026-03-19 12:08:09 +00:00
Chirag Aggarwal ecb8104340 register resources 2026-03-19 17:37:47 +05:30
Chirag Aggarwal e6090800e2 register resources 2026-03-19 17:35:31 +05:30
Damodar LohaniandGitHub 92b7760e94 Merge branch '1.8.x' into feat-audit-user-type-distinction 2026-03-19 17:44:50 +05:45
Damodar LohaniGitHubgreptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
fe988f4489 Update app/controllers/shared/api.php
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-03-19 17:44:35 +05:45
loks0nandClaude Sonnet 4.6 3a9e4305d3 ci: switch docker image sharing from cache to artifacts
actions/cache uses a runner-local cache backend, so GitHub-hosted
runners and Blacksmith self-hosted runners cannot share the same cache
entry. Switch to actions/upload-artifact@v7 / download-artifact@v7
which use GitHub's artifact storage, accessible from all runner types.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 11:54:50 +00:00
loks0nandClaude Sonnet 4.6 4b163c02ad ci: use blacksmith runners for slowest e2e services
Route the 6 slowest e2e test services (Databases, Sites, Functions,
Avatars, Realtime, TablesDB) to blacksmith-4vcpu-ubuntu-2404 runners
based on timing data from CI. All other services continue using
ubuntu-latest.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 11:44:12 +00:00
ArnabChatterjee20k 331fb7f689 fixed php stan issue 2026-03-19 17:10:45 +05:30
Matej Bačo b80d76e287 Fix failing test 2026-03-19 12:38:35 +01:00
ArnabChatterjee20k feddd77066 fixed analyze 2026-03-19 17:02:27 +05:30
ArnabChatterjee20k a55f3e1db8 updated env 2026-03-19 16:59:18 +05:30
Jake BarnbyandClaude Opus 4.6 9bc263ab22 fix: correct stale progress file cleanup status detection
Steps are keyed by step name (e.g. 'env-vars'), not by status value.
The old lookup used status constants as step keys, so $status was
always null and terminal-state cleanup never triggered. Detect
terminal state by checking for $data['error'] (failure) or all steps
having 'completed' status.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 00:29:03 +13:00
Jake Barnby 6c76783c09 Merge remote-tracking branch 'origin/1.8.x' into feat-installer
# Conflicts:
#	composer.lock
2026-03-20 00:25:32 +13:00
Matej Bačo 835bb49562 Merge branch '1.8.x' into fix-webhooks-duplication 2026-03-19 12:20:37 +01:00
Matej Bačo ab43d4995b Upgrade webhook tests 2026-03-19 12:20:33 +01:00
ArnabChatterjee20k f1e2ce3e09 Merge remote-tracking branch 'origin/1.8.x' into sync-mongodb 2026-03-19 16:42:25 +05:30
ArnabChatterjee20k 6fa34aef04 linting 2026-03-19 16:39:19 +05:30
Jake BarnbyandClaude Opus 4.6 d8748c9054 fix: make Webhook model public and fix Installer autoload path
The Webhook model was marked as non-public but is now exposed via
AuthType::KEY in the new Webhooks module, causing spec generation
to crash when building server SDK specs. The Installer Server.php
had a top-level require_once for vendor/autoload.php that fails
when CE is used as a composer dependency.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 23:50:21 +13:00
Matej Bačo 6f8a54273d AI review fixes 2026-03-19 11:47:02 +01:00
Matej Bačo 37a7c70c2b Fix webhook endpoints duplication 2026-03-19 11:27:13 +01:00
ArnabChatterjee20k 7071e6f080 Merge remote-tracking branch 'origin/1.8.x' into sync-mongodb 2026-03-19 15:55:28 +05:30
Jake BarnbyandGitHub 4c1a360797 Merge pull request #11578 from appwrite/fix/database-shared-table-reconciliation 2026-03-19 10:21:02 +00:00
Jake Barnby f7ea7b165f Update DB 2026-03-19 23:20:44 +13:00
ArnabChatterjee20k 29fcdcd766 update composer dependencies and fix database DSN retrieval in Create.php 2026-03-19 15:48:54 +05:30
Jake Barnby 62f91f746e (fix): clear stale installer lock and progress files on startup 2026-03-19 23:15:20 +13:00
Jake Barnby d27aad6e67 (feat): add enabledDatabases config to hide unsupported databases in installer 2026-03-19 22:10:23 +13:00
Chirag AggarwalandGitHub 0237cced6b Merge branch '1.8.x' into feat/migrate-di-container 2026-03-19 14:10:20 +05:30
Chirag Aggarwal 625ec4ce91 sync changes 2026-03-19 14:09:04 +05:30
Jake BarnbyandClaude Opus 4.6 3d55c27b10 fix: resolve PHPStan analysis errors
- Remove redundant !empty($previewRuleId) check in Deployment trait (always truthy within sites condition)
- Remove stale baseline ignores for $dbForPlatform in cli.php and $database in http.php

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 21:35:14 +13:00
Jake Barnby 30b18bef2b Update deps 2026-03-19 21:08:36 +13:00
ArnabChatterjee20k 221b5d104c updated migration 2026-03-19 13:20:46 +05:30
ArnabChatterjee20k 826ac1abe9 dummy commit to check some tests and edge cases 2026-03-19 12:45:16 +05:30
Chirag Aggarwal 5339592b3f update http 2026-03-19 10:20:47 +05:30
Chirag Aggarwal 9f1d092717 analyze fixes 2026-03-19 10:17:25 +05:30
Chirag Aggarwal e38a4e4347 update queue 2026-03-19 10:08:40 +05:30
Chirag Aggarwal fdbc5b6737 update queue 2026-03-19 10:07:08 +05:30
Chirag Aggarwal fa2b7955d0 update dependencies 2026-03-19 10:04:47 +05:30
Damodar LohaniandClaude Opus 4.6 8b3d3c6f8b feat: distinguish user types in audit logs
Introduce granular audit user types to differentiate between regular
users, console admins, guests, and the various API key scopes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 03:55:22 +00:00
Jake Barnby e1c327235a (feat): add dark mode support to installer 2026-03-19 15:03:58 +13:00
premtsd-codeandGitHub e6bf327b25 Remove worker number setting from CI workflow
Removed the setting for the number of workers in the CI configuration.
2026-03-18 18:29:18 +00:00
Prem Palanisamy 7e0bf72a89 ci: reduce database worker count to 2 for E2E tests 2026-03-18 16:29:46 +00:00
Prem Palanisamy 1d27986fa1 ci: increase database worker count to 4 for E2E tests 2026-03-18 16:28:27 +00:00
Matej Bačo 4bfb958146 fix events 2026-03-18 16:21:26 +01:00
Matej Bačo 33e3e5e63d Fix noop patch call scenario 2026-03-18 16:17:04 +01:00
Matej Bačo 564f56e0f5 Finalize tests 2026-03-18 16:12:47 +01:00
Matej Bačo 1a6a66406b Improve E2E tests for sites&functions 2026-03-18 16:00:03 +01:00
Matej Bačo 84316fdfb5 Add project variable tests 2026-03-18 15:41:43 +01:00
Matej Bačo a6eac0a01e Remvoe breaking change 2026-03-18 15:24:56 +01:00
Matej Bačo 412d858c49 AI comments fixes 2026-03-18 15:23:19 +01:00
premtsd-codeandGitHub 7d7bb0591f Merge branch '1.8.x' into fix/database-shared-table-reconciliation 2026-03-18 13:58:04 +00:00
Prem Palanisamy 9de6ec7279 fix: use database branch with shared-table reconciliation fix 2026-03-18 13:56:05 +00:00
Matej Bačo ba94bff8d4 Public project variables API 2026-03-18 14:48:31 +01:00
Chirag Aggarwal ea11af1e15 chore: worker changes 2026-03-18 17:50:25 +05:30
Chirag AggarwalandGitHub 6cbae88131 Merge branch '1.8.x' into feat/migrate-di-container 2026-03-18 17:14:45 +05:30
Chirag Aggarwal 03b2755aaf fix async execution 2026-03-18 16:43:14 +05:30
ArnabChatterjee20kandGitHub 979cfb0ba8 Merge pull request #11570 from appwrite/fix/vectorsdb-console-improvements
fix: vectorsdb SDK spec and DatabaseType enum
2026-03-18 16:18:16 +05:30
Luke B. SilverandGitHub f36821148f Merge branch '1.8.x' into chore/bump-databases 2026-03-18 09:46:21 +00:00
eldadfux 85fcc52b84 Merge origin/1.8.x into feat-user-impersonation 2026-03-18 10:23:03 +01:00
Chirag AggarwalandGitHub 2095a94bc5 Merge branch '1.8.x' into feat/migrate-di-container 2026-03-18 14:19:59 +05:30
Chirag Aggarwal df7796d5cb restore phpunit 2026-03-18 14:19:43 +05:30
ArnabChatterjee20k 6b851cccdb updated composer 2026-03-18 13:57:18 +05:30
ArnabChatterjee20k 540979f9fb updated migration test 2026-03-18 13:35:42 +05:30
ArnabChatterjee20k 2add5e2d25 fix: update required field comment and rename createTextEmbedding method 2026-03-18 12:38:54 +05:30
ArnabChatterjee20k 8d58383c2e Merge remote-tracking branch 'origin/1.8.x' into sync-mongodb 2026-03-18 11:38:37 +05:30
Chirag Aggarwal d177e3adfc fix pool issue 2026-03-18 11:27:26 +05:30
Jake Barnby a3bb7af634 (chore): improve wait time 2026-03-18 18:44:13 +13:00
Jake Barnby 114016bbca (fix): preserve session cookie across installer redirect 2026-03-18 17:50:47 +13:00
Chirag Aggarwal d04ac5c3e6 fix pool issue 2026-03-18 09:42:18 +05:30
Prem Palanisamy 8ba35d8810 fix: vectorsdb SDK spec and DatabaseType enum
- Fix createTextEmbeddings SDK parameters (texts, model instead of databaseId, collectionId, documents)
- Add vectorsdb to DatabaseType enum in Database response model
2026-03-17 16:12:48 +00:00
Chirag Aggarwal 60939da801 fix graphql 2026-03-17 21:39:50 +05:30
loks0nandClaude Sonnet 4.6 73a01d56a7 chore: bump utopia-php/database to stable 5.3.15 release
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 13:15:02 +00:00
Chirag Aggarwal cdb301a293 fix PHPStan errors without regenerating baseline
- Fix dispatch() type hint to use \Swoole\Http\Server instead of Utopia adapter
- Remove unused $register from go() closure in http.php
- Remove unnecessary ?? '' on non-nullable $hostname
- Remove unsupported override: param from addHeader() call
- Update Resolvers.php for new getResource()/execute() signatures
- Migrate Installer/Server.php from static Http::setResource() to container
- Remove stale baseline entries, add 1 for pre-existing Deployment.php issue
2026-03-17 17:30:42 +05:30
Chirag Aggarwal 14a9aa890f fix issues 2026-03-17 17:18:57 +05:30
Chirag Aggarwal d3833d06d5 Merge 1.8.x (accept theirs) 2026-03-17 17:15:54 +05:30
Chirag Aggarwal 5ba19ec763 fix alias 2026-03-17 17:14:35 +05:30
Chirag Aggarwal 0564936c4f update file name 2026-03-17 16:29:21 +05:30
Chirag Aggarwal c2795376d8 update server 2026-03-17 16:25:52 +05:30
Chirag AggarwalandGitHub 2b898dfe50 Merge branch '1.8.x' into feat/migrate-di-container 2026-03-17 15:22:02 +05:30
Chirag Aggarwal fa1404be52 cleanup 2026-03-17 15:20:29 +05:30
Chirag Aggarwal be87675e1c fix realtime 2026-03-17 15:03:55 +05:30
Chirag Aggarwal b1edf75c4e phpunit 2026-03-17 14:29:04 +05:30
Chirag Aggarwal 8a0f092342 fix: only throw container lookup errors 2026-03-17 14:28:37 +05:30
Chirag Aggarwal 87b02de612 baseline 2026-03-17 11:32:15 +05:30
Chirag Aggarwal f8e9f71de3 move resources 2026-03-17 11:19:17 +05:30
Chirag Aggarwal c055c32371 enable coroutines 2026-03-17 10:52:35 +05:30
Chirag Aggarwal d60858cd8f fix phpstan 2026-03-17 10:37:49 +05:30
Chirag Aggarwal 22bd655ad1 fix autoload 2026-03-17 10:28:08 +05:30
Chirag Aggarwal c900b22dc0 fix connection container and view class 2026-03-17 09:52:55 +05:30
Chirag Aggarwal 27e5ade92b fix login 2026-03-17 09:43:54 +05:30
Chirag Aggarwal d9c1b9db2a chore: register request resources seperately 2026-03-17 08:49:43 +05:30
Chirag Aggarwal eb399c7bd0 wip 2026-03-16 23:45:18 +05:30
Chirag Aggarwal b8e51366d7 add missing compression methods 2026-03-16 23:32:31 +05:30
Chirag Aggarwal b75cd993be missing trusted ip headers 2026-03-16 23:27:34 +05:30
Chirag Aggarwal a1bc503ce4 formatting 2026-03-16 23:23:03 +05:30
Chirag Aggarwal e475a7ac5a lock file 2026-03-16 23:19:07 +05:30
Chirag Aggarwal 2dc24dabfd Merge branch '1.8.x' into feat/migrate-di-container 2026-03-16 23:10:09 +05:30
Chirag Aggarwal aaa2a0525f feat: migrate from static Http::setResource() to DI Container
Upgrade utopia-php/framework from 0.33.x to 0.34.x which removes the
static Http::setResource() API. Resources are now registered on a
Utopia\DI\Container instance.

- Replace 81 Http::setResource() calls in resources.php with $container->set()
- Refactor http.php to use Swoole HttpServer adapter with shared container
- Refactor realtime.php to use FPM adapter with global container
- Refactor cli.php to use direct $cli->setResource() calls
- Update Specs.php to use local container + FPM adapter
- Update Migrate.php to inject console document instead of creating Http instance
- Update GraphQL Schema.php to use instance setResource()
2026-03-16 23:00:36 +05:30
Matej Bačo afd8d8a020 Implement a fix to oauth missing provider 2026-03-16 16:57:35 +01:00
Matej Bačo 90f0282ce3 Implement oauth2 token flow tests 2026-03-16 16:31:08 +01:00
eldadfux 468eff690d Merge origin/1.8.x into feat-user-impersonation
Made-with: Cursor
2026-03-15 19:53:57 +01:00
eldadfux 5e04480cc0 Merge branch '1.8.x' of origin into feat-disposable-emails
Made-with: Cursor
2026-03-15 19:53:04 +01:00
eldadfux ea2b1519d5 Updated deps 2026-03-15 09:26:37 +01:00
eldadfux e5385f7512 Removed old validator 2026-03-15 08:54:11 +01:00
eldadfux e13fe32f78 Update packages 2026-03-15 08:44:06 +01:00
eldadfux d8bf4b9f89 Implement email validation rules for disposable, canonical, and free emails in user account creation and project settings. Update error handling for invalid email types and adjust related configurations in the console and project models. 2026-03-14 09:21:22 +01:00
eldadfux 2d2c55e07c Merge remote-tracking branch 'origin/1.8.x' into feat-disposable-emails
Made-with: Cursor

# Conflicts:
#	app/controllers/api/account.php
#	app/controllers/api/messaging.php
#	app/controllers/api/projects.php
#	app/controllers/api/teams.php
#	app/controllers/api/users.php
#	composer.lock
#	src/Appwrite/GraphQL/Types/Mapper.php
#	src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Email/Create.php
#	src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Email/Update.php
#	src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Email/Create.php
#	src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Email/Update.php
#	src/Appwrite/SDK/Specification/Format/OpenAPI3.php
#	src/Appwrite/SDK/Specification/Format/Swagger2.php
2026-03-13 21:56:58 +01:00
eldadfux f6d38fe1ce Merge remote-tracking branch 'origin/1.8.x' into feat-user-impersonation
Made-with: Cursor

# Conflicts:
#	app/controllers/shared/api.php
2026-03-13 21:48:41 +01:00
eldadfux b85cf2fdb6 applied new logic for logs 2026-03-13 09:18:39 +01:00
eldadfux d8df5f1ea1 Updated comments and docs 2026-03-13 08:21:02 +01:00
eldadfux 10c587cbc2 Removed demo app 2026-03-13 08:08:34 +01:00
eldadfux e409524033 Fixed cors, added a test, fixed scope management 2026-03-13 08:06:07 +01:00
eldadfux 29d9c138c9 Added new tests 2026-03-13 07:25:36 +01:00
eldadfux 8304a8e0e4 Add impersonation feature for user management
- Introduced a new API endpoint to update user impersonator capability.
- Enhanced user model to include impersonator attributes.
- Updated database schema to support impersonation.
- Implemented impersonation logic in the request handling to allow users with impersonator capability to act as other users.
- Added relevant API documentation for impersonation headers.

This feature allows users with the appropriate permissions to impersonate other users, enhancing flexibility in user management.
2026-03-12 19:08:25 +01:00
ArnabChatterjee20k 64dc730a3b refactor: remove debug output and enhance collection creation test with eventual assertion 2026-03-11 16:52:11 +05:30
ArnabChatterjee20k 3096404db5 added log vectordb colleciton creation 2026-03-11 16:23:13 +05:30
ArnabChatterjee20kandGitHub 6818f0b1c3 Merge branch '1.8.x' into sync-mongodb 2026-03-11 15:52:11 +05:30
ArnabChatterjee20k 3ddc073874 updated ollama 2026-03-11 15:51:27 +05:30
ArnabChatterjee20k 4f3468de7b Merge remote-tracking branch 'origin/1.8.x' into sync-mongodb 2026-03-11 14:36:30 +05:30
ArnabChatterjee20k 7a9d7202e5 updated docker file 2026-03-11 14:24:59 +05:30
ArnabChatterjee20k 09bfab03a7 updated ollama 2026-03-11 13:56:48 +05:30
ArnabChatterjee20k 52cd8b7992 updating ollama 2026-03-11 13:47:26 +05:30
ArnabChatterjee20kandGitHub 1cb7ea49ec Merge branch '1.8.x' into sync-mongodb 2026-03-10 18:43:50 +05:30
ArnabChatterjee20k 9989820e17 Fix database type handling and update dependencies in composer files 2026-03-10 18:43:17 +05:30
ArnabChatterjee20k 798c605492 updated postgres 2026-03-10 17:43:34 +05:30
ArnabChatterjee20k ffa4645d25 Refactor database adapter initialization for attribute support
- Moved the `setSupportForAttributes` method call from the adapter initialization to the database resource creation in `getDatabasesDB`.
- Updated the logic to ensure attribute support is set correctly based on the database type.
2026-03-10 17:32:38 +05:30
ArnabChatterjee20k ca0656ce35 Refactor PostgreSQL setup in Docker configuration
- Updated docker-compose.yml to use a pre-built PostgreSQL image instead of a custom build.
- Removed obsolete PostgreSQL Dockerfiles from the project.
- Updated composer.lock with a new content hash.
2026-03-10 16:43:04 +05:30
ArnabChatterjee20k e4178d9ae6 Merge remote-tracking branch 'origin/sync-mongodb' into sync-mongodb 2026-03-10 16:34:10 +05:30
ArnabChatterjee20k 843dfa5c6c Update OLLAMA_KEEP_ALIVE setting in docker-compose.yml to disable keep-alive 2026-03-10 15:53:51 +05:30
ArnabChatterjee20k 52e2276445 Update dependencies and refactor code for clarity
- Changed version constraint for `spomky-labs/otphp` in composer.json to allow any 11.x version.
- Refactored `getDatabasesDBForProject` method in Migrations class to use the shorthand syntax for callable.
- Updated return values in Format class to use more descriptive types for index creation.
- Corrected parameter documentation in Attributes class for clarity and consistency.
2026-03-10 15:49:03 +05:30
ArnabChatterjee20k 9f78fc5341 updated env 2026-03-10 15:41:38 +05:30
ArnabChatterjee20k bf0d49cc76 Refactor embedding timeout and clean database logic
- Updated embedding agent timeout to be configurable via environment variable.
- Removed commented code in XList for clarity.
- Refactored database cleaning logic into separate methods for better readability and maintainability.
2026-03-10 15:41:32 +05:30
ArnabChatterjee20k b5b2338c82 pr comments addressed
* reverted install php
* deleted databasebase from tablesdb
2026-03-10 15:39:10 +05:30
ArnabChatterjee20k cef4d899e3 Add LEGACY constant and update database type references
- Introduced a new constant `LEGACY` in Constants.php.
- Updated `databaseType` in Action classes to use `LEGACY` instead of `TABLESDB`.
- Cleaned up duplicate parameter definition in Create.php for transactionId.
2026-03-10 14:38:14 +05:30
ArnabChatterjee20k f6655343c7 pr comments addressed
* Added new exception `MIGRATION_DATABASE_TYPE_UNSUPPORTED` with proper error metadata and HTTP 400 response.
* Replaced generic CSV database type errors with the new migration-specific exception for clearer error handling.
* Added support for `DOCUMENTSDB` in migration transfer resource service mapping.
* Fixed Appwrite report initialization by correctly injecting `getDatabasesDB`.
* Updated database adapter initialization to conditionally disable attribute support for `DOCUMENTSDB`.
* Moved `setSupportForAttributes` logic from pool initialization to database resource creation.
* Removed duplicate `getDatabasesDB` resource definition and redundant database event listener setup.
* Cleaned up unused variables and minor code inconsistencies.
* Fixed docblock formatting in `TransactionState`.
* Adjusted metrics handling in VectorDB embeddings text creation (removed unnecessary trigger/reset flow).
2026-03-10 14:30:57 +05:30
Jake BarnbyandGitHub 4efababbb4 Merge pull request #11486 from appwrite/vectorsdb 2026-03-10 15:15:21 +13:00
Jake BarnbyandGitHub 5a258b9da1 Merge branch '1.8.x' into sync-mongodb 2026-03-10 15:11:54 +13:00
ArnabChatterjee20k ef248bd5fb fixed usage vectordb model issue on graphql 2026-03-09 17:30:42 +05:30
ArnabChatterjee20k 9c737d906b fixed vectordb db creation issue 2026-03-09 16:40:18 +05:30
ArnabChatterjee20k b8d2f7ece5 updated 2026-03-09 16:10:49 +05:30
ArnabChatterjee20k 5990422a2d updated lock 2026-03-09 15:55:33 +05:30
ArnabChatterjee20k ec500939df updated lock 2026-03-09 15:54:48 +05:30
ArnabChatterjee20k fddaebf254 Merge branch 'sync-mongodb' into vectorsdb 2026-03-09 15:51:30 +05:30
ArnabChatterjee20k d6bc9f120e Merge remote-tracking branch 'origin/1.8.x' into sync-mongodb 2026-03-09 15:50:48 +05:30
ArnabChatterjee20k 1705266779 remove debug logging from database creation process 2026-03-09 15:48:46 +05:30
ArnabChatterjee20k 97d64c3e01 fixing the dsn issue 2026-03-09 15:10:05 +05:30
ArnabChatterjee20k f839cf79d7 added logs for checking dsns on ci 2026-03-09 14:31:23 +05:30
ArnabChatterjee20k 844891c0ae updated 2026-03-09 14:29:39 +05:30
ArnabChatterjee20k c417206e7c updated migration 2026-03-09 14:22:26 +05:30
ArnabChatterjee20k 5326e6571b updated migration 2026-03-09 14:07:29 +05:30
ArnabChatterjee20k 681d930da3 renamed vectordb to vectordb 2026-03-09 13:51:48 +05:30
ArnabChatterjee20k f102419d7c updated format 2026-03-05 14:03:17 +05:30
ArnabChatterjee20k a863ac773a updated index specs 2026-03-05 13:30:54 +05:30
ArnabChatterjee20k 26e92c6a8a Merge remote-tracking branch 'origin/1.8.x' into sync-mongodb 2026-03-05 12:32:19 +05:30
ArnabChatterjee20k 9f5172c1e9 updated migration 2026-03-05 12:30:30 +05:30
ArnabChatterjee20k 519ade1e51 updated realtime to include break 2026-03-05 12:22:45 +05:30
ArnabChatterjee20k 356cd0287c updated index types on vectordb
* merged all indexes together on the vectordb route so that the indexes on the sdk generator doesn't get overriden and fallback to the database adapter to know index is correct or not
2026-03-05 12:21:46 +05:30
ArnabChatterjee20k e84a26be76 Merge remote-tracking branch 'origin/1.8.x' into sync-mongodb 2026-03-05 12:11:44 +05:30
ArnabChatterjee20k d6c8aeee2a Merge remote-tracking branch 'origin/1.8.x' into sync-mongodb 2026-03-02 12:49:01 +05:30
ArnabChatterjee20k 8c3f11b3d5 updated tests 2026-03-02 12:25:40 +05:30
ArnabChatterjee20k e37a9ad059 updated compose 2026-02-27 22:25:08 +05:30
ArnabChatterjee20k 9fc11ce475 updated tests and docker compose 2026-02-27 19:28:24 +05:30
ArnabChatterjee20k 9a32c59340 added vectordb tests 2026-02-27 19:27:24 +05:30
ArnabChatterjee20k c78d19de0d updated 2026-02-27 18:47:46 +05:30
ArnabChatterjee20k 1353f1f56c added status 2026-02-27 18:44:59 +05:30
ArnabChatterjee20k ee34d83cd8 added output of not starting 2026-02-27 18:40:36 +05:30
ArnabChatterjee20k d68449d25b reset 2026-02-27 18:34:18 +05:30
ArnabChatterjee20k d34c83a798 updated tests 2026-02-27 18:25:33 +05:30
ArnabChatterjee20k 6e485bcd5a updated tests file to see the logs of appwrite 2026-02-27 18:12:00 +05:30
ArnabChatterjee20k bab3a49b0b updated 2026-02-27 18:01:06 +05:30
ArnabChatterjee20k 55796eca5c updated lock 2026-02-27 17:21:03 +05:30
ArnabChatterjee20k abf497d509 Merge remote-tracking branch 'origin/1.8.x' into sync-mongodb 2026-02-27 17:19:31 +05:30
ArnabChatterjee20k a76a5cda38 Refactor code structure for improved readability and maintainability 2026-02-27 17:15:00 +05:30
ArnabChatterjee20k 377a9d915e updated env 2026-02-27 13:09:56 +05:30
ArnabChatterjee20k c7b3972fa0 fixed event 2026-02-26 17:12:44 +05:30
ArnabChatterjee20k fda53c0520 updated composer 2026-02-26 17:07:24 +05:30
ArnabChatterjee20k f2e789537c updated lock 2026-02-26 16:34:42 +05:30
ArnabChatterjee20k 3287ef6a57 Merge remote-tracking branch 'origin/1.8.x' into sync-mongodb 2026-02-26 16:33:56 +05:30
ArnabChatterjee20k 74b3d8f493 updated lock 2026-02-26 16:22:59 +05:30
ArnabChatterjee20k e0972f4636 updated lock 2026-02-25 16:50:13 +05:30
ArnabChatterjee20k 42f914f6aa fixed merge conflicts 2026-02-25 16:49:31 +05:30
ArnabChatterjee20k 58f4fff864 added 2026-02-25 16:20:44 +05:30
ArnabChatterjee20k 681c9743b1 added controllers 2026-02-25 16:09:28 +05:30
ArnabChatterjee20k cd2d2a1238 added configs 2026-02-25 16:09:16 +05:30
ArnabChatterjee20k 905763e755 added tests 2026-02-25 16:08:40 +05:30
ArnabChatterjee20k 2550482432 updated docs 2026-02-25 16:07:19 +05:30
ArnabChatterjee20k 8d028066ae update transaction not to start if there is no operation in the logs 2026-02-25 15:44:14 +05:30
ArnabChatterjee20k bd4adad545 refractored transaction update 2026-02-25 12:49:27 +05:30
ArnabChatterjee20k c40f9b1de0 addressed comments 2026-02-24 18:23:52 +05:30
ArnabChatterjee20k 8237250453 refactor: streamline database DSN handling and remove deprecated attributes 2026-02-24 17:40:50 +05:30
ArnabChatterjee20k c8a7ed2022 updated migration to remove the dependency on dsn at project level attribute 2026-02-24 10:43:10 +05:30
ArnabChatterjee20k 4847c5a308 changed deps so that create document depends on test create collection 2026-02-23 13:27:01 +05:30
ArnabChatterjee20k f6985bae41 updated lock 2026-02-23 13:15:52 +05:30
ArnabChatterjee20k cc9df4f5c5 Merge remote-tracking branch 'origin/1.8.x' into documents-db-api 2026-02-23 13:09:49 +05:30
ArnabChatterjee20k 83b7b4e599 updated 2026-02-23 13:05:12 +05:30
ArnabChatterjee20k fdc573009c updated tests 2026-02-20 16:53:46 +05:30
ArnabChatterjee20k 51d769b0a1 Merge remote-tracking branch 'origin/1.8.x' into documents-db-api 2026-02-20 13:58:56 +05:30
ArnabChatterjee20k f6b04d05b2 updated composer lock 2026-02-18 16:10:13 +05:30
ArnabChatterjee20k 2926412604 Merge remote-tracking branch 'origin/1.8.x' into documents-db-api 2026-02-18 16:07:44 +05:30
ArnabChatterjee20k ee21b1feda updated composer 2026-02-18 16:06:31 +05:30
ArnabChatterjee20k 2d0140f705 Merge remote-tracking branch 'origin/1.8.x' into documents-db-api 2026-02-18 16:01:55 +05:30
ArnabChatterjee20k 183bd4143c updated tests 2026-02-16 18:36:42 +05:30
ArnabChatterjee20k 57ac67a106 linting and updated timeouts 2026-02-16 16:23:38 +05:30
ArnabChatterjee20k 4b6d809a3b updated docker compose 2026-02-16 16:09:15 +05:30
ArnabChatterjee20k 0221556ae7 Merge remote-tracking branch 'origin/documents-db-api' into documents-db-api 2026-02-16 15:16:51 +05:30
ArnabChatterjee20k 6a1955d20d updated tests 2026-02-16 15:16:04 +05:30
ArnabChatterjee20kandGitHub 8c7d2c58f5 Merge branch '1.8.x' into documents-db-api 2026-02-16 14:30:53 +05:30
ArnabChatterjee20k 5b7746b464 empty 2026-02-16 14:12:46 +05:30
ArnabChatterjee20k e6e0cef91e linting 2026-02-16 13:55:34 +05:30
ArnabChatterjee20k bdc4c4a18d Merge remote-tracking branch 'origin/documents-db-api' into documents-db-api 2026-02-16 13:53:21 +05:30
ArnabChatterjee20k 49efa02e09 synced documents db and vectordb with 1.8.x 2026-02-16 13:52:16 +05:30
ArnabChatterjee20k fd96bcbd3f Merge remote-tracking branch 'origin/1.8.x' into documents-db-api 2026-02-16 12:20:53 +05:30
Jake BarnbyandGitHub 9647c3fa7d Merge pull request #11310 from appwrite/database-type-filters 2026-02-12 09:38:45 +00:00
Darshan 1e411bbef5 update: lint. 2026-02-12 13:25:44 +05:30
Darshan 7c47a4db72 add: filtering. 2026-02-12 13:22:07 +05:30
ArnabChatterjee20k ae33104ecf added admin auth to the index creation 2026-02-11 18:19:46 +05:30
ArnabChatterjee20k b90a9e8cab added admin auth to the index creation 2026-02-11 16:49:33 +05:30
ArnabChatterjee20k 4024cf0492 Merge remote-tracking branch 'origin/1.8.x' into documents-db-api 2026-02-11 16:22:37 +05:30
ArnabChatterjee20k e07f9a7b49 added admin auth to vector db 2026-02-11 14:04:14 +05:30
ArnabChatterjee20k 61f778196b Merge remote-tracking branch 'origin/1.8.x' into documents-db-api 2026-02-11 13:58:43 +05:30
ArnabChatterjee20k 93897f86d0 Merge remote-tracking branch 'origin/1.8.x' into documents-db-api 2026-02-10 15:34:51 +05:30
ArnabChatterjee20k 095e08441f added get databases db in the document logs 2026-02-10 13:33:29 +05:30
ArnabChatterjee20k da63463a3c updated migration 2026-02-10 11:20:53 +05:30
ArnabChatterjee20k 8e140bae6c updated project and migraiton worker 2026-02-09 17:43:42 +05:30
ArnabChatterjee20k 3b2eb02f8a Merge branch '1.8.x' into documents-db-api 2026-02-09 17:27:09 +05:30
ArnabChatterjee20k e53f5b4023 add admin permission to create 2026-01-28 19:06:19 +05:30
ArnabChatterjee20k 02cddb5c08 updated the format specs 2026-01-28 10:55:19 +05:30
ArnabChatterjee20k cb4d11a1d4 updated 2026-01-27 18:13:27 +05:30
ArnabChatterjee20k 139535e83c updated 2026-01-27 17:18:24 +05:30
ArnabChatterjee20k b9e95f6d82 updated 2026-01-27 17:17:23 +05:30
ArnabChatterjee20k 79cb668a50 updated composer lock 2026-01-27 16:34:34 +05:30
ArnabChatterjee20k 8078516e2f fixed composer lock 2026-01-27 16:32:59 +05:30
ArnabChatterjee20k 1729bd23b2 Merge remote-tracking branch 'origin/1.8.x' into documents-db-api 2026-01-27 16:29:25 +05:30
ArnabChatterjee20k ac7fc7ae69 linting 2026-01-22 12:47:26 +05:30
ArnabChatterjee20k 4b570dc3f0 updetad pools 2026-01-22 12:41:18 +05:30
ArnabChatterjee20k aa3d66df7b Merge branch 'new-pool-adapter' into documents-db-api 2026-01-22 12:34:22 +05:30
ArnabChatterjee20k a16e05ed4d updated timeout 2026-01-21 13:36:35 +05:30
ArnabChatterjee20k 84dc5682d5 updated auth param 2026-01-21 12:31:42 +05:30
ArnabChatterjee20k a112de41c5 updated ollama timeout 2026-01-21 12:23:29 +05:30
ArnabChatterjee20k 02bba85fe7 updated migration tests for adding endpoint 2026-01-21 12:21:37 +05:30
ArnabChatterjee20k 56f68d33df updated migration 2026-01-21 11:44:13 +05:30
ArnabChatterjee20k d4edf9ed13 updated vectordb test 2026-01-20 20:10:57 +05:30
ArnabChatterjee20k 12b38e2a27 commenting mongodb out for now to check the issues on the ci 2026-01-20 19:45:19 +05:30
ArnabChatterjee20k 9f1807a128 linting and fixing resources timeout and validator 2026-01-20 19:41:52 +05:30
ArnabChatterjee20k 6bdfcc3fb5 fixed endpoint in migration 2026-01-20 18:44:54 +05:30
ArnabChatterjee20k d8dd1918c4 synced vectordb 2026-01-20 15:45:12 +05:30
ArnabChatterjee20k 376c5e47f5 added missing injections 2026-01-20 14:01:37 +05:30
ArnabChatterjee20k edfb9e0bd7 updated resources to use the listeners based on the databasetype 2026-01-20 13:56:52 +05:30
ArnabChatterjee20k eab4c7f28a Merge remote-tracking branch 'origin/1.8.x' into documents-db-api 2026-01-20 13:56:32 +05:30
ArnabChatterjee20k c361340252 linting 2026-01-19 18:45:05 +05:30
ArnabChatterjee20k 41e197003e fixed merge conflicts 2026-01-19 18:43:30 +05:30
ArnabChatterjee20k c7ba37ec49 fixed merge conflicts 2026-01-19 14:35:31 +05:30
ArnabChatterjee20k ba82aab563 Merge remote-tracking branch 'origin/1.8.x' into documents-db-api 2026-01-19 13:22:53 +05:30
ArnabChatterjee20k f580dc9145 Merge remote-tracking branch 'upstream/1.8.x' into documents-db-api 2026-01-13 14:32:46 +05:30
Jake BarnbyandGitHub 421dca2f99 Merge pull request #10653 from appwrite/vector-db-api
vectordb api endpoints
2025-12-10 10:59:58 +00:00
ArnabChatterjee20k 991b2b1c3b updated delete worker for vectordb and updated tests 2025-12-09 18:44:26 +05:30
ArnabChatterjee20k d22743a1cf Merge remote-tracking branch 'upstream/documents-db-api' into vector-db-api 2025-12-09 18:37:09 +05:30
ArnabChatterjee20k a2039c0b76 added deletion test for multidb 2025-12-09 18:34:25 +05:30
ArnabChatterjee20k c549ddd149 updated project deletion worker to delete per database 2025-12-09 18:27:10 +05:30
ArnabChatterjee20k f3721f712a fixed missing project usage fields 2025-12-09 15:38:43 +05:30
ArnabChatterjee20k 480757dcfe added usage test for the vectordb 2025-12-09 12:07:26 +05:30
ArnabChatterjee20k fbaeff8920 Merge remote-tracking branch 'upstream/documents-db-api' into vector-db-api 2025-12-08 20:31:07 +05:30
ArnabChatterjee20k de88ec4065 updated missing skip metrics and tablesdb tests 2025-12-08 20:30:37 +05:30
ArnabChatterjee20k f149ba33d4 updated stats for vectordb 2025-12-08 20:27:30 +05:30
ArnabChatterjee20k b15f2eed8f Merge remote-tracking branch 'upstream/documents-db-api' into vector-db-api 2025-12-08 19:32:21 +05:30
ArnabChatterjee20k ec63dc22d4 updated controllers 2025-12-08 19:09:54 +05:30
ArnabChatterjee20k 7c7b98399c linting 2025-12-08 17:51:01 +05:30
ArnabChatterjee20k 9d679e726e updated composer lock 2025-12-08 17:46:00 +05:30
ArnabChatterjee20k 070ec03c0c Merge remote-tracking branch 'upstream/1.8.x' into documents-db-api 2025-12-08 17:31:04 +05:30
ArnabChatterjee20k be89224c36 updated console client test 2025-12-05 19:14:29 +05:30
ArnabChatterjee20k 8edd3c87cb added documentsdb total reads and writes in the project response 2025-12-05 19:09:35 +05:30
ArnabChatterjee20k 9ac3cd73a8 Refactor database metrics handling for DocumentsDB
- Introduced new methods to retrieve metrics for write and read operations based on database type.
- Updated bulk delete, update, upsert, and create actions to use the new metric methods.
- Enhanced document retrieval and update actions to utilize the new metric structure.
- Added support for DocumentsDB metrics in usage statistics and response models.
- Created new response models for DocumentsDB usage and updated existing models to accommodate new metrics.
- Adjusted migration and stats resources to include DocumentsDB metrics.
- Updated tests to validate the new metrics and ensure correct functionality for DocumentsDB usage.
2025-12-05 18:58:13 +05:30
ArnabChatterjee20k 203170c36d updated database and collection stats 2025-12-04 15:57:18 +05:30
ArnabChatterjee20k 6f5bfdb726 updated stats 2025-12-03 20:18:53 +05:30
b499857871 Update src/Appwrite/Utopia/Database/Validator/Queries/Base.php
Co-authored-by: Jake Barnby <jakeb994@gmail.com>
2025-12-03 19:12:56 +05:30
1388b0ca21 Update app/config/variables.php
Co-authored-by: Jake Barnby <jakeb994@gmail.com>
2025-12-03 19:12:38 +05:30
ArnabChatterjee20k af095bdf4a updated stats 2025-12-03 19:11:46 +05:30
ArnabChatterjee20k 7af361fa16 * added logger in the create text embedding
* aadded error text metric
2025-12-03 13:38:24 +05:30
ArnabChatterjee20k 069f231b3a updated composer lock 2025-12-02 18:40:28 +05:30
ArnabChatterjee20k def028e970 Merge remote-tracking branch 'upstream/documents-db-api' into vector-db-api 2025-12-02 18:38:21 +05:30
ArnabChatterjee20k 16f49d13c8 updated composer lock 2025-12-02 18:36:29 +05:30
ArnabChatterjee20k e72d7979e0 updated composer lock 2025-12-02 18:31:44 +05:30
ArnabChatterjee20k ca4c7b5361 added stats usage for text embeddings 2025-12-02 18:13:49 +05:30
ArnabChatterjee20k 7ad6bb048c merged 1.8.x 2025-12-02 16:35:40 +05:30
ArnabChatterjee20k d2c9ac079a Refactor VectorDB Embeddings API and Update Dimension Handling
- Updated the Create.php file to streamline the embedding creation process by removing unnecessary authentication checks and simplifying the parameter structure.
- Changed the parameter from 'documents' to 'texts' for clarity and adjusted the embedding model handling.
- Modified the response model to include error handling for embedding generation.
- Updated the Embedding response model to reflect changes in dimension naming and added error message handling.
- Refactored VectorDBCollection model to replace 'dimensions' with 'dimension' for consistency across the codebase.
- Adjusted all relevant tests to accommodate the new parameter names and response structures, ensuring comprehensive coverage for the updated functionality.
2025-12-02 14:20:25 +05:30
ArnabChatterjee20k abe1257bd5 Merge remote-tracking branch 'upstream/documents-db-api' into vector-db-api 2025-11-21 20:17:59 +05:30
ArnabChatterjee20k 3db22fd2db fixed enum route 2025-11-21 20:03:44 +05:30
ArnabChatterjee20k a423067b36 Merge remote-tracking branch 'upstream/1.8.x' into documents-db-api 2025-11-21 19:53:52 +05:30
ArnabChatterjee20k 33aab0357f updated tests 2025-11-21 19:52:28 +05:30
ArnabChatterjee20k 34e9c69a62 updated tests condition 2025-11-21 19:11:26 +05:30
ArnabChatterjee20k fdf5ca0067 updated tests condition 2025-11-21 19:08:35 +05:30
ArnabChatterjee20k 6eb17b847f updated composer 2025-11-21 18:06:32 +05:30
ArnabChatterjee20k 918ebb8ecc Add VectorDB text embedding creation endpoint and update related tests 2025-11-21 18:00:51 +05:30
ArnabChatterjee20k c0ac52810e Merge remote-tracking branch 'upstream/documents-db-api' into vector-db-api 2025-11-21 17:25:18 +05:30
ArnabChatterjee20k b658cddc71 updated get project dsn to be of the project db type(dedicate, shared) so that databasesdb and projectdb are not in conflict 2025-11-21 17:22:19 +05:30
ArnabChatterjee20k fe753db78c Merge remote-tracking branch 'upstream/documents-db-api' into vector-db-api 2025-11-20 18:57:38 +05:30
ArnabChatterjee20k 2f1e47e35a updated migrations 2025-11-20 18:56:14 +05:30
ArnabChatterjee20k c3ad337398 updated migrations for vectordb importing/exporting in csv endpoints 2025-11-20 18:54:41 +05:30
ArnabChatterjee20k cc759868b8 Merge remote-tracking branch 'upstream/documents-db-api' into vector-db-api 2025-11-20 15:51:01 +05:30
ArnabChatterjee20k 1004435e9e skipped operator tests for mongodb 2025-11-20 14:07:05 +05:30
ArnabChatterjee20k a9e749a81f updated migration service extractor for csv import/export 2025-11-20 14:06:42 +05:30
ArnabChatterjee20k 553fe15ba9 resolved merge conflicts 2025-11-19 20:01:02 +05:30
ArnabChatterjee20k fec758d237 Merge remote-tracking branch 'upstream/1.8.x' into documents-db-api 2025-11-19 20:00:17 +05:30
ArnabChatterjee20k 70a1d5d0f7 updated composer lock 2025-11-14 21:33:17 +05:30
ArnabChatterjee20k c66308e4e9 Merge remote-tracking branch 'upstream/documents-db-api' into vector-db-api 2025-11-14 21:28:40 +05:30
ArnabChatterjee20k 21aae468f2 updated database type function 2025-11-14 19:35:53 +05:30
ArnabChatterjee20k f007b83ccc updated composer 2025-11-14 19:18:03 +05:30
ArnabChatterjee20k 28be12d827 Merge remote-tracking branch 'upstream/1.8.x' into documents-db-api 2025-11-14 19:16:08 +05:30
ArnabChatterjee20k 93bd6b39a4 updated migrations + index finding issue in vectordb 2025-11-13 16:47:00 +05:30
ArnabChatterjee20k 75045ffd58 added constants for the vector dimension 2025-11-12 16:28:58 +05:30
ArnabChatterjee20k ed8419986a linting 2025-11-12 16:23:59 +05:30
ArnabChatterjee20k 954011b2c4 added transactions for vectordb 2025-11-12 16:21:27 +05:30
ArnabChatterjee20k c3f221f1fb added timestamp mutation test 2025-11-07 13:13:13 +05:30
ArnabChatterjee20k de59f53de3 reverted error tracing 2025-11-06 18:25:01 +05:30
ArnabChatterjee20k 04b169fab8 updated composer packages 2025-11-06 18:01:01 +05:30
ArnabChatterjee20k 857ec3b099 reverted docker compose 2025-11-06 17:54:21 +05:30
ArnabChatterjee20k b1dd377b67 Merge branch 'documents-db-api' into vector-db-api 2025-11-06 17:52:14 +05:30
ArnabChatterjee20k 81128e8446 reverted changes 2025-11-06 17:51:33 +05:30
ArnabChatterjee20k 86371e2082 Merge branch 'documents-db-api' into vector-db-api 2025-11-06 17:50:14 +05:30
ArnabChatterjee20k bd6f62dd68 updated realtime, create endpoint , resources, registers 2025-11-06 17:48:39 +05:30
ArnabChatterjee20k d2275c05fb updated tests 2025-11-05 20:34:12 +05:30
ArnabChatterjee20k 4988881568 updated method names and sdk namespaces for vectordb 2025-11-03 11:17:57 +05:30
ArnabChatterjee20k f9be1ddfc5 added vector embedding creation + update endpoint 2025-10-31 19:07:34 +05:30
ArnabChatterjee20k 60e95458a7 vector db endpoints
* configs and default attributes + indexes
* separate collection creation setup
* fix -> database metadata fix on collection creation
2025-10-31 18:01:47 +05:30
ArnabChatterjee20k 2bb6b0a2a9 updated database dsn for create in the create database 2025-10-31 16:45:01 +05:30
ArnabChatterjee20k 3311bd27cb Merge branch 'documents-db-api' into vector-db-api 2025-10-31 16:35:00 +05:30
ArnabChatterjee20k 5d9ef0dcca added metadata create on database 2025-10-31 16:33:54 +05:30
ArnabChatterjee20k 7ddecdd9ef Merge branch 'documents-db-api' into vector-db-api 2025-10-31 10:45:58 +05:30
ArnabChatterjee20k c8f966b0e5 added the default database type for the transactions context 2025-10-30 13:44:26 +05:30
ArnabChatterjee20k 3d2f21be7c updated composer lock 2025-10-30 13:02:49 +05:30
ArnabChatterjee20k 3e876f5bd8 fixed merge conflicts 2025-10-30 12:57:41 +05:30
ArnabChatterjee20k a5e56438c7 Merge remote-tracking branch 'upstream/1.8.x' into documents-db-api 2025-10-30 12:35:57 +05:30
ArnabChatterjee20k 4e79259c4e Refactor database handling to streamline DSN retrieval for documents and tables databases 2025-10-30 12:27:52 +05:30
ArnabChatterjee20k fae79e5c3f updated tests from feat-mongodb branch 2025-10-29 13:49:18 +05:30
ArnabChatterjee20k b169be9dda updated migration merge conflicts 2025-10-29 11:41:46 +05:30
ArnabChatterjee20k 8f4496346d Merge remote-tracking branch 'upstream/1.8.x' into documents-db-api 2025-10-28 18:41:03 +05:30
ArnabChatterjee20k f306c03030 updated tests and index creation 2025-10-28 18:32:17 +05:30
ArnabChatterjee20k a8469a9b92 updated composer lock 2025-10-27 18:10:41 +05:30
ArnabChatterjee20k b63510acb5 Merge remote-tracking branch 'upstream/1.8.x' into documents-db-api 2025-10-27 18:10:08 +05:30
ArnabChatterjee20k 3e612ec88d flask test fixing via retry 2025-10-27 18:08:44 +05:30
ArnabChatterjee20k 1be36ae552 updated deps 2025-10-27 16:19:04 +05:30
ArnabChatterjee20k 445b657207 Merge remote-tracking branch 'upstream/1.8.x' into documents-db-api 2025-10-27 16:15:10 +05:30
Eldad Fux da6dff2d17 Merge branch 'feat-disposable-emails' of github.com:appwrite/appwrite into feat-disposable-emails 2025-10-19 22:22:13 +01:00
Eldad Fux b4aab6134e Merge remote-tracking branch 'origin/1.8.x' into feat-disposable-emails 2025-10-19 22:20:31 +01:00
Eldad Fux 1c465a8cda refactor: standardize email validation across API controllers
- Reintroduced the Utopia Emails validator in multiple API controllers to ensure consistent email validation practices.
- Removed deprecated email validation references, streamlining the codebase and aligning with recent updates to the Utopia Emails library.
2025-10-19 22:16:03 +01:00
Eldad A. FuxandGitHub 8ab6c2d83b Merge branch '1.8.x' into feat-disposable-emails 2025-10-19 22:13:31 +01:00
Eldad Fux c89d075200 refactor: streamline disposable email validation in account API
- Replaced the previous disposable email validation logic with the EmailNotDisposable validator from the Utopia Emails library for consistency and improved maintainability.
- Removed unnecessary configuration loading related to disposable emails, aligning with recent updates to the email validation process.
2025-10-19 22:09:26 +01:00
Eldad Fux 6c6c71484d refactor: replace disposable email validation logic with Utopia Emails library
- Introduced the EmailNotDisposable validator from the Utopia Emails library for improved disposable email validation in the account API.
- Removed the deprecated disposable emails configuration loading from the config files to streamline the codebase.
2025-10-19 22:01:27 +01:00
Eldad Fux 18a66bae4e chore: update Utopia Emails dependency to version 0.5 and remove deprecated email validator
- Updated the Utopia Emails dependency version from 0.4.* to 0.5.* in composer.json and composer.lock.
- Removed the custom Email validator class and its associated tests as it is no longer needed with the new Utopia Emails library.
2025-10-19 21:55:52 +01:00
Eldad Fux 68b72f3162 feat: integrate Utopia Emails library for email validation
- Added Utopia Emails library as a dependency in composer.json.
- Updated email validation references across multiple API controllers to use the new Utopia Emails validator.
- Removed the deprecated disposable emails configuration file.
- Updated composer.lock to reflect the new library and version changes for existing dependencies.
2025-10-19 21:44:22 +01:00
ArnabChatterjee20k 5098973e0d reverted longtext resource file 2025-10-17 18:14:16 +05:30
ArnabChatterjee20k d0976ff429 updated registers config 2025-10-17 14:16:45 +05:30
ArnabChatterjee20k 157783f782 merged feat-mongodb 2025-10-17 13:58:27 +05:30
ArnabChatterjee20k e3a70373e1 updated docuemntsdb transaction path names 2025-10-16 22:19:53 +05:30
ArnabChatterjee20k a6e5563f8e reverted docker compose 2025-10-16 21:51:56 +05:30
ArnabChatterjee20k 470d84a843 * updated transactions
* updated tests
2025-10-16 21:45:30 +05:30
ArnabChatterjee20k 80037f7e8d updated http php for mongodb setup 2025-10-16 15:57:13 +05:30
ArnabChatterjee20k cc848c90a7 updated lock file 2025-10-16 14:25:22 +05:30
ArnabChatterjee20k e0c293c5b4 Merge remote-tracking branch 'upstream/1.8.x' into documents-db-api 2025-10-16 14:25:14 +05:30
ArnabChatterjee20k 2d82013261 updated docker compose setup for mongodb which was causing issue for mongo transactions 2025-10-16 14:22:28 +05:30
ArnabChatterjee20k 5a6561f158 vectordb (in progress)
* pools , env setup
* collections, db endpoints
2025-10-16 12:53:39 +05:30
ArnabChatterjee20k f28588ecb6 updated migration 2025-10-15 14:24:59 +05:30
ArnabChatterjee20k 6579ea5b56 Merge remote-tracking branch 'upstream/1.8.x' into documents-db-api 2025-10-15 14:16:49 +05:30
ArnabChatterjee20k 9373688be2 updated migration worker to pass the dsn resolver for the destination database 2025-10-15 10:33:46 +05:30
Eldad A. FuxandGitHub f6438065e3 Merge branch '1.8.x' into feat-disposable-emails 2025-10-15 00:14:06 +01:00
ArnabChatterjee20k 2cec06be18 Merge remote-tracking branch 'upstream/1.8.x' into documents-db-api 2025-10-14 21:18:38 +05:30
ArnabChatterjee20k 3a60bb306c * updated migration tests
* database dsn fetching
* removed dsn from response models
2025-10-14 21:13:26 +05:30
ArnabChatterjee20k e2ddb0d6d0 updated migration 2025-10-14 13:23:48 +05:30
Darshan 0049265c8a Merge remote-tracking branch 'origin/documents-db-api' into documents-db-api 2025-10-14 12:49:41 +05:30
Darshan 1f27867565 update: method name. 2025-10-14 12:49:34 +05:30
ArnabChatterjee20k faf0cdec35 changed default fallbackForDB to mariadb 2025-10-14 12:46:01 +05:30
4c6961f7d6 Update app/controllers/api/users.php
Co-authored-by: Jake Barnby <jakeb994@gmail.com>
2025-10-14 07:58:30 +01:00
0bd27c4182 Update app/controllers/api/users.php
Co-authored-by: Jake Barnby <jakeb994@gmail.com>
2025-10-14 07:58:24 +01:00
Eldad Fux ddcfac4f0f feat: enhance account handling with plan support for disposable email validation
- Added 'plan' injection to account-related API endpoints.
- Updated logic to check for disposable email validation support based on the plan.
- Improved handling of disposable email checks during account creation and email updates.
2025-10-14 00:03:11 +01:00
Eldad Fux 3e58014dbd fixes 2025-10-13 23:48:50 +01:00
Eldad Fux ff06920e24 feat: add support for blocking disposable email addresses
- Introduced a new exception for disposable email addresses.
- Updated user account creation and email handling to check against a list of disposable email domains.
- Added a new API endpoint to enable or disable disposable email checks for projects.
- Updated project model to include disposable email settings.
- Configured loading of disposable email domains from a separate configuration file.
2025-10-13 23:43:54 +01:00
ArnabChatterjee20k 6073eab496 * updated env to have mongodb support for the tablesdb/legacy
* setAttributeSupport to support context schema and schemaless in mongodb
* updated the connection registers
2025-10-13 15:47:40 +05:30
ArnabChatterjee20k 50ddc611a4 updated migrations 2025-10-12 20:49:02 +05:30
ArnabChatterjee20k 0ef2cfa753 reverted docker compose 2025-10-12 20:26:39 +05:30
ArnabChatterjee20k 790e503ad9 updated composer lock 2025-10-12 20:19:57 +05:30
ArnabChatterjee20k f6dbc0c882 Merge remote-tracking branch 'upstream/feat-mongodb' into documents-db-api 2025-10-12 20:17:53 +05:30
ArnabChatterjee20k a6c226aac9 removed the documentsdb changes from the migration tests 2025-10-10 20:38:54 +05:30
ArnabChatterjee20k 2d41a3353d * migration fix for tablesdb, legacy with multitype callback
* updated patch script
2025-10-10 20:29:05 +05:30
ArnabChatterjee20k 34e4208b3e addressed comments
* renamed getDatabaseDB to getDatabasesDB
* renamed dbForDatabase to dbForDatabases
* removed call_user_func and using newer callable syntax
2025-10-10 20:00:43 +05:30
ArnabChatterjee20k d51486c62a * fixed stats usage events(hacky fix)
* updated redundant routes(happened during merge conflicts)
2025-10-10 19:39:59 +05:30
ArnabChatterjee20k f62f00d4d9 Merge remote-tracking branch 'upstream/documents-db-api' into documents-db-api 2025-10-10 15:26:33 +05:30
ArnabChatterjee20k 44e6ef3146 event generation fix 2025-10-10 15:24:42 +05:30
Jake BarnbyandGitHub dea49d54c9 Merge pull request #10628 from appwrite/fix-enums
Update: enums
2025-10-10 22:16:45 +13:00
Darshan 8a9523fa48 update: enums. 2025-10-10 14:40:26 +05:30
Darshan 5dba0183c0 update: enums. 2025-10-10 14:40:01 +05:30
ArnabChatterjee20k 0108f86180 linting + merge conflicts resolving 2025-10-10 13:39:16 +05:30
ArnabChatterjee20k bc71d83697 empty commit 2025-10-10 10:58:11 +05:30
ArnabChatterjee20k 0754c6263e merged changes from 1.8.x 2025-10-10 10:55:48 +05:30
ArnabChatterjee20k 2c037803f0 updated sdk names and enums for documentsdb 2025-10-09 18:41:10 +05:30
ArnabChatterjee20k 48410d0cf6 * added getDatabaseDB in stats worker, migration worker
* added prefixed events for multitype db support with backward compat
* added realtime tests, migration tests
2025-10-09 18:26:05 +05:30
ArnabChatterjee20k 66b0f3c9df * added waiting time in usage test so that aggregator can work
* added documentsdb usage test
2025-10-09 18:24:04 +05:30
ArnabChatterjee20k d8910eea73 fix - updated documentsdb sdk namespace to documentsDB 2025-10-09 10:30:57 +05:30
ArnabChatterjee20k f7f965a812 * added prefixed channels for realtime channels along with backward compatibility
* added documentsdb tests
2025-10-09 00:35:38 +05:30
ArnabChatterjee20k d806bdbca7 added project region in the get database dsn 2025-10-08 11:17:12 +05:30
ArnabChatterjee20k 19b1196633 * new db pool implementation for connecting to db type
* fixed migration tests
2025-10-08 11:09:32 +05:30
ArnabChatterjee20k 88f2b1ad50 added patch script for adding database dsn in the databases collection 2025-10-07 13:18:11 +05:30
ArnabChatterjee20k f5ea19b15d updated tests 2025-10-07 12:45:21 +05:30
ArnabChatterjee20k d2645ecfb1 Merge remote-tracking branch 'upstream/1.8.x' into documents-db-api 2025-10-07 10:47:26 +05:30
ArnabChatterjee20k 44b75d50f4 revmoved databasse logs xlist action for documentsdb 2025-10-03 19:56:20 +05:30
ArnabChatterjee20k df23aacbc0 updated routes and endpoints 2025-10-03 19:51:00 +05:30
ArnabChatterjee20k 7d4dfe06e2 * removed dbForDocumentsRecords
* using callback based db resolution
* updated env
2025-10-03 19:50:29 +05:30
ArnabChatterjee20k e50af4c981 add references docs for the documentsdb 2025-10-02 19:24:35 +05:30
ArnabChatterjee20k 4fdab6fd52 updated database db resolution in the resources itself instead of switch in the worker action 2025-10-02 18:58:11 +05:30
ArnabChatterjee20k c54450f582 updated composer 2025-10-02 18:57:15 +05:30
ArnabChatterjee20k 6bf9693146 Merge remote-tracking branch 'upstream/1.8.x' into documents-db-api 2025-10-02 18:56:48 +05:30
ArnabChatterjee20k 422fd8b157 updated get endpoint 2025-09-30 20:02:30 +05:30
ArnabChatterjee20k bf061b460b added documentsdb in the github actions 2025-09-30 19:29:36 +05:30
ArnabChatterjee20k a035760b00 updated injections and reverted reduntant changes 2025-09-30 19:15:43 +05:30
ArnabChatterjee20k 85c0454c49 linting 2025-09-30 18:45:53 +05:30
ArnabChatterjee20k 7bfb0401c6 linting 2025-09-30 18:44:27 +05:30
ArnabChatterjee20k c097f4a450 updated composer lock 2025-09-30 18:43:02 +05:30
ArnabChatterjee20k a3faa108c5 Merge remote-tracking branch 'upstream/1.8.x' into documents-db-api 2025-09-30 18:42:18 +05:30
ArnabChatterjee20k c89b76ba80 *updated tests
*updated create documents to use the on next callback
2025-09-30 18:19:44 +05:30
ArnabChatterjee20k 80636e8a91 updated timeouts 2025-09-30 00:03:12 +05:30
ArnabChatterjee20k 206845e106 updated tests and delete delete documents 2025-09-28 14:43:44 +05:30
ArnabChatterjee20k 64d015065f * added new databasetype dsn
* updated worker to use the dsn to choose the database resource
2025-09-26 19:52:25 +05:30
ArnabChatterjee20k 56af56e341 updated endpoints 2025-09-26 18:01:39 +05:30
ArnabChatterjee20k 3086e1cbc1 updated increment, and decrements routes 2025-09-25 19:58:59 +05:30
ArnabChatterjee20k b032a55045 updated the endpoints 2025-09-25 00:48:09 +05:30
ArnabChatterjee20k d71c56b11f * Migrating endpoints
* New adapter resolution and injection
* Removed attribute creation for documentsdb
2025-09-23 23:15:46 +05:30
ArnabChatterjee20k 98ee242812 migrated tests 2025-09-20 18:21:00 +05:30
ArnabChatterjee20k 79267c8123 migrated endpoints 2025-09-20 18:20:50 +05:30
ArnabChatterjee20k d2ad0e7c74 documentsdb database creation endpoints 2025-09-19 19:44:46 +05:30
1059 changed files with 80792 additions and 17040 deletions
@@ -0,0 +1,174 @@
# Parallel Chunk Upload Support for utopia-php/storage
## Context
The Appwrite API now supports out-of-order chunked uploads (chunks can arrive in any sequence). The next step is **parallel uploads** — multiple chunks uploaded simultaneously via separate HTTP requests. The SDK guarantees the first chunk is sent before any parallel chunks, so the document creation race is handled at the API layer. However, the storage device layer has a race condition that must be fixed.
## Problem: `Local::joinChunks()` Race
When two requests upload the final missing chunks in parallel, both can observe `countChunks() == $chunks` and call `joinChunks()` simultaneously.
### Current behavior (loser throws)
```php
// Local::joinChunks()
$dest = \fopen($tmpAssemble, 'wb');
// ... stream all parts into $tmpAssemble ...
if (! \rename($tmpAssemble, $path)) {
\unlink($tmpAssemble);
throw new Exception('Failed to finalize assembled file '.$path);
}
```
The winner succeeds with `rename()`. The loser gets `false` from `rename()` (file already exists at `$path`) and throws a 500-error exception. The client that lost the race receives an error even though the file is fully assembled.
### Required behavior
If `$path` already exists, another request already assembled the file. The loser should **silently succeed** — the file is complete, nothing more to do.
## Proposed Changes
### 1. `Local::joinChunks()` — Handle assembly race
Before opening `$tmpAssemble`, check if the final file already exists. If it does, skip assembly entirely.
```php
private function joinChunks(string $path, int $chunks): void
{
// Race winner already assembled the file
if (\file_exists($path)) {
return;
}
$tmp = \dirname($path).DIRECTORY_SEPARATOR.'tmp_'.asename($path);
$tmpAssemble = \dirname($path).DIRECTORY_SEPARATOR.'tmp_assemble_'.asename($path);
// ... rest of assembly logic ...
if (! \rename($tmpAssemble, $path)) {
// Another request may have won the race between fclose and rename
if (\file_exists($path)) {
\unlink($tmpAssemble);
return;
}
\unlink($tmpAssemble);
throw new Exception('Failed to finalize assembled file '.$path);
}
// ... cleanup ...
}
```
### 2. `Local::countChunks()` — Reliability under concurrent writes
`countChunks()` uses `glob()` on the temp directory. Under heavy parallel load, `glob()` might miss files or return inconsistent counts. The current implementation is already fairly robust (it validates `.part.\d+` suffix), but we should document that the return value is a best-effort snapshot.
No code change needed here unless tests reveal issues.
### 3. Tests — Concurrent chunk uploads
Add a test that simulates two parallel requests completing a multi-chunk upload:
```php
public function testParallelChunkUpload(): void
{
$storage = $this->makeJoinTestStorage();
$dest = $storage->getRoot().DIRECTORY_SEPARATOR.'parallel.dat';
// Upload chunk 1 (creates temp directory)
$storage->uploadData('AAAA', $dest, 'application/octet-stream', 1, 2);
// Simulate two parallel requests uploading the last chunk
// In a real test, use pcntl_fork() or pthreads for true concurrency
// For the test suite, sequential calls are sufficient if we verify
// the second call doesn't throw after the first completed assembly
$storage->uploadData('BBBB', $dest, 'application/octet-stream', 2, 2);
// Verify file exists and is correct
$this->assertTrue(\file_exists($dest));
$this->assertSame('AAAABBBB', \file_get_contents($dest));
// Verify second assembly attempt doesn't throw
// (This simulates the race where another request already assembled)
try {
$storage->uploadData('BBBB', $dest, 'application/octet-stream', 2, 2);
} catch (\Exception $e) {
$this->fail('Duplicate assembly should not throw: '.$e->getMessage());
}
$storage->delete($storage->getRoot(), true);
}
```
A more realistic concurrent test using `pcntl_fork()`:
```php
public function testParallelChunkUploadWithFork(): void
{
if (!\function_exists('pcntl_fork')) {
$this->markTestSkipped('pcntl extension required for fork-based concurrency test');
}
$storage = $this->makeJoinTestStorage();
$dest = $storage->getRoot().DIRECTORY_SEPARATOR.'parallel-fork.dat';
// Pre-upload chunk 1
$storage->uploadData('AAAA', $dest, 'application/octet-stream', 1, 2);
$pid = pcntl_fork();
if ($pid === -1) {
$this->fail('Failed to fork');
} elseif ($pid === 0) {
// Child process: upload chunk 2
try {
$storage->uploadData('BBBB', $dest, 'application/octet-stream', 2, 2);
exit(0);
} catch (\Exception $e) {
exit(1);
}
}
// Parent process: also upload chunk 2 (race condition)
$parentSuccess = true;
try {
$storage->uploadData('BBBB', $dest, 'application/octet-stream', 2, 2);
} catch (\Exception $e) {
$parentSuccess = false;
}
pcntl_waitpid($pid, $status);
$childSuccess = pcntl_wexitstatus($status) === 0;
// At least one should succeed
$this->assertTrue($parentSuccess || $childSuccess, 'At least one parallel upload should succeed');
// File should be correctly assembled
$this->assertTrue(\file_exists($dest));
$this->assertSame('AAAABBBB', \file_get_contents($dest));
$storage->delete($storage->getRoot(), true);
}
```
## S3 Device
S3 already handles out-of-order multipart uploads natively. The `completeMultipartUpload` call with `ksort()` sorts parts by number regardless of upload order. However, parallel `completeMultipartUpload` calls for the same `uploadId` would still be problematic.
This is an **API-layer concern** — the Appwrite API should ensure only one request calls `completeMultipartUpload` per upload. The S3 device itself does not need changes.
## Files to Change
| File | Change |
|------|--------|
| `src/Storage/Device/Local.php` | Add `file_exists($path)` guard at start of `joinChunks()` and in `rename()` failure handler |
| `tests/Storage/Device/LocalTest.php` | Add `testParallelChunkUpload` and `testParallelChunkUploadWithFork` |
## Backwards Compatibility
Fully backwards compatible. The change only affects the error path when `rename()` fails due to an existing file. Previously it threw; now it returns silently. No public API signatures change.
## Related PRs
- Appwrite server PR: https://github.com/appwrite/appwrite/pull/12138 (out-of-order upload support)
- This storage PR is a prerequisite for the follow-up Appwrite PR that enables parallel chunk uploads at the API level.
@@ -0,0 +1,29 @@
# Patch Release Checklist for Appwrite
When bumping a patch version (e.g., `1.9.0` -> `1.9.1`), follow this checklist.
## Checklist
### Bump console image
Update the console Docker image tag in both files:
- [ ] `docker-compose.yml` -- update `image: appwrite/console:X.Y.Z`
- [ ] `app/views/install/compose.phtml` -- update `image: <?php echo $organization; ?>/console:X.Y.Z`
### Bump Appwrite version
- [ ] **`app/init/constants.php`** -- update `APP_VERSION_STABLE` to the new version (e.g., `'1.9.1'`). In same file, increment `APP_CACHE_BUSTER` by 1.
- [ ] **`README.md`** -- update the Docker image tag `appwrite/appwrite:X.Y.Z` in all 3 install code blocks (Unix, Windows CMD, PowerShell).
- [ ] **`README-CN.md`** -- same Docker image tag update in all 3 install code blocks.
- [ ] **`src/Appwrite/Migration/Migration.php`** -- add the new version to the `$versions` array, mapping it to a migration class. If new class exists, use that, otherwise use sle same class as previous version
### Update CHANGES.md
- [ ] Add a new `# Version X.Y.Z` section at the top of `CHANGES.md` with subsections: `### Notable changes`, `### Fixes`, `### Miscellaneous`
## Final review
- [ ] Ask user to review changes before commiting
- [ ] Ask user to update `CHANGES.md` with PRs
- [ ] Ask user to generate specs, if needed
- [ ] Ask user to add request and response filters, if needed
+12 -1
View File
@@ -39,7 +39,7 @@ _APP_REDIS_HOST=redis
_APP_REDIS_PORT=6379
_APP_REDIS_PASS=
_APP_REDIS_USER=
COMPOSE_PROFILES=mongodb
COMPOSE_PROFILES=mariadb,mongodb,postgresql
_APP_DB_ADAPTER=mongodb
_APP_DB_HOST=mongodb
_APP_DB_PORT=27017
@@ -47,6 +47,15 @@ _APP_DB_SCHEMA=appwrite
_APP_DB_USER=user
_APP_DB_PASS=password
_APP_DB_ROOT_PASS=rootsecretpassword
_APP_DB_ADAPTER_DOCUMENTSDB=mongodb
_APP_DB_HOST_DOCUMENTSDB=mongodb
_APP_DB_PORT_DOCUMENTSDB=27017
_APP_DB_ADAPTER_VECTORSDB=postgresql
_APP_DB_HOST_VECTORSDB=postgresql
_APP_DB_PORT_VECTORSDB=5432
_APP_EMBEDDING_MODELS=embeddinggemma
_APP_EMBEDDING_ENDPOINT='http://ollama:11434/api/embed'
_APP_EMBEDDING_TIMEOUT=30000
_APP_STORAGE_DEVICE=Local
_APP_STORAGE_S3_ACCESS_KEY=
_APP_STORAGE_S3_SECRET=
@@ -137,3 +146,5 @@ _APP_STATS_USAGE_DUAL_WRITING_DBS=database_db_main
_APP_TRUSTED_HEADERS=x-forwarded-for
_APP_POOL_ADAPTER=stack
_APP_WORKER_SCREENSHOTS_ROUTER=http://appwrite
_TESTS_OAUTH2_GITHUB_CLIENT_ID=
_TESTS_OAUTH2_GITHUB_CLIENT_SECRET=
+349
View File
@@ -0,0 +1,349 @@
const fs = require('fs');
const marker = '<!-- appwrite-benchmark-results -->';
const serviceLabels = ['Account', 'TablesDB', 'Storage', 'Functions'];
module.exports = async ({ github, context, core }) => {
const body = buildComment(core);
fs.writeFileSync('benchmark-comment.txt', body);
const pullRequest = context.payload.pull_request;
if (!pullRequest || pullRequest.head.repo.full_name !== `${context.repo.owner}/${context.repo.repo}`) {
return;
}
const comments = await github.paginate(github.rest.issues.listComments, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pullRequest.number,
per_page: 100,
});
const existing = comments.find((comment) => {
return comment.user?.type === 'Bot' && comment.body?.includes(marker);
}) || comments.find((comment) => {
return comment.user?.type === 'Bot' && comment.body?.includes('Benchmark results');
});
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
return;
}
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pullRequest.number,
body,
});
};
function buildComment(core) {
const before = readSummary('benchmark-before-summary.json', core);
const after = readSummary('benchmark-after-summary.json', core);
const beforeSamples = readSamples('benchmark-before-samples.json', core);
const afterSamples = readSamples('benchmark-after-samples.json', core);
const baseRef = markdownText(process.env.BENCHMARK_BASE_REF || 'base');
const headRef = markdownText(process.env.BENCHMARK_HEAD_REF || 'head');
const rows = benchmarkRows(before, after, beforeSamples, afterSamples);
const topWaits = topSamples(afterSamples, 'appwrite_api_waiting', 3);
const lines = [
marker,
'## :sparkles: Benchmark results',
'',
`Comparing ${baseRef} (before) to ${headRef} (after).`,
'',
];
if (before === null) {
lines.push('> Before benchmark did not complete; showing current branch metrics only.', '');
}
if (after === null) {
lines.push('> Current branch benchmark did not complete; showing available metrics only.', '');
}
lines.push(
'**Before**',
'',
metricTable(rows, 'before'),
'',
'**After**',
'',
metricTable(rows, 'after'),
'',
'**Delta**',
'',
'| Scenario | P95 delta (ms) |',
'| --- | ---: |',
...rows.map(deltaRow),
'',
'<details>',
'<summary><strong>Top API waits</strong></summary>',
'',
'<br>',
'',
'| API request | Max wait (ms) |',
'| --- | ---: |',
...topWaitRows(topWaits),
'',
'</details>',
);
return `${lines.join('\n')}\n`;
}
function readSummary(path, core) {
if (!fs.existsSync(path)) {
return null;
}
try {
return JSON.parse(fs.readFileSync(path, 'utf8'));
} catch (error) {
core?.warning(`Invalid benchmark summary ${path}: ${error.message}`);
return null;
}
}
function readSamples(path, core) {
if (!fs.existsSync(path)) {
return [];
}
const contents = fs.readFileSync(path, 'utf8').trim();
if (contents === '') {
return [];
}
return contents
.split('\n')
.filter(Boolean)
.flatMap((line) => {
try {
return [JSON.parse(line)];
} catch (error) {
core?.warning(`Invalid benchmark sample in ${path}: ${error.message}`);
return [];
}
});
}
function benchmarkRows(before, after, beforeSamples, afterSamples) {
const beforeServices = serviceStats(beforeSamples);
const afterServices = serviceStats(afterSamples);
return [
{
label: 'API total',
before: apiSampleStats(beforeSamples) || summaryStats(before, 'appwrite_api_duration'),
after: apiSampleStats(afterSamples) || summaryStats(after, 'appwrite_api_duration'),
},
...serviceLabels.map((label) => ({
label,
before: beforeServices.get(label) || null,
after: afterServices.get(label) || null,
})),
];
}
function summaryStats(summary, durationMetric, iterationsMetric = null, rpsMetric = null) {
const values = metricValues(summary, durationMetric);
if (!values) {
return null;
}
return {
p50: values.med ?? null,
p95: values['p(95)'] ?? null,
iterations: iterationsMetric ? metricValue(summary, iterationsMetric, 'count') : values.count ?? null,
rps: rpsMetric ? metricValue(summary, rpsMetric, 'rate') : null,
};
}
function serviceStats(samples) {
const apiSamples = samples.filter((sample) => {
return sample.metric === 'appwrite_api_duration' && typeof sample.data?.value === 'number';
});
const groups = new Map();
for (const sample of apiSamples) {
const service = serviceFromName(sample.data.tags?.name || '');
if (!service) {
continue;
}
const serviceSamples = groups.get(service) || [];
serviceSamples.push(sample);
groups.set(service, serviceSamples);
}
return new Map([...groups.entries()].map(([service, serviceSamples]) => {
const values = serviceSamples.map((sample) => sample.data.value);
const durationSeconds = sampleWindowSeconds(serviceSamples);
return [service, {
p50: percentile(values, 50),
p95: percentile(values, 95),
iterations: values.length,
rps: durationSeconds ? values.length / durationSeconds : null,
}];
}));
}
function apiSampleStats(samples) {
const apiSamples = samples.filter((sample) => {
return sample.metric === 'appwrite_api_duration' && typeof sample.data?.value === 'number';
});
const values = apiSamples.map((sample) => sample.data.value);
if (values.length === 0) {
return null;
}
const durationSeconds = sampleWindowSeconds(apiSamples);
return {
p50: percentile(values, 50),
p95: percentile(values, 95),
iterations: values.length,
rps: durationSeconds ? values.length / durationSeconds : null,
};
}
function serviceFromName(name) {
if (name.startsWith('account.')) {
return 'Account';
}
if (name.startsWith('tablesdb.')) {
return 'TablesDB';
}
if (name.startsWith('storage.') || name.startsWith('tokens.')) {
return 'Storage';
}
if (name.startsWith('functions.')) {
return 'Functions';
}
return null;
}
function sampleWindowSeconds(samples) {
const times = samples
.map((sample) => Date.parse(sample.data?.time))
.filter((value) => !Number.isNaN(value));
if (times.length < 2) {
return null;
}
return Math.max((Math.max(...times) - Math.min(...times)) / 1000, 1);
}
function percentile(values, percentileValue) {
if (values.length === 0) {
return null;
}
const sorted = [...values].sort((left, right) => left - right);
const index = Math.ceil((percentileValue / 100) * sorted.length) - 1;
return sorted[Math.max(0, Math.min(index, sorted.length - 1))];
}
function metricValues(data, metric) {
return data?.metrics?.[metric]?.values ?? null;
}
function metricValue(data, metric, stat) {
return metricValues(data, metric)?.[stat] ?? null;
}
function metricTable(rows, side) {
return [
'| Scenario | P50 (ms) | P95 (ms) | Requests | RPS |',
'| --- | ---: | ---: | ---: | ---: |',
...rows.map((row) => metricRow(row, side)),
].join('\n');
}
function metricRow(row, side) {
const values = row[side];
return `| ${row.label} | ${formatMs(values?.p50)} | ${formatMs(values?.p95)} | ${formatCount(values?.iterations)} | ${formatRate(values?.rps)} |`;
}
function deltaRow(row) {
return `| ${row.label} | ${formatDelta(row.before?.p95, row.after?.p95)} |`;
}
function topSamples(samples, metric, limit) {
const byName = samples.reduce((result, sample) => {
if (sample.metric !== metric || typeof sample.data?.value !== 'number') {
return result;
}
const name = sample.data.tags?.name || 'unknown';
const current = result.get(name);
if (!current || sample.data.value > current.value) {
result.set(name, { name, value: sample.data.value });
}
return result;
}, new Map());
return [...byName.values()]
.sort((left, right) => right.value - left.value)
.slice(0, limit);
}
function topWaitRows(samples) {
if (samples.length === 0) {
return ['| n/a | n/a |'];
}
return samples.map((sample) => {
return `| ${markdownText(sample.name).replace(/\|/g, '\\|')} | ${formatMs(sample.value)} |`;
});
}
function markdownText(value) {
return String(value || '').replace(/[\r\n]/g, ' ').replace(/[&<>"']/g, (char) => {
return ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;' })[char];
});
}
function formatMs(value) {
return formatNumber(value, 2);
}
function formatRate(value) {
return formatNumber(value, 2);
}
function formatCount(value) {
if (value === null || value === undefined || Number.isNaN(value)) {
return 'n/a';
}
return `${Math.round(value)}`;
}
function formatDelta(before, after) {
if (before === null || before === undefined || after === null || after === undefined || Number.isNaN(before) || Number.isNaN(after)) {
return 'n/a';
}
const difference = Number((after - before).toFixed(2));
return `${difference > 0 ? '+' : ''}${trimNumber(difference)}`;
}
function formatNumber(value, decimals) {
if (value === null || value === undefined || Number.isNaN(value)) {
return 'n/a';
}
return trimNumber(Number(value).toFixed(decimals));
}
function trimNumber(value) {
const text = String(value);
const trimmed = text.includes('.') ? text.replace(/\.?0+$/, '') : text;
return trimmed === '' ? '0' : trimmed;
}
+295 -129
View File
@@ -7,7 +7,8 @@ concurrency:
env:
COMPOSE_FILE: docker-compose.yml
IMAGE: appwrite-dev
CACHE_KEY: appwrite-dev-${{ github.event.pull_request.head.sha }}
REGISTRY_IMAGE: ghcr.io/${{ github.repository }}/appwrite-dev
K6_VERSION: '0.53.0'
on:
pull_request:
@@ -19,6 +20,10 @@ on:
type: string
default: ''
permissions:
contents: read
packages: write
jobs:
dependencies:
name: Checks / Dependencies
@@ -151,8 +156,37 @@ jobs:
- name: Install dependencies
run: composer install --prefer-dist --no-progress --ignore-platform-reqs
- name: Cache PHPStan result cache
uses: actions/cache@v4
with:
path: .phpstan-cache
key: phpstan-${{ github.sha }}
restore-keys: |
phpstan-
- name: Run PHPStan
run: composer analyze
run: composer analyze -- --no-progress
specs:
name: Checks / Specs
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v6
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: swoole
tools: composer:v2
coverage: none
- name: Install dependencies
run: composer install --prefer-dist --no-progress --ignore-platform-reqs
- name: Generate specs
run: _APP_STORAGE_LIMIT=5368709120 php app/cli.php specs --version=latest --git=no
locale:
name: Checks / Locale
@@ -182,7 +216,7 @@ jobs:
with:
script: |
const allDatabases = ['MariaDB', 'PostgreSQL', 'MongoDB'];
const allModes = ['dedicated', 'shared_v1', 'shared_v2'];
const allModes = ['dedicated', 'shared'];
const defaultDatabases = ['MongoDB'];
const defaultModes = ['dedicated'];
@@ -229,31 +263,30 @@ jobs:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Build Appwrite
- name: Build and push Appwrite
uses: docker/build-push-action@v6
with:
context: .
push: false
tags: ${{ env.IMAGE }}
load: true
push: true
tags: ${{ env.REGISTRY_IMAGE }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
outputs: type=docker,dest=/tmp/${{ env.IMAGE }}.tar
target: development
build-args: |
DEBUG=false
TESTING=true
VERSION=dev
- name: Cache Docker Image
uses: actions/cache@v5
with:
key: ${{ env.CACHE_KEY }}
path: /tmp/${{ env.IMAGE }}.tar
unit:
name: Tests / Unit
runs-on: ubuntu-latest
@@ -261,27 +294,32 @@ jobs:
permissions:
contents: read
pull-requests: write
packages: read
steps:
- name: checkout
uses: actions/checkout@v6
- name: Load Cache
uses: actions/cache@v5
with:
key: ${{ env.CACHE_KEY }}
path: /tmp/${{ env.IMAGE }}.tar
fail-on-cache-miss: true
- name: Login to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Pull Docker Image
run: |
docker pull ${{ env.REGISTRY_IMAGE }}:${{ github.sha }}
docker tag ${{ env.REGISTRY_IMAGE }}:${{ github.sha }} ${{ env.IMAGE }}
- name: Load and Start Appwrite
timeout-minutes: 5
run: |
docker load --input /tmp/${{ env.IMAGE }}.tar
docker compose pull --quiet --ignore-buildable
docker compose up -d --quiet-pull --wait
@@ -309,27 +347,32 @@ jobs:
permissions:
contents: read
pull-requests: write
packages: read
steps:
- name: checkout
uses: actions/checkout@v6
- name: Load Cache
uses: actions/cache@v5
with:
key: ${{ env.CACHE_KEY }}
path: /tmp/${{ env.IMAGE }}.tar
fail-on-cache-miss: true
- name: Login to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Pull Docker Image
run: |
docker pull ${{ env.REGISTRY_IMAGE }}:${{ github.sha }}
docker tag ${{ env.REGISTRY_IMAGE }}:${{ github.sha }} ${{ env.IMAGE }}
- name: Load and Start Appwrite
timeout-minutes: 5
run: |
docker load --input /tmp/${{ env.IMAGE }}.tar
docker compose pull --quiet --ignore-buildable
docker compose up -d --quiet-pull --wait
@@ -363,11 +406,12 @@ jobs:
e2e_service:
name: Tests / E2E / ${{ matrix.database }} (${{ matrix.mode }}) / ${{ matrix.service }}
runs-on: ubuntu-latest
runs-on: ${{ matrix.runner || 'ubuntu-latest' }}
needs: [build, matrix]
permissions:
contents: read
pull-requests: write
packages: read
strategy:
fail-fast: false
matrix:
@@ -396,21 +440,36 @@ jobs:
Webhooks,
VCS,
Messaging,
Migrations
Migrations,
Project
]
include:
- service: Databases
runner: blacksmith-4vcpu-ubuntu-2404
paratest_processes: 3
timeout_minutes: 30
- service: Sites
runner: blacksmith-4vcpu-ubuntu-2404
- service: Functions
runner: blacksmith-4vcpu-ubuntu-2404
- service: Avatars
runner: blacksmith-4vcpu-ubuntu-2404
- service: Realtime
runner: blacksmith-4vcpu-ubuntu-2404
- service: TablesDB
runner: blacksmith-4vcpu-ubuntu-2404
paratest_processes: 3
timeout_minutes: 30
- service: Migrations
paratest_processes: 1
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Load Cache
uses: actions/cache@v5
with:
key: ${{ env.CACHE_KEY }}
path: /tmp/${{ env.IMAGE }}.tar
fail-on-cache-miss: true
- name: Set database environment
- name: Set environment
run: |
echo "_APP_OPTIONS_ROUTER_PROTECTION=enabled" >> $GITHUB_ENV
if [ "${{ matrix.database }}" = "MariaDB" ]; then
echo "COMPOSE_PROFILES=mariadb" >> $GITHUB_ENV
echo "_APP_DB_ADAPTER=mariadb" >> $GITHUB_ENV
@@ -434,14 +493,26 @@ jobs:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Pull Docker Image
run: |
docker pull ${{ env.REGISTRY_IMAGE }}:${{ github.sha }}
docker tag ${{ env.REGISTRY_IMAGE }}:${{ github.sha }} ${{ env.IMAGE }}
- name: Load and Start Appwrite
timeout-minutes: 5
env:
_APP_BROWSER_HOST: http://invalid-browser/v1
_APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }}
_APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }}
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'documentsdb_db_main' || '' }}
_APP_DATABASE_VECTORSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'vectorsdb_db_main' || '' }}
run: |
docker load --input /tmp/${{ env.IMAGE }}.tar
docker compose pull --quiet --ignore-buildable
docker compose up -d --quiet-pull --wait
@@ -458,7 +529,7 @@ jobs:
with:
max_attempts: 2
retry_wait_seconds: 60
timeout_minutes: 20
timeout_minutes: ${{ matrix.timeout_minutes || 20 }}
job_id: ${{ job.check_run_id }}
github_token: ${{ secrets.GITHUB_TOKEN }}
test_dir: tests/e2e/Services/${{ matrix.service }}
@@ -468,12 +539,19 @@ jobs:
# Services that rely on sequential test method execution (shared static state)
FUNCTIONAL_FLAG="--functional"
case "${{ matrix.service }}" in
Databases|TablesDB|Functions|Realtime) FUNCTIONAL_FLAG="" ;;
Databases|TablesDB|Functions|Realtime|GraphQL|ProjectWebhooks) FUNCTIONAL_FLAG="" ;;
esac
PARATEST_PROCESSES="${{ matrix.paratest_processes }}"
if [ -z "$PARATEST_PROCESSES" ]; then
PARATEST_PROCESSES="$(nproc)"
fi
docker compose exec -T \
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \
appwrite vendor/bin/paratest --processes $(nproc) $FUNCTIONAL_FLAG "$SERVICE_PATH" --exclude-group abuseEnabled --exclude-group screenshots --log-junit tests/e2e/Services/${{ matrix.service }}/junit.xml
-e _TESTS_OAUTH2_GITHUB_CLIENT_ID="${{ secrets.TESTS_OAUTH2_GITHUB_CLIENT_ID }}" \
-e _TESTS_OAUTH2_GITHUB_CLIENT_SECRET="${{ secrets.TESTS_OAUTH2_GITHUB_CLIENT_SECRET }}" \
appwrite vendor/bin/paratest --processes "$PARATEST_PROCESSES" $FUNCTIONAL_FLAG "$SERVICE_PATH" --exclude-group abuseEnabled --exclude-group screenshots --log-junit tests/e2e/Services/${{ matrix.service }}/junit.xml
- name: Failure Logs
if: failure()
@@ -488,6 +566,7 @@ jobs:
permissions:
contents: read
pull-requests: write
packages: read
strategy:
fail-fast: false
matrix:
@@ -495,13 +574,8 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Load Cache
uses: actions/cache@v5
with:
key: ${{ env.CACHE_KEY }}
path: /tmp/${{ env.IMAGE }}.tar
fail-on-cache-miss: true
fetch-depth: 1
- name: Login to Docker Hub
uses: docker/login-action@v4
@@ -509,14 +583,26 @@ jobs:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Pull Docker Image
run: |
docker pull ${{ env.REGISTRY_IMAGE }}:${{ github.sha }}
docker tag ${{ env.REGISTRY_IMAGE }}:${{ github.sha }} ${{ env.IMAGE }}
- name: Load and Start Appwrite
timeout-minutes: 5
env:
_APP_OPTIONS_ABUSE: enabled
_APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }}
_APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }}
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'documentsdb_db_main' || '' }}
_APP_DATABASE_VECTORSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'vectorsdb_db_main' || '' }}
run: |
docker load --input /tmp/${{ env.IMAGE }}.tar
docker compose pull --quiet --ignore-buildable
docker compose up -d --quiet-pull --wait
@@ -547,6 +633,7 @@ jobs:
permissions:
contents: read
pull-requests: write
packages: read
strategy:
fail-fast: false
matrix:
@@ -555,26 +642,31 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v6
- name: Load Cache
uses: actions/cache@v5
with:
key: ${{ env.CACHE_KEY }}
path: /tmp/${{ env.IMAGE }}.tar
fail-on-cache-miss: true
- name: Login to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Pull Docker Image
run: |
docker pull ${{ env.REGISTRY_IMAGE }}:${{ github.sha }}
docker tag ${{ env.REGISTRY_IMAGE }}:${{ github.sha }} ${{ env.IMAGE }}
- name: Load and Start Appwrite
timeout-minutes: 5
env:
_APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }}
_APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }}
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'documentsdb_db_main' || '' }}
_APP_DATABASE_VECTORSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'vectorsdb_db_main' || '' }}
run: |
docker load --input /tmp/${{ env.IMAGE }}.tar
docker compose pull --quiet --ignore-buildable
docker compose up -d --quiet-pull --wait
@@ -608,20 +700,20 @@ jobs:
benchmark:
name: Benchmark
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
needs: build
permissions:
actions: read
contents: read
issues: write
pull-requests: write
packages: read
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Load Cache
uses: actions/cache@v5
with:
key: ${{ env.CACHE_KEY }}
path: /tmp/${{ env.IMAGE }}.tar
fail-on-cache-miss: true
fetch-depth: 1
- name: Login to Docker Hub
uses: docker/login-action@v4
@@ -629,79 +721,153 @@ jobs:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Load and Start Appwrite
- name: Login to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Pull Appwrite image
run: |
sed -i 's/traefik/localhost/g' .env
docker load --input /tmp/${{ env.IMAGE }}.tar
docker compose up -d
sleep 10
docker pull ${{ env.REGISTRY_IMAGE }}:${{ github.sha }}
docker tag ${{ env.REGISTRY_IMAGE }}:${{ github.sha }} ${{ env.IMAGE }}
docker tag ${{ env.REGISTRY_IMAGE }}:${{ github.sha }} ${{ env.IMAGE }}:after
- name: Install Oha
- name: Setup k6
uses: grafana/setup-k6-action@ffe7d7290dfa715e48c2ccc924d068444c94bde2
with:
k6-version: ${{ env.K6_VERSION }}
- name: Prepare benchmark before
id: benchmark_before_prepare
continue-on-error: true
run: |
echo "deb [signed-by=/usr/share/keyrings/azlux-archive-keyring.gpg] http://packages.azlux.fr/debian/ stable main" | sudo tee /etc/apt/sources.list.d/azlux.list
sudo wget -O /usr/share/keyrings/azlux-archive-keyring.gpg https://azlux.fr/repo.gpg
sudo apt update
sudo apt install oha
oha --version
git fetch --depth=1 origin ${{ github.event.pull_request.base.sha }}
git worktree add --detach /tmp/appwrite-benchmark-before ${{ github.event.pull_request.base.sha }}
docker build \
--cache-from ${{ env.IMAGE }}:after \
--target development \
--build-arg DEBUG=false \
--build-arg TESTING=true \
--build-arg VERSION=dev \
--tag ${{ env.IMAGE }}:before \
/tmp/appwrite-benchmark-before
- name: Benchmark PR
run: 'oha -z 180s http://localhost/v1/health/version --output-format json > benchmark.json'
- name: Cleaning
run: docker compose down -v
- name: Installing latest version
- name: Start before Appwrite
id: benchmark_before_start
if: steps.benchmark_before_prepare.outcome == 'success'
continue-on-error: true
working-directory: /tmp/appwrite-benchmark-before
env:
_APP_DOMAIN: localhost
_APP_CONSOLE_DOMAIN: localhost
_APP_DOMAIN_FUNCTIONS: functions.localhost
_APP_OPTIONS_ABUSE: disabled
run: |
rm docker-compose.yml
rm .env
curl https://appwrite.io/install/compose -o docker-compose.yml
curl https://appwrite.io/install/env -o .env
sed -i 's/_APP_OPTIONS_ABUSE=enabled/_APP_OPTIONS_ABUSE=disabled/g' .env
docker compose up -d
sleep 10
docker tag ${{ env.IMAGE }}:before ${{ env.IMAGE }}
docker compose up -d --wait --no-build
- name: Benchmark Latest
run: oha -z 180s http://localhost/v1/health/version --output-format json > benchmark-latest.json
- name: Prepare benchmark files
run: rm -f benchmark-before-summary.json benchmark-after-summary.json benchmark-before-samples.json benchmark-after-samples.json
- name: Prepare comment
- name: Benchmark before
if: steps.benchmark_before_start.outcome == 'success'
continue-on-error: true
uses: grafana/run-k6-action@a15e2072ede004e8d46141e33d7f7dad8ad08d9d
env:
APPWRITE_ENDPOINT: 'http://localhost/v1'
APPWRITE_BENCHMARK_ITERATIONS: '5'
APPWRITE_BENCHMARK_VUS: '1'
APPWRITE_WORKER_TIMEOUT_MS: '120000'
APPWRITE_BENCHMARK_SUMMARY_PATH: 'benchmark-before-summary.json'
with:
path: tests/benchmarks/http.js
flags: --quiet --out json=benchmark-before-samples.json
cloud-comment-on-pr: false
debug: true
- name: Stop before Appwrite
if: always()
run: |
echo '## :sparkles: Benchmark results' > benchmark.txt
echo ' ' >> benchmark.txt
echo "- Requests per second: $(jq -r '.summary.requestsPerSec|tonumber?|floor|tostring|[while(length>0;.[:-3])|.[-3:]]|reverse|join(",")' benchmark.json)" >> benchmark.txt
echo "- Requests with 200 status code: $(jq -r '.statusCodeDistribution."200"|tostring|[while(length>0;.[:-3])|.[-3:]]|reverse|join(",")' benchmark.json)" >> benchmark.txt
echo "- P99 latency: $(jq -r '.latencyPercentiles.p99' benchmark.json )" >> benchmark.txt
echo " " >> benchmark.txt
echo " " >> benchmark.txt
echo "## :zap: Benchmark Comparison" >> benchmark.txt
echo " " >> benchmark.txt
echo "| Metric | This PR | Latest version | " >> benchmark.txt
echo "| --- | --- | --- | " >> benchmark.txt
echo "| RPS | $(jq -r '.summary.requestsPerSec|tonumber?|floor|tostring|[while(length>0;.[:-3])|.[-3:]]|reverse|join(",")' benchmark.json) | $(jq -r '.summary.requestsPerSec|tonumber|floor|tostring|[while(length>0;.[:-3])|.[-3:]]|reverse|join(",")' benchmark-latest.json) | " >> benchmark.txt
echo "| 200 | $(jq -r '.statusCodeDistribution."200"|tostring|[while(length>0;.[:-3])|.[-3:]]|reverse|join(",")' benchmark.json) | $(jq -r '.statusCodeDistribution."200"|tostring|[while(length>0;.[:-3])|.[-3:]]|reverse|join(",")' benchmark-latest.json) | " >> benchmark.txt
echo "| P99 | $(jq -r '.latencyPercentiles.p99' benchmark.json ) | $(jq -r '.latencyPercentiles.p99' benchmark-latest.json ) | " >> benchmark.txt
if [ -d /tmp/appwrite-benchmark-before ]; then
cd /tmp/appwrite-benchmark-before
docker compose down -v || true
fi
- name: Wait for benchmark ports
if: always()
run: |
for port in 80 443 8080 9503; do
for attempt in $(seq 1 30); do
if ! ss -ltn | awk '{print $4}' | grep -Eq "[:.]${port}$"; then
break
fi
sleep 1
done
if ss -ltn | awk '{print $4}' | grep -Eq "[:.]${port}$"; then
echo "Port ${port} is still in use after stopping the before stack"
ss -ltn
exit 1
fi
done
- name: Start after Appwrite
env:
_APP_DOMAIN: localhost
_APP_CONSOLE_DOMAIN: localhost
_APP_DOMAIN_FUNCTIONS: functions.localhost
_APP_OPTIONS_ABUSE: disabled
run: |
docker tag ${{ env.IMAGE }}:after ${{ env.IMAGE }}
docker compose up -d --wait --no-build
- name: Benchmark after
id: benchmark_after
continue-on-error: true
uses: grafana/run-k6-action@a15e2072ede004e8d46141e33d7f7dad8ad08d9d
env:
APPWRITE_ENDPOINT: 'http://localhost/v1'
APPWRITE_BENCHMARK_ITERATIONS: '5'
APPWRITE_BENCHMARK_VUS: '1'
APPWRITE_WORKER_TIMEOUT_MS: '120000'
APPWRITE_BENCHMARK_PREVIOUS_SUMMARY_PATH: '../../benchmark-before-summary.json'
APPWRITE_BENCHMARK_SUMMARY_PATH: 'benchmark-after-summary.json'
with:
path: tests/benchmarks/http.js
flags: --quiet --out json=benchmark-after-samples.json
cloud-comment-on-pr: false
debug: true
- name: Stop after Appwrite
if: always()
run: docker compose down -v || true
- name: Comment on PR
if: always()
uses: actions/github-script@v8
env:
BENCHMARK_BASE_REF: ${{ github.event.pull_request.base.ref }}
BENCHMARK_HEAD_REF: ${{ github.event.pull_request.head.ref }}
with:
script: |
const comment = require('./.github/workflows/benchmark-comment.js');
await comment({ github, context, core });
- name: Save results
uses: actions/upload-artifact@v7
if: ${{ !cancelled() }}
with:
name: benchmark.json
path: benchmark.json
name: benchmark-results
path: |
benchmark-comment.txt
benchmark-before-summary.json
benchmark-after-summary.json
benchmark-before-samples.json
benchmark-after-samples.json
retention-days: 7
- name: Find Comment
if: github.event.pull_request.head.repo.full_name == github.repository
uses: peter-evans/find-comment@v3
id: fc
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: 'github-actions[bot]'
body-includes: Benchmark results
- name: Comment on PR
if: github.event.pull_request.head.repo.full_name == github.repository
uses: peter-evans/create-or-update-comment@v4
with:
comment-id: ${{ steps.fc.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
body-path: benchmark.txt
edit-mode: replace
- name: Fail benchmark
if: always() && steps.benchmark_after.outcome != 'success'
run: exit 1
+31 -1
View File
@@ -5,6 +5,11 @@ on:
types:
- closed
permissions:
actions: write
contents: read
packages: write
jobs:
cleanup:
runs-on: ubuntu-latest
@@ -36,4 +41,29 @@ jobs:
done
done
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Cleanup GHCR image
continue-on-error: true
run: |
package_path="${GITHUB_REPOSITORY#*/}/appwrite-dev"
encoded_path="$(printf '%s' "$package_path" | jq -Rr @uri)"
gh api --paginate "/repos/${GITHUB_REPOSITORY}/pulls/${{ github.event.pull_request.number }}/commits" --jq '.[].sha' | while read -r sha; do
version_ids=$(gh api --paginate -H "Accept: application/vnd.github+json" \
"/orgs/${GITHUB_REPOSITORY_OWNER}/packages/container/${encoded_path}/versions" \
--jq ".[] | select(.metadata.container.tags | index(\"${sha}\")) | .id")
if [ -z "$version_ids" ]; then
echo "No GHCR version found for SHA ${sha}"
continue
fi
echo "$version_ids" | while read -r version_id; do
gh api --method DELETE -H "Accept: application/vnd.github+json" \
"/orgs/${GITHUB_REPOSITORY_OWNER}/packages/container/${encoded_path}/versions/${version_id}"
echo "Deleted ${package_path}:${sha} (version ${version_id})"
done
done
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+8 -4
View File
@@ -16,7 +16,7 @@ jobs:
- name: Build the Docker image
run: DOCKER_BUILDKIT=1 docker build . --target production -t appwrite_image:latest
- name: Run Trivy vulnerability scanner on image
uses: aquasecurity/trivy-action@0.20.0
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0
with:
image-ref: 'appwrite_image:latest'
format: 'sarif'
@@ -24,9 +24,11 @@ jobs:
ignore-unfixed: 'false'
severity: 'CRITICAL,HIGH'
- name: Upload Docker Image Scan Results
uses: github/codeql-action/upload-sarif@v2
uses: github/codeql-action/upload-sarif@v4
if: always() && hashFiles('trivy-image-results.sarif') != ''
with:
sarif_file: 'trivy-image-results.sarif'
category: 'trivy-image'
scan-code:
name: Scan Code
@@ -35,13 +37,15 @@ jobs:
- name: Check out code
uses: actions/checkout@v6
- name: Run Trivy vulnerability scanner on filesystem
uses: aquasecurity/trivy-action@0.20.0
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0
with:
scan-type: 'fs'
format: 'sarif'
output: 'trivy-fs-results.sarif'
severity: 'CRITICAL,HIGH'
- name: Upload Code Scan Results
uses: github/codeql-action/upload-sarif@v2
uses: github/codeql-action/upload-sarif@v4
if: always() && hashFiles('trivy-fs-results.sarif') != ''
with:
sarif_file: 'trivy-fs-results.sarif'
category: 'trivy-source'
+1
View File
@@ -21,6 +21,7 @@ appwrite.config.json
/app/config/specs/
/docs/examples/
.phpunit.cache
.phpstan-cache
playwright-report
test-results
docker-compose.web-installer.yml
+101 -84
View File
@@ -1,107 +1,124 @@
# AGENTS.md
# Appwrite
Appwrite is an end-to-end backend server for web, mobile, native, and backend apps. This guide provides context and instructions for AI coding agents working on the Appwrite codebase.
Self-hosted Backend-as-a-Service platform. Hybrid monolithic-microservice architecture built with PHP 8.3+ on Swoole, delivered as Docker containers.
## Project Overview
## Commands
Appwrite is a self-hosted Backend-as-a-Service (BaaS) platform that provides developers with a set of APIs and tools to build secure, scalable applications. The project uses a hybrid monolithic-microservice architecture built with PHP, running on Swoole for high performance.
| Command | Purpose |
|---------|---------|
| `docker compose up -d --force-recreate --build` | Build and start all services |
| `docker compose exec appwrite test tests/e2e/Services/[Service]` | Run E2E tests for a service |
| `docker compose exec appwrite test tests/e2e/Services/[Service] --filter=[Method]` | Run a single test method |
| `docker compose exec appwrite test tests/unit/` | Run unit tests |
| `composer format` | Auto-format code (Pint, PSR-12) |
| `composer format <file>` | Format a specific file |
| `composer lint <file>` | Check formatting of a file |
| `composer analyze` | Static analysis (PHPStan level 3) |
| `composer check` | Same as `analyze` |
**Key Technologies:**
- **Backend:** PHP 8.3+, Swoole
- **Libraries:** Utopia PHP
- **Database:** MariaDB, Redis
- **Cache:** Redis
- **Queue:** Redis
- **Containers:** Docker
## Stack
## Development Commands
- PHP 8.3+, Swoole 6.x (async runtime, replaces PHP-FPM)
- Utopia PHP framework (HTTP routing, CLI, DI, queue)
- MongoDB (default), MariaDB, MySQL, PostgreSQL (adapters via utopia-php/database)
- Redis (cache, queue, pub/sub)
- Docker + Traefik (reverse proxy)
- PHPUnit 12, Pint (PSR-12), PHPStan level 3
```bash
# Run Appwrite
docker compose up -d --force-recreate --build
## Project layout
# Run specific test
docker compose exec appwrite test /usr/src/code/tests/e2e/Services/[ServiceName] --filter=[FunctionName]
- **src/Appwrite/Platform/Modules/** -- feature modules (Account, Avatars, Compute, Console, Databases, Functions, Health, Project, Projects, Proxy, Sites, Storage, Teams, Tokens, VCS, Webhooks)
- **src/Appwrite/Platform/Workers/** -- background job workers
- **src/Appwrite/Platform/Tasks/** -- CLI tasks
- **app/init.php** -- bootstrap (registers services, resources, listeners)
- **app/init/** -- configs, constants, locales, models, registers, resources, span, database filters/formats
- **bin/** -- CLI entry points: `worker-*` (14 workers), `schedule-*`, `queue-*`, plus `doctor`, `install`, `migrate`, `realtime`, `upgrade`, `ssl`, `vars`, `maintenance`, `interval`, `specs`, `sdks`, etc.
- **tests/e2e/** -- end-to-end tests per service
- **tests/unit/** -- unit tests
- **public/** -- static assets and generated SDKs
# Format code
composer format
## Module structure
Each module under `src/Appwrite/Platform/Modules/{Name}/` contains:
```
Module.php -- registers all services for the module
Services/Http.php -- registers HTTP endpoints
Services/Workers.php -- registers background workers
Services/Tasks.php -- registers CLI tasks
Http/{Service}/ -- endpoint actions (Create.php, Get.php, Update.php, Delete.php, XList.php)
Workers/ -- worker implementations
Tasks/ -- CLI task implementations
```
## Code Style Guidelines
HTTP endpoint nesting reflects the URL path. Sub-resources get subdirectories. For example, within the Functions module:
`Http/Deployments/Template/Create.php` -> `POST /v1/functions/:functionId/deployments/template`
- Follow [PSR-12](https://www.php-fig.org/psr/psr-12/) coding standard
- Use PSR-4 autoloading
- Strict type declarations where applicable
- Comprehensive PHPDoc comments
File names in Http directories must only be `Get.php`, `Create.php`, `Update.php`, `Delete.php`, or `XList.php`. For non-CRUD operations, model the endpoint as a property update. For example, updating a team membership status lives at `Teams/Http/Memberships/Status/Update.php` (`PATCH /v1/teams/:teamId/memberships/:membershipId/status`).
### Naming Conventions
Register new modules in `src/Appwrite/Platform/Appwrite.php`. Detailed module guide: `src/Appwrite/Platform/AGENTS.md`.
#### `resourceType` Naming Rule
## Action pattern (HTTP endpoints)
When a collection has a combination of `resourceType`, `resourceId`, and/or `resourceInternalId`, the value of `resourceType` MUST always be **plural** - for example: `functions`, `sites`, `deployments`.
Examples:
```php
'resourceType' => 'functions'
'resourceType' => 'sites'
'resourceType' => 'deployments'
class Create extends Action
{
public static function getName(): string { return 'createTeam'; }
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/teams')
->desc('Create team')
->groups(['api', 'teams'])
->label('event', 'teams.[teamId].create')
->label('scope', 'teams.write')
->param('teamId', '', new CustomId(), 'Team ID.')
->param('name', null, new Text(128), 'Team name.')
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(
string $teamId,
string $name,
Response $response,
Database $dbForProject,
Event $queueForEvents,
): void {
// implementation
}
}
```
## Performance Patterns
Common injections: `$response`, `$request`, `$dbForProject`, `$dbForPlatform`, `$user`, `$project`, `$queueForEvents`, `$queueForMails`, `$queueForDeletes`.
### Document Update Optimization
## Conventions
When updating documents, always pass only the changed attributes as a sparse `Document` rather than the full document. This is more efficient because `updateDocument()` internally performs `array_merge($old, $new)`.
- PSR-12 formatting enforced by Pint. PSR-4 autoloading.
- `resourceType` values are always **plural**: `'functions'`, `'sites'`, `'deployments'`.
- When updating documents, pass only changed attributes as a sparse Document:
```php
// correct
$dbForProject->updateDocument('users', $user->getId(), new Document([
'name' => $name,
]));
// incorrect -- passing full document is inefficient
$user->setAttribute('name', $name);
$dbForProject->updateDocument('users', $user->getId(), $user);
```
Exceptions: migrations, `array_merge()` with `getArrayCopy()`, updates where nearly all attributes change, complex nested relationship logic requiring full document state.
- Avoid introducing dependencies outside the `utopia-php` ecosystem.
- Never hardcode credentials -- use environment variables.
- Code changes may require container restart. No central log location -- check relevant containers.
**Correct Pattern:**
```php
// Good: Pass only changed attributes directly
$user = $dbForProject->updateDocument('users', $user->getId(), new Document([
'name' => $name,
'email' => $email,
]));
```
## Patch release process
**Incorrect Pattern:**
```php
$user->setAttribute('name', $name);
$user->setAttribute('email', $email);
For bumping patch versions (e.g., `1.9.0` -> `1.9.1`), follow the checklist in `.claude/skills/patch-release-checklist/SKILL.md`. It covers the 4 files that must be updated, console image bumps, CHANGES.md updates, and common pitfalls to avoid.
// Bad: Passing full document is inefficient
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
```
## Cross-repo context
**Exceptions:**
- Migration files (need full document updates by design)
- Cases already using `array_merge()` with `getArrayCopy()`
- Updates where almost all attributes of the document change at once (sparse update provides little benefit compared to passing the full document)
- Complex nested relationship logic where full document state is required
## Security Considerations
### Critical Security Practices
- **Never hardcode credentials** - Use environment variables
- **Rate limiting** - Respect abuse prevention mechanisms
## Dependencies
Avoid introducing new dependencies other than utopia-php.
## Adding new endpoints
When adding new endpoints, make sure to use modules and follow its patterns. Find instruction in [Modules AGENTS.md](src/Appwrite/Platform/AGENTS.md) file.
## Pull Request Guidelines
### Before Submitting
- Run `composer format`
- Update documentation if adding features
- Add/update tests for your changes
- Check that Docker build succeeds
`docs/specs/authentication.drawio.svg`
## Known Issues and Gotchas
- **Hot Reload:** Code changes require container restart in some cases
- **Logging:** There is no central place for logs, so when debugging, ensure to check all possibly relevant containers
Appwrite is the base server for `appwrite/cloud`. Changes to the Action pattern, module structure, DI system, or response models affect cloud. The `feat-dedicated-db` feature spans cloud, edge, and console.
+120 -87
View File
@@ -1,100 +1,133 @@
# Version 1.8.1
# Version 1.9.0
## What's Changed
### Notable changes
* Add branch deployments support in [#10486](https://github.com/appwrite/appwrite/pull/10486)
* Add TanStack Start sites support in [#10681](https://github.com/appwrite/appwrite/pull/10681)
* Add Next.js standalone support in [#10747](https://github.com/appwrite/appwrite/pull/10747)
* Add Resend integration in [#10690](https://github.com/appwrite/appwrite/pull/10690)
* Add option to enable/disable image transformations per-bucket in [#10722](https://github.com/appwrite/appwrite/pull/10722)
* Add operators support in [#10735](https://github.com/appwrite/appwrite/pull/10735) and [#10800](https://github.com/appwrite/appwrite/pull/10800)
* Add function and sites stats in [#10786](https://github.com/appwrite/appwrite/pull/10786)
* Add disable count feature in [#10668](https://github.com/appwrite/appwrite/pull/10668)
* Add ElevenLabs site template in [#10782](https://github.com/appwrite/appwrite/pull/10782)
* Add suggested environment variables in [#10795](https://github.com/appwrite/appwrite/pull/10795)
* Update GeoDB database in [#10890](https://github.com/appwrite/appwrite/pull/10890)
* Update Flutter default build runtime in [#10807](https://github.com/appwrite/appwrite/pull/10807)
* Upgrade runtimes in [#10804](https://github.com/appwrite/appwrite/pull/10804)
* Add PostgreSQL database adapter in [#9772](https://github.com/appwrite/appwrite/pull/9772) and [#11293](https://github.com/appwrite/appwrite/pull/11293)
* Add MongoDB support in [#11312](https://github.com/appwrite/appwrite/pull/11312)
* Add new webhooks API in [#11033](https://github.com/appwrite/appwrite/pull/11033) and [#11566](https://github.com/appwrite/appwrite/pull/11566)
* Add schedules API endpoints in [#11331](https://github.com/appwrite/appwrite/pull/11331)
* Add project labels in [#11056](https://github.com/appwrite/appwrite/pull/11056) and project status attribute in [#11291](https://github.com/appwrite/appwrite/pull/11291)
* Add resource-based API key structure in [#11003](https://github.com/appwrite/appwrite/pull/11003) with custom ID support in [#11277](https://github.com/appwrite/appwrite/pull/11277) and list queries in [#11278](https://github.com/appwrite/appwrite/pull/11278)
* Add string types (varchar, text, mediumtext, longtext) for attributes in [#11174](https://github.com/appwrite/appwrite/pull/11174)
* Add encrypt parameter to string attribute types in [#11334](https://github.com/appwrite/appwrite/pull/11334)
* Add int64 format support for integer attributes in [#11123](https://github.com/appwrite/appwrite/pull/11123)
* Add collection and row storage size in [#11254](https://github.com/appwrite/appwrite/pull/11254) and [#11069](https://github.com/appwrite/appwrite/pull/11069)
* Add totalSize on list responses in [#11102](https://github.com/appwrite/appwrite/pull/11102)
* Add custom start command for sites and functions in [#10842](https://github.com/appwrite/appwrite/pull/10842)
* Add separate build/runtime specifications in [#10849](https://github.com/appwrite/appwrite/pull/10849)
* Add deployment retention for sites and functions in [#10959](https://github.com/appwrite/appwrite/pull/10959)
* Add auto-delete old deployments in [#10959](https://github.com/appwrite/appwrite/pull/10959)
* Add custom JWT duration in [#11009](https://github.com/appwrite/appwrite/pull/11009)
* Add multiple application domains support in [#10911](https://github.com/appwrite/appwrite/pull/10911)
* Add GraphQL introspection in [#11159](https://github.com/appwrite/appwrite/pull/11159)
* Add realtime query subscriptions in [#11202](https://github.com/appwrite/appwrite/pull/11202) and [#11237](https://github.com/appwrite/appwrite/pull/11237)
* Add realtime metrics for connections, messages, and bandwidth in [#11438](https://github.com/appwrite/appwrite/pull/11438) and [#11488](https://github.com/appwrite/appwrite/pull/11488)
* Add messaging resource migration support in [#11495](https://github.com/appwrite/appwrite/pull/11495)
* Add cached documents list in [#10832](https://github.com/appwrite/appwrite/pull/10832)
* Add project queries support in [#10990](https://github.com/appwrite/appwrite/pull/10990)
* Add batch document creation in [#10894](https://github.com/appwrite/appwrite/pull/10894)
* Add async screenshots in [#11110](https://github.com/appwrite/appwrite/pull/11110)
* Add new file parameters (encryption, compression) in [#11135](https://github.com/appwrite/appwrite/pull/11135)
* Add new site templates in [#10031](https://github.com/appwrite/appwrite/pull/10031)
* Add VCS repository authorized field in [#11421](https://github.com/appwrite/appwrite/pull/11421)
* Add trusted console projects in [#11248](https://github.com/appwrite/appwrite/pull/11248)
### Refactoring
* Refactor to Utopia Platform modules architecture in [#11035](https://github.com/appwrite/appwrite/pull/11035), [#11049](https://github.com/appwrite/appwrite/pull/11049), [#11057](https://github.com/appwrite/appwrite/pull/11057), [#11103](https://github.com/appwrite/appwrite/pull/11103), [#11208](https://github.com/appwrite/appwrite/pull/11208), and [#11398](https://github.com/appwrite/appwrite/pull/11398)
* Refactor auth to single instance in [#10872](https://github.com/appwrite/appwrite/pull/10872) and [#11130](https://github.com/appwrite/appwrite/pull/11130)
* Refactor usage metrics to stateless publisher pattern in [#11449](https://github.com/appwrite/appwrite/pull/11449)
* Refactor messaging and queue in [#10961](https://github.com/appwrite/appwrite/pull/10961)
* Refactor functions schedule in [#10913](https://github.com/appwrite/appwrite/pull/10913)
* Refactor make Bus dispatch synchronous in [#11449](https://github.com/appwrite/appwrite/pull/11449)
* Remove proxy container in [#11039](https://github.com/appwrite/appwrite/pull/11039)
### Performance
* Optimize updateDocument() calls to use sparse documents in [#11465](https://github.com/appwrite/appwrite/pull/11465)
* Optimize Dockerfile in [#10947](https://github.com/appwrite/appwrite/pull/10947)
* Improve domain caching in [#11346](https://github.com/appwrite/appwrite/pull/11346)
* Improve memory usage in [#11345](https://github.com/appwrite/appwrite/pull/11345)
* Fix memory leak in [#11067](https://github.com/appwrite/appwrite/pull/11067) and [#11241](https://github.com/appwrite/appwrite/pull/11241)
* Improve realtime performance in [#11251](https://github.com/appwrite/appwrite/pull/11251)
* Enable SMTP keep-alive to reuse connections across mail jobs in [#11496](https://github.com/appwrite/appwrite/pull/11496)
### Fixes
* Fix duplicate document error while creating file in [#10891](https://github.com/appwrite/appwrite/pull/10891)
* Fix "Update external deployment (authorize)" throwing 500 error due to invalid query in [#10888](https://github.com/appwrite/appwrite/pull/10888)
* Fix error setting user password in [#10889](https://github.com/appwrite/appwrite/pull/10889)
* Fix error generating email MFA challenges in [#10884](https://github.com/appwrite/appwrite/pull/10884)
* Fix file token expiry in [#10877](https://github.com/appwrite/appwrite/pull/10877)
* Fix TanStack Nitro default in [#10860](https://github.com/appwrite/appwrite/pull/10860)
* Fix TanStack builds in [#10767](https://github.com/appwrite/appwrite/pull/10767)
* Fix nullable validation in [#10819](https://github.com/appwrite/appwrite/pull/10819) and [#10778](https://github.com/appwrite/appwrite/pull/10778)
* Fix WebP library in [#10738](https://github.com/appwrite/appwrite/pull/10738)
* Fix batch writes in [#10812](https://github.com/appwrite/appwrite/pull/10812)
* Fix error handler error in [#10719](https://github.com/appwrite/appwrite/pull/10719)
* Fix Next 16 compatibility in [#10713](https://github.com/appwrite/appwrite/pull/10713)
* Fix stats usage memory leak in [#10683](https://github.com/appwrite/appwrite/pull/10683)
* Fix author URL in template deployments in [#10535](https://github.com/appwrite/appwrite/pull/10535)
* Fix VCS lock deletion in [#10691](https://github.com/appwrite/appwrite/pull/10691)
* Fix blocked user/resource errors from 401 to 403 in [#11469](https://github.com/appwrite/appwrite/pull/11469)
* Fix OAuth for custom domains in [#10967](https://github.com/appwrite/appwrite/pull/10967) and [#11269](https://github.com/appwrite/appwrite/pull/11269)
* Fix OAuth redirect custom scheme in [#11292](https://github.com/appwrite/appwrite/pull/11292)
* Fix OAuth verified emails in [#10986](https://github.com/appwrite/appwrite/pull/10986)
* Fix MFA recovery code validation in [#10925](https://github.com/appwrite/appwrite/pull/10925)
* Fix users allow updating phone number to empty in [#11521](https://github.com/appwrite/appwrite/pull/11521)
* Fix users optional name error in [#11413](https://github.com/appwrite/appwrite/pull/11413)
* Fix file permissions in [#11026](https://github.com/appwrite/appwrite/pull/11026)
* Fix bulk insert webhook validation in [#11022](https://github.com/appwrite/appwrite/pull/11022)
* Fix execution status update in [#11134](https://github.com/appwrite/appwrite/pull/11134)
* Fix execution timeout status in [#11400](https://github.com/appwrite/appwrite/pull/11400)
* Fix CORS wildcard in [#10956](https://github.com/appwrite/appwrite/pull/10956)
* Fix preflight requests in [#10943](https://github.com/appwrite/appwrite/pull/10943)
* Fix SMTP auth check in [#10939](https://github.com/appwrite/appwrite/pull/10939)
* Fix scheduled executions trigger in [#10922](https://github.com/appwrite/appwrite/pull/10922)
* Fix schedule executions bug in [#10916](https://github.com/appwrite/appwrite/pull/10916)
* Fix deployment enum missing canceled value in [#11179](https://github.com/appwrite/appwrite/pull/11179)
* Fix invalid chunk total in [#11270](https://github.com/appwrite/appwrite/pull/11270)
* Fix sites domains in [#11240](https://github.com/appwrite/appwrite/pull/11240) and [#11355](https://github.com/appwrite/appwrite/pull/11355)
* Fix rule domains in [#11355](https://github.com/appwrite/appwrite/pull/11355) and [#11276](https://github.com/appwrite/appwrite/pull/11276)
* Fix rules deletion in [#11575](https://github.com/appwrite/appwrite/pull/11575)
* Fix VCS template flow in [#11275](https://github.com/appwrite/appwrite/pull/11275)
* Fix VCS comment empty in [#11490](https://github.com/appwrite/appwrite/pull/11490)
* Fix DSN VCS error in [#11364](https://github.com/appwrite/appwrite/pull/11364)
* Fix email URL params encoding in [#11369](https://github.com/appwrite/appwrite/pull/11369)
* Fix missing email warning in [#11378](https://github.com/appwrite/appwrite/pull/11378)
* Fix race condition in builds worker in [#11336](https://github.com/appwrite/appwrite/pull/11336)
* Fix realtime regions in [#11414](https://github.com/appwrite/appwrite/pull/11414)
* Fix realtime errors in [#11573](https://github.com/appwrite/appwrite/pull/11573)
* Fix realtime TablesDB channels in [#11404](https://github.com/appwrite/appwrite/pull/11404) and [#11430](https://github.com/appwrite/appwrite/pull/11430)
* Fix database shared table reconciliation in [#11578](https://github.com/appwrite/appwrite/pull/11578)
* Fix PostgreSQL race condition in shared mode project creation in [#11536](https://github.com/appwrite/appwrite/pull/11536)
* Fix compression enabled env in [#11171](https://github.com/appwrite/appwrite/pull/11171)
* Fix deletes bug in [#10965](https://github.com/appwrite/appwrite/pull/10965)
* Fix devkey scopes in [#10984](https://github.com/appwrite/appwrite/pull/10984)
* Fix phone auth limit in [#11143](https://github.com/appwrite/appwrite/pull/11143)
* Fix relationship document ID validation in [#11193](https://github.com/appwrite/appwrite/pull/11193)
* Fix stale project overwrites OAuth in [#11461](https://github.com/appwrite/appwrite/pull/11461)
* Fix storage health error swallowing in [#11492](https://github.com/appwrite/appwrite/pull/11492)
* Fix Origin validator type error in [#11297](https://github.com/appwrite/appwrite/pull/11297)
* Fix getScreenshot image format in [#11017](https://github.com/appwrite/appwrite/pull/11017)
* Fix migration error handling in [#11457](https://github.com/appwrite/appwrite/pull/11457)
* Fix deprecation warnings in [#11227](https://github.com/appwrite/appwrite/pull/11227)
### Installer
* New installer UI in [#11175](https://github.com/appwrite/appwrite/pull/11175) and [#11247](https://github.com/appwrite/appwrite/pull/11247)
### Miscellaneous
* Add CSV export functionality in [#10546](https://github.com/appwrite/appwrite/pull/10546), [#10750](https://github.com/appwrite/appwrite/pull/10750), [#10813](https://github.com/appwrite/appwrite/pull/10813), and [#10847](https://github.com/appwrite/appwrite/pull/10847)
* Add JWT disposition in [#10867](https://github.com/appwrite/appwrite/pull/10867)
* Add screenshots endpoint in [#10675](https://github.com/appwrite/appwrite/pull/10675)
* Add screenshot endpoint stats in [#10706](https://github.com/appwrite/appwrite/pull/10706)
* Add users attributes in [#10688](https://github.com/appwrite/appwrite/pull/10688)
* Add max build duration environment variable in [#10674](https://github.com/appwrite/appwrite/pull/10674)
* Add custom realtime logger in [#10871](https://github.com/appwrite/appwrite/pull/10871)
* Add logs in [#10869](https://github.com/appwrite/appwrite/pull/10869)
* Improve MFA docs endpoint order in [#10793](https://github.com/appwrite/appwrite/pull/10793)
* Auth refactor in [#10758](https://github.com/appwrite/appwrite/pull/10758), [#10837](https://github.com/appwrite/appwrite/pull/10837), [#10682](https://github.com/appwrite/appwrite/pull/10682), and [#10667](https://github.com/appwrite/appwrite/pull/10667)
* Bump assistant to 0.8.4 in [#10887](https://github.com/appwrite/appwrite/pull/10887)
* Bump database to 3.1.5 in [#10766](https://github.com/appwrite/appwrite/pull/10766)
* Bump Utopia DNS in [#10761](https://github.com/appwrite/appwrite/pull/10761)
* Update domains to 0.8.3 in [#10658](https://github.com/appwrite/appwrite/pull/10658)
* Update domains to 0.9.1 in [#10678](https://github.com/appwrite/appwrite/pull/10678)
* Update Apple Swift to 13.3.0 in [#10679](https://github.com/appwrite/appwrite/pull/10679)
* Update Apple Swift in [#10663](https://github.com/appwrite/appwrite/pull/10663)
* Update CLI to 10.2.2 in [#10672](https://github.com/appwrite/appwrite/pull/10672)
* Update to CLI 12.0.0 in [#10853](https://github.com/appwrite/appwrite/pull/10853)
* Update docs examples to use Permission class in [#10707](https://github.com/appwrite/appwrite/pull/10707)
* Update SDK examples docs in [#10855](https://github.com/appwrite/appwrite/pull/10855)
* Release Python SDK in [#10762](https://github.com/appwrite/appwrite/pull/10762)
* Release Flutter 20.3.2 in [#10838](https://github.com/appwrite/appwrite/pull/10838)
* Release Flutter/Dart add screenshot examples in [#10811](https://github.com/appwrite/appwrite/pull/10811)
* Release PHP CLI in [#10791](https://github.com/appwrite/appwrite/pull/10791)
* Release SDKs in [#10817](https://github.com/appwrite/appwrite/pull/10817)
* Update SDKs in [#10694](https://github.com/appwrite/appwrite/pull/10694), [#10729](https://github.com/appwrite/appwrite/pull/10729), and [#10744](https://github.com/appwrite/appwrite/pull/10744)
* Update SDK generator in [#10743](https://github.com/appwrite/appwrite/pull/10743)
* Update database in [#10664](https://github.com/appwrite/appwrite/pull/10664)
* Update README file in [#10763](https://github.com/appwrite/appwrite/pull/10763)
* SDK release documentation in [#10745](https://github.com/appwrite/appwrite/pull/10745)
* SDK release runtime config in [#10765](https://github.com/appwrite/appwrite/pull/10765)
* Sync specs in [#10789](https://github.com/appwrite/appwrite/pull/10789)
* Sync 1.8.0 in [#10677](https://github.com/appwrite/appwrite/pull/10677)
* Add workflow for issue triage in [#10718](https://github.com/appwrite/appwrite/pull/10718)
* Add issue auto-labeler in [#10700](https://github.com/appwrite/appwrite/pull/10700)
* Add AI moderator repo in [#10717](https://github.com/appwrite/appwrite/pull/10717)
* Browser bump in [#10850](https://github.com/appwrite/appwrite/pull/10850)
* Template type enum override in [#10848](https://github.com/appwrite/appwrite/pull/10848)
* VCS reference type in [#10852](https://github.com/appwrite/appwrite/pull/10852)
* Index scope description in [#10851](https://github.com/appwrite/appwrite/pull/10851)
* Config for environment in [#10833](https://github.com/appwrite/appwrite/pull/10833)
* Format instance in [#10830](https://github.com/appwrite/appwrite/pull/10830)
* Replace sleep in webhooks service in [#10656](https://github.com/appwrite/appwrite/pull/10656)
* Update email composer in [#10720](https://github.com/appwrite/appwrite/pull/10720)
* Update facts on GitHub sites and functions in [#10593](https://github.com/appwrite/appwrite/pull/10593) and [#10771](https://github.com/appwrite/appwrite/pull/10771)
* Fix wrong user type in [#10875](https://github.com/appwrite/appwrite/pull/10875)
* Fix limit and offset computation in [#10880](https://github.com/appwrite/appwrite/pull/10880)
* Fix enum examples in [#10828](https://github.com/appwrite/appwrite/pull/10828)
* Fix response models multi-methods in [#10815](https://github.com/appwrite/appwrite/pull/10815)
* Fix undefined variable in [#10654](https://github.com/appwrite/appwrite/pull/10654)
* Fix undefined sequence in [#10652](https://github.com/appwrite/appwrite/pull/10652)
* Fix description in [#10702](https://github.com/appwrite/appwrite/pull/10702)
* Fix warning in builds worker in [#10705](https://github.com/appwrite/appwrite/pull/10705)
* Fix sites create deployment docs in [#10566](https://github.com/appwrite/appwrite/pull/10566)
* Fix test dependencies projects in [#10655](https://github.com/appwrite/appwrite/pull/10655)
* Fix list sites test in [#10726](https://github.com/appwrite/appwrite/pull/10726)
* Add audits upgrade in [#10953](https://github.com/appwrite/appwrite/pull/10953)
* Add graceful workers shutdown in [#11104](https://github.com/appwrite/appwrite/pull/11104)
* Add pool resilience in [#11139](https://github.com/appwrite/appwrite/pull/11139)
* Add function queue job TTL in [#11226](https://github.com/appwrite/appwrite/pull/11226)
* Add cleanup stale executions in [#11146](https://github.com/appwrite/appwrite/pull/11146)
* Add success abuse reset in [#11085](https://github.com/appwrite/appwrite/pull/11085)
* Add SMTP connection validation in [#11079](https://github.com/appwrite/appwrite/pull/11079)
* Add allow custom email sender in [#10945](https://github.com/appwrite/appwrite/pull/10945)
* Add array domains env support in [#11213](https://github.com/appwrite/appwrite/pull/11213)
* Add file create after success hook in [#11054](https://github.com/appwrite/appwrite/pull/11054)
* Add delete subscribers in [#11115](https://github.com/appwrite/appwrite/pull/11115)
* Add cursor plugin in [#11371](https://github.com/appwrite/appwrite/pull/11371)
* Add observability spans in [#11320](https://github.com/appwrite/appwrite/pull/11320), [#11306](https://github.com/appwrite/appwrite/pull/11306), and [#11228](https://github.com/appwrite/appwrite/pull/11228)
* Upgrade PHPStan to v2 with full codebase coverage in [#11550](https://github.com/appwrite/appwrite/pull/11550)
* Upgrade Traefik in [#11265](https://github.com/appwrite/appwrite/pull/11265)
* Upgrade utopia-php/queue in [#11239](https://github.com/appwrite/appwrite/pull/11239)
* Upgrade spomky-labs/otphp in [#11263](https://github.com/appwrite/appwrite/pull/11263)
* Bump utopia-php/database to stable 5.3.15 in [#11573](https://github.com/appwrite/appwrite/pull/11573)
* Bump utopia-php/migration to 1.6.3 in [#11443](https://github.com/appwrite/appwrite/pull/11443)
* Consolidate CI workflows in [#11531](https://github.com/appwrite/appwrite/pull/11531) and [#11551](https://github.com/appwrite/appwrite/pull/11551)
* Hide deprecated methods from docs in [#10933](https://github.com/appwrite/appwrite/pull/10933)
* Deprecate project-level attributes in [#11203](https://github.com/appwrite/appwrite/pull/11203)
# Version 1.8.0
@@ -859,7 +892,7 @@
* Unset index length by @fogelito in https://github.com/appwrite/appwrite/pull/8978
* Update base to 0.9.5 by @basert in https://github.com/appwrite/appwrite/pull/9005
* Sync main into 1.6.x by @TorstenDittmann in https://github.com/appwrite/appwrite/pull/9011
* Improved shared tables V2 by @abnegate in https://github.com/appwrite/appwrite/pull/9013
* Improved shared tables by @abnegate in https://github.com/appwrite/appwrite/pull/9013
* Ensure backwards compatibility for 1.6.x by @christyjacob4 in https://github.com/appwrite/appwrite/pull/9018
# Version 1.6.0
+7 -2
View File
@@ -12,7 +12,7 @@ RUN composer install --ignore-platform-reqs --optimize-autoloader \
--no-plugins --no-scripts --prefer-dist \
`if [ "$TESTING" != "true" ]; then echo "--no-dev"; fi`
FROM appwrite/base:1.0.1 AS base
FROM appwrite/base:1.2.1 AS base
LABEL maintainer="team@appwrite.io"
@@ -24,6 +24,10 @@ ENV _APP_VERSION=$VERSION \
_APP_HOME=https://appwrite.io
RUN \
if [ "$DEBUG" != "true" ]; then \
rm -f /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini && \
rm -f /usr/local/lib/php/extensions/no-debug-non-zts-*/xdebug.so; \
fi && \
if [ "$DEBUG" == "true" ]; then \
apk add boost boost-dev; \
fi
@@ -100,7 +104,8 @@ RUN mkdir -p /etc/letsencrypt/live/ && chmod -Rf 755 /etc/letsencrypt/live/
FROM base AS production
RUN rm -rf /usr/src/code/app/config/specs && \
rm -f /usr/local/lib/php/extensions/no-debug-non-zts-20240924/xdebug.so && \
rm -f /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini /usr/local/etc/php/conf.d/xdebug.ini && \
rm -f /usr/local/lib/php/extensions/no-debug-non-zts-*/xdebug.so && \
find /usr -name '*.a' -delete 2>/dev/null || true && \
find /usr -type d -name '__pycache__' -exec rm -rf {} + 2>/dev/null || true && \
find /usr -name '*.pyc' -delete 2>/dev/null || true
+3 -3
View File
@@ -72,7 +72,7 @@ docker run -it --rm \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
--entrypoint="install" \
appwrite/appwrite:1.8.1
appwrite/appwrite:1.9.0
```
### Windows
@@ -84,7 +84,7 @@ docker run -it --rm ^
--volume //var/run/docker.sock:/var/run/docker.sock ^
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
--entrypoint="install" ^
appwrite/appwrite:1.8.1
appwrite/appwrite:1.9.0
```
#### PowerShell
@@ -94,7 +94,7 @@ docker run -it --rm `
--volume /var/run/docker.sock:/var/run/docker.sock `
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw `
--entrypoint="install" `
appwrite/appwrite:1.8.1
appwrite/appwrite:1.9.0
```
运行后,可以在浏览器上访问 http://localhost 找到 Appwrite 控制台。在非 Linux 的本机主机上完成安装后,服务器可能需要几分钟才能启动。
+46 -66
View File
@@ -1,43 +1,28 @@
> We just announced DB operators for Appwrite Databases - [Learn more](https://appwrite.io/blog/post/announcing-db-operators)
> Appwrite Cloud is now Generally Available - [Learn more](https://appwrite.io/cloud-ga)
> [Get started with Appwrite](https://apwr.dev/appcloud)
<img width="1920" height="1080" alt="image" src="https://github.com/user-attachments/assets/55a81268-4ecc-46cd-bdf5-73f7e8662fee" />
<br />
<p align="center">
<a href="https://appwrite.io" target="_blank"><img src="./public/images/banner.png" alt="Appwrite banner, with logo and text saying "The Developer's Cloud"></a>
<br />
<br />
<b>Appwrite is a best-in-class, developer-first platform that gives builders everything they need to create scalable, stable, and production-ready software, fast.</b>
<h1>Appwrite</h1>
<b>Appwrite is an open-source, all-in-one development platform. Use built-in backend infrastructure and web hosting, all from a single place.</b>
<br />
<br />
</p>
<!-- [![Build Status](https://img.shields.io/travis/com/appwrite/appwrite?style=flat-square)](https://travis-ci.com/appwrite/appwrite) -->
[![We're Hiring label](https://img.shields.io/static/v1?label=We're&message=Hiring&color=blue&style=flat-square)](https://appwrite.io/company/careers)
[![Hacktoberfest label](https://img.shields.io/static/v1?label=hacktoberfest&message=ready&color=191120&style=flat-square)](https://hacktoberfest.appwrite.io)
[![Discord label](https://img.shields.io/discord/564160730845151244?label=discord&style=flat-square)](https://appwrite.io/discord?r=Github)
[![Build Status label](https://img.shields.io/github/actions/workflow/status/appwrite/appwrite/tests.yml?branch=master&label=tests&style=flat-square)](https://github.com/appwrite/appwrite/actions)
[![X Account label](https://img.shields.io/twitter/follow/appwrite?color=00acee&label=twitter&style=flat-square)](https://twitter.com/appwrite)
<!-- [![Docker Pulls](https://img.shields.io/docker/pulls/appwrite/appwrite?color=f02e65&style=flat-square)](https://hub.docker.com/r/appwrite/appwrite) -->
<!-- [![Translate](https://img.shields.io/badge/translate-f02e65?style=flat-square)](docs/tutorials/add-translations.md) -->
<!-- [![Swag Store](https://img.shields.io/badge/swag%20store-f02e65?style=flat-square)](https://store.appwrite.io) -->
[![Discord](https://img.shields.io/badge/chat-5865F2?style=flat-square&logo=discord&logoColor=white)](https://appwrite.io/discord)
[![X](https://img.shields.io/badge/follow-000000?style=flat-square&logo=x&logoColor=white)](https://x.com/appwrite)
[![Appwrite Cloud](https://img.shields.io/badge/Cloud-F02E65?style=flat-square&logo=icloud&logoColor=white)](https://cloud.appwrite.io)
English | [简体中文](README-CN.md)
Appwrite is an end-to-end platform for building Web, Mobile, Native, or Backend apps, packaged as a set of Docker microservices. It includes both a backend server and a fully integrated hosting solution for deploying static and server-side rendered frontends. Appwrite abstracts the complexity and repetitiveness required to build modern apps from scratch and allows you to build secure, full-stack applications faster.
Appwrite is an open-source development platform for building web, mobile, and AI applications. It brings together backend infrastructure and web hosting in one place, so teams can build, ship, and scale without stitching together a fragmented stack. Appwrite is available as a managed cloud platform and can also be self-hosted on infrastructure you control.
Using Appwrite, you can easily integrate your app with user authentication and multiple sign-in methods, a database for storing and querying users and team data, storage and file management, image manipulation, Cloud Functions, messaging, and [more services](https://appwrite.io/docs).
With Appwrite, you can add authentication, databases, storage, functions, messaging, realtime capabilities, and integrated web app hosting through Sites. It is designed to reduce the repetitive backend work required to launch modern products while giving developers secure primitives and flexible APIs to build production-ready applications faster.
![Appwrite project dashboard showing various Appwrite features](public/images/github.png)
Find out more at: [https://appwrite.io](https://appwrite.io).
Find out more at [https://appwrite.io](https://appwrite.io).
Table of Contents:
- [Products](#products)
- [Installation \& Setup](#installation--setup)
- [Self-Hosting](#self-hosting)
- [Unix](#unix)
@@ -47,17 +32,31 @@ Table of Contents:
- [Upgrade from an Older Version](#upgrade-from-an-older-version)
- [One-Click Setups](#one-click-setups)
- [Getting Started](#getting-started)
- [Products](#products)
- [SDKs](#sdks)
- [Client](#client)
- [Server](#server)
- [Community](#community)
- [Architecture](#architecture)
- [Contributing](#contributing)
- [Security](#security)
- [Follow Us](#follow-us)
- [License](#license)
## Products
- **[Appwrite Auth](https://appwrite.io/docs/products/auth)** - Secure user authentication with multiple login methods including email/password, SMS, OAuth, anonymous sessions, and magic links. Includes session management, multi-factor authentication, and user verification flows.
- **[Appwrite Databases](https://appwrite.io/docs/products/databases)** - Scalable structured data storage with support for databases, tables, and rows. Includes querying, pagination, indexing, and relationships to model complex application data.
- **[Appwrite Storage](https://appwrite.io/docs/products/storage)** - Secure file storage with support for uploads, downloads, encryption, compression, and file transformations for media and assets.
- **[Appwrite Functions](https://appwrite.io/docs/products/functions)** - Serverless compute platform to run custom backend logic in isolated runtimes, triggered by events or scheduled jobs.15 runtimes supported.
- **[Appwrite Messaging](https://appwrite.io/docs/products/messaging)** - Multi-channel messaging system for sending emails, SMS, and push notifications to users for engagement, alerts, and transactional workflows.
- **[Appwrite Sites](https://appwrite.io/docs/products/sites)** - Integrated hosting platform to deploy and scale web applications with support for custom domains, SSR, and seamless backend integration. Git integration and previews are supported.
## Installation & Setup
The easiest way to get started with Appwrite is by [signing up for Appwrite Cloud](https://cloud.appwrite.io/). While Appwrite Cloud is in public beta, you can build with Appwrite completely free, and we won't collect your credit card information.
@@ -72,10 +71,11 @@ Before running the installation command, make sure you have [Docker](https://www
```bash
docker run -it --rm \
--publish 20080:20080 \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
--entrypoint="install" \
appwrite/appwrite:1.8.1
appwrite/appwrite:1.9.0
```
### Windows
@@ -84,20 +84,22 @@ docker run -it --rm \
```cmd
docker run -it --rm ^
--publish 20080:20080 ^
--volume //var/run/docker.sock:/var/run/docker.sock ^
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
--entrypoint="install" ^
appwrite/appwrite:1.8.1
appwrite/appwrite:1.9.0
```
#### PowerShell
```powershell
docker run -it --rm `
--publish 20080:20080 `
--volume /var/run/docker.sock:/var/run/docker.sock `
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw `
--entrypoint="install" `
appwrite/appwrite:1.8.1
appwrite/appwrite:1.9.0
```
Once the Docker installation is complete, go to http://localhost to access the Appwrite console from your browser. Please note that on non-Linux native hosts, the server might take a few minutes to start after completing the installation.
@@ -165,51 +167,29 @@ Getting started with Appwrite is as easy as creating a new project, choosing you
| | [Quick start for Kotlin](https://appwrite.io/docs/quick-starts/kotlin) |
| | [Quick start for Swift](https://appwrite.io/docs/quick-starts/swift) |
### Products
- [**Account**](https://appwrite.io/docs/references/cloud/client-web/account) - Manage current user authentication and account. Track and manage the user sessions, devices, sign-in methods, and security logs.
- [**Users**](https://appwrite.io/docs/server/users) - Manage and list all project users when building backend integrations with Server SDKs.
- [**Teams**](https://appwrite.io/docs/references/cloud/client-web/teams) - Manage and group users in teams. Manage memberships, invites, and user roles within a team.
- [**Databases**](https://appwrite.io/docs/references/cloud/client-web/databases) - Manage databases, collections, and documents. Read, create, update, and delete documents and filter lists of document collections using advanced filters.
- [**Storage**](https://appwrite.io/docs/references/cloud/client-web/storage) - Manage storage files. Read, create, delete, and preview files. Manipulate the preview of your files to perfectly fit your app. All files are scanned by ClamAV and stored in a secure and encrypted way.
- [**Functions**](https://appwrite.io/docs/references/cloud/server-nodejs/functions) - Customize your Appwrite project by executing your custom code in a secure, isolated environment. You can trigger your code on any Appwrite system event either manually or using a CRON schedule.
- [**Messaging**](https://appwrite.io/docs/references/cloud/client-web/messaging) - Communicate with your users through push notifications, emails, and SMS text messages using Appwrite Messaging.
- [**Realtime**](https://appwrite.io/docs/realtime) - Listen to real-time events for any of your Appwrite services including users, storage, functions, databases, and more.
- [**Locale**](https://appwrite.io/docs/references/cloud/client-web/locale) - Track your user's location and manage your app locale-based data.
- [**Avatars**](https://appwrite.io/docs/references/cloud/client-web/avatars) - Manage your users' avatars, countries' flags, browser icons, and credit card symbols. Generate QR codes from links or plaintext strings.
- [**MCP**](https://appwrite.io/docs/tooling/mcp) - Use Appwrite's Model Context Protocol (MCP) server to allow LLMs and AI tools like Claude Desktop, Cursor, and Windsurf Editor to directly interact with your Appwrite project through natural language.
- [**Sites**](https://appwrite.io/docs/products/sites) - Develop, deploy, and scale your web applications directly from Appwrite, alongside your backend.
For the complete API documentation, visit [https://appwrite.io/docs](https://appwrite.io/docs). For more tutorials, news and announcements check out our [blog](https://medium.com/appwrite-io) and [Discord Server](https://discord.gg/GSeTUeA).
### SDKs
Below is a list of currently supported platforms and languages. If you would like to help us add support to your platform of choice, you can go over to our [SDK Generator](https://github.com/appwrite/sdk-generator) project and view our [contribution guide](https://github.com/appwrite/sdk-generator/blob/master/CONTRIBUTING.md).
#### Client
- :white_check_mark: &nbsp; [Web](https://github.com/appwrite/sdk-for-web) (Maintained by the Appwrite Team)
- :white_check_mark: &nbsp; [Flutter](https://github.com/appwrite/sdk-for-flutter) (Maintained by the Appwrite Team)
- :white_check_mark: &nbsp; [Apple](https://github.com/appwrite/sdk-for-apple) (Maintained by the Appwrite Team)
- :white_check_mark: &nbsp; [Android](https://github.com/appwrite/sdk-for-android) (Maintained by the Appwrite Team)
- :white_check_mark: &nbsp; [React Native](https://github.com/appwrite/sdk-for-react-native) - **Beta** (Maintained by the Appwrite Team)
- :white_check_mark: &nbsp; [Web](https://github.com/appwrite/sdk-for-web)
- :white_check_mark: &nbsp; [Flutter](https://github.com/appwrite/sdk-for-flutter)
- :white_check_mark: &nbsp; [Apple](https://github.com/appwrite/sdk-for-apple)
- :white_check_mark: &nbsp; [Android](https://github.com/appwrite/sdk-for-android)
- :white_check_mark: &nbsp; [React Native](https://github.com/appwrite/sdk-for-react-native)
#### Server
- :white_check_mark: &nbsp; [NodeJS](https://github.com/appwrite/sdk-for-node) (Maintained by the Appwrite Team)
- :white_check_mark: &nbsp; [PHP](https://github.com/appwrite/sdk-for-php) (Maintained by the Appwrite Team)
- :white_check_mark: &nbsp; [Dart](https://github.com/appwrite/sdk-for-dart) (Maintained by the Appwrite Team)
- :white_check_mark: &nbsp; [Deno](https://github.com/appwrite/sdk-for-deno) (Maintained by the Appwrite Team)
- :white_check_mark: &nbsp; [Ruby](https://github.com/appwrite/sdk-for-ruby) (Maintained by the Appwrite Team)
- :white_check_mark: &nbsp; [Python](https://github.com/appwrite/sdk-for-python) (Maintained by the Appwrite Team)
- :white_check_mark: &nbsp; [Kotlin](https://github.com/appwrite/sdk-for-kotlin) (Maintained by the Appwrite Team)
- :white_check_mark: &nbsp; [Swift](https://github.com/appwrite/sdk-for-swift) (Maintained by the Appwrite Team)
- :white_check_mark: &nbsp; [.NET](https://github.com/appwrite/sdk-for-dotnet) - **Beta** (Maintained by the Appwrite Team)
#### Community
- :white_check_mark: &nbsp; [Appcelerator Titanium](https://github.com/m1ga/ti.appwrite) (Maintained by [Michael Gangolf](https://github.com/m1ga/))
- :white_check_mark: &nbsp; [Godot Engine](https://github.com/GodotNuts/appwrite-sdk) (Maintained by [fenix-hub @GodotNuts](https://github.com/fenix-hub))
- :white_check_mark: &nbsp; [NodeJS](https://github.com/appwrite/sdk-for-node)
- :white_check_mark: &nbsp; [PHP](https://github.com/appwrite/sdk-for-php)
- :white_check_mark: &nbsp; [Dart](https://github.com/appwrite/sdk-for-dart)
- :white_check_mark: &nbsp; [Deno](https://github.com/appwrite/sdk-for-deno)
- :white_check_mark: &nbsp; [Ruby](https://github.com/appwrite/sdk-for-ruby)
- :white_check_mark: &nbsp; [Python](https://github.com/appwrite/sdk-for-python)
- :white_check_mark: &nbsp; [Kotlin](https://github.com/appwrite/sdk-for-kotlin)
- :white_check_mark: &nbsp; [Swift](https://github.com/appwrite/sdk-for-swift)
- :white_check_mark: &nbsp; [.NET](https://github.com/appwrite/sdk-for-dotnet)
Looking for more SDKs? - Help us by contributing a pull request to our [SDK Generator](https://github.com/appwrite/sdk-generator)!
+47 -45
View File
@@ -2,12 +2,12 @@
require_once __DIR__ . '/init.php';
use Appwrite\Event\Certificate;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Publisher\Certificate as CertificatePublisher;
use Appwrite\Event\Publisher\StatsResources as StatsResourcesPublisher;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Event\StatsResources;
use Appwrite\Platform\Appwrite;
use Appwrite\Runtimes\Runtimes;
use Appwrite\Usage\Context as UsageContext;
@@ -18,13 +18,15 @@ use Swoole\Timer;
use Utopia\Cache\Adapter\Pool as CachePool;
use Utopia\Cache\Adapter\Sharding;
use Utopia\Cache\Cache;
use Utopia\CLI\Adapters\Generic;
use Utopia\CLI\CLI;
use Utopia\Config\Config;
use Utopia\Console;
use Utopia\Database\Adapter\Pool as DatabasePool;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\DI\Dependency;
use Utopia\DI\Container;
use Utopia\DSN\DSN;
use Utopia\Logger\Log;
use Utopia\Platform\Service;
@@ -47,7 +49,7 @@ require_once __DIR__ . '/controllers/general.php';
global $register;
$platform = new Appwrite();
$args = $platform->getEnv('argv');
$args = $_SERVER['argv'] ?? [];
\array_shift($args);
if (! isset($args[0])) {
@@ -56,21 +58,15 @@ if (! isset($args[0])) {
}
$taskName = $args[0];
$container = new Container();
$cli = new CLI(new Generic(), $_SERVER['argv'] ?? [], $container);
$platform->setCli($cli);
$platform->init(Service::TYPE_TASK);
$cli = $platform->getCli();
$setResource = function (string $name, callable $callback, array $injections = []) use ($cli) {
$dependency = new Dependency();
$dependency->setName($name)->setCallback($callback);
foreach ($injections as $injection) {
$dependency->inject($injection);
}
$cli->setResource($dependency);
};
$container->set('register', fn () => $register, []);
$setResource('register', fn () => $register, []);
$setResource('cache', function ($pools) {
$container->set('cache', function ($pools) {
$list = Config::getParam('pools-cache', []);
$adapters = [];
@@ -81,18 +77,18 @@ $setResource('cache', function ($pools) {
return new Cache(new Sharding($adapters));
}, ['pools']);
$setResource('pools', function (Registry $register) {
$container->set('pools', function (Registry $register) {
return $register->get('pools');
}, ['register']);
$setResource('authorization', function () {
$container->set('authorization', function () {
$authorization = new Authorization();
$authorization->disable();
return $authorization;
}, []);
$setResource('dbForPlatform', function ($pools, $cache, $authorization) {
$container->set('dbForPlatform', function ($pools, $cache, $authorization) {
$sleep = 3;
$maxAttempts = 5;
$attempts = 0;
@@ -135,17 +131,17 @@ $setResource('dbForPlatform', function ($pools, $cache, $authorization) {
return $dbForPlatform;
}, ['pools', 'cache', 'authorization']);
$setResource('console', function () {
$container->set('console', function () {
return new Document(Config::getParam('console'));
}, []);
$setResource(
$container->set(
'isResourceBlocked',
fn () => fn (Document $project, string $resourceType, ?string $resourceId) => false,
[]
);
$setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache, $authorization) {
$container->set('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache, $authorization) {
$databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools
return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$databases) {
@@ -207,10 +203,10 @@ $setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $c
};
}, ['pools', 'dbForPlatform', 'cache', 'authorization']);
$setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
$container->set('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
$database = null;
return function (?Document $project = null) use ($pools, $cache, $database, $authorization) {
return function (?Document $project = null) use ($pools, $cache, &$database, $authorization) {
if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant($project->getSequence());
return $database;
@@ -235,41 +231,43 @@ $setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $a
return $database;
};
}, ['pools', 'cache', 'authorization']);
$setResource('publisher', function (Group $pools) {
$container->set('publisher', function (Group $pools) {
return new BrokerPool(publisher: $pools->get('publisher'));
}, ['pools']);
$setResource('publisherDatabases', function (BrokerPool $publisher) {
$container->set('publisherDatabases', function (BrokerPool $publisher) {
return $publisher;
}, ['publisher']);
$setResource('publisherFunctions', function (BrokerPool $publisher) {
$container->set('publisherFunctions', function (BrokerPool $publisher) {
return $publisher;
}, ['publisher']);
$setResource('publisherMigrations', function (BrokerPool $publisher) {
$container->set('publisherMigrations', function (BrokerPool $publisher) {
return $publisher;
}, ['publisher']);
$setResource('publisherMessaging', function (BrokerPool $publisher) {
$container->set('publisherMessaging', function (BrokerPool $publisher) {
return $publisher;
}, ['publisher']);
$setResource('usage', function () {
$container->set('usage', function () {
return new UsageContext();
}, []);
$setResource('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher(
$container->set('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher(
$publisher,
new Queue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME))
), ['publisher']);
$setResource('queueForStatsResources', function (Publisher $publisher) {
return new StatsResources($publisher);
}, ['publisher']);
$setResource('queueForFunctions', function (Publisher $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('queueForFunctions', function (Publisher $publisher) {
return new Func($publisher);
}, ['publisher']);
$setResource('queueForDeletes', function (Publisher $publisher) {
$container->set('queueForDeletes', function (Publisher $publisher) {
return new Delete($publisher);
}, ['publisher']);
$setResource('queueForCertificates', function (Publisher $publisher) {
return new Certificate($publisher);
}, ['publisher']);
$setResource('logError', function (Registry $register) {
$container->set('logError', function (Registry $register) {
return function (Throwable $error, string $namespace, string $action) use ($register) {
Console::error('[Error] Timestamp: ' . date('c', time()));
Console::error('[Error] Type: ' . get_class($error));
@@ -321,25 +319,28 @@ $setResource('logError', function (Registry $register) {
};
}, ['register']);
$setResource('executor', fn () => new Executor(), []);
$container->set('executor', fn () => new Executor(), []);
$setResource('bus', function (Registry $register) use ($cli) {
return $register->get('bus')->setResolver(fn (string $name) => $cli->getResource($name));
$container->set('bus', function (Registry $register) use ($container) {
return $register->get('bus')->setResolver(fn (string $name) => $container->get($name));
}, ['register']);
$setResource('telemetry', fn () => new NoTelemetry(), []);
$container->set('telemetry', fn () => new NoTelemetry(), []);
$exitCode = 0;
$cli
->error()
->inject('error')
->inject('logError')
->action(function (Throwable $error, callable $logError) use ($taskName) {
->action(function (Throwable $error, callable $logError) use ($taskName, &$exitCode) {
call_user_func_array($logError, [
$error,
'Task',
$taskName,
]);
$exitCode = 1;
Timer::clearAll();
});
@@ -348,3 +349,4 @@ $cli->shutdown()->action(fn () => Timer::clearAll());
Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
require_once __DIR__ . '/init/span.php';
run($cli->run(...));
Console::exit($exitCode);
+2
View File
@@ -4,6 +4,7 @@
$common = include __DIR__ . '/collections/common.php';
$projects = include __DIR__ . '/collections/projects.php';
$databases = include __DIR__ . '/collections/databases.php';
$vectorsdb = include __DIR__ . '/collections/vectorsdb.php';
$platform = include __DIR__ . '/collections/platform.php';
$logs = include __DIR__ . '/collections/logs.php';
@@ -26,6 +27,7 @@ unset($common['files']);
$collections = [
'buckets' => $buckets,
'databases' => $databases,
'vectorsdb' => $vectorsdb,
'projects' => array_merge_recursive($projects, $common),
'console' => array_merge_recursive($platform, $common),
'logs' => $logs,
+18
View File
@@ -419,6 +419,17 @@ return [
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('impersonator'),
'type' => Database::VAR_BOOLEAN,
'signed' => true,
'size' => 0,
'format' => '',
'filters' => [],
'required' => false,
'default' => false,
'array' => false,
],
],
'indexes' => [
[
@@ -491,6 +502,13 @@ return [
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('impersonator'),
'type' => Database::INDEX_KEY,
'attributes' => [ID::custom('impersonator')],
'lengths' => [],
'orders' => [],
],
],
],
+3 -3
View File
@@ -594,7 +594,7 @@ $platformCollections = [
'filters' => [],
],
[
'$id' => ID::custom('key'),
'$id' => ID::custom('key'), // For app platforms
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
@@ -605,7 +605,7 @@ $platformCollections = [
'filters' => [],
],
[
'$id' => ID::custom('store'),
'$id' => ID::custom('store'), // Unused at the moment
'type' => Database::VAR_STRING,
'format' => '',
'size' => 256,
@@ -616,7 +616,7 @@ $platformCollections = [
'filters' => [],
],
[
'$id' => ID::custom('hostname'),
'$id' => ID::custom('hostname'), // For web platforms
'type' => Database::VAR_STRING,
'format' => '',
'size' => 256,
+9
View File
@@ -61,6 +61,15 @@ return [
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('database'),
'type' => Database::VAR_STRING,
'size' => 2000,
'required' => false,
'signed' => true,
'array' => false,
'filters' => [],
]
],
'indexes' => [
[
+165
View File
@@ -0,0 +1,165 @@
<?php
use Utopia\Database\Database;
use Utopia\Database\Helpers\ID;
return [
'collections' => [
'$collection' => ID::custom('databases'),
'$id' => ID::custom('collections'),
'name' => 'Collections',
'attributes' => [
[
'$id' => ID::custom('databaseInternalId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('databaseId'),
'type' => Database::VAR_STRING,
'signed' => true,
'size' => Database::LENGTH_KEY,
'format' => '',
'filters' => [],
'required' => true,
'default' => null,
'array' => false,
],
[
'$id' => ID::custom('name'),
'type' => Database::VAR_STRING,
'size' => 256,
'required' => true,
'signed' => true,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('dimension'),
'type' => Database::VAR_INTEGER,
'size' => 0,
'required' => true,
'signed' => false,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('enabled'),
'type' => Database::VAR_BOOLEAN,
'signed' => true,
'size' => 0,
'format' => '',
'filters' => [],
'required' => true,
'default' => null,
'array' => false,
],
[
'$id' => ID::custom('documentSecurity'),
'type' => Database::VAR_BOOLEAN,
'signed' => true,
'size' => 0,
'format' => '',
'filters' => [],
'required' => true,
'default' => null,
'array' => false,
],
[
'$id' => ID::custom('attributes'),
'type' => Database::VAR_STRING,
'size' => 1000000,
'required' => false,
'signed' => true,
'array' => false,
'filters' => ['subQueryAttributes'],
],
[
'$id' => ID::custom('indexes'),
'type' => Database::VAR_STRING,
'size' => 1000000,
'required' => false,
'signed' => true,
'array' => false,
'filters' => ['subQueryIndexes'],
],
[
'$id' => ID::custom('search'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
],
'defaultAttributes' => [
[
'$id' => ID::custom('embeddings'),
'type' => Database::VAR_VECTOR,
'required' => true,
'signed' => false,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('metadata'),
'type' => Database::VAR_OBJECT,
'default' => [],
'required' => false,
'size' => 0,
'signed' => false,
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
'$id' => ID::custom('_fulltext_search'),
'type' => Database::INDEX_FULLTEXT,
'attributes' => ['search'],
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_name'),
'type' => Database::INDEX_KEY,
'attributes' => ['name'],
'lengths' => [256],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_enabled'),
'type' => Database::INDEX_KEY,
'attributes' => ['enabled'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_documentSecurity'),
'type' => Database::INDEX_KEY,
'attributes' => ['documentSecurity'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
],
'defaultIndexes' => [
// not creating default indexes on the embeddings as it depends on the type of query users using the most
[
'$id' => ID::custom('_key_metadata'),
'type' => Database::INDEX_OBJECT,
'attributes' => ['metadata'],
'lengths' => [],
'orders' => [],
],
]
]
];
+9
View File
@@ -34,11 +34,20 @@ $console = [
'legalAddress' => '',
'legalTaxId' => '',
'auths' => [
'membershipsUserName' => true,
'membershipsUserEmail' => true,
'membershipsMfa' => true,
'membershipsUserId' => true,
'membershipsUserPhone' => true,
'mockNumbers' => [],
'invites' => System::getEnv('_APP_CONSOLE_INVITES', 'enabled') === 'enabled',
'limit' => (System::getEnv('_APP_CONSOLE_WHITELIST_ROOT', 'enabled') === 'enabled') ? 1 : 0, // limit signup to 1 user
'duration' => TOKEN_EXPIRATION_LOGIN_LONG, // 1 Year in seconds
'sessionAlerts' => System::getEnv('_APP_CONSOLE_SESSION_ALERTS', 'disabled') === 'enabled',
// For email configuration, false means feature is disabled; false means these emails are allowed during sign-ups
'disposableEmails' => false,
'canonicalEmails' => false,
'freeEmails' => false,
'invalidateSessions' => true
],
'authWhitelistEmails' => (!empty(System::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null))) ? \explode(',', System::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null)) : [],
+3
View File
@@ -28,6 +28,9 @@ return [
'X-Appwrite-Timestamp',
'X-Appwrite-Session',
'X-Appwrite-Platform',
'X-Appwrite-Impersonate-User-Id',
'X-Appwrite-Impersonate-User-Email',
'X-Appwrite-Impersonate-User-Phone',
// SDK generator
'X-SDK-Version',
'X-SDK-Name',
+51 -1
View File
@@ -54,6 +54,11 @@ return [
'description' => 'Rate limit for the current endpoint has been exceeded. Please try again after some time.',
'code' => 429,
],
Exception::GENERAL_RESOURCE_LOCKED => [
'name' => Exception::GENERAL_RESOURCE_LOCKED,
'description' => 'The requested resource is currently being modified by another request. Please retry after a brief delay.',
'code' => 409,
],
Exception::GENERAL_SMTP_DISABLED => [
'name' => Exception::GENERAL_SMTP_DISABLED,
'description' => 'SMTP is disabled on your Appwrite instance. You can <a href="/docs/email-delivery">learn more about setting up SMTP</a> in our docs.',
@@ -226,6 +231,21 @@ return [
'description' => 'A user with the same email already exists in the current project.',
'code' => 409,
],
Exception::USER_EMAIL_DISPOSABLE => [
'name' => Exception::USER_EMAIL_DISPOSABLE,
'description' => 'Disposable email addresses are not allowed. Please use a permanent email address.',
'code' => 400,
],
Exception::USER_EMAIL_FREE => [
'name' => Exception::USER_EMAIL_FREE,
'description' => 'Free email addresses are not allowed. Please use a business or custom-domain email address.',
'code' => 400,
],
Exception::USER_EMAIL_NOT_CANONICAL => [
'name' => Exception::USER_EMAIL_NOT_CANONICAL,
'description' => 'This email address must already be in its canonical form. Please remove aliases, tags, or provider-specific variations and try again.',
'code' => 400,
],
Exception::USER_PASSWORD_MISMATCH => [
'name' => Exception::USER_PASSWORD_MISMATCH,
'description' => 'Passwords do not match. Please check the password and confirm password.',
@@ -369,7 +389,7 @@ return [
],
Exception::API_KEY_EXPIRED => [
'name' => Exception::API_KEY_EXPIRED,
'description' => 'The dynamic API key has expired. Please don\'t use dynamic API keys for more than duration of the execution.',
'description' => 'The ephemeral API key has expired. Please don\'t use ephemeral API keys for more than duration of the execution.',
'code' => 401,
],
@@ -1164,6 +1184,16 @@ return [
'description' => 'Platform with the requested ID could not be found.',
'code' => 404,
],
Exception::PLATFORM_METHOD_UNSUPPORTED => [
'name' => Exception::PLATFORM_METHOD_UNSUPPORTED,
'description' => 'The requested platform has invalid type. Please use corresponding update method for the platform type.',
'code' => 400,
],
Exception::PLATFORM_ALREADY_EXISTS => [
'name' => Exception::PLATFORM_ALREADY_EXISTS,
'description' => 'Platform with the same ID already exists in this project. Try again with a different ID.',
'code' => 409,
],
Exception::VARIABLE_NOT_FOUND => [
'name' => Exception::VARIABLE_NOT_FOUND,
'description' => 'Variable with the requested ID could not be found.',
@@ -1206,6 +1236,11 @@ return [
'description' => 'Migration is already in progress. You can check the status of the migration in your Appwrite Console\'s "Settings" > "Migrations".',
'code' => 409,
],
Exception::MIGRATION_DATABASE_TYPE_UNSUPPORTED => [
'name' => Exception::MIGRATION_DATABASE_TYPE_UNSUPPORTED,
'description' => 'The specified database type is not supported for CSV import or export operations.',
'code' => 400,
],
/** Realtime */
Exception::REALTIME_MESSAGE_FORMAT_INVALID => [
@@ -1378,4 +1413,19 @@ return [
'description' => 'When using project API key, make sure to pass x-appwrite-project header with your project ID.',
'code' => 403,
],
Exception::MOCK_NUMBER_ALREADY_EXISTS => [
'name' => Exception::MOCK_NUMBER_ALREADY_EXISTS,
'description' => 'Mock number with the requested number already exists. Try again with a different number. or update OTP of existing mock number.',
'code' => 409,
],
Exception::MOCK_NUMBER_NOT_FOUND => [
'name' => Exception::MOCK_NUMBER_NOT_FOUND,
'description' => 'Mock number with the requested number could not be found.',
'code' => 404,
],
Exception::MOCK_NUMBER_LIMIT_EXCEEDED => [
'name' => Exception::MOCK_NUMBER_LIMIT_EXCEEDED,
'description' => 'The maximum number of mock phones for this project has been reached.',
'code' => 400,
],
];
-6
View File
@@ -9,11 +9,5 @@ return [
'mfaChallenge',
'sessionAlert',
'otpSession'
],
'sms' => [
'verification',
'login',
'invitation',
'mfaChallenge'
]
];
+15 -15
View File
@@ -57,21 +57,21 @@
"emails.recovery.thanks": "Thanks,",
"emails.recovery.buttonText": "Reset password",
"emails.recovery.signature": "{{project}} team",
"emails.csvExport.success.subject": "Your CSV export is ready",
"emails.csvExport.success.preview": "Your data export has been completed successfully.",
"emails.csvExport.success.hello": "Hello {{user}},",
"emails.csvExport.success.body": "Your CSV export is ready to download. Click the button below to download your data export.",
"emails.csvExport.success.footer": "This download link will expire in 1 hour.",
"emails.csvExport.success.thanks": "Thanks,",
"emails.csvExport.success.buttonText": "Download CSV",
"emails.csvExport.success.signature": "Appwrite team",
"emails.csvExport.failure.subject": "Your CSV export failed - file too large",
"emails.csvExport.failure.preview": "Your data export failed because the file size exceeds your plan limit.",
"emails.csvExport.failure.hello": "Hello {{user}},",
"emails.csvExport.failure.body": "Your CSV export could not be completed because the export file size ({{size}}MB) exceeds your plan limit. Please consider upgrading your plan or exporting a smaller dataset.",
"emails.csvExport.failure.footer": "If you have any questions, please contact our support team.",
"emails.csvExport.failure.thanks": "Thanks,",
"emails.csvExport.failure.signature": "{{project}} team",
"emails.dataExport.success.subject": "Your {{type}} export is ready",
"emails.dataExport.success.preview": "Your data export has been completed successfully.",
"emails.dataExport.success.hello": "Hello {{user}},",
"emails.dataExport.success.body": "Your {{type}} export is ready to download. Click the button below to download your data export.",
"emails.dataExport.success.footer": "This download link will expire in 1 hour.",
"emails.dataExport.success.thanks": "Thanks,",
"emails.dataExport.success.buttonText": "Download {{type}}",
"emails.dataExport.success.signature": "Appwrite team",
"emails.dataExport.failure.subject": "Your {{type}} export failed - file too large",
"emails.dataExport.failure.preview": "Your data export failed because the file size exceeds your plan limit.",
"emails.dataExport.failure.hello": "Hello {{user}},",
"emails.dataExport.failure.body": "Your {{type}} export could not be completed because the export file size ({{size}}MB) exceeds your plan limit. Please consider upgrading your plan or exporting a smaller dataset.",
"emails.dataExport.failure.footer": "If you have any questions, please contact our support team.",
"emails.dataExport.failure.thanks": "Thanks,",
"emails.dataExport.failure.signature": "{{project}} team",
"emails.invitation.subject": "Invitation to {{team}} Team at {{project}}",
"emails.invitation.preview": "{{owner}} invited you to join {{team}} at {{project}}",
"emails.invitation.hello": "Hello {{user}},",
+44
View File
@@ -167,6 +167,17 @@ return [
'mock' => false,
'class' => 'Appwrite\\Auth\\OAuth2\\Figma',
],
'fusionauth' => [
'name' => 'FusionAuth',
'developers' => 'https://fusionauth.io/docs/',
'icon' => 'icon-fusionauth',
'enabled' => true,
'sandbox' => false,
'form' => 'fusionauth.phtml',
'beta' => false,
'mock' => false,
'class' => 'Appwrite\\Auth\\OAuth2\\FusionAuth',
],
'github' => [
'name' => 'GitHub',
'developers' => 'https://developer.github.com/',
@@ -200,6 +211,28 @@ return [
'mock' => false,
'class' => 'Appwrite\\Auth\\OAuth2\\Google',
],
'keycloak' => [
'name' => 'Keycloak',
'developers' => 'https://www.keycloak.org/documentation',
'icon' => 'icon-keycloak',
'enabled' => true,
'sandbox' => false,
'form' => 'keycloak.phtml',
'beta' => false,
'mock' => false,
'class' => 'Appwrite\\Auth\\OAuth2\\Keycloak',
],
'kick' => [
'name' => 'Kick',
'developers' => 'https://docs.kick.com/',
'icon' => 'icon-kick',
'enabled' => true,
'sandbox' => false,
'form' => false,
'beta' => false,
'mock' => false,
'class' => 'Appwrite\\Auth\\OAuth2\\Kick',
],
'linkedin' => [
'name' => 'LinkedIn',
'developers' => 'https://developer.linkedin.com/',
@@ -376,6 +409,17 @@ return [
'mock' => false,
'class' => 'Appwrite\\Auth\\OAuth2\\Wordpress',
],
'x' => [
'name' => 'X',
'developers' => 'https://docs.x.com/fundamentals/authentication/oauth-2-0/authorization-code',
'icon' => 'icon-twitter',
'enabled' => true,
'sandbox' => false,
'form' => false,
'beta' => false,
'mock' => false,
'class' => 'Appwrite\\Auth\\OAuth2\\X',
],
'yahoo' => [
'name' => 'Yahoo',
'developers' => 'https://developer.yahoo.com/oauth2/guide/flows_authcode/',
@@ -9,8 +9,8 @@ return [
'key' => 'graphql',
'name' => 'GraphQL',
],
'realtime' => [
'key' => 'realtime',
'name' => 'Realtime',
'websocket' => [
'key' => 'websocket',
'name' => 'Websocket',
],
];
+15 -5
View File
@@ -21,8 +21,8 @@ $member = [
'projects.read',
'locale.read',
'avatars.read',
'execution.read',
'execution.write',
'executions.read',
'executions.write',
'targets.read',
'targets.write',
'subscribers.write',
@@ -55,6 +55,14 @@ $admins = [
'tables.write',
'platforms.read',
'platforms.write',
'oauth2.read',
'oauth2.write',
'mocks.read',
'mocks.write',
'policies.read',
'policies.write',
'templates.read',
'templates.write',
'projects.write',
'keys.read',
'keys.write',
@@ -62,6 +70,8 @@ $admins = [
'devKeys.write',
'webhooks.read',
'webhooks.write',
'project.read',
'project.write',
'locale.read',
'avatars.read',
'health.read',
@@ -71,8 +81,8 @@ $admins = [
'sites.write',
'log.read',
'log.write',
'execution.read',
'execution.write',
'executions.read',
'executions.write',
'rules.read',
'rules.write',
'migrations.read',
@@ -113,7 +123,7 @@ return [
'files.write',
'locale.read',
'avatars.read',
'execution.write',
'executions.write',
],
],
User::ROLE_USERS => [
-22
View File
@@ -3,13 +3,6 @@
// List of scopes for organization (teams) API keys
return [
"platforms.read" => [
"description" => 'Access to read project\'s platforms',
],
"platforms.write" => [
"description" =>
'Access to create, update, and delete project\'s platforms',
],
"projects.read" => [
"description" => 'Access to read organization\'s projects',
],
@@ -17,13 +10,6 @@ return [
"description" =>
"Access to create, update, and delete projects in organization",
],
"keys.read" => [
"description" => 'Access to read project\'s API keys',
],
"keys.write" => [
"description" =>
"Access to create, update, and delete project\'s API keys",
],
"devKeys.read" => [
"description" => 'Access to read project\'s development keys',
],
@@ -31,12 +17,4 @@ return [
"description" =>
"Access to create, update, and delete project\'s development keys",
],
"webhooks.read" => [
"description" =>
"Access to read project\'s webhooks",
],
"webhooks.write" => [
"description" =>
"Access to create, update, and delete project\'s webhooks",
],
];
+290 -133
View File
@@ -1,183 +1,340 @@
<?php
return [ // List of publicly visible scopes
'sessions.write' => [
'description' => 'Access to create, update, and delete user sessions',
// List of publicly visible scopes
return [
// Project
"project.read" => [
"description" =>
"Access to read project\'s information",
"category" => "Project",
],
"project.write" => [
"description" =>
"Access to update project\'s information",
"category" => "Project",
],
"keys.read" => [
"description" =>
"Access to read project\'s keys",
"category" => "Project",
],
"keys.write" => [
"description" =>
"Access to create, update, and delete project\'s keys",
"category" => "Project",
],
"platforms.read" => [
"description" =>
"Access to read project\'s platforms",
"category" => "Project",
],
"platforms.write" => [
"description" =>
"Access to create, update, and delete project\'s platforms",
"category" => "Project",
],
"mocks.read" => [
"description" =>
"Access to read project\'s mocks",
"category" => "Project",
],
"mocks.write" => [
"description" =>
"Access to create, update, and delete project\'s mocks",
"category" => "Project",
],
"policies.read" => [
"description" =>
"Access to read project\'s policies",
"category" => "Project",
],
"policies.write" => [
"description" =>
"Access to update project\'s policies",
"category" => "Project",
],
"templates.read" => [
"description" =>
"Access to read project\'s templates",
"category" => "Project",
],
"templates.write" => [
"description" =>
"Access to create, update, and delete project\'s templates",
"category" => "Project",
],
"oauth2.read" => [
"description" =>
"Access to read project\'s OAuth2 configuration",
"category" => "Project",
],
"oauth2.write" => [
"description" =>
"Access to update project\'s OAuth2 configuration",
"category" => "Project",
],
// Auth
'users.read' => [
'description' => 'Access to read your project\'s users',
'description' => 'Access to read users',
'category' => 'Auth',
],
'users.write' => [
'description' => 'Access to create, update, and delete your project\'s users',
'description' => 'Access to create, update, and delete users',
'category' => 'Auth',
],
'sessions.read' => [
'description' => 'Access to read user sessions',
'category' => 'Auth',
],
'sessions.write' => [
'description' => 'Access to create, update, and delete user sessions',
'category' => 'Auth',
],
'teams.read' => [
'description' => 'Access to read your project\'s teams',
'description' => 'Access to read teams',
'category' => 'Auth',
],
'teams.write' => [
'description' => 'Access to create, update, and delete your project\'s teams',
'description' => 'Access to create, update, and delete teams',
'category' => 'Auth',
],
// Databases
'databases.read' => [
'description' => 'Access to read your project\'s databases',
'description' => 'Access to read databases',
'category' => 'Databases',
],
'databases.write' => [
'description' => 'Access to create, update, and delete your project\'s databases',
],
'collections.read' => [
'description' => 'Access to read your project\'s database collections',
],
'collections.write' => [
'description' => 'Access to create, update, and delete your project\'s database collections',
'description' => 'Access to create, update, and delete databases',
'category' => 'Databases',
],
'tables.read' => [
'description' => 'Access to read your project\'s database tables',
'description' => 'Access to read database tables',
'category' => 'Databases',
],
'tables.write' => [
'description' => 'Access to create, update, and delete your project\'s database tables',
],
'attributes.read' => [
'description' => 'Access to read your project\'s database collection\'s attributes',
],
'attributes.write' => [
'description' => 'Access to create, update, and delete your project\'s database collection\'s attributes',
'description' => 'Access to create, update, and delete database tables',
'category' => 'Databases',
],
'columns.read' => [
'description' => 'Access to read your project\'s database table\'s columns',
'description' => 'Access to read database table columns',
'category' => 'Databases',
],
'columns.write' => [
'description' => 'Access to create, update, and delete your project\'s database table\'s columns',
'description' => 'Access to create, update, and delete database table columns',
'category' => 'Databases',
],
'indexes.read' => [
'description' => 'Access to read your project\'s database table\'s indexes',
'description' => 'Access to read database table indexes',
'category' => 'Databases',
],
'indexes.write' => [
'description' => 'Access to create, update, and delete your project\'s database table\'s indexes',
],
'documents.read' => [
'description' => 'Access to read your project\'s database documents',
],
'documents.write' => [
'description' => 'Access to create, update, and delete your project\'s database documents',
'description' => 'Access to create, update, and delete database table indexes',
'category' => 'Databases',
],
'rows.read' => [
'description' => 'Access to read your project\'s database rows',
'description' => 'Access to read database table rows',
'category' => 'Databases',
],
'rows.write' => [
'description' => 'Access to create, update, and delete your project\'s database rows',
'description' => 'Access to create, update, and delete database table rows',
'category' => 'Databases',
],
'files.read' => [
'description' => 'Access to read your project\'s storage files and preview images',
'collections.read' => [
'description' => 'Access to read database collections',
'category' => 'Databases',
'deprecated' => true,
],
'files.write' => [
'description' => 'Access to create, update, and delete your project\'s storage files',
'collections.write' => [
'description' => 'Access to create, update, and delete database collections',
'category' => 'Databases',
'deprecated' => true,
],
'attributes.read' => [
'description' => 'Access to read database collection attributes',
'category' => 'Databases',
'deprecated' => true,
],
'attributes.write' => [
'description' => 'Access to create, update, and delete database collection attributes',
'category' => 'Databases',
'deprecated' => true,
],
'documents.read' => [
'description' => 'Access to read database collection documents',
'category' => 'Databases',
'deprecated' => true,
],
'documents.write' => [
'description' => 'Access to create, update, and delete database collection documents',
'category' => 'Databases',
'deprecated' => true,
],
// Storage
'buckets.read' => [
'description' => 'Access to read your project\'s storage buckets',
'description' => 'Access to read storage buckets',
'category' => 'Storage',
],
'buckets.write' => [
'description' => 'Access to create, update, and delete your project\'s storage buckets',
'description' => 'Access to create, update, and delete storage buckets',
'category' => 'Storage',
],
'functions.read' => [
'description' => 'Access to read your project\'s functions and code deployments',
'files.read' => [
'description' => 'Access to read storage files and preview images',
'category' => 'Storage',
],
'functions.write' => [
'description' => 'Access to create, update, and delete your project\'s functions and code deployments',
],
'sites.read' => [
'description' => 'Access to read your project\'s sites and deployments',
],
'sites.write' => [
'description' => 'Access to create, update, and delete your project\'s sites and deployments',
],
'log.read' => [
'description' => 'Access to read your site\'s logs',
],
'log.write' => [
'description' => 'Access to update, and delete your site\'s logs',
],
'execution.read' => [
'description' => 'Access to read your project\'s execution logs',
],
'execution.write' => [
'description' => 'Access to execute your project\'s functions',
],
'locale.read' => [
'description' => 'Access to access your project\'s Locale service',
],
'avatars.read' => [
'description' => 'Access to access your project\'s Avatars service',
],
'health.read' => [
'description' => 'Access to read your project\'s health status',
],
'providers.read' => [
'description' => 'Access to read your project\'s providers',
],
'providers.write' => [
'description' => 'Access to create, update, and delete your project\'s providers',
],
'messages.read' => [
'description' => 'Access to read your project\'s messages',
],
'messages.write' => [
'description' => 'Access to create, update, and delete your project\'s messages',
],
'topics.read' => [
'description' => 'Access to read your project\'s topics',
],
'topics.write' => [
'description' => 'Access to create, update, and delete your project\'s topics',
],
'subscribers.read' => [
'description' => 'Access to read your project\'s subscribers',
],
'subscribers.write' => [
'description' => 'Access to create, update, and delete your project\'s subscribers',
],
'targets.read' => [
'description' => 'Access to read your project\'s targets',
],
'targets.write' => [
'description' => 'Access to create, update, and delete your project\'s targets',
],
'rules.read' => [
'description' => 'Access to read your project\'s proxy rules',
],
'rules.write' => [
'description' => 'Access to create, update, and delete your project\'s proxy rules',
],
'schedules.read' => [
'description' => 'Access to read your project\'s schedules',
],
'schedules.write' => [
'description' => 'Access to create, update, and delete your project\'s schedules',
],
'migrations.read' => [
'description' => 'Access to read your project\'s migrations',
],
'migrations.write' => [
'description' => 'Access to create, update, and delete your project\'s migrations.',
],
'vcs.read' => [
'description' => 'Access to read your project\'s VCS repositories',
],
'vcs.write' => [
'description' => 'Access to create, update, and delete your project\'s VCS repositories',
],
'assistant.read' => [
'description' => 'Access to read the Assistant service',
'files.write' => [
'description' => 'Access to create, update, and delete storage files',
'category' => 'Storage',
],
'tokens.read' => [
'description' => 'Access to read your project\'s tokens',
'description' => 'Access to read storage file tokens',
'category' => 'Storage',
],
'tokens.write' => [
'description' => 'Access to create, update, and delete your project\'s tokens',
'description' => 'Access to create, update, and delete storage file tokens',
'category' => 'Storage',
],
// Functions
'functions.read' => [
'description' => 'Access to read functions and deployments',
'category' => 'Functions',
],
'functions.write' => [
'description' => 'Access to create, update, and delete functions and deployments',
'category' => 'Functions',
],
'executions.read' => [
'description' => 'Access to read function executions',
'category' => 'Functions',
],
'executions.write' => [
'description' => 'Access to create function executions',
'category' => 'Functions',
],
// Sites
'sites.read' => [
'description' => 'Access to read sites and deployments',
'category' => 'Sites',
],
'sites.write' => [
'description' => 'Access to create, update, and delete sites and deployments',
'category' => 'Sites',
],
'log.read' => [
'description' => 'Access to read site logs',
'category' => 'Sites',
],
'log.write' => [
'description' => 'Access to update, and delete site logs',
'category' => 'Sites',
],
// Messaging
'providers.read' => [
'description' => 'Access to read messaging providers',
'category' => 'Messaging',
],
'providers.write' => [
'description' => 'Access to create, update, and delete messaging providers',
'category' => 'Messaging',
],
'topics.read' => [
'description' => 'Access to read messaging topics',
'category' => 'Messaging',
],
'topics.write' => [
'description' => 'Access to create, update, and delete messaging topics',
'category' => 'Messaging',
],
'subscribers.read' => [
'description' => 'Access to read messaging subscribers',
'category' => 'Messaging',
],
'subscribers.write' => [
'description' => 'Access to create, update, and delete messaging subscribers',
'category' => 'Messaging',
],
'targets.read' => [
'description' => 'Access to read messaging targets',
'category' => 'Messaging',
],
'targets.write' => [
'description' => 'Access to create, update, and delete messaging targets',
'category' => 'Messaging',
],
'messages.read' => [
'description' => 'Access to read messaging messages',
'category' => 'Messaging',
],
'messages.write' => [
'description' => 'Access to create, update, and delete messaging messages',
'category' => 'Messaging',
],
// Other
"webhooks.read" => [
"description" =>
"Access to read project\'s webhooks",
"Access to read webhooks",
'category' => 'Other',
],
"webhooks.write" => [
"description" =>
"Access to create, update, and delete project\'s webhooks",
"Access to create, update, and delete webhooks",
'category' => 'Other',
],
'locale.read' => [
'description' => 'Access to use Locale service',
'category' => 'Other',
],
'avatars.read' => [
'description' => 'Access to use Avatars service',
'category' => 'Other',
],
'health.read' => [
'description' => 'Access to use Health service',
'category' => 'Other',
],
'assistant.read' => [
'description' => 'Access to use Assistant service',
'category' => 'Other',
],
'migrations.read' => [
'description' => 'Access to read migrations',
'category' => 'Other',
],
'migrations.write' => [
'description' => 'Access to create, update, and delete migrations.',
'category' => 'Other',
],
// TODO: Figure out where to move those
'schedules.read' => [
'description' => 'Access to read schedules.',
'category' => 'Other',
],
'schedules.write' => [
'description' => 'Access to create, update, and delete schedules.',
'category' => 'Other',
],
'vcs.read' => [
'description' => 'Access to read resources under VCS service.',
'category' => 'Other',
],
'vcs.write' => [
'description' => 'Access to create, update, and delete resources under VCS service.',
'category' => 'Other',
],
'rules.read' => [
'description' => 'Access to read proxy rules.',
'category' => 'Other',
],
'rules.write' => [
'description' => 'Access to create, update, and delete proxy rules.',
'category' => 'Other',
],
];
+55 -24
View File
@@ -250,26 +250,16 @@ return [
],
],
],
[
'key' => 'markdown',
'name' => 'Markdown',
'version' => '0.3.0',
'url' => 'https://github.com/appwrite/sdk-for-md.git',
'package' => 'https://www.npmjs.com/package/@appwrite.io/docs',
'enabled' => true,
'beta' => false,
'dev' => false,
'hidden' => false,
'family' => APP_SDK_PLATFORM_CONSOLE,
'prism' => 'markdown',
'source' => \realpath(__DIR__ . '/../sdks/console-md'),
'gitUrl' => 'git@github.com:appwrite/sdk-for-md.git',
'gitRepoName' => 'sdk-for-md',
'gitUserName' => 'appwrite',
'gitBranch' => 'dev',
'repoBranch' => 'main',
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/md/CHANGELOG.md'),
],
],
],
APP_SDK_PLATFORM_STATIC => [
'key' => APP_SDK_PLATFORM_STATIC,
'name' => 'Static',
'description' => 'SDK artifacts for Appwrite integrations that do not require a generated platform API specification.',
'enabled' => true,
'beta' => false,
'sdks' => [
[
'key' => 'agent-skills',
'name' => 'AgentSkills',
@@ -279,9 +269,10 @@ return [
'beta' => false,
'dev' => false,
'hidden' => false,
'family' => APP_SDK_PLATFORM_CONSOLE,
'spec' => 'static',
'family' => APP_SDK_PLATFORM_STATIC,
'prism' => 'agent-skills',
'source' => \realpath(__DIR__ . '/../sdks/console-agent-skills'),
'source' => \realpath(__DIR__ . '/../sdks/static-agent-skills'),
'gitUrl' => 'git@github.com:appwrite/agent-skills.git',
'gitRepoName' => 'agent-skills',
'gitUserName' => 'appwrite',
@@ -298,9 +289,10 @@ return [
'beta' => false,
'dev' => false,
'hidden' => false,
'family' => APP_SDK_PLATFORM_CONSOLE,
'spec' => 'static',
'family' => APP_SDK_PLATFORM_STATIC,
'prism' => 'cursor-plugin',
'source' => \realpath(__DIR__ . '/../sdks/console-cursor-plugin'),
'source' => \realpath(__DIR__ . '/../sdks/static-cursor-plugin'),
'gitUrl' => 'git@github.com:appwrite/cursor-plugin.git',
'gitRepoName' => 'cursor-plugin',
'gitUserName' => 'appwrite',
@@ -308,6 +300,26 @@ return [
'repoBranch' => 'main',
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/cursor-plugin/CHANGELOG.md'),
],
[
'key' => 'claude-plugin',
'name' => 'ClaudePlugin',
'version' => '0.1.0',
'url' => 'https://github.com/appwrite/claude-plugin.git',
'enabled' => true,
'beta' => false,
'dev' => false,
'hidden' => false,
'spec' => 'static',
'family' => APP_SDK_PLATFORM_STATIC,
'prism' => 'claude-plugin',
'source' => \realpath(__DIR__ . '/../sdks/static-claude-plugin'),
'gitUrl' => 'git@github.com:appwrite/claude-plugin.git',
'gitRepoName' => 'claude-plugin',
'gitUserName' => 'appwrite',
'gitBranch' => 'dev',
'repoBranch' => 'main',
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/claude-plugin/CHANGELOG.md'),
],
],
],
@@ -494,6 +506,25 @@ return [
'gitBranch' => 'dev',
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/swift/CHANGELOG.md'),
],
[
'key' => 'rust',
'name' => 'Rust',
'version' => '0.1.0',
'url' => 'https://github.com/appwrite/sdk-for-rust',
'package' => 'https://crates.io/crates/appwrite',
'enabled' => true,
'beta' => true,
'dev' => true,
'hidden' => false,
'family' => APP_SDK_PLATFORM_SERVER,
'prism' => 'rust',
'source' => \realpath(__DIR__ . '/../sdks/server-rust'),
'gitUrl' => 'git@github.com:appwrite/sdk-for-rust.git',
'gitRepoName' => 'sdk-for-rust',
'gitUserName' => 'appwrite',
'gitBranch' => 'dev',
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/rust/CHANGELOG.md'),
],
[
'key' => 'graphql',
'name' => 'GraphQL',
+5 -5
View File
@@ -137,7 +137,7 @@ return [
'docs' => true,
'docsUrl' => '',
'tests' => false,
'optional' => false,
'optional' => true,
'icon' => '',
'platforms' => ['client', 'server', 'console'],
],
@@ -193,7 +193,7 @@ return [
'docs' => false,
'docsUrl' => '',
'tests' => false,
'optional' => false,
'optional' => true,
'icon' => '',
'platforms' => ['client', 'server', 'console'],
],
@@ -235,7 +235,7 @@ return [
'docs' => true,
'docsUrl' => 'https://appwrite.io/docs/proxy',
'tests' => false,
'optional' => false,
'optional' => true,
'icon' => '/images/services/proxy.png',
'platforms' => ['client', 'server', 'console'],
],
@@ -286,12 +286,12 @@ return [
'name' => 'Migrations',
'subtitle' => 'The Migrations service allows you to migrate third-party data to your Appwrite project.',
'description' => '/docs/services/migrations.md',
'controller' => 'api/migrations.php',
'controller' => '', // Uses modules
'sdk' => true,
'docs' => true,
'docsUrl' => 'https://appwrite.io/docs/migrations',
'tests' => true,
'optional' => false,
'optional' => true,
'icon' => '/images/services/migrations.png',
'platforms' => ['client', 'server', 'console'],
],
-3
View File
@@ -31,9 +31,6 @@ class FunctionUseCases
public const DEV_TOOLS = 'dev-tools';
public const AUTH = 'auth';
/**
* @var array<string>
*/
public static function getAll(): array
{
return [
+9 -12
View File
@@ -25,9 +25,6 @@ class SiteUseCases
public const FORMS = 'forms';
public const DASHBOARD = 'dashboard';
/**
* @var array<string>
*/
public static function getAll(): array
{
return [
@@ -252,7 +249,7 @@ return [
'frameworks' => [
getFramework('VITE', [
'providerRootDirectory' => './vite/vitepress',
'outputDirectory' => '404.html',
'fallbackFile' => '404.html',
'installCommand' => 'npm i vitepress && npm install',
'buildCommand' => 'npm run docs:build',
'outputDirectory' => './.vitepress/dist',
@@ -275,7 +272,7 @@ return [
'frameworks' => [
getFramework('VUE', [
'providerRootDirectory' => './vue/vuepress',
'outputDirectory' => '404.html',
'fallbackFile' => '404.html',
'installCommand' => 'npm install',
'buildCommand' => 'npm run build',
'outputDirectory' => './src/.vuepress/dist',
@@ -298,7 +295,7 @@ return [
'frameworks' => [
getFramework('REACT', [
'providerRootDirectory' => './react/docusaurus',
'outputDirectory' => '404.html',
'fallbackFile' => '404.html',
'installCommand' => 'npm install',
'buildCommand' => 'npm run build',
'outputDirectory' => './build',
@@ -1490,13 +1487,13 @@ return [
]
],
[
'key' => 'crm-dashboard-react-admin',
'name' => 'CRM dashboard with React Admin',
'tagline' => 'A React-based admin dashboard template with CRM features.',
'key' => 'dashboard-react-admin',
'name' => 'E-commerce dashboard with React Admin',
'tagline' => 'A React-based admin dashboard template with e-commerce features.',
'score' => 4, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible)
'useCases' => [SiteUseCases::DASHBOARD],
'screenshotDark' => $url . '/images/sites/templates/crm-dashboard-react-admin-dark.png',
'screenshotLight' => $url . '/images/sites/templates/crm-dashboard-react-admin-light.png',
'useCases' => [SiteUseCases::DASHBOARD, SiteUseCases::ECOMMERCE],
'screenshotDark' => $url . '/images/sites/templates/dashboard-react-admin-dark.png',
'screenshotLight' => $url . '/images/sites/templates/dashboard-react-admin-light.png',
'frameworks' => [
getFramework('REACT', [
'providerRootDirectory' => './react/react-admin',
+13 -4
View File
@@ -34,6 +34,15 @@ return [
'question' => '',
'filter' => ''
],
[
'name' => '_APP_LOCKING_ENABLED',
'description' => 'Enable distributed locking for platform writes. Locks coordinate concurrent updates across API pods so that read-modify-write operations on shared documents do not lose updates. By default, set to \'enabled\'. Set to \'disabled\' as an emergency kill switch — locks become no-ops (fail-open) and concurrent writes will race.',
'introduction' => '1.9.3',
'default' => 'enabled',
'required' => false,
'question' => '',
'filter' => ''
],
[
'name' => '_APP_OPTIONS_FORCE_HTTPS',
'description' => 'Allows you to force HTTPS connection to your API. This feature redirects any HTTP call to HTTPS and adds the \'Strict-Transport-Security\' header to all HTTP responses. By default, set to \'enabled\'. To disable, set to \'disabled\'. This feature will work only when your ports are set to default 80 and 443, and you have set up wildcard certificates with DNS challenge.',
@@ -872,18 +881,18 @@ return [
],
[
'name' => '_APP_FUNCTIONS_BUILD_TIMEOUT',
'description' => 'Deprecated since 1.7.0. The maximum number of seconds allowed as a timeout value when building a new function. The default value is 900 seconds.',
'description' => 'Deprecated since 1.7.0. The maximum number of seconds allowed as a timeout value when building a new function. The default value is 2700 seconds.',
'introduction' => '0.13.0',
'default' => '900',
'default' => '2700',
'required' => false,
'question' => '',
'filter' => ''
],
[
'name' => '_APP_COMPUTE_BUILD_TIMEOUT',
'description' => 'The maximum number of seconds allowed as a timeout value when building a new function or site. The default value is 900 seconds.',
'description' => 'The maximum number of seconds allowed as a timeout value when building a new function or site. The default value is 2700 seconds.',
'introduction' => '1.7.0',
'default' => '900',
'default' => '2700',
'required' => false,
'question' => '',
'filter' => ''
File diff suppressed because it is too large Load Diff
+9 -3
View File
@@ -28,12 +28,18 @@ use Utopia\Validator\Text;
Http::init()
->groups(['graphql'])
->inject('project')
->inject('user')
->inject('request')
->inject('response')
->inject('authorization')
->action(function (Document $project, Authorization $authorization) {
->action(function (Document $project, User $user, Request $request, Response $response, Authorization $authorization) {
$response->setUser($user);
$request->setUser($user);
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->isApp($authorization->getRoles()))
) {
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
}
@@ -225,7 +231,7 @@ function execute(
$validations = GraphQL::getStandardValidationRules();
if (System::getEnv('_APP_GRAPHQL_INTROSPECTION', 'enabled') === 'disabled') {
$validations[] = new DisableIntrospection();
$validations[] = new DisableIntrospection(DisableIntrospection::ENABLED);
}
if (System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled') {
+1
View File
@@ -231,6 +231,7 @@ Http::get('/v1/locale/continents')
->inject('locale')
->action(function (Response $response, Locale $locale) {
$list = array_keys(Config::getParam('locale-continents'));
$output = [];
foreach ($list as $value) {
$output[] = new Document([
+7 -16
View File
@@ -8,7 +8,6 @@ use Appwrite\Event\Event;
use Appwrite\Event\Messaging;
use Appwrite\Extend\Exception;
use Appwrite\Messaging\Status as MessageStatus;
use Appwrite\Network\Validator\Email;
use Appwrite\Permission;
use Appwrite\Role;
use Appwrite\SDK\AuthType;
@@ -43,6 +42,7 @@ use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\Roles;
use Utopia\Database\Validator\UID;
use Utopia\Emails\Validator\Email;
use Utopia\Http\Http;
use Utopia\Locale\Locale;
use Utopia\System\System;
@@ -482,7 +482,6 @@ Http::post('/v1/messaging/providers/msg91')
$enabled === true
&& \array_key_exists('senderId', $credentials)
&& \array_key_exists('authKey', $credentials)
&& \array_key_exists('from', $options)
) {
$enabled = true;
} else {
@@ -1180,6 +1179,7 @@ Http::get('/v1/messaging/providers/:providerId/logs')
'userEmail' => $log['data']['userEmail'] ?? null,
'userName' => $log['data']['userName'] ?? null,
'mode' => $log['data']['mode'] ?? null,
'userType' => $log['data']['userType'] ?? null,
'ip' => $log['ip'],
'time' => $log['time'],
'osCode' => $os['osCode'],
@@ -2585,6 +2585,7 @@ Http::get('/v1/messaging/topics/:topicId/logs')
'userEmail' => $log['data']['userEmail'] ?? null,
'userName' => $log['data']['userName'] ?? null,
'mode' => $log['data']['mode'] ?? null,
'userType' => $log['data']['userType'] ?? null,
'ip' => $log['ip'],
'time' => $log['time'],
'osCode' => $os['osCode'],
@@ -3000,6 +3001,7 @@ Http::get('/v1/messaging/subscribers/:subscriberId/logs')
'userEmail' => $log['data']['userEmail'] ?? null,
'userName' => $log['data']['userName'] ?? null,
'mode' => $log['data']['mode'] ?? null,
'userType' => $log['data']['userType'] ?? null,
'ip' => $log['ip'],
'time' => $log['time'],
'osCode' => $os['osCode'],
@@ -3204,10 +3206,6 @@ Http::post('/v1/messaging/messages/email')
throw new Exception(Exception::MESSAGE_MISSING_TARGET);
}
if ($status === MessageStatus::SCHEDULED && \is_null($scheduledAt)) {
throw new Exception(Exception::MESSAGE_MISSING_SCHEDULE);
}
$mergedTargets = \array_merge($targets, $cc, $bcc);
if (!empty($mergedTargets)) {
@@ -3383,10 +3381,6 @@ Http::post('/v1/messaging/messages/sms')
throw new Exception(Exception::MESSAGE_MISSING_TARGET);
}
if ($status === MessageStatus::SCHEDULED && \is_null($scheduledAt)) {
throw new Exception(Exception::MESSAGE_MISSING_SCHEDULE);
}
if (!empty($targets)) {
$foundTargets = $dbForProject->find('targets', [
Query::equal('$id', $targets),
@@ -3524,10 +3518,6 @@ Http::post('/v1/messaging/messages/push')
throw new Exception(Exception::MESSAGE_MISSING_TARGET);
}
if ($status === MessageStatus::SCHEDULED && \is_null($scheduledAt)) {
throw new Exception(Exception::MESSAGE_MISSING_SCHEDULE);
}
if (!empty($targets)) {
$foundTargets = $dbForProject->find('targets', [
Query::equal('$id', $targets),
@@ -3566,7 +3556,7 @@ Http::post('/v1/messaging/messages/push')
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
$endpoint = "$protocol://{$platform['apiHostname']}/v1";
$scheduleTime = $currentScheduledAt ?? $scheduledAt;
$scheduleTime = $scheduledAt;
if (!\is_null($scheduleTime)) {
$expiry = (new \DateTime($scheduleTime))->add(new \DateInterval('P15D'))->format('U');
} else {
@@ -3813,6 +3803,7 @@ Http::get('/v1/messaging/messages/:messageId/logs')
'userEmail' => $log['data']['userEmail'] ?? null,
'userName' => $log['data']['userName'] ?? null,
'mode' => $log['data']['mode'] ?? null,
'userType' => $log['data']['userType'] ?? null,
'ip' => $log['ip'],
'time' => $log['time'],
'osCode' => $os['osCode'],
@@ -4656,7 +4647,7 @@ Http::delete('/v1/messaging/messages/:messageId')
if (!empty($scheduleId)) {
try {
$dbForPlatform->deleteDocument('schedules', $scheduleId);
} catch (Exception) {
} catch (\Throwable) {
// Ignore
}
}
-957
View File
@@ -1,957 +0,0 @@
<?php
use Appwrite\Event\Event;
use Appwrite\Event\Migration;
use Appwrite\Extend\Exception;
use Appwrite\OpenSSL\OpenSSL;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\CompoundUID;
use Appwrite\Utopia\Database\Validator\Queries\Migrations;
use Appwrite\Utopia\Response;
use Utopia\Compression\Algorithms\GZIP;
use Utopia\Compression\Algorithms\Zstd;
use Utopia\Compression\Compression;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Order as OrderException;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Queries\Documents;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\UID;
use Utopia\Http\Http;
use Utopia\Migration\Resource;
use Utopia\Migration\Sources\Appwrite;
use Utopia\Migration\Sources\CSV;
use Utopia\Migration\Sources\Firebase;
use Utopia\Migration\Sources\NHost;
use Utopia\Migration\Sources\Supabase;
use Utopia\Migration\Transfer;
use Utopia\Storage\Device;
use Utopia\System\System;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
use Utopia\Validator\Integer;
use Utopia\Validator\Text;
use Utopia\Validator\URL;
use Utopia\Validator\WhiteList;
include_once __DIR__ . '/../shared/api.php';
Http::post('/v1/migrations/appwrite')
->groups(['api', 'migrations'])
->desc('Create Appwrite migration')
->label('scope', 'migrations.write')
->label('event', 'migrations.[migrationId].create')
->label('audits.event', 'migration.create')
->label('sdk', new Method(
namespace: 'migrations',
group: null,
name: 'createAppwriteMigration',
description: '/docs/references/migrations/migration-appwrite.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_ACCEPTED,
model: Response::MODEL_MIGRATION,
)
]
))
->param('resources', [], new ArrayList(new WhiteList(Appwrite::getSupportedResources())), 'List of resources to migrate')
->param('endpoint', '', new URL(), 'Source Appwrite endpoint')
->param('projectId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Source Project ID', false, ['dbForProject'])
->param('apiKey', '', new Text(512), 'Source API Key')
->inject('response')
->inject('dbForProject')
->inject('project')
->inject('platform')
->inject('user')
->inject('queueForEvents')
->inject('queueForMigrations')
->action(function (array $resources, string $endpoint, string $projectId, string $apiKey, Response $response, Database $dbForProject, Document $project, array $platform, Document $user, Event $queueForEvents, Migration $queueForMigrations) {
$migration = $dbForProject->createDocument('migrations', new Document([
'$id' => ID::unique(),
'status' => 'pending',
'stage' => 'init',
'source' => Appwrite::getName(),
'destination' => Appwrite::getName(),
'credentials' => [
'endpoint' => $endpoint,
'projectId' => $projectId,
'apiKey' => $apiKey,
],
'resources' => $resources,
'statusCounters' => '{}',
'resourceData' => '{}',
'errors' => [],
]));
$queueForEvents->setParam('migrationId', $migration->getId());
// Trigger Transfer
$queueForMigrations
->setMigration($migration)
->setProject($project)
->setPlatform($platform)
->setUser($user)
->trigger();
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($migration, Response::MODEL_MIGRATION);
});
Http::post('/v1/migrations/firebase')
->groups(['api', 'migrations'])
->desc('Create Firebase migration')
->label('scope', 'migrations.write')
->label('event', 'migrations.[migrationId].create')
->label('audits.event', 'migration.create')
->label('sdk', new Method(
namespace: 'migrations',
group: null,
name: 'createFirebaseMigration',
description: '/docs/references/migrations/migration-firebase.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_ACCEPTED,
model: Response::MODEL_MIGRATION,
)
]
))
->param('resources', [], new ArrayList(new WhiteList(Firebase::getSupportedResources())), 'List of resources to migrate')
->param('serviceAccount', '', new Text(65536), 'JSON of the Firebase service account credentials')
->inject('response')
->inject('dbForProject')
->inject('project')
->inject('platform')
->inject('user')
->inject('queueForEvents')
->inject('queueForMigrations')
->action(function (array $resources, string $serviceAccount, Response $response, Database $dbForProject, Document $project, array $platform, Document $user, Event $queueForEvents, Migration $queueForMigrations) {
$serviceAccountData = json_decode($serviceAccount, true);
if (empty($serviceAccountData)) {
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Invalid Service Account JSON');
}
if (!isset($serviceAccountData['project_id']) || !isset($serviceAccountData['client_email']) || !isset($serviceAccountData['private_key'])) {
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Invalid Service Account JSON');
}
$migration = $dbForProject->createDocument('migrations', new Document([
'$id' => ID::unique(),
'status' => 'pending',
'stage' => 'init',
'source' => Firebase::getName(),
'destination' => Appwrite::getName(),
'credentials' => [
'serviceAccount' => $serviceAccount,
],
'resources' => $resources,
'statusCounters' => '{}',
'resourceData' => '{}',
'errors' => [],
]));
$queueForEvents->setParam('migrationId', $migration->getId());
// Trigger Transfer
$queueForMigrations
->setMigration($migration)
->setProject($project)
->setPlatform($platform)
->setUser($user)
->trigger();
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($migration, Response::MODEL_MIGRATION);
});
Http::post('/v1/migrations/supabase')
->groups(['api', 'migrations'])
->desc('Create Supabase migration')
->label('scope', 'migrations.write')
->label('event', 'migrations.[migrationId].create')
->label('audits.event', 'migration.create')
->label('sdk', new Method(
namespace: 'migrations',
group: null,
name: 'createSupabaseMigration',
description: '/docs/references/migrations/migration-supabase.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_ACCEPTED,
model: Response::MODEL_MIGRATION,
)
]
))
->param('resources', [], new ArrayList(new WhiteList(Supabase::getSupportedResources(), true)), 'List of resources to migrate')
->param('endpoint', '', new URL(), 'Source\'s Supabase Endpoint')
->param('apiKey', '', new Text(512), 'Source\'s API Key')
->param('databaseHost', '', new Text(512), 'Source\'s Database Host')
->param('username', '', new Text(512), 'Source\'s Database Username')
->param('password', '', new Text(512), 'Source\'s Database Password')
->param('port', 5432, new Integer(true), 'Source\'s Database Port', true)
->inject('response')
->inject('dbForProject')
->inject('project')
->inject('platform')
->inject('user')
->inject('queueForEvents')
->inject('queueForMigrations')
->action(function (array $resources, string $endpoint, string $apiKey, string $databaseHost, string $username, string $password, int $port, Response $response, Database $dbForProject, Document $project, array $platform, Document $user, Event $queueForEvents, Migration $queueForMigrations) {
$migration = $dbForProject->createDocument('migrations', new Document([
'$id' => ID::unique(),
'status' => 'pending',
'stage' => 'init',
'source' => Supabase::getName(),
'destination' => Appwrite::getName(),
'credentials' => [
'endpoint' => $endpoint,
'apiKey' => $apiKey,
'databaseHost' => $databaseHost,
'username' => $username,
'password' => $password,
'port' => $port,
],
'resources' => $resources,
'statusCounters' => '{}',
'resourceData' => '{}',
'errors' => [],
]));
$queueForEvents->setParam('migrationId', $migration->getId());
// Trigger Transfer
$queueForMigrations
->setMigration($migration)
->setProject($project)
->setPlatform($platform)
->setUser($user)
->trigger();
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($migration, Response::MODEL_MIGRATION);
});
Http::post('/v1/migrations/nhost')
->groups(['api', 'migrations'])
->desc('Create NHost migration')
->label('scope', 'migrations.write')
->label('event', 'migrations.[migrationId].create')
->label('audits.event', 'migration.create')
->label('sdk', new Method(
namespace: 'migrations',
group: null,
name: 'createNHostMigration',
description: '/docs/references/migrations/migration-nhost.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_ACCEPTED,
model: Response::MODEL_MIGRATION,
)
]
))
->param('resources', [], new ArrayList(new WhiteList(NHost::getSupportedResources())), 'List of resources to migrate')
->param('subdomain', '', new Text(512), 'Source\'s Subdomain')
->param('region', '', new Text(512), 'Source\'s Region')
->param('adminSecret', '', new Text(512), 'Source\'s Admin Secret')
->param('database', '', new Text(512), 'Source\'s Database Name')
->param('username', '', new Text(512), 'Source\'s Database Username')
->param('password', '', new Text(512), 'Source\'s Database Password')
->param('port', 5432, new Integer(true), 'Source\'s Database Port', true)
->inject('response')
->inject('dbForProject')
->inject('project')
->inject('platform')
->inject('user')
->inject('queueForEvents')
->inject('queueForMigrations')
->action(function (array $resources, string $subdomain, string $region, string $adminSecret, string $database, string $username, string $password, int $port, Response $response, Database $dbForProject, Document $project, array $platform, Document $user, Event $queueForEvents, Migration $queueForMigrations) {
$migration = $dbForProject->createDocument('migrations', new Document([
'$id' => ID::unique(),
'status' => 'pending',
'stage' => 'init',
'source' => NHost::getName(),
'destination' => Appwrite::getName(),
'credentials' => [
'subdomain' => $subdomain,
'region' => $region,
'adminSecret' => $adminSecret,
'database' => $database,
'username' => $username,
'password' => $password,
'port' => $port,
],
'resources' => $resources,
'statusCounters' => '{}',
'resourceData' => '{}',
'errors' => [],
]));
$queueForEvents->setParam('migrationId', $migration->getId());
// Trigger Transfer
$queueForMigrations
->setMigration($migration)
->setProject($project)
->setPlatform($platform)
->setUser($user)
->trigger();
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($migration, Response::MODEL_MIGRATION);
});
Http::post('/v1/migrations/csv/imports')
->alias('/v1/migrations/csv')
->groups(['api', 'migrations'])
->desc('Import documents from a CSV')
->label('scope', 'migrations.write')
->label('event', 'migrations.[migrationId].create')
->label('audits.event', 'migration.create')
->label('sdk', new Method(
namespace: 'migrations',
group: null,
name: 'createCSVImport',
description: '/docs/references/migrations/migration-csv-import.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_ACCEPTED,
model: Response::MODEL_MIGRATION,
)
]
))
->param('bucketId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Storage bucket unique ID. You can create a new storage bucket using the Storage service [server integration](https://appwrite.io/docs/server/storage#createBucket).', false, ['dbForProject'])
->param('fileId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'File ID.', false, ['dbForProject'])
->param('resourceId', null, new CompoundUID(), 'Composite ID in the format {databaseId:collectionId}, identifying a collection within a database.')
->param('internalFile', false, new Boolean(), 'Is the file stored in an internal bucket?', true)
->inject('response')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('authorization')
->inject('project')
->inject('platform')
->inject('deviceForFiles')
->inject('deviceForMigrations')
->inject('queueForEvents')
->inject('queueForMigrations')
->action(function (
string $bucketId,
string $fileId,
string $resourceId,
bool $internalFile,
Response $response,
Database $dbForProject,
Database $dbForPlatform,
Authorization $authorization,
Document $project,
array $platform,
Device $deviceForFiles,
Device $deviceForMigrations,
Event $queueForEvents,
Migration $queueForMigrations
) {
$bucket = $authorization->skip(function () use ($internalFile, $dbForPlatform, $dbForProject, $bucketId) {
if ($internalFile) {
return $dbForPlatform->getDocument('buckets', 'default');
}
return $dbForProject->getDocument('buckets', $bucketId);
});
if ($bucket->isEmpty()) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
$file = $authorization->skip(fn () => $internalFile ? $dbForPlatform->getDocument('bucket_' . $bucket->getSequence(), $fileId) : $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId));
if ($file->isEmpty()) {
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
}
$path = $file->getAttribute('path', '');
if (!$deviceForFiles->exists($path)) {
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND, 'File not found in ' . $path);
}
// No encryption or compression on files above 20MB.
$hasEncryption = !empty($file->getAttribute('openSSLCipher'));
$compression = $file->getAttribute('algorithm', Compression::NONE);
$hasCompression = $compression !== Compression::NONE;
$migrationId = ID::unique();
$newPath = $deviceForMigrations->getPath($migrationId . '_' . $fileId . '.csv');
if ($hasEncryption || $hasCompression) {
$source = $deviceForFiles->read($path);
if ($hasEncryption) {
$source = OpenSSL::decrypt(
$source,
$file->getAttribute('openSSLCipher'),
System::getEnv('_APP_OPENSSL_KEY_V' . $file->getAttribute('openSSLVersion')),
0,
hex2bin($file->getAttribute('openSSLIV')),
hex2bin($file->getAttribute('openSSLTag'))
);
}
if ($hasCompression) {
switch ($compression) {
case Compression::ZSTD:
$source = (new Zstd())->decompress($source);
break;
case Compression::GZIP:
$source = (new GZIP())->decompress($source);
break;
}
}
// Manual write after decryption and/or decompression
if (!$deviceForMigrations->write($newPath, $source, 'text/csv')) {
throw new \Exception('Unable to copy file');
}
} elseif (!$deviceForFiles->transfer($path, $newPath, $deviceForMigrations)) {
throw new \Exception('Unable to copy file');
}
$fileSize = $deviceForMigrations->getFileSize($newPath);
$resources = Transfer::extractServices([Transfer::GROUP_DATABASES]);
$migration = $dbForProject->createDocument('migrations', new Document([
'$id' => $migrationId,
'status' => 'pending',
'stage' => 'init',
'source' => CSV::getName(),
'destination' => Appwrite::getName(),
'resources' => $resources,
'resourceId' => $resourceId,
'resourceType' => Resource::TYPE_DATABASE,
'statusCounters' => '{}',
'resourceData' => '{}',
'errors' => [],
'options' => [
'path' => $newPath,
'size' => $fileSize,
],
]));
$queueForEvents->setParam('migrationId', $migration->getId());
$queueForMigrations
->setMigration($migration)
->setProject($project)
->setProject($project)
->trigger();
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($migration, Response::MODEL_MIGRATION);
});
Http::post('/v1/migrations/csv/exports')
->groups(['api', 'migrations'])
->desc('Export documents to CSV')
->label('scope', 'migrations.write')
->label('event', 'migrations.[migrationId].create')
->label('audits.event', 'migration.create')
->label('sdk', new Method(
namespace: 'migrations',
group: null,
name: 'createCSVExport',
description: '/docs/references/migrations/migration-csv-export.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_ACCEPTED,
model: Response::MODEL_MIGRATION,
)
]
))
->param('resourceId', null, new CompoundUID(), 'Composite ID in the format {databaseId:collectionId}, identifying a collection within a database to export.')
->param('filename', '', new Text(255), 'The name of the file to be created for the export, excluding the .csv extension.')
->param('columns', [], new ArrayList(new Text(Database::LENGTH_KEY)), 'List of attributes to export. If empty, all attributes will be exported. You can use the `*` wildcard to export all attributes from the collection.', true)
->param('queries', [], new ArrayList(new Text(0)), 'Array of query strings generated using the Query class provided by the SDK to filter documents to export. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true)
->param('delimiter', ',', new Text(1), 'The character that separates each column value. Default is comma.', true)
->param('enclosure', '"', new Text(1), 'The character that encloses each column value. Default is double quotes.', true)
->param('escape', '"', new Text(1), 'The escape character for the enclosure character. Default is double quotes.', true)
->param('header', true, new Boolean(), 'Whether to include the header row with column names. Default is true.', true)
->param('notify', true, new Boolean(), 'Set to true to receive an email when the export is complete. Default is true.', true)
->inject('user')
->inject('response')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('authorization')
->inject('project')
->inject('platform')
->inject('queueForEvents')
->inject('queueForMigrations')
->action(function (
string $resourceId,
string $filename,
array $columns,
array $queries,
string $delimiter,
string $enclosure,
string $escape,
bool $header,
bool $notify,
Document $user,
Response $response,
Database $dbForProject,
Database $dbForPlatform,
Authorization $authorization,
Document $project,
array $platform,
Event $queueForEvents,
Migration $queueForMigrations
) {
try {
$parsedQueries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$bucket = $authorization->skip(fn () => $dbForPlatform->getDocument('buckets', 'default'));
if ($bucket->isEmpty()) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
[$databaseId, $collectionId] = \explode(':', $resourceId, 2);
if (empty($databaseId)) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
if (empty($collectionId)) {
throw new Exception(Exception::COLLECTION_NOT_FOUND);
}
$database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collection = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId));
if ($collection->isEmpty()) {
throw new Exception(Exception::COLLECTION_NOT_FOUND);
}
$validator = new Documents(
attributes: $collection->getAttribute('attributes', []),
indexes: $collection->getAttribute('indexes', []),
idAttributeType: $dbForProject->getAdapter()->getIdAttributeType(),
);
if (!$validator->isValid($parsedQueries)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
}
$migration = $dbForProject->createDocument('migrations', new Document([
'$id' => ID::unique(),
'status' => 'pending',
'stage' => 'init',
'source' => Appwrite::getName(),
'destination' => CSV::getName(),
'resources' => Transfer::extractServices([Transfer::GROUP_DATABASES]),
'resourceId' => $resourceId,
'resourceType' => Resource::TYPE_DATABASE,
'statusCounters' => '{}',
'resourceData' => '{}',
'errors' => [],
'options' => [
'bucketId' => 'default', // Always use internal bucket
'filename' => $filename,
'columns' => $columns,
'queries' => $queries,
'delimiter' => $delimiter,
'enclosure' => $enclosure,
'escape' => $escape,
'header' => $header,
'notify' => $notify,
'userInternalId' => $user->getSequence(),
],
]));
$queueForEvents->setParam('migrationId', $migration->getId());
$queueForMigrations
->setMigration($migration)
->setProject($project)
->setPlatform($platform)
->trigger();
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($migration, Response::MODEL_MIGRATION);
});
Http::get('/v1/migrations')
->groups(['api', 'migrations'])
->desc('List migrations')
->label('scope', 'migrations.read')
->label('sdk', new Method(
namespace: 'migrations',
group: null,
name: 'list',
description: '/docs/references/migrations/list-migrations.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_MIGRATION_LIST,
)
]
))
->param('queries', [], new Migrations(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Migrations::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('response')
->inject('dbForProject')
->action(function (array $queries, string $search, bool $includeTotal, Response $response, Database $dbForProject) {
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
if (!empty($search)) {
$queries[] = Query::search('search', $search);
}
$cursor = Query::getCursorQueries($queries, false);
$cursor = \reset($cursor);
if ($cursor !== false) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
}
$migrationId = $cursor->getValue();
$cursorDocument = $dbForProject->getDocument('migrations', $migrationId);
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Migration '{$migrationId}' for the 'cursor' value not found.");
}
$cursor->setValue($cursorDocument);
}
$filterQueries = Query::groupByType($queries)['filters'];
try {
$migrations = $dbForProject->find('migrations', $queries);
$total = $includeTotal ? $dbForProject->count('migrations', $filterQueries, APP_LIMIT_COUNT) : 0;
} catch (OrderException $e) {
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
}
$response->dynamic(new Document([
'migrations' => $migrations,
'total' => $total,
]), Response::MODEL_MIGRATION_LIST);
});
Http::get('/v1/migrations/:migrationId')
->groups(['api', 'migrations'])
->desc('Get migration')
->label('scope', 'migrations.read')
->label('sdk', new Method(
namespace: 'migrations',
group: null,
name: 'get',
description: '/docs/references/migrations/get-migration.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_MIGRATION,
)
]
))
->param('migrationId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Migration unique ID.', false, ['dbForProject'])
->inject('response')
->inject('dbForProject')
->action(function (string $migrationId, Response $response, Database $dbForProject) {
$migration = $dbForProject->getDocument('migrations', $migrationId);
if ($migration->isEmpty()) {
throw new Exception(Exception::MIGRATION_NOT_FOUND);
}
$response->dynamic($migration, Response::MODEL_MIGRATION);
});
Http::get('/v1/migrations/appwrite/report')
->groups(['api', 'migrations'])
->desc('Get Appwrite migration report')
->label('scope', 'migrations.write')
->label('sdk', new Method(
namespace: 'migrations',
group: null,
name: 'getAppwriteReport',
description: '/docs/references/migrations/migration-appwrite-report.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_MIGRATION_REPORT,
)
]
))
->param('resources', [], new ArrayList(new WhiteList(Appwrite::getSupportedResources())), 'List of resources to migrate')
->param('endpoint', '', new URL(), "Source's Appwrite Endpoint")
->param('projectID', '', new Text(512), "Source's Project ID")
->param('key', '', new Text(512), "Source's API Key")
->inject('response')
->inject('dbForProject')
->inject('project')
->inject('user')
->action(function (array $resources, string $endpoint, string $projectID, string $key, Response $response) {
try {
$appwrite = new Appwrite($projectID, $endpoint, $key);
$report = $appwrite->report($resources);
} catch (\Throwable $e) {
throw new Exception(
Exception::MIGRATION_PROVIDER_ERROR,
'Unable to connect to the migration source. Please verify your credentials and ensure the source is reachable from this server. Check for network restrictions such as firewalls, IP allowlists, or outbound connectivity limits.'
);
}
$response
->setStatusCode(Response::STATUS_CODE_OK)
->dynamic(new Document($report), Response::MODEL_MIGRATION_REPORT);
});
Http::get('/v1/migrations/firebase/report')
->groups(['api', 'migrations'])
->desc('Get Firebase migration report')
->label('scope', 'migrations.write')
->label('sdk', new Method(
namespace: 'migrations',
group: null,
name: 'getFirebaseReport',
description: '/docs/references/migrations/migration-firebase-report.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_MIGRATION_REPORT,
)
]
))
->param('resources', [], new ArrayList(new WhiteList(Firebase::getSupportedResources())), 'List of resources to migrate')
->param('serviceAccount', '', new Text(65536), 'JSON of the Firebase service account credentials')
->inject('response')
->action(function (array $resources, string $serviceAccount, Response $response) {
$serviceAccount = json_decode($serviceAccount, true);
if (empty($serviceAccount)) {
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Invalid Service Account JSON');
}
if (!isset($serviceAccount['project_id']) || !isset($serviceAccount['client_email']) || !isset($serviceAccount['private_key'])) {
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Invalid Service Account JSON');
}
try {
$firebase = new Firebase($serviceAccount);
$report = $firebase->report($resources);
} catch (\Throwable $e) {
throw new Exception(
Exception::MIGRATION_PROVIDER_ERROR,
'Unable to connect to the migration source. Please verify your credentials and ensure the source is reachable from this server. Check for network restrictions such as firewalls, IP allowlists, or outbound connectivity limits.'
);
}
$response
->setStatusCode(Response::STATUS_CODE_OK)
->dynamic(new Document($report), Response::MODEL_MIGRATION_REPORT);
});
Http::get('/v1/migrations/supabase/report')
->groups(['api', 'migrations'])
->desc('Get Supabase migration report')
->label('scope', 'migrations.write')
->label('sdk', new Method(
namespace: 'migrations',
group: null,
name: 'getSupabaseReport',
description: '/docs/references/migrations/migration-supabase-report.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_MIGRATION_REPORT,
)
]
))
->param('resources', [], new ArrayList(new WhiteList(Supabase::getSupportedResources(), true)), 'List of resources to migrate')
->param('endpoint', '', new URL(), 'Source\'s Supabase Endpoint.')
->param('apiKey', '', new Text(512), 'Source\'s API Key.')
->param('databaseHost', '', new Text(512), 'Source\'s Database Host.')
->param('username', '', new Text(512), 'Source\'s Database Username.')
->param('password', '', new Text(512), 'Source\'s Database Password.')
->param('port', 5432, new Integer(true), 'Source\'s Database Port.', true)
->inject('response')
->inject('dbForProject')
->action(function (array $resources, string $endpoint, string $apiKey, string $databaseHost, string $username, string $password, int $port, Response $response) {
try {
$supabase = new Supabase($endpoint, $apiKey, $databaseHost, 'postgres', $username, $password, $port);
$report = $supabase->report($resources);
} catch (\Throwable $e) {
throw new Exception(
Exception::MIGRATION_PROVIDER_ERROR,
'Unable to connect to the migration source. Please verify your credentials and ensure the source is reachable from this server. Check for network restrictions such as firewalls, IP allowlists, or outbound connectivity limits.'
);
}
$response
->setStatusCode(Response::STATUS_CODE_OK)
->dynamic(new Document($report), Response::MODEL_MIGRATION_REPORT);
});
Http::get('/v1/migrations/nhost/report')
->groups(['api', 'migrations'])
->desc('Get NHost migration report')
->label('scope', 'migrations.write')
->label('sdk', new Method(
namespace: 'migrations',
group: null,
name: 'getNHostReport',
description: '/docs/references/migrations/migration-nhost-report.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_MIGRATION_REPORT,
)
]
))
->param('resources', [], new ArrayList(new WhiteList(NHost::getSupportedResources())), 'List of resources to migrate.')
->param('subdomain', '', new Text(512), 'Source\'s Subdomain.')
->param('region', '', new Text(512), 'Source\'s Region.')
->param('adminSecret', '', new Text(512), 'Source\'s Admin Secret.')
->param('database', '', new Text(512), 'Source\'s Database Name.')
->param('username', '', new Text(512), 'Source\'s Database Username.')
->param('password', '', new Text(512), 'Source\'s Database Password.')
->param('port', 5432, new Integer(true), 'Source\'s Database Port.', true)
->inject('response')
->action(function (array $resources, string $subdomain, string $region, string $adminSecret, string $database, string $username, string $password, int $port, Response $response) {
try {
$nhost = new NHost($subdomain, $region, $adminSecret, $database, $username, $password, $port);
$report = $nhost->report($resources);
} catch (\Throwable $e) {
throw new Exception(
Exception::MIGRATION_PROVIDER_ERROR,
'Unable to connect to the migration source. Please verify your credentials and ensure the source is reachable from this server. Check for network restrictions such as firewalls, IP allowlists, or outbound connectivity limits.'
);
}
$response
->setStatusCode(Response::STATUS_CODE_OK)
->dynamic(new Document($report), Response::MODEL_MIGRATION_REPORT);
});
Http::patch('/v1/migrations/:migrationId')
->groups(['api', 'migrations'])
->desc('Update retry migration')
->label('scope', 'migrations.write')
->label('event', 'migrations.[migrationId].retry')
->label('audits.event', 'migration.retry')
->label('audits.resource', 'migrations/{request.migrationId}')
->label('sdk', new Method(
namespace: 'migrations',
group: null,
name: 'retry',
description: '/docs/references/migrations/retry-migration.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_ACCEPTED,
model: Response::MODEL_MIGRATION,
)
]
))
->param('migrationId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Migration unique ID.', false, ['dbForProject'])
->inject('response')
->inject('dbForProject')
->inject('project')
->inject('platform')
->inject('user')
->inject('queueForMigrations')
->action(function (string $migrationId, Response $response, Database $dbForProject, Document $project, array $platform, Document $user, Migration $queueForMigrations) {
$migration = $dbForProject->getDocument('migrations', $migrationId);
if ($migration->isEmpty()) {
throw new Exception(Exception::MIGRATION_NOT_FOUND);
}
if ($migration->getAttribute('status') !== 'failed') {
throw new Exception(Exception::MIGRATION_IN_PROGRESS, 'Migration not failed yet');
}
$migration
->setAttribute('status', 'pending')
->setAttribute('dateUpdated', \time());
// Trigger Migration
$queueForMigrations
->setMigration($migration)
->setProject($project)
->setPlatform($platform)
->setUser($user)
->trigger();
$response->noContent();
});
Http::delete('/v1/migrations/:migrationId')
->groups(['api', 'migrations'])
->desc('Delete migration')
->label('scope', 'migrations.write')
->label('event', 'migrations.[migrationId].delete')
->label('audits.event', 'migrationId.delete')
->label('audits.resource', 'migrations/{request.migrationId}')
->label('sdk', new Method(
namespace: 'migrations',
group: null,
name: 'delete',
description: '/docs/references/migrations/delete-migration.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_NOCONTENT,
model: Response::MODEL_NONE,
)
],
contentType: ContentType::NONE
))
->param('migrationId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Migration ID.', false, ['dbForProject'])
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->action(function (string $migrationId, Response $response, Database $dbForProject, Event $queueForEvents) {
$migration = $dbForProject->getDocument('migrations', $migrationId);
if ($migration->isEmpty()) {
throw new Exception(Exception::MIGRATION_NOT_FOUND);
}
if (!$dbForProject->deleteDocument('migrations', $migration->getId())) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove migration from DB');
}
$queueForEvents->setParam('migrationId', $migration->getId());
$response->noContent();
});
+62 -236
View File
@@ -1,25 +1,15 @@
<?php
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Datetime as DateTimeValidator;
use Utopia\Database\Validator\UID;
use Utopia\Http\Http;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
Http::get('/v1/project/usage')
@@ -62,16 +52,33 @@ Http::get('/v1/project/usage')
METRIC_EXECUTIONS_MB_SECONDS,
METRIC_BUILDS_MB_SECONDS,
METRIC_DOCUMENTS,
METRIC_DOCUMENTS_DOCUMENTSDB,
METRIC_DATABASES,
METRIC_DATABASES_DOCUMENTSDB,
METRIC_USERS,
METRIC_BUCKETS,
METRIC_FILES_STORAGE,
METRIC_DATABASES_STORAGE,
METRIC_DATABASES_STORAGE_DOCUMENTSDB,
METRIC_DEPLOYMENTS_STORAGE,
METRIC_BUILDS_STORAGE,
METRIC_DATABASES_OPERATIONS_READS,
METRIC_DATABASES_OPERATIONS_READS_DOCUMENTSDB,
METRIC_DATABASES_OPERATIONS_WRITES,
METRIC_DATABASES_OPERATIONS_WRITES_DOCUMENTSDB,
METRIC_FILES_IMAGES_TRANSFORMED,
// VectorsDB totals
METRIC_DATABASES_VECTORSDB,
METRIC_COLLECTIONS_VECTORSDB,
METRIC_DOCUMENTS_VECTORSDB,
METRIC_DATABASES_STORAGE_VECTORSDB,
METRIC_DATABASES_OPERATIONS_READS_VECTORSDB,
METRIC_DATABASES_OPERATIONS_WRITES_VECTORSDB,
// Embeddings totals
METRIC_EMBEDDINGS_TEXT,
METRIC_EMBEDDINGS_TEXT_TOTAL_TOKENS,
METRIC_EMBEDDINGS_TEXT_TOTAL_DURATION,
METRIC_EMBEDDINGS_TEXT_TOTAL_ERROR
],
'period' => [
METRIC_NETWORK_REQUESTS,
@@ -80,22 +87,38 @@ Http::get('/v1/project/usage')
METRIC_USERS,
METRIC_EXECUTIONS,
METRIC_DATABASES_STORAGE,
METRIC_DATABASES_STORAGE_DOCUMENTSDB,
METRIC_EXECUTIONS_MB_SECONDS,
METRIC_BUILDS_MB_SECONDS,
METRIC_DATABASES_OPERATIONS_READS,
METRIC_DATABASES_OPERATIONS_READS_DOCUMENTSDB,
METRIC_DATABASES_OPERATIONS_WRITES,
METRIC_DATABASES_OPERATIONS_WRITES_DOCUMENTSDB,
METRIC_FILES_IMAGES_TRANSFORMED,
// VectorsDB time series
METRIC_DATABASES_VECTORSDB,
METRIC_COLLECTIONS_VECTORSDB,
METRIC_DOCUMENTS_VECTORSDB,
METRIC_DATABASES_STORAGE_VECTORSDB,
METRIC_DATABASES_OPERATIONS_READS_VECTORSDB,
METRIC_DATABASES_OPERATIONS_WRITES_VECTORSDB,
// Embeddings time series
METRIC_EMBEDDINGS_TEXT,
METRIC_EMBEDDINGS_TEXT_TOTAL_TOKENS,
METRIC_EMBEDDINGS_TEXT_TOTAL_DURATION,
METRIC_EMBEDDINGS_TEXT_TOTAL_ERROR
]
];
$factor = match ($period) {
'1h' => 3600,
'1d' => 86400,
default => throw new \LogicException('Unsupported period: ' . $period),
};
$limit = match ($period) {
'1h' => (new DateTime($startDate))->diff(new DateTime($endDate))->days * 24,
'1d' => (new DateTime($startDate))->diff(new DateTime($endDate))->days
'1d' => (new DateTime($startDate))->diff(new DateTime($endDate))->days,
};
$format = match ($period) {
@@ -357,8 +380,11 @@ Http::get('/v1/project/usage')
'buildsMbSecondsTotal' => $total[METRIC_BUILDS_MB_SECONDS],
'documentsTotal' => $total[METRIC_DOCUMENTS],
'rowsTotal' => $total[METRIC_DOCUMENTS],
'documentsdbDocumentsTotal' => $total[METRIC_DOCUMENTS_DOCUMENTSDB],
'databasesTotal' => $total[METRIC_DATABASES],
'documentsdbTotal' => $total[METRIC_DATABASES_DOCUMENTSDB],
'databasesStorageTotal' => $total[METRIC_DATABASES_STORAGE],
'documentsdbDatabasesStorageTotal' => $total[METRIC_DATABASES_STORAGE_DOCUMENTSDB],
'usersTotal' => $total[METRIC_USERS],
'bucketsTotal' => $total[METRIC_BUCKETS],
'filesStorageTotal' => $total[METRIC_FILES_STORAGE],
@@ -367,10 +393,27 @@ Http::get('/v1/project/usage')
'deploymentsStorageTotal' => $total[METRIC_DEPLOYMENTS_STORAGE],
'databasesReadsTotal' => $total[METRIC_DATABASES_OPERATIONS_READS],
'databasesWritesTotal' => $total[METRIC_DATABASES_OPERATIONS_WRITES],
'documentsdbDatabasesReadsTotal' => $total[METRIC_DATABASES_OPERATIONS_READS_DOCUMENTSDB],
'documentsdbDatabasesWritesTotal' => $total[METRIC_DATABASES_OPERATIONS_WRITES_DOCUMENTSDB],
'vectorsdbDatabasesTotal' => $total[METRIC_DATABASES_VECTORSDB] ?? 0,
'vectorsdbCollectionsTotal' => $total[METRIC_COLLECTIONS_VECTORSDB] ?? 0,
'vectorsdbDocumentsTotal' => $total[METRIC_DOCUMENTS_VECTORSDB] ?? 0,
'vectorsdbDatabasesStorageTotal' => $total[METRIC_DATABASES_STORAGE_VECTORSDB] ?? 0,
'vectorsdbDatabasesReadsTotal' => $total[METRIC_DATABASES_OPERATIONS_READS_VECTORSDB] ?? 0,
'vectorsdbDatabasesWritesTotal' => $total[METRIC_DATABASES_OPERATIONS_WRITES_VECTORSDB] ?? 0,
'executionsBreakdown' => $executionsBreakdown,
'bucketsBreakdown' => $bucketsBreakdown,
'databasesReads' => $usage[METRIC_DATABASES_OPERATIONS_READS],
'databasesWrites' => $usage[METRIC_DATABASES_OPERATIONS_WRITES],
'documentsdbDatabasesReads' => $usage[METRIC_DATABASES_OPERATIONS_READS_DOCUMENTSDB],
'documentsdbDatabasesWrites' => $usage[METRIC_DATABASES_OPERATIONS_WRITES_DOCUMENTSDB],
'documentsdbDatabasesStorage' => $usage[METRIC_DATABASES_STORAGE_DOCUMENTSDB],
'vectorsdbDatabases' => $usage[METRIC_DATABASES_VECTORSDB] ?? [],
'vectorsdbCollections' => $usage[METRIC_COLLECTIONS_VECTORSDB] ?? [],
'vectorsdbDocuments' => $usage[METRIC_DOCUMENTS_VECTORSDB] ?? [],
'vectorsdbDatabasesStorage' => $usage[METRIC_DATABASES_STORAGE_VECTORSDB] ?? [],
'vectorsdbDatabasesReads' => $usage[METRIC_DATABASES_OPERATIONS_READS_VECTORSDB] ?? [],
'vectorsdbDatabasesWrites' => $usage[METRIC_DATABASES_OPERATIONS_WRITES_VECTORSDB] ?? [],
'databasesStorageBreakdown' => $databasesStorageBreakdown,
'executionsMbSecondsBreakdown' => $executionsMbSecondsBreakdown,
'buildsMbSecondsBreakdown' => $buildsMbSecondsBreakdown,
@@ -380,230 +423,13 @@ Http::get('/v1/project/usage')
'authPhoneCountryBreakdown' => $authPhoneCountryBreakdown,
'imageTransformations' => $usage[METRIC_FILES_IMAGES_TRANSFORMED],
'imageTransformationsTotal' => $total[METRIC_FILES_IMAGES_TRANSFORMED],
'embeddingsText' => $usage[METRIC_EMBEDDINGS_TEXT] ?? [],
'embeddingsTextTokens' => $usage[METRIC_EMBEDDINGS_TEXT_TOTAL_TOKENS] ?? [],
'embeddingsTextDuration' => $usage[METRIC_EMBEDDINGS_TEXT_TOTAL_DURATION] ?? [],
'embeddingsTextErrors' => $usage[METRIC_EMBEDDINGS_TEXT_TOTAL_ERROR] ?? [],
'embeddingsTextTotal' => $total[METRIC_EMBEDDINGS_TEXT] ?? 0,
'embeddingsTextTokensTotal' => $total[METRIC_EMBEDDINGS_TEXT_TOTAL_TOKENS] ?? 0,
'embeddingsTextDurationTotal' => $total[METRIC_EMBEDDINGS_TEXT_TOTAL_DURATION] ?? 0,
'embeddingsTextErrorsTotal' => $total[METRIC_EMBEDDINGS_TEXT_TOTAL_ERROR] ?? 0,
]), Response::MODEL_USAGE_PROJECT);
});
// Variables
Http::post('/v1/project/variables')
->desc('Create variable')
->groups(['api'])
->label('scope', 'projects.write')
->label('audits.event', 'variable.create')
->label('sdk', new Method(
namespace: 'project',
group: null,
name: 'createVariable',
description: '/docs/references/project/create-variable.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
model: Response::MODEL_VARIABLE,
)
]
))
->param('key', null, new Text(Database::LENGTH_KEY), 'Variable key. Max length: ' . Database::LENGTH_KEY . ' chars.', false)
->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', false)
->param('secret', true, new Boolean(), 'Secret variables can be updated or deleted, but only projects can read them during build and runtime.', true)
->inject('project')
->inject('response')
->inject('dbForProject')
->inject('dbForPlatform')
->action(function (string $key, string $value, bool $secret, Document $project, Response $response, Database $dbForProject, Database $dbForPlatform) {
$variableId = ID::unique();
$variable = new Document([
'$id' => $variableId,
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'resourceInternalId' => '',
'resourceId' => '',
'resourceType' => 'project',
'key' => $key,
'value' => $value,
'secret' => $secret,
'search' => implode(' ', [$variableId, $key, 'project']),
]);
try {
$variable = $dbForProject->createDocument('variables', $variable);
} catch (DuplicateException $th) {
throw new Exception(Exception::VARIABLE_ALREADY_EXISTS);
}
$functions = $dbForProject->find('functions', [
Query::limit(APP_LIMIT_SUBQUERY)
]);
foreach ($functions as $function) {
$dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false));
}
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($variable, Response::MODEL_VARIABLE);
});
Http::get('/v1/project/variables')
->desc('List variables')
->groups(['api'])
->label('scope', 'projects.read')
->label('sdk', new Method(
namespace: 'project',
group: null,
name: 'listVariables',
description: '/docs/references/project/list-variables.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_VARIABLE_LIST,
)
]
))
->inject('response')
->inject('dbForProject')
->action(function (Response $response, Database $dbForProject) {
$variables = $dbForProject->find('variables', [
Query::equal('resourceType', ['project']),
Query::limit(APP_LIMIT_SUBQUERY)
]);
$response->dynamic(new Document([
'variables' => $variables,
'total' => \count($variables),
]), Response::MODEL_VARIABLE_LIST);
});
Http::get('/v1/project/variables/:variableId')
->desc('Get variable')
->groups(['api'])
->label('scope', 'projects.read')
->label('sdk', new Method(
namespace: 'project',
group: null,
name: 'getVariable',
description: '/docs/references/project/get-variable.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_VARIABLE,
)
]
))
->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID.', false, ['dbForProject'])
->inject('response')
->inject('project')
->inject('dbForProject')
->action(function (string $variableId, Response $response, Document $project, Database $dbForProject) {
$variable = $dbForProject->getDocument('variables', $variableId);
if ($variable === false || $variable->isEmpty() || $variable->getAttribute('resourceType') !== 'project') {
throw new Exception(Exception::VARIABLE_NOT_FOUND);
}
$response->dynamic($variable, Response::MODEL_VARIABLE);
});
Http::put('/v1/project/variables/:variableId')
->desc('Update variable')
->groups(['api'])
->label('scope', 'projects.write')
->label('sdk', new Method(
namespace: 'project',
group: null,
name: 'updateVariable',
description: '/docs/references/project/update-variable.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_VARIABLE,
)
]
))
->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID.', false, ['dbForProject'])
->param('key', null, new Text(255), 'Variable key. Max length: 255 chars.', false)
->param('value', null, new Nullable(new Text(8192, 0)), 'Variable value. Max length: 8192 chars.', true)
->param('secret', null, new Nullable(new Boolean()), 'Secret variables can be updated or deleted, but only projects can read them during build and runtime.', true)
->inject('project')
->inject('response')
->inject('dbForProject')
->inject('dbForPlatform')
->action(function (string $variableId, string $key, ?string $value, ?bool $secret, Document $project, Response $response, Database $dbForProject, Database $dbForPlatform) {
$variable = $dbForProject->getDocument('variables', $variableId);
if ($variable === false || $variable->isEmpty() || $variable->getAttribute('resourceType') !== 'project') {
throw new Exception(Exception::VARIABLE_NOT_FOUND);
}
if ($variable->getAttribute('secret') === true && $secret === false) {
throw new Exception(Exception::VARIABLE_CANNOT_UNSET_SECRET);
}
$variable
->setAttribute('key', $key)
->setAttribute('value', $value ?? $variable->getAttribute('value'))
->setAttribute('secret', $secret ?? $variable->getAttribute('secret'))
->setAttribute('search', implode(' ', [$variableId, $key, 'project']));
try {
$dbForProject->updateDocument('variables', $variable->getId(), $variable);
} catch (DuplicateException $th) {
throw new Exception(Exception::VARIABLE_ALREADY_EXISTS);
}
$functions = $dbForProject->find('functions', [
Query::limit(APP_LIMIT_SUBQUERY)
]);
foreach ($functions as $function) {
$dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false));
}
$response->dynamic($variable, Response::MODEL_VARIABLE);
});
Http::delete('/v1/project/variables/:variableId')
->desc('Delete variable')
->groups(['api'])
->label('scope', 'projects.write')
->label('sdk', new Method(
namespace: 'project',
group: null,
name: 'deleteVariable',
description: '/docs/references/project/delete-variable.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_NOCONTENT,
model: Response::MODEL_NONE,
)
],
contentType: ContentType::NONE
))
->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID.', false, ['dbForProject'])
->inject('project')
->inject('response')
->inject('dbForProject')
->action(function (string $variableId, Document $project, Response $response, Database $dbForProject) {
$variable = $dbForProject->getDocument('variables', $variableId);
if ($variable === false || $variable->isEmpty() || $variable->getAttribute('resourceType') !== 'project') {
throw new Exception(Exception::VARIABLE_NOT_FOUND);
}
$dbForProject->deleteDocument('variables', $variable->getId());
$functions = $dbForProject->find('functions', [
Query::limit(APP_LIMIT_SUBQUERY)
]);
foreach ($functions as $function) {
$dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false));
}
$response->noContent();
});
File diff suppressed because it is too large Load Diff
+157 -56
View File
@@ -15,7 +15,6 @@ use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Hooks\Hooks;
use Appwrite\Network\Validator\Email as EmailValidator;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Deprecated;
@@ -60,6 +59,7 @@ use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Database\Validator\UID;
use Utopia\Emails\Email;
use Utopia\Emails\Validator\Email as EmailValidator;
use Utopia\Http\Http;
use Utopia\Locale\Locale;
use Utopia\System\System;
@@ -73,7 +73,7 @@ use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
/** TODO: Remove function when we move to using utopia/platform */
function createUser(Hash $hash, string $userId, ?string $email, ?string $password, ?string $phone, ?string $name, Document $project, Database $dbForProject, Hooks $hooks): Document
function createUser(Hash $hash, string $userId, ?string $email, ?string $password, ?string $phone, ?string $name, Document $project, Database $dbForProject, Hooks $hooks, array $plan): Document
{
$name = $name ?? '';
$plaintextPassword = $password;
@@ -110,11 +110,39 @@ function createUser(Hash $hash, string $userId, ?string $email, ?string $passwor
}
}
$emailMetadata = [
'emailCanonical' => null,
'emailIsCanonical' => null,
'emailIsCorporate' => null,
'emailIsDisposable' => null,
'emailIsFree' => null,
];
try {
$emailCanonical = new Email($email);
} catch (Throwable) {
$emailCanonical = null;
$parsedEmail = new Email($email ?? '');
$canonical = $parsedEmail->getCanonical();
$emailMetadata = [
'emailCanonical' => $canonical,
'emailIsCanonical' => $parsedEmail->get() === $canonical,
'emailIsCorporate' => $parsedEmail->isCorporate(),
'emailIsDisposable' => $parsedEmail->isDisposable(),
'emailIsFree' => $parsedEmail->isFree(),
];
} catch (\Throwable) {
}
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
throw new Exception(Exception::USER_EMAIL_DISPOSABLE);
}
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL);
}
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
throw new Exception(Exception::USER_EMAIL_FREE);
}
$hashedPassword = null;
$isHashed = !$hash instanceof Plaintext;
@@ -159,11 +187,11 @@ function createUser(Hash $hash, string $userId, ?string $email, ?string $passwor
'tokens' => null,
'memberships' => null,
'search' => implode(' ', [$userId, $email, $phone, $name]),
'emailCanonical' => $emailCanonical?->getCanonical(),
'emailIsCanonical' => $emailCanonical?->isCanonicalSupported(),
'emailIsCorporate' => $emailCanonical?->isCorporate(),
'emailIsDisposable' => $emailCanonical?->isDisposable(),
'emailIsFree' => $emailCanonical?->isFree(),
'emailCanonical' => $emailMetadata['emailCanonical'],
'emailIsCanonical' => $emailMetadata['emailIsCanonical'],
'emailIsCorporate' => $emailMetadata['emailIsCorporate'],
'emailIsDisposable' => $emailMetadata['emailIsDisposable'],
'emailIsFree' => $emailMetadata['emailIsFree'],
]);
if (!$isHashed && !empty($password)) {
@@ -256,10 +284,11 @@ Http::post('/v1/users')
->inject('project')
->inject('dbForProject')
->inject('hooks')
->action(function (string $userId, ?string $email, ?string $phone, ?string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
->inject('plan')
->action(function (string $userId, ?string $email, ?string $phone, ?string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks, array $plan) {
$plaintext = new Plaintext();
$user = createUser($plaintext, $userId, $email, $password, $phone, $name, $project, $dbForProject, $hooks);
$user = createUser($plaintext, $userId, $email, $password, $phone, $name, $project, $dbForProject, $hooks, $plan);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($user, Response::MODEL_USER);
@@ -292,11 +321,12 @@ Http::post('/v1/users/bcrypt')
->inject('project')
->inject('dbForProject')
->inject('hooks')
->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
->inject('plan')
->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks, array $plan) {
$bcrypt = new Bcrypt();
$bcrypt->setCost(8); // Default cost
$user = createUser($bcrypt, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$user = createUser($bcrypt, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks, $plan);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -330,10 +360,11 @@ Http::post('/v1/users/md5')
->inject('project')
->inject('dbForProject')
->inject('hooks')
->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
->inject('plan')
->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks, array $plan) {
$md5 = new MD5();
$user = createUser($md5, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$user = createUser($md5, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks, $plan);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -367,10 +398,11 @@ Http::post('/v1/users/argon2')
->inject('project')
->inject('dbForProject')
->inject('hooks')
->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
->inject('plan')
->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks, array $plan) {
$argon2 = new Argon2();
$user = createUser($argon2, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$user = createUser($argon2, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks, $plan);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -405,13 +437,14 @@ Http::post('/v1/users/sha')
->inject('project')
->inject('dbForProject')
->inject('hooks')
->action(function (string $userId, string $email, string $password, string $passwordVersion, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
->inject('plan')
->action(function (string $userId, string $email, string $password, string $passwordVersion, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks, array $plan) {
$sha = new Sha();
if (!empty($passwordVersion)) {
$sha->setVersion($passwordVersion);
}
$user = createUser($sha, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$user = createUser($sha, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks, $plan);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -445,10 +478,11 @@ Http::post('/v1/users/phpass')
->inject('project')
->inject('dbForProject')
->inject('hooks')
->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
->inject('plan')
->action(function (string $userId, string $email, string $password, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks, array $plan) {
$phpass = new PHPass();
$user = createUser($phpass, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$user = createUser($phpass, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks, $plan);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -487,7 +521,8 @@ Http::post('/v1/users/scrypt')
->inject('project')
->inject('dbForProject')
->inject('hooks')
->action(function (string $userId, string $email, string $password, string $passwordSalt, int $passwordCpu, int $passwordMemory, int $passwordParallel, int $passwordLength, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
->inject('plan')
->action(function (string $userId, string $email, string $password, string $passwordSalt, int $passwordCpu, int $passwordMemory, int $passwordParallel, int $passwordLength, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks, array $plan) {
$scrypt = new Scrypt();
$scrypt
->setSalt($passwordSalt)
@@ -496,7 +531,7 @@ Http::post('/v1/users/scrypt')
->setParallelCost($passwordParallel)
->setLength($passwordLength);
$user = createUser($scrypt, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$user = createUser($scrypt, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks, $plan);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -533,14 +568,15 @@ Http::post('/v1/users/scrypt-modified')
->inject('project')
->inject('dbForProject')
->inject('hooks')
->action(function (string $userId, string $email, string $password, string $passwordSalt, string $passwordSaltSeparator, string $passwordSignerKey, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
->inject('plan')
->action(function (string $userId, string $email, string $password, string $passwordSalt, string $passwordSaltSeparator, string $passwordSignerKey, ?string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks, array $plan) {
$scryptModified = new ScryptModified();
$scryptModified
->setSalt($passwordSalt)
->setSaltSeparator($passwordSaltSeparator)
->setSignerKey($passwordSignerKey);
$user = createUser($scryptModified, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
$user = createUser($scryptModified, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks, $plan);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
@@ -820,7 +856,7 @@ Http::get('/v1/users/:userId/targets/:targetId')
Http::get('/v1/users/:userId/sessions')
->desc('List user sessions')
->groups(['api', 'users'])
->label('scope', 'users.read')
->label('scope', ['users.read', 'sessions.read'])
->label('sdk', new Method(
namespace: 'users',
group: 'sessions',
@@ -972,6 +1008,8 @@ Http::get('/v1/users/:userId/logs')
'userId' => ID::custom($log['data']['userId']),
'userEmail' => $log['data']['userEmail'] ?? null,
'userName' => $log['data']['userName'] ?? null,
'mode' => $log['data']['mode'] ?? null,
'userType' => $log['data']['userType'] ?? null,
'ip' => $log['ip'],
'time' => $log['time'],
'osCode' => $os['osCode'],
@@ -1212,6 +1250,47 @@ Http::put('/v1/users/:userId/labels')
$response->dynamic($user, Response::MODEL_USER);
});
Http::patch('/v1/users/:userId/impersonator')
->desc('Update user impersonator capability')
->groups(['api', 'users'])
->label('event', 'users.[userId].update.impersonator')
->label('scope', 'users.write')
->label('audits.event', 'user.update')
->label('audits.resource', 'user/{response.$id}')
->label('sdk', new Method(
namespace: 'users',
group: 'users',
name: 'updateImpersonator',
description: '/docs/references/users/update-user-impersonator.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_USER,
)
]
))
->param('userId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'User ID.', false, ['dbForProject'])
->param('impersonator', false, new Boolean(true), 'Whether the user can impersonate other users. When true, the user can browse project users to choose a target and can pass impersonation headers to act as that user. Internal audit logs still attribute impersonated actions to the original impersonator and store the target user details only in internal audit payload data.')
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->action(function (string $userId, bool $impersonator, Response $response, Database $dbForProject, Event $queueForEvents) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
$user = $dbForProject->updateDocument('users', $user->getId(), new Document(['impersonator' => $impersonator]));
$queueForEvents
->setParam('userId', $user->getId());
$response->dynamic($user, Response::MODEL_USER);
});
Http::patch('/v1/users/:userId/verification/phone')
->desc('Update phone verification')
->groups(['api', 'users'])
@@ -1429,8 +1508,10 @@ Http::patch('/v1/users/:userId/email')
->param('email', '', new EmailValidator(allowEmpty: true), 'User email.')
->inject('response')
->inject('dbForProject')
->inject('project')
->inject('plan')
->inject('queueForEvents')
->action(function (string $userId, string $email, Response $response, Database $dbForProject, Event $queueForEvents) {
->action(function (string $userId, string $email, Response $response, Database $dbForProject, Document $project, array $plan, Event $queueForEvents) {
$user = $dbForProject->getDocument('users', $userId);
@@ -1454,27 +1535,54 @@ Http::patch('/v1/users/:userId/email')
Query::equal('identifier', [$email]),
]);
if ($target instanceof Document && !$target->isEmpty()) {
if (!$target->isEmpty()) {
throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS);
}
}
$oldEmail = $user->getAttribute('email');
$emailMetadata = [
'emailCanonical' => null,
'emailIsCanonical' => null,
'emailIsCorporate' => null,
'emailIsDisposable' => null,
'emailIsFree' => null,
];
try {
$emailCanonical = new Email($email);
} catch (Throwable) {
$emailCanonical = null;
$parsedEmail = new Email($email);
$canonical = $parsedEmail->getCanonical();
$emailMetadata = [
'emailCanonical' => $canonical,
'emailIsCanonical' => $parsedEmail->get() === $canonical,
'emailIsCorporate' => $parsedEmail->isCorporate(),
'emailIsDisposable' => $parsedEmail->isDisposable(),
'emailIsFree' => $parsedEmail->isFree(),
];
} catch (\Throwable) {
}
if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
throw new Exception(Exception::USER_EMAIL_DISPOSABLE);
}
if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL);
}
if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
throw new Exception(Exception::USER_EMAIL_FREE);
}
$user
->setAttribute('email', $email)
->setAttribute('emailVerification', false)
->setAttribute('emailCanonical', $emailCanonical?->getCanonical())
->setAttribute('emailIsCanonical', $emailCanonical?->isCanonicalSupported())
->setAttribute('emailIsCorporate', $emailCanonical?->isCorporate())
->setAttribute('emailIsDisposable', $emailCanonical?->isDisposable())
->setAttribute('emailIsFree', $emailCanonical?->isFree())
->setAttribute('emailCanonical', $emailMetadata['emailCanonical'])
->setAttribute('emailIsCanonical', $emailMetadata['emailIsCanonical'])
->setAttribute('emailIsCorporate', $emailMetadata['emailIsCorporate'])
->setAttribute('emailIsDisposable', $emailMetadata['emailIsDisposable'])
->setAttribute('emailIsFree', $emailMetadata['emailIsFree'])
;
try {
@@ -1487,9 +1595,6 @@ Http::patch('/v1/users/:userId/email')
'emailIsDisposable' => $user->getAttribute('emailIsDisposable'),
'emailIsFree' => $user->getAttribute('emailIsFree'),
]));
/**
* @var Document $oldTarget
*/
$oldTarget = $user->find('identifier', $oldEmail, 'targets');
if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) {
@@ -1573,7 +1678,7 @@ Http::patch('/v1/users/:userId/phone')
Query::equal('identifier', [$number]),
]);
if ($target instanceof Document && !$target->isEmpty()) {
if (!$target->isEmpty()) {
throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS);
}
}
@@ -1583,9 +1688,6 @@ Http::patch('/v1/users/:userId/phone')
'phone' => $phoneValue,
'phoneVerification' => $user->getAttribute('phoneVerification'),
]));
/**
* @var Document $oldTarget
*/
$oldTarget = $user->find('identifier', $oldPhone, 'targets');
if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) {
@@ -2144,8 +2246,8 @@ Http::delete('/v1/users/:userId/mfa/authenticators/:type')
->label('event', 'users.[userId].delete.mfa')
->label('scope', 'users.write')
->label('audits.event', 'user.update')
->label('audits.resource', 'user/{response.$id}')
->label('audits.userId', '{response.$id}')
->label('audits.resource', 'user/{request.userId}')
->label('audits.userId', '{request.userId}')
->label('usage.metric', 'users.{scope}.requests.update')
->label('sdk', [
new Method(
@@ -2212,7 +2314,7 @@ Http::post('/v1/users/:userId/sessions')
->desc('Create session')
->groups(['api', 'users'])
->label('event', 'users.[userId].sessions.[sessionId].create')
->label('scope', 'users.write')
->label('scope', ['users.write', 'sessions.write'])
->label('audits.event', 'session.create')
->label('audits.resource', 'user/{request.userId}')
->label('usage.metric', 'sessions.{scope}.requests.create')
@@ -2296,9 +2398,8 @@ Http::post('/v1/users/:userId/sessions')
->setParam('sessionId', $session->getId())
->setPayload($response->output($session, Response::MODEL_SESSION));
return $response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($session, Response::MODEL_SESSION);
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($session, Response::MODEL_SESSION);
});
Http::post('/v1/users/:userId/tokens')
@@ -2361,16 +2462,15 @@ Http::post('/v1/users/:userId/tokens')
->setParam('tokenId', $token->getId())
->setPayload($response->output($token, Response::MODEL_TOKEN));
return $response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($token, Response::MODEL_TOKEN);
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($token, Response::MODEL_TOKEN);
});
Http::delete('/v1/users/:userId/sessions/:sessionId')
->desc('Delete user session')
->groups(['api', 'users'])
->label('event', 'users.[userId].sessions.[sessionId].delete')
->label('scope', 'users.write')
->label('scope', ['users.write', 'sessions.write'])
->label('audits.event', 'session.delete')
->label('audits.resource', 'user/{request.userId}')
->label('sdk', new Method(
@@ -2421,7 +2521,7 @@ Http::delete('/v1/users/:userId/sessions')
->desc('Delete user sessions')
->groups(['api', 'users'])
->label('event', 'users.[userId].sessions.delete')
->label('scope', 'users.write')
->label('scope', ['users.write', 'sessions.write'])
->label('audits.event', 'session.delete')
->label('audits.resource', 'user/{user.$id}')
->label('sdk', new Method(
@@ -2617,7 +2717,7 @@ Http::delete('/v1/users/identities/:identityId')
->setParam('identityId', $identity->getId())
->setPayload($response->output($identity, Response::MODEL_IDENTITY));
return $response->noContent();
$response->noContent();
});
Http::post('/v1/users/:userId/jwts')
@@ -2736,6 +2836,7 @@ Http::get('/v1/users/usage')
$format = match ($days['period']) {
'1h' => 'Y-m-d\TH:00:00.000P',
'1d' => 'Y-m-d\T00:00:00.000P',
default => throw new \LogicException('Unsupported period: ' . $days['period']),
};
foreach ($metrics as $metric) {
+129 -103
View File
@@ -7,10 +7,11 @@ use Ahc\Jwt\JWTException;
use Appwrite\Auth\Key;
use Appwrite\Bus\Events\ExecutionCompleted;
use Appwrite\Bus\Events\RequestCompleted;
use Appwrite\Event\Certificate;
use Appwrite\Event\Delete as DeleteEvent;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Certificate;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Locking\Lock;
use Appwrite\Network\Cors;
use Appwrite\Platform\Appwrite;
use Appwrite\SDK\Method;
@@ -25,6 +26,9 @@ use Appwrite\Utopia\Request\Filters\V18 as RequestV18;
use Appwrite\Utopia\Request\Filters\V19 as RequestV19;
use Appwrite\Utopia\Request\Filters\V20 as RequestV20;
use Appwrite\Utopia\Request\Filters\V21 as RequestV21;
use Appwrite\Utopia\Request\Filters\V22 as RequestV22;
use Appwrite\Utopia\Request\Filters\V23 as RequestV23;
use Appwrite\Utopia\Request\Filters\V24 as RequestV24;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Filters\V16 as ResponseV16;
use Appwrite\Utopia\Response\Filters\V17 as ResponseV17;
@@ -32,6 +36,9 @@ use Appwrite\Utopia\Response\Filters\V18 as ResponseV18;
use Appwrite\Utopia\Response\Filters\V19 as ResponseV19;
use Appwrite\Utopia\Response\Filters\V20 as ResponseV20;
use Appwrite\Utopia\Response\Filters\V21 as ResponseV21;
use Appwrite\Utopia\Response\Filters\V22 as ResponseV22;
use Appwrite\Utopia\Response\Filters\V23 as ResponseV23;
use Appwrite\Utopia\Response\Filters\V24 as ResponseV24;
use Appwrite\Utopia\View;
use Executor\Executor;
use MaxMind\Db\Reader;
@@ -61,13 +68,11 @@ use Utopia\System\System;
use Utopia\Validator;
use Utopia\Validator\Text;
Config::setParam('domainVerification', false);
Config::setParam('cookieDomain', 'localhost');
Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE);
function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Authorization $authorization, ?Key $apiKey, DeleteEvent $queueForDeletes, int $executionsRetentionCount)
function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Authorization $authorization, ?Key $apiKey, DeleteEvent $queueForDeletes, int $executionsRetentionCount, Lock $lock)
{
$host = $request->getHostname() ?? '';
$host = $request->getHostname();
if (!empty($previewHostname)) {
$host = $previewHostname;
}
@@ -118,7 +123,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
}
}
if (!in_array($host, $platformHostnames)) {
if (!in_array($host, $platformHostnames) && System::getEnv('_APP_OPTIONS_ROUTER_PROTECTION', 'enabled') === 'enabled') {
throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'Router protection does not allow accessing Appwrite over this domain. Please add it as custom domain to your project or disable _APP_OPTIONS_ROUTER_PROTECTION environment variable.', view: $errorView);
}
@@ -134,9 +139,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
if (!$project->isEmpty() && $project->getId() !== 'console') {
$accessedAt = $project->getAttribute('accessedAt', 0);
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) {
$authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), new Document([
'accessedAt' => DateTime::now()
])));
$lock->set('projects', $project->getId(), 'accessedAt', DateTime::now());
}
/**
@@ -166,14 +169,14 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
if ($request->getMethod() !== Request::METHOD_GET) {
throw new AppwriteException(AppwriteException::GENERAL_PROTOCOL_UNSUPPORTED, 'Method unsupported over HTTP. Please use HTTPS instead.', view: $errorView);
}
return $response->redirect('https://' . $request->getHostname() . $request->getURI());
$response->redirect('https://' . $request->getHostname() . $request->getURI());
return false;
}
}
/** @var Database $dbForProject */
$dbForProject = $getProjectDB($project);
/** @var Document $deployment */
if (!empty($rule->getAttribute('deploymentId', ''))) {
$deployment = $authorization->skip(fn () => $dbForProject->getDocument('deployments', $rule->getAttribute('deploymentId')));
} else {
@@ -200,12 +203,6 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
$deployment = $authorization->skip(fn () => $dbForProject->getDocument('deployments', $activeDeploymentId));
}
if ($deployment->getAttribute('resourceType', '') === 'functions') {
$type = 'function';
} elseif ($deployment->getAttribute('resourceType', '') === 'sites') {
$type = 'site';
}
if ($deployment->isEmpty()) {
$resourceType = $rule->getAttribute('deploymentResourceType', '');
$resourceId = $rule->getAttribute('deploymentResourceId', '');
@@ -215,6 +212,14 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
throw $exception;
}
if ($deployment->getAttribute('resourceType', '') === 'functions') {
$type = 'function';
} elseif ($deployment->getAttribute('resourceType', '') === 'sites') {
$type = 'site';
} else {
throw new AppwriteException(AppwriteException::GENERAL_SERVER_ERROR, 'Unknown deployment resource type', view: $errorView);
}
$resource = $type === 'function' ?
$authorization->skip(fn () => $dbForProject->getDocument('functions', $deployment->getAttribute('resourceId', ''))) :
$authorization->skip(fn () => $dbForProject->getDocument('sites', $deployment->getAttribute('resourceId', '')));
@@ -244,6 +249,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
if ($isPreview && $requirePreview) {
$cookie = $request->getCookie(COOKIE_NAME_PREVIEW, '');
$authorized = false;
$user = new Document();
// Security checks to mark authorized true
if (!empty($cookie)) {
@@ -273,7 +279,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
$membershipExists = false;
$project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
if (!$project->isEmpty() && isset($user)) {
if (!$project->isEmpty() && !$user->isEmpty()) {
$teamId = $project->getAttribute('teamId', '');
$membership = $user->find('teamId', $teamId, 'memberships');
if (!empty($membership)) {
@@ -301,13 +307,13 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
}
}
$body = $swooleRequest->getContent() ?? '';
$body = $swooleRequest->getContent() ?: '';
$method = $swooleRequest->server['request_method'];
$requestHeaders = $request->getHeaders();
if ($resource->isEmpty() || !$resource->getAttribute('enabled')) {
if ($type === 'functions') {
if ($type === 'function') {
throw new AppwriteException(AppwriteException::FUNCTION_NOT_FOUND, view: $errorView);
} else {
throw new AppwriteException(AppwriteException::SITE_NOT_FOUND, view: $errorView);
@@ -329,7 +335,6 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
$runtime = match ($type) {
'function' => $runtimes[$resource->getAttribute('runtime')] ?? null,
'site' => $runtimes[$resource->getAttribute('buildRuntime')] ?? null,
default => null
};
// Static site enforced runtime
@@ -379,7 +384,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
$executionId = ID::unique();
$headers = \array_merge([], $requestHeaders);
$headers['x-appwrite-execution-id'] = $executionId ?? '';
$headers['x-appwrite-execution-id'] = $executionId;
$headers['x-appwrite-user-id'] = '';
$headers['x-appwrite-country-code'] = '';
$headers['x-appwrite-continent-code'] = '';
@@ -393,7 +398,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
'projectId' => $project->getId(),
'scopes' => $resource->getAttribute('scopes', [])
]);
$headers['x-appwrite-key'] = API_KEY_DYNAMIC . '_' . $jwtKey;
$headers['x-appwrite-key'] = API_KEY_EPHEMERAL . '_' . $jwtKey;
$headers['x-appwrite-trigger'] = 'http';
$headers['x-appwrite-user-jwt'] = '';
@@ -458,10 +463,10 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
// V2 vars
if ($version === 'v2') {
$vars = \array_merge($vars, [
'APPWRITE_FUNCTION_TRIGGER' => $headers['x-appwrite-trigger'] ?? '',
'APPWRITE_FUNCTION_DATA' => $body ?? '',
'APPWRITE_FUNCTION_USER_ID' => $headers['x-appwrite-user-id'] ?? '',
'APPWRITE_FUNCTION_JWT' => $headers['x-appwrite-user-jwt'] ?? ''
'APPWRITE_FUNCTION_TRIGGER' => $headers['x-appwrite-trigger'],
'APPWRITE_FUNCTION_DATA' => $body,
'APPWRITE_FUNCTION_USER_ID' => $headers['x-appwrite-user-id'],
'APPWRITE_FUNCTION_JWT' => $headers['x-appwrite-user-jwt']
]);
}
@@ -529,6 +534,11 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
}
/** Execute function */
$executionResponse = [
'headers' => [],
'body' => '',
];
try {
$version = match ($type) {
'function' => $resource->getAttribute('version', 'v2'),
@@ -672,9 +682,8 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
if (\is_string($logs) && \strlen($logs) > $maxLogLength) {
$warningMessage = "[WARNING] Logs truncated. The output exceeded {$maxLogLength} characters.\n";
$warningLength = \strlen($warningMessage);
$maxContentLength = max(0, $maxLogLength - $warningLength);
$logs = $warningMessage . ($maxContentLength > 0 ? \substr($logs, -$maxContentLength) : '');
$maxContentLength = $maxLogLength - \strlen($warningMessage);
$logs = $warningMessage . \substr($logs, -$maxContentLength);
}
// Truncate errors if they exceed the limit
@@ -683,9 +692,8 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
if (\is_string($errors) && \strlen($errors) > $maxErrorLength) {
$warningMessage = "[WARNING] Errors truncated. The output exceeded {$maxErrorLength} characters.\n";
$warningLength = \strlen($warningMessage);
$maxContentLength = max(0, $maxErrorLength - $warningLength);
$errors = $warningMessage . ($maxContentLength > 0 ? \substr($errors, -$maxContentLength) : '');
$maxContentLength = $maxErrorLength - \strlen($warningMessage);
$errors = $warningMessage . \substr($errors, -$maxContentLength);
}
/** Update execution status */
$status = $executionResponse['statusCode'] >= 500 ? 'failed' : 'completed';
@@ -713,14 +721,12 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
throw $th;
}
} finally {
if ($type === 'function' || $type === 'site') {
$bus->dispatch(new ExecutionCompleted(
execution: $execution->getArrayCopy(),
project: $project->getArrayCopy(),
spec: $spec,
resource: $resource->getArrayCopy(),
));
}
$bus->dispatch(new ExecutionCompleted(
execution: $execution->getArrayCopy(),
project: $project->getArrayCopy(),
spec: $spec,
resource: $resource->getArrayCopy(),
));
}
$execution->setAttribute('logs', '');
@@ -734,7 +740,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
$execution->setAttribute('responseBody', $executionResponse['body'] ?? '');
$execution->setAttribute('responseHeaders', $headers);
$body = $execution['responseBody'] ?? '';
$body = $execution['responseBody'];
$contentType = 'text/plain';
foreach ($executionResponse['headers'] as $name => $values) {
@@ -748,11 +754,8 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
}
if (\is_array($values)) {
$count = 0;
foreach ($values as $value) {
$override = $count === 0;
$response->addHeader($name, $value, override: $override);
$count++;
$response->addHeader($name, $value);
}
} else {
$response->addHeader($name, $values);
@@ -845,15 +848,16 @@ Http::init()
->inject('authorization')
->inject('queueForDeletes')
->inject('executionsRetentionCount')
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, Reader $geodb, Event $queueForEvents, Bus $bus, Executor $executor, array $platform, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
->inject('lock')
->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, DeleteEvent $queueForDeletes, int $executionsRetentionCount, Lock $lock) {
/*
* Appwrite Router
*/
$hostname = $request->getHostname() ?? '';
$hostname = $request->getHostname();
$platformHostnames = $platform['hostnames'] ?? [];
// Only run Router when external domain
if (!\in_array($hostname, $platformHostnames) || !empty($previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount, $lock)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -862,12 +866,12 @@ Http::init()
* Request format
*/
$route = $utopia->getRoute();
Request::setRoute($route);
$request->setRoute($route);
if ($route === null) {
return $response
->setStatusCode(404)
->send('Not Found');
$response->setStatusCode(404);
$response->send('Not Found');
return;
}
$requestFormat = $request->getHeader('x-appwrite-response-format', System::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
@@ -891,6 +895,15 @@ Http::init()
if (version_compare($requestFormat, '1.9.0', '<')) {
$request->addFilter(new RequestV21());
}
if (version_compare($requestFormat, '1.9.1', '<')) {
$request->addFilter(new RequestV22());
}
if (version_compare($requestFormat, '1.9.2', '<')) {
$request->addFilter(new RequestV23());
}
if (version_compare($requestFormat, '1.9.3', '<')) {
$request->addFilter(new RequestV24());
}
}
$localeParam = (string) $request->getParam('locale', $request->getHeader('x-appwrite-locale', ''));
@@ -898,40 +911,16 @@ Http::init()
$locale->setDefault($localeParam);
}
$origin = \parse_url($request->getOrigin($request->getReferer('')), PHP_URL_HOST);
$selfDomain = new Domain($request->getHostname());
$endDomain = new Domain((string)$origin);
Config::setParam(
'domainVerification',
($selfDomain->getRegisterable() === $endDomain->getRegisterable()) &&
$endDomain->getRegisterable() !== ''
);
$localHosts = ['localhost','localhost:'.$request->getPort()];
$migrationHost = System::getEnv('_APP_MIGRATION_HOST');
if (!empty($migrationHost)) {
// Treat the migration host like localhost because internal migration and
// CI traffic may use it before a public domain is configured.
$localHosts[] = $migrationHost;
$localHosts[] = $migrationHost.':'.$request->getPort();
}
$isLocalHost = in_array($request->getHostname(), $localHosts);
$isIpAddress = filter_var($request->getHostname(), FILTER_VALIDATE_IP) !== false;
$isConsoleProject = $project->getAttribute('$id', '') === 'console';
$isConsoleRootSession = System::getEnv('_APP_CONSOLE_ROOT_SESSION', 'disabled') === 'enabled';
Config::setParam(
'cookieDomain',
$isLocalHost || $isIpAddress
? null
: (
$isConsoleProject && $isConsoleRootSession
? '.' . $selfDomain->getRegisterable()
: '.' . $request->getHostname()
)
);
$warnings = [];
/*
@@ -939,6 +928,15 @@ Http::init()
*/
$responseFormat = $request->getHeader('x-appwrite-response-format', System::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
if ($responseFormat) {
if (version_compare($responseFormat, '1.9.3', '<')) {
$response->addFilter(new ResponseV24());
}
if (version_compare($responseFormat, '1.9.2', '<')) {
$response->addFilter(new ResponseV23());
}
if (version_compare($responseFormat, '1.9.1', '<')) {
$response->addFilter(new ResponseV22());
}
if (version_compare($responseFormat, '1.9.0', '<')) {
$response->addFilter(new ResponseV21());
}
@@ -973,7 +971,8 @@ Http::init()
throw new AppwriteException(AppwriteException::GENERAL_PROTOCOL_UNSUPPORTED, 'Method unsupported over HTTP. Please use HTTPS instead.');
}
return $response->redirect('https://' . $request->getHostname() . $request->getURI());
$response->redirect('https://' . $request->getHostname() . $request->getURI());
return;
}
}
});
@@ -1012,7 +1011,7 @@ Http::init()
return;
}
$route = $request->getRoute();
if ($route->getLabel('origin', false) === '*') {
if ($route?->getLabel('origin', false) === '*') {
return;
}
if (!$originValidator->isValid($origin)) {
@@ -1028,11 +1027,11 @@ Http::init()
->inject('request')
->inject('console')
->inject('dbForPlatform')
->inject('queueForCertificates')
->inject('publisherForCertificates')
->inject('platform')
->inject('authorization')
->inject('certifiedDomains')
->action(function (Request $request, Document $console, Database $dbForPlatform, Certificate $queueForCertificates, array $platform, Authorization $authorization, Table $certifiedDomains) {
->action(function (Request $request, Document $console, Database $dbForPlatform, Certificate $publisherForCertificates, array $platform, Authorization $authorization, Table $certifiedDomains) {
$hostname = $request->getHostname();
$platformHostnames = $platform['hostnames'] ?? [];
@@ -1058,7 +1057,7 @@ Http::init()
}
// 4. Check/create rule (requires DB access)
$authorization->skip(function () use ($dbForPlatform, $domain, $console, $queueForCertificates, $certifiedDomains) {
$authorization->skip(function () use ($dbForPlatform, $domain, $console, $publisherForCertificates, $certifiedDomains) {
try {
// TODO: (@Meldiron) Remove after 1.7.x migration
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
@@ -1114,10 +1113,11 @@ Http::init()
$dbForPlatform->createDocument('rules', $document);
Console::info('Issuing a TLS certificate for the main domain (' . $domain->get() . ') in a few seconds...');
$queueForCertificates
->setDomain($document)
->setSkipRenewCheck(true)
->trigger();
$publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate(
project: $console,
domain: $document,
skipRenewCheck: true,
));
} catch (Duplicate $e) {
Console::info('Certificate already exists');
} finally {
@@ -1148,14 +1148,15 @@ Http::options()
->inject('authorization')
->inject('queueForDeletes')
->inject('executionsRetentionCount')
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
->inject('lock')
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount, Lock $lock) {
/*
* Appwrite Router
*/
$platformHostnames = $platform['hostnames'] ?? [];
// Only run Router when external domain
if (!in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount, $lock)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -1199,9 +1200,7 @@ Http::error()
$line = $error->getLine();
$trace = $error->getTrace();
if (php_sapi_name() === 'cli') {
Span::error($error);
}
Span::error($error);
switch ($class) {
case Utopia\Http\Exception::class:
@@ -1261,7 +1260,16 @@ Http::error()
* If not a publishable error, track usage stats. Publishable errors are >= 500 or those explicitly marked as publish=true in errors.php
*/
if (!$publish && $project->getId() !== 'console') {
if (!DBUser::isPrivileged($authorization->getRoles())) {
$errorUser = new DBUser();
try {
$resolvedUser = $utopia->getResource('user');
if ($resolvedUser instanceof DBUser) {
$errorUser = $resolvedUser;
}
} catch (\Throwable) {
// User resource may not be available in error context
}
if (!$errorUser->isPrivileged($authorization->getRoles())) {
$bus->dispatch(new RequestCompleted(
project: $project->getArrayCopy(),
request: $request,
@@ -1434,6 +1442,7 @@ Http::error()
case 402: // Error allowed publicly
case 403: // Error allowed publicly
case 404: // Error allowed publicly
case 405: // Error allowed publicly
case 408: // Error allowed publicly
case 409: // Error allowed publicly
case 412: // Error allowed publicly
@@ -1468,6 +1477,21 @@ Http::error()
'type' => $type,
];
// Add CORS headers to error responses so browsers can read the error.
// Wrapped in try-catch: if the error itself is a DB failure, resolving
// the cors resource (which depends on rule -> DB) would cascade.
// Uses override:true to avoid duplicate headers if init() already set them.
try {
$cors = $utopia->getResource('cors');
foreach ($cors->headers($request->getOrigin()) as $name => $value) {
$response
->removeHeader($name)
->addHeader($name, $value);
}
} catch (Throwable) {
// Degrade gracefully - error response without CORS is no worse than before.
}
$response
->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
->addHeader('Expires', '0')
@@ -1489,9 +1513,9 @@ Http::error()
->setParam('development', Http::isDevelopment())
->setParam('projectName', $project->getAttribute('name'))
->setParam('projectURL', $project->getAttribute('url'))
->setParam('message', $output['message'] ?? '')
->setParam('type', $output['type'] ?? '')
->setParam('code', $output['code'] ?? '')
->setParam('message', $output['message'])
->setParam('type', $output['type'])
->setParam('code', $output['code'])
->setParam('trace', $output['trace'] ?? [])
->setParam('exception', $error);
@@ -1527,13 +1551,14 @@ Http::get('/robots.txt')
->inject('authorization')
->inject('queueForDeletes')
->inject('executionsRetentionCount')
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
->inject('lock')
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount, Lock $lock) {
$platformHostnames = $platform['hostnames'] ?? [];
if (in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
$template = new View(__DIR__ . '/../views/general/robots.phtml');
$response->text($template->render(false));
} else {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount, $lock)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -1561,13 +1586,14 @@ Http::get('/humans.txt')
->inject('authorization')
->inject('queueForDeletes')
->inject('executionsRetentionCount')
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
->inject('lock')
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount, Lock $lock) {
$platformHostnames = $platform['hostnames'] ?? [];
if (in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
$template = new View(__DIR__ . '/../views/general/humans.phtml');
$response->text($template->render(false));
} else {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount, $lock)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -1606,7 +1632,7 @@ Http::get('/.well-known/acme-challenge/*')
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND, 'Unknown path');
}
if (!\substr($absolute, 0, \strlen($base)) === $base) {
if (\substr($absolute, 0, \strlen($base)) !== $base) {
throw new AppwriteException(AppwriteException::GENERAL_UNAUTHORIZED_SCOPE, 'Invalid path');
}
@@ -1685,7 +1711,7 @@ Http::get('/_appwrite/authorize')
->inject('previewHostname')
->action(function (Request $request, Response $response, string $previewHostname) {
$host = $request->getHostname() ?? '';
$host = $request->getHostname();
if (!empty($previewHostname)) {
$host = $previewHostname;
}
+30 -28
View File
@@ -244,36 +244,38 @@ Http::get('/v1/mock/github/callback')
throw new Exception(Exception::PROJECT_NOT_FOUND, $error);
}
if (!empty($providerInstallationId)) {
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
$owner = $github->getOwnerName($providerInstallationId) ?? '';
$projectInternalId = $project->getSequence();
$teamId = $project->getAttribute('teamId', '');
$installation = new Document([
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::team(ID::custom($teamId))),
Permission::update(Role::team(ID::custom($teamId), 'owner')),
Permission::update(Role::team(ID::custom($teamId), 'developer')),
Permission::delete(Role::team(ID::custom($teamId), 'owner')),
Permission::delete(Role::team(ID::custom($teamId), 'developer')),
],
'providerInstallationId' => $providerInstallationId,
'projectId' => $projectId,
'projectInternalId' => $projectInternalId,
'provider' => 'github',
'organization' => $owner,
'personal' => false
]);
$installation = $dbForPlatform->createDocument('installations', $installation);
if (empty($providerInstallationId)) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Missing provider installation ID');
}
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
$owner = $github->getOwnerName($providerInstallationId);
$projectInternalId = $project->getSequence();
$teamId = $project->getAttribute('teamId', '');
$installation = new Document([
'$id' => ID::unique(),
'$permissions' => [
Permission::read(Role::team(ID::custom($teamId))),
Permission::update(Role::team(ID::custom($teamId), 'owner')),
Permission::update(Role::team(ID::custom($teamId), 'developer')),
Permission::delete(Role::team(ID::custom($teamId), 'owner')),
Permission::delete(Role::team(ID::custom($teamId), 'developer')),
],
'providerInstallationId' => $providerInstallationId,
'projectId' => $projectId,
'projectInternalId' => $projectInternalId,
'provider' => 'github',
'organization' => $owner,
'personal' => false
]);
$installation = $dbForPlatform->createDocument('installations', $installation);
$response->json([
'installationId' => $installation->getId(),
]);
+141 -78
View File
@@ -3,21 +3,24 @@
use Appwrite\Auth\Key;
use Appwrite\Auth\MFA\Type\TOTP;
use Appwrite\Bus\Events\RequestCompleted;
use Appwrite\Event\Audit;
use Appwrite\Event\Build;
use Appwrite\Event\Context\Audit as AuditContext;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Message\Audit as AuditMessage;
use Appwrite\Event\Message\Usage as UsageMessage;
use Appwrite\Event\Messaging;
use Appwrite\Event\Publisher\Audit;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Event\Realtime;
use Appwrite\Event\Webhook;
use Appwrite\Extend\Exception;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Functions\EventProcessor;
use Appwrite\Locking\Lock;
use Appwrite\SDK\Method;
use Appwrite\Usage\Context;
use Appwrite\Utopia\Database\Documents\User;
@@ -37,13 +40,14 @@ use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Authorization\Input;
use Utopia\Database\Validator\Roles;
use Utopia\Http\Http;
use Utopia\Span\Span;
use Utopia\System\System;
use Utopia\Telemetry\Adapter as Telemetry;
use Utopia\Validator\WhiteList;
$parseLabel = function (string $label, array $responsePayload, array $requestParams, User $user) {
$parseLabel = function (string $label, array $responsePayload, array $requestParams, User $user, Document $project) {
preg_match_all('/{(.*?)}/', $label, $matches);
foreach ($matches[1] ?? [] as $pos => $match) {
foreach ($matches[1] as $pos => $match) {
$find = $matches[0][$pos];
$parts = explode('.', $match);
@@ -51,11 +55,12 @@ $parseLabel = function (string $label, array $responsePayload, array $requestPar
throw new Exception(Exception::GENERAL_SERVER_ERROR, "The server encountered an error while parsing the label: $label. Please create an issue on GitHub to allow us to investigate further https://github.com/appwrite/appwrite/issues/new/choose");
}
$namespace = $parts[0] ?? '';
$replace = $parts[1] ?? '';
$namespace = $parts[0];
$replace = $parts[1];
$params = match ($namespace) {
'user' => (array) $user,
'project' => $project->getArrayCopy(),
'request' => $requestParams,
default => $responsePayload,
};
@@ -87,7 +92,7 @@ Http::init()
->inject('request')
->inject('dbForPlatform')
->inject('dbForProject')
->inject('queueForAudits')
->inject('auditContext')
->inject('project')
->inject('user')
->inject('session')
@@ -96,8 +101,12 @@ Http::init()
->inject('team')
->inject('apiKey')
->inject('authorization')
->action(function (Http $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, Document $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization) {
->inject('lock')
->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, Lock $lock) {
$route = $utopia->getRoute();
if ($route === null) {
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND);
}
/**
* Handle user authentication and session validation.
@@ -176,20 +185,21 @@ Http::init()
// Handle special app role case
if ($apiKey->getRole() === User::ROLE_APPS) {
// Disable authorization checks for project API keys
if (($apiKey->getType() === API_KEY_STANDARD || $apiKey->getType() === API_KEY_DYNAMIC) && $apiKey->getProjectId() === $project->getId()) {
// Dynamic supported for backwards compatibility
if (($apiKey->getType() === API_KEY_STANDARD || $apiKey->getType() === API_KEY_EPHEMERAL || $apiKey->getType() === 'dynamic') && $apiKey->getProjectId() === $project->getId()) {
$authorization->setDefaultStatus(false);
}
$user = new User([
'$id' => '',
'status' => true,
'type' => ACTIVITY_TYPE_APP,
'type' => ACTIVITY_TYPE_KEY_PROJECT,
'email' => 'app.' . $project->getId() . '@service.' . $request->getHostname(),
'password' => '',
'name' => $apiKey->getName(),
]);
$queueForAudits->setUser($user);
$auditContext->user = $user;
}
// For standard keys, update last accessed time
@@ -237,23 +247,31 @@ Http::init()
$sdks[] = $sdk;
$updates->setAttribute('sdks', $sdks);
$updates->setAttribute('accessedAt', Datetime::now());
$updates->setAttribute('accessedAt', DateTime::now());
}
}
if (! $updates->isEmpty()) {
$dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->updateDocument('keys', $dbKey->getId(), $updates));
$lock->run('keys', $dbKey->getId(), function () use ($dbForPlatform, $dbKey, $updates, $apiKey, $project, $user, $team) {
$dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->updateDocument('keys', $dbKey->getId(), $updates));
if (! empty($apiKey->getProjectId())) {
$dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
} elseif (! empty($apiKey->getUserId())) {
$dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->purgeCachedDocument('users', $user->getId()));
} elseif (! empty($apiKey->getTeamId())) {
$dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->purgeCachedDocument('teams', $team->getId()));
}
if (! empty($apiKey->getProjectId())) {
$dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
} elseif (! empty($apiKey->getUserId())) {
$dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->purgeCachedDocument('users', $user->getId()));
} elseif (! empty($apiKey->getTeamId())) {
$dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->purgeCachedDocument('teams', $team->getId()));
}
});
}
$queueForAudits->setUser($user);
$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,
});
$auditContext->user = $userClone;
}
// Apply permission
@@ -338,6 +356,19 @@ Http::init()
$scopes = \array_unique($scopes);
// Intentional: impersonators get users.read so they can discover a target user
// before impersonation starts, and keep that access while impersonating.
if (
!$user->isEmpty()
&& (
$user->getAttribute('impersonator', false)
|| $user->getAttribute('impersonatorUserId')
)
) {
$scopes[] = 'users.read';
$scopes = \array_unique($scopes);
}
$authorization->addRole($role);
foreach ($user->getRoles($authorization) as $authRole) {
$authorization->addRole($authRole);
@@ -359,18 +390,19 @@ Http::init()
}
// Step 6: Update project and user last activity
if (! $project->isEmpty() && $project->getId() !== 'console') {
if ($project->getId() !== 'console') {
$accessedAt = $project->getAttribute('accessedAt', 0);
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) {
$authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), new Document([
'accessedAt' => DateTime::now()
])));
$lock->set('projects', $project->getId(), 'accessedAt', DateTime::now());
}
}
if (! empty($user->getId())) {
$impersonatorUserId = $user->getAttribute('impersonatorUserId');
$accessedAt = $user->getAttribute('accessedAt', 0);
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_USER_ACCESS)) > $accessedAt) {
// Skip updating accessedAt for impersonated requests so we don't attribute activity to the target user.
if (! $impersonatorUserId && DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_USER_ACCESS)) > $accessedAt) {
$user->setAttribute('accessedAt', DateTime::now());
if ($project->getId() !== 'console' && $mode !== APP_MODE_ADMIN) {
@@ -378,17 +410,12 @@ Http::init()
'accessedAt' => $user->getAttribute('accessedAt')
]));
} else {
$authorization->skip(fn () => $dbForPlatform->updateDocument('users', $user->getId(), new Document([
'accessedAt' => $user->getAttribute('accessedAt')
])));
$lock->set('users', $user->getId(), 'accessedAt', $user->getAttribute('accessedAt'));
}
}
}
// Steps 7-9: Access Control - Method, Namespace and Scope Validation
/**
* @var ?Method $method
*/
$method = $route->getLabel('sdk', false);
// Take the first method if there's more than one,
@@ -398,17 +425,26 @@ Http::init()
}
if (! empty($method)) {
$namespace = $method->getNamespace();
$namespace = \strtolower($method->getNamespace());
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->isApp($authorization->getRoles()))
) {
throw new Exception(Exception::GENERAL_SERVICE_DISABLED);
}
}
// Step 8b: Check REST protocol status
if (
array_key_exists('rest', $project->getAttribute('apis', []))
&& ! $project->getAttribute('apis', [])['rest']
&& ! ($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles()))
) {
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
}
// Step 9: Validate scope permissions
$allowed = (array) $route->getLabel('scope', 'none');
if (empty(\array_intersect($allowed, $scopes))) {
@@ -450,7 +486,7 @@ Http::init()
->inject('user')
->inject('queueForEvents')
->inject('queueForMessaging')
->inject('queueForAudits')
->inject('auditContext')
->inject('queueForDeletes')
->inject('queueForDatabase')
->inject('queueForBuilds')
@@ -467,18 +503,23 @@ Http::init()
->inject('telemetry')
->inject('platform')
->inject('authorization')
->action(function (Http $utopia, Request $request, Response $response, Document $project, Document $user, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Context $usage, Func $queueForFunctions, Mail $queueForMails, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry, array $platform, Authorization $authorization) {
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Messaging $queueForMessaging, AuditContext $auditContext, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Context $usage, Func $queueForFunctions, Mail $queueForMails, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry, array $platform, Authorization $authorization) {
$response->setUser($user);
$request->setUser($user);
$route = $utopia->getRoute();
if (
array_key_exists('rest', $project->getAttribute('apis', []))
&& ! $project->getAttribute('apis', [])['rest']
&& ! (User::isPrivileged($authorization->getRoles()) || User::isApp($authorization->getRoles()))
) {
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
if ($route === null) {
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND);
}
$path = $route->getMatchedPath();
$databaseType = match (true) {
str_contains($path, '/documentsdb') => DATABASE_TYPE_DOCUMENTSDB,
str_contains($path, '/vectorsdb') => DATABASE_TYPE_VECTORSDB,
default => '',
};
/*
* Abuse Check
*/
@@ -506,8 +547,8 @@ Http::init()
$closestLimit = null;
$roles = $authorization->getRoles();
$isPrivilegedUser = User::isPrivileged($roles);
$isAppUser = User::isApp($roles);
$isPrivilegedUser = $user->isPrivileged($roles);
$isAppUser = $user->isApp($roles);
foreach ($timeLimitArray as $timeLimit) {
foreach ($request->getParams() as $key => $value) { // Set request params as potential abuse keys
@@ -555,20 +596,21 @@ Http::init()
->setProject($project)
->setUser($user);
$queueForAudits
->setMode($mode)
->setUserAgent($request->getUserAgent(''))
->setIP($request->getIP())
->setHostname($request->getHostname())
->setEvent($route->getLabel('audits.event', ''))
->setProject($project);
$auditContext->mode = $mode;
$auditContext->userAgent = $request->getUserAgent('');
$auditContext->ip = $request->getIP();
$auditContext->hostname = $request->getHostname();
$auditContext->event = $route->getLabel('audits.event', '');
$auditContext->project = $project;
/* If a session exists, use the user associated with the session */
if (! $user->isEmpty()) {
$userClone = clone $user;
// $user doesn't support `type` and can cause unintended effects.
$userClone->setAttribute('type', ACTIVITY_TYPE_USER);
$queueForAudits->setUser($userClone);
if (empty($user->getAttribute('type'))) {
$userClone->setAttribute('type', $mode === APP_MODE_ADMIN ? ACTIVITY_TYPE_ADMIN : ACTIVITY_TYPE_USER);
}
$auditContext->user = $userClone;
}
/* Auto-set projects */
@@ -589,9 +631,10 @@ Http::init()
if ($useCache) {
$route = $utopia->match($request);
$isImageTransformation = $route->getPath() === '/v1/storage/buckets/:bucketId/files/:fileId/preview';
$isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && ! User::isPrivileged($authorization->getRoles());
$isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && ! $user->isPrivileged($authorization->getRoles());
$key = $request->cacheIdentifier();
Span::add('storage.cache.key', $key);
$cacheLog = $authorization->skip(fn () => $dbForProject->getDocument('cache', $key));
$cache = new Cache(
new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId())
@@ -601,14 +644,14 @@ Http::init()
if (! empty($data) && ! $cacheLog->isEmpty()) {
$parts = explode('/', $cacheLog->getAttribute('resourceType', ''));
$type = $parts[0] ?? null;
$type = $parts[0];
if ($type === 'bucket' && (! $isImageTransformation || ! $isDisabled)) {
$bucketId = $parts[1] ?? null;
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isToken = ! $resourceToken->isEmpty() && $resourceToken->getAttribute('bucketInternalId') === $bucket->getSequence();
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
if ($bucket->isEmpty() || (! $bucket->getAttribute('enabled') && ! $isAppUser && ! $isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
@@ -640,8 +683,10 @@ Http::init()
if ($file->isEmpty()) {
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
}
Span::add('storage.bucket.id', $bucketId);
Span::add('storage.file.id', $fileId);
// Do not update transformedAt if it's a console user
if (! User::isPrivileged($authorization->getRoles())) {
if (! $user->isPrivileged($authorization->getRoles())) {
$transformedAt = $file->getAttribute('transformedAt', '');
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $transformedAt) {
$file->setAttribute('transformedAt', DateTime::now());
@@ -652,16 +697,27 @@ Http::init()
}
}
$accessedAt = $cacheLog->getAttribute('accessedAt', '');
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_CACHE_UPDATE)) > $accessedAt) {
$authorization->skip(fn () => $dbForProject->updateDocument('cache', $cacheLog->getId(), new Document([
'accessedAt' => DateTime::now(),
])));
// Refresh the filesystem file's mtime so TTL-based expiry in cache->load() stays valid
$cache->save($key, $data);
}
$response
->addHeader('Cache-Control', sprintf('private, max-age=%d', $timestamp))
->addHeader('X-Appwrite-Cache', 'hit')
->setContentType($cacheLog->getAttribute('mimeType'));
$storageCacheOperationsCounter->add(1, ['result' => 'hit']);
if (! $isImageTransformation || ! $isDisabled) {
Span::add('storage.cache.hit', true);
$response->send($data);
}
} else {
$storageCacheOperationsCounter->add(1, ['result' => 'miss']);
Span::add('storage.cache.hit', false);
$response
->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
->addHeader('Pragma', 'no-cache')
@@ -675,7 +731,7 @@ Http::init()
->groups(['session'])
->inject('user')
->inject('request')
->action(function (Document $user, Request $request) {
->action(function (User $user, Request $request) {
if (\str_contains($request->getURI(), 'oauth2')) {
return;
}
@@ -699,7 +755,12 @@ Http::shutdown()
->inject('project')
->inject('dbForProject')
->action(function (Http $utopia, Request $request, Response $response, Document $project, Database $dbForProject) {
$sessionLimit = $project->getAttribute('auths', [])['maxSessions'] ?? APP_LIMIT_USER_SESSIONS_DEFAULT;
$sessionLimit = $project->getAttribute('auths', [])['maxSessions'] ?? 0;
if ($sessionLimit === 0) {
return;
}
$session = $response->getPayload();
$userId = $session['userId'] ?? '';
if (empty($userId)) {
@@ -733,7 +794,8 @@ Http::shutdown()
->inject('project')
->inject('user')
->inject('queueForEvents')
->inject('queueForAudits')
->inject('auditContext')
->inject('publisherForAudits')
->inject('usage')
->inject('publisherForUsage')
->inject('queueForDeletes')
@@ -749,7 +811,8 @@ Http::shutdown()
->inject('eventProcessor')
->inject('bus')
->inject('apiKey')
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Audit $queueForAudits, Context $usage, UsagePublisher $publisherForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor, Bus $bus, ?Key $apiKey) use ($parseLabel) {
->inject('mode')
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Audit $publisherForAudits, Context $usage, UsagePublisher $publisherForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor, Bus $bus, ?Key $apiKey, string $mode) use ($parseLabel) {
$responsePayload = $response->getPayload();
@@ -842,18 +905,20 @@ Http::shutdown()
*/
$pattern = $route->getLabel('audits.resource', null);
if (! empty($pattern)) {
$resource = $parseLabel($pattern, $responsePayload, $requestParams, $user);
$resource = $parseLabel($pattern, $responsePayload, $requestParams, $user, $project);
if (! empty($resource) && $resource !== $pattern) {
$queueForAudits->setResource($resource);
$auditContext->resource = $resource;
}
}
if (! $user->isEmpty()) {
$userClone = clone $user;
// $user doesn't support `type` and can cause unintended effects.
$userClone->setAttribute('type', ACTIVITY_TYPE_USER);
$queueForAudits->setUser($userClone);
} elseif ($queueForAudits->getUser() === null || $queueForAudits->getUser()->isEmpty()) {
if (empty($user->getAttribute('type'))) {
$userClone->setAttribute('type', $mode === APP_MODE_ADMIN ? ACTIVITY_TYPE_ADMIN : ACTIVITY_TYPE_USER);
}
$auditContext->user = $userClone;
} elseif ($auditContext->user === null || $auditContext->user->isEmpty()) {
/**
* User in the request is empty, and no user was set for auditing previously.
* This indicates:
@@ -871,24 +936,21 @@ Http::shutdown()
'name' => 'Guest',
]);
$queueForAudits->setUser($user);
$auditContext->user = $user;
}
if (! empty($queueForAudits->getResource()) && ! $queueForAudits->getUser()->isEmpty()) {
$auditUser = $auditContext->user;
if (! empty($auditContext->resource) && ! $auditUser->isEmpty()) {
/**
* audits.payload is switched to default true
* in order to auto audit payload for all endpoints
*/
$pattern = $route->getLabel('audits.payload', true);
if (! empty($pattern)) {
$queueForAudits->setPayload($responsePayload);
$auditContext->payload = $responsePayload;
}
foreach ($queueForEvents->getParams() as $key => $value) {
$queueForAudits->setParam($key, $value);
}
$queueForAudits->trigger();
$publisherForAudits->enqueue(AuditMessage::fromContext($auditContext));
}
if (! empty($queueForDeletes->getType())) {
@@ -912,15 +974,16 @@ Http::shutdown()
if ($useCache) {
$resource = $resourceType = null;
$data = $response->getPayload();
if (! empty($data['payload'])) {
$statusCode = $response->getStatusCode();
if (! empty($data['payload']) && $statusCode >= 200 && $statusCode < 300) {
$pattern = $route->getLabel('cache.resource', null);
if (! empty($pattern)) {
$resource = $parseLabel($pattern, $responsePayload, $requestParams, $user);
$resource = $parseLabel($pattern, $responsePayload, $requestParams, $user, $project);
}
$pattern = $route->getLabel('cache.resourceType', null);
if (! empty($pattern)) {
$resourceType = $parseLabel($pattern, $responsePayload, $requestParams, $user);
$resourceType = $parseLabel($pattern, $responsePayload, $requestParams, $user, $project);
}
$cache = new Cache(
@@ -962,7 +1025,7 @@ Http::shutdown()
}
if ($project->getId() !== 'console') {
if (! User::isPrivileged($authorization->getRoles())) {
if (! $user->isPrivileged($authorization->getRoles())) {
$bus->dispatch(new RequestCompleted(
project: $project->getArrayCopy(),
request: $request,
+4 -3
View File
@@ -36,8 +36,9 @@ Http::init()
->inject('request')
->inject('project')
->inject('geodb')
->inject('user')
->inject('authorization')
->action(function (Http $utopia, Request $request, Document $project, Reader $geodb, Authorization $authorization) {
->action(function (Http $utopia, 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);
@@ -50,8 +51,8 @@ Http::init()
$route = $utopia->match($request);
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
$isAppUser = User::isApp($authorization->getRoles());
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
$isAppUser = $user->isApp($authorization->getRoles());
if ($isAppUser || $isPrivilegedUser) { // Skip limits for app and console devs
return;
+70 -57
View File
@@ -1,14 +1,13 @@
<?php
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/init.php';
require_once __DIR__ . '/init/span.php';
$registerRequestResources = require __DIR__ . '/init/resources/request.php';
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Swoole\Constant;
use Swoole\Http\Request as SwooleRequest;
use Swoole\Http\Response as SwooleResponse;
use Swoole\Http\Server;
use Swoole\Process;
use Swoole\Table;
use Swoole\Timer;
@@ -27,11 +26,11 @@ use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Http\Adapter\Swoole\Server;
use Utopia\Http\Files;
use Utopia\Http\Http;
use Utopia\Logger\Log;
use Utopia\Logger\Log\User;
use Utopia\Pools\Group;
use Utopia\Span\Span;
use Utopia\System\System;
@@ -48,18 +47,33 @@ $certifiedDomains = new Table(100_000);
$certifiedDomains->column('value', Table::TYPE_INT, 1);
$certifiedDomains->create();
Http::setResource('riskyDomains', fn () => $riskyDomains);
Http::setResource('certifiedDomains', fn () => $certifiedDomains);
$http = new Server(
host: "0.0.0.0",
port: System::getEnv('PORT', 80),
mode: SWOOLE_PROCESS,
);
global $container;
$container->set('riskyDomains', fn () => $riskyDomains);
$container->set('certifiedDomains', fn () => $certifiedDomains);
$container->set('pools', function ($register) {
return $register->get('pools');
}, ['register']);
$payloadSize = 12 * (1024 * 1024); // 12MB - adding slight buffer for headers and other data that might be sent with the payload - update later with valid testing
$totalWorkers = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
$swooleAdapter = new Server(
host: "0.0.0.0",
port: System::getEnv('PORT', 80),
settings: [
Constant::OPTION_WORKER_NUM => $totalWorkers,
Constant::OPTION_DISPATCH_FUNC => dispatch(...),
Constant::OPTION_DISPATCH_MODE => SWOOLE_DISPATCH_UIDMOD,
Constant::OPTION_HTTP_COMPRESSION => false,
Constant::OPTION_PACKAGE_MAX_LENGTH => $payloadSize,
Constant::OPTION_OUTPUT_BUFFER_SIZE => $payloadSize,
Constant::OPTION_TASK_WORKER_NUM => 1, // required for the task to fetch domains background
],
container: $container,
);
$http = $swooleAdapter->getServer();
/**
* Assigns HTTP requests to worker threads by analyzing its payload/content.
*
@@ -68,16 +82,16 @@ $totalWorkers = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intva
* riskier tasks to a dedicated worker subset. Prefers idle workers, with fallback to random selection if necessary.
* doc: https://openswoole.com/docs/modules/swoole-server/configuration#dispatch_func
*
* @param Server $server Swoole server instance.
* @param \Swoole\Http\Server $server Swoole server instance.
* @param int $fd client ID
* @param int $type the type of data and its current state
* @param string|null $data Request content for categorization.
* @global int $totalThreads Total number of workers.
* @return int Chosen worker ID for the request.
*/
function dispatch(Server $server, int $fd, int $type, $data = null): int
function dispatch(\Swoole\Http\Server $server, int $fd, int $type, $data = null): int
{
$resolveWorkerId = function (Server $server, $data = null) {
$resolveWorkerId = function (\Swoole\Http\Server $server, $data = null) {
global $totalWorkers, $riskyDomains;
// If data is not set we can send request to any worker
@@ -103,7 +117,7 @@ function dispatch(Server $server, int $fd, int $type, $data = null): int
$lines = explode("\n", $data, 3);
$request = $lines[0];
if (count($lines) > 1) {
$domain = trim(explode('Host: ', $lines[1])[1]);
$domain = trim(explode('Host: ', $lines[1])[1] ?? '');
}
// Sync executions are considered risky
@@ -160,18 +174,6 @@ function dispatch(Server $server, int $fd, int $type, $data = null): int
return $workerId;
}
$http
->set([
Constant::OPTION_WORKER_NUM => $totalWorkers,
Constant::OPTION_DISPATCH_FUNC => dispatch(...),
Constant::OPTION_DISPATCH_MODE => SWOOLE_DISPATCH_UIDMOD,
Constant::OPTION_HTTP_COMPRESSION => false,
Constant::OPTION_PACKAGE_MAX_LENGTH => $payloadSize,
Constant::OPTION_OUTPUT_BUFFER_SIZE => $payloadSize,
Constant::OPTION_TASK_WORKER_NUM => 1, // required for the task to fetch domains background
]);
$http->on(Constant::EVENT_WORKER_START, function ($server, $workerId) {
});
@@ -188,9 +190,9 @@ $http->on(Constant::EVENT_AFTER_RELOAD, function ($server) {
Console::success('Reload completed...');
});
Http::setResource('bus', function ($register, $utopia) {
return $register->get('bus')->setResolver(fn (string $name) => $utopia->getResource($name));
}, ['register', 'utopia']);
$container->set('bus', function ($register) use ($swooleAdapter) {
return $register->get('bus')->setResolver(fn (string $name) => $swooleAdapter->getContainer()->get($name));
}, ['register']);
include __DIR__ . '/controllers/general.php';
@@ -286,13 +288,13 @@ function createDatabase(Http $app, string $resourceKey, string $dbName, array $c
Span::current()?->finish();
}
$http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $totalWorkers, $register) {
$app = new Http('UTC');
$http->on(Constant::EVENT_START, function ($http) use ($payloadSize, $totalWorkers, $swooleAdapter) {
$app = new Http($swooleAdapter, 'UTC');
go(function () use ($register, $app) {
$pools = $register->get('pools');
/** @var Group $pools */
Http::setResource('pools', fn () => $pools);
/** @var \Utopia\Pools\Group $pools */
$pools = $app->getResource('pools');
go(function () use ($app, $pools) {
/** @var array $collections */
$collections = Config::getParam('collections', []);
@@ -409,13 +411,21 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $tot
});
$projectCollections = $collections['projects'];
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
$sharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES_V1', ''));
$sharedTablesV2 = \array_diff($sharedTables, $sharedTablesV1);
$documentsSharedTables = \explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES', ''));
$vectorSharedTables = \explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES', ''));
$cache = $app->getResource('cache');
foreach ($sharedTablesV2 as $hostname) {
// All shared tables pools that need project metadata collections
$allSharedTables = \array_values(\array_unique(\array_filter([
...$sharedTables,
...$documentsSharedTables,
...$vectorSharedTables,
])));
foreach ($allSharedTables as $hostname) {
Span::init('database.setup');
Span::add('database.hostname', $hostname);
@@ -492,14 +502,11 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $tot
});
});
$http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, SwooleResponse $swooleResponse) use ($register, $files) {
$swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files, $swooleAdapter, $registerRequestResources) {
Span::init('http.request');
Http::setResource('swooleRequest', fn () => $swooleRequest);
Http::setResource('swooleResponse', fn () => $swooleResponse);
$request = new Request($swooleRequest);
$response = new Response($swooleResponse);
$request = new Request($utopiaRequest->getSwooleRequest());
$response = new Response($utopiaResponse->getSwooleResponse());
Span::add('http.method', $request->getMethod());
@@ -515,13 +522,19 @@ $http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, Swool
return;
}
$app = new Http('UTC');
$requestContainer = $swooleAdapter->getContainer();
$requestContainer->set('container', fn () => $requestContainer);
$requestContainer->set('request', fn () => $request);
$requestContainer->set('response', fn () => $response);
$app = new Http($swooleAdapter, 'UTC');
$requestContainer->set('utopia', fn () => $app);
$registerRequestResources($requestContainer);
$app->setCompression(System::getEnv('_APP_COMPRESSION_ENABLED', 'enabled') === 'enabled');
$app->setCompressionMinSize(intval(System::getEnv('_APP_COMPRESSION_MIN_SIZE_BYTES', '1024'))); // 1KB
$pools = $register->get('pools');
Http::setResource('pools', fn () => $pools);
try {
$authorization = $app->getResource('authorization');
@@ -605,6 +618,7 @@ $http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, Swool
}
}
$swooleResponse = $utopiaResponse->getSwooleResponse();
$swooleResponse->setStatusCode(500);
$output = ((Http::isDevelopment())) ? [
@@ -628,16 +642,15 @@ $http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, Swool
});
// Fetch domains every `DOMAIN_SYNC_TIMER` seconds and update in the memory
$http->on(Constant::EVENT_TASK, function () use ($register) {
$http->on(Constant::EVENT_TASK, function () use ($swooleAdapter) {
$lastSyncUpdate = null;
$pools = $register->get('pools');
Http::setResource('pools', fn () => $pools);
$app = new Http('UTC');
$app = new Http($swooleAdapter, 'UTC');
/** @var Utopia\Database\Database $dbForPlatform */
$dbForPlatform = $app->getResource('dbForPlatform');
/** @var Table $riskyDomains */
/** @var \Swoole\Table $riskyDomains */
$riskyDomains = $app->getResource('riskyDomains');
Timer::tick(DOMAIN_SYNC_TIMER * 1000, function () use ($dbForPlatform, $riskyDomains, &$lastSyncUpdate, $app) {
@@ -707,4 +720,4 @@ $http->on(Constant::EVENT_TASK, function () use ($register) {
});
});
$http->start();
$swooleAdapter->start();
+1 -1
View File
@@ -12,7 +12,7 @@ Config::load('runtimes-v2', __DIR__ . '/../config/runtimes-v2.php', $configAdapt
Config::load('template-runtimes', __DIR__ . '/../config/template-runtimes.php', $configAdapter);
Config::load('events', __DIR__ . '/../config/events.php', $configAdapter);
Config::load('auth', __DIR__ . '/../config/auth.php', $configAdapter);
Config::load('apis', __DIR__ . '/../config/apis.php', $configAdapter); // List of APIs
Config::load('protocols', __DIR__ . '/../config/protocols.php', $configAdapter);
Config::load('errors', __DIR__ . '/../config/errors.php', $configAdapter);
Config::load('oAuthProviders', __DIR__ . '/../config/oAuthProviders.php', $configAdapter);
Config::load('sdks', __DIR__ . '/../config/sdks.php', $configAdapter);
+64 -8
View File
@@ -1,6 +1,7 @@
<?php
use Appwrite\Platform\Modules\Compute\Specification;
use Utopia\System\System;
const APP_NAME = 'Appwrite';
const APP_DOMAIN = 'appwrite.io';
@@ -24,9 +25,6 @@ const APP_MODE_ADMIN = 'admin';
const APP_PAGING_LIMIT = 12;
const APP_LIMIT_COUNT = 5000;
const APP_LIMIT_USERS = 10_000;
const APP_LIMIT_USER_PASSWORD_HISTORY = 20;
const APP_LIMIT_USER_SESSIONS_MAX = 100;
const APP_LIMIT_USER_SESSIONS_DEFAULT = 10;
const APP_LIMIT_ANTIVIRUS = 20_000_000; //20MB
const APP_LIMIT_ENCRYPTION = 20_000_000; //20MB
const APP_LIMIT_COMPRESSION = 20_000_000; //20MB
@@ -46,8 +44,8 @@ const APP_PROJECT_ACCESS = 24 * 60 * 60; // 24 hours
const APP_RESOURCE_TOKEN_ACCESS = 24 * 60 * 60; // 24 hours
const APP_FILE_ACCESS = 24 * 60 * 60; // 24 hours
const APP_CACHE_UPDATE = 24 * 60 * 60; // 24 hours
const APP_CACHE_BUSTER = 4321;
const APP_VERSION_STABLE = '1.8.1';
const APP_CACHE_BUSTER = 4324;
const APP_VERSION_STABLE = '1.9.3';
const APP_DATABASE_ATTRIBUTE_EMAIL = 'email';
const APP_DATABASE_ATTRIBUTE_ENUM = 'enum';
const APP_DATABASE_ATTRIBUTE_IP = 'ip';
@@ -97,6 +95,7 @@ const APP_COMPUTE_DEPLOYMENT_MAX_RETENTION = 100 * 365; // 100 years
const APP_SDK_PLATFORM_SERVER = 'server';
const APP_SDK_PLATFORM_CLIENT = 'client';
const APP_SDK_PLATFORM_CONSOLE = 'console';
const APP_SDK_PLATFORM_STATIC = 'static';
const APP_VCS_GITHUB_USERNAME = 'Appwrite';
const APP_VCS_GITHUB_EMAIL = 'team@appwrite.io';
const APP_VCS_GITHUB_URL = 'https://github.com/TeamAppwrite';
@@ -155,9 +154,12 @@ const SESSION_PROVIDER_SERVER = 'server';
/**
* Activity associated with user or the app.
*/
const ACTIVITY_TYPE_APP = 'app';
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';
/**
* MFA
@@ -186,7 +188,7 @@ const BUILD_TYPE_RETRY = 'retry';
// Deletion Types
const ENABLE_EXECUTIONS_LIMIT_ON_ROUTE = false;
\define('ENABLE_EXECUTIONS_LIMIT_ON_ROUTE', System::getEnv('_APP_EXECUTIONS_LIMIT_ON_ROUTE', 'disabled') === 'enabled');
const DELETE_TYPE_DATABASES = 'databases';
const DELETE_TYPE_DOCUMENT = 'document';
@@ -242,6 +244,7 @@ const APP_AUTH_TYPE_KEY = 'Key';
const APP_AUTH_TYPE_ADMIN = 'Admin';
// Response related
const MAX_OUTPUT_CHUNK_SIZE = 10 * 1024 * 1024; // 10MB
const APP_LIMIT_UPLOAD_CHUNK_SIZE = 5 * 1024 * 1024; // 5MB
const APP_FUNCTION_LOG_LENGTH_LIMIT = 1000000;
const APP_FUNCTION_ERROR_LENGTH_LIMIT = 1000000;
// Function headers
@@ -253,7 +256,7 @@ const MESSAGE_TYPE_SMS = 'sms';
const MESSAGE_TYPE_PUSH = 'push';
// API key types
const API_KEY_STANDARD = 'standard';
const API_KEY_DYNAMIC = 'dynamic';
const API_KEY_EPHEMERAL = 'ephemeral';
const API_KEY_ORGANIZATION = 'organization';
const API_KEY_ACCOUNT = 'account';
// Usage metrics
@@ -288,6 +291,45 @@ const METRIC_DATABASES_OPERATIONS_READS = 'databases.operations.reads';
const METRIC_DATABASE_ID_OPERATIONS_READS = '{databaseInternalId}.databases.operations.reads';
const METRIC_DATABASES_OPERATIONS_WRITES = 'databases.operations.writes';
const METRIC_DATABASE_ID_OPERATIONS_WRITES = '{databaseInternalId}.databases.operations.writes';
// documentsdb
const METRIC_DATABASES_DOCUMENTSDB = 'documentsdb.databases';
const METRIC_COLLECTIONS_DOCUMENTSDB = 'documentsdb.collections';
const METRIC_DATABASES_STORAGE_DOCUMENTSDB = 'documentsdb.databases.storage';
const METRIC_DATABASE_ID_COLLECTIONS_DOCUMENTSDB = 'documentsdb.{databaseInternalId}.collections';
const METRIC_DATABASE_ID_STORAGE_DOCUMENTSDB = 'documentsdb.{databaseInternalId}.databases.storage';
const METRIC_DOCUMENTS_DOCUMENTSDB = 'documentsdb.documents';
const METRIC_DATABASE_ID_DOCUMENTS_DOCUMENTSDB = 'documentsdb.{databaseInternalId}.documents';
const METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS_DOCUMENTSDB = 'documentsdb.{databaseInternalId}.{collectionInternalId}.documents';
const METRIC_DATABASE_ID_COLLECTION_ID_STORAGE_DOCUMENTSDB = 'documentsdb.{databaseInternalId}.{collectionInternalId}.databases.storage';
const METRIC_DATABASES_OPERATIONS_READS_DOCUMENTSDB = 'documentsdb.databases.operations.reads';
const METRIC_DATABASE_ID_OPERATIONS_READS_DOCUMENTSDB = 'documentsdb.{databaseInternalId}.databases.operations.reads';
const METRIC_DATABASES_OPERATIONS_WRITES_DOCUMENTSDB = 'documentsdb.databases.operations.writes';
const METRIC_DATABASE_ID_OPERATIONS_WRITES_DOCUMENTSDB = 'documentsdb.{databaseInternalId}.databases.operations.writes';
// vectorsdb
const METRIC_DATABASES_VECTORSDB = 'vectorsdb.databases';
const METRIC_COLLECTIONS_VECTORSDB = 'vectorsdb.collections';
const METRIC_DATABASES_STORAGE_VECTORSDB = 'vectorsdb.databases.storage';
const METRIC_DATABASE_ID_COLLECTIONS_VECTORSDB = 'vectorsdb.{databaseInternalId}.collections';
const METRIC_DATABASE_ID_STORAGE_VECTORSDB = 'vectorsdb.{databaseInternalId}.databases.storage';
const METRIC_DOCUMENTS_VECTORSDB = 'vectorsdb.documents';
const METRIC_DATABASE_ID_DOCUMENTS_VECTORSDB = 'vectorsdb.{databaseInternalId}.documents';
const METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS_VECTORSDB = 'vectorsdb.{databaseInternalId}.{collectionInternalId}.documents';
const METRIC_DATABASE_ID_COLLECTION_ID_STORAGE_VECTORSDB = 'vectorsdb.{databaseInternalId}.{collectionInternalId}.databases.storage';
const METRIC_DATABASES_OPERATIONS_READS_VECTORSDB = 'vectorsdb.databases.operations.reads';
const METRIC_DATABASE_ID_OPERATIONS_READS_VECTORSDB = 'vectorsdb.{databaseInternalId}.databases.operations.reads';
const METRIC_DATABASES_OPERATIONS_WRITES_VECTORSDB = 'vectorsdb.databases.operations.writes';
const METRIC_DATABASE_ID_OPERATIONS_WRITES_VECTORSDB = 'vectorsdb.{databaseInternalId}.databases.operations.writes';
const METRIC_EMBEDDINGS_TEXT = 'embeddings.text';
const METRIC_EMBEDDINGS_MODEL_TEXT = 'embeddings.text.{embeddingModel}';
const METRIC_EMBEDDINGS_TEXT_TOTAL_ERROR = 'embeddings.text.totalErrors';
const METRIC_EMBEDDINGS_MODEL_TEXT_TOTAL_ERROR = 'embeddings.text.{embeddingModel}.totalErrors';
const METRIC_EMBEDDINGS_TEXT_TOTAL_DURATION = 'embeddings.text.totalDuration';
const METRIC_EMBEDDINGS_MODEL_TEXT_TOTAL_DURATION = 'embeddings.text.{embeddingModel}.totalDuration';
const METRIC_EMBEDDINGS_TEXT_TOTAL_TOKENS = 'embeddings.text.totalTokens';
const METRIC_EMBEDDINGS_MODEL_TEXT_TOTAL_TOKENS = 'embeddings.text.{embeddingModel}.totalTokens';
const METRIC_BUCKETS = 'buckets';
const METRIC_FILES = 'files';
const METRIC_FILES_STORAGE = 'files.storage';
@@ -380,6 +422,7 @@ const RESOURCE_TYPE_SUBSCRIBERS = 'subscribers';
const RESOURCE_TYPE_MESSAGES = 'messages';
const RESOURCE_TYPE_EXECUTIONS = 'executions';
const RESOURCE_TYPE_VCS = 'vcs';
const RESOURCE_TYPE_EMBEDDINGS_TEXT = 'embeddingsText';
// Resource types for Tokens
const TOKENS_RESOURCE_TYPE_FILES = 'files';
@@ -401,3 +444,16 @@ const CACHE_RECONNECT_RETRY_DELAY = 1000;
// Project status
const PROJECT_STATUS_ACTIVE = 'active';
// Database types
const DATABASE_TYPE_LEGACY = 'legacy';
const DATABASE_TYPE_TABLESDB = 'tablesdb';
const DATABASE_TYPE_DOCUMENTSDB = 'documentsdb';
const DATABASE_TYPE_VECTORSDB = 'vectorsdb';
// CSV import/export allowed database types
const CSV_ALLOWED_DATABASE_TYPES = [
DATABASE_TYPE_LEGACY,
DATABASE_TYPE_TABLESDB,
DATABASE_TYPE_VECTORSDB
];
+8 -1
View File
@@ -1,5 +1,6 @@
<?php
use Appwrite\Network\Platform;
use Appwrite\OpenSSL\OpenSSL;
use Utopia\Database\Database;
use Utopia\Database\Document;
@@ -123,11 +124,17 @@ Database::addFilter(
return;
},
function (mixed $value, Document $document, Database $database) {
return $database->getAuthorization()->skip(fn () => $database
$platforms = $database->getAuthorization()->skip(fn () => $database
->find('platforms', [
Query::equal('projectInternalId', [$document->getSequence()]),
Query::limit(APP_LIMIT_SUBQUERY),
]));
foreach ($platforms as $platform) {
$platform->setAttribute('type', Platform::mapDeprecatedType($platform->getAttribute('type')));
}
return $platforms;
}
);
+1 -1
View File
@@ -1,9 +1,9 @@
<?php
use Appwrite\Network\Validator\Email;
use Utopia\Database\Database;
use Utopia\Database\Validator\Datetime as DatetimeValidator;
use Utopia\Database\Validator\Structure;
use Utopia\Emails\Validator\Email;
use Utopia\Validator\IP;
use Utopia\Validator\Range;
use Utopia\Validator\URL;
+155 -8
View File
@@ -22,6 +22,7 @@ use Appwrite\Utopia\Response\Model\AttributeLine;
use Appwrite\Utopia\Response\Model\AttributeList;
use Appwrite\Utopia\Response\Model\AttributeLongtext;
use Appwrite\Utopia\Response\Model\AttributeMediumtext;
use Appwrite\Utopia\Response\Model\AttributeObject;
use Appwrite\Utopia\Response\Model\AttributePoint;
use Appwrite\Utopia\Response\Model\AttributePolygon;
use Appwrite\Utopia\Response\Model\AttributeRelationship;
@@ -29,6 +30,7 @@ use Appwrite\Utopia\Response\Model\AttributeString;
use Appwrite\Utopia\Response\Model\AttributeText;
use Appwrite\Utopia\Response\Model\AttributeURL;
use Appwrite\Utopia\Response\Model\AttributeVarchar;
use Appwrite\Utopia\Response\Model\AttributeVector;
use Appwrite\Utopia\Response\Model\AuthProvider;
use Appwrite\Utopia\Response\Model\BaseList;
use Appwrite\Utopia\Response\Model\Branch;
@@ -54,6 +56,11 @@ use Appwrite\Utopia\Response\Model\ColumnString;
use Appwrite\Utopia\Response\Model\ColumnText;
use Appwrite\Utopia\Response\Model\ColumnURL;
use Appwrite\Utopia\Response\Model\ColumnVarchar;
use Appwrite\Utopia\Response\Model\ConsoleKeyScope;
use Appwrite\Utopia\Response\Model\ConsoleKeyScopeList;
use Appwrite\Utopia\Response\Model\ConsoleOAuth2Provider;
use Appwrite\Utopia\Response\Model\ConsoleOAuth2ProviderList;
use Appwrite\Utopia\Response\Model\ConsoleOAuth2ProviderParameter;
use Appwrite\Utopia\Response\Model\ConsoleVariables;
use Appwrite\Utopia\Response\Model\Continent;
use Appwrite\Utopia\Response\Model\Country;
@@ -65,6 +72,8 @@ use Appwrite\Utopia\Response\Model\DetectionRuntime;
use Appwrite\Utopia\Response\Model\DetectionVariable;
use Appwrite\Utopia\Response\Model\DevKey;
use Appwrite\Utopia\Response\Model\Document as ModelDocument;
use Appwrite\Utopia\Response\Model\Embedding;
use Appwrite\Utopia\Response\Model\EphemeralKey;
use Appwrite\Utopia\Response\Model\Error;
use Appwrite\Utopia\Response\Model\ErrorDev;
use Appwrite\Utopia\Response\Model\Execution;
@@ -102,14 +111,72 @@ use Appwrite\Utopia\Response\Model\MigrationReport;
use Appwrite\Utopia\Response\Model\Mock;
use Appwrite\Utopia\Response\Model\MockNumber;
use Appwrite\Utopia\Response\Model\None;
use Appwrite\Utopia\Response\Model\OAuth2Amazon;
use Appwrite\Utopia\Response\Model\OAuth2Apple;
use Appwrite\Utopia\Response\Model\OAuth2Auth0;
use Appwrite\Utopia\Response\Model\OAuth2Authentik;
use Appwrite\Utopia\Response\Model\OAuth2Autodesk;
use Appwrite\Utopia\Response\Model\OAuth2Bitbucket;
use Appwrite\Utopia\Response\Model\OAuth2Bitly;
use Appwrite\Utopia\Response\Model\OAuth2Box;
use Appwrite\Utopia\Response\Model\OAuth2Dailymotion;
use Appwrite\Utopia\Response\Model\OAuth2Discord;
use Appwrite\Utopia\Response\Model\OAuth2Disqus;
use Appwrite\Utopia\Response\Model\OAuth2Dropbox;
use Appwrite\Utopia\Response\Model\OAuth2Etsy;
use Appwrite\Utopia\Response\Model\OAuth2Facebook;
use Appwrite\Utopia\Response\Model\OAuth2Figma;
use Appwrite\Utopia\Response\Model\OAuth2FusionAuth;
use Appwrite\Utopia\Response\Model\OAuth2GitHub;
use Appwrite\Utopia\Response\Model\OAuth2Gitlab;
use Appwrite\Utopia\Response\Model\OAuth2Google;
use Appwrite\Utopia\Response\Model\OAuth2Keycloak;
use Appwrite\Utopia\Response\Model\OAuth2Kick;
use Appwrite\Utopia\Response\Model\OAuth2Linkedin;
use Appwrite\Utopia\Response\Model\OAuth2Microsoft;
use Appwrite\Utopia\Response\Model\OAuth2Notion;
use Appwrite\Utopia\Response\Model\OAuth2Oidc;
use Appwrite\Utopia\Response\Model\OAuth2Okta;
use Appwrite\Utopia\Response\Model\OAuth2Paypal;
use Appwrite\Utopia\Response\Model\OAuth2Podio;
use Appwrite\Utopia\Response\Model\OAuth2ProviderList;
use Appwrite\Utopia\Response\Model\OAuth2Salesforce;
use Appwrite\Utopia\Response\Model\OAuth2Slack;
use Appwrite\Utopia\Response\Model\OAuth2Spotify;
use Appwrite\Utopia\Response\Model\OAuth2Stripe;
use Appwrite\Utopia\Response\Model\OAuth2Tradeshift;
use Appwrite\Utopia\Response\Model\OAuth2Twitch;
use Appwrite\Utopia\Response\Model\OAuth2WordPress;
use Appwrite\Utopia\Response\Model\OAuth2X;
use Appwrite\Utopia\Response\Model\OAuth2Yahoo;
use Appwrite\Utopia\Response\Model\OAuth2Yandex;
use Appwrite\Utopia\Response\Model\OAuth2Zoho;
use Appwrite\Utopia\Response\Model\OAuth2Zoom;
use Appwrite\Utopia\Response\Model\Phone;
use Appwrite\Utopia\Response\Model\Platform;
use Appwrite\Utopia\Response\Model\PlatformAndroid;
use Appwrite\Utopia\Response\Model\PlatformApple;
use Appwrite\Utopia\Response\Model\PlatformLinux;
use Appwrite\Utopia\Response\Model\PlatformList;
use Appwrite\Utopia\Response\Model\PlatformWeb;
use Appwrite\Utopia\Response\Model\PlatformWindows;
use Appwrite\Utopia\Response\Model\PolicyList;
use Appwrite\Utopia\Response\Model\PolicyMembershipPrivacy;
use Appwrite\Utopia\Response\Model\PolicyPasswordDictionary;
use Appwrite\Utopia\Response\Model\PolicyPasswordHistory;
use Appwrite\Utopia\Response\Model\PolicyPasswordPersonalData;
use Appwrite\Utopia\Response\Model\PolicySessionAlert;
use Appwrite\Utopia\Response\Model\PolicySessionDuration;
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\Project;
use Appwrite\Utopia\Response\Model\Provider;
use Appwrite\Utopia\Response\Model\ProviderRepository;
use Appwrite\Utopia\Response\Model\ProviderRepositoryFramework;
use Appwrite\Utopia\Response\Model\ProviderRepositoryFrameworkList;
use Appwrite\Utopia\Response\Model\ProviderRepositoryRuntime;
use Appwrite\Utopia\Response\Model\ProviderRepositoryRuntimeList;
use Appwrite\Utopia\Response\Model\ResourceToken;
use Appwrite\Utopia\Response\Model\Row;
use Appwrite\Utopia\Response\Model\Rule;
@@ -127,7 +194,6 @@ use Appwrite\Utopia\Response\Model\TemplateFramework;
use Appwrite\Utopia\Response\Model\TemplateFunction;
use Appwrite\Utopia\Response\Model\TemplateRuntime;
use Appwrite\Utopia\Response\Model\TemplateSite;
use Appwrite\Utopia\Response\Model\TemplateSMS;
use Appwrite\Utopia\Response\Model\TemplateVariable;
use Appwrite\Utopia\Response\Model\Token;
use Appwrite\Utopia\Response\Model\Topic;
@@ -136,6 +202,8 @@ use Appwrite\Utopia\Response\Model\UsageBuckets;
use Appwrite\Utopia\Response\Model\UsageCollection;
use Appwrite\Utopia\Response\Model\UsageDatabase;
use Appwrite\Utopia\Response\Model\UsageDatabases;
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\UsageProject;
@@ -144,9 +212,12 @@ use Appwrite\Utopia\Response\Model\UsageSites;
use Appwrite\Utopia\Response\Model\UsageStorage;
use Appwrite\Utopia\Response\Model\UsageTable;
use Appwrite\Utopia\Response\Model\UsageUsers;
use Appwrite\Utopia\Response\Model\UsageVectorsDB;
use Appwrite\Utopia\Response\Model\UsageVectorsDBs;
use Appwrite\Utopia\Response\Model\User;
use Appwrite\Utopia\Response\Model\Variable;
use Appwrite\Utopia\Response\Model\VcsContent;
use Appwrite\Utopia\Response\Model\VectorsDBCollection;
use Appwrite\Utopia\Response\Model\Webhook;
// General
@@ -177,19 +248,18 @@ Response::setModel(new BaseList('Site Templates List', Response::MODEL_TEMPLATE_
Response::setModel(new BaseList('Functions List', Response::MODEL_FUNCTION_LIST, 'functions', Response::MODEL_FUNCTION));
Response::setModel(new BaseList('Function Templates List', Response::MODEL_TEMPLATE_FUNCTION_LIST, 'templates', Response::MODEL_TEMPLATE_FUNCTION));
Response::setModel(new BaseList('Installations List', Response::MODEL_INSTALLATION_LIST, 'installations', Response::MODEL_INSTALLATION));
Response::setModel(new BaseList('Framework Provider Repositories List', Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST, 'frameworkProviderRepositories', Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK));
Response::setModel(new BaseList('Runtime Provider Repositories List', Response::MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST, 'runtimeProviderRepositories', Response::MODEL_PROVIDER_REPOSITORY_RUNTIME));
Response::setModel(new ProviderRepositoryFrameworkList());
Response::setModel(new ProviderRepositoryRuntimeList());
Response::setModel(new BaseList('Branches List', Response::MODEL_BRANCH_LIST, 'branches', Response::MODEL_BRANCH));
Response::setModel(new BaseList('Frameworks List', Response::MODEL_FRAMEWORK_LIST, 'frameworks', Response::MODEL_FRAMEWORK));
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('Webhooks List', Response::MODEL_WEBHOOK_LIST, 'webhooks', Response::MODEL_WEBHOOK, true, false));
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));
Response::setModel(new BaseList('Auth Providers List', Response::MODEL_AUTH_PROVIDER_LIST, 'platforms', Response::MODEL_AUTH_PROVIDER, true, false));
Response::setModel(new BaseList('Platforms List', Response::MODEL_PLATFORM_LIST, 'platforms', Response::MODEL_PLATFORM, true, false));
Response::setModel(new BaseList('Countries List', Response::MODEL_COUNTRY_LIST, 'countries', Response::MODEL_COUNTRY));
Response::setModel(new BaseList('Continents List', Response::MODEL_CONTINENT_LIST, 'continents', Response::MODEL_CONTINENT));
Response::setModel(new BaseList('Languages List', Response::MODEL_LANGUAGE_LIST, 'languages', Response::MODEL_LANGUAGE));
@@ -197,6 +267,9 @@ Response::setModel(new BaseList('Currencies List', Response::MODEL_CURRENCY_LIST
Response::setModel(new BaseList('Phones List', Response::MODEL_PHONE_LIST, 'phones', Response::MODEL_PHONE));
Response::setModel(new BaseList('Metric List', Response::MODEL_METRIC_LIST, 'metrics', Response::MODEL_METRIC, true, false));
Response::setModel(new BaseList('Variables List', Response::MODEL_VARIABLE_LIST, 'variables', Response::MODEL_VARIABLE));
Response::setModel(new BaseList('Mock Numbers List', Response::MODEL_MOCK_NUMBER_LIST, 'mockNumbers', Response::MODEL_MOCK_NUMBER));
Response::setModel(new PolicyList());
Response::setModel(new BaseList('Email Templates List', Response::MODEL_EMAIL_TEMPLATE_LIST, 'templates', Response::MODEL_EMAIL_TEMPLATE));
Response::setModel(new BaseList('Status List', Response::MODEL_HEALTH_STATUS_LIST, 'statuses', Response::MODEL_HEALTH_STATUS));
Response::setModel(new BaseList('Rule List', Response::MODEL_PROXY_RULE_LIST, 'rules', Response::MODEL_PROXY_RULE));
Response::setModel(new BaseList('Schedules List', Response::MODEL_SCHEDULE_LIST, 'schedules', Response::MODEL_SCHEDULE));
@@ -211,9 +284,12 @@ Response::setModel(new BaseList('Migrations List', Response::MODEL_MIGRATION_LIS
Response::setModel(new BaseList('Migrations Firebase Projects List', Response::MODEL_MIGRATION_FIREBASE_PROJECT_LIST, 'projects', Response::MODEL_MIGRATION_FIREBASE_PROJECT));
Response::setModel(new BaseList('Specifications List', Response::MODEL_SPECIFICATION_LIST, 'specifications', Response::MODEL_SPECIFICATION));
Response::setModel(new BaseList('VCS Content List', Response::MODEL_VCS_CONTENT_LIST, 'contents', Response::MODEL_VCS_CONTENT));
Response::setModel(new BaseList('VectorsDB Collections List', Response::MODEL_VECTORSDB_COLLECTION_LIST, 'collections', Response::MODEL_VECTORSDB_COLLECTION));
Response::setModel(new BaseList('Embedding list', Response::MODEL_EMBEDDING_LIST, 'embeddings', Response::MODEL_EMBEDDING));
// Entities
Response::setModel(new Database());
Response::setModel(new Embedding());
// Collection API Models
Response::setModel(new Collection());
@@ -237,6 +313,17 @@ Response::setModel(new AttributeText());
Response::setModel(new AttributeMediumtext());
Response::setModel(new AttributeLongtext());
// DocumentsDB API Models
Response::setModel(new UsageDocumentsDBs());
Response::setModel(new UsageDocumentsDB());
// VectorsDB API Models
Response::setModel(new VectorsDBCollection());
Response::setModel(new AttributeObject());
Response::setModel(new AttributeVector());
Response::setModel(new UsageVectorsDBs());
Response::setModel(new UsageVectorsDB());
// Table API Models
Response::setModel(new Table());
Response::setModel(new Column());
@@ -308,10 +395,66 @@ Response::setModel(new Execution());
Response::setModel(new Project());
Response::setModel(new Webhook());
Response::setModel(new Key());
Response::setModel(new EphemeralKey());
Response::setModel(new DevKey());
Response::setModel(new MockNumber());
Response::setModel(new OAuth2GitHub());
Response::setModel(new OAuth2Discord());
Response::setModel(new OAuth2Figma());
Response::setModel(new OAuth2Dropbox());
Response::setModel(new OAuth2Dailymotion());
Response::setModel(new OAuth2Bitbucket());
Response::setModel(new OAuth2Bitly());
Response::setModel(new OAuth2Box());
Response::setModel(new OAuth2Autodesk());
Response::setModel(new OAuth2Google());
Response::setModel(new OAuth2Zoom());
Response::setModel(new OAuth2Zoho());
Response::setModel(new OAuth2Yandex());
Response::setModel(new OAuth2X());
Response::setModel(new OAuth2WordPress());
Response::setModel(new OAuth2Twitch());
Response::setModel(new OAuth2Stripe());
Response::setModel(new OAuth2Spotify());
Response::setModel(new OAuth2Slack());
Response::setModel(new OAuth2Podio());
Response::setModel(new OAuth2Notion());
Response::setModel(new OAuth2Salesforce());
Response::setModel(new OAuth2Yahoo());
Response::setModel(new OAuth2Linkedin());
Response::setModel(new OAuth2Disqus());
Response::setModel(new OAuth2Amazon());
Response::setModel(new OAuth2Etsy());
Response::setModel(new OAuth2Facebook());
Response::setModel(new OAuth2Tradeshift());
Response::setModel(new OAuth2Paypal());
Response::setModel(new OAuth2Gitlab());
Response::setModel(new OAuth2Authentik());
Response::setModel(new OAuth2Auth0());
Response::setModel(new OAuth2FusionAuth());
Response::setModel(new OAuth2Keycloak());
Response::setModel(new OAuth2Oidc());
Response::setModel(new OAuth2Okta());
Response::setModel(new OAuth2Kick());
Response::setModel(new OAuth2Apple());
Response::setModel(new OAuth2Microsoft());
Response::setModel(new OAuth2ProviderList());
Response::setModel(new PolicyPasswordDictionary());
Response::setModel(new PolicyPasswordHistory());
Response::setModel(new PolicyPasswordPersonalData());
Response::setModel(new PolicySessionAlert());
Response::setModel(new PolicySessionDuration());
Response::setModel(new PolicySessionInvalidation());
Response::setModel(new PolicySessionLimit());
Response::setModel(new PolicyUserLimit());
Response::setModel(new PolicyMembershipPrivacy());
Response::setModel(new AuthProvider());
Response::setModel(new Platform());
Response::setModel(new PlatformWeb());
Response::setModel(new PlatformApple());
Response::setModel(new PlatformAndroid());
Response::setModel(new PlatformWindows());
Response::setModel(new PlatformLinux());
Response::setModel(new PlatformList());
Response::setModel(new Variable());
Response::setModel(new Country());
Response::setModel(new Continent());
@@ -342,9 +485,13 @@ Response::setModel(new Headers());
Response::setModel(new Specification());
Response::setModel(new Rule());
Response::setModel(new Schedule());
Response::setModel(new TemplateSMS());
Response::setModel(new TemplateEmail());
Response::setModel(new ConsoleVariables());
Response::setModel(new ConsoleOAuth2ProviderParameter());
Response::setModel(new ConsoleOAuth2Provider());
Response::setModel(new ConsoleOAuth2ProviderList());
Response::setModel(new ConsoleKeyScope());
Response::setModel(new ConsoleKeyScopeList());
Response::setModel(new MFAChallenge());
Response::setModel(new MFARecoveryCodes());
Response::setModel(new MFAType());
+368
View File
@@ -0,0 +1,368 @@
<?php
use Ahc\Jwt\JWT;
use Ahc\Jwt\JWTException;
use Appwrite\Extend\Exception;
use Appwrite\Network\Platform;
use Appwrite\Network\Validator\Origin;
use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Request;
use Utopia\Auth\Hashes\Sha;
use Utopia\Auth\Proofs\Token;
use Utopia\Auth\Store;
use Utopia\Database\DateTime as DatabaseDateTime;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\DI\Container;
use Utopia\System\System;
use Utopia\Validator\URL;
use Utopia\Validator\WhiteList;
/**
* Register the minimal per-connection resources required by realtime.
*/
return function (Container $container): void {
$getProjectId = static function (Request $request): string {
$projectId = $request->getHeader('x-appwrite-project', '');
if (!empty($projectId)) {
return $projectId;
}
$projectId = $request->getParam('project', '');
return \is_string($projectId) ? $projectId : '';
};
$getMode = static function (Request $request, Document $project) use ($getProjectId): string {
$mode = $request->getParam('mode', $request->getHeader('x-appwrite-mode', APP_MODE_DEFAULT));
$projectId = $getProjectId($request);
if (!empty($projectId) && $project->getId() !== $projectId) {
$mode = APP_MODE_ADMIN;
}
return $mode;
};
$getDbForPlatform = static function (Authorization $authorization) {
$database = getConsoleDB();
$database->setAuthorization($authorization);
return $database;
};
$getDbForProject = static function (Document $project, Authorization $authorization) use ($getDbForPlatform) {
if ($project->isEmpty() || $project->getId() === 'console') {
return $getDbForPlatform($authorization);
}
$database = getProjectDB($project);
$database->setAuthorization($authorization);
return $database;
};
$findRule = static function (Request $request, Document $project, Authorization $authorization) use ($getDbForPlatform): Document {
$domain = \parse_url($request->getOrigin(), PHP_URL_HOST);
if (empty($domain)) {
$domain = \parse_url($request->getReferer(), PHP_URL_HOST);
}
if (empty($domain)) {
return new Document();
}
$dbForPlatform = $getDbForPlatform($authorization);
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
$rule = $authorization->skip(function () use ($dbForPlatform, $domain, $isMd5) {
if ($isMd5) {
return $dbForPlatform->getDocument('rules', md5($domain));
}
return $dbForPlatform->findOne('rules', [
Query::equal('domain', [$domain]),
]);
});
$permitsCurrentProject = $rule->getAttribute('projectInternalId', '') === $project->getSequence();
if (!$permitsCurrentProject && !$rule->isEmpty() && !empty($rule->getAttribute('projectId', ''))) {
$trustedProjects = [];
foreach (\explode(',', System::getEnv('_APP_CONSOLE_TRUSTED_PROJECTS', '')) as $trustedProject) {
if (empty($trustedProject)) {
continue;
}
$trustedProjects[] = $trustedProject;
}
if (\in_array($rule->getAttribute('projectId', ''), $trustedProjects, true)) {
$permitsCurrentProject = true;
}
}
if (!$permitsCurrentProject) {
return new Document();
}
return $rule;
};
$findDevKey = static function (Request $request, Document $project, array $servers, Authorization $authorization) use ($getDbForPlatform): Document {
$devKey = $request->getHeader('x-appwrite-dev-key', $request->getParam('devKey', ''));
$key = $project->find('secret', $devKey, 'devKeys');
if (!$key) {
return new Document([]);
}
$expire = $key->getAttribute('expire');
if (!empty($expire) && $expire < DatabaseDateTime::formatTz(DatabaseDateTime::now())) {
return new Document([]);
}
$dbForPlatform = $getDbForPlatform($authorization);
$accessedAt = $key->getAttribute('accessedAt', 0);
if (empty($accessedAt) || DatabaseDateTime::formatTz(DatabaseDateTime::addSeconds(new \DateTime(), -APP_KEY_ACCESS)) > $accessedAt) {
$key->setAttribute('accessedAt', DatabaseDateTime::now());
$authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), new Document([
'accessedAt' => $key->getAttribute('accessedAt'),
])));
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
}
$sdkValidator = new WhiteList($servers, true);
$sdk = \strtolower($request->getHeader('x-sdk-name', 'UNKNOWN'));
if ($sdk !== 'unknown' && $sdkValidator->isValid($sdk)) {
$sdks = $key->getAttribute('sdks', []);
if (!\in_array($sdk, $sdks, true)) {
$sdks[] = $sdk;
$key->setAttribute('sdks', $sdks);
$key->setAttribute('accessedAt', DatabaseDateTime::now());
$key = $authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), new Document([
'sdks' => $key->getAttribute('sdks'),
'accessedAt' => $key->getAttribute('accessedAt'),
])));
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
}
}
return $key;
};
$container->set('authorization', function () {
return new Authorization();
}, []);
$container->set('project', function (Request $request, Document $console, Authorization $authorization) use ($getProjectId, $getDbForPlatform) {
$projectId = $getProjectId($request);
if (empty($projectId) || $projectId === 'console') {
return $console;
}
$dbForPlatform = $getDbForPlatform($authorization);
return $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
}, ['request', 'console', 'authorization']);
$container->set('originValidator', function (array $platform, Request $request, Document $project, array $servers, Authorization $authorization) use ($findDevKey, $findRule) {
$devKey = $findDevKey($request, $project, $servers, $authorization);
if (!$devKey->isEmpty()) {
return new URL();
}
$allowedHostnames = [...($platform['hostnames'] ?? [])];
if (!$project->isEmpty() && $project->getId() !== 'console') {
$allowedHostnames = [...$allowedHostnames, ...Platform::getHostnames($project->getAttribute('platforms', []))];
}
$rule = $findRule($request, $project, $authorization);
if (!$rule->isEmpty() && !empty($rule->getAttribute('domain', ''))) {
$allowedHostnames[] = $rule->getAttribute('domain', '');
}
$originHostname = \parse_url($request->getOrigin(), PHP_URL_HOST);
$refererHostname = \parse_url($request->getReferer(), PHP_URL_HOST);
$hostname = $originHostname ?: $refererHostname;
if ($request->getMethod() === 'OPTIONS' && !empty($hostname)) {
$allowedHostnames[] = $hostname;
}
$allowedSchemes = [...($platform['schemas'] ?? [])];
if (!$project->isEmpty() && $project->getId() !== 'console') {
$allowedSchemes[] = 'exp';
$allowedSchemes[] = 'appwrite-callback-' . $project->getId();
$allowedSchemes = [...$allowedSchemes, ...Platform::getSchemes($project->getAttribute('platforms', []))];
}
return new Origin(\array_unique($allowedHostnames), \array_unique($allowedSchemes));
}, ['platform', 'request', 'project', 'servers', 'authorization']);
$container->set('user', function (Request $request, Document $project, Document $console, Authorization $authorization) use ($getMode, $getDbForPlatform, $getDbForProject) {
$mode = $getMode($request, $project);
$store = new Store();
$proofForToken = new Token();
$proofForToken->setHash(new Sha());
$authorization->setDefaultStatus(true);
$dbForPlatform = $getDbForPlatform($authorization);
$dbForProject = $getDbForProject($project, $authorization);
$store->setKey('a_session_' . $project->getId());
if ($mode === APP_MODE_ADMIN) {
$store->setKey('a_session_' . $console->getId());
}
$store->decode(
$request->getCookie(
$store->getKey(),
$request->getCookie($store->getKey() . '_legacy', '')
)
);
if (empty($store->getProperty('id', '')) && empty($store->getProperty('secret', ''))) {
$sessionHeader = $request->getHeader('x-appwrite-session', '');
if (!empty($sessionHeader)) {
$store->decode($sessionHeader);
}
}
if (empty($store->getProperty('id', '')) && empty($store->getProperty('secret', ''))) {
$fallback = \json_decode($request->getHeader('x-fallback-cookies', ''), true);
$store->decode((\is_array($fallback) && isset($fallback[$store->getKey()])) ? $fallback[$store->getKey()] : '');
}
$user = null;
if ($mode === APP_MODE_ADMIN) {
/** @var User $user */
$user = $dbForPlatform->getDocument('users', $store->getProperty('id', ''));
} else {
if ($project->isEmpty()) {
$user = new User([]);
} elseif (!empty($store->getProperty('id', ''))) {
if ($project->getId() === 'console') {
/** @var User $user */
$user = $dbForPlatform->getDocument('users', $store->getProperty('id', ''));
} else {
/** @var User $user */
$user = $dbForProject->getDocument('users', $store->getProperty('id', ''));
}
}
}
if (
!$user
|| $user->isEmpty()
|| !$user->sessionVerify($store->getProperty('secret', ''), $proofForToken)
) {
$user = new User([]);
}
$authJWT = $request->getHeader('x-appwrite-jwt', '');
if (!empty($authJWT) && !$project->isEmpty()) {
if (!$user->isEmpty()) {
throw new Exception(Exception::USER_JWT_AND_COOKIE_SET);
}
$jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0);
try {
$payload = $jwt->decode($authJWT);
} catch (JWTException $error) {
throw new Exception(Exception::USER_JWT_INVALID, 'Failed to verify JWT. ' . $error->getMessage());
}
$jwtUserId = $payload['userId'] ?? '';
if (!empty($jwtUserId)) {
if ($mode === APP_MODE_ADMIN) {
$user = $dbForPlatform->getDocument('users', $jwtUserId);
} else {
$user = $dbForProject->getDocument('users', $jwtUserId);
}
}
$jwtSessionId = $payload['sessionId'] ?? '';
if (!empty($jwtSessionId) && empty($user->find('$id', $jwtSessionId, 'sessions'))) {
$user = new User([]);
}
}
$accountKey = $request->getHeader('x-appwrite-key', '');
$accountKeyUserId = $request->getHeader('x-appwrite-user', '');
if (!empty($accountKeyUserId) && !empty($accountKey)) {
if (!$user->isEmpty()) {
throw new Exception(Exception::USER_API_KEY_AND_SESSION_SET);
}
$accountKeyUser = $authorization->skip(fn () => $dbForPlatform->getDocument('users', $accountKeyUserId));
if (!$accountKeyUser->isEmpty()) {
$key = $accountKeyUser->find(
key: 'secret',
find: $accountKey,
subject: 'keys'
);
if (!empty($key)) {
$expire = $key->getAttribute('expire');
if (!empty($expire) && $expire < DatabaseDateTime::formatTz(DatabaseDateTime::now())) {
throw new Exception(Exception::ACCOUNT_KEY_EXPIRED);
}
$user = $accountKeyUser;
}
}
}
// Query params mirror the header fallback pattern used by ?project= and ?devKey=,
// allowing Console to embed impersonation in direct file/image URLs where headers cannot be set.
$impersonateUserId = $request->getHeader('x-appwrite-impersonate-user-id', (string)$request->getParam('impersonateUserId', ''));
$impersonateEmail = $request->getHeader('x-appwrite-impersonate-user-email', (string)$request->getParam('impersonateEmail', ''));
$impersonatePhone = $request->getHeader('x-appwrite-impersonate-user-phone', (string)$request->getParam('impersonatePhone', ''));
if (!$user->isEmpty() && $user->getAttribute('impersonator', false)) {
$userDb = ($mode === APP_MODE_ADMIN || $project->getId() === 'console') ? $dbForPlatform : $dbForProject;
$targetUser = null;
if (!empty($impersonateUserId)) {
$targetUser = $authorization->skip(fn () => $userDb->getDocument('users', $impersonateUserId));
} elseif (!empty($impersonateEmail)) {
$targetUser = $authorization->skip(fn () => $userDb->findOne('users', [
Query::equal('email', [\strtolower($impersonateEmail)]),
]));
} elseif (!empty($impersonatePhone)) {
$targetUser = $authorization->skip(fn () => $userDb->findOne('users', [
Query::equal('phone', [$impersonatePhone]),
]));
}
if ($targetUser !== null && !$targetUser->isEmpty()) {
$impersonator = clone $user;
$user = clone $targetUser;
$user->setAttribute('impersonatorUserId', $impersonator->getId());
$user->setAttribute('impersonatorUserInternalId', $impersonator->getSequence());
$user->setAttribute('impersonatorUserName', $impersonator->getAttribute('name', ''));
$user->setAttribute('impersonatorUserEmail', $impersonator->getAttribute('email', ''));
$user->setAttribute('impersonatorAccessedAt', $impersonator->getAttribute('accessedAt', 0));
}
}
$dbForPlatform->setMetadata('user', $user->getId());
$dbForProject->setMetadata('user', $user->getId());
return $user;
}, ['request', 'project', 'console', 'authorization']);
};
+60 -59
View File
@@ -6,7 +6,6 @@ use Appwrite\Hooks\Hooks;
use Appwrite\PubSub\Adapter\Redis as PubSub;
use Appwrite\URL\URL as AppwriteURL;
use MaxMind\Db\Reader;
use PHPMailer\PHPMailer\PHPMailer;
use Swoole\Database\PDOProxy;
use Utopia\Cache\Adapter\Redis as RedisCache;
use Utopia\Config\Config;
@@ -25,6 +24,7 @@ use Utopia\Logger\Adapter\LogOwl;
use Utopia\Logger\Adapter\Raygun;
use Utopia\Logger\Adapter\Sentry;
use Utopia\Logger\Logger;
use Utopia\Messaging\Adapter\Email\SMTP;
use Utopia\Mongo\Client as MongoClient;
use Utopia\Pools\Adapter\Stack as StackPool;
use Utopia\Pools\Adapter\Swoole as SwoolePool;
@@ -56,7 +56,7 @@ $register->set('logger', function () {
}
try {
$loggingProvider = new DSN($providerConfig ?? '');
$loggingProvider = new DSN($providerConfig);
$providerName = $loggingProvider->getScheme();
$providerConfig = match ($providerName) {
@@ -71,12 +71,12 @@ $register->set('logger', function () {
$providerConfig = match ($providerName) {
'sentry' => [ 'key' => $configChunks[0], 'projectId' => $configChunks[1] ?? '', 'host' => '',],
'logowl' => ['ticket' => $configChunks[0] ?? '', 'host' => ''],
'logowl' => ['ticket' => $configChunks[0], 'host' => ''],
default => ['key' => $providerConfig],
};
}
if (empty($providerName) || empty($providerConfig)) {
if (empty($providerName)) {
return;
}
@@ -121,7 +121,7 @@ $register->set('realtimeLogger', function () {
default => ['key' => $loggingProvider->getHost()],
};
if (empty($providerName) || empty($providerConfig)) {
if (empty($providerName)) {
return;
}
@@ -160,7 +160,6 @@ $register->set('pools', function () {
'pass' => System::getEnv('_APP_DB_PASS', ''),
'path' => System::getEnv('_APP_DB_SCHEMA', ''),
]);
$fallbackForRedis = 'redis_main=' . AppwriteURL::unparse([
'scheme' => 'redis',
'host' => System::getEnv('_APP_REDIS_HOST', 'redis'),
@@ -169,6 +168,23 @@ $register->set('pools', function () {
'pass' => System::getEnv('_APP_REDIS_PASS', ''),
]);
$fallbackForDocumentsDB = 'db_main=' . AppwriteURL::unparse([
'scheme' => System::getEnv('_APP_DB_ADAPTER_DOCUMENTSDB', 'mongodb'),
'host' => System::getEnv('_APP_DB_HOST_DOCUMENTSDB', 'mongodb'),
'port' => System::getEnv('_APP_DB_PORT_DOCUMENTSDB', '27017'),
'user' => System::getEnv('_APP_DB_USER', ''),
'pass' => System::getEnv('_APP_DB_PASS', ''),
'path' => System::getEnv('_APP_DB_SCHEMA', ''),
]);
$fallbackForVectorsDB = 'db_main=' . AppwriteURL::unparse([
'scheme' => System::getEnv('_APP_DB_ADAPTER_VECTORSDB', 'postgresql'),
'host' => System::getEnv('_APP_DB_HOST_VECTORSDB', 'postgresql'),
'port' => System::getEnv('_APP_DB_PORT_VECTORSDB', '5432'),
'user' => System::getEnv('_APP_DB_USER', ''),
'pass' => System::getEnv('_APP_DB_PASS', ''),
'path' => System::getEnv('_APP_DB_SCHEMA', ''),
]);
$connections = [
'console' => [
'type' => 'database',
@@ -180,13 +196,25 @@ $register->set('pools', function () {
'type' => 'database',
'dsns' => $fallbackForDB,
'multiple' => true,
'schemes' => ['mariadb', 'mongodb', 'mysql', 'postgresql'],
'schemes' => ['mongodb','mariadb', 'mysql','postgresql'],
],
'documentsdb' => [
'type' => 'database',
'dsns' => System::getEnv('_APP_CONNECTIONS_DATABASE_DOCUMENTSDB', $fallbackForDocumentsDB),
'multiple' => true,
'schemes' => ['mongodb'],
],
'vectorsdb' => [
'type' => 'database',
'dsns' => System::getEnv('_APP_CONNECTIONS_DATABASE_VECTORSDB', $fallbackForVectorsDB),
'multiple' => true,
'schemes' => ['postgresql'],
],
'logs' => [
'type' => 'database',
'dsns' => System::getEnv('_APP_CONNECTIONS_DB_LOGS', $fallbackForDB),
'multiple' => false,
'schemes' => ['mariadb', 'mongodb', 'mysql', 'postgresql'],
'schemes' => ['mongodb','mariadb', 'mysql','postgresql'],
],
'publisher' => [
'type' => 'publisher',
@@ -214,29 +242,18 @@ $register->set('pools', function () {
],
];
$maxConnections = System::getEnv('_APP_CONNECTIONS_MAX', 151);
$instanceConnections = $maxConnections / System::getEnv('_APP_POOL_CLIENTS', 14);
$maxConnections = (int) System::getEnv('_APP_CONNECTIONS_MAX', 151);
$instanceConnections = $maxConnections / (int) System::getEnv('_APP_POOL_CLIENTS', 14);
$multiprocessing = System::getEnv('_APP_SERVER_MULTIPROCESS', 'disabled') === 'enabled';
if ($multiprocessing) {
$workerCount = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
} else {
$workerCount = 1;
}
if ($workerCount > $instanceConnections) {
throw new \Exception('Pool size is too small. Increase the number of allowed database connections or decrease the number of workers.', 500);
}
$poolSize = (int)($instanceConnections / $workerCount);
$workerCount = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
$poolSize = max(1, (int)($instanceConnections / $workerCount));
foreach ($connections as $key => $connection) {
$type = $connection['type'] ?? '';
$multiple = $connection['multiple'] ?? false;
$schemes = $connection['schemes'] ?? [];
$type = $connection['type'];
$multiple = $connection['multiple'];
$schemes = $connection['schemes'];
$config = [];
$dsns = explode(',', $connection['dsns'] ?? '');
$dsns = explode(',', $connection['dsns']);
foreach ($dsns as &$dsn) {
$dsn = explode('=', $dsn);
$name = ($multiple) ? $key . '_' . $dsn[0] : $key;
@@ -280,7 +297,7 @@ $register->set('pools', function () {
]);
});
},
'mongodb' => function () use ($dsnHost, $dsnPort, $dsnUser, $dsnPass, $dsnDatabase, $dsn) {
'mongodb' => function () use ($dsnHost, $dsnPort, $dsnUser, $dsnPass, $dsnDatabase) {
try {
$mongo = new MongoClient($dsnDatabase, $dsnHost, (int)$dsnPort, $dsnUser, $dsnPass, false);
@$mongo->connect();
@@ -301,7 +318,7 @@ $register->set('pools', function () {
));
});
},
'redis' => function () use ($dsnHost, $dsnPort, $dsnPass) {
default => function () use ($dsnHost, $dsnPort, $dsnPass) {
$redis = new \Redis();
@$redis->pconnect($dsnHost, (int)$dsnPort);
if ($dsnPass) {
@@ -311,7 +328,6 @@ $register->set('pools', function () {
return $redis;
},
default => throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Invalid scheme'),
};
$poolAdapter = System::getEnv('_APP_POOL_ADAPTER', default: 'stack') === 'swoole' ? new SwoolePool() : new StackPool();
@@ -405,35 +421,20 @@ $register->set('db', function () {
});
$register->set('smtp', function () {
$mail = new PHPMailer(true);
$mail->isSMTP();
$username = System::getEnv('_APP_SMTP_USERNAME');
$password = System::getEnv('_APP_SMTP_PASSWORD');
$mail->XMailer = 'Appwrite Mailer';
$mail->Host = System::getEnv('_APP_SMTP_HOST', 'smtp');
$mail->Port = System::getEnv('_APP_SMTP_PORT', 25);
$mail->SMTPAuth = !empty($username) && !empty($password);
$mail->Username = $username;
$mail->Password = $password;
$mail->SMTPSecure = System::getEnv('_APP_SMTP_SECURE', '');
$mail->SMTPAutoTLS = false;
$mail->SMTPKeepAlive = true;
$mail->CharSet = 'UTF-8';
$mail->Timeout = 10; /* Connection timeout */
$mail->getSMTPInstance()->Timelimit = 30; /* Timeout for each individual SMTP command (e.g. HELO, EHLO, etc.) */
$from = \urldecode(System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'));
$email = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM);
$mail->setFrom($email, $from);
$mail->addReplyTo($email, $from);
$mail->isHTML(true);
return $mail;
$username = System::getEnv('_APP_SMTP_USERNAME', '');
$password = System::getEnv('_APP_SMTP_PASSWORD', '');
return new SMTP(
host: System::getEnv('_APP_SMTP_HOST', 'smtp'),
port: (int) System::getEnv('_APP_SMTP_PORT', 25),
username: $username,
password: $password,
smtpSecure: System::getEnv('_APP_SMTP_SECURE', ''),
smtpAutoTLS: false,
xMailer: 'Appwrite Mailer',
timeout: 10,
keepAlive: true,
timelimit: 30,
);
});
$register->set('geodb', function () {
return new Reader(__DIR__ . '/../assets/dbip/dbip-country-lite-2025-12.mmdb');
+69 -1155
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+436
View File
@@ -0,0 +1,436 @@
<?php
use Appwrite\Event\Build;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\Realtime;
use Appwrite\Event\Webhook;
use Appwrite\Usage\Context;
use Appwrite\Utopia\Database\Documents\User;
use Utopia\Audit\Adapter\Database as AdapterDatabase;
use Utopia\Audit\Audit as UtopiaAudit;
use Utopia\Cache\Cache;
use Utopia\Console;
use Utopia\Database\Adapter\Pool as DatabasePool;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\DI\Container;
use Utopia\DSN\DSN;
use Utopia\Logger\Log;
use Utopia\Pools\Group;
use Utopia\Queue\Publisher;
use Utopia\Registry\Registry;
use Utopia\Storage\Device\Telemetry as TelemetryDevice;
use Utopia\System\System;
use Utopia\Telemetry\Adapter as Telemetry;
/**
* Register per-job resources on the given container.
* These resources depend on the queue message or keep mutable state and
* must be fresh for each worker job.
*/
return function (Container $container): void {
$container->set('log', fn () => new Log(), []);
$container->set('usage', fn () => new Context(), []);
$container->set('authorization', function () {
$authorization = new Authorization();
$authorization->disable();
return $authorization;
}, []);
$container->set('dbForPlatform', function (Cache $cache, Group $pools, Authorization $authorization) {
$adapter = new DatabasePool($pools->get('console'));
$dbForPlatform = new Database($adapter, $cache);
$dbForPlatform
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setNamespace('_console')
->setDocumentType('users', User::class);
return $dbForPlatform;
}, ['cache', 'pools', 'authorization']);
$container->set('project', function ($message, Database $dbForPlatform) {
$payload = $message->getPayload() ?? [];
$project = new Document($payload['project'] ?? []);
if ($project->isEmpty() || $project->getId() === 'console') {
return $project;
}
return $dbForPlatform->getDocument('projects', $project->getId());
}, ['message', 'dbForPlatform']);
$container->set('dbForProject', function (Cache $cache, Group $pools, Document $project, Database $dbForPlatform, Authorization $authorization) {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForPlatform;
}
try {
$dsn = new DSN($project->getAttribute('database'));
} catch (\InvalidArgumentException) {
// TODO: Temporary until all projects are using shared tables
$dsn = new DSN('mysql://' . $project->getAttribute('database'));
}
$adapter = new DatabasePool($pools->get($dsn->getHost()));
$database = new Database($adapter, $cache);
$database->setDocumentType('users', User::class);
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
->setSharedTables(false)
->setTenant(null)
->setNamespace('_' . $project->getSequence());
}
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
return $database;
}, ['cache', 'pools', 'project', 'dbForPlatform', 'authorization']);
$container->set('getProjectDB', function (Group $pools, Database $dbForPlatform, Cache $cache, Authorization $authorization) {
$databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools
return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$databases): Database {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForPlatform;
}
try {
$dsn = new DSN($project->getAttribute('database'));
} catch (\InvalidArgumentException) {
// TODO: Temporary until all projects are using shared tables
$dsn = new DSN('mysql://' . $project->getAttribute('database'));
}
if (isset($databases[$dsn->getHost()])) {
$database = $databases[$dsn->getHost()];
$database->setAuthorization($authorization);
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
->setSharedTables(false)
->setTenant(null)
->setNamespace('_' . $project->getSequence());
}
return $database;
}
$adapter = new DatabasePool($pools->get($dsn->getHost()));
$database = new Database($adapter, $cache);
$databases[$dsn->getHost()] = $database;
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
->setSharedTables(false)
->setTenant(null)
->setNamespace('_' . $project->getSequence());
}
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
return $database;
};
}, ['pools', 'dbForPlatform', 'cache', 'authorization']);
$container->set('getDatabasesDB', function (Cache $cache, Registry $register, Document $project, Authorization $authorization) {
return function (Document $database, ?Document $projectDocument = null) use ($cache, $register, $project, $authorization): Database {
$projectDocument ??= $project;
$databaseDSN = $database->getAttribute('database', $project->getAttribute('database', ''));
$databaseType = $database->getAttribute('type', '');
// Backwards-compatibility: older or seeded legacy databases may not have a DSN stored
// in the "database" attribute. In that case, fall back to the project's database DSN.
if ($databaseDSN === '') {
$databaseDSN = $projectDocument->getAttribute('database', '');
}
try {
$databaseDSN = new DSN($databaseDSN);
} catch (\InvalidArgumentException) {
$databaseDSN = new DSN('mysql://' . $databaseDSN);
}
try {
$dsn = new DSN($projectDocument->getAttribute('database'));
} catch (\InvalidArgumentException) {
// Temporary fallback until all projects use shared tables
$dsn = new DSN('mysql://' . $projectDocument->getAttribute('database'));
}
$pools = $register->get('pools');
$databaseHost = $databaseDSN->getHost();
$pool = $pools->get($databaseHost);
$adapter = new DatabasePool($pool);
$database = new Database($adapter, $cache);
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization);
$database->getAdapter()->setSupportForAttributes($databaseType !== DOCUMENTSDB);
$sharedTables = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')));
// For separate pools (documentsdb/vectorsdb), check their own shared tables config.
// If not configured, use dedicated mode to avoid cross-engine tenant type mismatches.
if ($databaseHost !== $dsn->getHost()) {
$dbTypeSharedTables = match ($databaseType) {
DOCUMENTSDB => \array_filter(\explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES', ''))),
VECTORSDB => \array_filter(\explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES', ''))),
default => [],
};
if (\in_array($databaseHost, $dbTypeSharedTables)) {
$database
->setSharedTables(true)
->setTenant($projectDocument->getSequence())
->setNamespace($databaseDSN->getParam('namespace'));
} else {
$database
->setSharedTables(false)
->setTenant(null)
->setNamespace('_' . $projectDocument->getSequence());
}
} elseif (\in_array($dsn->getHost(), $sharedTables, true)) {
$database
->setSharedTables(true)
->setTenant($projectDocument->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
->setSharedTables(false)
->setTenant(null)
->setNamespace('_' . $projectDocument->getSequence());
}
$database->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
return $database;
};
}, ['cache', 'register', 'project', 'authorization']);
$container->set('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
$database = null;
return function (?Document $project = null) use ($pools, $cache, $authorization, &$database) {
if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant($project->getSequence());
return $database;
}
$adapter = new DatabasePool($pools->get('logs'));
$database = new Database($adapter, $cache);
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setSharedTables(true)
->setNamespace('logsV1')
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER)
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES_WORKER);
if ($project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant($project->getSequence());
}
return $database;
};
}, ['pools', 'cache', 'authorization']);
$container->set('abuseRetention', function () {
return \time() - (int) System::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', 86400); // 1 day
}, []);
$container->set('auditRetention', function (Document $project) {
if ($project->getId() === 'console') {
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE', 15778800)); // 6 months
}
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 1209600)); // 14 days
}, ['project']);
$container->set('executionRetention', function () {
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', 1209600)); // 14 days
}, []);
$container->set('queueForDatabase', function (Publisher $publisher) {
return new EventDatabase($publisher);
}, ['publisher']);
$container->set('queueForMessaging', function (Publisher $publisher) {
return new Messaging($publisher);
}, ['publisher']);
$container->set('queueForMails', function (Publisher $publisher) {
return new Mail($publisher);
}, ['publisher']);
$container->set('queueForBuilds', function (Publisher $publisher) {
return new Build($publisher);
}, ['publisher']);
$container->set('queueForDeletes', function (Publisher $publisher) {
return new Delete($publisher);
}, ['publisher']);
$container->set('queueForEvents', function (Publisher $publisher) {
return new Event($publisher);
}, ['publisher']);
$container->set('queueForWebhooks', function (Publisher $publisher) {
return new Webhook($publisher);
}, ['publisher']);
$container->set('queueForFunctions', function (Publisher $publisher) {
return new Func($publisher);
}, ['publisher']);
$container->set('queueForRealtime', function () {
return new Realtime();
}, []);
$container->set('deviceForSites', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
$container->set('deviceForMigrations', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_IMPORTS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
$container->set('deviceForFunctions', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
$container->set('deviceForFiles', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
$container->set('deviceForBuilds', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
$container->set('deviceForCache', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_CACHE . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
$container->set('logError', function (Registry $register, Document $project) {
return function (Throwable $error, string $namespace, string $action, ?array $extras = null) use ($register, $project) {
$logger = $register->get('logger');
if ($logger) {
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
$log = new Log();
$log->setNamespace($namespace);
$log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname()));
$log->setVersion($version);
$log->setType(Log::TYPE_ERROR);
$log->setMessage($error->getMessage());
$log->addTag('code', $error->getCode());
$log->addTag('verboseType', \get_class($error));
$log->addTag('projectId', $project->getId());
$log->addExtra('file', $error->getFile());
$log->addExtra('line', $error->getLine());
$log->addExtra('trace', $error->getTraceAsString());
if ($error->getPrevious() !== null) {
if ($error->getPrevious()->getMessage() != $error->getMessage()) {
$log->addExtra('previousMessage', $error->getPrevious()->getMessage());
}
$log->addExtra('previousFile', $error->getPrevious()->getFile());
$log->addExtra('previousLine', $error->getPrevious()->getLine());
}
foreach (($extras ?? []) as $key => $value) {
$log->addExtra($key, $value);
}
$log->setAction($action);
$isProduction = System::getEnv('_APP_ENV', 'development') === 'production';
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
try {
$responseCode = $logger->addLog($log);
Console::info('Error log pushed with status code: ' . $responseCode);
} catch (Throwable $th) {
Console::error('Error pushing log: ' . $th->getMessage());
}
}
Console::warning("Failed: {$error->getMessage()}");
Console::warning($error->getTraceAsString());
if ($error->getPrevious() !== null) {
if ($error->getPrevious()->getMessage() != $error->getMessage()) {
Console::warning("Previous Failed: {$error->getPrevious()->getMessage()}");
}
Console::warning("Previous File: {$error->getPrevious()->getFile()} Line: {$error->getPrevious()->getLine()}");
}
};
}, ['register', 'project']);
$container->set('getAudit', function (Database $dbForPlatform, callable $getProjectDB) {
return function (Document $project) use ($dbForPlatform, $getProjectDB) {
if ($project->isEmpty() || $project->getId() === 'console') {
$adapter = new AdapterDatabase($dbForPlatform);
return new UtopiaAudit($adapter);
}
$dbForProject = $getProjectDB($project);
$adapter = new AdapterDatabase($dbForProject);
return new UtopiaAudit($adapter);
};
}, ['dbForPlatform', 'getProjectDB']);
$container->set('executionsRetentionCount', function (Document $project, array $plan) {
if ($project->getId() === 'console' || empty($plan)) {
return 0;
}
return (int) ($plan['executionsRetentionCount'] ?? 100);
}, ['project', 'plan']);
};
+2
View File
@@ -1,9 +1,11 @@
<?php
use Appwrite\Bus\Listeners\Log;
use Appwrite\Bus\Listeners\Mails;
use Appwrite\Bus\Listeners\Usage;
return [
new Log(),
new Mails(),
new Usage(),
];
+462 -61
View File
@@ -33,21 +33,28 @@ use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\DI\Container;
use Utopia\DSN\DSN;
use Utopia\Http\Http;
use Utopia\Logger\Log;
use Utopia\Pools\Group;
use Utopia\Registry\Registry;
use Utopia\Span\Span;
use Utopia\System\System;
use Utopia\Telemetry\Adapter\None as NoTelemetry;
use Utopia\WebSocket\Adapter;
use Utopia\WebSocket\Server;
/**
* @var Registry $register
*/
require_once __DIR__ . '/init.php';
if (System::getEnv('_APP_EDITION', 'self-hosted') === 'self-hosted') {
require_once __DIR__ . '/init/span.php';
}
/** @var Registry $register */
$register = $GLOBALS['register'] ?? throw new \RuntimeException('Registry not initialized');
$registerConnectionResources ??= require __DIR__ . '/init/realtime/connection.php';
Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
// Log uncaught exceptions in one line instead of relying on Swoole's full backtrace dump
@@ -237,10 +244,14 @@ if (!function_exists('getTelemetry')) {
if (!function_exists('triggerStats')) {
function triggerStats(array $event, string $projectId): void
{
return;
}
}
global $container;
$container->set('pools', function ($register) {
return $register->get('pools');
}, ['register']);
$realtime = getRealtime();
/**
@@ -256,7 +267,9 @@ $stats->create();
$containerId = uniqid();
$statsDocument = null;
$workerNumber = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
$workerNumber = intval(System::getEnv('_APP_WORKERS_NUM', 0))
?: intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
$adapter = new Adapter\Swoole(port: System::getEnv('PORT', 80));
$adapter
@@ -320,14 +333,14 @@ if (!function_exists('logError')) {
$server->error(logError(...));
$server->onStart(function () use ($stats, $register, $containerId, &$statsDocument) {
$server->onStart(function () use ($stats, $containerId, &$statsDocument) {
sleep(5); // wait for the initial database schema to be ready
Console::success('Server started successfully');
/**
* Create document for this worker to share stats across Containers.
*/
go(function () use ($register, $containerId, &$statsDocument) {
go(function () use ($containerId, &$statsDocument) {
$attempts = 0;
$database = getConsoleDB();
@@ -357,7 +370,7 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume
*/
// TODO: Remove this if check once it doesn't cause issues for cloud
if (System::getEnv('_APP_EDITION', 'self-hosted') === 'self-hosted') {
Timer::tick(5000, function () use ($register, $stats, &$statsDocument) {
Timer::tick(5000, function () use ($stats, &$statsDocument) {
$payload = [];
foreach ($stats as $projectId => $value) {
$payload[$projectId] = $stats->get($projectId, 'connectionsTotal');
@@ -388,15 +401,32 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
Console::success('Worker ' . $workerId . ' started successfully');
$telemetry = getTelemetry($workerId);
$realtimeDelayBuckets = [100, 250, 500, 750, 1000, 1500, 2000, 3000, 5000, 7500, 10000, 15000, 30000];
$workerTelemetryAttributes = ['workerId' => (string) $workerId];
$register->set('telemetry', fn () => $telemetry);
$register->set('telemetry.workerAttributes', fn () => $workerTelemetryAttributes);
$register->set('telemetry.workerCounter', fn () => $telemetry->createUpDownCounter('realtime.server.active_workers'));
$register->set('telemetry.workerClientCounter', fn () => $telemetry->createUpDownCounter('realtime.server.worker_clients'));
$register->set('telemetry.workerSubscriptionCounter', fn () => $telemetry->createUpDownCounter('realtime.server.worker_subscriptions'));
$register->set('telemetry.connectionCounter', fn () => $telemetry->createUpDownCounter('realtime.server.open_connections'));
$register->set('telemetry.connectionCreatedCounter', fn () => $telemetry->createCounter('realtime.server.connection.created'));
$register->set('telemetry.messageSentCounter', fn () => $telemetry->createCounter('realtime.server.message.sent'));
$register->set('telemetry.deliveryDelayHistogram', fn () => $telemetry->createHistogram(
name: 'realtime.server.delivery_delay',
unit: 'ms',
advisory: ['ExplicitBucketBoundaries' => $realtimeDelayBuckets],
));
$register->set('telemetry.arrivalDelayHistogram', fn () => $telemetry->createHistogram(
name: 'realtime.server.arrival_delay',
unit: 'ms',
advisory: ['ExplicitBucketBoundaries' => $realtimeDelayBuckets],
));
$register->get('telemetry.workerCounter')->add(1);
$attempts = 0;
$start = time();
Timer::tick(5000, function () use ($server, $register, $realtime, $stats) {
Timer::tick(5000, function () use ($server, $realtime, $stats) {
/**
* Sending current connections to project channels on the console project every 5 seconds.
*/
@@ -442,7 +472,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
]
];
$server->send($realtime->getSubscribers($event), json_encode([
$server->send(array_keys($realtime->getSubscribers($event)), json_encode([
'type' => 'event',
'data' => $event['data']
]));
@@ -508,21 +538,38 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
$pubsub->subscribe(['realtime'], function (mixed $redis, string $channel, string $payload) use ($server, $workerId, $stats, $register, $realtime) {
$event = json_decode($payload, true);
$eventTimestamp = $event['data']['timestamp'] ?? null;
if (\is_string($eventTimestamp)) {
try {
$eventDate = new \DateTimeImmutable($eventTimestamp, new \DateTimeZone('UTC'));
$now = new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
$eventTimestampMs = (float) $eventDate->format('U.u') * 1000;
$nowTimestampMs = (float) $now->format('U.u') * 1000;
$arrivalDelayMs = (int) \max(0, $nowTimestampMs - $eventTimestampMs);
$register->get('telemetry.arrivalDelayHistogram')->record($arrivalDelayMs);
} catch (\Throwable) {
// Ignore invalid timestamp payloads.
}
}
if ($event['permissionsChanged'] && isset($event['userId'])) {
$projectId = $event['project'];
$userId = $event['userId'];
if ($realtime->hasSubscriber($projectId, 'user:' . $userId)) {
$connection = array_key_first(reset($realtime->subscriptions[$projectId]['user:' . $userId]));
$subscriptionsBefore = \count($realtime->getSubscriptionMetadata($connection));
$consoleDatabase = getConsoleDB();
$project = $consoleDatabase->getAuthorization()->skip(fn () => $consoleDatabase->getDocument('projects', $projectId));
$database = getProjectDB($project);
/** @var Appwrite\Utopia\Database\Documents\User $user */
/** @var User $user */
$user = $database->getDocument('users', $userId);
$roles = $user->getRoles($database->getAuthorization());
$authorization = $realtime->connections[$connection]['authorization'] ?? null;
$previousUserId = $realtime->connections[$connection]['userId'] ?? '';
$meta = $realtime->getSubscriptionMetadata($connection);
@@ -530,13 +577,19 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
foreach ($meta as $subscriptionId => $subscription) {
$queries = Query::parseQueries($subscription['queries'] ?? []);
$channels = Realtime::rebindAccountChannels(
$subscription['channels'] ?? [],
$previousUserId,
$userId
);
$realtime->subscribe(
$projectId,
$connection,
$subscriptionId,
$roles,
$subscription['channels'] ?? [],
$queries
$channels,
$queries,
$userId
);
}
@@ -544,12 +597,18 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
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'));
}
}
}
$receivers = $realtime->getSubscribers($event);
if (Http::isDevelopment() && !empty($receivers)) {
if (System::getEnv('_APP_ENV', 'production') === 'development' && !empty($receivers)) {
Console::log("[Debug][Worker {$workerId}] Receivers: " . count($receivers));
Console::log("[Debug][Worker {$workerId}] Connection IDs: " . json_encode(array_keys($receivers)));
Console::log("[Debug][Worker {$workerId}] Matched: " . json_encode(array_values($receivers)));
@@ -586,6 +645,20 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
if ($total > 0) {
$register->get('telemetry.messageSentCounter')->add($total);
$stats->incr($event['project'], 'messages', $total);
$updatedAt = $event['data']['payload']['$updatedAt'] ?? null;
if (\is_string($updatedAt)) {
try {
$updatedAtDate = new \DateTimeImmutable($updatedAt, new \DateTimeZone('UTC'));
$now = new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
$updatedAtTimestampMs = (float) $updatedAtDate->format('U.u') * 1000;
$nowTimestampMs = (float) $now->format('U.u') * 1000;
$delayMs = (int) \max(0, $nowTimestampMs - $updatedAtTimestampMs);
$register->get('telemetry.deliveryDelayHistogram')->record($delayMs);
} catch (\Throwable) {
// Ignore invalid timestamp payloads.
}
}
$projectId = $event['project'] ?? null;
@@ -615,25 +688,50 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
Console::error('Failed to restart pub/sub...');
});
$server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $register, $stats, &$realtime) {
$app = new Http('UTC');
$server->onWorkerStop(function (int $workerId) use ($register) {
Console::warning('Worker ' . $workerId . ' stopping');
try {
$register->get('telemetry.workerCounter')->add(-1);
} catch (\Throwable $th) {
Console::error('Realtime onWorkerStop telemetry error: ' . $th->getMessage());
}
});
$server->onOpen(function (int $connection, SwooleRequest $request) use ($server, $register, $stats, &$realtime, $registerConnectionResources) {
global $container;
$request = new Request($request);
$response = new Response(new SwooleResponse());
Console::info("Connection open (user: {$connection})");
Http::setResource('pools', fn () => $register->get('pools'));
Http::setResource('request', fn () => $request);
Http::setResource('response', fn () => $response);
$connectionContainer = new Container($container);
$connectionContainer->set('request', fn () => $request);
$registerConnectionResources($connectionContainer);
$project = null;
$logUser = null;
$authorization = null;
$rawSize = $request->getSize();
$channelCount = 0;
$subscriptionCount = 0;
$outboundBytes = 0;
$responseCode = 200;
$subscriptionMode = 'message';
$success = false;
Span::init('realtime.open');
Span::add('realtime.connectionId', $connection);
Span::add('realtime.inboundBytes', $rawSize);
if (!empty($request->getOrigin())) {
Span::add('realtime.origin', $request->getOrigin());
}
try {
/** @var Document $project */
$project = $app->getResource('project');
$authorization = $app->getResource('authorization');
$project = $connectionContainer->get('project');
$authorization = $connectionContainer->get('authorization');
/*
* Project Check
@@ -642,10 +740,16 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, 'Missing or unknown project ID');
}
$timelimit = $connectionContainer->get('timelimit');
$user = $connectionContainer->get('user'); /** @var User $user */
$logUser = $user;
$apis = $project->getAttribute('apis', []);
// Websocket is what to check, but realtime is checked too for backwards compatibility
$websocketEnabled = $apis['websocket'] ?? $apis['realtime'] ?? true;
if (
array_key_exists('realtime', $project->getAttribute('apis', []))
&& !$project->getAttribute('apis', [])['realtime']
&& !(User::isPrivileged($authorization->getRoles()) || User::isApp($authorization->getRoles()))
!$websocketEnabled
&& !($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles()))
) {
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
}
@@ -656,10 +760,6 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'Project is not accessible in this region. Please make sure you are using the correct endpoint');
}
$timelimit = $app->getResource('timelimit');
$user = $app->getResource('user'); /** @var User $user */
$logUser = $user;
/*
* Abuse Check
*
@@ -676,8 +776,6 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
throw new Exception(Exception::REALTIME_TOO_MANY_MESSAGES, 'Too many requests');
}
$rawSize = $request->getSize();
triggerStats([
METRIC_REALTIME_INBOUND => $rawSize,
], $project->getId());
@@ -688,7 +786,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
* Skip this check for non-web platforms which are not required to send an origin header.
*/
$origin = $request->getOrigin();
$originValidator = $app->getResource('originValidator');
$originValidator = $connectionContainer->get('originValidator');
if (!empty($origin) && !$originValidator->isValid($origin) && $project->getId() !== 'console') {
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, $originValidator->getDescription());
@@ -697,15 +795,53 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
$roles = $user->getRoles($authorization);
$channels = Realtime::convertChannels($request->getQuery('channels', []), $user->getId());
$channelCount = \count($channels);
$updateStats = static function (string $projectId, ?string $teamId, string $payloadJson) use ($register, $stats): void {
$register->get('telemetry.connectionCounter')->add(1);
$register->get('telemetry.workerClientCounter')->add(1, $register->get('telemetry.workerAttributes'));
$register->get('telemetry.connectionCreatedCounter')->add(1);
$stats->set($projectId, [
'projectId' => $projectId,
'teamId' => $teamId
]);
$stats->incr($projectId, 'connections');
$stats->incr($projectId, 'connectionsTotal');
triggerStats([
METRIC_REALTIME_CONNECTIONS => 1,
METRIC_REALTIME_OUTBOUND => \strlen($payloadJson),
], $projectId);
};
/**
* Channels Check
*/
if (empty($channels)) {
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, 'Missing channels');
// in case of message based 'subscribe' channels will be empty at first and only projectId and roles will be available
$sanitizedUser = empty($user->getId()) ? null : $response->output($user, Response::MODEL_ACCOUNT);
$connectedPayloadJson = json_encode([
'type' => 'connected',
'data' => [
'channels' => [],
'subscriptions' => [],
'user' => $sanitizedUser
]
]);
$realtime->subscribe($project->getId(), $connection, '', $roles, [], [], $user->getId());
$realtime->connections[$connection]['authorization'] = $authorization;
$server->send([$connection], $connectedPayloadJson);
$outboundBytes += \strlen($connectedPayloadJson);
$updateStats($project->getId(), $project->getAttribute('teamId'), $connectedPayloadJson);
$subscriptionMode = 'message';
$success = true;
return;
}
$names = array_keys($channels);
$subscriptionMode = 'url';
try {
$subscriptions = Realtime::constructSubscriptions(
@@ -726,11 +862,16 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
$subscriptionId,
$roles,
$subscription['channels'],
$subscription['queries']
$subscription['queries'],
$user->getId()
);
$mapping[$index] = $subscriptionId;
}
$subscriptionCount = \count($subscriptions);
if (!empty($subscriptions)) {
$register->get('telemetry.workerSubscriptionCounter')->add(\count($subscriptions), $register->get('telemetry.workerAttributes'));
}
$realtime->connections[$connection]['authorization'] = $authorization;
@@ -746,21 +887,9 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
]);
$server->send([$connection], $connectedPayloadJson);
$register->get('telemetry.connectionCounter')->add(1);
$register->get('telemetry.connectionCreatedCounter')->add(1);
$stats->set($project->getId(), [
'projectId' => $project->getId(),
'teamId' => $project->getAttribute('teamId')
]);
$stats->incr($project->getId(), 'connections');
$stats->incr($project->getId(), 'connectionsTotal');
$connectedOutboundBytes = \strlen($connectedPayloadJson);
triggerStats([METRIC_REALTIME_CONNECTIONS => 1, METRIC_REALTIME_OUTBOUND => $connectedOutboundBytes], $project->getId());
$outboundBytes += \strlen($connectedPayloadJson);
$updateStats($project->getId(), $project->getAttribute('teamId'), $connectedPayloadJson);
$success = true;
} catch (Throwable $th) {
logError($th, 'realtime', project: $project, user: $logUser, authorization: $authorization);
@@ -770,12 +899,13 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
if (!\is_int($code)) {
$code = 500;
}
$responseCode = $code;
$message = $th->getMessage();
// sanitize 0 && 5xx errors
$realtimeViolation = $th instanceof AppwriteException && $th->getType() === AppwriteException::REALTIME_POLICY_VIOLATION;
if (($code === 0 || $code >= 500) && !$realtimeViolation && !Http::isDevelopment()) {
if (($code === 0 || $code >= 500) && !$realtimeViolation && System::getEnv('_APP_ENV', 'production') !== 'development') {
$message = 'Error: Server Error';
}
@@ -787,30 +917,59 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
]
];
$server->send([$connection], json_encode($response));
$responsePayloadJson = json_encode($response);
$server->send([$connection], $responsePayloadJson);
$outboundBytes += \strlen($responsePayloadJson);
$server->close($connection, $code);
if (Http::isDevelopment()) {
if (System::getEnv('_APP_ENV', 'production') === 'development') {
Console::error('[Error] Connection Error');
Console::error('[Error] Code: ' . $response['data']['code']);
Console::error('[Error] Message: ' . $response['data']['message']);
}
Span::error($th);
} finally {
Span::add('realtime.success', $success);
Span::add('realtime.responseCode', $responseCode);
Span::add('realtime.subscriptionMode', $subscriptionMode);
Span::add('realtime.channelCount', $channelCount);
Span::add('realtime.subscriptionCount', $subscriptionCount);
Span::add('realtime.outboundBytes', $outboundBytes);
if (!empty($project?->getId())) {
Span::add('realtime.projectId', $project->getId());
}
if (!empty($logUser?->getId())) {
Span::add('realtime.userId', $logUser->getId());
}
Span::current()?->finish();
}
});
$server->onMessage(function (int $connection, string $message) use ($server, $register, $realtime, $containerId) {
$server->onMessage(function (int $connection, string $message) use ($server, $realtime, $containerId, $register) {
$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;
Span::init('realtime.message');
Span::add('realtime.connectionId', $connection);
Span::add('realtime.inboundBytes', $rawSize);
Span::add('realtime.containerId', $containerId);
try {
$rawSize = \strlen($message);
$response = new Response(new SwooleResponse());
$projectId = $realtime->connections[$connection]['projectId'] ?? null;
// Get authorization from connection (stored during onOpen)
$authorization = $realtime->connections[$connection]['authorization'] ?? null;
if ($authorization === null) {
$authorization = new Authorization('');
$authorization = new Authorization();
}
$database = getConsoleDB();
@@ -855,6 +1014,12 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Message format is not valid.');
}
$messageType = $message['type'] ?? 'invalid';
if (!\is_scalar($messageType)) {
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.');
@@ -867,6 +1032,7 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
]);
$server->send([$connection], $pongPayloadJson);
$outboundBytes += \strlen($pongPayloadJson);
if ($project !== null && !$project->isEmpty()) {
$pongOutboundBytes = \strlen($pongPayloadJson);
@@ -912,7 +1078,13 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
$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);
@@ -920,14 +1092,20 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
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,
$subscription['channels'] ?? [],
$queries
$channels,
$queries,
$user->getId()
);
}
}
@@ -936,6 +1114,12 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
$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([
@@ -948,6 +1132,7 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
]);
$server->send([$connection], $authResponsePayloadJson);
$outboundBytes += \strlen($authResponsePayloadJson);
if ($project !== null && !$project->isEmpty()) {
$authOutboundBytes = \strlen($authResponsePayloadJson);
@@ -961,20 +1146,185 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
break;
case 'subscribe':
/**
* Message based upsertion of a subscription
* If subscriptionId is given then it will match subId of the connection and update the subscription with channels and queries
* If non-existing subid is given or not given a new subid will be generated
* Similar to what we have now -> two subscribe() block with same channels and queries still two different subscriptions
*
* structure of the payload -> array of maps
* 'data' : [subscriptionId:"" , channels:[] , queries:[]]
*/
if (!is_array($message['data']) || !array_is_list($message['data'])) {
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Payload is not valid.');
}
$roles = $realtime->connections[$connection]['roles'] ?? [Role::guests()->toString()];
$userId = $realtime->connections[$connection]['userId'] ?? '';
// bulk validation + parsing before subscribing
$parsedPayloads = [];
$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) {
logError($th, 'realtimeMessage', project: $project, authorization: $authorization);
$code = $th->getCode();
if (!is_int($code)) {
$code = 500;
}
$responseCode = $code;
$message = $th->getMessage();
// sanitize 0 && 5xx errors
if (($code === 0 || $code >= 500) && !Http::isDevelopment()) {
if (($code === 0 || $code >= 500) && System::getEnv('_APP_ENV', 'production') !== 'development') {
$message = 'Error: Server Error';
}
@@ -986,19 +1336,52 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
]
];
$server->send([$connection], json_encode($response));
$responsePayloadJson = json_encode($response);
$server->send([$connection], $responsePayloadJson);
$outboundBytes += \strlen($responsePayloadJson);
if ($th->getCode() === 1008) {
$server->close($connection, $th->getCode());
}
Span::error($th);
} finally {
Span::add('realtime.success', $success);
Span::add('realtime.responseCode', $responseCode);
Span::add('realtime.subscriptionDelta', $subscriptionDelta);
Span::add('realtime.subscriptionsRequested', $subscriptionsRequested);
Span::add('realtime.subscriptionsRemoved', $subscriptionsRemoved);
Span::add('realtime.subscribe.subscriptionsCount', $subscriptionsRequested);
Span::add('realtime.outboundBytes', $outboundBytes);
Span::add('realtime.projectId', $project?->getId() ?? $projectId);
Span::add('realtime.userId', $realtime->connections[$connection]['userId'] ?? null);
Span::add('realtime.messageType', $messageType);
Span::current()?->finish();
}
});
$server->onClose(function (int $connection) use ($realtime, $stats, $register) {
$projectId = null;
$userId = null;
$subscriptionsBeforeClose = 0;
$success = false;
Span::init('realtime.close');
Span::add('realtime.connectionId', $connection);
if (array_key_exists($connection, $realtime->connections)) {
$projectId = $realtime->connections[$connection]['projectId'] ?? null;
$userId = $realtime->connections[$connection]['userId'] ?? null;
}
try {
if (array_key_exists($connection, $realtime->connections)) {
$stats->decr($realtime->connections[$connection]['projectId'], 'connectionsTotal');
$register->get('telemetry.connectionCounter')->add(-1);
$register->get('telemetry.workerClientCounter')->add(-1, $register->get('telemetry.workerAttributes'));
$subscriptionsBeforeClose = \count($realtime->getSubscriptionMetadata($connection));
if ($subscriptionsBeforeClose > 0) {
$register->get('telemetry.workerSubscriptionCounter')->add(-$subscriptionsBeforeClose, $register->get('telemetry.workerAttributes'));
}
$projectId = $realtime->connections[$connection]['projectId'];
@@ -1006,12 +1389,30 @@ $server->onClose(function (int $connection) use ($realtime, $stats, $register) {
METRIC_REALTIME_CONNECTIONS => -1,
], $projectId);
}
$success = true;
} catch (\Throwable $th) {
// Log only; do not rethrow. If we let this bubble, Swoole dumps full coroutine
// backtraces and unsubscribe() below would never run (connection cleanup would fail).
Console::error('Realtime onClose error: ' . $th->getMessage());
Span::error($th);
} finally {
try {
$realtime->unsubscribe($connection);
} catch (\Throwable $th) {
Console::error('Realtime onClose unsubscribe error: ' . $th->getMessage());
Span::error($th);
}
Span::add('realtime.success', $success);
if (!empty($projectId)) {
Span::add('realtime.projectId', $projectId);
}
if (!empty($userId)) {
Span::add('realtime.userId', $userId);
}
Span::add('realtime.subscriptionsBeforeClose', $subscriptionsBeforeClose);
Span::current()?->finish();
}
$realtime->unsubscribe($connection);
Console::info('Connection close: ' . $connection);
});
+32 -24
View File
@@ -13,7 +13,7 @@ $organization = $this->getParam('organization', '');
$image = $this->getParam('image', '');
$enableAssistant = $this->getParam('enableAssistant', false);
$dbService = $this->getParam('database', 'mongodb');
$allowedDbServices = ['mariadb', 'mongodb', 'postgresql'];
$allowedDbServices = ['mariadb', 'mongodb'];
if (!\in_array($dbService, $allowedDbServices, true)) {
$dbService = 'mongodb';
}
@@ -120,7 +120,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_SMTP_HOST
- _APP_SMTP_PORT
- _APP_SMTP_SECURE
@@ -194,7 +193,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
appwrite-console:
<<: *x-logging
container_name: appwrite-console
image: <?php echo $organization; ?>/console:7.6.4
image: <?php echo $organization; ?>/console:7.8.26
restart: unless-stopped
networks:
- appwrite
@@ -256,7 +255,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_USAGE_STATS
- _APP_LOGGING_CONFIG
@@ -287,7 +285,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_LOGGING_CONFIG
appwrite-worker-webhooks:
@@ -315,7 +312,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
@@ -356,7 +352,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_STORAGE_DEVICE
- _APP_STORAGE_S3_ACCESS_KEY
- _APP_STORAGE_S3_SECRET
@@ -416,7 +411,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_LOGGING_CONFIG
appwrite-worker-builds:
@@ -453,7 +447,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_LOGGING_CONFIG
- _APP_VCS_GITHUB_APP_NAME
- _APP_VCS_GITHUB_PRIVATE_KEY
@@ -529,7 +522,35 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_LOGGING_CONFIG
appwrite-worker-executions:
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
entrypoint: worker-executions
<<: *x-logging
container_name: appwrite-worker-executions
restart: unless-stopped
networks:
- appwrite
depends_on:
redis:
condition: service_healthy
<?= $dbService ?>:
condition: service_healthy
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_ENV
- _APP_DB_ADAPTER
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_LOGGING_CONFIG
appwrite-worker-functions:
@@ -563,7 +584,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_FUNCTIONS_TIMEOUT
- _APP_SITES_TIMEOUT
- _APP_COMPUTE_BUILD_TIMEOUT
@@ -601,7 +621,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
@@ -644,7 +663,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_LOGGING_CONFIG
- _APP_SMS_FROM
- _APP_SMS_PROVIDER
@@ -705,7 +723,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_LOGGING_CONFIG
- _APP_MIGRATIONS_FIREBASE_CLIENT_ID
- _APP_MIGRATIONS_FIREBASE_CLIENT_SECRET
@@ -744,7 +761,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_MAINTENANCE_INTERVAL
- _APP_MAINTENANCE_RETENTION_EXECUTION
- _APP_MAINTENANCE_RETENTION_CACHE
@@ -777,7 +793,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
@@ -810,7 +825,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
@@ -842,7 +856,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
@@ -878,7 +891,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
appwrite-task-scheduler-executions:
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
@@ -907,7 +919,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
appwrite-task-scheduler-messages:
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
@@ -936,7 +947,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
<?php if ($enableAssistant): ?>
appwrite-assistant:
@@ -964,7 +974,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
<<: *x-logging
restart: unless-stopped
stop_signal: SIGINT
image: openruntimes/executor:0.7.22
image: openruntimes/executor:0.11.4
networks:
- appwrite
- runtimes
@@ -1039,13 +1049,12 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
image: mongo:8.2.5
container_name: appwrite-mongodb
<<: *x-logging
restart: unless-stopped
networks:
- appwrite
volumes:
- appwrite-mongodb:/data/db
- appwrite-mongodb-keyfile:/data/keyfile
ports:
- "27017:27017"
environment:
- MONGO_INITDB_ROOT_USERNAME=root
- MONGO_INITDB_ROOT_PASSWORD=${_APP_DB_ROOT_PASS}
@@ -1176,7 +1185,6 @@ volumes:
<?php elseif ($dbService === 'mongodb'): ?>
appwrite-mongodb:
appwrite-mongodb-keyfile:
appwrite-mongodb-config:
<?php endif; ?>
appwrite-redis:
appwrite-cache:
+3 -1
View File
@@ -9,10 +9,11 @@ $defaultAppDomain = ($defaultAppDomain === 'traefik') ? 'localhost' : $defaultAp
$defaultEmailCertificates = $defaultEmailCertificates ?? '';
$defaultDatabase = $vars['_APP_DB_ADAPTER']['default'] ?? 'mongodb';
$lockedDatabase = $isUpgrade && empty($lockedDatabase) ? $defaultDatabase : $lockedDatabase;
$enabledDatabases = $enabledDatabases ?? ['mongodb', 'mariadb', 'postgresql'];
$isLocalInstall = $isLocalInstall ?? false;
$cardStep = min(4, $step);
$cardStep = ($step === 5) ? 4 : $step;
$stepFile = __DIR__ . "/installer/templates/steps/step-{$cardStep}.phtml";
if (!is_file($stepFile)) {
$stepFile = __DIR__ . "/installer/templates/steps/step-1.phtml";
@@ -64,6 +65,7 @@ $installerVersion = @filemtime(__DIR__ . '/installer/js/installer.js') ?: time()
data-default-secret-key=""
data-default-assistant-openai-key=""
data-default-database="<?php echo htmlspecialchars((string) $defaultDatabase, ENT_QUOTES, 'UTF-8'); ?>"
data-enabled-databases="<?php echo htmlspecialchars(json_encode(array_values($enabledDatabases)), ENT_QUOTES, 'UTF-8'); ?>"
<?php if ($isLocalInstall) { ?>
data-dev-mode="true"
<?php } ?>
+160
View File
@@ -11,9 +11,13 @@
--neutral-300: rgba(173, 173, 176, 1);
--neutral-400: rgba(151, 151, 155, 1);
--neutral-500: rgba(129, 129, 134, 1);
--neutral-600: rgba(108, 108, 113, 1);
--neutral-700: rgba(86, 86, 92, 1);
--neutral-750: rgba(65, 65, 70, 1);
--neutral-800: rgba(45, 45, 49, 1);
--neutral-850: rgba(29, 29, 33, 1);
--neutral-900: rgba(25, 25, 28, 1);
--neutral-250: rgba(195, 195, 198, 1);
/* Warning colors */
--web-orange-200: rgba(255, 213, 194, 1);
@@ -172,6 +176,44 @@
--fgColor-neutral-weak: var(--fgcolor-neutral-weak);
--fgColor-accent: var(--fgcolor-accent);
--fgColor-on-accent: var(--fgcolor-on-accent);
color-scheme: light dark;
}
@media (prefers-color-scheme: dark) {
:root {
--bgcolor-neutral-default: var(--neutral-900);
--bgcolor-neutral-primary: var(--neutral-850);
--bgcolor-neutral-secondary: var(--neutral-800);
--bgcolor-neutral-tertiary: var(--neutral-800);
--bgcolor-neutral-invert-weaker: var(--neutral-400);
--bgcolor-neutral-invert-weak: var(--neutral-300);
--bgcolor-success-weak: rgba(16, 185, 129, 0.12);
--bgcolor-warning-weaker: rgba(254, 124, 67, 0.08);
--bgcolor-warning-weak: rgba(254, 124, 67, 0.12);
--bgcolor-error-weaker: rgba(255, 69, 58, 0.08);
--fgcolor-neutral-primary: var(--neutral-50);
--fgcolor-neutral-secondary: var(--neutral-250);
--fgcolor-neutral-tertiary: var(--neutral-500);
--fgcolor-neutral-weak: var(--neutral-600);
--fgcolor-on-accent: var(--neutral-0);
--fgcolor-on-invert: var(--neutral-900);
--fgcolor-on-success-weak: rgba(52, 211, 153, 1);
--fgcolor-warning: var(--web-orange-500);
--fgcolor-on-warning-weak: var(--web-orange-500);
--fgcolor-error: var(--web-red-500);
--fgcolor-on-error: var(--neutral-0);
--border-neutral: var(--neutral-800);
--border-neutral-strong: var(--neutral-750);
--border-neutral-stronger: var(--neutral-600);
--border-focus: var(--neutral-600);
--overlay-neutral-hover: rgba(255, 255, 255, 0.04);
--overlay-neutral-pressed: rgba(255, 255, 255, 0.08);
--overlay-scrim: rgba(0, 0, 0, 0.48);
}
}
.installer-toast-stack {
@@ -436,6 +478,10 @@ body {
overflow: hidden;
}
.installer-page[data-upgrade='true'] .installer-step {
min-height: 0;
}
.action-shell {
display: flex;
flex-direction: column;
@@ -649,6 +695,19 @@ body {
transform: translateY(10px);
}
.install-counter {
margin-left: auto;
opacity: 0;
transition: opacity 0.2s ease;
white-space: nowrap;
user-select: none;
color: var(--fgcolor-neutral-secondary);
}
.install-row[data-status='in-progress'] .install-counter:not(:empty) {
opacity: 1;
}
.install-row-toggle {
margin-left: auto;
width: 32px;
@@ -855,6 +914,17 @@ body {
gap: var(--gap-m);
}
.install-global-actions {
display: flex;
justify-content: center;
gap: var(--gap-m);
padding: var(--space-4) 0;
}
.install-global-actions.is-hidden {
display: none;
}
.install-error-details .button {
align-self: center;
margin-top: 0;
@@ -1354,6 +1424,7 @@ body {
align-items: center;
justify-content: center;
line-height: 0;
color: var(--fgcolor-neutral-weak);
}
.step-indicator svg {
@@ -1741,3 +1812,92 @@ body {
gap: var(--gap-s);
}
}
.migration-option {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--gap-l);
padding: var(--space-6);
background: var(--bgcolor-neutral-default);
border-radius: var(--border-radius-m);
outline: var(--border-width-s) solid var(--border-neutral);
outline-offset: calc(var(--border-width-s) * -1);
cursor: pointer;
transition: outline-color 0.15s ease-in-out;
}
.migration-option:hover {
outline-color: var(--border-neutral-stronger);
}
.migration-option-content {
display: flex;
flex-direction: column;
gap: 2px;
}
.migration-switch {
flex-shrink: 0;
}
.migration-switch-track {
position: relative;
display: block;
width: 32px;
height: 20px;
border-radius: 10px;
background: var(--bgcolor-neutral-invert-weaker);
transition: background 0.15s ease-in-out;
}
.migration-switch-thumb {
position: absolute;
top: 2px;
left: 2px;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--bgcolor-neutral-primary);
transition: transform 0.15s ease-in-out;
}
#run-migration:checked ~ .migration-switch-track {
background: var(--bgcolor-neutral-invert-weak);
}
#run-migration:checked ~ .migration-switch-track .migration-switch-thumb {
transform: translateX(12px);
}
#run-migration:focus-visible ~ .migration-switch-track {
box-shadow: 0 0 0 var(--border-width-l) var(--border-focus);
}
.migration-hint {
display: flex;
align-items: flex-start;
gap: var(--gap-s);
padding: 0 var(--space-2);
}
.migration-hint-icon {
flex-shrink: 0;
width: 16px;
height: 16px;
color: var(--fgcolor-neutral-tertiary);
margin-top: 1px;
}
.migration-hint-icon svg {
width: 100%;
height: 100%;
}
.migration-code {
padding: 1px 4px;
border-radius: var(--border-radius-xs, 4px);
background: var(--bgcolor-neutral-secondary);
font-family: monospace;
font-size: inherit;
}
@@ -1,13 +1,13 @@
<svg width="131" height="25" viewBox="0 0 131 25" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M38.2643 19.5087C40.4069 19.5087 41.49 18.3947 41.9609 17.6362H42.1728C42.267 18.4421 42.832 19.2006 43.9386 19.2006H46.0341V16.8304H45.4926C45.1159 16.8304 44.9275 16.617 44.9275 16.2852V6.78055H42.1492V8.29751H41.9373C41.3958 7.53903 40.2656 6.47242 38.1937 6.47242C34.8974 6.47242 32.4487 9.2219 32.4487 12.9906C32.4487 16.7593 34.9445 19.5087 38.2643 19.5087ZM38.7588 16.8067C36.8045 16.8067 35.2741 15.3608 35.2741 13.0143C35.2741 10.7151 36.7574 9.15079 38.7352 9.15079C40.6188 9.15079 42.1963 10.5492 42.1963 13.0143C42.1963 15.1238 40.8543 16.8067 38.7588 16.8067Z" fill="#19191D"/>
<path d="M47.6745 24.0166H50.4528V17.6362H50.6647C51.1826 18.3947 52.2893 19.5087 54.4789 19.5087C57.7752 19.5087 60.1768 16.7118 60.1768 12.9906C60.1768 9.2456 57.6104 6.47242 54.2906 6.47242C52.1715 6.47242 51.1356 7.63384 50.6411 8.2738H50.4292V6.78055H47.6745V24.0166ZM53.8903 16.8778C51.9832 16.8778 50.4057 15.4556 50.4057 12.9906C50.4057 10.8811 51.7477 9.10339 53.8432 9.10339C55.7974 9.10339 57.3279 10.644 57.3279 12.9906C57.3279 15.2897 55.8445 16.8778 53.8903 16.8778Z" fill="#19191D"/>
<path d="M61.6104 24.0166H64.3887V17.6362H64.6006C65.1186 18.3947 66.2252 19.5087 68.4149 19.5087C71.7112 19.5087 73.8839 16.7118 73.8839 12.9906C73.8839 9.2456 71.5464 6.47242 68.2265 6.47242C66.1075 6.47242 65.0715 7.63384 64.5771 8.2738H64.3652V6.78055H61.6104V24.0166ZM67.8263 16.8778C65.9191 16.8778 64.3416 15.4556 64.3416 12.9906C64.3416 10.8811 65.6837 9.10339 67.7792 9.10339C69.7334 9.10339 71.2638 10.644 71.2638 12.9906C71.2638 15.2897 69.7805 16.8778 67.8263 16.8778Z" fill="#19191D"/>
<path d="M77.5565 19.489H81.4885L83.7252 9.74733H83.8665L86.1033 19.489H90.0117L93.1415 7.06896H90.3414L88.1046 16.8343H87.8927L85.6559 7.06896H81.9594L79.6991 16.8343H79.4872L77.2739 7.06896H74.3073L77.5565 19.489Z" fill="#19191D"/>
<path d="M94.549 19.489H97.3273V13.3501C97.3273 11.0036 98.4104 9.55771 100.435 9.55771H101.66V6.76083H100.741C99.1638 6.76083 97.963 7.85114 97.4921 8.89405H97.3038V7.06896H94.549V19.489Z" fill="#19191D"/>
<path d="M115.447 19.489H117.613V17.0003H115.47C114.623 17.0003 114.27 16.621 114.27 15.744V9.53401H117.754V7.06896H114.27V3.58472H111.633V7.06896H109.325V9.53401H111.468V15.7677C111.468 18.3987 113.045 19.489 115.447 19.489Z" fill="#19191D"/>
<path d="M125.067 19.5087C127.633 19.5087 129.893 18.2288 130.694 15.6452L128.151 15.029C127.704 16.4037 126.409 17.1148 125.043 17.1148C123.018 17.1148 121.676 15.7875 121.653 13.7016H131V12.9195C131 9.2219 128.716 6.47242 124.949 6.47242C121.629 6.47242 118.78 9.10339 118.78 13.0143C118.78 16.8067 121.3 19.5087 125.067 19.5087ZM121.676 11.6632C121.841 10.17 123.183 8.91377 124.949 8.91377C126.644 8.91377 128.033 9.98037 128.175 11.6632H121.676Z" fill="#19191D"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M108.09 19.489H105.312V9.53401H103.145V7.06896H108.09V19.489Z" fill="#19191D"/>
<path d="M106.494 5.34533C107.507 5.34533 108.26 4.58686 108.26 3.59136C108.26 2.61956 107.507 1.86108 106.494 1.86108C105.482 1.86108 104.729 2.61956 104.729 3.59136C104.729 4.58686 105.482 5.34533 106.494 5.34533Z" fill="#19191D"/>
<path d="M38.2643 19.5087C40.4069 19.5087 41.49 18.3947 41.9609 17.6362H42.1728C42.267 18.4421 42.832 19.2006 43.9386 19.2006H46.0341V16.8304H45.4926C45.1159 16.8304 44.9275 16.617 44.9275 16.2852V6.78055H42.1492V8.29751H41.9373C41.3958 7.53903 40.2656 6.47242 38.1937 6.47242C34.8974 6.47242 32.4487 9.2219 32.4487 12.9906C32.4487 16.7593 34.9445 19.5087 38.2643 19.5087ZM38.7588 16.8067C36.8045 16.8067 35.2741 15.3608 35.2741 13.0143C35.2741 10.7151 36.7574 9.15079 38.7352 9.15079C40.6188 9.15079 42.1963 10.5492 42.1963 13.0143C42.1963 15.1238 40.8543 16.8067 38.7588 16.8067Z" fill="currentColor"/>
<path d="M47.6745 24.0166H50.4528V17.6362H50.6647C51.1826 18.3947 52.2893 19.5087 54.4789 19.5087C57.7752 19.5087 60.1768 16.7118 60.1768 12.9906C60.1768 9.2456 57.6104 6.47242 54.2906 6.47242C52.1715 6.47242 51.1356 7.63384 50.6411 8.2738H50.4292V6.78055H47.6745V24.0166ZM53.8903 16.8778C51.9832 16.8778 50.4057 15.4556 50.4057 12.9906C50.4057 10.8811 51.7477 9.10339 53.8432 9.10339C55.7974 9.10339 57.3279 10.644 57.3279 12.9906C57.3279 15.2897 55.8445 16.8778 53.8903 16.8778Z" fill="currentColor"/>
<path d="M61.6104 24.0166H64.3887V17.6362H64.6006C65.1186 18.3947 66.2252 19.5087 68.4149 19.5087C71.7112 19.5087 73.8839 16.7118 73.8839 12.9906C73.8839 9.2456 71.5464 6.47242 68.2265 6.47242C66.1075 6.47242 65.0715 7.63384 64.5771 8.2738H64.3652V6.78055H61.6104V24.0166ZM67.8263 16.8778C65.9191 16.8778 64.3416 15.4556 64.3416 12.9906C64.3416 10.8811 65.6837 9.10339 67.7792 9.10339C69.7334 9.10339 71.2638 10.644 71.2638 12.9906C71.2638 15.2897 69.7805 16.8778 67.8263 16.8778Z" fill="currentColor"/>
<path d="M77.5565 19.489H81.4885L83.7252 9.74733H83.8665L86.1033 19.489H90.0117L93.1415 7.06896H90.3414L88.1046 16.8343H87.8927L85.6559 7.06896H81.9594L79.6991 16.8343H79.4872L77.2739 7.06896H74.3073L77.5565 19.489Z" fill="currentColor"/>
<path d="M94.549 19.489H97.3273V13.3501C97.3273 11.0036 98.4104 9.55771 100.435 9.55771H101.66V6.76083H100.741C99.1638 6.76083 97.963 7.85114 97.4921 8.89405H97.3038V7.06896H94.549V19.489Z" fill="currentColor"/>
<path d="M115.447 19.489H117.613V17.0003H115.47C114.623 17.0003 114.27 16.621 114.27 15.744V9.53401H117.754V7.06896H114.27V3.58472H111.633V7.06896H109.325V9.53401H111.468V15.7677C111.468 18.3987 113.045 19.489 115.447 19.489Z" fill="currentColor"/>
<path d="M125.067 19.5087C127.633 19.5087 129.893 18.2288 130.694 15.6452L128.151 15.029C127.704 16.4037 126.409 17.1148 125.043 17.1148C123.018 17.1148 121.676 15.7875 121.653 13.7016H131V12.9195C131 9.2219 128.716 6.47242 124.949 6.47242C121.629 6.47242 118.78 9.10339 118.78 13.0143C118.78 16.8067 121.3 19.5087 125.067 19.5087ZM121.676 11.6632C121.841 10.17 123.183 8.91377 124.949 8.91377C126.644 8.91377 128.033 9.98037 128.175 11.6632H121.676Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M108.09 19.489H105.312V9.53401H103.145V7.06896H108.09V19.489Z" fill="currentColor"/>
<path d="M106.494 5.34533C107.507 5.34533 108.26 4.58686 108.26 3.59136C108.26 2.61956 107.507 1.86108 106.494 1.86108C105.482 1.86108 104.729 2.61956 104.729 3.59136C104.729 4.58686 105.482 5.34533 106.494 5.34533Z" fill="currentColor"/>
<path d="M24.2577 16.4436V21.9248H10.6705C6.71194 21.9248 3.25559 19.7204 1.40636 16.4436C1.13754 15.9672 0.90225 15.4674 0.704883 14.9487C0.31744 13.9322 0.0738912 12.8415 0 11.7034V10.2214C0.0160422 9.96781 0.0413207 9.71617 0.0743773 9.46752C0.141949 8.95727 0.244035 8.45799 0.378206 7.97265C1.64748 3.37143 5.77469 0 10.6705 0C15.5662 0 19.693 3.37143 20.9622 7.97265H15.1526C14.1988 6.47279 12.5479 5.4812 10.6705 5.4812C8.79305 5.4812 7.14216 6.47279 6.18839 7.97265C5.89768 8.42859 5.67212 8.93136 5.52434 9.46752C5.39308 9.94289 5.32308 10.4442 5.32308 10.9624C5.32308 12.5335 5.96768 13.9497 7.00119 14.9487C7.95886 15.876 9.25 16.4436 10.6705 16.4436H24.2577Z" fill="#FD366E"/>
<path d="M24.2578 9.46753V14.9487H14.3398C15.3733 13.9497 16.018 12.5335 16.018 10.9624C16.018 10.4442 15.9479 9.9429 15.8167 9.46753H24.2578Z" fill="#FD366E"/>
</svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M5 8C5 6.34315 6.34315 5 8 5C9.65685 5 11 6.34315 11 8C11 9.65685 9.65685 11 8 11C6.34315 11 5 9.65685 5 8Z" fill="#D8D8DB"/>
<path d="M5 8C5 6.34315 6.34315 5 8 5C9.65685 5 11 6.34315 11 8C11 9.65685 9.65685 11 8 11C6.34315 11 5 9.65685 5 8Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 261 B

After

Width:  |  Height:  |  Size: 266 B

+13 -6
View File
@@ -12,7 +12,7 @@
const { validateInstallRequest } = window.InstallerStepsProgress || {};
const isUpgrade = document.body?.dataset.upgrade === 'true';
const stepFlow = isUpgrade ? [1, 4, 5] : [1, 2, 3, 4, 5];
const stepFlow = isUpgrade ? [1, 6, 4, 5] : [1, 2, 3, 4, 5];
const cardSteps = stepFlow.filter((step) => step !== 5);
const normalizeStep = (step) => {
@@ -53,7 +53,7 @@
let pendingStep = null;
let pendingPushState = false;
const clampStep = (step) => Math.max(1, Math.min(5, step));
const clampStep = (step) => Math.max(1, Math.min(6, step));
const isInstallLocked = () => Boolean(window.InstallerSteps?.isInstallLocked?.());
const scrollToFirstError = (panel) => {
@@ -399,11 +399,18 @@
}
}
}
if (action === 'next' && String(target) === '5' && typeof validateInstallRequest === 'function') {
const isValid = await validateInstallRequest();
if (!isValid) {
return;
if (action === 'next' && String(target) === '5') {
if (typeof validateInstallRequest === 'function') {
const isValid = await validateInstallRequest();
if (!isValid) {
return;
}
}
// Clear stale install data from previous runs so initStep5
// starts a fresh install instead of trying to resume.
const { clearInstallLock, clearInstallId } = window.InstallerStepsState || {};
clearInstallLock?.();
clearInstallId?.();
}
if (isInstallLocked() && Number(target) !== 5) {
requestStep(5, true);
@@ -2,13 +2,21 @@
const getBodyDataset = () => document.body?.dataset ?? {};
const isUpgradeMode = () => getBodyDataset().upgrade === 'true';
const getLockedDatabase = () => getBodyDataset().lockedDatabase || '';
const getEnabledDatabases = () => {
const raw = getBodyDataset().enabledDatabases;
if (!raw) return ['mongodb', 'mariadb', 'postgresql'];
try { return JSON.parse(raw); } catch (e) { return ['mongodb', 'mariadb', 'postgresql']; }
};
const STEP_IDS = Object.freeze({
CONFIG_FILES: 'config-files',
DOCKER_COMPOSE: 'docker-compose',
ENV_VARS: 'env-vars',
DOCKER_CONTAINERS: 'docker-containers',
ACCOUNT_SETUP: 'account-setup'
ACCOUNT_SETUP: 'account-setup',
MIGRATION: 'migration',
SSL_CERTIFICATE: 'ssl-certificate',
REDIRECT: 'redirect'
});
const STATUS = Object.freeze({
@@ -45,6 +53,11 @@
id: STEP_IDS.DOCKER_CONTAINERS,
inProgress: 'Restarting Docker containers...',
done: 'Docker containers restarted'
},
{
id: STEP_IDS.MIGRATION,
inProgress: 'Running database migration...',
done: 'Database migration completed'
}
] : [
{
@@ -70,7 +83,7 @@
{
id: STEP_IDS.ACCOUNT_SETUP,
inProgress: 'Creating Appwrite account...',
done: 'Appwrite account created (redirecting...)'
done: 'Appwrite account created'
}
]);
@@ -88,13 +101,14 @@
const clampStep = (step) => {
const numeric = Number(step);
if (Number.isNaN(numeric)) return 1;
return Math.max(1, Math.min(5, numeric));
return Math.max(1, Math.min(6, numeric));
};
window.InstallerStepsContext = Object.freeze({
getBodyDataset,
isUpgradeMode,
getLockedDatabase,
getEnabledDatabases,
STEP_IDS,
STATUS,
SSE_EVENTS,
@@ -21,12 +21,13 @@
storeInstallId,
clearInstallId
} = window.InstallerStepsState || {};
const { extractHostname, isLocalHost } = window.InstallerStepsValidation || {};
const { extractHostname, isLocalHost, isIPAddress } = window.InstallerStepsValidation || {};
const { generateSecretKey } = window.InstallerStepsUI || {};
const { showToast } = window.InstallerToast || {};
let activeInstall = null;
let unloadGuard = null;
let sseSessionDetails = null;
const csrfToken = document.querySelector('meta[name="appwrite-installer-csrf"]')?.getAttribute('content') || '';
const withCsrfHeader = (headers = {}) => {
@@ -110,10 +111,10 @@
return normalized.summary || 'Installation failed.';
}
if (status === STATUS.COMPLETED) return step.done;
return step.inProgress;
return message || step.inProgress;
};
const updateInstallRow = (row, step, status, message) => {
const updateInstallRow = (row, step, status, message, details) => {
if (!row || !step) return;
row.dataset.status = status;
row.dataset.step = step.id;
@@ -137,6 +138,15 @@
}
}
const counter = row.querySelector('[data-install-counter]');
if (counter) {
const started = details?.containerStarted ?? 0;
const total = details?.containerTotal;
counter.textContent = (status === STATUS.IN_PROGRESS && total > 0 && started < total)
? `${started}/${total}`
: '';
}
// Show/hide "Navigate to Console" button for account setup errors
const consoleBtn = row.querySelector('[data-install-console]');
if (consoleBtn) {
@@ -250,7 +260,7 @@
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('');
};
const buildRedirectUrl = () => {
const buildRedirectUrl = (protocol) => {
const dataset = getBodyDataset?.() ?? {};
const rawDomain = (formState?.appDomain || dataset.defaultAppDomain || '').trim();
if (!rawDomain) return '';
@@ -265,22 +275,53 @@
} else if (normalizedHost === 'traefik') {
host = rawDomain.replace(hostForProtocol, 'localhost');
}
let protocol = 'http';
let port = httpPort;
if (httpsPort && httpsPort !== '0' && !isLocalHost?.(normalizedHost)) {
protocol = 'https';
port = httpsPort;
}
if (!hasPort && port && ((protocol === 'http' && port !== '80') || (protocol === 'https' && port !== '443'))) {
const port = protocol === 'https' ? httpsPort : httpPort;
const defaultPort = protocol === 'https' ? '443' : '80';
if (!hasPort && port && port !== defaultPort) {
host = `${host}:${port}`;
}
return `${protocol}://${host}`;
};
const redirectToApp = () => {
const url = buildRedirectUrl();
const normalizeHostname = (rawDomain) => {
const hostname = extractHostname?.(rawDomain)?.toLowerCase?.() ?? '';
if (hostname === '0.0.0.0' || hostname === 'traefik') return 'localhost';
return hostname;
};
const canUseHttps = () => {
const dataset = getBodyDataset?.() ?? {};
const rawDomain = (formState?.appDomain || dataset.defaultAppDomain || '').trim();
const httpsPort = (formState?.httpsPort || dataset.defaultHttpsPort || '').trim();
if (!httpsPort || httpsPort === '0') return false;
const hostname = normalizeHostname(rawDomain);
return !isLocalHost?.(hostname) && !isIPAddress?.(hostname);
};
const pollCertificate = async (domain, port, maxAttempts, intervalMs) => {
for (let i = 0; i < maxAttempts; i++) {
try {
const response = await fetch(
`/install/certificate?domain=${encodeURIComponent(domain)}&port=${encodeURIComponent(port)}`,
{ cache: 'no-store' }
);
if (response.ok) {
const data = await response.json();
if (data.ready) return true;
}
} catch {
// Installer server may have shut down
}
if (i < maxAttempts - 1) {
await new Promise((resolve) => setTimeout(resolve, intervalMs));
}
}
return false;
};
const redirectToApp = (protocol) => {
const url = buildRedirectUrl(protocol);
if (!url) return;
// Fire-and-forget: tell the installer server it can shut down
fetch('/install/shutdown', { method: 'POST', headers: withCsrfHeader() }).catch(() => {});
window.location.href = url;
};
@@ -317,7 +358,7 @@
const normalizedDomain = (formState?.appDomain || '').trim() || 'localhost';
const normalizedHttpPort = (formState?.httpPort || '').trim() || '80';
const normalizedHttpsPort = (formState?.httpsPort || '').trim() || '443';
const normalizedEmail = (formState?.emailCertificates || '').trim();
const normalizedEmail = (formState?.emailCertificates || '').trim() || (formState?.accountEmail || '').trim();
const normalizedAssistantKey = (formState?.assistantOpenAIKey || '').trim();
const normalizedAccountEmail = (formState?.accountEmail || '').trim();
const normalizedAccountPassword = (formState?.accountPassword || '').trim();
@@ -332,7 +373,8 @@
opensslKey: (formState?.opensslKey || '').trim(),
assistantOpenAIKey: normalizedAssistantKey,
accountEmail: normalizedAccountEmail,
accountPassword: normalizedAccountPassword
accountPassword: normalizedAccountPassword,
migrate: formState?.migrate ?? false
};
};
@@ -405,6 +447,7 @@
const initStep5 = (root) => {
if (!root) return;
let resolvedProtocol = 'http';
if (activeInstall?.controller) {
activeInstall.controller.abort();
@@ -496,7 +539,7 @@
if (!state) return;
const row = ensureRow(step);
if (row) {
updateInstallRow(row, step, state.status || STATUS.IN_PROGRESS, state.message);
updateInstallRow(row, step, state.status || STATUS.IN_PROGRESS, state.message, state.details);
if (state.status === STATUS?.ERROR) {
updateInstallErrorDetails(row, {
message: state.message,
@@ -524,6 +567,9 @@
inProgress: payload.message || payload.step,
done: payload.message || payload.step
};
if (step.id === STEP_IDS.ACCOUNT_SETUP && payload.details?.sessionSecret) {
sseSessionDetails = payload.details;
}
progressState.set(step.id, {
status: payload.status || STATUS.IN_PROGRESS,
message: payload.message,
@@ -543,6 +589,9 @@
}
}
}
if (payload.status === STATUS.ERROR) {
showGlobalActions();
}
scheduleFallback();
};
@@ -580,6 +629,7 @@
const applySnapshot = (snapshot) => {
if (!snapshot || !snapshot.steps) return;
let hasErrors = false;
INSTALLATION_STEPS.forEach((step) => {
const detail = snapshot.steps[step.id];
if (!detail) return;
@@ -588,8 +638,14 @@
message: detail.message,
details: snapshot.details?.[step.id]
});
if (detail.status === STATUS.ERROR) {
hasErrors = true;
}
});
renderProgress();
if (hasErrors) {
showGlobalActions();
}
};
const checkAllCompleted = () => {
@@ -599,11 +655,9 @@
});
if (!allDone) return;
const accountState = progressState.get(STEP_IDS.ACCOUNT_SETUP);
const sessionDetails = accountState?.details;
const sessionDetails = sseSessionDetails || accountState?.details;
finalizeInstall();
notifyInstallComplete(activeInstall?.installId, sessionDetails).finally(() => {
setTimeout(() => redirectToApp(), TIMINGS?.redirectDelay ?? 0);
});
startSslCheck(sessionDetails);
};
const startPolling = () => {
@@ -640,6 +694,78 @@
}
stopSyncedSpinnerRotation();
setUnloadGuard(false);
clearInstallLock?.();
};
const SSL_STEP = {
id: STEP_IDS.SSL_CERTIFICATE,
inProgress: 'Generating SSL certificate...',
done: 'SSL certificate verified'
};
const REDIRECT_STEP = {
id: STEP_IDS.REDIRECT,
inProgress: 'Redirecting to console...',
done: 'Redirecting to console...'
};
const showRedirectStep = (sessionDetails, protocol) => {
animatePanelHeight(() => {
progressState.set(REDIRECT_STEP.id, {
status: STATUS.IN_PROGRESS,
message: REDIRECT_STEP.inProgress
});
const row = ensureRow(REDIRECT_STEP);
if (row) {
updateInstallRow(row, REDIRECT_STEP, STATUS.IN_PROGRESS, REDIRECT_STEP.inProgress);
}
});
startSyncedSpinnerRotation(list);
const completeId = activeInstall?.installId || getStoredInstallId?.();
notifyInstallComplete(completeId, sessionDetails).finally(() => {
setTimeout(() => redirectToApp(protocol), TIMINGS?.redirectDelay ?? 0);
});
};
const startSslCheck = (sessionDetails) => {
if (!canUseHttps()) {
showRedirectStep(sessionDetails, 'http');
return;
}
animatePanelHeight(() => {
progressState.set(SSL_STEP.id, {
status: STATUS.IN_PROGRESS,
message: SSL_STEP.inProgress
});
const row = ensureRow(SSL_STEP);
if (row) {
updateInstallRow(row, SSL_STEP, STATUS.IN_PROGRESS, SSL_STEP.inProgress);
}
});
startSyncedSpinnerRotation(list);
const dataset = getBodyDataset?.() ?? {};
const rawDomain = (formState?.appDomain || dataset.defaultAppDomain || '').trim();
const httpsPort = (formState?.httpsPort || dataset.defaultHttpsPort || '443').trim();
const domain = normalizeHostname(rawDomain);
pollCertificate(domain, httpsPort, 15, 2000).then((ready) => {
stopSyncedSpinnerRotation();
const certMessage = ready ? SSL_STEP.done : 'Certificate not ready, continuing over HTTP';
animatePanelHeight(() => {
progressState.set(SSL_STEP.id, {
status: STATUS.COMPLETED,
message: certMessage
});
const row = ensureRow(SSL_STEP);
if (row) {
updateInstallRow(row, SSL_STEP, STATUS.COMPLETED, certMessage);
}
});
resolvedProtocol = ready ? 'https' : 'http';
showRedirectStep(sessionDetails, resolvedProtocol);
});
};
const startInstallStream = async (installId, options = {}) => {
@@ -740,11 +866,9 @@
}
const accountState = progressState.get(STEP_IDS.ACCOUNT_SETUP);
const sessionDetails = accountState?.details;
const sessionDetails = sseSessionDetails || accountState?.details;
finalizeInstall();
notifyInstallComplete(activeInstall?.installId, sessionDetails).finally(() => {
setTimeout(() => redirectToApp(), TIMINGS?.redirectDelay ?? 0);
});
startSslCheck(sessionDetails);
return;
}
if (event === SSE_EVENTS.ERROR) {
@@ -788,9 +912,29 @@
}
};
const isSnapshotTerminal = (snapshot) => {
if (!snapshot?.steps) return 'empty';
const stepEntries = Object.values(snapshot.steps);
if (stepEntries.length === 0) return 'empty';
const hasError = stepEntries.some((s) => s.status === STATUS.ERROR);
if (hasError) return 'error';
const allCompleted = INSTALLATION_STEPS.every((step) => {
const detail = snapshot.steps[step.id];
return detail && detail.status === STATUS.COMPLETED;
});
if (allCompleted) return 'completed';
return false;
};
const resumeInstall = async (installId) => {
const snapshot = await fetchInstallStatus(installId);
if (!snapshot) return false;
const terminal = isSnapshotTerminal(snapshot);
if (!snapshot || terminal) {
if (terminal === 'completed') {
return 'completed';
}
return false;
}
activeInstall = {
installId,
controller: new AbortController(),
@@ -853,7 +997,7 @@
const retryButton = event.target.closest('[data-install-retry]');
if (consoleButton) {
redirectToApp();
redirectToApp(resolvedProtocol);
return;
}
@@ -864,6 +1008,60 @@
}
});
const globalActions = root.querySelector('[data-install-global-actions]');
const showGlobalActions = () => {
if (globalActions) {
globalActions.classList.remove('is-hidden');
}
};
const performReset = async (hard) => {
const installId = activeInstall?.installId || getInstallLock?.()?.installId || getStoredInstallId?.();
try {
const res = await fetch('/install/reset', {
method: 'POST',
headers: withCsrfHeader({ 'Content-Type': 'application/json' }),
body: JSON.stringify({ installId: installId || '', hard })
});
if (hard && !res.ok) {
const data = await res.json().catch(() => ({}));
showToast?.({
status: 'error',
title: 'Reset failed',
description: data?.message || 'Could not stop containers. Try running "docker compose down -v" manually.',
dismissible: true
});
return;
}
} catch (e) {
console.error('Reset request failed:', e);
}
clearInstallLock?.();
clearInstallId?.();
cleanupInstallFlow();
window.location.href = '/?step=1';
};
const startOverButton = root.querySelector('[data-install-start-over]');
if (startOverButton) {
startOverButton.addEventListener('click', () => performReset(false));
}
const hardResetButton = root.querySelector('[data-install-hard-reset]');
if (hardResetButton) {
hardResetButton.addEventListener('click', () => {
const confirmed = window.confirm(
'This will stop all containers, remove all volumes (including database data, uploads, and certificates), and delete configuration files.\n\nThis action cannot be undone. Continue?'
);
if (confirmed) {
performReset(true);
}
});
}
// When the user switches back to this tab, check if installation
// finished while the tab was in the background.
document.addEventListener('visibilitychange', () => {
@@ -872,22 +1070,45 @@
}
});
const lock = getInstallLock?.();
const existingInstallId = lock?.installId || getStoredInstallId?.();
if (existingInstallId) {
resumeInstall(existingInstallId).then((resumed) => {
if (!resumed) {
clearInstallId?.();
clearInstallLock?.();
const newInstallId = generateInstallId();
storeInstallId?.(newInstallId);
startInstallStream(newInstallId);
}
});
} else {
const startFreshInstall = () => {
clearInstallId?.();
clearInstallLock?.();
const newInstallId = generateInstallId();
storeInstallId?.(newInstallId);
startInstallStream(newInstallId);
};
const recoverToLastStep = () => {
clearInstallId?.();
clearInstallLock?.();
const url = new URL(window.location.href);
const lastStep = url.searchParams.get('step');
// Stay on the current URL so the user keeps their place;
// only navigate away if we're already on step 5 (the
// progress screen) since there's nothing to show.
if (!lastStep || String(lastStep) === '5') {
window.location.href = '/?step=1';
}
};
const lock = getInstallLock?.();
const existingInstallId = lock?.installId || getStoredInstallId?.();
if (existingInstallId) {
resumeInstall(existingInstallId).then((result) => {
if (result === 'completed') {
// Install already finished — redirect to console
// instead of bouncing back to step 1.
stopSyncedSpinnerRotation();
setUnloadGuard(false);
clearInstallLock?.();
clearInstallId?.();
startSslCheck(null);
} else if (!result) {
recoverToLastStep();
}
});
} else {
startFreshInstall();
}
};
+39 -11
View File
@@ -7,6 +7,8 @@
const INSTALL_LOCK_KEY = 'appwrite-install-lock';
const INSTALL_ID_KEY = 'appwrite-install-id';
const INSTALL_LOCK_LOCAL_KEY = 'appwrite-install-lock-backup';
const INSTALL_ID_LOCAL_KEY = 'appwrite-install-id-backup';
const formState = {
appDomain: null,
@@ -55,13 +57,24 @@
const getInstallLock = () => {
try {
const raw = sessionStorage.getItem(INSTALL_LOCK_KEY);
if (!raw) return null;
const parsed = JSON.parse(raw);
if (!parsed || typeof parsed !== 'object') return null;
return parsed;
} catch (error) {
return null;
}
if (raw) {
const parsed = JSON.parse(raw);
if (parsed && typeof parsed === 'object') return parsed;
}
} catch (error) {}
try {
const raw = localStorage.getItem(INSTALL_LOCK_LOCAL_KEY);
if (raw) {
const parsed = JSON.parse(raw);
if (parsed && typeof parsed === 'object') {
sessionStorage.setItem(INSTALL_LOCK_KEY, raw);
return parsed;
}
}
} catch (error) {}
return null;
};
const setInstallLock = (installId, payload) => {
@@ -79,6 +92,9 @@
try {
sessionStorage.setItem(INSTALL_LOCK_KEY, JSON.stringify(lock));
} catch (error) {}
try {
localStorage.setItem(INSTALL_LOCK_LOCAL_KEY, JSON.stringify(lock));
} catch (error) {}
if (document.body) {
document.body.dataset.installLocked = 'true';
}
@@ -89,6 +105,9 @@
try {
sessionStorage.removeItem(INSTALL_LOCK_KEY);
} catch (error) {}
try {
localStorage.removeItem(INSTALL_LOCK_LOCAL_KEY);
} catch (error) {}
if (document.body) {
delete document.body.dataset.installLocked;
}
@@ -121,22 +140,31 @@
const getStoredInstallId = () => {
try {
return sessionStorage.getItem(INSTALL_ID_KEY);
} catch (error) {
return null;
}
const val = sessionStorage.getItem(INSTALL_ID_KEY);
if (val) return val;
} catch (error) {}
try {
return localStorage.getItem(INSTALL_ID_LOCAL_KEY);
} catch (error) {}
return null;
};
const storeInstallId = (installId) => {
try {
sessionStorage.setItem(INSTALL_ID_KEY, installId);
} catch (error) {}
try {
localStorage.setItem(INSTALL_ID_LOCAL_KEY, installId);
} catch (error) {}
};
const clearInstallId = () => {
try {
sessionStorage.removeItem(INSTALL_ID_KEY);
} catch (error) {}
try {
localStorage.removeItem(INSTALL_ID_LOCAL_KEY);
} catch (error) {}
};
window.InstallerStepsState = {
@@ -240,6 +240,9 @@
if (key === 'database') {
value = toDatabaseLabel(formState?.database);
}
if (key === 'emailCertificates' && !value) {
value = formState?.accountEmail;
}
if (value) {
node.textContent = value;
}
@@ -106,12 +106,18 @@
return LOCAL_HOSTS.has(normalized);
};
const isIPAddress = (host) => {
if (!host) return false;
return isValidIPv4(host) || isValidIPv6(host);
};
window.InstallerStepsValidation = {
isValidEmail,
isValidPort,
isValidPassword,
isValidHostnameInput,
extractHostname,
isLocalHost
isLocalHost,
isIPAddress
};
})();
+43 -5
View File
@@ -9,7 +9,8 @@
const {
INSTALLATION_STEPS,
clampStep,
isUpgradeMode
isUpgradeMode,
getEnabledDatabases
} = Context;
const {
@@ -79,6 +80,19 @@
}
};
const applyEnabledDatabases = (root) => {
const enabled = getEnabledDatabases?.() || [];
const radios = root.querySelectorAll('input[name="database"]');
radios.forEach((radio) => {
if (!enabled.includes(radio.value)) {
const card = radio.closest('.selector-card');
if (card) {
card.remove();
}
}
});
};
const bindDatabaseSelection = (root) => {
const radios = root.querySelectorAll('input[name="database"]');
radios.forEach((radio) => {
@@ -139,6 +153,8 @@
return;
}
applyEnabledDatabases(root);
const lockedDatabase = getLockedDatabase?.() || '';
if (lockedDatabase) {
lockDatabaseSelection(root, lockedDatabase);
@@ -313,6 +329,30 @@
}
};
const initStep6 = (root) => {
if (!root) return;
syncInstallLockFlag?.();
applyLockPayload?.();
applyBodyDefaults?.();
const checkbox = root.querySelector('#run-migration');
if (checkbox) {
if (formState.migrate !== undefined) {
checkbox.checked = formState.migrate;
} else {
formState.migrate = checkbox.checked;
}
checkbox.addEventListener('change', () => {
formState.migrate = checkbox.checked;
dispatchStateChange?.('migrate');
});
}
if (isInstallLocked?.()) {
disableControls?.(root);
}
};
const initStep = (step, container) => {
if (!container) return;
const root = container.querySelector('.step-layout') || container;
@@ -330,6 +370,7 @@
if (normalized === 3) initStep3(root);
if (normalized === 4) initStep4(root);
if (normalized === 5) Progress.initStep5?.(root);
if (normalized === 6) initStep6(root);
};
window.InstallerSteps = {
@@ -374,10 +415,7 @@
if (!parsePort(httpPort, 'HTTP')) valid = false;
if (!parsePort(httpsPort, 'HTTPS')) valid = false;
if (!sslEmail || !sslEmail.value.trim()) {
setFieldError?.(sslEmail, 'Please enter an email address for SSL certificates');
valid = false;
} else if (!isValidEmail?.(sslEmail.value.trim())) {
if (sslEmail && sslEmail.value.trim() && !isValidEmail?.(sslEmail.value.trim())) {
setFieldError?.(sslEmail, 'Please enter a valid email address');
valid = false;
}
@@ -7,8 +7,12 @@ $defaultHttpsPort = $defaultHttpsPort ?? '443';
$defaultEmailCertificates = $defaultEmailCertificates ?? '';
$defaultAssistantOpenAIKey = $defaultAssistantOpenAIKey ?? '';
$defaultDatabase = $defaultDatabase ?? 'mongodb';
$enabledDatabases = $enabledDatabases ?? ['mongodb', 'mariadb', 'postgresql'];
$selectedDatabase = $lockedDatabase ?: $defaultDatabase;
$isDatabaseLocked = !empty($lockedDatabase);
$mongoEnabled = in_array('mongodb', $enabledDatabases, true);
$mariaEnabled = in_array('mariadb', $enabledDatabases, true);
$postgresEnabled = in_array('postgresql', $enabledDatabases, true);
$mongoDisabled = $isDatabaseLocked && $selectedDatabase !== 'mongodb';
$mariaDisabled = $isDatabaseLocked && $selectedDatabase !== 'mariadb';
$postgresDisabled = $isDatabaseLocked && $selectedDatabase !== 'postgresql';
@@ -43,6 +47,7 @@ $assistantOpenAIKeyValue = htmlspecialchars((string) $defaultAssistantOpenAIKey,
<div class="input-group stack-xs">
<label class="label-text typography-text-m-500 text-neutral-secondary">Database</label>
<div class="selector-group<?php echo $isDatabaseLocked ? ' is-locked' : ''; ?>">
<?php if ($mongoEnabled) { ?>
<label class="selector-card <?php echo ($selectedDatabase === 'mongodb') ? 'selected' : ''; ?><?php echo $mongoDisabled ? ' is-disabled has-tooltip' : ''; ?>"<?php echo $mongoDisabled ? ' aria-disabled="true"' : ''; ?>>
<input type="radio" name="database" value="mongodb" <?php echo ($selectedDatabase === 'mongodb') ? 'checked' : ''; ?> class="sr-only" <?php echo $mongoDisabled ? 'disabled' : ''; ?>>
<div class="selector-content">
@@ -54,7 +59,9 @@ $assistantOpenAIKeyValue = htmlspecialchars((string) $defaultAssistantOpenAIKey,
<span class="tooltip tooltip-db-locked typography-text-m-400 text-on-invert" role="tooltip" data-tooltip-portal="true">Database cannot be changed after initial setup.</span>
<?php } ?>
</label>
<?php } ?>
<?php if ($mariaEnabled) { ?>
<label class="selector-card <?php echo ($selectedDatabase === 'mariadb') ? 'selected' : ''; ?><?php echo $mariaDisabled ? ' is-disabled has-tooltip' : ''; ?>"<?php echo $mariaDisabled ? ' aria-disabled="true"' : ''; ?>>
<input type="radio" name="database" value="mariadb" <?php echo ($selectedDatabase === 'mariadb') ? 'checked' : ''; ?> class="sr-only" <?php echo $mariaDisabled ? 'disabled' : ''; ?>>
<div class="selector-content">
@@ -66,7 +73,9 @@ $assistantOpenAIKeyValue = htmlspecialchars((string) $defaultAssistantOpenAIKey,
<span class="tooltip tooltip-db-locked typography-text-m-400 text-on-invert" role="tooltip" data-tooltip-portal="true">Database cannot be changed after initial setup.</span>
<?php } ?>
</label>
<?php } ?>
<?php if ($postgresEnabled) { ?>
<label class="selector-card <?php echo ($selectedDatabase === 'postgresql') ? 'selected' : ''; ?><?php echo $postgresDisabled ? ' is-disabled has-tooltip' : ''; ?>"<?php echo $postgresDisabled ? ' aria-disabled="true"' : ''; ?>>
<input type="radio" name="database" value="postgresql" <?php echo ($selectedDatabase === 'postgresql') ? 'checked' : ''; ?> class="sr-only" <?php echo $postgresDisabled ? 'disabled' : ''; ?>>
<div class="selector-content">
@@ -78,6 +87,7 @@ $assistantOpenAIKeyValue = htmlspecialchars((string) $defaultAssistantOpenAIKey,
<span class="tooltip tooltip-db-locked typography-text-m-400 text-on-invert" role="tooltip" data-tooltip-portal="true">Database cannot be changed after initial setup.</span>
<?php } ?>
</label>
<?php } ?>
</div>
</div>
@@ -62,12 +62,14 @@ $badgeClass = $defaultSecretKey !== '' ? 'badge-success' : 'badge-warning';
<span class="badge badge-neutral typography-text-xs-400" data-review-assistant-badge>Disabled</span>
<div class="review-label typography-text-xs-400 text-neutral-tertiary">Appwrite Assistant</div>
</div>
<?php if (!$isUpgrade) { ?>
<div class="review-row">
<span class="badge <?php echo $badgeClass; ?> typography-text-xs-400" data-review-badge>
<?php echo htmlspecialchars((string) $badgeLabel, ENT_QUOTES, 'UTF-8'); ?>
</span>
<div class="review-label typography-text-xs-400 text-neutral-tertiary">Secret API key</div>
</div>
<?php } ?>
</div>
</div>
</div>
@@ -6,7 +6,7 @@ $isUpgrade = $isUpgrade ?? false;
<div class="install-panel">
<div class="install-header">
<div class="typography-text-m-400 text-neutral-primary">
<?php echo $isUpgrade ? 'Updating your app…' : 'Installing your app…'; ?>
<?php echo $isUpgrade ? 'Updating Appwrite…' : 'Installing Appwrite…'; ?>
</div>
</div>
<div class="install-list" data-install-list></div>
@@ -30,6 +30,7 @@ $isUpgrade = $isUpgrade ?? false;
</span>
<span class="install-text typography-text-m-400 text-neutral-primary" data-install-text></span>
</div>
<span class="install-counter typography-text-xs-400" data-install-counter></span>
<button type="button" class="install-row-toggle" aria-expanded="false" data-install-toggle>
<?php include __DIR__ . '/../../icons/chevron-down.svg'; ?>
</button>
@@ -50,4 +51,13 @@ $isUpgrade = $isUpgrade ?? false;
</div>
</div>
</template>
<div class="install-global-actions is-hidden" data-install-global-actions>
<button type="button" class="button secondary" data-install-start-over>
<span class="button-text typography-text-m-500">Start Over</span>
</button>
<button type="button" class="button secondary" data-install-hard-reset>
<span class="button-text typography-text-m-500">Reset Everything</span>
</button>
</div>
</div>
@@ -0,0 +1,37 @@
<?php
$isUpgrade = $isUpgrade ?? false;
?>
<div class="step-layout" data-step="6">
<div class="stack-xl">
<div class="stack-xxxs">
<h1 class="typography-title-s text-neutral-primary">Database migration</h1>
<p class="typography-text-m-400 text-neutral-secondary">
Run database migration after the update to apply schema changes.
</p>
</div>
<div class="stack-xl">
<label class="migration-option" for="run-migration">
<span class="migration-option-content">
<span class="typography-text-m-500 text-neutral-primary">Run migration automatically</span>
<span class="typography-text-xs-400 text-neutral-tertiary">Recommended when upgrading to a new version</span>
</span>
<span class="migration-switch">
<input type="checkbox" id="run-migration" name="migrate" class="sr-only" checked>
<span class="migration-switch-track" aria-hidden="true">
<span class="migration-switch-thumb"></span>
</span>
</span>
</label>
<div class="migration-hint">
<span class="migration-hint-icon">
<?php include __DIR__ . '/../../icons/info.svg'; ?>
</span>
<span class="typography-text-xs-400 text-neutral-tertiary">
To run manually later: <code class="migration-code">docker compose exec appwrite migrate</code>
</span>
</div>
</div>
</div>
</div>
+42 -477
View File
@@ -1,511 +1,69 @@
<?php
require_once __DIR__ . '/init.php';
$registerWorkerMessageResources = require __DIR__ . '/init/worker/message.php';
use Appwrite\Certificates\LetsEncrypt;
use Appwrite\Event\Audit;
use Appwrite\Event\Build;
use Appwrite\Event\Certificate;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\Migration;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Event\Realtime;
use Appwrite\Event\Screenshot;
use Appwrite\Event\Webhook;
use Appwrite\Platform\Appwrite;
use Appwrite\Usage\Context;
use Appwrite\Utopia\Database\Documents\User;
use Executor\Executor;
use Swoole\Runtime;
use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis;
use Utopia\Audit\Adapter\Database as AdapterDatabase;
use Utopia\Audit\Audit as UtopiaAudit;
use Utopia\Cache\Adapter\Pool as CachePool;
use Utopia\Cache\Adapter\Sharding;
use Utopia\Cache\Cache;
use Utopia\Config\Config;
use Utopia\Console;
use Utopia\Database\Adapter\Pool as DatabasePool;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\DSN\DSN;
use Utopia\Logger\Log;
use Utopia\Logger\Logger;
use Utopia\Platform\Service;
use Utopia\Pools\Group;
use Utopia\Queue\Adapter\Swoole;
use Utopia\Queue\Broker\Pool as BrokerPool;
use Utopia\Queue\Message;
use Utopia\Queue\Publisher;
use Utopia\Queue\Queue;
use Utopia\Queue\Server;
use Utopia\Registry\Registry;
use Utopia\Storage\Device\Telemetry as TelemetryDevice;
use Utopia\System\System;
use Utopia\Telemetry\Adapter as Telemetry;
use Utopia\Telemetry\Adapter\None as NoTelemetry;
Runtime::enableCoroutine();
require_once __DIR__ . '/init/span.php';
global $register;
Server::setResource('register', fn () => $register);
global $container;
$container->set('pools', function ($register) {
return $register->get('pools');
}, ['register']);
Server::setResource('authorization', function () {
$container->set('authorization', function () {
$authorization = new Authorization();
$authorization->disable();
return $authorization;
}, []);
Server::setResource('dbForPlatform', function (Cache $cache, Registry $register, Authorization $authorization) {
$pools = $register->get('pools');
$adapter = new DatabasePool($pools->get('console'));
$dbForPlatform = new Database($adapter, $cache);
$container->set('project', fn () => new Document([]), []);
$dbForPlatform
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setNamespace('_console')
->setDocumentType('users', User::class);
$container->set('log', fn () => new Log(), []);
return $dbForPlatform;
}, ['cache', 'register', 'authorization']);
Server::setResource('project', function (Message $message, Database $dbForPlatform) {
$payload = $message->getPayload() ?? [];
$project = new Document($payload['project'] ?? []);
if ($project->getId() === 'console') {
return $project;
}
return $dbForPlatform->getDocument('projects', $project->getId());
}, ['message', 'dbForPlatform']);
Server::setResource('dbForProject', function (Cache $cache, Registry $register, Message $message, Document $project, Database $dbForPlatform, Authorization $authorization) {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForPlatform;
}
$pools = $register->get('pools');
try {
$dsn = new DSN($project->getAttribute('database'));
} catch (\InvalidArgumentException) {
// TODO: Temporary until all projects are using shared tables
$dsn = new DSN('mysql://' . $project->getAttribute('database'));
}
$adapter = new DatabasePool($pools->get($dsn->getHost()));
$database = new Database($adapter, $cache);
$database->setDocumentType('users', User::class);
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
->setSharedTables(false)
->setTenant(null)
->setNamespace('_' . $project->getSequence());
}
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
return $database;
}, ['cache', 'register', 'message', 'project', 'dbForPlatform', 'authorization']);
Server::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache, Authorization $authorization) {
$databases = []; // TODO: @Meldiron This should probably be responsibility of utopia-php/pools
return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$databases): Database {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForPlatform;
}
try {
$dsn = new DSN($project->getAttribute('database'));
} catch (\InvalidArgumentException) {
// TODO: Temporary until all projects are using shared tables
$dsn = new DSN('mysql://' . $project->getAttribute('database'));
}
if (isset($databases[$dsn->getHost()])) {
$database = $databases[$dsn->getHost()];
$database->setAuthorization($authorization);
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
->setSharedTables(false)
->setTenant(null)
->setNamespace('_' . $project->getSequence());
}
return $database;
}
$adapter = new DatabasePool($pools->get($dsn->getHost()));
$database = new Database($adapter, $cache);
$databases[$dsn->getHost()] = $database;
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
->setSharedTables(false)
->setTenant(null)
->setNamespace('_' . $project->getSequence());
}
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER);
return $database;
};
}, ['pools', 'dbForPlatform', 'cache', 'authorization']);
Server::setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
$database = null;
return function (?Document $project = null) use ($pools, $cache, $database, $authorization) {
if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant($project->getSequence());
return $database;
}
$adapter = new DatabasePool($pools->get('logs'));
$database = new Database($adapter, $cache);
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setSharedTables(true)
->setNamespace('logsV1')
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER)
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES_WORKER);
if ($project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant($project->getSequence());
}
return $database;
};
}, ['pools', 'cache', 'authorization']);
Server::setResource('abuseRetention', function () {
return time() - (int) System::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', 86400); // 1 day
});
Server::setResource('auditRetention', function (Document $project) {
if ($project->getId() === 'console') {
return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE', 15778800)); // 6 months
}
return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 1209600)); // 14 days
}, ['project']);
Server::setResource('executionRetention', function () {
return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', 1209600)); // 14 days
});
Server::setResource('cache', function (Registry $register) {
$pools = $register->get('pools');
$list = Config::getParam('pools-cache', []);
$adapters = [];
foreach ($list as $value) {
$adapters[] = new CachePool($pools->get($value));
}
return new Cache(new Sharding($adapters));
}, ['register']);
Server::setResource('redis', function () {
$host = System::getEnv('_APP_REDIS_HOST', 'localhost');
$port = System::getEnv('_APP_REDIS_PORT', 6379);
$pass = System::getEnv('_APP_REDIS_PASS', '');
$redis = new \Redis();
@$redis->pconnect($host, (int) $port);
if ($pass) {
$redis->auth($pass);
}
$redis->setOption(\Redis::OPT_READ_TIMEOUT, -1);
return $redis;
});
Server::setResource('timelimit', function (\Redis $redis) {
return function (string $key, int $limit, int $time) use ($redis) {
return new TimeLimitRedis($key, $limit, $time, $redis);
};
}, ['redis']);
Server::setResource('log', fn () => new Log());
Server::setResource('publisher', function (Group $pools) {
return new BrokerPool(publisher: $pools->get('publisher'));
}, ['pools']);
Server::setResource('publisherDatabases', function (BrokerPool $publisher) {
return $publisher;
}, ['publisher']);
Server::setResource('publisherFunctions', function (BrokerPool $publisher) {
return $publisher;
}, ['publisher']);
Server::setResource('publisherMigrations', function (BrokerPool $publisher) {
return $publisher;
}, ['publisher']);
Server::setResource('publisherMessaging', function (BrokerPool $publisher) {
return $publisher;
}, ['publisher']);
Server::setResource('consumer', function (Group $pools) {
$container->set('consumer', function (Group $pools) {
return new BrokerPool(consumer: $pools->get('consumer'));
}, ['pools']);
Server::setResource('consumerDatabases', function (BrokerPool $consumer) {
$container->set('consumerDatabases', function (BrokerPool $consumer) {
return $consumer;
}, ['consumer']);
Server::setResource('consumerMigrations', function (BrokerPool $consumer) {
$container->set('consumerMigrations', function (BrokerPool $consumer) {
return $consumer;
}, ['consumer']);
Server::setResource('consumerStatsUsage', function (BrokerPool $consumer) {
$container->set('consumerStatsUsage', function (BrokerPool $consumer) {
return $consumer;
}, ['consumer']);
Server::setResource('usage', function () {
return new Context();
}, []);
Server::setResource('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher(
$publisher,
new Queue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME))
), ['publisher']);
Server::setResource('queueForDatabase', function (Publisher $publisher) {
return new EventDatabase($publisher);
}, ['publisher']);
Server::setResource('queueForMessaging', function (Publisher $publisher) {
return new Messaging($publisher);
}, ['publisher']);
Server::setResource('queueForMails', function (Publisher $publisher) {
return new Mail($publisher);
}, ['publisher']);
Server::setResource('queueForBuilds', function (Publisher $publisher) {
return new Build($publisher);
}, ['publisher']);
Server::setResource('queueForScreenshots', function (Publisher $publisher) {
return new Screenshot($publisher);
}, ['publisher']);
Server::setResource('queueForDeletes', function (Publisher $publisher) {
return new Delete($publisher);
}, ['publisher']);
Server::setResource('queueForEvents', function (Publisher $publisher) {
return new Event($publisher);
}, ['publisher']);
Server::setResource('queueForAudits', function (Publisher $publisher) {
return new Audit($publisher);
}, ['publisher']);
Server::setResource('queueForWebhooks', function (Publisher $publisher) {
return new Webhook($publisher);
}, ['publisher']);
Server::setResource('queueForFunctions', function (Publisher $publisher) {
return new Func($publisher);
}, ['publisher']);
Server::setResource('queueForRealtime', function () {
return new Realtime();
}, []);
Server::setResource('queueForCertificates', function (Publisher $publisher) {
return new Certificate($publisher);
}, ['publisher']);
Server::setResource('queueForMigrations', function (Publisher $publisher) {
return new Migration($publisher);
}, ['publisher']);
Server::setResource('logger', function (Registry $register) {
return $register->get('logger');
}, ['register']);
Server::setResource('pools', function (Registry $register) {
return $register->get('pools');
}, ['register']);
Server::setResource('telemetry', fn () => new NoTelemetry());
Server::setResource('deviceForSites', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
Server::setResource('deviceForMigrations', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_IMPORTS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
Server::setResource('deviceForFunctions', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
Server::setResource('deviceForFiles', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
Server::setResource('deviceForBuilds', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
Server::setResource('deviceForCache', function (Document $project, Telemetry $telemetry) {
return new TelemetryDevice($telemetry, getDevice(APP_STORAGE_CACHE . '/app-' . $project->getId()));
}, ['project', 'telemetry']);
Server::setResource(
'isResourceBlocked',
fn () => fn (Document $project, string $resourceType, ?string $resourceId) => false
);
Server::setResource('plan', function (array $plan = []) {
return [];
});
Server::setResource('certificates', function () {
$container->set('certificates', function () {
$email = System::getEnv('_APP_EMAIL_CERTIFICATES', System::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS'));
if (empty($email)) {
throw new Exception('You must set a valid security email address (_APP_EMAIL_CERTIFICATES) to issue a LetsEncrypt SSL certificate.');
}
return new LetsEncrypt($email);
});
}, []);
Server::setResource('logError', function (Registry $register, Document $project) {
return function (Throwable $error, string $namespace, string $action, ?array $extras = null) use ($register, $project) {
$logger = $register->get('logger');
if ($logger) {
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
$log = new Log();
$log->setNamespace($namespace);
$log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname()));
$log->setVersion($version);
$log->setType(Log::TYPE_ERROR);
$log->setMessage($error->getMessage());
$log->addTag('code', $error->getCode());
$log->addTag('verboseType', get_class($error));
$log->addTag('projectId', $project->getId() ?? '');
$log->addExtra('file', $error->getFile());
$log->addExtra('line', $error->getLine());
$log->addExtra('trace', $error->getTraceAsString());
if ($error->getPrevious() !== null) {
if ($error->getPrevious()->getMessage() != $error->getMessage()) {
$log->addExtra('previousMessage', $error->getPrevious()->getMessage());
}
$log->addExtra('previousFile', $error->getPrevious()->getFile());
$log->addExtra('previousLine', $error->getPrevious()->getLine());
}
foreach (($extras ?? []) as $key => $value) {
$log->addExtra($key, $value);
}
$log->setAction($action);
$isProduction = System::getEnv('_APP_ENV', 'development') === 'production';
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
try {
$responseCode = $logger->addLog($log);
Console::info('Error log pushed with status code: ' . $responseCode);
} catch (Throwable $th) {
Console::error('Error pushing log: ' . $th->getMessage());
}
}
Console::warning("Failed: {$error->getMessage()}");
Console::warning($error->getTraceAsString());
if ($error->getPrevious() !== null) {
if ($error->getPrevious()->getMessage() != $error->getMessage()) {
Console::warning("Previous Failed: {$error->getPrevious()->getMessage()}");
}
Console::warning("Previous File: {$error->getPrevious()->getFile()} Line: {$error->getPrevious()->getLine()}");
}
};
}, ['register', 'project']);
Server::setResource('executor', fn () => new Executor());
Server::setResource('getAudit', function (Database $dbForPlatform, callable $getProjectDB) {
return function (Document $project) use ($dbForPlatform, $getProjectDB) {
if ($project->isEmpty() || $project->getId() === 'console') {
$adapter = new AdapterDatabase($dbForPlatform);
return new UtopiaAudit($adapter);
}
$dbForProject = $getProjectDB($project);
$adapter = new AdapterDatabase($dbForProject);
return new UtopiaAudit($adapter);
};
}, ['dbForPlatform', 'getProjectDB']);
Server::setResource('executionsRetentionCount', function (Document $project, array $plan) {
if ($project->getId() === 'console' || empty($plan)) {
return 0;
}
return (int) ($plan['executionsRetentionCount'] ?? 100);
}, ['project', 'plan']);
$pools = $register->get('pools');
$platform = new Appwrite();
$args = $platform->getEnv('argv');
$args = $_SERVER['argv'] ?? [];
if (! isset($args[1])) {
Console::error('Missing worker name');
@@ -521,38 +79,45 @@ if (\str_starts_with($workerName, 'databases')) {
$queueName = System::getEnv('_APP_QUEUE_NAME', 'v1-' . strtolower($workerName));
}
/** @var \Utopia\Pools\Group $pools */
$pools = $container->get('pools');
$adapter = new Swoole(
$pools->get('consumer')->pop()->getResource(),
System::getEnv('_APP_WORKERS_NUM', 1),
$queueName
);
$worker = new Server($adapter, $container);
try {
/**
* Any worker can be configured with the following env vars:
* - _APP_WORKERS_NUM The total number of worker processes
* - _APP_WORKER_PER_CORE The number of worker processes per core (ignored if _APP_WORKERS_NUM is set)
* - _APP_QUEUE_NAME The name of the queue to read for database events
*/
$worker->init()->action(function () use ($worker, $registerWorkerMessageResources) {
$registerWorkerMessageResources($worker->getContainer());
});
$container->set('bus', function ($register) use ($worker) {
return $register->get('bus')->setResolver(
fn (string $name) => $worker->getContainer()->get($name)
);
}, ['register']);
$platform->setWorker($worker);
$platform->init(Service::TYPE_WORKER, [
'workersNum' => System::getEnv('_APP_WORKERS_NUM', 1),
'connection' => $pools->get('consumer')->pop()->getResource(),
'workerName' => strtolower($workerName) ?? null,
'queueName' => $queueName,
'workerName' => strtolower($workerName),
]);
} catch (\Throwable $e) {
Console::error($e->getMessage() . ', File: ' . $e->getFile() . ', Line: ' . $e->getLine());
Console::exit(1);
}
$worker = $platform->getWorker();
Server::setResource('bus', function ($register) use ($worker) {
return $register->get('bus')->setResolver(fn (string $name) => $worker->getResource($name));
}, ['register']);
$worker
->error()
->inject('error')
->inject('logger')
->inject('log')
->inject('pools')
->inject('project')
->inject('authorization')
->action(function (Throwable $error, ?Logger $logger, Log $log, Group $pools, Document $project, Authorization $authorization) use ($queueName) {
->action(function (Throwable $error, ?Logger $logger, Log $log, Document $project, Authorization $authorization) use ($queueName) {
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
if ($logger) {
@@ -564,7 +129,7 @@ $worker
$log->setAction('appwrite-queue-' . $queueName);
$log->addTag('verboseType', get_class($error));
$log->addTag('code', $error->getCode());
$log->addTag('projectId', $project->getId() ?? 'n/a');
$log->addTag('projectId', $project->getId());
$log->addExtra('file', $error->getFile());
$log->addExtra('line', $error->getLine());
$log->addExtra('trace', $error->getTraceAsString());
+16 -16
View File
@@ -13,9 +13,9 @@
"test": "vendor/bin/phpunit",
"lint": "vendor/bin/pint --test --config pint.json",
"format": "vendor/bin/pint --config pint.json",
"analyze": "./vendor/bin/phpstan analyse -c phpstan.neon --memory-limit=1G",
"analyze": "./vendor/bin/phpstan analyse -c phpstan.neon --memory-limit=1G",
"bench": "vendor/bin/phpbench run --report=benchmark",
"check": "./vendor/bin/phpstan analyse -c phpstan.neon",
"check": "./vendor/bin/phpstan analyse -c phpstan.neon --memory-limit=1G",
"installer:clean": "php src/Appwrite/Platform/Installer/Server.php --clean",
"installer:dev": "docker compose build && composer installer:clean && php src/Appwrite/Platform/Installer/Server.php --docker"
},
@@ -57,34 +57,35 @@
"utopia-php/audit": "2.2.*",
"utopia-php/auth": "0.5.*",
"utopia-php/cache": "1.0.*",
"utopia-php/cli": "0.22.*",
"utopia-php/cli": "0.23.*",
"utopia-php/compression": "0.1.*",
"utopia-php/config": "1.*",
"utopia-php/console": "0.1.*",
"utopia-php/database": "dev-fix-collection-recreate as 5.3.15",
"utopia-php/database": "5.*",
"utopia-php/detector": "0.2.*",
"utopia-php/domains": "1.*",
"utopia-php/emails": "0.6.*",
"utopia-php/dns": "1.6.*",
"utopia-php/dsn": "0.2.1",
"utopia-php/framework": "0.33.*",
"utopia-php/http": "0.34.*",
"utopia-php/fetch": "0.5.*",
"utopia-php/validators": "0.2.*",
"utopia-php/image": "0.8.*",
"utopia-php/locale": "0.8.*",
"utopia-php/logger": "0.6.*",
"utopia-php/messaging": "0.20.*",
"utopia-php/migration": "1.7.*",
"utopia-php/platform": "0.7.*",
"utopia-php/messaging": "0.22.*",
"utopia-php/migration": "1.9.*",
"utopia-php/platform": "0.13.*",
"utopia-php/pools": "1.*",
"utopia-php/span": "1.1.*",
"utopia-php/preloader": "0.2.*",
"utopia-php/queue": "0.15.*",
"utopia-php/servers": "0.2.5",
"utopia-php/queue": "0.17.*",
"utopia-php/servers": "0.3.*",
"utopia-php/registry": "0.5.*",
"utopia-php/storage": "1.0.*",
"utopia-php/storage": "2.*",
"utopia-php/system": "0.10.*",
"utopia-php/telemetry": "0.2.*",
"utopia-php/vcs": "2.*",
"utopia-php/vcs": "3.*",
"utopia-php/websocket": "1.0.*",
"matomo/device-detector": "6.4.*",
"dragonmantank/cron-expression": "3.4.*",
@@ -92,15 +93,15 @@
"chillerlan/php-qrcode": "4.3.*",
"adhocore/jwt": "1.1.*",
"spomky-labs/otphp": "11.*",
"webonyx/graphql-php": "14.11.*",
"webonyx/graphql-php": "15.31.*",
"league/csv": "9.14.*",
"enshrined/svg-sanitize": "0.22.*",
"utopia-php/di": "0.1.0"
"utopia-php/lock": "0.2.*"
},
"repositories": [
{
"type": "vcs",
"url": "https://github.com/utopia-php/database"
"url": "https://github.com/utopia-php/lock"
}
],
"require-dev": {
@@ -119,7 +120,6 @@
},
"config": {
"platform": {
"php": "8.3"
},
"allow-plugins": {
"php-http/discovery": true,
Generated
+426 -386
View File
File diff suppressed because it is too large Load Diff
+60 -16
View File
@@ -112,6 +112,8 @@ services:
condition: service_healthy
coredns:
condition: service_started
ollama:
condition: service_started
entrypoint:
- php
- -e
@@ -159,6 +161,12 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER_VECTORSDB
- _APP_DB_HOST_VECTORSDB
- _APP_DB_PORT_VECTORSDB
- _APP_DB_SCHEMA_VECTORSDB
- _APP_DB_USER_VECTORSDB
- _APP_DB_PASS_VECTORSDB
- _APP_SMTP_HOST
- _APP_SMTP_PORT
- _APP_SMTP_SECURE
@@ -204,6 +212,7 @@ services:
- _APP_EXECUTOR_SECRET
- _APP_EXECUTOR_HOST
- _APP_LOGGING_CONFIG
- _APP_LOCKING_ENABLED
- _APP_MAINTENANCE_INTERVAL
- _APP_MAINTENANCE_RETENTION_EXECUTION
- _APP_MAINTENANCE_RETENTION_CACHE
@@ -234,19 +243,20 @@ services:
- _APP_EXPERIMENT_LOGGING_PROVIDER
- _APP_EXPERIMENT_LOGGING_CONFIG
- _APP_DATABASE_SHARED_TABLES
- _APP_DATABASE_SHARED_TABLES_V1
- _APP_DATABASE_SHARED_NAMESPACE
- _APP_FUNCTIONS_CREATION_ABUSE_LIMIT
- _APP_CUSTOM_DOMAIN_DENY_LIST
- _APP_TRUSTED_HEADERS
- _APP_MIGRATION_HOST
- _TESTS_OAUTH2_GITHUB_CLIENT_ID
- _TESTS_OAUTH2_GITHUB_CLIENT_SECRET
extra_hosts:
- "host.docker.internal:host-gateway"
appwrite-console:
<<: *x-logging
container_name: appwrite-console
image: appwrite/console:7.5.7
image: appwrite/console:7.8.45
restart: unless-stopped
networks:
- appwrite
@@ -295,6 +305,7 @@ services:
depends_on:
- ${_APP_DB_HOST:-mongodb}
- redis
- ollama
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -311,6 +322,12 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER_VECTORSDB
- _APP_DB_HOST_VECTORSDB
- _APP_DB_PORT_VECTORSDB
- _APP_DB_SCHEMA_VECTORSDB
- _APP_DB_USER_VECTORSDB
- _APP_DB_PASS_VECTORSDB
- _APP_USAGE_STATS
- _APP_LOGGING_CONFIG
- _APP_LOGGING_CONFIG_REALTIME
@@ -330,6 +347,7 @@ services:
depends_on:
- redis
- ${_APP_DB_HOST:-mongodb}
- ollama
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -363,6 +381,7 @@ services:
- ${_APP_DB_HOST:-mongodb}
- request-catcher-sms
- request-catcher-webhook
- ollama
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -393,6 +412,7 @@ services:
depends_on:
- redis
- ${_APP_DB_HOST:-mongodb}
- ollama
volumes:
- appwrite-uploads:/storage/uploads:rw
- appwrite-cache:/storage/cache:rw
@@ -402,6 +422,7 @@ services:
- appwrite-certificates:/storage/certificates:rw
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -443,7 +464,6 @@ services:
- _APP_EXECUTOR_SECRET
- _APP_EXECUTOR_HOST
- _APP_DATABASE_SHARED_TABLES
- _APP_DATABASE_SHARED_TABLES_V1
- _APP_EMAIL_CERTIFICATES
- _APP_MAINTENANCE_RETENTION_AUDIT
- _APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE
@@ -458,9 +478,11 @@ services:
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- redis
- ${_APP_DB_HOST:-mongodb}
- ollama
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -476,6 +498,12 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER_VECTORSDB
- _APP_DB_HOST_VECTORSDB
- _APP_DB_PORT_VECTORSDB
- _APP_DB_SCHEMA_VECTORSDB
- _APP_DB_USER_VECTORSDB
- _APP_DB_PASS_VECTORSDB
- _APP_LOGGING_CONFIG
- _APP_WORKERS_NUM
- _APP_QUEUE_NAME
@@ -497,6 +525,7 @@ services:
depends_on:
- redis
- ${_APP_DB_HOST:-mongodb}
- ollama
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -629,6 +658,7 @@ services:
depends_on:
- redis
- ${_APP_DB_HOST:-mongodb}
- ollama
volumes:
- appwrite-config:/storage/config:rw
- appwrite-certificates:/storage/certificates:rw
@@ -848,6 +878,7 @@ services:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
- ./tests:/usr/src/code/tests
depends_on:
- ${_APP_DB_HOST:-mongodb}
environment:
@@ -1044,6 +1075,7 @@ services:
depends_on:
- redis
- ${_APP_DB_HOST:-mongodb}
- ollama
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -1077,6 +1109,7 @@ services:
depends_on:
- ${_APP_DB_HOST:-mongodb}
- redis
- ollama
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -1107,6 +1140,7 @@ services:
depends_on:
- ${_APP_DB_HOST:-mongodb}
- redis
- ollama
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -1137,6 +1171,7 @@ services:
depends_on:
- ${_APP_DB_HOST:-mongodb}
- redis
- ollama
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -1228,7 +1263,6 @@ services:
start_period: 5s
mariadb:
profiles: ["mariadb"]
image: mariadb:10.11 # fix issues when upgrading using: mysql_upgrade -u root -p
container_name: appwrite-mariadb
<<: *x-logging
@@ -1252,10 +1286,10 @@ services:
retries: 12
mongodb:
profiles: ["mongodb"]
image: mongo:8.2.5
container_name: appwrite-mongodb
<<: *x-logging
restart: on-failure:3
networks:
- appwrite
volumes:
@@ -1288,32 +1322,41 @@ services:
retries: 10
start_period: 30s
postgresql:
profiles: ["postgresql"]
build:
context: ./tests/resources/postgresql
args:
POSTGRES_VERSION: 17
image: appwrite/postgres:0.1.0
container_name: appwrite-postgresql
<<: *x-logging
networks:
- appwrite
volumes:
- appwrite-postgresql:/var/lib/postgresql:rw
- appwrite-postgresql:/var/lib/postgresql/18/data:rw
ports:
- "5432:5432"
environment:
- POSTGRES_DB=${_APP_DB_SCHEMA}
- POSTGRES_USER=${_APP_DB_USER}
- POSTGRES_PASSWORD=${_APP_DB_PASS}
command: "postgres"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${_APP_DB_USER}"]
test: ["CMD-SHELL", "pg_isready -U ${_APP_DB_USER} -d ${_APP_DB_SCHEMA}"]
interval: 5s
timeout: 5s
retries: 12
retries: 10
start_period: 10s
command: "postgres"
ollama:
image: appwrite/ollama:0.1.1
container_name: ollama
ports:
- "11434:11434"
restart: unless-stopped
environment:
MODELS: ${_APP_EMBEDDING_MODELS:-embeddinggemma}
OLLAMA_KEEP_ALIVE: 24h
volumes:
- appwrite-models:/root/.ollama
networks:
- appwrite
redis:
image: redis:7.4.7-alpine
@@ -1436,3 +1479,4 @@ volumes:
appwrite-sites:
appwrite-builds:
appwrite-config:
appwrite-models:
@@ -1 +0,0 @@
Initialize an MFA challenge of the specified factor. The factor must be available on the account.
@@ -1 +0,0 @@
Use this endpoint to log out the currently logged in user from their account. When successful this endpoint will delete the user session and remove the session secret cookie from the user client.
-1
View File
@@ -1 +0,0 @@
Get all Environment Variables that are relevant for the console.
@@ -0,0 +1 @@
Create a new Collection. Before using this route, you should create a new database resource using either a [server integration](https://appwrite.io/docs/server/databases#documentsDBCreateCollection) API or directly from your database console.
@@ -0,0 +1 @@
Create a new Document. Before using this route, you should create a new collection resource using either a [server integration](https://appwrite.io/docs/server/databases#documentsDBCreateCollection) API or directly from your database console.
@@ -0,0 +1 @@
Create new Documents. Before using this route, you should create a new collection resource using either a [server integration](https://appwrite.io/docs/server/databases#documentsDBCreateCollection) API or directly from your database console.
@@ -0,0 +1,2 @@
Creates an index on the attributes listed. Your index should include all the attributes you will query in a single request.
Attributes can be `key`, `fulltext`, and `unique`.
@@ -0,0 +1 @@
Create multiple operations in a single transaction.
@@ -0,0 +1 @@
Create a new transaction.
+1
View File
@@ -0,0 +1 @@
Create a new Database.
@@ -0,0 +1 @@
Decrement a specific column of a row by a given value.
@@ -0,0 +1 @@
Delete a collection by its unique ID. Only users with write permissions have access to delete this resource.
@@ -0,0 +1 @@
Delete a document by its unique ID.
@@ -0,0 +1 @@
Bulk delete documents using queries, if no queries are passed then all documents are deleted.
@@ -0,0 +1 @@
Delete an index.
@@ -0,0 +1 @@
Delete a transaction by its unique ID.
+1
View File
@@ -0,0 +1 @@
Delete a database by its unique ID. Only API keys with with databases.write scope can delete a database.
@@ -0,0 +1 @@
Get usage metrics and statistics for a collection. Returning the total number of documents. The response includes both current totals and historical data over time. Use the optional range parameter to specify the time window for historical data: 24h (last 24 hours), 30d (last 30 days), or 90d (last 90 days). If not specified, range defaults to 30 days.
@@ -0,0 +1 @@
Get a collection by its unique ID. This endpoint response returns a JSON object with the collection metadata.
@@ -0,0 +1 @@
Get usage metrics and statistics for a database. You can view the total number of collections, documents, and storage usage. The response includes both current totals and historical data over time. Use the optional range parameter to specify the time window for historical data: 24h (last 24 hours), 30d (last 30 days), or 90d (last 90 days). If not specified, range defaults to 30 days.
@@ -0,0 +1 @@
Get a document by its unique ID. This endpoint response returns a JSON object with the document data.

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