Compare commits

...
Author SHA1 Message Date
ArnabChatterjee20k 331fb7f689 fixed php stan issue 2026-03-19 17:10:45 +05:30
ArnabChatterjee20k feddd77066 fixed analyze 2026-03-19 17:02:27 +05:30
ArnabChatterjee20k a55f3e1db8 updated env 2026-03-19 16:59:18 +05:30
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
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 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
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
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
HemachandarandGitHub 6b573524e7 fix: Ensure rules are deleted properly (#11575)
* fix: Ensure rules are deleted properly

* tests
2026-03-18 19:02:03 +05:30
Matej BačoandGitHub 12d274a72c Merge pull request #11566 from appwrite/feat-platform-endpoints
Feat: Public project APIs
2026-03-18 11:57:18 +01:00
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
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
Eldad A. FuxandGitHub 6c383b41f5 Merge pull request #11573 from appwrite/fix-realtime-errors
Fix realtime errors
2026-03-17 22:48:30 +01:00
eldadfux b2cb9397fd better realtime errors
Made-with: Cursor
2026-03-17 20:54:47 +01:00
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
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
Matej Bačo 6d8bf6c0dd AI review fixes 2026-03-17 13:56:27 +01:00
Matej Bačo bf375d1d40 Fix bugs 2026-03-17 13:52:50 +01:00
Matej Bačo fd05827089 Implement tests 2026-03-17 13:27:53 +01:00
Matej Bačo ac2b35004d Add more public webhook endpoints 2026-03-17 13:03:26 +01:00
Luke B. SilverandGitHub c4b7bb8ee9 Merge pull request #11568 from appwrite/ci/phpstan-baseline-2
ci: reduce PHPStan level to 3 and regenerate baseline
2026-03-17 11:25:58 +00:00
loks0nandClaude Sonnet 4.6 48853cdbe1 ci: reduce PHPStan level to 3 and regenerate baseline
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 11:16:59 +00:00
Matej Bačo 6b6092c9ec Add more webhook endpoints 2026-03-17 11:20:18 +01:00
Matej Bačo 677bb048cc Introduce new webhooks API 2026-03-17 11:03:18 +01:00
Luke B. SilverandGitHub b093a3fb24 Merge pull request #11563 from appwrite/test/flaky-usage
fix: use console headers for attribute status polling in usage tests
2026-03-16 18:47:55 +00:00
loks0nandClaude Sonnet 4.6 3dc9a10fab fix: use console headers for execution status polling in usage tests
Switch function execution status polling in testPrepareFunctionsStats to
use console headers so the non-deterministic number of poll requests does
not skew the project request count assertion or cause null-status failures.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 17:41:25 +00:00
Luke B. SilverandGitHub 2142905409 Merge branch '1.8.x' into test/flaky-usage 2026-03-16 17:28:27 +00:00
loks0nandClaude Sonnet 4.6 af401eb950 fix: use console headers for attribute status polling in usage tests
Switch attribute/column status polling in testPrepareDatabaseStatsCollectionsAPI
and testPrepareDatabaseStatsTablesAPI to use console headers so the non-deterministic
number of poll requests does not skew the project request count assertion.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 17:27:41 +00:00
Luke B. SilverandGitHub 392926bc1a Merge pull request #11247 from appwrite/feat-installer
Add web installer
2026-03-16 14:54:09 +00:00
Jake BarnbyandClaude Opus 4.6 06c5a2845f fix: restore deployment polling timeout in Realtime testChannelExecutions
The timeout parameters were accidentally removed in daaf1d7f77, causing
the assertEventually to use the default timeout which is too short for
deployment builds in CI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 01:30:55 +13:00
Jake BarnbyandClaude Opus 4.6 e6272f7a35 fix: update utopia-php/database to fix shared tables collection recreation
Points to fix-collection-recreate branch which prevents the orphan
reconciliation logic from dropping and recreating shared physical tables
when a new tenant creates collections. In shared-tables mode, a
DuplicateException from the adapter means the table is shared with other
tenants, not an orphan from a partial failure.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 00:58:44 +13:00
Jake BarnbyandClaude Opus 4.6 e8790c405b fix: sync composer.lock content-hash with composer.json
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 00:10:15 +13:00
Jake Barnby 83a8e2811d Update DB 2026-03-17 00:08:01 +13:00
Jake BarnbyandClaude Opus 4.6 41017bb0af fix: revert app/sdks exclusion (not present in CI)
The app/sdks directory is gitignored and doesn't exist in CI. Removing
the exclusion to avoid PHPStan failing on a non-existent path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 23:48:22 +13:00
Jake BarnbyandClaude Opus 4.6 5478d3d601 fix: regenerate PHPStan baseline and exclude SDK tests
The baseline had gone stale after System::getEnv signature changed from
string|null to string. Regenerated to remove unmatched patterns and add
new installer code entries. Also excluded app/sdks from analysis since
those are auto-generated SDK files.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 23:30:19 +13:00
Jake Barnby 176a29d608 Remove redundant dep 2026-03-16 21:42:37 +13:00
Jake Barnby ee732581d8 Update DB 2026-03-16 21:40:06 +13:00
Jake Barnby a94649fb58 Merge remote-tracking branch 'origin/1.8.x' into feat-installer
# Conflicts:
#	composer.json
#	composer.lock
2026-03-16 21:25:10 +13:00
Chirag AggarwalandGitHub 366da19b61 Merge pull request #11472 from appwrite/update-python-sdk-readme
chore: update python sdk readme with pydantic models
2026-03-16 13:40:36 +05:30
Jake BarnbyandClaude Opus 4.6 daaf1d7f77 fix: revert test polling timeouts to original values
The root cause (PostgreSQL duplicate index race condition) has been
fixed in the database library, so the timeout workarounds are no
longer needed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 21:00:36 +13:00
Jake BarnbyandClaude Opus 4.6 1209a2e344 fix: increase Webhooks test polling timeouts to 240s for CI stability
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 13:27:15 +13:00
Jake BarnbyandClaude Opus 4.6 04bbc30e7e fix: move attribute setup outside Swoole coroutine to prevent fatal errors
The assertEventually call inside Coroutine\run() causes a fatal PHP
error (exit 255) when it times out, killing the entire test process.
Moving DB/collection/attribute setup and polling outside the coroutine
ensures PHPUnit can properly handle assertion failures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 12:55:14 +13:00
Jake BarnbyandClaude Opus 4.6 48c9ecb53f fix: increase all Realtime test polling timeouts to 240s for CI stability
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 12:12:48 +13:00
Jake BarnbyandClaude Opus 4.6 eb082cdd86 fix: increase GraphQL Legacy attribute polling timeout to 240s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 12:03:01 +13:00
Jake BarnbyandClaude Opus 4.6 0c7a9290bd fix: increase Sites deployment timeouts from 120s to 200s for CI
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 11:48:57 +13:00
Jake BarnbyandClaude Opus 4.6 341aabb942 fix: handle PostgreSQL race condition in shared mode project creation
PostgreSQL adapter throws non-Duplicate exceptions when a table/index
already exists during concurrent createCollection. Catch Throwable and
attempt metadata-only creation as fallback. Also increase Realtime
coroutine attribute polling timeout from 30s to 60s.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 11:29:53 +13:00
Jake BarnbyandClaude Opus 4.6 09c963abda fix: use $sequence for team lookup instead of $internalId
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 10:51:18 +13:00
Jake BarnbyandClaude Opus 4.6 eb2f0b1cea fix: restore team lookup by $internalId and remove duplicate composer key
The team resource query was changed from $internalId to $sequence,
breaking console session authentication (401 on project creation).
Also removed duplicate utopia-php/servers entry in composer.json
and updated composer.lock hash.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 10:40:29 +13:00
Jake BarnbyandGitHub 01a9340eaf Merge branch '1.8.x' into feat-installer 2026-03-14 10:30:29 +13:00
Jake BarnbyandClaude Opus 4.6 8eb309e92c fix: update utopia-php/database to 5.3.12 for tenant type comparison fix
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 01:17:07 +13:00
Jake Barnby 6fee0cb73e Revert "fix: cast tenant to int for SQL adapters to fix strict type comparison"
This reverts commit 840b1fbef3.
2026-03-14 01:16:14 +13:00
Jake BarnbyandClaude Opus 4.6 840b1fbef3 fix: cast tenant to int for SQL adapters to fix strict type comparison
Document::getTenant() casts numeric tenants to (int), but
adapter->getTenant() held a string from getSequence(). The strict
!== comparison in Database::getCollection() then failed, returning
"Collection not found" for all shared-table MariaDB projects.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 01:06:43 +13:00
Jake BarnbyandClaude Opus 4.6 03ebc4857a fix: pin utopia-php/di and servers to prevent breaking upgrades
The composer update pulled in utopia-php/di 0.3.1 which removed the
Dependency class, causing fatal errors. Pin to 0.1.0 and servers 0.2.5.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 00:49:31 +13:00
Jake BarnbyandClaude Opus 4.6 1db4c40c6a fix: update composer.lock to clean database 5.3.11
Picks up reverted Sequence validator changes that were breaking
MariaDB shared tables.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 00:37:23 +13:00
Jake BarnbyandClaude Opus 4.6 99f08b9f95 fix: update utopia-php/database to 5.3.11 and utopia-php/mongo to 1.*
Use released versions instead of dev branches:
- database 5.3.11: $tenant type VAR_INTEGER -> VAR_ID for MongoDB compatibility
- mongo 1.0.1: collection exists exception uses error code 48

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 00:17:34 +13:00
Jake BarnbyandClaude Opus 4.6 cdbb8ff7e4 fix: use mongo library branch with collection exists error code fix
Adds utopia-php/mongo dev-fix-collection-exists-code which sets error
code 48 (NamespaceExists) when createCollection detects the collection
already exists. This allows the database adapter to handle it as a
DuplicateException and return true, fixing shared tables mode where
collections like _audit are shared across projects.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 20:20:06 +13:00
Jake BarnbyandClaude Opus 4.6 99f21b8af6 fix: use database library fix-mongo-perms branch for VAR_ID tenant type
Updates utopia-php/database to dev-fix-mongo-perms which changes the
$tenant internal attribute type from VAR_INTEGER to VAR_ID, fixing
MongoDB shared tables where tenant values are UUID7 strings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 20:04:42 +13:00
Jake BarnbyandClaude Opus 4.6 871795efa9 fix: remove conditional tenant int cast, use string tenant directly
The $tenant attribute type in the database library is being changed from
VAR_INTEGER to VAR_ID, which handles both SQL (integer) and MongoDB
(UUID7 string) adapters natively. This removes the now-unnecessary
conditional casting pattern throughout the codebase.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 19:53:42 +13:00
Jake Barnby dba8c28741 (fix): resolve permissions test collisions in parallel execution 2026-03-13 19:13:17 +13:00
Jake Barnby 526aa590e1 (fix): add _APP_DATABASE_SHARED_TABLES env to scheduler-executions container 2026-03-13 19:13:13 +13:00
Jake Barnby d91e3543c8 (fix): use adapter ID type to determine tenant value for MongoDB compatibility 2026-03-13 19:13:07 +13:00
Jake BarnbyandClaude Opus 4.6 f15f63c0c1 fix: update utopia-php/database to 5.3.9 for project permission fix
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 16:31:54 +13:00
Jake BarnbyandClaude Opus 4.6 fa406adab9 fix: update composer.lock to resolve paratest dependency conflict
symfony/console and symfony/string needed upgrading to satisfy
brianium/paratest v7.19.2's version constraints.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 14:49:34 +13:00
Jake Barnby aa1012ffb6 Merge remote-tracking branch 'origin/1.8.x' into feat-installer
# Conflicts:
#	.github/workflows/tests.yml
#	composer.lock
2026-03-13 14:47:30 +13:00
Jake Barnby c192a154f5 (fix): Prevent auto-redirect on installer step errors 2026-03-13 14:36:45 +13:00
Jake Barnby 6319d961eb (refactor): Simplify installer progress completion flow 2026-03-13 00:32:42 +13:00
Jake Barnby 5260577872 (fix): Make installer idempotent and fix session secret encoding 2026-03-13 00:32:40 +13:00
Jake Barnby 254c670c1d (feat): Add service healthchecks and health-based depends_on conditions 2026-03-13 00:32:37 +13: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
Jake Barnby 2f01965014 Remove redundant images 2026-03-11 17:03:44 +13:00
Jake BarnbyandClaude Opus 4.6 36bd7a4667 (fix): Address code review security and correctness findings
- Remove var_dump debug calls leaking API keys to stdout
- Stop embedding secret keys in HTML data attributes on upgrades
- Strip sensitive fields from sessionStorage install lock
- Quote hostPath in Docker Compose YAML template
- Remove stack traces from client-facing error responses
- Strip sessionSecret and traces from Status endpoint response
- Fix undefined $input variable (should be $userInput) in CLI install
- Add backtick escaping in .env template to prevent shell injection
- Add 2-hour timeout to isInstallationComplete infinite loop
- Escape user-supplied startCommand in shell strings
- Add LOCK_EX to progress file writes
- Fix typo in Upgrade.php error message
- Remove unused variable in V21 response filter
- Remove dead code in applyLockPayload after sessionStorage sanitization

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:58:57 +13:00
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 Barnby fda8aee280 (style): Remove extra blank line in ConfigTest 2026-03-10 20:12:38 +13:00
Jake Barnby e2900d4349 Revert package 2026-03-10 19:49:02 +13:00
Jake Barnby 8b632eb428 (chore): Remove WEB_INSTALLER.md 2026-03-10 19:42:28 +13:00
Jake Barnby d19f14b60f (style): Remove section comment headers from installer tests 2026-03-10 19:42:19 +13:00
Jake Barnby c4c2534f9a (fix): Add CSRF validation to shutdown endpoint and quote .env values 2026-03-10 19:42:09 +13:00
Jake Barnby 91dffddf3b Format 2026-03-10 17:10:39 +13:00
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
Jake BarnbyandGitHub 06e3e98cec Merge branch '1.8.x' into feat-installer 2026-03-10 15:07:42 +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
Chirag Aggarwal d80aacd706 add type safety 2026-03-07 14:20:36 +05:30
Chirag Aggarwal 4cd1dcc6f3 Merge branch '1.8.x' into update-python-sdk-readme 2026-03-07 12:03:54 +05:30
Chirag Aggarwal 576435c24b chore: update python sdk readme with pydantic models 2026-03-07 09:32:00 +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
Jake Barnby 321528cac0 (refactor): Use Composer autoloading, Installer Platform class, and Utopia param validation 2026-03-04 22:58:55 +13:00
Jake Barnby 00d10e62b5 (feat): Add AppDomain validator for installer input 2026-03-04 22:58:48 +13:00
Jake Barnby cd5f3c3eb3 (chore): Bump base Docker image to 1.0.1 2026-03-04 22:58:43 +13:00
Jake BarnbyandClaude Opus 4.6 34dd5006bb fix: update installer ModuleTest for new Shutdown action
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 22:04:06 +13:00
Jake Barnby 3cc2375b15 Merge remote-tracking branch 'origin/1.8.x' into feat-installer 2026-03-03 20:57:13 +13:00
Jake Barnby 5ee58dcc81 (fix): Fix installer progress step transitions and redirect handling 2026-03-03 19:47:37 +13:00
Jake Barnby 1dfee199c0 (fix): Fix installer health check, network connect, and shutdown flow 2026-03-03 19:47:29 +13:00
Jake Barnby 4fda4d4ac3 Fix host access 2026-03-03 00:46:51 +13:00
Jake Barnby 9c7cef2971 Fix pg 18 path 2026-03-03 00:46:38 +13:00
Jake Barnby 029cd695dd Use custom image 2026-03-03 00:46:25 +13:00
Jake Barnby 349bc63618 Trap ctrl+c on start 2026-03-02 21:53:08 +13:00
Jake Barnby dcfc33c65d (chore): Update lock 2026-03-02 21:50:30 +13:00
Jake Barnby 4c76ddc2a3 (fix): Fix installer Swoole migration and database selection 2026-03-02 21:50:25 +13:00
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
Jake Barnby f540df50b1 Merge remote-tracking branch 'origin/1.8.x' into feat-installer 2026-03-02 18:40:37 +13:00
Jake Barnby adda327509 Update lock 2026-03-02 18:40:19 +13:00
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
Jake BarnbyandGitHub 1790f56d99 Merge branch '1.8.x' into feat-installer 2026-02-27 06:07:56 +00:00
Jake BarnbyandClaude Opus 4.6 18d7b12dff fix: capture readExistingCompose result and document shadowed param in Upgrade
- Store readExistingCompose() return value in $data before using it
  (previously $data was undefined on line 60)
- Add comment documenting that CLI database param is intentionally
  overridden by detection from existing configuration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 02:56:55 +13:00
Jake BarnbyandClaude Opus 4.6 118199fd4b fix: revert name/email hashing in analytics payload
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 02:54:06 +13:00
Jake BarnbyandClaude Opus 4.6 7a27cf4ac7 fix: address review feedback for installer PR
- Initialize $isUpgrade=false in Install.php action() to prevent undefined variable
- Assign $this->lockedDatabase in Upgrade.php before calling parent::action()
- Remove stack trace exposure from buildErrorDetails() in Http Install action
- Suppress raw exception messages for 500+ errors in Error handler
- Remove sessionSecret from progress details to prevent credential leak
- Hash name/email in analytics payload to avoid sending raw PII
- Validate and default dbService in compose.phtml to prevent invalid output
- Fix host normalization in progress.js redirect URL builder
- Release global lock on early return for existing installation conflict
- Consolidate duplicate database host/port assignment blocks
- Add @runInSeparateProcess to testRouteRegistration to prevent global state leak

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 01:44:46 +13:00
Jake Barnby f8d8997ae3 Update installer 2026-02-27 01:29:38 +13:00
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
Jake Barnby a7046dbe3c (fix): Replace tab characters with spaces in tests.yml workflow 2026-02-26 22:43:23 +13:00
Jake Barnby 5f58e1da90 (fix): Fix syntax error and lint issues in Upgrade.php 2026-02-26 22:13:46 +13:00
Jake Barnby 7f29baec77 (style): Fix indentation and import ordering lint errors 2026-02-26 22:02:49 +13:00
Jake Barnby bc992f15fc (chore): Update composer.lock to match composer.json 2026-02-26 21:56:38 +13:00
Jake Barnby 904f94659f (docs): Update web installer docs for Swoole architecture 2026-02-26 21:42:48 +13:00
Jake Barnby 8892e4f30e (test): Add unit tests for installer module, state, and config 2026-02-26 21:42:42 +13:00
Jake Barnby 41948cb337 (refactor): Replace HttpHandler with Utopia Platform module/action pattern and Swoole HTTP server 2026-02-26 21:42:37 +13:00
Jake Barnby 26dac3a410 Merge remote-tracking branch 'origin/1.8.x' into feat-installer
# Conflicts:
#	.gitignore
#	composer.lock
#	tests/e2e/Services/Databases/DatabasesBase.php
2026-02-26 18:51:23 +13:00
Jake Barnby 3ecb4ee4e2 Sync 1.8.x 2026-02-26 18:50:29 +13:00
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 Barnby b41678d57a Merge remote-tracking branch 'origin/feat-mongodb' into feat-installer
# Conflicts:
#	.github/workflows/tests.yml
#	Dockerfile
#	app/views/install/compose.phtml
#	composer.lock
#	mongo-entrypoint.sh
#	src/Appwrite/Platform/Tasks/Install.php
#	src/Appwrite/Platform/Tasks/Upgrade.php
#	tests/e2e/Client.php
#	tests/e2e/Services/Databases/DatabasesBase.php
#	tests/e2e/Services/Databases/Legacy/DatabasesCustomClientTest.php
#	tests/e2e/Services/Databases/Legacy/DatabasesCustomServerTest.php
#	tests/e2e/Services/Databases/TablesDB/DatabasesBase.php
#	tests/e2e/Services/Databases/TablesDB/DatabasesCustomClientTest.php
#	tests/e2e/Services/Databases/TablesDB/DatabasesCustomServerTest.php
#	tests/e2e/Services/Databases/Transactions/TransactionsBase.php
#	tests/e2e/Services/GraphQL/Legacy/DatabaseServerTest.php
#	tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php
#	tests/e2e/Services/Projects/ProjectsConsoleClientTest.php
#	tests/e2e/Services/Teams/TeamsCustomClientTest.php
2026-02-13 17:09:42 +13:00
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
Jake Barnby 97be6e2cf4 Merge branch 'feat-installer' of github.com:appwrite/appwrite into feat-installer 2026-02-05 00:31:10 +13:00
Jake Barnby 1df76ed975 Merge remote-tracking branch 'origin/1.8.x' into feat-installer
# Conflicts:
#	Dockerfile
#	app/views/install/compose.phtml
#	composer.lock
#	src/Appwrite/Platform/Tasks/Install.php
2026-02-05 00:29:29 +13:00
Jake BarnbyandGitHub 2278a423f5 Merge pull request #11175 from appwrite/new-install-ui
Add web installer
2026-02-04 11:15:20 +00:00
Darshan 282b3c74ec fix: local install path. 2026-02-04 15:49:05 +05:30
Darshan 180276bd9d remove: mock stuff.
fix: layout regression.
2026-02-04 15:48:37 +05:30
Darshan abab48c1b9 remove: mock stuff. 2026-02-04 14:48:42 +05:30
Darshan 587f9965a2 misc. 2026-02-03 14:37:29 +05:30
Darshan bdad14e83a fix: gradient. 2026-02-03 14:32:24 +05:30
Darshan 92e94e365f update: send to existing analyics endpoint. 2026-02-03 13:54:20 +05:30
Darshan 35918465f8 update: address comments, remove name field and auto-derive. 2026-02-02 19:00:54 +05:30
Darshan 42e52fcaa6 revert: handle fallbacks on growth. 2026-01-30 12:11:39 +05:30
Darshan ba29e66292 add: fallbacks. 2026-01-30 12:09:09 +05:30
Darshan d8cc3202c6 add: tracking on installations. 2026-01-30 11:44:00 +05:30
Darshan 02ce669993 update: design changes on creds step. 2026-01-30 11:08:26 +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
Darshan a2a24ee367 update: behavioural hotfix. 2026-01-26 19:27:22 +05:30
Darshan 5e0f163f1f fix: tests. 2026-01-26 19:25:08 +05:30
Darshan c9a6a0f0aa add: deps. 2026-01-26 18:57:50 +05:30
Darshan f5681f1167 fix: bun availability on CI. 2026-01-26 18:44:55 +05:30
Darshan 0e4d514007 update: docs. 2026-01-26 18:41:44 +05:30
Darshan c39d8ad591 fix: tests. 2026-01-26 18:31:16 +05:30
Darshan f59796e290 update: use helpers. 2026-01-26 18:06:36 +05:30
Darshan cff11cae5a update: use more constants. 2026-01-26 18:00:45 +05:30
Darshan aa3047f338 update: use constants for regexes. 2026-01-26 17:48:08 +05:30
Darshan e17b8cec38 update: consistent naming pattern. 2026-01-26 17:40:19 +05:30
Darshan fc2ee0c32a update: inline args. 2026-01-26 17:35:13 +05:30
Darshan 5690eb1dc5 update: filter checks. 2026-01-26 17:26:18 +05:30
Darshan fbba7166ae update: send mock/default values from backend.
update: update task callback.
2026-01-26 17:22:02 +05:30
Darshan 289c3e233e update: misc fixes. 2026-01-26 17:14:31 +05:30
Darshan 6345802b1a update: use helper method. 2026-01-26 17:06:52 +05:30
Darshan 7fb654e3a4 misc: updates, cleanups. 2026-01-26 16:10:51 +05:30
Darshan 8b9051751a update: password validation.
misc: updates, cleanups.
2026-01-26 15:26:56 +05:30
Darshan 335c3f363c fix: ci. 2026-01-25 18:07:42 +05:30
Darshan 6dac2ce253 improve: animations like imagine.
fix: password eye button not showing again.
2026-01-25 18:01:58 +05:30
Darshan f34e40e1e4 add: ci tests. 2026-01-25 17:12:55 +05:30
Darshan fe74760177 update: screenshot tests. 2026-01-25 17:06:10 +05:30
Darshan 2d5e64a21e update: screenshot tests. 2026-01-25 15:42:09 +05:30
Darshan b503965e8c update: tests. 2026-01-25 15:20:09 +05:30
Darshan a9da80687f remove: unneeded check. 2026-01-25 11:33:24 +05:30
Darshan d4dd6cf634 add: completion marker. 2026-01-25 11:33:05 +05:30
Darshan 85d2e08dc5 update: installer doc. 2026-01-25 10:51:09 +05:30
Darshan 0eda9e6315 add: account creation.
feat: mocking options.
2026-01-24 20:09:56 +05:30
Darshan eb8abcfe63 fix: empty input considered as invalid.
fix: referencing wrong file in upgrade task.
2026-01-24 16:35:14 +05:30
Darshan 80a5c24840 bump: gitignore. 2026-01-24 16:32:01 +05:30
Darshan 5144b1ce88 update: splits.
fix: broken SSE.
fix: upgrade flow not working.
feat: csrf tokens for better security.
fix: local testing mode with wrong paths.
feat: csrf validations pre-post hook plus proper permissions handling on files.
feat: js modules for better handling of js operations on web installer frontend ui.
2026-01-24 16:30:51 +05:30
Darshan 3d86f67acb update: move server handling to a new file.
update: tooltip things.
remove: unused var.
2026-01-22 19:52:46 +05:30
Darshan 58440e70ac misc. 2026-01-22 18:12:41 +05:30
Darshan dd16ead898 update: new installer ui. 2026-01-22 17:28:23 +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
Jake Barnby bdb8bc081f Add doc 2026-01-21 18:09:46 +13:00
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
Jake Barnby cbf0cc842d Add web installer 2025-12-02 23:33:21 +13:00
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
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
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
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
352 changed files with 38609 additions and 86184 deletions
+10 -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=
+1
View File
@@ -392,6 +392,7 @@ jobs:
Tokens,
Teams,
Users,
ProjectWebhooks,
Webhooks,
VCS,
Messaging,
+6
View File
@@ -21,3 +21,9 @@ appwrite.config.json
/app/config/specs/
/docs/examples/
.phpunit.cache
playwright-report
test-results
docker-compose.web-installer.yml
.env.web-installer
docker-compose.web-installer.yml.**.backup
tests/playwright/screenshots
+2 -1
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.0 AS base
FROM appwrite/base:1.0.1 AS base
LABEL maintainer="team@appwrite.io"
@@ -121,5 +121,6 @@ RUN if [ "$DEBUG" = "true" ]; then \
fi
EXPOSE 80
EXPOSE 8080
CMD [ "php", "app/http.php" ]
+6 -7
View File
@@ -167,7 +167,7 @@ $setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $c
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant((int) $project->getSequence())
->setTenant($project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
@@ -188,7 +188,7 @@ $setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $c
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant((int) $project->getSequence())
->setTenant($project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
@@ -211,9 +211,8 @@ $setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $a
$database = null;
return function (?Document $project = null) use ($pools, $cache, $database, $authorization) {
if ($database !== null && $project !== null && ! $project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant((int) $project->getSequence());
if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant($project->getSequence());
return $database;
}
@@ -229,8 +228,8 @@ $setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $a
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES);
// set tenant
if ($project !== null && ! $project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant((int) $project->getSequence());
if ($project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant($project->getSequence());
}
return $database;
+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,
+9
View File
@@ -61,6 +61,15 @@ return [
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('database'),
'type' => Database::VAR_STRING,
'size' => 128,
'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' => [],
],
]
]
];
+10
View File
@@ -1144,6 +1144,11 @@ return [
'description' => 'Webhook with the requested ID could not be found.',
'code' => 404,
],
Exception::WEBHOOK_ALREADY_EXISTS => [
'name' => Exception::WEBHOOK_ALREADY_EXISTS,
'description' => 'Webhook with the same ID already exists. Try again with a different ID.',
'code' => 409,
],
Exception::KEY_NOT_FOUND => [
'name' => Exception::KEY_NOT_FOUND,
'description' => 'Key with the requested ID could not be found.',
@@ -1201,6 +1206,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 => [
+8
View File
@@ -172,4 +172,12 @@ return [ // List of publicly visible scopes
'tokens.write' => [
'description' => 'Access to create, update, and delete your project\'s tokens',
],
"webhooks.read" => [
"description" =>
"Access to read project\'s webhooks",
],
"webhooks.write" => [
"description" =>
"Access to create, update, and delete project\'s webhooks",
],
];
+34 -7
View File
@@ -43,6 +43,16 @@ use Utopia\Validator\WhiteList;
include_once __DIR__ . '/../shared/api.php';
function getDatabaseTransferResourceServices(string $databaseType)
{
return match($databaseType) {
DATABASE_TYPE_LEGACY,
DATABASE_TYPE_TABLESDB => Transfer::GROUP_DATABASES_TABLES_DB,
DATABASE_TYPE_VECTORSDB => Transfer::GROUP_DATABASES_VECTOR_DB,
DATABASE_TYPE_DOCUMENTSDB => Transfer::GROUP_DATABASES_DOCUMENTS_DB
};
}
Http::post('/v1/migrations/appwrite')
->groups(['api', 'migrations'])
->desc('Create Appwrite migration')
@@ -427,8 +437,16 @@ Http::post('/v1/migrations/csv/imports')
throw new \Exception('Unable to copy file');
}
// getting databasetype
$resources = explode(':', $resourceId);
$databaseId = $resources[0];
$database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
$databaseType = $database->getAttribute('type');
if (!in_array($databaseType, CSV_ALLOWED_DATABASE_TYPES)) {
throw new Exception(Exception::MIGRATION_DATABASE_TYPE_UNSUPPORTED, 'Database type not supported for csv');
}
$fileSize = $deviceForMigrations->getFileSize($newPath);
$resources = Transfer::extractServices([Transfer::GROUP_DATABASES]);
$resources = Transfer::extractServices([getDatabaseTransferResourceServices($databaseType)]);
$migration = $dbForProject->createDocument('migrations', new Document([
'$id' => $migrationId,
@@ -557,13 +575,23 @@ Http::post('/v1/migrations/csv/exports')
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
}
// getting databasetype
$resources = explode(':', $resourceId);
$databaseId = $resources[0];
$database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
$databaseType = $database->getAttribute('type');
if (!in_array($databaseType, CSV_ALLOWED_DATABASE_TYPES)) {
throw new Exception(Exception::MIGRATION_DATABASE_TYPE_UNSUPPORTED, 'Database type not supported for csv');
}
$resources = Transfer::extractServices([getDatabaseTransferResourceServices($databaseType)]);
$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]),
'resources' => $resources,
'resourceId' => $resourceId,
'resourceType' => Resource::TYPE_DATABASE,
'statusCounters' => '{}',
@@ -713,12 +741,11 @@ Http::get('/v1/migrations/appwrite/report')
->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) {
->inject('getDatabasesDB')
->action(function (array $resources, string $endpoint, string $projectID, string $key, Response $response, callable $getDatabasesDB) {
try {
$appwrite = new Appwrite($projectID, $endpoint, $key);
$appwrite = new Appwrite($projectID, $endpoint, $key, $getDatabasesDB);
$report = $appwrite->report($resources);
} catch (\Throwable $e) {
throw new Exception(
+60
View File
@@ -62,16 +62,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,11 +97,26 @@ 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
]
];
@@ -357,8 +389,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 +402,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,6 +432,14 @@ 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);
});
+1 -1
View File
@@ -555,7 +555,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
}
if (!empty($deployment->getAttribute('startCommand', ''))) {
$startCommand = 'cd /usr/local/server/src/function/ && ' . $deployment->getAttribute('startCommand', '');
$startCommand = 'cd /usr/local/server/src/function/ && ' . str_replace(['"', '`', '$'], ['\\"', '\\`', '\\$'], $deployment->getAttribute('startCommand', ''));
}
$runtimeEntrypoint = match ($version) {
+7 -1
View File
@@ -215,7 +215,7 @@ Http::init()
);
}
if (! $dbKey) {
if (!$dbKey) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
@@ -470,6 +470,12 @@ Http::init()
->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) {
$route = $utopia->getRoute();
$path = $route->getMatchedPath();
$databaseType = match (true) {
str_contains($path, '/documentsdb') => DATABASE_TYPE_DOCUMENTSDB,
str_contains($path, '/vectorsdb') => DATABASE_TYPE_VECTORSDB,
default => '',
};
if (
array_key_exists('rest', $project->getAttribute('apis', []))
+19 -1
View File
@@ -196,6 +196,8 @@ include __DIR__ . '/controllers/general.php';
function createDatabase(Http $app, string $resourceKey, string $dbName, array $collections, mixed $pools, ?callable $extraSetup = null): void
{
$max = 15;
$sleep = 2;
$max = 15;
$sleep = 2;
$attempts = 0;
@@ -409,13 +411,29 @@ $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', ''));
$documentsSharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1', ''));
$documentsSharedTablesV2 = \array_diff($documentsSharedTables, $documentsSharedTablesV1);
$vectorSharedTables = \explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES', ''));
$vectorSharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1', ''));
$vectorSharedTablesV2 = \array_diff($vectorSharedTables, $vectorSharedTablesV1);
$cache = $app->getResource('cache');
foreach ($sharedTablesV2 as $hostname) {
// All shared tables V2 pools that need project metadata collections
$sharedTablesV2All = \array_values(\array_unique(\array_filter([
...$sharedTablesV2,
...$documentsSharedTablesV2,
...$vectorSharedTablesV2,
])));
foreach ($sharedTablesV2All as $hostname) {
Span::init('database.setup');
Span::add('database.hostname', $hostname);
+53
View File
@@ -288,6 +288,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 +419,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 +441,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
];
+23 -1
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;
@@ -65,6 +67,7 @@ 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\Error;
use Appwrite\Utopia\Response\Model\ErrorDev;
use Appwrite\Utopia\Response\Model\Execution;
@@ -136,6 +139,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 +149,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
@@ -185,7 +193,7 @@ Response::setModel(new BaseList('Runtimes List', Response::MODEL_RUNTIME_LIST, '
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));
@@ -211,9 +219,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 +248,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());
+31 -3
View File
@@ -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',
+189 -19
View File
@@ -32,6 +32,8 @@ use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Executor\Executor;
use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis;
use Utopia\Agents\Adapters\Ollama;
use Utopia\Agents\Agent;
use Utopia\Audit\Adapter\Database as AdapterDatabase;
use Utopia\Audit\Audit;
use Utopia\Auth\Hashes\Argon2;
@@ -555,7 +557,7 @@ Http::setResource('authorization', function () {
return new Authorization();
}, []);
Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatform, Cache $cache, Document $project, Response $response, Publisher $publisher, Publisher $publisherFunctions, Publisher $publisherWebhooks, Event $queueForEvents, Func $queueForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime, UsageContext $usage, Authorization $authorization) {
Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatform, Cache $cache, Document $project, Response $response, Publisher $publisher, Publisher $publisherFunctions, Publisher $publisherWebhooks, Event $queueForEvents, Func $queueForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime, UsageContext $usage, Authorization $authorization, Request $request) {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForPlatform;
}
@@ -589,7 +591,7 @@ Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatfor
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant((int) $project->getSequence())
->setTenant($project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
@@ -661,7 +663,31 @@ Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatfor
$dbForProject->getCache()->purge($cacheKey);
};
$usageDatabaseListener = function (string $event, Document $document, UsageContext $usage) {
/**
* Prefix metrics with database type when applicable.
* Avoids prefixing for legacy and tablesdb types to preserve historical metrics.
*/
$getDatabaseTypePrefixedMetric = function (string $databaseType, string $metric): string {
if (
$databaseType === '' ||
$databaseType === DATABASE_TYPE_LEGACY ||
$databaseType === DATABASE_TYPE_TABLESDB
) {
return $metric;
}
return $databaseType . '.' . $metric;
};
// Determine database type from request path, similar to api.php
$path = $request->getURI();
$databaseType = match (true) {
str_contains($path, '/documentsdb') => DATABASE_TYPE_DOCUMENTSDB,
str_contains($path, '/vectorsdb') => DATABASE_TYPE_VECTORSDB,
default => '',
};
$usageDatabaseListener = function (string $event, Document $document, UsageContext $usage) use ($getDatabaseTypePrefixedMetric, $databaseType) {
$value = 1;
switch ($event) {
@@ -693,7 +719,8 @@ Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatfor
$usage->addMetric(METRIC_SESSIONS, $value); // per project
break;
case $document->getCollection() === 'databases': // databases
$usage->addMetric(METRIC_DATABASES, $value); // per project
$metric = $getDatabaseTypePrefixedMetric($databaseType, METRIC_DATABASES);
$usage->addMetric($metric, $value); // per project
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$usage->addReduce($document);
@@ -702,9 +729,11 @@ Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatfor
case str_starts_with($document->getCollection(), 'database_') && ! str_contains($document->getCollection(), 'collection'): // collections
$parts = explode('_', $document->getCollection());
$databaseInternalId = $parts[1] ?? 0;
$collectionMetric = $getDatabaseTypePrefixedMetric($databaseType, METRIC_COLLECTIONS);
$databaseIdCollectionMetric = $getDatabaseTypePrefixedMetric($databaseType, METRIC_DATABASE_ID_COLLECTIONS);
$usage
->addMetric(METRIC_COLLECTIONS, $value) // per project
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_COLLECTIONS), $value);
->addMetric($collectionMetric, $value) // per project
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, $databaseIdCollectionMetric), $value);
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$usage->addReduce($document);
@@ -714,10 +743,13 @@ Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatfor
$parts = explode('_', $document->getCollection());
$databaseInternalId = $parts[1] ?? 0;
$collectionInternalId = $parts[3] ?? 0;
$documentsMetric = $getDatabaseTypePrefixedMetric($databaseType, METRIC_DOCUMENTS);
$databaseIdDocumentsMetric = $getDatabaseTypePrefixedMetric($databaseType, METRIC_DATABASE_ID_DOCUMENTS);
$databaseIdCollectionIdDocumentsMetric = $getDatabaseTypePrefixedMetric($databaseType, METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS);
$usage
->addMetric(METRIC_DOCUMENTS, $value) // per project
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_DOCUMENTS), $value) // per database
->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS), $value); // per collection
->addMetric($documentsMetric, $value) // per project
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, $databaseIdDocumentsMetric), $value) // per database
->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], $databaseIdCollectionIdDocumentsMetric), $value); // per collection
break;
case $document->getCollection() === 'buckets': // buckets
$usage->addMetric(METRIC_BUCKETS, $value); // per project
@@ -792,7 +824,7 @@ Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatfor
->on(Database::EVENT_DOCUMENT_DELETE, 'purge-function-events-cache', fn ($event, $document) => $functionsEventsCacheListener($event, $document, $project, $database));
return $database;
}, ['pools', 'dbForPlatform', 'cache', 'project', 'response', 'publisher', 'publisherFunctions', 'publisherWebhooks', 'queueForEvents', 'queueForFunctions', 'queueForWebhooks', 'queueForRealtime', 'usage', 'authorization']);
}, ['pools', 'dbForPlatform', 'cache', 'project', 'response', 'publisher', 'publisherFunctions', 'publisherWebhooks', 'queueForEvents', 'queueForFunctions', 'queueForWebhooks', 'queueForRealtime', 'usage', 'authorization', 'request']);
Http::setResource('dbForPlatform', function (Group $pools, Cache $cache, Authorization $authorization) {
@@ -813,6 +845,138 @@ Http::setResource('dbForPlatform', function (Group $pools, Cache $cache, Authori
return $database;
}, ['pools', 'cache', 'authorization']);
Http::setResource('getDatabasesDB', function (Group $pools, Cache $cache, Document $project, Request $request, UsageContext $usage, Authorization $authorization) {
return function (Document $database) use ($pools, $cache, $project, $request, $usage, $authorization): Database {
$databaseDSN = $database->getAttribute('database', $project->getAttribute('database', ''));
$databaseType = $database->getAttribute('type', '');
try {
$databaseDSN = new DSN($databaseDSN);
} catch (\InvalidArgumentException) {
// for old databases migrated through patch script
// databaseDSN determines the adapter
$databaseDSN = new DSN('mysql://'.$databaseDSN);
}
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'));
}
$pool = $pools->get($databaseDSN->getHost());
$adapter = new DatabasePool($pool);
$database = new Database($adapter, $cache);
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setMetadata('host', \gethostname())
->setMetadata('project', $project->getId())
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API)
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES);
// inside pools authorization needs to be set first
$database->getAdapter()->setSupportForAttributes($databaseType !== DOCUMENTSDB);
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant((int)$project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
->setSharedTables(false)
->setTenant(null)
->setNamespace('_' . $project->getSequence());
}
$timeout = \intval($request->getHeader('x-appwrite-timeout'));
if (!empty($timeout) && Http::isDevelopment()) {
$database->setTimeout($timeout);
}
// Register database event listeners for usage stats collection
$documentsMetric = METRIC_DOCUMENTS;
$databaseIdDocumentsMetric = METRIC_DATABASE_ID_DOCUMENTS;
$databaseIdCollectionIdDocumentsMetric = METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS;
if ($databaseType !== DATABASE_TYPE_LEGACY && $databaseType !== DATABASE_TYPE_TABLESDB) {
$documentsMetric = $databaseType. '.' .$documentsMetric;
$databaseIdDocumentsMetric = $databaseType. '.' .$databaseIdDocumentsMetric;
$databaseIdCollectionIdDocumentsMetric = $databaseType . '.' .$databaseIdCollectionIdDocumentsMetric;
}
$database
->on(Database::EVENT_DOCUMENT_CREATE, 'calculate-usage', function ($event, $document) use ($usage, $documentsMetric, $databaseIdDocumentsMetric, $databaseIdCollectionIdDocumentsMetric) {
$value = 1;
if (str_starts_with($document->getCollection(), 'database_') && str_contains($document->getCollection(), '_collection_')) {
$parts = explode('_', $document->getCollection());
$databaseInternalId = $parts[1] ?? 0;
$collectionInternalId = $parts[3] ?? 0;
$usage
->addMetric($documentsMetric, $value) // per project
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, $databaseIdDocumentsMetric), $value) // per database
->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], $databaseIdCollectionIdDocumentsMetric), $value); // per collection
}
})
->on(Database::EVENT_DOCUMENT_DELETE, 'calculate-usage', function ($event, $document) use ($usage, $documentsMetric, $databaseIdDocumentsMetric, $databaseIdCollectionIdDocumentsMetric) {
$value = -1;
if (str_starts_with($document->getCollection(), 'database_') && str_contains($document->getCollection(), '_collection_')) {
$parts = explode('_', $document->getCollection());
$databaseInternalId = $parts[1] ?? 0;
$collectionInternalId = $parts[3] ?? 0;
$usage
->addMetric($documentsMetric, $value) // per project
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, $databaseIdDocumentsMetric), $value) // per database
->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], $databaseIdCollectionIdDocumentsMetric), $value); // per collection
}
})
->on(Database::EVENT_DOCUMENTS_CREATE, 'calculate-usage', function ($event, $document) use ($usage, $documentsMetric, $databaseIdDocumentsMetric, $databaseIdCollectionIdDocumentsMetric) {
$value = $document->getAttribute('modified', 0);
if (str_starts_with($document->getCollection(), 'database_') && str_contains($document->getCollection(), '_collection_')) {
$parts = explode('_', $document->getCollection());
$databaseInternalId = $parts[1] ?? 0;
$collectionInternalId = $parts[3] ?? 0;
$usage
->addMetric($documentsMetric, $value) // per project
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, $databaseIdDocumentsMetric), $value) // per database
->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], $databaseIdCollectionIdDocumentsMetric), $value); // per collection
}
})
->on(Database::EVENT_DOCUMENTS_DELETE, 'calculate-usage', function ($event, $document) use ($usage, $documentsMetric, $databaseIdDocumentsMetric, $databaseIdCollectionIdDocumentsMetric) {
$value = -1 * $document->getAttribute('modified', 0);
if (str_starts_with($document->getCollection(), 'database_') && str_contains($document->getCollection(), '_collection_')) {
$parts = explode('_', $document->getCollection());
$databaseInternalId = $parts[1] ?? 0;
$collectionInternalId = $parts[3] ?? 0;
$usage
->addMetric($documentsMetric, $value) // per project
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, $databaseIdDocumentsMetric), $value) // per database
->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], $databaseIdCollectionIdDocumentsMetric), $value); // per collection
}
})
->on(Database::EVENT_DOCUMENTS_UPSERT, 'calculate-usage', function ($event, $document) use ($usage, $documentsMetric, $databaseIdDocumentsMetric, $databaseIdCollectionIdDocumentsMetric) {
$value = $document->getAttribute('created', 0);
if (str_starts_with($document->getCollection(), 'database_') && str_contains($document->getCollection(), '_collection_')) {
$parts = explode('_', $document->getCollection());
$databaseInternalId = $parts[1] ?? 0;
$collectionInternalId = $parts[3] ?? 0;
$usage
->addMetric($documentsMetric, $value) // per project
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, $databaseIdDocumentsMetric), $value) // per database
->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], $databaseIdCollectionIdDocumentsMetric), $value); // per collection
}
});
return $database;
};
}, ['pools','cache','project','request','usage','authorization']);
Http::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache, Authorization $authorization) {
$databases = [];
@@ -848,7 +1012,7 @@ Http::setResource('getProjectDB', function (Group $pools, Database $dbForPlatfor
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant((int) $project->getSequence())
->setTenant($project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
@@ -878,9 +1042,8 @@ Http::setResource('getLogsDB', function (Group $pools, Cache $cache, Authorizati
$database = null;
return function (?Document $project = null) use ($pools, $cache, $authorization, &$database) {
if ($database !== null && $project !== null && ! $project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant((int) $project->getSequence());
if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant($project->getSequence());
return $database;
}
@@ -896,8 +1059,8 @@ Http::setResource('getLogsDB', function (Group $pools, Cache $cache, Authorizati
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES);
// set tenant
if ($project !== null && ! $project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant((int) $project->getSequence());
if ($project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant($project->getSequence());
}
return $database;
@@ -1442,9 +1605,9 @@ Http::setResource('resourceToken', function ($project, $dbForProject, $request,
return new Document([]);
}, ['project', 'dbForProject', 'request', 'authorization']);
Http::setResource('transactionState', function (Database $dbForProject, Authorization $authorization) {
return new TransactionState($dbForProject, $authorization);
}, ['dbForProject', 'authorization']);
Http::setResource('transactionState', function (Database $dbForProject, Authorization $authorization, callable $getDatabasesDB) {
return new TransactionState($dbForProject, $authorization, $getDatabasesDB);
}, ['dbForProject', 'authorization', 'getDatabasesDB']);
Http::setResource('executionsRetentionCount', function (Document $project, array $plan) {
if ($project->getId() === 'console' || empty($plan)) {
@@ -1453,3 +1616,10 @@ Http::setResource('executionsRetentionCount', function (Document $project, array
return (int) ($plan['executionsRetentionCount'] ?? 100);
}, ['project', 'plan']);
Http::setResource('embeddingAgent', function ($register) {
$adapter = new Ollama();
$adapter->setEndpoint(System::getEnv('_APP_EMBEDDING_ENDPOINT', 'http://ollama:11434/api/embed'));
$adapter->setTimeout((int) System::getEnv('_APP_EMBEDDING_TIMEOUT', '30000'));
return new Agent($adapter);
}, ['register']);
+24 -8
View File
@@ -50,6 +50,16 @@ require_once __DIR__ . '/init.php';
Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
// Log uncaught exceptions in one line instead of relying on Swoole's full backtrace dump
set_exception_handler(function (\Throwable $e) {
Console::error(sprintf(
'Realtime uncaught exception: %s in %s:%d',
$e->getMessage(),
$e->getFile(),
$e->getLine()
));
});
// Allows overriding
if (!function_exists('getConsoleDB')) {
function getConsoleDB(): Database
@@ -115,7 +125,7 @@ if (!function_exists('getProjectDB')) {
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant((int)$project->getSequence())
->setTenant($project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
@@ -985,15 +995,21 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
});
$server->onClose(function (int $connection) use ($realtime, $stats, $register) {
if (array_key_exists($connection, $realtime->connections)) {
$stats->decr($realtime->connections[$connection]['projectId'], 'connectionsTotal');
$register->get('telemetry.connectionCounter')->add(-1);
try {
if (array_key_exists($connection, $realtime->connections)) {
$stats->decr($realtime->connections[$connection]['projectId'], 'connectionsTotal');
$register->get('telemetry.connectionCounter')->add(-1);
$projectId = $realtime->connections[$connection]['projectId'];
$projectId = $realtime->connections[$connection]['projectId'];
triggerStats([
METRIC_REALTIME_CONNECTIONS => -1,
], $projectId);
triggerStats([
METRIC_REALTIME_CONNECTIONS => -1,
], $projectId);
}
} 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());
}
$realtime->unsubscribe($connection);
+106 -43
View File
@@ -12,8 +12,12 @@ $version = $this->getParam('version', '');
$organization = $this->getParam('organization', '');
$image = $this->getParam('image', '');
$enableAssistant = $this->getParam('enableAssistant', false);
$dbService = $this->getParam('database');
$dbService = $this->getParam('database', 'mongodb');
$allowedDbServices = ['mariadb', 'mongodb', 'postgresql'];
if (!\in_array($dbService, $allowedDbServices, true)) {
$dbService = 'mongodb';
}
$hostPath = rtrim($this->getParam('hostPath', ''), '/');
?>services:
traefik:
image: traefik:3.6
@@ -63,6 +67,9 @@ $dbService = $this->getParam('database');
- traefik.http.routers.appwrite_api_https.service=appwrite_api
- traefik.http.routers.appwrite_api_https.tls=true
volumes:
<?php if ($version === 'local' && !empty($hostPath)): ?>
- "<?php echo $hostPath; ?>:/usr/src/code:rw"
<?php endif; ?>
- appwrite-uploads:/storage/uploads:rw
- appwrite-imports:/storage/imports:rw
- appwrite-cache:/storage/cache:rw
@@ -72,8 +79,10 @@ $dbService = $this->getParam('database');
- appwrite-sites:/storage/sites:rw
- appwrite-builds:/storage/builds:rw
depends_on:
- redis
- <?= $dbService . "\n" ?>
redis:
condition: service_healthy
<?= $dbService ?>:
condition: service_healthy
# - clamav
environment:
- _APP_ENV
@@ -227,8 +236,10 @@ $dbService = $this->getParam('database');
networks:
- appwrite
depends_on:
- redis
- <?= $dbService . "\n" ?>
redis:
condition: service_healthy
<?= $dbService ?>:
condition: service_healthy
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -258,8 +269,10 @@ $dbService = $this->getParam('database');
networks:
- appwrite
depends_on:
- redis
- <?= $dbService . "\n" ?>
redis:
condition: service_healthy
<?= $dbService ?>:
condition: service_healthy
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -286,8 +299,10 @@ $dbService = $this->getParam('database');
networks:
- appwrite
depends_on:
- redis
- <?= $dbService . "\n" ?>
redis:
condition: service_healthy
<?= $dbService ?>:
condition: service_healthy
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -316,8 +331,10 @@ $dbService = $this->getParam('database');
networks:
- appwrite
depends_on:
- redis
- <?= $dbService . "\n" ?>
redis:
condition: service_healthy
<?= $dbService ?>:
condition: service_healthy
volumes:
- appwrite-uploads:/storage/uploads:rw
- appwrite-cache:/storage/cache:rw
@@ -381,8 +398,10 @@ $dbService = $this->getParam('database');
networks:
- appwrite
depends_on:
- redis
- <?= $dbService . "\n" ?>
redis:
condition: service_healthy
<?= $dbService ?>:
condition: service_healthy
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -409,8 +428,10 @@ $dbService = $this->getParam('database');
networks:
- appwrite
depends_on:
- redis
- <?= $dbService . "\n" ?>
redis:
condition: service_healthy
<?= $dbService ?>:
condition: service_healthy
volumes:
- appwrite-functions:/storage/functions:rw
- appwrite-sites:/storage/sites:rw
@@ -479,8 +500,10 @@ $dbService = $this->getParam('database');
networks:
- appwrite
depends_on:
- redis
- <?= $dbService . "\n" ?>
redis:
condition: service_healthy
<?= $dbService ?>:
condition: service_healthy
volumes:
- appwrite-config:/storage/config:rw
- appwrite-certificates:/storage/certificates:rw
@@ -518,9 +541,12 @@ $dbService = $this->getParam('database');
networks:
- appwrite
depends_on:
- redis
- <?= $dbService . "\n" ?>
- openruntimes-executor
redis:
condition: service_healthy
<?= $dbService ?>:
condition: service_healthy
openruntimes-executor:
condition: service_started
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -559,8 +585,10 @@ $dbService = $this->getParam('database');
networks:
- appwrite
depends_on:
- redis
- <?= $dbService . "\n" ?>
redis:
condition: service_healthy
<?= $dbService ?>:
condition: service_healthy
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -598,8 +626,10 @@ $dbService = $this->getParam('database');
volumes:
- appwrite-uploads:/storage/uploads:rw
depends_on:
- redis
- <?= $dbService . "\n" ?>
redis:
condition: service_healthy
<?= $dbService ?>:
condition: service_healthy
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -652,7 +682,8 @@ $dbService = $this->getParam('database');
volumes:
- appwrite-imports:/storage/imports:rw
depends_on:
- <?= $dbService . "\n" ?>
<?= $dbService ?>:
condition: service_healthy
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -688,8 +719,10 @@ $dbService = $this->getParam('database');
networks:
- appwrite
depends_on:
- redis
- <?= $dbService . "\n" ?>
redis:
condition: service_healthy
<?= $dbService ?>:
condition: service_healthy
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -730,8 +763,10 @@ $dbService = $this->getParam('database');
networks:
- appwrite
depends_on:
- redis
- <?= $dbService . "\n" ?>
redis:
condition: service_healthy
<?= $dbService ?>:
condition: service_healthy
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -761,8 +796,10 @@ $dbService = $this->getParam('database');
networks:
- appwrite
depends_on:
- redis
- <?= $dbService . "\n" ?>
redis:
condition: service_healthy
<?= $dbService ?>:
condition: service_healthy
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -791,8 +828,10 @@ $dbService = $this->getParam('database');
networks:
- appwrite
depends_on:
- redis
- <?= $dbService . "\n" ?>
redis:
condition: service_healthy
<?= $dbService ?>:
condition: service_healthy
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -821,8 +860,10 @@ $dbService = $this->getParam('database');
networks:
- appwrite
depends_on:
- <?= $dbService . "\n" ?>
- redis
<?= $dbService ?>:
condition: service_healthy
redis:
condition: service_healthy
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -848,8 +889,10 @@ $dbService = $this->getParam('database');
networks:
- appwrite
depends_on:
- <?= $dbService . "\n" ?>
- redis
<?= $dbService ?>:
condition: service_healthy
redis:
condition: service_healthy
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -875,8 +918,10 @@ $dbService = $this->getParam('database');
networks:
- appwrite
depends_on:
- <?= $dbService . "\n" ?>
- redis
<?= $dbService ?>:
condition: service_healthy
redis:
condition: service_healthy
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -966,7 +1011,6 @@ $dbService = $this->getParam('database');
- OPR_EXECUTOR_STORAGE_WASABI_BUCKET=$_APP_STORAGE_WASABI_BUCKET
<?php if ($dbService === 'mariadb'): ?>
mariadb:
image: mariadb:10.11
container_name: appwrite-mariadb
@@ -982,6 +1026,12 @@ $dbService = $this->getParam('database');
- MYSQL_PASSWORD=${_APP_DB_PASS}
- MARIADB_AUTO_UPGRADE=1
command: 'mysqld --innodb-flush-method=fsync'
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
timeout: 10s
retries: 10
start_period: 30s
<?php elseif ($dbService === 'mongodb'): ?>
@@ -1059,18 +1109,24 @@ $dbService = $this->getParam('database');
<?php elseif ($dbService === 'postgresql'): ?>
postgresql:
image: postgres:18
image: appwrite/postgres:0.1.0
container_name: appwrite-postgresql
restart: unless-stopped
networks:
- appwrite
volumes:
- appwrite-postgresql:/var/lib/postgresql/data:rw
- appwrite-postgresql:/var/lib/postgresql:rw
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} -d ${_APP_DB_SCHEMA}"]
interval: 10s
timeout: 10s
retries: 10
start_period: 30s
<?php endif; ?>
@@ -1088,6 +1144,12 @@ $dbService = $this->getParam('database');
- appwrite
volumes:
- appwrite-redis:/data:rw
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
# clamav:
# image: appwrite/clamav:1.2.0
@@ -1114,6 +1176,7 @@ volumes:
<?php elseif ($dbService === 'mongodb'): ?>
appwrite-mongodb:
appwrite-mongodb-keyfile:
appwrite-mongodb-config:
<?php endif; ?>
appwrite-redis:
appwrite-cache:
+5 -1
View File
@@ -3,6 +3,10 @@
$vars = $this->getParam('vars');
foreach ($vars as $key => $value) {
echo $key.'='.$value."\n";
if ($value === null || $value === '') {
echo $key . "=\n";
} else {
echo $key . '="' . addcslashes((string) $value, '"\\$`') . '"' . "\n";
}
}
?>
+145
View File
@@ -0,0 +1,145 @@
<?php
$isUpgrade = $isUpgrade ?? false;
$lockedDatabase = $lockedDatabase ?? null;
$vars = $vars ?? [];
$defaultHttpPort = $defaultHttpPort ?? '80';
$defaultHttpsPort = $defaultHttpsPort ?? '443';
$defaultAppDomain = $vars['_APP_DOMAIN']['default'] ?? 'localhost';
$defaultAppDomain = ($defaultAppDomain === 'traefik') ? 'localhost' : $defaultAppDomain;
$defaultEmailCertificates = $defaultEmailCertificates ?? '';
$defaultDatabase = $vars['_APP_DB_ADAPTER']['default'] ?? 'mongodb';
$lockedDatabase = $isUpgrade && empty($lockedDatabase) ? $defaultDatabase : $lockedDatabase;
$isLocalInstall = $isLocalInstall ?? false;
$cardStep = min(4, $step);
$stepFile = __DIR__ . "/installer/templates/steps/step-{$cardStep}.phtml";
if (!is_file($stepFile)) {
$stepFile = __DIR__ . "/installer/templates/steps/step-1.phtml";
}
$cssVersion = @filemtime(__DIR__ . '/installer/css/styles.css') ?: time();
$tooltipsVersion = @filemtime(__DIR__ . '/installer/js/tooltips.js') ?: time();
$constantsVersion = @filemtime(__DIR__ . '/installer/js/constants.js') ?: time();
$stepsContextVersion = @filemtime(__DIR__ . '/installer/js/modules/context.js') ?: time();
$stepsStateVersion = @filemtime(__DIR__ . '/installer/js/modules/state.js') ?: time();
$stepsValidationVersion = @filemtime(__DIR__ . '/installer/js/modules/validation.js') ?: time();
$stepsUiVersion = @filemtime(__DIR__ . '/installer/js/modules/ui.js') ?: time();
$stepsToastVersion = @filemtime(__DIR__ . '/installer/js/modules/toast.js') ?: time();
$stepsProgressVersion = @filemtime(__DIR__ . '/installer/js/modules/progress.js') ?: time();
$stepsVersion = @filemtime(__DIR__ . '/installer/js/steps.js') ?: time();
$installerVersion = @filemtime(__DIR__ . '/installer/js/installer.js') ?: time();
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo $isUpgrade ? 'Appwrite Update' : 'Appwrite Installation'; ?></title>
<meta name="appwrite-installer-csrf" content="<?php echo htmlspecialchars($csrfToken ?? '', ENT_QUOTES); ?>">
<link rel="icon" type="image/svg+xml" href="installer/icons/appwrite-mark.svg">
<link rel="stylesheet" href="installer/css/styles.css?v=<?php echo $cssVersion; ?>">
<script src="installer/js/tooltips.js?v=<?php echo $tooltipsVersion; ?>" defer></script>
<script src="installer/js/constants.js?v=<?php echo $constantsVersion; ?>" defer></script>
<!-- Load context first; other modules depend on InstallerStepsContext. -->
<script src="installer/js/modules/context.js?v=<?php echo $stepsContextVersion; ?>" defer></script>
<script src="installer/js/modules/state.js?v=<?php echo $stepsStateVersion; ?>" defer></script>
<script src="installer/js/modules/validation.js?v=<?php echo $stepsValidationVersion; ?>" defer></script>
<script src="installer/js/modules/ui.js?v=<?php echo $stepsUiVersion; ?>" defer></script>
<script src="installer/js/modules/toast.js?v=<?php echo $stepsToastVersion; ?>" defer></script>
<script src="installer/js/modules/progress.js?v=<?php echo $stepsProgressVersion; ?>" defer></script>
<script src="installer/js/steps.js?v=<?php echo $stepsVersion; ?>" defer></script>
<script src="installer/js/installer.js?v=<?php echo $installerVersion; ?>" defer></script>
</head>
<body
class="installer-page"
data-step="<?php echo $step; ?>"
data-upgrade="<?php echo $isUpgrade ? 'true' : 'false'; ?>"
<?php if (!empty($lockedDatabase)) { ?>
data-locked-database="<?php echo htmlspecialchars((string) $lockedDatabase, ENT_QUOTES, 'UTF-8'); ?>"
<?php } ?>
data-default-http-port="<?php echo htmlspecialchars((string) $defaultHttpPort, ENT_QUOTES, 'UTF-8'); ?>"
data-default-https-port="<?php echo htmlspecialchars((string) $defaultHttpsPort, ENT_QUOTES, 'UTF-8'); ?>"
data-default-app-domain="<?php echo htmlspecialchars((string) $defaultAppDomain, ENT_QUOTES, 'UTF-8'); ?>"
data-default-email-certificates="<?php echo htmlspecialchars((string) $defaultEmailCertificates, ENT_QUOTES, 'UTF-8'); ?>"
data-default-secret-key=""
data-default-assistant-openai-key=""
data-default-database="<?php echo htmlspecialchars((string) $defaultDatabase, ENT_QUOTES, 'UTF-8'); ?>"
<?php if ($isLocalInstall) { ?>
data-dev-mode="true"
<?php } ?>
>
<div class="installer-backdrop" aria-hidden="true">
<span class="installer-gradients">
<span class="installer-blob blob-one">
<?php include __DIR__ . '/installer/icons/install-bg-1.svg'; ?>
</span>
<span class="installer-blob blob-two">
<?php include __DIR__ . '/installer/icons/install-bg-2.svg'; ?>
</span>
</span>
</div>
<main class="installer-main">
<div class="installer-card" data-step="<?php echo $step; ?>">
<div class="installer-step">
<div class="step-panel is-active">
<?php include $stepFile; ?>
</div>
</div>
<div class="action-shell">
<div class="divider"></div>
<div class="action-bar">
<button class="button secondary" type="button" data-action="back">
<span class="button-text typography-text-m-500">Back</span>
</button>
<div class="step-indicators" aria-hidden="true">
<span class="step-indicator" data-step="1">
<span class="indicator-active"><?php include __DIR__ . '/installer/icons/indicator-active.svg'; ?></span>
<span class="indicator-inactive"><?php include __DIR__ . '/installer/icons/indicator-inactive.svg'; ?></span>
</span>
<span class="step-indicator" data-step="2">
<span class="indicator-active"><?php include __DIR__ . '/installer/icons/indicator-active.svg'; ?></span>
<span class="indicator-inactive"><?php include __DIR__ . '/installer/icons/indicator-inactive.svg'; ?></span>
</span>
<span class="step-indicator" data-step="3">
<span class="indicator-active"><?php include __DIR__ . '/installer/icons/indicator-active.svg'; ?></span>
<span class="indicator-inactive"><?php include __DIR__ . '/installer/icons/indicator-inactive.svg'; ?></span>
</span>
<span class="step-indicator" data-step="4">
<span class="indicator-active"><?php include __DIR__ . '/installer/icons/indicator-active.svg'; ?></span>
<span class="indicator-inactive"><?php include __DIR__ . '/installer/icons/indicator-inactive.svg'; ?></span>
</span>
</div>
<button class="button primary" type="button" data-action="next">
<span class="button-text typography-text-m-500">Next</span>
</button>
</div>
</div>
</div>
<div class="install-screen" aria-live="polite">
<div class="install-screen-content">
<?php if ($step === 5) { include __DIR__ . '/installer/templates/steps/step-5.phtml'; } ?>
</div>
</div>
</main>
<footer class="installer-footer">
<div class="appwrite-logo">
<?php include __DIR__ . '/installer/icons/appwrite-logo.svg'; ?>
</div>
</footer>
<template id="field-error-template">
<div class="field-error typography-caption-400 text-error">
<span class="field-error-icon">
<?php include __DIR__ . '/installer/icons/exclamation-circle.svg'; ?>
</span>
<span class="field-error-text"></span>
</div>
</template>
<div id="installer-toast-stack" class="installer-toast-stack" aria-live="polite" aria-atomic="true"></div>
</body>
</html>
File diff suppressed because it is too large Load Diff
@@ -0,0 +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="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>

After

Width:  |  Height:  |  Size: 4.0 KiB

@@ -0,0 +1,4 @@
<svg width="25" height="22" viewBox="0 0 25 22" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<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>

After

Width:  |  Height:  |  Size: 980 B

@@ -0,0 +1,3 @@
<svg class="accordion-chevron" data-open="false" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.29289 7.29289C5.68342 6.90237 6.31658 6.90237 6.70711 7.29289L10 10.5858L13.2929 7.29289C13.6834 6.90237 14.3166 6.90237 14.7071 7.29289C15.0976 7.68342 15.0976 8.31658 14.7071 8.70711L10.7071 12.7071C10.3166 13.0976 9.68342 13.0976 9.29289 12.7071L5.29289 8.70711C4.90237 8.31658 4.90237 7.68342 5.29289 7.29289Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 536 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20" aria-hidden="true">
<path fill="currentColor" d="M7 9a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2H9a2 2 0 0 1-2-2z"/>
<path fill="currentColor" d="M5 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2V5h8a2 2 0 0 0-2-2z"/>
</svg>

After

Width:  |  Height:  |  Size: 293 B

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20" aria-hidden="true">
<path fill="currentColor" fill-rule="evenodd" d="M18 10a8 8 0 1 1-16 0 8 8 0 0 1 16 0m-7 4a1 1 0 1 1-2 0 1 1 0 0 1 2 0m-1-9a1 1 0 0 0-1 1v4a1 1 0 1 0 2 0V6a1 1 0 0 0-1-1" clip-rule="evenodd"/>
</svg>

After

Width:  |  Height:  |  Size: 296 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
<path fill="currentColor" fill-rule="evenodd" d="M3.707 2.293a1 1 0 1 0-1.414 1.414l14 14a1 1 0 1 0 1.414-1.414l-1.473-1.473A10 10 0 0 0 19.543 10C18.268 5.943 14.478 3 10 3a9.96 9.96 0 0 0-4.512 1.074zm4.261 4.26 1.514 1.515a2.003 2.003 0 0 1 2.45 2.45l1.515 1.514a4 4 0 0 0-5.478-5.478" clip-rule="evenodd"/>
<path fill="currentColor" d="M12.454 16.697 9.75 13.992a4 4 0 0 1-3.742-3.742L2.335 6.578A10 10 0 0 0 .458 10c1.274 4.057 5.065 7 9.542 7 .847 0 1.67-.105 2.454-.303"/>
</svg>

After

Width:  |  Height:  |  Size: 568 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
<path fill="currentColor" d="M10 12a2 2 0 1 0 0-4 2 2 0 0 0 0 4"/>
<path fill="currentColor" fill-rule="evenodd" d="M.457 10C1.732 5.943 5.522 3 10 3s8.267 2.943 9.542 7c-1.275 4.057-5.065 7-9.542 7-4.478 0-8.268-2.943-9.543-7M14 10a4 4 0 1 1-8 0 4 4 0 0 1 8 0" clip-rule="evenodd"/>
</svg>

After

Width:  |  Height:  |  Size: 372 B

@@ -0,0 +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="#FD366E"/>
</svg>

After

Width:  |  Height:  |  Size: 261 B

@@ -0,0 +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"/>
</svg>

After

Width:  |  Height:  |  Size: 261 B

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20" aria-hidden="true">
<path fill="currentColor" fill-rule="evenodd" d="M18 10a8 8 0 1 1-16 0 8 8 0 0 1 16 0m-7-4a1 1 0 1 1-2 0 1 1 0 0 1 2 0M9 9a1 1 0 1 0 0 2v3a1 1 0 0 0 1 1h1a1 1 0 1 0 0-2v-3a1 1 0 0 0-1-1z" clip-rule="evenodd"/>
</svg>

After

Width:  |  Height:  |  Size: 313 B

@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" width="408" height="353" viewBox="0 0 408 353" fill="none" aria-hidden="true">
<g filter="url(#filter0_f_3091_1065)">
<path d="M216.4 88.698C162.554 64.5014 104.764 118.126 82.6001 147.963C90.6227 156.819 114.663 172.323 146.645 163.495C186.622 152.459 191.109 162.269 229.454 181.887C267.799 201.506 268.207 261.18 291.051 268.537C313.895 275.894 322.054 244.422 324.501 193.74C326.949 143.058 283.709 118.944 216.4 88.698Z" fill="url(#paint0_radial_3091_1065)"/>
</g>
<defs>
<filter id="filter0_f_3091_1065" x="9.91821e-05" y="-2.28882e-05" width="407.2" height="352.2" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="41.3" result="effect1_foregroundBlur_3091_1065"/>
</filter>
<radialGradient id="paint0_radial_3091_1065" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(203.6 176.1) rotate(-90) scale(93.5 121)">
<stop stop-color="#FE9567"/>
<stop offset="1" stop-color="#FE9567" stop-opacity="0"/>
</radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,16 @@
<svg width="569" height="367" viewBox="0 0 569 367" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<g filter="url(#install-bg-2-filter)">
<path d="M136.265 227.973C89.0703 207.713 99.6672 144.647 110.865 115.646C130.119 111.289 176.904 102.33 210.005 101.35C251.382 100.125 288.253 121.773 333.317 135.252C378.381 148.731 466.051 125.449 467.28 160.577C468.509 195.704 412.793 254.522 402.142 242.269C391.49 230.015 319.798 227.973 246.057 254.522C172.316 281.072 195.257 253.297 136.265 227.973Z" fill="url(#install-bg-2-paint)"/>
</g>
<defs>
<filter id="install-bg-2-filter" x="-0.000198364" y="0.0000457764" width="568.6" height="366.6" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="50.65" result="effect1_foregroundBlur_install_bg_2"/>
</filter>
<radialGradient id="install-bg-2-paint" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(284.3 183.3) rotate(-90) scale(82 183)">
<stop stop-color="#FD366E"/>
<stop offset="1" stop-color="#FD366E" stop-opacity="0"/>
</radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<circle class="check-bg" cx="8" cy="8" r="7" fill="var(--fgcolor-neutral-primary)"/>
<path class="check-mark" d="M5 8.2L7.2 10.2L11.2 6.2" fill="none" stroke="var(--fgcolor-on-invert)" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 383 B

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path class="spinner-ring" opacity="0.2" d="M14.4001 7.99998C14.4001 11.5346 11.5347 14.4 8.0001 14.4C4.46548 14.4 1.6001 11.5346 1.6001 7.99998C1.6001 4.46535 4.46548 1.59998 8.0001 1.59998C11.5347 1.59998 14.4001 4.46535 14.4001 7.99998ZM2.8801 7.99998C2.8801 10.8277 5.1724 13.12 8.0001 13.12C10.8278 13.12 13.1201 10.8277 13.1201 7.99998C13.1201 5.17228 10.8278 2.87998 8.0001 2.87998C5.1724 2.87998 2.8801 5.17228 2.8801 7.99998Z" fill="var(--fgcolor-neutral-secondary)"/>
<path class="spinner-arc" d="M4.32921 2.7574C5.15654 2.1781 6.10923 1.80282 7.10939 1.66226C8.10955 1.5217 9.12877 1.61984 10.0837 1.94866C11.0387 2.27748 11.9023 2.82764 12.6039 3.55416C13.3055 4.28069 13.8252 5.16294 14.1204 6.1288C14.4157 7.09465 14.4782 8.11668 14.3029 9.11133C14.1275 10.106 13.7192 11.045 13.1114 11.8516C12.5035 12.6582 11.7134 13.3095 10.8057 13.7523C9.8979 14.195 8.89823 14.4166 7.8884 14.399L7.91074 13.1192C8.71861 13.1333 9.51834 12.956 10.2446 12.6018C10.9708 12.2476 11.6029 11.7266 12.0891 11.0813C12.5754 10.436 12.902 9.68477 13.0423 8.88906C13.1826 8.09334 13.1326 7.27572 12.8964 6.50303C12.6601 5.73035 12.2444 5.02454 11.6831 4.44333C11.1218 3.86211 10.431 3.42198 9.66701 3.15892C8.90304 2.89586 8.08766 2.81735 7.28753 2.9298C6.48741 3.04225 5.72525 3.34247 5.06339 3.80592L4.32921 2.7574Z" fill="var(--fgcolor-neutral-secondary)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path fill="currentColor" fill-rule="evenodd" d="M5 9V7a5 5 0 1 1 10 0v2a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2m8-2v2H7V7a3 3 0 1 1 6 0" clip-rule="evenodd"/>
</svg>

After

Width:  |  Height:  |  Size: 309 B

@@ -0,0 +1,10 @@
<svg width="34" height="34" viewBox="0 0 34 34" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" y="0.5" width="33" height="33" rx="8.5" fill="white" stroke="#EDEDF0"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M28.3208 9.43304C27.9548 9.4451 28.0607 9.61 27.2684 9.80574C26.468 10.0028 25.5 9.88216 24.6393 10.2441C22.387 11.188 22.0224 14.8372 19.3397 16.249C17.5807 17.2424 15.7869 17.4703 14.1835 17.973C12.8965 18.4892 12.0961 18.8311 11.1523 19.6448C10.4203 20.2763 10.242 20.889 9.47647 21.6813C8.70022 22.7364 5.75747 21.7993 5 23.1225C5.39952 23.3813 5.6301 23.4523 6.33127 23.3598C6.18647 23.6346 5.26411 23.9966 5.43169 24.4015C5.43169 24.4015 7.66121 24.8077 9.54081 23.6735C10.4176 23.3169 11.2394 22.5621 12.6015 22.3811C14.3658 22.1465 16.354 22.7512 18.487 22.9214C18.046 23.7928 17.5915 24.3077 17.1048 25.0263C16.9547 25.1885 17.2335 25.3319 17.751 25.2341C18.6814 25.0035 19.3558 24.7541 20.0248 24.2916C20.8975 23.6896 21.2729 23.1386 22.009 22.2658C22.6484 23.2914 24.9034 23.5167 25.366 22.6305C24.5053 22.2658 24.3216 20.3688 24.6165 19.5497C24.9651 18.7694 25.2158 17.666 25.4974 16.6404C25.7507 15.7167 25.9076 14.3077 26.2106 13.5864C26.5726 12.6895 27.2764 12.4093 27.8046 11.9334C28.3328 11.4574 28.8584 11.0606 28.8423 9.97198C28.8369 9.61805 28.6546 9.42231 28.3208 9.43304Z" fill="#1F305F"/>
<path d="M5.673 24.2137C7.05522 24.2553 7.43061 24.2191 8.52324 23.7445C9.45232 23.341 10.6951 22.2537 11.7931 21.9024C13.4046 21.3849 15.1327 21.4627 16.8474 21.6651C17.4212 21.7335 17.9977 21.8287 18.4093 21.7844C19.0514 21.3903 19.0823 20.3204 19.4805 20.2346C19.3719 22.3046 18.4843 23.6292 17.5968 24.8304C19.467 24.5006 20.7165 23.3329 21.4727 21.8877C21.7019 21.4493 22.0827 20.732 22.257 20.2387C22.395 20.5658 22.0773 20.7736 22.2261 21.1302C23.43 20.1368 23.9985 19.0053 24.4865 17.3482C25.0522 15.4311 25.6327 13.7834 25.9974 13.2163C26.3527 12.6626 26.9064 12.3207 27.4118 11.9668C27.9856 11.5633 28.4991 11.1436 28.5875 10.3754C27.9816 10.3191 27.8421 10.1797 27.7523 9.87402C27.4493 10.0443 27.1705 10.0818 26.8554 10.0912C26.5819 10.0993 26.2816 10.0872 25.9143 10.1247C22.8804 10.4358 22.7396 13.4013 20.5342 15.5785C20.3921 15.7166 20.1494 15.9204 19.9886 16.0411C19.3075 16.5492 18.5782 16.828 17.81 17.1216C16.5658 17.5962 15.3861 17.7343 14.2197 18.1446C13.363 18.4449 12.5667 18.7894 11.8628 19.2707C11.6872 19.3914 11.4486 19.6059 11.2957 19.7306C10.8828 20.0684 10.612 20.4424 10.3492 20.8286C10.0784 21.2254 9.81831 21.6343 9.42014 22.0244C8.77528 22.6572 6.36746 22.2094 5.51882 22.7966C5.42497 22.8623 5.34855 22.9401 5.29761 23.0339C5.76014 23.2444 6.06982 23.1157 6.60207 23.1734C6.66776 23.6815 5.50005 23.9818 5.673 24.2137Z" fill="#C0765A"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M24.9033 11.3797C25.3323 11.7524 26.2333 11.4534 26.0724 10.712C25.4047 10.6557 25.0186 10.8823 24.9033 11.3797Z" fill="#1F305F"/>
<path d="M27.8971 10.5123C27.7831 10.7523 27.5646 11.0606 27.5646 11.6706C27.5632 11.7752 27.4855 11.8476 27.4841 11.6854C27.4895 11.0888 27.6477 10.8314 27.8153 10.4935C27.8917 10.3554 27.9386 10.4117 27.8971 10.5123Z" fill="#1F305F"/>
<path d="M27.7818 10.4225C27.6464 10.6504 27.322 11.0674 27.2697 11.676C27.2603 11.7806 27.1745 11.845 27.1879 11.6841C27.2469 11.0915 27.5057 10.7202 27.7028 10.3971C27.7912 10.2657 27.8328 10.326 27.7818 10.4225Z" fill="#1F305F"/>
<path d="M27.6772 10.303C27.523 10.5189 27.0243 11.0203 26.9197 11.6209C26.901 11.7241 26.8112 11.7804 26.838 11.6222C26.9466 11.0364 27.3769 10.5752 27.5995 10.2708C27.6987 10.1462 27.7362 10.2105 27.6772 10.303Z" fill="#1F305F"/>
<path d="M27.5835 10.1688C27.4012 10.3619 26.8059 11 26.6182 11.5805C26.5847 11.6797 26.4882 11.724 26.5365 11.5698C26.7255 11.0041 27.247 10.3954 27.5111 10.1259C27.6278 10.0173 27.6546 10.087 27.5835 10.1688Z" fill="#1F305F"/>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

@@ -0,0 +1,6 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="32" height="32" rx="8" fill="#081E2B"/>
<g transform="translate(11.4, 6.1)">
<path d="M6.03701 2.19958C5.24603 1.21687 4.56489 0.218806 4.42574 0.0115161C4.41109 -0.00383871 4.38911 -0.00383871 4.37447 0.0115161C4.23531 0.218806 3.55418 1.21687 2.76319 2.19958C-4.02614 11.2666 3.8325 17.3856 3.8325 17.3856L3.89841 17.4316C3.95699 18.3759 4.10348 19.7348 4.10348 19.7348H4.39644H4.6894C4.6894 19.7348 4.83588 18.3836 4.89447 17.4316L4.96038 17.3779C4.9677 17.3779 12.8264 11.2666 6.03701 2.19958ZM4.39644 17.2473C4.39644 17.2473 4.04489 16.9325 3.94968 16.7713V16.756L4.37447 6.88281C4.37447 6.8521 4.41842 6.8521 4.41842 6.88281L4.8432 16.756V16.7713C4.74799 16.9325 4.39644 17.2473 4.39644 17.2473Z" fill="#00ED64"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 861 B

@@ -0,0 +1,27 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" y="0.5" width="31" height="31" rx="7.5" fill="white" stroke="#EDEDF0"/>
<g transform="translate(3.2, 3.2) scale(1)">
<svg viewBox="0 0 25.6 25.6" width="25.6" height="25.6">
<style><![CDATA[.B{stroke-linecap:round}.C{stroke-linejoin:round}.D{stroke-linejoin:miter}.E{stroke-width:.716}]]></style>
<g fill="none" stroke="#fff">
<path d="M18.983 18.636c.163-1.357.114-1.555 1.124-1.336l.257.023c.777.035 1.793-.125 2.4-.402 1.285-.596 2.047-1.592.78-1.33-2.89.596-3.1-.383-3.1-.383 3.053-4.53 4.33-10.28 3.227-11.687-3.004-3.84-8.205-2.024-8.292-1.976l-.028.005c-.57-.12-1.2-.19-1.93-.2-1.308-.02-2.3.343-3.054.914 0 0-9.277-3.822-8.846 4.807.092 1.836 2.63 13.9 5.66 10.25C8.29 15.987 9.36 14.86 9.36 14.86c.53.353 1.167.533 1.834.468l.052-.044a2.01 2.01 0 0 0 .021.518c-.78.872-.55 1.025-2.11 1.346-1.578.325-.65.904-.046 1.056.734.184 2.432.444 3.58-1.162l-.046.183c.306.245.285 1.76.33 2.842s.116 2.093.337 2.688.48 2.13 2.53 1.7c1.713-.367 3.023-.896 3.143-5.81" fill="#000" stroke="#000" stroke-linecap="butt" stroke-width="2.149" class="D"/>
<path d="M23.535 15.6c-2.89.596-3.1-.383-3.1-.383 3.053-4.53 4.33-10.28 3.228-11.687-3.004-3.84-8.205-2.023-8.292-1.976l-.028.005a10.31 10.31 0 0 0-1.929-.201c-1.308-.02-2.3.343-3.054.914 0 0-9.278-3.822-8.846 4.807.092 1.836 2.63 13.9 5.66 10.25C8.29 15.987 9.36 14.86 9.36 14.86c.53.353 1.167.533 1.834.468l.052-.044a2.02 2.02 0 0 0 .021.518c-.78.872-.55 1.025-2.11 1.346-1.578.325-.65.904-.046 1.056.734.184 2.432.444 3.58-1.162l-.046.183c.306.245.52 1.593.484 2.815s-.06 2.06.18 2.716.48 2.13 2.53 1.7c1.713-.367 2.6-1.32 2.725-2.906.088-1.128.286-.962.3-1.97l.16-.478c.183-1.53.03-2.023 1.085-1.793l.257.023c.777.035 1.794-.125 2.39-.402 1.285-.596 2.047-1.592.78-1.33z" fill="#336791" stroke="none"/>
<g class="E">
<g class="B">
<path d="M12.814 16.467c-.08 2.846.02 5.712.298 6.4s.875 2.05 2.926 1.612c1.713-.367 2.337-1.078 2.607-2.647l.633-5.017M10.356 2.2S1.072-1.596 1.504 7.033c.092 1.836 2.63 13.9 5.66 10.25C8.27 15.95 9.27 14.907 9.27 14.907m6.1-13.4c-.32.1 5.164-2.005 8.282 1.978 1.1 1.407-.175 7.157-3.228 11.687" class="C"/>
<path d="M20.425 15.17s.2.98 3.1.382c1.267-.262.504.734-.78 1.33-1.054.49-3.418.615-3.457-.06-.1-1.745 1.244-1.215 1.147-1.652-.088-.394-.69-.78-1.086-1.744-.347-.84-4.76-7.29 1.224-6.333.22-.045-1.56-5.7-7.16-5.782S7.99 8.196 7.99 8.196" stroke-linejoin="bevel"/>
</g>
<g class="C">
<path d="M11.247 15.768c-.78.872-.55 1.025-2.11 1.346-1.578.325-.65.904-.046 1.056.734.184 2.432.444 3.58-1.163.35-.49-.002-1.27-.482-1.468-.232-.096-.542-.216-.94.23z"/>
<path d="M11.196 15.753c-.08-.513.168-1.122.433-1.836.398-1.07 1.316-2.14.582-5.537-.547-2.53-4.22-.527-4.22-.184s.166 1.74-.06 3.365c-.297 2.122 1.35 3.916 3.246 3.733" class="B"/>
</g>
</g>
<g fill="#fff" class="D">
<path d="M10.322 8.145c-.017.117.215.43.516.472s.558-.202.575-.32-.215-.246-.516-.288-.56.02-.575.136z" stroke-width=".239"/>
<path d="M19.486 7.906c.016.117-.215.43-.516.472s-.56-.202-.575-.32.215-.246.516-.288.56.02.575.136z" stroke-width=".119"/>
</g>
<path d="M20.562 7.095c.05.92-.198 1.545-.23 2.524-.046 1.422.678 3.05-.413 4.68" class="B C E"/>
</g>
</svg>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.99976 2C4.55204 2 4.99976 2.44772 4.99976 3V5.10125C6.26985 3.80489 8.04028 3 9.99976 3C13.0492 3 15.6407 4.94932 16.6012 7.66675C16.7852 8.18747 16.5123 8.75879 15.9916 8.94284C15.4709 9.12689 14.8996 8.85396 14.7155 8.33325C14.0286 6.38991 12.1752 5 9.99976 5C8.36482 5 6.91179 5.78502 5.99911 7H8.99976C9.55204 7 9.99976 7.44771 9.99976 8C9.99976 8.55228 9.55204 9 8.99976 9H3.99976C3.44747 9 2.99976 8.55228 2.99976 8V3C2.99976 2.44772 3.44747 2 3.99976 2ZM4.00792 11.0572C4.52864 10.8731 5.09996 11.146 5.28401 11.6668C5.97088 13.6101 7.82429 15 9.99976 15C11.6347 15 13.0877 14.215 14.0004 13L10.9998 13C10.4475 13 9.99976 12.5523 9.99976 12C9.99976 11.4477 10.4475 11 10.9998 11H15.9998C16.265 11 16.5193 11.1054 16.7069 11.2929C16.8944 11.4804 16.9998 11.7348 16.9998 12V17C16.9998 17.5523 16.552 18 15.9998 18C15.4475 18 14.9998 17.5523 14.9998 17V14.8987C13.7297 16.1951 11.9592 17 9.99976 17C6.95035 17 4.3588 15.0507 3.39833 12.3332C3.21428 11.8125 3.48721 11.2412 4.00792 11.0572Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="8" cy="8" r="3" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 157 B

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.25706 3.09882C9.02167 1.73952 10.9788 1.73952 11.7434 3.09882L17.3237 13.0194C18.0736 14.3526 17.1102 15.9999 15.5805 15.9999H4.4199C2.89025 15.9999 1.92682 14.3526 2.67675 13.0194L8.25706 3.09882ZM11.0001 13C11.0001 13.5523 10.5524 14 10.0001 14C9.44784 14 9.00012 13.5523 9.00012 13C9.00012 12.4477 9.44784 12 10.0001 12C10.5524 12 11.0001 12.4477 11.0001 13ZM10.0001 5C9.44784 5 9.00012 5.44772 9.00012 6V9C9.00012 9.55228 9.44784 10 10.0001 10C10.5524 10 11.0001 9.55228 11.0001 9V6C11.0001 5.44772 10.5524 5 10.0001 5Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 725 B

@@ -0,0 +1,11 @@
(() => {
window.InstallerConstants = Object.freeze({
stepTransitionMs: 260,
errorClearMs: 180,
installPollIntervalMs: 4000,
installFallbackDelayMs: 12000,
redirectDelayMs: 2500,
progressTransitionDelayMs: 320,
progressCompleteDelayMs: 140,
});
})();
+450
View File
@@ -0,0 +1,450 @@
(() => {
const stepContainer = document.querySelector('.installer-step');
const installerCard = document.querySelector('.installer-card');
const backButton = document.querySelector('[data-action="back"]');
const nextButton = document.querySelector('[data-action="next"]');
const installScreen = document.querySelector('.install-screen-content');
const indicatorNodes = Array.from(document.querySelectorAll('.step-indicator'));
const STEP_TRANSITION_TIMEOUT = window.InstallerConstants?.stepTransitionMs ?? 260;
if (!stepContainer || !installerCard) return;
const { validateInstallRequest } = window.InstallerStepsProgress || {};
const isUpgrade = document.body?.dataset.upgrade === 'true';
const stepFlow = isUpgrade ? [1, 4, 5] : [1, 2, 3, 4, 5];
const cardSteps = stepFlow.filter((step) => step !== 5);
const normalizeStep = (step) => {
const numeric = clampStep(step);
if (stepFlow.includes(numeric)) return numeric;
if (numeric <= stepFlow[0]) return stepFlow[0];
for (let i = 0; i < stepFlow.length; i += 1) {
if (numeric < stepFlow[i]) {
return stepFlow[i];
}
}
return stepFlow[stepFlow.length - 1];
};
const buildStepConfig = () => {
const config = {};
stepFlow.forEach((step, index) => {
if (step === 5) {
config[step] = { back: { target: null }, next: { target: null } };
return;
}
const prev = stepFlow[index - 1] ?? null;
const next = stepFlow[index + 1] ?? null;
const label = next === 5 ? (isUpgrade ? 'Update' : 'Install') : 'Next';
config[step] = {
back: { target: prev },
next: { label, target: next }
};
});
return config;
};
const STEP_CONFIG = buildStepConfig();
const stepCache = new Map();
let maxStepHeight = 0;
let isTransitioning = false;
let pendingStep = null;
let pendingPushState = false;
const clampStep = (step) => Math.max(1, Math.min(5, step));
const isInstallLocked = () => Boolean(window.InstallerSteps?.isInstallLocked?.());
const scrollToFirstError = (panel) => {
if (!panel) return;
const getErrorNode = () => panel.querySelector('.field-error.is-visible')
|| panel.querySelector('.field-error')
|| panel.querySelector('.input-field.is-error, .input-action.is-error');
const container = panel.closest('.step-panel') || panel;
const attemptScroll = () => {
const target = getErrorNode();
if (!target || typeof target.getBoundingClientRect !== 'function') return false;
const targetRect = target.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
const targetTop = targetRect.top - containerRect.top + container.scrollTop;
const targetBottom = targetTop + targetRect.height;
const viewTop = container.scrollTop;
const viewBottom = viewTop + containerRect.height;
const padding = 12;
let nextScrollTop = viewTop;
if (targetTop < viewTop + padding) {
nextScrollTop = Math.max(0, targetTop - padding);
} else if (targetBottom > viewBottom - padding) {
nextScrollTop = Math.max(0, targetBottom - containerRect.height + padding);
}
if (Math.abs(nextScrollTop - viewTop) < 1) {
return false;
}
container.scrollTo({ top: nextScrollTop, behavior: 'smooth' });
return true;
};
let remaining = 20;
let lastScrollTop = -1;
const settle = () => {
if (remaining <= 0) return;
const moved = attemptScroll();
remaining -= 1;
const currentTop = container.scrollTop;
const delta = Math.abs(currentTop - lastScrollTop);
lastScrollTop = currentTop;
if (!moved && delta < 0.5) {
return;
}
requestAnimationFrame(settle);
};
requestAnimationFrame(settle);
};
const getStepFromUrl = () => {
const url = new URL(window.location.href);
const step = Number(url.searchParams.get('step') || 1);
return normalizeStep(Number.isNaN(step) ? 1 : step);
};
const buildStepUrl = (step) => {
const url = new URL(window.location.href);
url.searchParams.set('step', step);
return url;
};
const setStepInUrl = (step, pushState) => {
const url = new URL(window.location.href);
url.searchParams.set('step', step);
if (pushState) {
window.history.pushState({ step }, '', url.toString());
}
return url;
};
const updateActionBar = (step) => {
const config = STEP_CONFIG[step] || STEP_CONFIG[1];
if (!backButton || !nextButton) return;
const locked = isInstallLocked();
const setButtonLabel = (button, label) => {
if (!button) return;
let text = button.querySelector('.button-text');
if (!text) {
text = document.createElement('span');
text.className = 'button-text typography-text-m-500';
button.textContent = '';
button.appendChild(text);
}
text.textContent = label;
};
if (!locked && config.back?.target) {
backButton.disabled = false;
backButton.setAttribute('data-step-target', String(config.back.target));
} else {
backButton.disabled = true;
backButton.removeAttribute('data-step-target');
}
setButtonLabel(backButton, 'Back');
if (!locked && config.next?.target) {
setButtonLabel(nextButton, config.next?.label || 'Next');
nextButton.setAttribute('data-step-target', String(config.next?.target || 1));
nextButton.disabled = false;
} else {
setButtonLabel(nextButton, config.next?.label || 'Next');
nextButton.removeAttribute('data-step-target');
nextButton.disabled = true;
}
indicatorNodes.forEach((node, index) => {
const isVisible = index < cardSteps.length;
node.classList.toggle('is-hidden', !isVisible);
if (!isVisible) {
node.classList.remove('is-active');
return;
}
node.classList.toggle('is-active', cardSteps[index] === step);
});
installerCard.setAttribute('data-step', String(step));
document.body.dataset.step = String(step);
if (locked) {
document.body.dataset.installLocked = 'true';
} else {
delete document.body.dataset.installLocked;
}
};
const measureStepHeight = (panel) => {
if (!panel) return;
const height = panel.getBoundingClientRect().height;
if (!height) return;
maxStepHeight = Math.max(maxStepHeight, height);
stepContainer.style.setProperty('--step-min-height', `${maxStepHeight}px`);
};
const runStepInit = (step, rootElement) => {
if (!window.InstallerSteps || typeof window.InstallerSteps.initStep !== 'function') return;
const root = rootElement || stepContainer;
window.InstallerSteps.initStep(step, root);
updateActionBar(step);
};
const fetchStepHtml = (step, url) => {
if (stepCache.has(step)) {
return Promise.resolve(stepCache.get(step));
}
const fetchUrl = new URL(url);
fetchUrl.searchParams.set('partial', '1');
return fetch(fetchUrl.toString(), {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then((response) => {
if (!response.ok) {
throw new Error('Failed to load step');
}
return response.text();
})
.then((html) => {
stepCache.set(step, html);
return html;
});
};
const preloadSteps = (steps) => {
const current = getStepFromUrl();
const targets = steps.filter((step) => step !== current);
return Promise.all(
targets.map((step) => {
const url = buildStepUrl(step);
return fetchStepHtml(step, url)
.then((html) => {
const panel = document.createElement('div');
panel.className = 'step-panel is-measure';
panel.innerHTML = html;
stepContainer.appendChild(panel);
panel.getBoundingClientRect();
measureStepHeight(panel);
panel.remove();
})
.catch(() => null);
})
);
};
const swapPanels = (step, html, onDone) => {
const activePanel = stepContainer.querySelector('.step-panel');
const measurePanel = document.createElement('div');
measurePanel.className = 'step-panel is-measure';
measurePanel.innerHTML = html;
stepContainer.appendChild(measurePanel);
measurePanel.getBoundingClientRect();
measureStepHeight(measurePanel);
measurePanel.remove();
const newPanel = document.createElement('div');
newPanel.className = 'step-panel is-entering';
newPanel.innerHTML = html;
stepContainer.appendChild(newPanel);
runStepInit(step, newPanel);
newPanel.getBoundingClientRect();
requestAnimationFrame(() => {
newPanel.classList.remove('is-entering');
newPanel.classList.add('is-active');
if (activePanel) {
activePanel.classList.add('is-exiting');
}
});
const finalize = () => {
if (activePanel && activePanel.parentNode) {
activePanel.parentNode.removeChild(activePanel);
}
newPanel.classList.remove('is-entering');
if (typeof onDone === 'function') {
onDone();
}
};
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (prefersReduced) {
finalize();
return;
}
let finished = false;
const finishOnce = () => {
if (finished) return;
finished = true;
finalize();
};
newPanel.addEventListener(
'transitionend',
(event) => {
if (event.propertyName === 'opacity') {
finishOnce();
}
},
{ once: true }
);
setTimeout(finishOnce, STEP_TRANSITION_TIMEOUT);
};
const showInstallScreen = (step, html) => {
if (!installScreen) return;
installScreen.innerHTML = html;
runStepInit(step, installScreen);
};
const hideInstallScreen = () => {
if (!installScreen) return;
installScreen.innerHTML = '';
};
const loadStep = (step, pushState) => {
const targetStep = normalizeStep(Number(step));
const currentStep = getStepFromUrl();
if (targetStep === currentStep && pushState) return;
isTransitioning = true;
const url = setStepInUrl(targetStep, pushState);
fetchStepHtml(targetStep, url)
.then((html) => {
if (targetStep === 5) {
showInstallScreen(targetStep, html);
isTransitioning = false;
if (pendingStep !== null && pendingStep !== targetStep) {
const nextStep = pendingStep;
const nextPushState = pendingPushState;
pendingStep = null;
pendingPushState = false;
loadStep(nextStep, nextPushState);
return;
}
pendingStep = null;
pendingPushState = false;
return;
}
hideInstallScreen();
swapPanels(targetStep, html, () => {
isTransitioning = false;
if (pendingStep !== null && pendingStep !== targetStep) {
const nextStep = pendingStep;
const nextPushState = pendingPushState;
pendingStep = null;
pendingPushState = false;
loadStep(nextStep, nextPushState);
return;
}
pendingStep = null;
pendingPushState = false;
});
})
.catch(() => {
isTransitioning = false;
window.location.href = url.toString();
});
};
const requestStep = (step, pushState) => {
const targetStep = normalizeStep(Number(step));
if (isInstallLocked() && targetStep !== 5) {
loadStep(5, true);
return;
}
if (isTransitioning) {
pendingStep = targetStep;
pendingPushState = pendingPushState || pushState;
return;
}
loadStep(targetStep, pushState);
};
document.addEventListener('click', async (event) => {
const button = event.target.closest('[data-step-target]');
if (!button || button.disabled) return;
event.preventDefault();
const target = button.getAttribute('data-step-target');
if (!target) return;
const action = button.getAttribute('data-action');
if (action === 'next') {
const currentStep = getStepFromUrl();
const panel = stepContainer.querySelector('.step-panel') || stepContainer;
const validator = window.InstallerSteps?.validateStep;
if (typeof validator === 'function') {
const valid = validator(currentStep, panel);
if (!valid) {
scrollToFirstError(panel);
return;
}
}
}
if (action === 'next' && String(target) === '5' && typeof validateInstallRequest === 'function') {
const isValid = await validateInstallRequest();
if (!isValid) {
return;
}
}
if (isInstallLocked() && Number(target) !== 5) {
requestStep(5, true);
return;
}
requestStep(target, true);
});
window.addEventListener('popstate', (event) => {
const step = event.state?.step || getStepFromUrl();
if (isInstallLocked() && Number(step) !== 5) {
requestStep(5, false);
return;
}
requestStep(step, false);
});
document.addEventListener('DOMContentLoaded', () => {
let step = getStepFromUrl();
if (isInstallLocked() && step !== 5) {
const url = buildStepUrl(5);
window.history.replaceState({ step: 5 }, '', url.toString());
step = 5;
} else {
const url = buildStepUrl(step);
window.history.replaceState({ step }, '', url.toString());
}
const activePanel = stepContainer.querySelector('.step-panel') || stepContainer;
runStepInit(step, activePanel);
measureStepHeight(activePanel);
if (step === 5 && installScreen) {
runStepInit(step, installScreen);
}
const preload = () => {
measureStepHeight(activePanel);
preloadSteps(cardSteps);
};
if (document.fonts && document.fonts.ready) {
document.fonts.ready.then(preload).catch(preload);
} else {
preload();
}
});
})();
@@ -0,0 +1,105 @@
(() => {
const getBodyDataset = () => document.body?.dataset ?? {};
const isUpgradeMode = () => getBodyDataset().upgrade === 'true';
const getLockedDatabase = () => getBodyDataset().lockedDatabase || '';
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'
});
const STATUS = Object.freeze({
IN_PROGRESS: 'in-progress',
COMPLETED: 'completed',
ERROR: 'error'
});
const SSE_EVENTS = Object.freeze({
PING: 'ping',
INSTALL_ID: 'install-id',
PROGRESS: 'progress',
DONE: 'done',
ERROR: 'error'
});
const buildInstallationSteps = (upgrade) => (upgrade ? [
{
id: STEP_IDS.CONFIG_FILES,
inProgress: 'Updating configuration files...',
done: 'Configuration files updated'
},
{
id: STEP_IDS.DOCKER_COMPOSE,
inProgress: 'Updating Docker Compose file...',
done: 'Docker Compose file updated'
},
{
id: STEP_IDS.ENV_VARS,
inProgress: 'Updating environment variables...',
done: 'Environment variables updated'
},
{
id: STEP_IDS.DOCKER_CONTAINERS,
inProgress: 'Restarting Docker containers...',
done: 'Docker containers restarted'
}
] : [
{
id: STEP_IDS.CONFIG_FILES,
inProgress: 'Creating configuration files...',
done: 'Configuration files created'
},
{
id: STEP_IDS.DOCKER_COMPOSE,
inProgress: 'Generating Docker Compose file...',
done: 'Docker Compose file generated'
},
{
id: STEP_IDS.ENV_VARS,
inProgress: 'Configuring environment variables...',
done: 'Environment variables configured'
},
{
id: STEP_IDS.DOCKER_CONTAINERS,
inProgress: 'Starting Docker containers...',
done: 'Docker containers started'
},
{
id: STEP_IDS.ACCOUNT_SETUP,
inProgress: 'Creating Appwrite account...',
done: 'Appwrite account created (redirecting...)'
}
]);
const INSTALLATION_STEPS = buildInstallationSteps(isUpgradeMode());
const CONSTANTS = window.InstallerConstants || {};
const TIMINGS = {
errorClear: CONSTANTS.errorClearMs ?? 180,
installPollInterval: CONSTANTS.installPollIntervalMs ?? 4000,
installFallbackDelay: CONSTANTS.installFallbackDelayMs ?? 12000,
redirectDelay: CONSTANTS.redirectDelayMs ?? 500,
progressTransitionDelay: CONSTANTS.progressTransitionDelayMs ?? 140,
progressCompleteDelay: CONSTANTS.progressCompleteDelayMs ?? 120
};
const clampStep = (step) => {
const numeric = Number(step);
if (Number.isNaN(numeric)) return 1;
return Math.max(1, Math.min(5, numeric));
};
window.InstallerStepsContext = Object.freeze({
getBodyDataset,
isUpgradeMode,
getLockedDatabase,
STEP_IDS,
STATUS,
SSE_EVENTS,
INSTALLATION_STEPS,
TIMINGS,
clampStep
});
})();
@@ -0,0 +1,899 @@
(() => {
const {
INSTALLATION_STEPS,
TIMINGS,
getBodyDataset,
isUpgradeMode,
STEP_IDS,
STATUS,
SSE_EVENTS
} = window.InstallerStepsContext;
const {
formState,
applyLockPayload,
applyBodyDefaults,
setInstallLock,
getInstallLock,
clearInstallLock,
isInstallLocked,
syncInstallLockFlag,
getStoredInstallId,
storeInstallId,
clearInstallId
} = window.InstallerStepsState || {};
const { extractHostname, isLocalHost } = window.InstallerStepsValidation || {};
const { generateSecretKey } = window.InstallerStepsUI || {};
const { showToast } = window.InstallerToast || {};
let activeInstall = null;
let unloadGuard = null;
const csrfToken = document.querySelector('meta[name="appwrite-installer-csrf"]')?.getAttribute('content') || '';
const withCsrfHeader = (headers = {}) => {
if (!csrfToken) {
return headers;
}
return { ...headers, 'X-Appwrite-Installer-CSRF': csrfToken };
};
const showCsrfToast = () => {
showToast?.({
status: 'error',
title: 'Session expired',
description: 'Refresh the page and try again.',
dismissible: true
});
};
const validateInstallRequest = async () => {
try {
const response = await fetch('/install/validate', {
method: 'POST',
headers: withCsrfHeader({
'Content-Type': 'application/json'
})
});
if (!response.ok) {
showCsrfToast();
return false;
}
const data = await response.json().catch(() => ({}));
if (!data?.success) {
showCsrfToast();
return false;
}
return true;
} catch (error) {
showCsrfToast();
return false;
}
};
const setUnloadGuard = (enabled) => {
if (!enabled && unloadGuard) {
window.removeEventListener('beforeunload', unloadGuard);
unloadGuard = null;
return;
}
if (enabled && !unloadGuard) {
unloadGuard = (event) => {
event.preventDefault();
event.returnValue = '';
return '';
};
window.addEventListener('beforeunload', unloadGuard);
}
};
const cleanupInstallFlow = () => {
if (activeInstall?.controller) {
activeInstall.controller.abort();
if (activeInstall.pollTimer) {
clearInterval(activeInstall.pollTimer);
}
if (activeInstall.fallbackTimer) {
clearTimeout(activeInstall.fallbackTimer);
}
activeInstall = null;
}
stopSyncedSpinnerRotation();
setUnloadGuard(false);
};
const getStepDefinition = (id) => INSTALLATION_STEPS.find((step) => step.id === id);
const getProgressLabel = (step, status, message) => {
if (!step) return message || '';
if (status === STATUS.ERROR) {
const normalized = normalizeInstallError(message || '');
return normalized.summary || 'Installation failed.';
}
if (status === STATUS.COMPLETED) return step.done;
return step.inProgress;
};
const updateInstallRow = (row, step, status, message) => {
if (!row || !step) return;
row.dataset.status = status;
row.dataset.step = step.id;
if (status !== STATUS.ERROR) {
row.classList.remove('is-open');
const toggle = row.querySelector('[data-install-toggle]');
if (toggle) {
toggle.setAttribute('aria-expanded', 'false');
}
}
const label = getProgressLabel(step, status, message);
const text = row.querySelector('[data-install-text]');
if (text) {
if (text.textContent !== label) {
text.classList.remove('is-enter');
text.textContent = label;
text.classList.add('is-enter');
requestAnimationFrame(() => {
text.classList.remove('is-enter');
});
}
}
// Show/hide "Navigate to Console" button for account setup errors
const consoleBtn = row.querySelector('[data-install-console]');
if (consoleBtn) {
const shouldShow = step.id === STEP_IDS.ACCOUNT_SETUP && status === STATUS.ERROR;
consoleBtn.classList.toggle('is-hidden', !shouldShow);
}
};
const normalizeInstallError = (message) => {
const text = String(message || '').trim();
if (!text) {
return { summary: '', details: '' };
}
const colonIndex = text.indexOf(':');
if (colonIndex > 0 && colonIndex < 80) {
const summary = text.slice(0, colonIndex).trim();
const details = text.slice(colonIndex + 1).trim();
return { summary, details };
}
if (text.length > 180) {
return { summary: text.slice(0, 180).trim() + '…', details: text };
}
return { summary: text, details: '' };
};
let spinnerAnimationFrame = null;
const stopSyncedSpinnerRotation = () => {
if (spinnerAnimationFrame) {
cancelAnimationFrame(spinnerAnimationFrame);
spinnerAnimationFrame = null;
}
};
const startSyncedSpinnerRotation = (container) => {
stopSyncedSpinnerRotation();
if (!container) return;
let startTime = null;
const animate = (timestamp) => {
if (!startTime) startTime = timestamp;
const elapsed = timestamp - startTime;
const rotation = ((elapsed / 1000) * 360 * 1.5) % 360;
container.style.setProperty('--spinner-rotation', `${rotation}deg`);
spinnerAnimationFrame = requestAnimationFrame(animate);
};
spinnerAnimationFrame = requestAnimationFrame(animate);
};
const updateInstallErrorDetails = (row, error) => {
if (!row) return;
const traceNode = row.querySelector('[data-install-trace]');
const normalized = normalizeInstallError(error?.message || '');
const output = error?.output || '';
const trace = error?.trace || '';
const detailChunks = [];
if (normalized.details) detailChunks.push(normalized.details);
if (output) detailChunks.push(output);
if (trace) detailChunks.push(trace);
const detailText = detailChunks.join('\n\n');
if (traceNode) {
traceNode.textContent = detailText;
traceNode.style.display = detailText ? 'block' : 'none';
}
};
const createInstallRow = (template, step) => {
const fragment = template.content.cloneNode(true);
const row = fragment.querySelector('.install-row');
if (!row) return null;
const toggle = row.querySelector('[data-install-toggle]');
const setOpenState = (isOpen) => {
row.classList.toggle('is-open', isOpen);
if (toggle) {
toggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
}
};
const toggleRow = () => {
if (!row.dataset.status || row.dataset.status !== STATUS?.ERROR) {
return;
}
setOpenState(!row.classList.contains('is-open'));
};
row.addEventListener('click', (event) => {
if (event.target.closest('[data-install-retry]')) {
return;
}
if (event.target.closest('[data-install-toggle]')) {
return;
}
if (event.target.closest('.install-row-details')) {
return;
}
toggleRow();
});
if (toggle) {
toggle.addEventListener('click', (event) => {
event.stopPropagation();
toggleRow();
});
}
updateInstallRow(row, step, STATUS.IN_PROGRESS);
return row;
};
const generateInstallId = () => {
if (window.crypto?.randomUUID) {
return window.crypto.randomUUID();
}
const bytes = new Uint8Array(16);
window.crypto.getRandomValues(bytes);
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('');
};
const buildRedirectUrl = () => {
const dataset = getBodyDataset?.() ?? {};
const rawDomain = (formState?.appDomain || dataset.defaultAppDomain || '').trim();
if (!rawDomain) return '';
const httpPort = (formState?.httpPort || dataset.defaultHttpPort || '').trim();
const httpsPort = (formState?.httpsPort || dataset.defaultHttpsPort || '').trim();
const hasPort = rawDomain.includes(':') || rawDomain.startsWith('[');
let host = rawDomain;
const hostForProtocol = extractHostname?.(rawDomain);
const normalizedHost = hostForProtocol?.toLowerCase?.() ?? '';
if (hostForProtocol === '0.0.0.0') {
host = rawDomain.replace('0.0.0.0', 'localhost');
} 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'))) {
host = `${host}:${port}`;
}
return `${protocol}://${host}`;
};
const redirectToApp = () => {
const url = buildRedirectUrl();
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;
};
const notifyInstallComplete = (installId, session) => {
if (!installId) return Promise.resolve();
const payload = { installId };
const sessionSecret = session?.sessionSecret || session?.secret;
const sessionId = session?.sessionId || session?.id;
const sessionExpire = session?.sessionExpire || session?.expire;
if (sessionSecret) {
payload.sessionSecret = sessionSecret;
}
if (sessionId) {
payload.sessionId = sessionId;
}
if (sessionExpire) {
payload.sessionExpire = sessionExpire;
}
return fetch('/install/complete', {
method: 'POST',
headers: withCsrfHeader({
'Content-Type': 'application/json'
}),
body: JSON.stringify(payload)
}).catch(() => {});
};
const buildInstallPayload = (installId) => {
const normalizedSecret = (formState?.opensslKey || '').trim();
if (!normalizedSecret && generateSecretKey && !isUpgradeMode?.()) {
formState.opensslKey = generateSecretKey();
}
const normalizedDomain = (formState?.appDomain || '').trim() || 'localhost';
const normalizedHttpPort = (formState?.httpPort || '').trim() || '80';
const normalizedHttpsPort = (formState?.httpsPort || '').trim() || '443';
const normalizedEmail = (formState?.emailCertificates || '').trim();
const normalizedAssistantKey = (formState?.assistantOpenAIKey || '').trim();
const normalizedAccountEmail = (formState?.accountEmail || '').trim();
const normalizedAccountPassword = (formState?.accountPassword || '').trim();
return {
installId,
httpPort: normalizedHttpPort,
httpsPort: normalizedHttpsPort,
database: formState?.database || 'mongodb',
appDomain: normalizedDomain,
emailCertificates: normalizedEmail,
opensslKey: (formState?.opensslKey || '').trim(),
assistantOpenAIKey: normalizedAssistantKey,
accountEmail: normalizedAccountEmail,
accountPassword: normalizedAccountPassword
};
};
const fetchInstallStatus = async (installId) => {
if (!installId) return null;
const response = await fetch(`/install/status?installId=${encodeURIComponent(installId)}`, {
cache: 'no-store'
});
if (!response.ok) return null;
const json = await response.json();
return json.progress || null;
};
const readEventStream = async (stream, onEvent) => {
const reader = stream.getReader();
const decoder = new TextDecoder('utf-8');
let buffer = '';
try {
const processEvent = (rawEvent) => {
if (!rawEvent) return;
const lines = rawEvent.split('\n');
let eventName = 'message';
let data = '';
lines.forEach((line) => {
if (line.startsWith('event:')) {
eventName = line.replace('event:', '').trim();
} else if (line.startsWith('data:')) {
data += line.replace('data:', '').trim();
}
});
if (data) {
try {
const parsed = JSON.parse(data);
onEvent(eventName, parsed);
} catch (error) {
onEvent(eventName, { message: data });
}
}
};
while (true) {
const { value, done } = await reader.read();
if (done) {
buffer = buffer.replace(/\r\n/g, '\n');
if (buffer.trim()) {
processEvent(buffer);
}
break;
}
buffer += decoder.decode(value, { stream: true });
buffer = buffer.replace(/\r\n/g, '\n');
let separatorIndex = buffer.indexOf('\n\n');
while (separatorIndex !== -1) {
const rawEvent = buffer.slice(0, separatorIndex);
buffer = buffer.slice(separatorIndex + 2);
processEvent(rawEvent);
separatorIndex = buffer.indexOf('\n\n');
}
}
} finally {
try {
reader.releaseLock();
} catch (error) {}
}
};
const initStep5 = (root) => {
if (!root) return;
if (activeInstall?.controller) {
activeInstall.controller.abort();
}
if (activeInstall?.pollTimer) {
clearInterval(activeInstall.pollTimer);
}
if (activeInstall?.fallbackTimer) {
clearTimeout(activeInstall.fallbackTimer);
}
activeInstall = null;
const list = root.querySelector('[data-install-list]');
const template = root.querySelector('#install-row-template');
if (!list || !template) return;
startSyncedSpinnerRotation(list);
list.innerHTML = '';
const rowsById = new Map();
const progressState = new Map();
syncInstallLockFlag?.();
applyLockPayload?.();
applyBodyDefaults?.();
const ensureRow = (step) => {
if (!step) return null;
if (rowsById.has(step.id)) {
return rowsById.get(step.id);
}
const row = createInstallRow(template, step);
if (!row) return null;
row.classList.add('is-entering');
list.appendChild(row);
row.getBoundingClientRect();
requestAnimationFrame(() => {
row.classList.remove('is-entering');
});
rowsById.set(step.id, row);
return row;
};
const installPanel = root.querySelector('.install-panel');
let panelHeightCleanup = null;
const animatePanelHeight = (mutate) => {
if (!installPanel) {
mutate();
return;
}
if (panelHeightCleanup) {
panelHeightCleanup();
panelHeightCleanup = null;
}
const currentHeight = installPanel.getBoundingClientRect().height;
installPanel.style.height = `${currentHeight}px`;
installPanel.getBoundingClientRect();
mutate();
const nextHeight = installPanel.getBoundingClientRect().height;
if (currentHeight === nextHeight) {
installPanel.style.height = '';
return;
}
installPanel.style.height = `${currentHeight}px`;
installPanel.getBoundingClientRect();
installPanel.style.height = `${nextHeight}px`;
const cleanup = () => {
installPanel.style.height = '';
installPanel.removeEventListener('transitionend', onEnd);
};
const onEnd = (event) => {
if (event.propertyName === 'height') {
cleanup();
}
};
panelHeightCleanup = cleanup;
installPanel.addEventListener('transitionend', onEnd);
};
const renderProgress = () => {
animatePanelHeight(() => {
const visibleSteps = [];
for (const step of INSTALLATION_STEPS) {
const state = progressState.get(step.id);
if (!state) break;
visibleSteps.push(step);
}
visibleSteps.forEach((step) => {
const state = progressState.get(step.id);
if (!state) return;
const row = ensureRow(step);
if (row) {
updateInstallRow(row, step, state.status || STATUS.IN_PROGRESS, state.message);
if (state.status === STATUS?.ERROR) {
updateInstallErrorDetails(row, {
message: state.message,
trace: state.details?.trace,
output: state.details?.output
});
}
}
});
});
};
const firstStep = INSTALLATION_STEPS[0];
if (firstStep) {
progressState.set(firstStep.id, {
status: STATUS.IN_PROGRESS,
message: firstStep.inProgress
});
}
renderProgress();
const applyProgress = (payload) => {
const step = getStepDefinition(payload.step) || {
id: payload.step,
inProgress: payload.message || payload.step,
done: payload.message || payload.step
};
progressState.set(step.id, {
status: payload.status || STATUS.IN_PROGRESS,
message: payload.message,
details: payload.details
});
renderProgress();
if (activeInstall) {
activeInstall.lastEventAt = Date.now();
if (payload.status === STATUS.ERROR) {
if (activeInstall.pollTimer) {
clearInterval(activeInstall.pollTimer);
activeInstall.pollTimer = null;
}
if (activeInstall.fallbackTimer) {
clearTimeout(activeInstall.fallbackTimer);
activeInstall.fallbackTimer = null;
}
}
}
scheduleFallback();
};
const handleProgress = (payload) => {
if (!payload || !payload.step) return;
const existingState = progressState.get(payload.step);
if (existingState && existingState.status === STATUS.COMPLETED && payload.status === STATUS.IN_PROGRESS) {
return;
}
const step = getStepDefinition(payload.step) || {
id: payload.step,
inProgress: payload.message || payload.step,
done: payload.message || payload.step
};
if (payload.status === STATUS.IN_PROGRESS) {
const currentIndex = INSTALLATION_STEPS.findIndex((candidate) => candidate.id === step.id);
if (currentIndex > 0) {
for (let i = 0; i < currentIndex; i += 1) {
const previousStep = INSTALLATION_STEPS[i];
const previousState = progressState.get(previousStep.id);
if (previousState && previousState.status !== STATUS.COMPLETED) {
progressState.set(previousStep.id, {
status: STATUS.COMPLETED,
message: previousStep.done,
details: previousState.details
});
}
}
}
}
applyProgress(payload);
};
const applySnapshot = (snapshot) => {
if (!snapshot || !snapshot.steps) return;
INSTALLATION_STEPS.forEach((step) => {
const detail = snapshot.steps[step.id];
if (!detail) return;
progressState.set(step.id, {
status: detail.status,
message: detail.message,
details: snapshot.details?.[step.id]
});
});
renderProgress();
};
const checkAllCompleted = () => {
const allDone = INSTALLATION_STEPS.every((step) => {
const state = progressState.get(step.id);
return state && state.status === STATUS.COMPLETED;
});
if (!allDone) return;
const accountState = progressState.get(STEP_IDS.ACCOUNT_SETUP);
const sessionDetails = accountState?.details;
finalizeInstall();
notifyInstallComplete(activeInstall?.installId, sessionDetails).finally(() => {
setTimeout(() => redirectToApp(), TIMINGS?.redirectDelay ?? 0);
});
};
const startPolling = () => {
if (!activeInstall || activeInstall.pollTimer) return;
activeInstall.pollTimer = setInterval(async () => {
if (!activeInstall || activeInstall.completed) return;
const snapshot = await fetchInstallStatus(activeInstall.installId);
if (snapshot) {
applySnapshot(snapshot);
checkAllCompleted();
}
}, TIMINGS?.installPollInterval ?? 0);
};
const scheduleFallback = () => {
if (!activeInstall) return;
if (activeInstall.fallbackTimer) {
clearTimeout(activeInstall.fallbackTimer);
}
activeInstall.fallbackTimer = setTimeout(() => {
if (!activeInstall) return;
startPolling();
}, TIMINGS?.installFallbackDelay ?? 0);
};
const finalizeInstall = () => {
if (!activeInstall) return;
activeInstall.completed = true;
if (activeInstall.pollTimer) {
clearInterval(activeInstall.pollTimer);
}
if (activeInstall.fallbackTimer) {
clearTimeout(activeInstall.fallbackTimer);
}
stopSyncedSpinnerRotation();
setUnloadGuard(false);
};
const startInstallStream = async (installId, options = {}) => {
const isValid = await validateInstallRequest();
if (!isValid) {
return;
}
activeInstall = {
installId,
controller: new AbortController(),
lastEventAt: Date.now(),
pollTimer: null,
fallbackTimer: null,
completed: false
};
const payload = buildInstallPayload(installId);
if (options.retryStep) {
payload.retryStep = options.retryStep;
}
setInstallLock?.(installId, payload);
setUnloadGuard(true);
try {
scheduleFallback();
const response = await fetch('/install', {
method: 'POST',
headers: withCsrfHeader({
'Content-Type': 'application/json',
'Accept': 'text/event-stream'
}),
body: JSON.stringify(payload),
signal: activeInstall.controller.signal
});
if (!response.ok || !response.body) {
let errorMessage = null;
try {
const contentType = response.headers.get('Content-Type') || '';
if (contentType.includes('application/json')) {
const data = await response.json();
errorMessage = data?.message || null;
}
} catch (error) {
errorMessage = null;
}
if (errorMessage) {
handleProgress({
step: STEP_IDS.CONFIG_FILES,
status: STATUS.ERROR,
message: errorMessage
});
finalizeInstall();
return;
}
startPolling();
return;
}
await readEventStream(response.body, (event, data) => {
if (!activeInstall) return;
if (event === SSE_EVENTS.INSTALL_ID && data?.installId) {
activeInstall.installId = data.installId;
storeInstallId?.(data.installId);
return;
}
if (event === SSE_EVENTS.PROGRESS) {
handleProgress(data);
return;
}
if (event === SSE_EVENTS.DONE) {
// Mark every step as completed (preserving details
// from earlier progress events, e.g. session info).
INSTALLATION_STEPS.forEach((step) => {
const existing = progressState.get(step.id);
if (!existing || (existing.status !== STATUS.COMPLETED && existing.status !== STATUS.ERROR)) {
progressState.set(step.id, {
status: STATUS.COMPLETED,
message: step.done,
details: existing?.details
});
}
});
renderProgress();
// If any step ended in error (e.g. account creation
// failed), stay on the progress screen so the user can
// see the error and choose to retry or navigate to the
// console manually — don't auto-redirect.
const hasErrors = INSTALLATION_STEPS.some((step) => {
const state = progressState.get(step.id);
return state && state.status === STATUS.ERROR;
});
if (hasErrors) {
finalizeInstall();
return;
}
const accountState = progressState.get(STEP_IDS.ACCOUNT_SETUP);
const sessionDetails = accountState?.details;
finalizeInstall();
notifyInstallComplete(activeInstall?.installId, sessionDetails).finally(() => {
setTimeout(() => redirectToApp(), TIMINGS?.redirectDelay ?? 0);
});
return;
}
if (event === SSE_EVENTS.ERROR) {
if (data?.message) {
const existingError = Array.from(progressState.values()).some((state) => state?.status === STATUS.ERROR);
if (data.step || !existingError) {
let targetStep = data.step;
if (!targetStep) {
for (const candidate of INSTALLATION_STEPS) {
const state = progressState.get(candidate.id);
if (!state || state.status !== STATUS.COMPLETED) {
targetStep = candidate.id;
break;
}
}
}
handleProgress({
step: targetStep || STEP_IDS.CONFIG_FILES,
status: STATUS.ERROR,
message: data.message,
details: data.details
});
}
}
finalizeInstall();
}
});
if (activeInstall && !activeInstall.completed) {
// Stream ended without a "done" event (e.g. browser
// throttled the background tab). Check if we're done.
checkAllCompleted();
if (!activeInstall?.completed) {
startPolling();
}
}
} catch (error) {
if (!activeInstall || activeInstall.controller.signal.aborted) {
return;
}
startPolling();
}
};
const resumeInstall = async (installId) => {
const snapshot = await fetchInstallStatus(installId);
if (!snapshot) return false;
activeInstall = {
installId,
controller: new AbortController(),
lastEventAt: Date.now(),
pollTimer: null,
fallbackTimer: null,
completed: false
};
applySnapshot(snapshot);
startPolling();
setUnloadGuard(true);
return true;
};
const resetProgressFrom = (stepId) => {
const index = INSTALLATION_STEPS.findIndex((step) => step.id === stepId);
if (index === -1) return;
INSTALLATION_STEPS.slice(index).forEach((step) => {
progressState.delete(step.id);
const row = rowsById.get(step.id);
if (row && row.parentNode) {
row.parentNode.removeChild(row);
}
rowsById.delete(step.id);
});
};
const retryInstallStep = (stepId) => {
if (!stepId) return;
if (activeInstall?.controller) {
activeInstall.controller.abort();
}
if (activeInstall?.pollTimer) {
clearInterval(activeInstall.pollTimer);
}
if (activeInstall?.fallbackTimer) {
clearTimeout(activeInstall.fallbackTimer);
}
resetProgressFrom(stepId);
const step = getStepDefinition(stepId);
progressState.set(stepId, {
status: STATUS.IN_PROGRESS,
message: step?.inProgress || 'Retrying...'
});
const row = ensureRow(step);
if (row) {
updateInstallRow(row, step, STATUS.IN_PROGRESS, step.inProgress || 'Retrying...');
}
const installId = activeInstall?.installId || getInstallLock?.()?.installId || generateInstallId();
storeInstallId?.(installId);
startInstallStream(installId, { retryStep: stepId });
};
list.addEventListener('click', (event) => {
const consoleButton = event.target.closest('[data-install-console]');
const retryButton = event.target.closest('[data-install-retry]');
if (consoleButton) {
redirectToApp();
return;
}
if (retryButton) {
const row = retryButton.closest('.install-row');
const stepId = row?.dataset.step;
retryInstallStep(stepId);
}
});
// When the user switches back to this tab, check if installation
// finished while the tab was in the background.
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible' && activeInstall && !activeInstall.completed) {
checkAllCompleted();
}
});
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 newInstallId = generateInstallId();
storeInstallId?.(newInstallId);
startInstallStream(newInstallId);
}
};
window.InstallerStepsProgress = {
initStep5,
cleanupInstallFlow,
validateInstallRequest
};
})();
@@ -0,0 +1,158 @@
(() => {
const {
getBodyDataset,
isUpgradeMode,
getLockedDatabase
} = window.InstallerStepsContext || {};
const INSTALL_LOCK_KEY = 'appwrite-install-lock';
const INSTALL_ID_KEY = 'appwrite-install-id';
const formState = {
appDomain: null,
database: null,
httpPort: null,
httpsPort: null,
emailCertificates: null,
opensslKey: null,
assistantOpenAIKey: null,
accountEmail: null,
accountPassword: null
};
const dispatchStateChange = (key) => {
if (!key || typeof document === 'undefined') return;
try {
document.dispatchEvent(new CustomEvent('installer:state-change', {
detail: { key, value: formState[key] }
}));
} catch (error) {}
};
const setStateIfEmpty = (key, value) => {
if (value === null || value === undefined || value === '') return;
if (formState[key] === null || formState[key] === undefined || formState[key] === '') {
formState[key] = value;
}
};
const applyBodyDefaults = () => {
const data = getBodyDataset?.() ?? {};
setStateIfEmpty('appDomain', data.defaultAppDomain);
setStateIfEmpty('httpPort', data.defaultHttpPort);
setStateIfEmpty('httpsPort', data.defaultHttpsPort);
setStateIfEmpty('emailCertificates', data.defaultEmailCertificates);
setStateIfEmpty('opensslKey', data.defaultSecretKey);
setStateIfEmpty('assistantOpenAIKey', data.defaultAssistantOpenaiKey);
if (data.lockedDatabase) {
formState.database = data.lockedDatabase;
}
if (!isUpgradeMode?.()) {
setStateIfEmpty('database', data.defaultDatabase);
}
};
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;
}
};
const setInstallLock = (installId, payload) => {
const sanitizedPayload = payload ? { ...payload } : null;
if (sanitizedPayload) {
delete sanitizedPayload.opensslKey;
delete sanitizedPayload.accountPassword;
delete sanitizedPayload.assistantOpenAIKey;
}
const lock = {
installId,
payload: sanitizedPayload,
startedAt: Date.now()
};
try {
sessionStorage.setItem(INSTALL_LOCK_KEY, JSON.stringify(lock));
} catch (error) {}
if (document.body) {
document.body.dataset.installLocked = 'true';
}
return lock;
};
const clearInstallLock = () => {
try {
sessionStorage.removeItem(INSTALL_LOCK_KEY);
} catch (error) {}
if (document.body) {
delete document.body.dataset.installLocked;
}
};
const isInstallLocked = () => {
return Boolean(getInstallLock());
};
const syncInstallLockFlag = () => {
if (!document.body) return;
if (isInstallLocked()) {
document.body.dataset.installLocked = 'true';
} else {
delete document.body.dataset.installLocked;
}
};
const applyLockPayload = () => {
const lock = getInstallLock();
if (!lock || !lock.payload) return;
const payload = lock.payload;
setStateIfEmpty('appDomain', payload.appDomain);
setStateIfEmpty('database', payload.database);
setStateIfEmpty('httpPort', payload.httpPort);
setStateIfEmpty('httpsPort', payload.httpsPort);
setStateIfEmpty('emailCertificates', payload.emailCertificates);
setStateIfEmpty('accountEmail', payload.accountEmail);
};
const getStoredInstallId = () => {
try {
return sessionStorage.getItem(INSTALL_ID_KEY);
} catch (error) {
return null;
}
};
const storeInstallId = (installId) => {
try {
sessionStorage.setItem(INSTALL_ID_KEY, installId);
} catch (error) {}
};
const clearInstallId = () => {
try {
sessionStorage.removeItem(INSTALL_ID_KEY);
} catch (error) {}
};
window.InstallerStepsState = {
formState,
dispatchStateChange,
setStateIfEmpty,
applyBodyDefaults,
applyLockPayload,
getInstallLock,
setInstallLock,
clearInstallLock,
isInstallLocked,
syncInstallLockFlag,
getStoredInstallId,
storeInstallId,
clearInstallId,
getLockedDatabase: getLockedDatabase || (() => '')
};
})();
@@ -0,0 +1,95 @@
(() => {
const TOAST_STACK_ID = 'installer-toast-stack';
const DEFAULT_TIMEOUT = 5000;
const MAX_TOASTS = 3;
const ICONS = {
error: '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M18 10a8 8 0 1 1-16 0 8 8 0 0 1 16 0m-7 4a1 1 0 1 1-2 0 1 1 0 0 1 2 0m-1-9a1 1 0 0 0-1 1v4a1 1 0 1 0 2 0V6a1 1 0 0 0-1-1" clip-rule="evenodd"/></svg>',
close: '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M5.293 5.293a1 1 0 0 1 1.414 0L10 8.586l3.293-3.293a1 1 0 1 1 1.414 1.414L11.414 10l3.293 3.293a1 1 0 0 1-1.414 1.414L10 11.414l-3.293 3.293a1 1 0 0 1-1.414-1.414L8.586 10 5.293 6.707a1 1 0 0 1 0-1.414" clip-rule="evenodd"/></svg>'
};
const getStack = () => document.getElementById(TOAST_STACK_ID);
const dismissToast = (toast) => {
if (!toast) return;
if (toast.classList.contains('is-leaving')) return;
toast.classList.add('is-leaving');
const remove = () => toast.remove();
toast.addEventListener('transitionend', remove, { once: true });
setTimeout(remove, 450);
};
const showToast = ({
title = '',
description = '',
status = 'error',
dismissible = true,
timeout = DEFAULT_TIMEOUT
} = {}) => {
const stack = getStack();
if (!stack) return;
const visibleToasts = Array.from(
stack.querySelectorAll('.installer-toast:not(.is-leaving)')
);
if (visibleToasts.length >= MAX_TOASTS) {
dismissToast(visibleToasts[0]);
}
const toast = document.createElement('div');
toast.className = 'installer-toast is-entering';
toast.dataset.status = status;
toast.setAttribute('role', status === 'error' ? 'alert' : 'status');
const content = document.createElement('div');
content.className = 'installer-toast-content';
const icon = document.createElement('span');
icon.className = 'installer-toast-icon';
icon.dataset.status = status;
icon.innerHTML = ICONS.error;
content.appendChild(icon);
const body = document.createElement('section');
body.className = 'installer-toast-body';
if (title) {
const titleNode = document.createElement('p');
titleNode.className = 'installer-toast-title typography-text-m-500';
titleNode.textContent = title;
body.appendChild(titleNode);
}
if (description) {
const descNode = document.createElement('p');
descNode.className = 'installer-toast-description typography-text-m-400';
descNode.textContent = description;
body.appendChild(descNode);
}
content.appendChild(body);
toast.appendChild(content);
if (dismissible) {
const close = document.createElement('button');
close.type = 'button';
close.className = 'installer-toast-close';
close.setAttribute('aria-label', 'Dismiss notification');
close.innerHTML = ICONS.close;
close.addEventListener('click', () => dismissToast(toast));
toast.appendChild(close);
}
stack.appendChild(toast);
toast.getBoundingClientRect();
requestAnimationFrame(() => {
toast.classList.remove('is-entering');
});
if (timeout > 0) {
setTimeout(() => dismissToast(toast), timeout);
}
};
window.InstallerToast = Object.freeze({
showToast
});
})();
@@ -0,0 +1,281 @@
(() => {
const { TIMINGS } = window.InstallerStepsContext || {};
const { formState } = window.InstallerStepsState || {};
const clearFieldErrors = (root) => {
if (!root) return;
root.querySelectorAll('.field-error').forEach((node) => {
node.classList.remove('is-visible');
});
root.querySelectorAll('.input-field.is-error, .input-action.is-error').forEach((node) => {
node.classList.remove('is-error');
});
root.querySelectorAll('.field-helper').forEach((helper) => {
helper.style.display = '';
});
};
const setFieldError = (input, message) => {
if (!input) return;
const group = input.closest('.input-group');
if (!group) return;
let error = group.querySelector('.field-error');
let errorText = error?.querySelector('.field-error-text');
const hasSameMessage = Boolean(errorText && errorText.textContent === message);
const alreadyVisible = Boolean(error && error.classList.contains('is-visible'));
if (hasSameMessage && alreadyVisible) {
return;
}
if (!error) {
const template = document.getElementById('field-error-template');
if (template && template.content) {
const fragment = template.content.cloneNode(true);
error = fragment.querySelector('.field-error');
group.appendChild(fragment);
}
errorText = error?.querySelector('.field-error-text');
}
if (errorText) {
errorText.textContent = message;
}
if (!alreadyVisible) {
requestAnimationFrame(() => {
error.classList.add('is-visible');
});
}
input.classList.add('is-error');
const actionWrapper = input.closest('.input-action');
if (actionWrapper) {
actionWrapper.classList.add('is-error');
}
const helper = group.querySelector('.field-helper');
if (helper) {
helper.style.display = 'none';
}
};
const bindErrorClear = (input) => {
if (!input) return;
const handler = () => {
const group = input.closest('.input-group');
const error = group?.querySelector('.field-error');
if (error) {
error.classList.remove('is-visible');
}
input.classList.remove('is-error');
const actionWrapper = input.closest('.input-action');
if (actionWrapper) {
actionWrapper.classList.remove('is-error');
}
const helper = group?.querySelector('.field-helper');
if (helper) {
helper.style.display = '';
}
};
input.addEventListener('input', handler);
input.addEventListener('change', handler);
};
const toDatabaseLabel = (value) => {
if (!value) return '';
const lower = value.toLowerCase();
if (lower === 'mariadb') return 'MariaDB';
if (lower === 'postgresql') return 'PostgreSQL';
return 'MongoDB';
};
const updateDatabaseSelection = (radio, root) => {
if (!radio || !root) return;
const allOptions = root.querySelectorAll('.selector-card');
allOptions.forEach((option) => option.classList.remove('selected'));
const selectedOption = radio.closest('.selector-card');
if (selectedOption) {
selectedOption.classList.add('selected');
}
};
const syncResetButton = (input, button) => {
const defaultValue = input.dataset.default ?? '';
button.disabled = input.value === defaultValue;
};
const setupResetButtons = (root) => {
const inputs = root.querySelectorAll('.input-field[data-default]');
inputs.forEach((input) => {
const button = root.querySelector(`[data-reset-target="${input.id}"]`);
if (!button) return;
syncResetButton(input, button);
input.addEventListener('input', () => syncResetButton(input, button));
button.addEventListener('click', () => {
input.value = input.dataset.default ?? '';
syncResetButton(input, button);
input.dispatchEvent(new Event('input', { bubbles: true }));
});
});
};
const toggleAccordion = (button) => {
const content = button.nextElementSibling;
const icon = button.querySelector('.accordion-chevron');
const isOpen = button.classList.contains('is-open');
button.classList.toggle('is-open', !isOpen);
button.setAttribute('aria-expanded', String(!isOpen));
if (content) {
if (!isOpen) {
content.classList.add('open');
content.style.maxHeight = `${content.scrollHeight}px`;
} else {
content.style.maxHeight = '0px';
content.classList.remove('open');
}
}
if (icon) {
icon.setAttribute('data-open', String(!isOpen));
}
};
const setupAccordion = (root) => {
const buttons = root.querySelectorAll('.accordion-toggle');
buttons.forEach((button) => {
button.addEventListener('click', () => toggleAccordion(button));
});
};
const openAccordion = (root) => {
const toggle = root.querySelector('.accordion-toggle');
const content = root.querySelector('.accordion-content');
if (!toggle || !content) return;
if (!toggle.classList.contains('is-open')) {
toggle.classList.add('is-open');
toggle.setAttribute('aria-expanded', 'true');
content.classList.add('open');
content.style.maxHeight = `${content.scrollHeight}px`;
}
};
const disableControls = (root) => {
const inputs = root.querySelectorAll('input, select, textarea');
inputs.forEach((input) => {
if (input.type === 'radio' || input.type === 'checkbox') {
input.disabled = true;
} else {
input.readOnly = true;
input.setAttribute('aria-disabled', 'true');
}
});
const buttons = root.querySelectorAll('button');
buttons.forEach((button) => {
if (button.matches('[data-copy-target]')) return;
button.disabled = true;
button.setAttribute('aria-disabled', 'true');
});
root.classList.add('is-locked');
};
const generateSecretKey = () => {
const array = new Uint8Array(32);
window.crypto.getRandomValues(array);
return Array.from(array, (byte) => byte.toString(16).padStart(2, '0')).join('');
};
const copyToClipboard = (value, input) => {
if (!value) return;
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(value);
return;
}
if (input) {
input.select();
document.execCommand('copy');
input.setSelectionRange(0, 0);
return;
}
const textArea = document.createElement('textarea');
textArea.value = value;
textArea.style.position = 'fixed';
textArea.style.top = '-9999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
} catch (error) {} finally {
document.body.removeChild(textArea);
}
};
const setTooltipText = (wrapper, message) => {
if (!wrapper) return;
const tooltip = wrapper.querySelector('.tooltip');
if (tooltip && message) {
tooltip.textContent = message;
}
};
const resetTooltipText = (wrapper) => {
if (!wrapper) return;
const defaultText = wrapper.dataset.tooltipDefault;
if (!defaultText) return;
setTooltipText(wrapper, defaultText);
};
const updateReviewSummary = (root) => {
if (!root) return;
const valueNodes = root.querySelectorAll('[data-review-value]');
valueNodes.forEach((node) => {
const key = node.dataset.reviewValue;
if (!key) return;
let value = formState?.[key];
if (key === 'database') {
value = toDatabaseLabel(formState?.database);
}
if (value) {
node.textContent = value;
}
});
const badge = root.querySelector('[data-review-badge]');
if (badge) {
const hasKey = Boolean((formState?.opensslKey || '').trim());
badge.textContent = hasKey ? 'Generated' : 'Missing';
badge.classList.remove('badge-success', 'badge-warning');
badge.classList.add(hasKey ? 'badge-success' : 'badge-warning');
}
const assistantBadge = root.querySelector('[data-review-assistant-badge]');
if (assistantBadge) {
const hasAssistantKey = Boolean((formState?.assistantOpenAIKey || '').trim());
assistantBadge.textContent = hasAssistantKey ? 'Enabled' : 'Disabled';
assistantBadge.classList.remove('badge-success', 'badge-neutral');
assistantBadge.classList.add(hasAssistantKey ? 'badge-success' : 'badge-neutral');
}
};
window.InstallerStepsUI = {
clearFieldErrors,
setFieldError,
bindErrorClear,
toDatabaseLabel,
updateDatabaseSelection,
setupResetButtons,
setupAccordion,
openAccordion,
disableControls,
generateSecretKey,
copyToClipboard,
setTooltipText,
resetTooltipText,
updateReviewSummary
};
})();
@@ -0,0 +1,117 @@
(() => {
const isValidEmail = (email) => {
if (!email) return false;
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return re.test(email);
};
const isValidPort = (value) => {
const numeric = Number(value);
if (!Number.isInteger(numeric)) return false;
return numeric >= 1 && numeric <= 65535;
};
const isValidPassword = (value) => {
if (!value) return false;
return value.length >= 8 && /\S/.test(value);
};
const isValidIPv4 = (host) => {
if (!/^\d{1,3}(\.\d{1,3}){3}$/.test(host)) return false;
return host.split('.').every((part) => {
const num = Number(part);
return num >= 0 && num <= 255;
});
};
const isValidIPv6 = (host) => {
try {
const url = new URL(`http://[${host}]`);
return url.hostname.toLowerCase() === host.toLowerCase();
} catch (error) {
return false;
}
};
const isValidHostnameLabel = (label) => {
if (!label || label.length > 63) return false;
if (label.startsWith('-') || label.endsWith('-')) return false;
return /^[a-zA-Z0-9-]+$/.test(label);
};
const isValidDomain = (host) => {
if (host.length > 253) return false;
const labels = host.split('.');
return labels.every((label) => isValidHostnameLabel(label));
};
const isValidHost = (host) => {
if (host === 'localhost') return true;
if (isValidIPv4(host)) return true;
if (isValidIPv6(host)) return true;
return isValidDomain(host);
};
const isValidHostnameInput = (value) => {
if (!value) return false;
const trimmed = value.trim();
if (!trimmed) return false;
let host = trimmed;
let port = null;
if (trimmed.startsWith('[')) {
const match = trimmed.match(/^\[([^\]]+)\](?::(\d+))?$/);
if (!match) return false;
host = match[1] || '';
port = match[2] || null;
} else {
const parts = trimmed.split(':');
if (parts.length > 2) return false;
if (parts.length === 2) {
host = parts[0];
port = parts[1];
}
}
if (port !== null && port !== '' && !isValidPort(port)) {
return false;
}
return isValidHost(host);
};
const extractHostname = (value) => {
if (!value) return '';
const trimmed = value.trim();
if (trimmed.startsWith('[')) {
const end = trimmed.indexOf(']');
if (end !== -1) {
return trimmed.slice(1, end);
}
return trimmed;
}
const colonCount = (trimmed.match(/:/g) || []).length;
if (colonCount === 1) {
return trimmed.split(':')[0];
}
return trimmed;
};
const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '::1', '0.0.0.0']);
const isLocalHost = (host) => {
if (!host) return false;
const normalized = host.toLowerCase();
return LOCAL_HOSTS.has(normalized);
};
window.InstallerStepsValidation = {
isValidEmail,
isValidPort,
isValidPassword,
isValidHostnameInput,
extractHostname,
isLocalHost
};
})();
+435
View File
@@ -0,0 +1,435 @@
(() => {
const Context = window.InstallerStepsContext || {};
const State = window.InstallerStepsState || {};
const Validation = window.InstallerStepsValidation || {};
const UI = window.InstallerStepsUI || {};
const Progress = window.InstallerStepsProgress || {};
const Tooltips = window.InstallerTooltips || null;
const {
INSTALLATION_STEPS,
clampStep,
isUpgradeMode
} = Context;
const {
formState,
dispatchStateChange,
applyBodyDefaults,
applyLockPayload,
clearInstallLock,
clearInstallId,
isInstallLocked,
syncInstallLockFlag,
getInstallLock,
getLockedDatabase
} = State;
const {
isValidEmail,
isValidPort,
isValidHostnameInput,
isValidPassword
} = Validation;
const {
clearFieldErrors,
setFieldError,
bindErrorClear,
updateDatabaseSelection,
setupResetButtons,
setupAccordion,
openAccordion,
disableControls,
generateSecretKey,
copyToClipboard,
setTooltipText,
resetTooltipText,
updateReviewSummary
} = UI;
let reviewListener = null;
const bindInputToState = (input, key) => {
if (!input) return;
const update = () => {
formState[key] = input.value;
dispatchStateChange?.(key);
};
input.addEventListener('input', update);
input.addEventListener('change', update);
update();
};
const lockDatabaseSelection = (root, lockedDatabase) => {
if (lockedDatabase) {
const radios = root.querySelectorAll('input[name="database"]');
radios.forEach((radio) => {
const isLockedChoice = radio.value === lockedDatabase;
const card = radio.closest('.selector-card');
radio.disabled = !isLockedChoice;
if (card) {
card.classList.toggle('is-disabled', !isLockedChoice);
}
if (isLockedChoice) {
radio.checked = true;
updateDatabaseSelection?.(radio, root);
}
});
}
};
const bindDatabaseSelection = (root) => {
const radios = root.querySelectorAll('input[name="database"]');
radios.forEach((radio) => {
radio.addEventListener('change', () => {
formState.database = radio.value;
updateDatabaseSelection?.(radio, root);
});
});
};
const hydrateStep1State = (root) => {
State.setStateIfEmpty?.('appDomain', root.querySelector('#hostname')?.value);
State.setStateIfEmpty?.('database', root.querySelector('input[name="database"]:checked')?.value);
State.setStateIfEmpty?.('httpPort', root.querySelector('#http-port')?.value);
State.setStateIfEmpty?.('httpsPort', root.querySelector('#https-port')?.value);
State.setStateIfEmpty?.('emailCertificates', root.querySelector('#ssl-email')?.value);
State.setStateIfEmpty?.('assistantOpenAIKey', root.querySelector('#assistant-openai-key')?.value);
};
const applyStep1State = (root) => {
const hostname = root.querySelector('#hostname');
if (hostname && formState.appDomain) hostname.value = formState.appDomain;
const httpPort = root.querySelector('#http-port');
if (httpPort && formState.httpPort) httpPort.value = formState.httpPort;
const httpsPort = root.querySelector('#https-port');
if (httpsPort && formState.httpsPort) httpsPort.value = formState.httpsPort;
const sslEmail = root.querySelector('#ssl-email');
if (sslEmail && formState.emailCertificates) sslEmail.value = formState.emailCertificates;
const assistantKey = root.querySelector('#assistant-openai-key');
if (assistantKey && formState.assistantOpenAIKey) {
assistantKey.value = formState.assistantOpenAIKey;
}
if (formState.database) {
const radio = root.querySelector(`input[name="database"][value="${formState.database}"]`);
if (radio) {
radio.checked = true;
updateDatabaseSelection?.(radio, root);
}
}
};
const initStep1 = (root) => {
if (!root) return;
syncInstallLockFlag?.();
applyLockPayload?.();
applyBodyDefaults?.();
hydrateStep1State(root);
applyStep1State(root);
if (isInstallLocked?.()) {
openAccordion?.(root);
disableControls?.(root);
return;
}
const lockedDatabase = getLockedDatabase?.() || '';
if (lockedDatabase) {
lockDatabaseSelection(root, lockedDatabase);
} else {
bindDatabaseSelection(root);
}
const hostname = root.querySelector('#hostname');
const httpPort = root.querySelector('#http-port');
const httpsPort = root.querySelector('#https-port');
const sslEmail = root.querySelector('#ssl-email');
const assistantKey = root.querySelector('#assistant-openai-key');
bindInputToState(hostname, 'appDomain');
bindInputToState(httpPort, 'httpPort');
bindInputToState(httpsPort, 'httpsPort');
bindInputToState(sslEmail, 'emailCertificates');
bindInputToState(assistantKey, 'assistantOpenAIKey');
bindErrorClear?.(hostname);
bindErrorClear?.(httpPort);
bindErrorClear?.(httpsPort);
bindErrorClear?.(sslEmail);
bindErrorClear?.(assistantKey);
const checked = root.querySelector('input[name="database"]:checked');
if (checked) {
updateDatabaseSelection?.(checked, root);
}
setupResetButtons?.(root);
setupAccordion?.(root);
Tooltips?.setupTooltipPortals?.(root);
};
const hydrateStep2State = (root) => {
const value = root.querySelector('#secret-key')?.value;
if (formState.opensslKey) return;
if (value) {
formState.opensslKey = value;
}
};
const applyStep2State = (root) => {
const input = root.querySelector('#secret-key');
if (input && formState.opensslKey) {
input.value = formState.opensslKey;
}
};
const initStep2 = (root) => {
if (!root) return;
syncInstallLockFlag?.();
applyLockPayload?.();
applyBodyDefaults?.();
hydrateStep2State(root);
if (!isUpgradeMode?.() && (!formState.opensslKey || !formState.opensslKey.trim())) {
formState.opensslKey = generateSecretKey?.();
dispatchStateChange?.('opensslKey');
}
applyStep2State(root);
const input = root.querySelector('#secret-key');
if (input) {
bindInputToState(input, 'opensslKey');
bindErrorClear?.(input);
}
const copyButton = root.querySelector('[data-copy-target]');
const tooltipWrapper = copyButton?.closest('.tooltip-wrapper');
if (tooltipWrapper) {
tooltipWrapper.addEventListener('mouseenter', () => resetTooltipText?.(tooltipWrapper));
tooltipWrapper.addEventListener('focusin', () => resetTooltipText?.(tooltipWrapper));
}
if (copyButton) {
copyButton.addEventListener('click', () => {
const targetId = copyButton.getAttribute('data-copy-target');
const targetInput = targetId ? root.querySelector(`#${targetId}`) : null;
const value = targetInput?.value || '';
copyToClipboard?.(value, targetInput);
copyButton.blur();
if (tooltipWrapper) {
const successText = tooltipWrapper.dataset.tooltipSuccess || 'Copied';
setTooltipText?.(tooltipWrapper, successText);
}
});
}
const regenerateButton = root.querySelector('[data-regenerate-target]');
if (regenerateButton && !isInstallLocked?.()) {
regenerateButton.addEventListener('click', () => {
const targetId = regenerateButton.getAttribute('data-regenerate-target');
const targetInput = targetId ? root.querySelector(`#${targetId}`) : null;
if (!targetInput) return;
regenerateButton.classList.remove('is-rotating');
void regenerateButton.offsetWidth;
regenerateButton.classList.add('is-rotating');
const handleAnimationEnd = () => {
regenerateButton.classList.remove('is-rotating');
};
regenerateButton.addEventListener('animationend', handleAnimationEnd, { once: true });
targetInput.value = generateSecretKey?.();
targetInput.dispatchEvent(new Event('input', { bubbles: true }));
});
}
if (isInstallLocked?.()) {
disableControls?.(root);
}
};
const hydrateStep3State = (root) => {
State.setStateIfEmpty?.('accountEmail', root.querySelector('#account-email')?.value);
State.setStateIfEmpty?.('accountPassword', root.querySelector('#account-password')?.value);
};
const applyStep3State = (root) => {
const email = root.querySelector('#account-email');
if (email && formState.accountEmail) email.value = formState.accountEmail;
const password = root.querySelector('#account-password');
if (password && formState.accountPassword) password.value = formState.accountPassword;
};
const initStep3 = (root) => {
if (!root) return;
syncInstallLockFlag?.();
applyLockPayload?.();
applyBodyDefaults?.();
hydrateStep3State(root);
applyStep3State(root);
const email = root.querySelector('#account-email');
const password = root.querySelector('#account-password');
const passwordToggle = root.querySelector('[data-password-toggle="account-password"]');
bindInputToState(email, 'accountEmail');
bindInputToState(password, 'accountPassword');
bindErrorClear?.(email);
bindErrorClear?.(password);
if (password && passwordToggle) {
passwordToggle.addEventListener('click', () => {
const isVisible = passwordToggle.classList.toggle('is-visible');
password.type = isVisible ? 'text' : 'password';
passwordToggle.setAttribute('aria-label', isVisible ? 'Hide password' : 'Show password');
});
}
if (isInstallLocked?.()) {
disableControls?.(root);
}
};
const initStep4 = (root) => {
if (!root) return;
syncInstallLockFlag?.();
applyLockPayload?.();
applyBodyDefaults?.();
updateReviewSummary?.(root);
if (reviewListener) {
document.removeEventListener('installer:state-change', reviewListener);
}
reviewListener = () => updateReviewSummary?.(root);
document.addEventListener('installer:state-change', reviewListener);
if (isInstallLocked?.()) {
disableControls?.(root);
}
};
const initStep = (step, container) => {
if (!container) return;
const root = container.querySelector('.step-layout') || container;
const normalized = clampStep?.(step) ?? 1;
Tooltips?.cleanupTooltipPortals?.();
if (normalized !== 4 && reviewListener) {
document.removeEventListener('installer:state-change', reviewListener);
reviewListener = null;
}
if (normalized !== 5) {
Progress.cleanupInstallFlow?.();
}
if (normalized === 1) initStep1(root);
if (normalized === 2) initStep2(root);
if (normalized === 3) initStep3(root);
if (normalized === 4) initStep4(root);
if (normalized === 5) Progress.initStep5?.(root);
};
window.InstallerSteps = {
initStep1,
initStep2,
initStep3,
initStep4,
initStep5: Progress.initStep5,
installationSteps: INSTALLATION_STEPS || [],
isInstallLocked,
getInstallLock,
clearInstallLock,
initStep,
validateStep: (step, container) => {
const root = container?.querySelector('.step-layout') || container;
const normalized = clampStep?.(step) ?? 1;
if (normalized === 1) {
clearFieldErrors?.(root);
let valid = true;
const hostname = root?.querySelector('#hostname');
const httpPort = root?.querySelector('#http-port');
const httpsPort = root?.querySelector('#https-port');
const sslEmail = root?.querySelector('#ssl-email');
if (!hostname || !hostname.value.trim()) {
setFieldError?.(hostname, 'Please enter your Appwrite hostname');
valid = false;
} else if (!isValidHostnameInput?.(hostname.value.trim())) {
setFieldError?.(hostname, 'Please enter a valid hostname');
valid = false;
}
const parsePort = (input, label) => {
const value = input?.value;
if (!value || !isValidPort?.(value)) {
setFieldError?.(input, `Please enter a valid ${label} port (1-65535)`);
return false;
}
return true;
};
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())) {
setFieldError?.(sslEmail, 'Please enter a valid email address');
valid = false;
}
if (!valid) {
openAccordion?.(root);
}
return valid;
}
if (normalized === 2) {
clearFieldErrors?.(root);
const secretKey = root?.querySelector('#secret-key');
const secretValue = secretKey?.value.trim() || '';
if (!secretKey || !secretValue) {
setFieldError?.(secretKey, 'Please enter or generate a secret API key');
return false;
}
if (secretValue.length > 64) {
setFieldError?.(secretKey, 'Secret API key must be 1-64 characters');
return false;
}
}
if (normalized === 3) {
clearFieldErrors?.(root);
let valid = true;
const email = root?.querySelector('#account-email');
const password = root?.querySelector('#account-password');
if (!email || !email.value.trim()) {
setFieldError?.(email, 'This field is required');
valid = false;
} else if (!isValidEmail?.(email.value.trim())) {
setFieldError?.(email, 'Please enter a valid email address');
valid = false;
}
const passwordValue = password?.value ?? '';
if (!password || !/\S/.test(passwordValue)) {
setFieldError?.(password, 'This field is required');
valid = false;
} else if (!isValidPassword?.(passwordValue)) {
setFieldError?.(password, 'Password must be at least 8 characters long');
valid = false;
}
return valid;
}
return true;
}
};
})();
@@ -0,0 +1,77 @@
(() => {
const tooltipPortals = new Set();
const positionTooltipPortal = (tooltip, anchor) => {
if (!tooltip || !anchor) return;
const rect = anchor.getBoundingClientRect();
const tooltipRect = tooltip.getBoundingClientRect();
const offset = Number(tooltip.dataset.tooltipOffset || 6);
const padding = 8;
let left = rect.left + (rect.width / 2) - (tooltipRect.width / 2);
left = Math.max(padding, Math.min(left, window.innerWidth - tooltipRect.width - padding));
const top = rect.bottom + offset;
tooltip.style.left = `${left}px`;
tooltip.style.top = `${top}px`;
};
const attachTooltipPortal = (tooltip) => {
if (!tooltip || tooltip.dataset.portalInitialized === 'true') return;
const anchor = tooltip.parentElement;
if (!anchor) return;
tooltip.dataset.portalInitialized = 'true';
tooltip.classList.add('tooltip-portal');
document.body.appendChild(tooltip);
const show = () => {
tooltip.classList.add('is-open');
positionTooltipPortal(tooltip, anchor);
};
const hide = () => {
tooltip.classList.remove('is-open');
};
const refresh = () => {
if (tooltip.classList.contains('is-open')) {
positionTooltipPortal(tooltip, anchor);
}
};
anchor.addEventListener('mouseenter', show);
anchor.addEventListener('mouseleave', hide);
anchor.addEventListener('focusin', show);
anchor.addEventListener('focusout', hide);
window.addEventListener('scroll', refresh, true);
window.addEventListener('resize', refresh);
tooltipPortals.add({
tooltip,
cleanup: () => {
anchor.removeEventListener('mouseenter', show);
anchor.removeEventListener('mouseleave', hide);
anchor.removeEventListener('focusin', show);
anchor.removeEventListener('focusout', hide);
window.removeEventListener('scroll', refresh, true);
window.removeEventListener('resize', refresh);
if (tooltip.parentElement) {
tooltip.parentElement.removeChild(tooltip);
}
}
});
};
const setupTooltipPortals = (root) => {
if (!root) return;
const portalTooltips = root.querySelectorAll('.tooltip[data-tooltip-portal]');
portalTooltips.forEach((tooltip) => attachTooltipPortal(tooltip));
};
const cleanupTooltipPortals = () => {
tooltipPortals.forEach((entry) => entry.cleanup());
tooltipPortals.clear();
};
window.InstallerTooltips = {
setupTooltipPortals,
cleanupTooltipPortals
};
})();
@@ -0,0 +1,184 @@
<?php
$isUpgrade = $isUpgrade ?? false;
$lockedDatabase = $lockedDatabase ?? null;
$defaultAppDomain = $defaultAppDomain ?? 'localhost';
$defaultHttpPort = $defaultHttpPort ?? '80';
$defaultHttpsPort = $defaultHttpsPort ?? '443';
$defaultEmailCertificates = $defaultEmailCertificates ?? '';
$defaultAssistantOpenAIKey = $defaultAssistantOpenAIKey ?? '';
$defaultDatabase = $defaultDatabase ?? 'mongodb';
$selectedDatabase = $lockedDatabase ?: $defaultDatabase;
$isDatabaseLocked = !empty($lockedDatabase);
$mongoDisabled = $isDatabaseLocked && $selectedDatabase !== 'mongodb';
$mariaDisabled = $isDatabaseLocked && $selectedDatabase !== 'mariadb';
$postgresDisabled = $isDatabaseLocked && $selectedDatabase !== 'postgresql';
$hostnameValue = htmlspecialchars((string) $defaultAppDomain, ENT_QUOTES, 'UTF-8');
$httpPortValue = htmlspecialchars((string) $defaultHttpPort, ENT_QUOTES, 'UTF-8');
$httpsPortValue = htmlspecialchars((string) $defaultHttpsPort, ENT_QUOTES, 'UTF-8');
$sslEmailValue = htmlspecialchars((string) $defaultEmailCertificates, ENT_QUOTES, 'UTF-8');
$assistantOpenAIKeyValue = htmlspecialchars((string) $defaultAssistantOpenAIKey, ENT_QUOTES, 'UTF-8');
?>
<div class="step-layout" data-step="1">
<div class="stack-xl">
<div class="stack-xxxs">
<h1 class="typography-title-s text-neutral-primary"><?php echo $isUpgrade ? 'Update your app' : 'Setup your app'; ?></h1>
<p class="typography-text-m-400 text-neutral-secondary">
<?php echo $isUpgrade ? 'Review your current settings before updating.' : 'Choose where your app will live and how it will store data'; ?>
</p>
</div>
<div class="stack-xl">
<div class="input-group stack-xs">
<label for="hostname" class="label-text typography-text-m-500 text-neutral-secondary">Hostname</label>
<input
type="text"
id="hostname"
name="hostname"
class="input-field typography-text-m-400 text-neutral-primary"
placeholder="localhost"
value="<?php echo $hostnameValue; ?>"
>
</div>
<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' : ''; ?>">
<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">
<div class="typography-text-m-500 text-neutral-primary">MongoDB</div>
<div class="typography-text-xs-400 text-neutral-secondary">Document based database</div>
</div>
<img src="installer/icons/mongodb.svg" alt="MongoDB" class="selector-icon">
<?php if ($mongoDisabled) { ?>
<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>
<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">
<div class="typography-text-m-500 text-neutral-primary">MariaDB</div>
<div class="typography-text-xs-400 text-neutral-secondary">Relational SQL database</div>
</div>
<img src="installer/icons/mariadb.svg" alt="MariaDB" class="selector-icon">
<?php if ($mariaDisabled) { ?>
<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>
<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">
<div class="typography-text-m-500 text-neutral-primary">PostgreSQL</div>
<div class="typography-text-xs-400 text-neutral-secondary">Relational SQL database</div>
</div>
<img src="installer/icons/postgresql.svg" alt="PostgreSQL" class="selector-icon">
<?php if ($postgresDisabled) { ?>
<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>
</div>
</div>
<div class="accordion">
<button
type="button"
class="accordion-toggle"
aria-expanded="false"
>
<span class="label-text typography-text-m-500 text-neutral-secondary">Advanced settings</span>
<span class="accordion-chevron" data-open="false">
<?php include __DIR__ . '/../../icons/chevron-down.svg'; ?>
</span>
</button>
<div class="accordion-content">
<div class="input-row">
<div class="input-group stack-xs">
<label for="http-port" class="label-text typography-text-m-500 text-neutral-secondary">HTTP port</label>
<input
type="number"
id="http-port"
name="httpPort"
class="input-field typography-text-m-400 text-neutral-primary"
placeholder="<?php echo $httpPortValue; ?>"
value="<?php echo $httpPortValue; ?>"
data-default="<?php echo $httpPortValue; ?>"
min="1"
max="65535"
step="1"
>
</div>
<button type="button" class="button secondary" data-reset-target="http-port" disabled>
<span class="button-text typography-text-m-500">Reset</span>
</button>
</div>
<div class="input-row">
<div class="input-group stack-xs">
<label for="https-port" class="label-text typography-text-m-500 text-neutral-secondary">HTTPS port</label>
<input
type="number"
id="https-port"
name="httpsPort"
class="input-field typography-text-m-400 text-neutral-primary"
placeholder="<?php echo $httpsPortValue; ?>"
value="<?php echo $httpsPortValue; ?>"
data-default="<?php echo $httpsPortValue; ?>"
min="1"
max="65535"
step="1"
>
</div>
<button type="button" class="button secondary" data-reset-target="https-port" disabled>
<span class="button-text typography-text-m-500">Reset</span>
</button>
</div>
<div class="input-row">
<div class="input-group stack-xs">
<label for="ssl-email" class="label-text typography-text-m-500 text-neutral-secondary">SSL certificate email</label>
<input
type="email"
id="ssl-email"
name="sslEmail"
class="input-field typography-text-m-400 text-neutral-primary"
placeholder="you@example.com"
value="<?php echo $sslEmailValue; ?>"
data-default="<?php echo $sslEmailValue; ?>"
>
</div>
<button type="button" class="button secondary" data-reset-target="ssl-email" disabled>
<span class="button-text typography-text-m-500">Reset</span>
</button>
</div>
<div class="input-row">
<div class="input-group stack-xs">
<label for="assistant-openai-key" class="label-text typography-text-m-500 text-neutral-secondary">
OpenAI key
<span class="label-optional typography-text-m-400 text-neutral-tertiary">optional</span>
<span class="tooltip-wrapper">
<button type="button" class="label-info-button" aria-label="OpenAI key info">
<?php include __DIR__ . '/../../icons/info.svg'; ?>
</button>
<span class="tooltip tooltip-assistant typography-text-m-400 text-on-invert" role="tooltip">Add your OpenAI key to enable Appwrite Assistant. The key is stored locally and wont be shared.</span>
</span>
</label>
<input
type="text"
id="assistant-openai-key"
name="assistantOpenAIKey"
class="input-field typography-text-m-400 text-neutral-primary"
placeholder="Enter OpenAI key for Appwrite Assistant"
value="<?php echo $assistantOpenAIKeyValue; ?>"
data-default="<?php echo $assistantOpenAIKeyValue; ?>"
>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -0,0 +1,58 @@
<?php
$isUpgrade = $isUpgrade ?? false;
$defaultSecretKey = $defaultSecretKey ?? '';
$secretKeyValue = htmlspecialchars((string) $defaultSecretKey, ENT_QUOTES, 'UTF-8');
?>
<div class="step-layout" data-step="2">
<div class="stack-xl">
<div class="stack-xxxs">
<h1 class="typography-title-s text-neutral-primary">Secure your app</h1>
<p class="typography-text-m-400 text-neutral-secondary">
<?php echo $isUpgrade ? 'Review your existing secret key or generate a new one.' : 'Your server\'s private secret key for encryption. Generate a random secure key, or provide your own.'; ?>
</p>
</div>
<div class="stack-xl">
<div class="inline-alert inline-alert--warning">
<div class="inline-alert-content">
<div class="inline-alert-icon" aria-hidden="true">
<?php include __DIR__ . '/../../icons/warning.svg'; ?>
</div>
<div class="inline-alert-text">
<div class="inline-alert-title typography-text-m-500 text-warning">Save your key</div>
<div class="inline-alert-description typography-text-m-400 text-neutral-primary">You won't be able to see this key again. Copy it somewhere safe before continuing.</div>
</div>
</div>
</div>
<div class="input-row">
<div class="input-group stack-xs">
<label for="secret-key" class="label-text typography-text-m-500 text-neutral-secondary">Secret API key</label>
<div class="input-action">
<input
type="text"
id="secret-key"
name="secretKey"
class="input-action-input typography-text-m-400 text-neutral-primary"
value="<?php echo $secretKeyValue; ?>"
minlength="1"
maxlength="64"
>
<div class="input-action-buttons">
<span class="tooltip-wrapper" data-tooltip-default="Copy" data-tooltip-success="Copied">
<button type="button" class="input-icon-button" aria-label="Copy secret key" aria-describedby="copy-tooltip" data-copy-target="secret-key">
<?php include __DIR__ . '/../../icons/copy.svg'; ?>
</button>
<span id="copy-tooltip" class="tooltip typography-text-m-400 text-on-invert" role="tooltip">Copy</span>
</span>
</div>
</div>
</div>
<button type="button" class="icon-button" aria-label="Regenerate secret key" data-regenerate-target="secret-key">
<?php include __DIR__ . '/../../icons/refresh.svg'; ?>
</button>
</div>
</div>
</div>
</div>
@@ -0,0 +1,61 @@
<?php
$defaultAccountEmail = $defaultAccountEmail ?? '';
$accountEmailValue = htmlspecialchars($defaultAccountEmail, ENT_QUOTES, 'UTF-8');
?>
<div class="step-layout" data-step="3">
<div class="stack-xl">
<div class="stack-xxxs">
<h1 class="typography-title-s text-neutral-primary">Create your account</h1>
<p class="typography-text-m-400 text-neutral-secondary">
Set up the email and password for your Appwrite account. You can use these
credentials to sign in later.
</p>
</div>
<div class="stack-l">
<div class="input-group stack-xs">
<label for="account-email" class="label-text typography-text-m-500 text-neutral-secondary">Email</label>
<input
type="email"
id="account-email"
name="accountEmail"
class="input-field typography-text-m-400 text-neutral-primary"
placeholder="you@example.com"
value="<?php echo $accountEmailValue; ?>"
autocomplete="email"
>
</div>
<div class="input-group stack-xs">
<label for="account-password"
class="label-text typography-text-m-500 text-neutral-secondary">Password</label>
<div class="input-action">
<input
type="password"
id="account-password"
name="accountPassword"
class="input-action-input typography-text-m-400 text-neutral-primary"
placeholder="Enter password"
autocomplete="new-password"
>
<div class="input-action-buttons">
<button type="button" class="input-icon-button password-toggle" aria-label="Show password" data-password-toggle="account-password">
<span class="password-toggle-icon" data-password-icon="hide">
<?php include __DIR__ . '/../../icons/eye-off.svg'; ?>
</span>
<span class="password-toggle-icon" data-password-icon="show">
<?php include __DIR__ . '/../../icons/eye.svg'; ?>
</span>
</button>
</div>
</div>
<div class="field-helper typography-caption-400 text-neutral-secondary">
<span class="field-helper-icon">
<?php include __DIR__ . '/../../icons/info.svg'; ?>
</span>
<span class="field-helper-text">Password must be at least 8 characters long</span>
</div>
</div>
</div>
</div>
</div>
@@ -0,0 +1,76 @@
<?php
$isUpgrade = $isUpgrade ?? false;
$lockedDatabase = $lockedDatabase ?? null;
$defaultAppDomain = $defaultAppDomain ?? 'localhost';
$defaultHttpPort = $defaultHttpPort ?? '80';
$defaultHttpsPort = $defaultHttpsPort ?? '443';
$defaultEmailCertificates = $defaultEmailCertificates ?? '';
$defaultSecretKey = $defaultSecretKey ?? '';
$defaultDatabase = $defaultDatabase ?? 'mongodb';
$selectedDatabase = $lockedDatabase ?: $defaultDatabase;
$defaultDatabaseLabel = match ($selectedDatabase) {
'mariadb' => 'MariaDB',
'postgresql' => 'PostgreSQL',
default => 'MongoDB',
};
$badgeLabel = $defaultSecretKey !== '' ? 'Generated' : 'Missing';
$badgeClass = $defaultSecretKey !== '' ? 'badge-success' : 'badge-warning';
?>
<div class="step-layout" data-step="4">
<div class="stack-xl">
<div class="stack-xxxs">
<h1 class="typography-title-s text-neutral-primary"><?php echo $isUpgrade ? 'Review your update' : 'Review your setup'; ?></h1>
<p class="typography-text-m-400 text-neutral-secondary">
<?php echo $isUpgrade ? 'Confirm your settings before updating Appwrite.' : 'Check your settings below before completing the installation.'; ?>
</p>
</div>
<div class="stack-xl">
<div class="review-card">
<div class="stack-s">
<div class="review-row">
<div class="typography-text-m-500 text-neutral-primary" data-review-value="appDomain">
<?php echo htmlspecialchars((string) $defaultAppDomain, ENT_QUOTES, 'UTF-8'); ?>
</div>
<div class="review-label typography-text-xs-400 text-neutral-tertiary">Hostname</div>
</div>
<div class="review-row">
<div class="typography-text-m-500 text-neutral-primary" data-review-value="database">
<?php echo htmlspecialchars((string) $defaultDatabaseLabel, ENT_QUOTES, 'UTF-8'); ?>
</div>
<div class="review-label typography-text-xs-400 text-neutral-tertiary">Database</div>
</div>
<div class="review-row">
<div class="typography-text-m-500 text-neutral-primary" data-review-value="httpPort">
<?php echo htmlspecialchars((string) $defaultHttpPort, ENT_QUOTES, 'UTF-8'); ?>
</div>
<div class="review-label typography-text-xs-400 text-neutral-tertiary">HTTP port</div>
</div>
<div class="review-row">
<div class="typography-text-m-500 text-neutral-primary" data-review-value="httpsPort">
<?php echo htmlspecialchars((string) $defaultHttpsPort, ENT_QUOTES, 'UTF-8'); ?>
</div>
<div class="review-label typography-text-xs-400 text-neutral-tertiary">HTTPS port</div>
</div>
<div class="review-row">
<div class="typography-text-m-500 text-neutral-primary" data-review-value="emailCertificates">
<?php echo htmlspecialchars((string) $defaultEmailCertificates, ENT_QUOTES, 'UTF-8'); ?>
</div>
<div class="review-label typography-text-xs-400 text-neutral-tertiary">SSL certificate email</div>
</div>
<div class="review-row">
<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>
<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>
</div>
</div>
</div>
</div>
</div>
@@ -0,0 +1,53 @@
<?php
$isUpgrade = $isUpgrade ?? false;
?>
<div class="step-layout install-layout" data-step="5">
<div class="install-card">
<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…'; ?>
</div>
</div>
<div class="install-list" data-install-list></div>
</div>
</div>
<template id="install-row-template">
<div class="install-row" data-status="in-progress">
<div class="install-row-main">
<div class="install-row-label">
<span class="install-icon" aria-hidden="true">
<span class="install-icon-spinner">
<?php include __DIR__ . '/../../icons/install-spinner.svg'; ?>
</span>
<span class="install-icon-check">
<?php include __DIR__ . '/../../icons/install-check.svg'; ?>
</span>
<span class="install-icon-error">
<?php include __DIR__ . '/../../icons/exclamation-circle.svg'; ?>
</span>
</span>
<span class="install-text typography-text-m-400 text-neutral-primary" data-install-text></span>
</div>
<button type="button" class="install-row-toggle" aria-expanded="false" data-install-toggle>
<?php include __DIR__ . '/../../icons/chevron-down.svg'; ?>
</button>
</div>
<div class="install-row-details">
<div class="install-row-details-inner">
<pre class="install-error-code typography-text-xs-400" data-install-trace></pre>
<div class="install-error-actions">
<button type="button" class="button secondary" data-install-retry>
<span class="button-text typography-text-m-500">Retry</span>
</button>
<button type="button" class="button primary is-hidden" data-install-console>
<span class="button-text typography-text-m-500">Navigate to Console</span>
</button>
</div>
</div>
</div>
</div>
</template>
</div>
+61 -9
View File
@@ -112,7 +112,7 @@ Server::setResource('dbForProject', function (Cache $cache, Registry $register,
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant((int) $project->getSequence())
->setTenant($project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
@@ -152,7 +152,7 @@ Server::setResource('getProjectDB', function (Group $pools, Database $dbForPlatf
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant((int) $project->getSequence())
->setTenant($project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
@@ -174,7 +174,7 @@ Server::setResource('getProjectDB', function (Group $pools, Database $dbForPlatf
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant((int) $project->getSequence())
->setTenant($project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
@@ -196,9 +196,8 @@ Server::setResource('getLogsDB', function (Group $pools, Cache $cache, Authoriza
$database = null;
return function (?Document $project = null) use ($pools, $cache, $database, $authorization) {
if ($database !== null && $project !== null && ! $project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant((int) $project->getSequence());
if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant($project->getSequence());
return $database;
}
@@ -213,15 +212,68 @@ Server::setResource('getLogsDB', function (Group $pools, Cache $cache, Authoriza
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER)
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES_WORKER);
// set tenant
if ($project !== null && ! $project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant((int) $project->getSequence());
if ($project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant($project->getSequence());
}
return $database;
};
}, ['pools', 'cache', 'authorization']);
Server::setResource('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', '');
// Backwardscompatibility: 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');
$pool = $pools->get($databaseDSN->getHost());
$adapter = new DatabasePool($pool);
$database = new Database($adapter, $cache);
$database
->setDatabase(APP_DATABASE)
->setAuthorization($authorization);
$database->getAdapter()->setSupportForAttributes($databaseType !== DOCUMENTSDB);
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables, true)) {
$database
->setSharedTables(true)
->setTenant((int) $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']);
Server::setResource('abuseRetention', function () {
return time() - (int) System::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', 86400); // 1 day
});
+9 -5
View File
@@ -13,7 +13,11 @@
"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",
"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"
},
"autoload": {
"psr-4": {
@@ -48,7 +52,6 @@
"appwrite/php-runtimes": "0.19.*",
"appwrite/php-clamav": "2.0.*",
"utopia-php/abuse": "1.2.*",
"utopia-php/agents": "1.2.*",
"utopia-php/analytics": "0.15.*",
"utopia-php/audit": "2.2.*",
"utopia-php/auth": "0.5.*",
@@ -58,6 +61,7 @@
"utopia-php/config": "1.*",
"utopia-php/console": "0.1.*",
"utopia-php/database": "5.*",
"utopia-php/agents": "1.*",
"utopia-php/detector": "0.2.*",
"utopia-php/domains": "1.*",
"utopia-php/emails": "0.6.*",
@@ -69,7 +73,7 @@
"utopia-php/locale": "0.8.*",
"utopia-php/logger": "0.6.*",
"utopia-php/messaging": "0.20.*",
"utopia-php/migration": "1.7.*",
"utopia-php/migration": "1.8.*",
"utopia-php/platform": "0.7.*",
"utopia-php/pools": "1.*",
"utopia-php/span": "1.1.*",
@@ -90,7 +94,8 @@
"spomky-labs/otphp": "11.*",
"webonyx/graphql-php": "14.11.*",
"league/csv": "9.14.*",
"enshrined/svg-sanitize": "0.22.*"
"enshrined/svg-sanitize": "0.22.*",
"utopia-php/di": "0.1.0"
},
"require-dev": {
"ext-fileinfo": "*",
@@ -108,7 +113,6 @@
},
"config": {
"platform": {
"php": "8.3"
},
"allow-plugins": {
"php-http/discovery": true,
Generated
+121 -123
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "ff0d44c80f0ea7ff7d700533e5d81786",
"content-hash": "3416a116f2912385f16498aea77ce6a3",
"packages": [
{
"name": "adhocore/jwt",
@@ -686,23 +686,23 @@
},
{
"name": "google/protobuf",
"version": "v4.33.5",
"version": "v4.33.6",
"source": {
"type": "git",
"url": "https://github.com/protocolbuffers/protobuf-php.git",
"reference": "ebe8010a61b2ae0cff0d246fe1c4d44e9f7dfa6d"
"reference": "84b008c23915ed94536737eae46f41ba3bccfe67"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/ebe8010a61b2ae0cff0d246fe1c4d44e9f7dfa6d",
"reference": "ebe8010a61b2ae0cff0d246fe1c4d44e9f7dfa6d",
"url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/84b008c23915ed94536737eae46f41ba3bccfe67",
"reference": "84b008c23915ed94536737eae46f41ba3bccfe67",
"shasum": ""
},
"require": {
"php": ">=8.1.0"
},
"require-dev": {
"phpunit/phpunit": ">=5.0.0 <8.5.27"
"phpunit/phpunit": ">=10.5.62 <11.0.0"
},
"suggest": {
"ext-bcmath": "Need to support JSON deserialization"
@@ -724,9 +724,9 @@
"proto"
],
"support": {
"source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.33.5"
"source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.33.6"
},
"time": "2026-01-29T20:49:00+00:00"
"time": "2026-03-18T17:32:05+00:00"
},
{
"name": "halaxa/json-machine",
@@ -1996,16 +1996,16 @@
},
{
"name": "phpseclib/phpseclib",
"version": "3.0.49",
"version": "3.0.50",
"source": {
"type": "git",
"url": "https://github.com/phpseclib/phpseclib.git",
"reference": "6233a1e12584754e6b5daa69fe1289b47775c1b9"
"reference": "aa6ad8321ed103dc3624fb600a25b66ebf78ec7b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/6233a1e12584754e6b5daa69fe1289b47775c1b9",
"reference": "6233a1e12584754e6b5daa69fe1289b47775c1b9",
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/aa6ad8321ed103dc3624fb600a25b66ebf78ec7b",
"reference": "aa6ad8321ed103dc3624fb600a25b66ebf78ec7b",
"shasum": ""
},
"require": {
@@ -2086,7 +2086,7 @@
],
"support": {
"issues": "https://github.com/phpseclib/phpseclib/issues",
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.49"
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.50"
},
"funding": [
{
@@ -2102,7 +2102,7 @@
"type": "tidelift"
}
],
"time": "2026-01-27T09:17:28+00:00"
"time": "2026-03-19T02:57:58+00:00"
},
{
"name": "psr/clock",
@@ -3606,16 +3606,16 @@
},
{
"name": "utopia-php/cache",
"version": "1.0.0",
"version": "1.0.1",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/cache.git",
"reference": "7068870c086a6aea16173563a26b93ef3e408439"
"reference": "05ceba981436a4022553f7aaa2a05fa049d0f71c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/cache/zipball/7068870c086a6aea16173563a26b93ef3e408439",
"reference": "7068870c086a6aea16173563a26b93ef3e408439",
"url": "https://api.github.com/repos/utopia-php/cache/zipball/05ceba981436a4022553f7aaa2a05fa049d0f71c",
"reference": "05ceba981436a4022553f7aaa2a05fa049d0f71c",
"shasum": ""
},
"require": {
@@ -3652,9 +3652,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/cache/issues",
"source": "https://github.com/utopia-php/cache/tree/1.0.0"
"source": "https://github.com/utopia-php/cache/tree/1.0.1"
},
"time": "2026-01-28T10:55:44+00:00"
"time": "2026-03-12T03:39:09+00:00"
},
{
"name": "utopia-php/cli",
@@ -3850,16 +3850,16 @@
},
{
"name": "utopia-php/database",
"version": "5.3.8",
"version": "5.3.16",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
"reference": "4920bb60afb98d4bd81f4d331765716ae1d40255"
"reference": "f121418c4dc7169576735620fd8c17093c3b851d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/database/zipball/4920bb60afb98d4bd81f4d331765716ae1d40255",
"reference": "4920bb60afb98d4bd81f4d331765716ae1d40255",
"url": "https://api.github.com/repos/utopia-php/database/zipball/f121418c4dc7169576735620fd8c17093c3b851d",
"reference": "f121418c4dc7169576735620fd8c17093c3b851d",
"shasum": ""
},
"require": {
@@ -3868,9 +3868,10 @@
"ext-pdo": "*",
"php": ">=8.4",
"utopia-php/cache": "1.*",
"utopia-php/framework": "0.33.*",
"utopia-php/console": "0.1.*",
"utopia-php/mongo": "1.*",
"utopia-php/pools": "1.*"
"utopia-php/pools": "1.*",
"utopia-php/validators": "0.2.*"
},
"require-dev": {
"fakerphp/faker": "1.23.*",
@@ -3880,7 +3881,7 @@
"phpunit/phpunit": "9.*",
"rregeer/phpunit-coverage-check": "0.3.*",
"swoole/ide-helper": "5.1.3",
"utopia-php/cli": "0.14.*"
"utopia-php/cli": "0.22.*"
},
"type": "library",
"autoload": {
@@ -3902,9 +3903,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/database/issues",
"source": "https://github.com/utopia-php/database/tree/5.3.8"
"source": "https://github.com/utopia-php/database/tree/5.3.16"
},
"time": "2026-03-11T01:03:34+00:00"
"time": "2026-03-19T10:10:02+00:00"
},
{
"name": "utopia-php/detector",
@@ -4058,16 +4059,16 @@
},
{
"name": "utopia-php/domains",
"version": "1.0.2",
"version": "1.0.5",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/domains.git",
"reference": "b4896a6746f0fbe29dfd5e32f7790bd94c1af1e6"
"reference": "0edf6bb2b07f30db849a267027077bf5abb994c6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/domains/zipball/b4896a6746f0fbe29dfd5e32f7790bd94c1af1e6",
"reference": "b4896a6746f0fbe29dfd5e32f7790bd94c1af1e6",
"url": "https://api.github.com/repos/utopia-php/domains/zipball/0edf6bb2b07f30db849a267027077bf5abb994c6",
"reference": "0edf6bb2b07f30db849a267027077bf5abb994c6",
"shasum": ""
},
"require": {
@@ -4114,9 +4115,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/domains/issues",
"source": "https://github.com/utopia-php/domains/tree/1.0.2"
"source": "https://github.com/utopia-php/domains/tree/1.0.5"
},
"time": "2026-02-25T08:18:25+00:00"
"time": "2026-03-03T09:20:50+00:00"
},
{
"name": "utopia-php/dsn",
@@ -4167,16 +4168,16 @@
},
{
"name": "utopia-php/emails",
"version": "0.6.8",
"version": "0.6.9",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/emails.git",
"reference": "25dfcd46ed47b862d2a7e7c98d92a3a4680b6f1b"
"reference": "3a59fb392a03a88f5497e5fdb0ea84a252a4dfdf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/emails/zipball/25dfcd46ed47b862d2a7e7c98d92a3a4680b6f1b",
"reference": "25dfcd46ed47b862d2a7e7c98d92a3a4680b6f1b",
"url": "https://api.github.com/repos/utopia-php/emails/zipball/3a59fb392a03a88f5497e5fdb0ea84a252a4dfdf",
"reference": "3a59fb392a03a88f5497e5fdb0ea84a252a4dfdf",
"shasum": ""
},
"require": {
@@ -4222,9 +4223,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/emails/issues",
"source": "https://github.com/utopia-php/emails/tree/0.6.8"
"source": "https://github.com/utopia-php/emails/tree/0.6.9"
},
"time": "2026-02-09T12:31:56+00:00"
"time": "2026-03-14T13:52:56+00:00"
},
{
"name": "utopia-php/fetch",
@@ -4517,16 +4518,16 @@
},
{
"name": "utopia-php/migration",
"version": "1.7.0",
"version": "1.8.3",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/migration.git",
"reference": "97583ae502e40621ea91a71de19d053c5ae2e558"
"reference": "8633523b3343d492427331b6eec53f020f6ab7a7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/migration/zipball/97583ae502e40621ea91a71de19d053c5ae2e558",
"reference": "97583ae502e40621ea91a71de19d053c5ae2e558",
"url": "https://api.github.com/repos/utopia-php/migration/zipball/8633523b3343d492427331b6eec53f020f6ab7a7",
"reference": "8633523b3343d492427331b6eec53f020f6ab7a7",
"shasum": ""
},
"require": {
@@ -4566,22 +4567,22 @@
],
"support": {
"issues": "https://github.com/utopia-php/migration/issues",
"source": "https://github.com/utopia-php/migration/tree/1.7.0"
"source": "https://github.com/utopia-php/migration/tree/1.8.3"
},
"time": "2026-03-10T06:36:27+00:00"
"time": "2026-03-19T09:18:47+00:00"
},
{
"name": "utopia-php/mongo",
"version": "1.0.0",
"version": "1.0.2",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/mongo.git",
"reference": "45bedf36c2c946ec7a0a3e59b9f12f772de0b01d"
"reference": "677a21c53f7a1316c528b4b45b3fce886cee7223"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/mongo/zipball/45bedf36c2c946ec7a0a3e59b9f12f772de0b01d",
"reference": "45bedf36c2c946ec7a0a3e59b9f12f772de0b01d",
"url": "https://api.github.com/repos/utopia-php/mongo/zipball/677a21c53f7a1316c528b4b45b3fce886cee7223",
"reference": "677a21c53f7a1316c528b4b45b3fce886cee7223",
"shasum": ""
},
"require": {
@@ -4627,9 +4628,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/mongo/issues",
"source": "https://github.com/utopia-php/mongo/tree/1.0.0"
"source": "https://github.com/utopia-php/mongo/tree/1.0.2"
},
"time": "2026-02-12T05:54:06+00:00"
"time": "2026-03-18T02:45:50+00:00"
},
{
"name": "utopia-php/platform",
@@ -5059,16 +5060,16 @@
},
{
"name": "utopia-php/system",
"version": "0.10.0",
"version": "0.10.1",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/system.git",
"reference": "6441a9c180958a373e5ddb330264dd638539dfdb"
"reference": "7c1669533bb9c285de19191270c8c1439161a78a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/system/zipball/6441a9c180958a373e5ddb330264dd638539dfdb",
"reference": "6441a9c180958a373e5ddb330264dd638539dfdb",
"url": "https://api.github.com/repos/utopia-php/system/zipball/7c1669533bb9c285de19191270c8c1439161a78a",
"reference": "7c1669533bb9c285de19191270c8c1439161a78a",
"shasum": ""
},
"require": {
@@ -5109,9 +5110,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/system/issues",
"source": "https://github.com/utopia-php/system/tree/0.10.0"
"source": "https://github.com/utopia-php/system/tree/0.10.1"
},
"time": "2025-10-15T19:12:00+00:00"
"time": "2026-03-15T21:07:41+00:00"
},
{
"name": "utopia-php/telemetry",
@@ -5215,29 +5216,28 @@
},
{
"name": "utopia-php/vcs",
"version": "2.0.0",
"version": "2.0.2",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/vcs.git",
"reference": "058049326e04a2a0c2f0ce8ad00c7e84825aba14"
"reference": "5769679308bad498f2777547d48ab332166c4c0b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/vcs/zipball/058049326e04a2a0c2f0ce8ad00c7e84825aba14",
"reference": "058049326e04a2a0c2f0ce8ad00c7e84825aba14",
"url": "https://api.github.com/repos/utopia-php/vcs/zipball/5769679308bad498f2777547d48ab332166c4c0b",
"reference": "5769679308bad498f2777547d48ab332166c4c0b",
"shasum": ""
},
"require": {
"adhocore/jwt": "^1.1",
"php": ">=8.0",
"utopia-php/cache": "1.0.*",
"utopia-php/framework": "0.*.*",
"utopia-php/system": "0.10.*"
"utopia-php/cache": "1.0.*"
},
"require-dev": {
"laravel/pint": "1.*.*",
"phpstan/phpstan": "1.*.*",
"phpunit/phpunit": "^9.4"
"phpunit/phpunit": "^9.4",
"utopia-php/system": "0.10.*"
},
"type": "library",
"autoload": {
@@ -5258,9 +5258,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/vcs/issues",
"source": "https://github.com/utopia-php/vcs/tree/2.0.0"
"source": "https://github.com/utopia-php/vcs/tree/2.0.2"
},
"time": "2026-02-25T11:36:45+00:00"
"time": "2026-03-13T15:25:16+00:00"
},
{
"name": "utopia-php/websocket",
@@ -5438,16 +5438,16 @@
"packages-dev": [
{
"name": "appwrite/sdk-generator",
"version": "1.11.1",
"version": "1.11.10",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-generator.git",
"reference": "6ff411f26f2750eea05c7598c14bb3a2ada898cb"
"reference": "96e6b79a241fc615627a820107ca64bbd1b550a6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/6ff411f26f2750eea05c7598c14bb3a2ada898cb",
"reference": "6ff411f26f2750eea05c7598c14bb3a2ada898cb",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/96e6b79a241fc615627a820107ca64bbd1b550a6",
"reference": "96e6b79a241fc615627a820107ca64bbd1b550a6",
"shasum": ""
},
"require": {
@@ -5483,22 +5483,22 @@
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
"support": {
"issues": "https://github.com/appwrite/sdk-generator/issues",
"source": "https://github.com/appwrite/sdk-generator/tree/1.11.1"
"source": "https://github.com/appwrite/sdk-generator/tree/1.11.10"
},
"time": "2026-02-25T07:15:19+00:00"
"time": "2026-03-19T05:27:36+00:00"
},
{
"name": "brianium/paratest",
"version": "v7.19.0",
"version": "v7.19.2",
"source": {
"type": "git",
"url": "https://github.com/paratestphp/paratest.git",
"reference": "7c6c29af7c4b406b49ce0c6b0a3a81d3684474e6"
"reference": "66e4f7910cecf67736bccf2b8bd53a2e3eb98bd9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paratestphp/paratest/zipball/7c6c29af7c4b406b49ce0c6b0a3a81d3684474e6",
"reference": "7c6c29af7c4b406b49ce0c6b0a3a81d3684474e6",
"url": "https://api.github.com/repos/paratestphp/paratest/zipball/66e4f7910cecf67736bccf2b8bd53a2e3eb98bd9",
"reference": "66e4f7910cecf67736bccf2b8bd53a2e3eb98bd9",
"shasum": ""
},
"require": {
@@ -5512,9 +5512,9 @@
"phpunit/php-code-coverage": "^12.5.3 || ^13.0.1",
"phpunit/php-file-iterator": "^6.0.1 || ^7",
"phpunit/php-timer": "^8 || ^9",
"phpunit/phpunit": "^12.5.9 || ^13",
"phpunit/phpunit": "^12.5.14 || ^13.0.5",
"sebastian/environment": "^8.0.3 || ^9",
"symfony/console": "^7.4.4 || ^8.0.4",
"symfony/console": "^7.4.7 || ^8.0.7",
"symfony/process": "^7.4.5 || ^8.0.5"
},
"require-dev": {
@@ -5522,11 +5522,11 @@
"ext-pcntl": "*",
"ext-pcov": "*",
"ext-posix": "*",
"phpstan/phpstan": "^2.1.38",
"phpstan/phpstan-deprecation-rules": "^2.0.3",
"phpstan/phpstan-phpunit": "^2.0.12",
"phpstan/phpstan-strict-rules": "^2.0.8",
"symfony/filesystem": "^7.4.0 || ^8.0.1"
"phpstan/phpstan": "^2.1.40",
"phpstan/phpstan-deprecation-rules": "^2.0.4",
"phpstan/phpstan-phpunit": "^2.0.16",
"phpstan/phpstan-strict-rules": "^2.0.10",
"symfony/filesystem": "^7.4.6 || ^8.0.6"
},
"bin": [
"bin/paratest",
@@ -5566,7 +5566,7 @@
],
"support": {
"issues": "https://github.com/paratestphp/paratest/issues",
"source": "https://github.com/paratestphp/paratest/tree/v7.19.0"
"source": "https://github.com/paratestphp/paratest/tree/v7.19.2"
},
"funding": [
{
@@ -5578,7 +5578,7 @@
"type": "paypal"
}
],
"time": "2026-02-06T10:53:26+00:00"
"time": "2026-03-09T14:33:17+00:00"
},
{
"name": "czproject/git-php",
@@ -5767,16 +5767,16 @@
},
{
"name": "laravel/pint",
"version": "v1.27.1",
"version": "v1.29.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/pint.git",
"reference": "54cca2de13790570c7b6f0f94f37896bee4abcb5"
"reference": "bdec963f53172c5e36330f3a400604c69bf02d39"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/pint/zipball/54cca2de13790570c7b6f0f94f37896bee4abcb5",
"reference": "54cca2de13790570c7b6f0f94f37896bee4abcb5",
"url": "https://api.github.com/repos/laravel/pint/zipball/bdec963f53172c5e36330f3a400604c69bf02d39",
"reference": "bdec963f53172c5e36330f3a400604c69bf02d39",
"shasum": ""
},
"require": {
@@ -5787,13 +5787,14 @@
"php": "^8.2.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.93.1",
"illuminate/view": "^12.51.0",
"larastan/larastan": "^3.9.2",
"friendsofphp/php-cs-fixer": "^3.94.2",
"illuminate/view": "^12.54.1",
"larastan/larastan": "^3.9.3",
"laravel-zero/framework": "^12.0.5",
"mockery/mockery": "^1.6.12",
"nunomaduro/termwind": "^2.3.3",
"pestphp/pest": "^3.8.5"
"nunomaduro/termwind": "^2.4.0",
"pestphp/pest": "^3.8.6",
"shipfastlabs/agent-detector": "^1.1.0"
},
"bin": [
"builds/pint"
@@ -5830,7 +5831,7 @@
"issues": "https://github.com/laravel/pint/issues",
"source": "https://github.com/laravel/pint"
},
"time": "2026-02-10T20:00:20+00:00"
"time": "2026-03-12T15:51:39+00:00"
},
{
"name": "matthiasmullie/minify",
@@ -6193,11 +6194,11 @@
},
{
"name": "phpstan/phpstan",
"version": "2.1.40",
"version": "2.1.42",
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/9b2c7aeb83a75d8680ea5e7c9b7fca88052b766b",
"reference": "9b2c7aeb83a75d8680ea5e7c9b7fca88052b766b",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/1279e1ce86ba768f0780c9d889852b4e02ff40d0",
"reference": "1279e1ce86ba768f0780c9d889852b4e02ff40d0",
"shasum": ""
},
"require": {
@@ -6242,7 +6243,7 @@
"type": "github"
}
],
"time": "2026-02-23T15:04:35+00:00"
"time": "2026-03-17T14:58:32+00:00"
},
{
"name": "phpunit/php-code-coverage",
@@ -6984,16 +6985,16 @@
},
{
"name": "sebastian/environment",
"version": "8.0.3",
"version": "8.0.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/environment.git",
"reference": "24a711b5c916efc6d6e62aa65aa2ec98fef77f68"
"reference": "7b8842c2d8e85d0c3a5831236bf5869af6ab2a11"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/24a711b5c916efc6d6e62aa65aa2ec98fef77f68",
"reference": "24a711b5c916efc6d6e62aa65aa2ec98fef77f68",
"url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/7b8842c2d8e85d0c3a5831236bf5869af6ab2a11",
"reference": "7b8842c2d8e85d0c3a5831236bf5869af6ab2a11",
"shasum": ""
},
"require": {
@@ -7036,7 +7037,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/environment/issues",
"security": "https://github.com/sebastianbergmann/environment/security/policy",
"source": "https://github.com/sebastianbergmann/environment/tree/8.0.3"
"source": "https://github.com/sebastianbergmann/environment/tree/8.0.4"
},
"funding": [
{
@@ -7056,7 +7057,7 @@
"type": "tidelift"
}
],
"time": "2025-08-12T14:11:56+00:00"
"time": "2026-03-15T07:05:40+00:00"
},
{
"name": "sebastian/exporter",
@@ -7679,16 +7680,16 @@
},
{
"name": "symfony/console",
"version": "v8.0.4",
"version": "v8.0.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "ace03c4cf9805080ff40cbeec69fca180c339a3b"
"reference": "15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/ace03c4cf9805080ff40cbeec69fca180c339a3b",
"reference": "ace03c4cf9805080ff40cbeec69fca180c339a3b",
"url": "https://api.github.com/repos/symfony/console/zipball/15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a",
"reference": "15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a",
"shasum": ""
},
"require": {
@@ -7745,7 +7746,7 @@
"terminal"
],
"support": {
"source": "https://github.com/symfony/console/tree/v8.0.4"
"source": "https://github.com/symfony/console/tree/v8.0.7"
},
"funding": [
{
@@ -7765,7 +7766,7 @@
"type": "tidelift"
}
],
"time": "2026-01-13T13:06:50+00:00"
"time": "2026-03-06T14:06:22+00:00"
},
{
"name": "symfony/polyfill-ctype",
@@ -8164,16 +8165,16 @@
},
{
"name": "symfony/string",
"version": "v8.0.4",
"version": "v8.0.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
"reference": "758b372d6882506821ed666032e43020c4f57194"
"reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/758b372d6882506821ed666032e43020c4f57194",
"reference": "758b372d6882506821ed666032e43020c4f57194",
"url": "https://api.github.com/repos/symfony/string/zipball/6c9e1108041b5dce21a9a4984b531c4923aa9ec4",
"reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4",
"shasum": ""
},
"require": {
@@ -8230,7 +8231,7 @@
"utf8"
],
"support": {
"source": "https://github.com/symfony/string/tree/v8.0.4"
"source": "https://github.com/symfony/string/tree/v8.0.6"
},
"funding": [
{
@@ -8250,7 +8251,7 @@
"type": "tidelift"
}
],
"time": "2026-01-12T12:37:40+00:00"
"time": "2026-02-09T10:14:57+00:00"
},
{
"name": "textalk/websocket",
@@ -8454,8 +8455,5 @@
"platform-dev": {
"ext-fileinfo": "*"
},
"platform-overrides": {
"php": "8.3"
},
"plugin-api-version": "2.9.0"
}
+69 -12
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
@@ -295,6 +303,7 @@ services:
depends_on:
- ${_APP_DB_HOST:-mongodb}
- redis
- ollama
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -311,6 +320,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 +345,7 @@ services:
depends_on:
- redis
- ${_APP_DB_HOST:-mongodb}
- ollama
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -363,6 +379,7 @@ services:
- ${_APP_DB_HOST:-mongodb}
- request-catcher-sms
- request-catcher-webhook
- ollama
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -393,6 +410,7 @@ services:
depends_on:
- redis
- ${_APP_DB_HOST:-mongodb}
- ollama
volumes:
- appwrite-uploads:/storage/uploads:rw
- appwrite-cache:/storage/cache:rw
@@ -402,6 +420,7 @@ services:
- appwrite-certificates:/storage/certificates:rw
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -458,9 +477,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 +497,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 +524,7 @@ services:
depends_on:
- redis
- ${_APP_DB_HOST:-mongodb}
- ollama
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -629,6 +657,7 @@ services:
depends_on:
- redis
- ${_APP_DB_HOST:-mongodb}
- ollama
volumes:
- appwrite-config:/storage/config:rw
- appwrite-certificates:/storage/certificates:rw
@@ -848,6 +877,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 +1074,7 @@ services:
depends_on:
- redis
- ${_APP_DB_HOST:-mongodb}
- ollama
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -1077,6 +1108,7 @@ services:
depends_on:
- ${_APP_DB_HOST:-mongodb}
- redis
- ollama
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -1107,6 +1139,7 @@ services:
depends_on:
- ${_APP_DB_HOST:-mongodb}
- redis
- ollama
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -1122,6 +1155,7 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DATABASE_SHARED_TABLES
appwrite-task-scheduler-messages:
entrypoint: schedule-messages
@@ -1136,6 +1170,7 @@ services:
depends_on:
- ${_APP_DB_HOST:-mongodb}
- redis
- ollama
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -1227,7 +1262,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
@@ -1251,7 +1285,6 @@ services:
retries: 12
mongodb:
profiles: ["mongodb"]
image: mongo:8.2.5
container_name: appwrite-mongodb
<<: *x-logging
@@ -1287,32 +1320,55 @@ services:
retries: 10
start_period: 30s
appwrite-mongo-express:
image: mongo-express
container_name: appwrite-mongo-express
networks:
- appwrite
ports:
- "8082:8081"
environment:
ME_CONFIG_MONGODB_URL: "mongodb://root:${_APP_DB_ROOT_PASS}@appwrite-mongodb:27017/?replicaSet=rs0&directConnection=true"
ME_CONFIG_BASICAUTH_USERNAME: ${_APP_DB_USER}
ME_CONFIG_BASICAUTH_PASSWORD: ${_APP_DB_PASS}
depends_on:
- mongodb
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/data: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
@@ -1435,3 +1491,4 @@ volumes:
appwrite-sites:
appwrite-builds:
appwrite-config:
appwrite-models:
@@ -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`.
+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.
+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 the collection activity logs list by its unique ID.
@@ -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 the document activity logs list by its unique ID.
@@ -0,0 +1 @@
Get a document by its unique ID. This endpoint response returns a JSON object with the document data.
+1
View File
@@ -0,0 +1 @@
Get index by ID.
+1
View File
@@ -0,0 +1 @@
Get the database activity logs list by its unique ID.
+1
View File
@@ -0,0 +1 @@
Get a database by its unique ID. This endpoint response returns a JSON object with the database metadata.
@@ -0,0 +1 @@
Increment a specific column of a row by a given value.
@@ -0,0 +1 @@
List attributes in the collection.
@@ -0,0 +1 @@
Get a list of all collections that belong to the provided databaseId. You can use the search parameter to filter your results.
@@ -0,0 +1 @@
Get a list of all the user's documents in a given collection. You can use the query params to filter your results.
@@ -0,0 +1 @@
List indexes in the collection.
@@ -0,0 +1 @@
List usage metrics and statistics for all databases in the project. You can view the total number of databases, 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.
+1
View File
@@ -0,0 +1 @@
Get a list of all databases from the current Appwrite project. You can use the search parameter to filter your results.
@@ -0,0 +1 @@
Update a collection by its unique ID.
@@ -0,0 +1 @@
Update a document by its unique ID. Using the patch method you can pass only specific fields that will get updated.
@@ -0,0 +1 @@
Update all documents that match your queries, if no queries are submitted then all documents are updated. You can pass only specific fields to be updated.
+1
View File
@@ -0,0 +1 @@
Update a database by its unique ID.
@@ -0,0 +1 @@
Create or update a 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 or update 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.
+63 -3
View File
@@ -20,10 +20,16 @@ client = Client()
### Make Your First Request
Once your SDK object is set, create any of the Appwrite service objects and choose any request to send. Full documentation for any service method you would like to use can be found in your SDK documentation or in the [API References](https://appwrite.io/docs) section.
All service methods return typed Pydantic models, so you can access response fields as attributes:
```python
users = Users(client)
result = users.create(ID.unique(), email = "email@example.com", phone = "+123456789", password = "password", name = "Walter O'Brien")
user = users.create(ID.unique(), email = "email@example.com", phone = "+123456789", password = "password", name = "Walter O'Brien")
print(user.name) # "Walter O'Brien"
print(user.email) # "email@example.com"
print(user.id) # The generated user ID
```
### Full Example
@@ -43,7 +49,60 @@ client = Client()
users = Users(client)
result = users.create(ID.unique(), email = "email@example.com", phone = "+123456789", password = "password", name = "Walter O'Brien")
user = users.create(ID.unique(), email = "email@example.com", phone = "+123456789", password = "password", name = "Walter O'Brien")
print(user.name) # Access fields as attributes
print(user.to_dict()) # Convert to dictionary if needed
```
### Type Safety with Models
The Appwrite Python SDK provides type safety when working with database rows through generic methods. Methods like `get_row`, `list_rows`, and others accept a `model_type` parameter that allows you to specify your custom Pydantic model for full type safety.
```python
from pydantic import BaseModel
from datetime import datetime
from typing import Optional
from appwrite.client import Client
from appwrite.services.tables_db import TablesDB
# Define your custom model matching your table schema
class Post(BaseModel):
postId: int
authorId: int
title: str
content: str
createdAt: datetime
updatedAt: datetime
isPublished: bool
excerpt: Optional[str] = None
client = Client()
# ... configure your client ...
tables_db = TablesDB(client)
# Fetch a single row with type safety
row = tables_db.get_row(
database_id="your-database-id",
table_id="your-table-id",
row_id="your-row-id",
model_type=Post # Pass your custom model type
)
print(row.data.title) # Fully typed - IDE autocomplete works
print(row.data.postId) # int type, not Any
print(row.data.createdAt) # datetime type
# Fetch multiple rows with type safety
result = tables_db.list_rows(
database_id="your-database-id",
table_id="your-table-id",
model_type=Post
)
for row in result.rows:
print(f"{row.data.title} by {row.data.authorId}")
```
### Error Handling
@@ -52,7 +111,8 @@ The Appwrite Python SDK raises `AppwriteException` object with `message`, `code`
```python
users = Users(client)
try:
result = users.create(ID.unique(), email = "email@example.com", phone = "+123456789", password = "password", name = "Walter O'Brien")
user = users.create(ID.unique(), email = "email@example.com", phone = "+123456789", password = "password", name = "Walter O'Brien")
print(user.name)
except AppwriteException as e:
print(e.message)
```
Executable → Regular
View File
Executable → Regular
View File

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