mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8452dbd9d | ||
|
|
0bbe82a98b | ||
|
|
a7c1649c67 | ||
|
|
a6b875d3da | ||
|
|
c925ad8ff2 | ||
|
|
15cd55cb06 | ||
|
|
5f66306226 | ||
|
|
d924072b82 | ||
|
|
fc0f19b7a8 | ||
|
|
1f3523b24f | ||
|
|
283efcb3c8 | ||
|
|
3f555e3061 |
@@ -39,6 +39,7 @@ _APP_REDIS_HOST=redis
|
||||
_APP_REDIS_PORT=6379
|
||||
_APP_REDIS_PASS=
|
||||
_APP_REDIS_USER=
|
||||
COMPOSE_PROFILES=mariadb,mongodb,postgresql
|
||||
_APP_DB_ADAPTER=mongodb
|
||||
_APP_DB_HOST=mongodb
|
||||
_APP_DB_PORT=27017
|
||||
|
||||
+139
-78
@@ -174,46 +174,22 @@ jobs:
|
||||
outputs:
|
||||
databases: ${{ steps.generate.outputs.databases }}
|
||||
modes: ${{ steps.generate.outputs.modes }}
|
||||
services: ${{ steps.generate.outputs.services }}
|
||||
steps:
|
||||
- name: Generate matrix
|
||||
id: generate
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const databases = [
|
||||
{ name: 'MariaDB', env: '.env,compose/mariadb.env', compose: 'docker-compose.yml:compose/mariadb.yml' },
|
||||
{ name: 'PostgreSQL', env: '.env,compose/postgresql.env', compose: 'docker-compose.yml:compose/postgresql.yml' },
|
||||
{ name: 'MongoDB', env: '.env', compose: 'docker-compose.yml' },
|
||||
];
|
||||
const modes = ['dedicated', 'shared_v1', 'shared_v2'];
|
||||
const allDatabases = ['MariaDB', 'PostgreSQL', 'MongoDB'];
|
||||
const allModes = ['dedicated', 'shared_v1', 'shared_v2'];
|
||||
|
||||
const services = [
|
||||
{ name: 'Account', parallel: true },
|
||||
{ name: 'Avatars', parallel: true, runner: 'blacksmith-4vcpu-ubuntu-2404' },
|
||||
{ name: 'Databases', runner: 'blacksmith-4vcpu-ubuntu-2404' },
|
||||
{ name: 'TablesDB', runner: 'blacksmith-4vcpu-ubuntu-2404' },
|
||||
{ name: 'Functions', runner: 'blacksmith-4vcpu-ubuntu-2404' },
|
||||
{ name: 'FunctionsSchedule', parallel: true },
|
||||
{ name: 'GraphQL', parallel: true },
|
||||
{ name: 'Projects', parallel: true },
|
||||
{ name: 'Realtime', runner: 'blacksmith-4vcpu-ubuntu-2404' },
|
||||
{ name: 'Sites', parallel: true, runner: 'blacksmith-4vcpu-ubuntu-2404' },
|
||||
{ name: 'Teams', parallel: true },
|
||||
{ name: 'Users', parallel: true },
|
||||
{ name: 'ProjectWebhooks', parallel: true },
|
||||
{ name: 'Migrations', parallel: true },
|
||||
{ name: 'Project', parallel: true },
|
||||
];
|
||||
core.setOutput('services', JSON.stringify(services));
|
||||
|
||||
const defaultDatabases = [databases.find(d => d.name === 'MongoDB')];
|
||||
const defaultDatabases = ['MongoDB'];
|
||||
const defaultModes = ['dedicated'];
|
||||
|
||||
const pr = context.payload.pull_request;
|
||||
if (!pr) {
|
||||
core.setOutput('databases', JSON.stringify(databases));
|
||||
core.setOutput('modes', JSON.stringify(modes));
|
||||
core.setOutput('databases', JSON.stringify(allDatabases));
|
||||
core.setOutput('modes', JSON.stringify(allModes));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -234,8 +210,8 @@ jobs:
|
||||
const decode = (content) => JSON.parse(Buffer.from(content, 'base64').toString());
|
||||
const databaseChanged = getDbVersion(decode(base.content)) !== getDbVersion(decode(head.content));
|
||||
|
||||
core.setOutput('databases', JSON.stringify(databaseChanged ? databases : defaultDatabases));
|
||||
core.setOutput('modes', JSON.stringify(databaseChanged ? modes : defaultModes));
|
||||
core.setOutput('databases', JSON.stringify(databaseChanged ? allDatabases : defaultDatabases));
|
||||
core.setOutput('modes', JSON.stringify(databaseChanged ? allModes : defaultModes));
|
||||
|
||||
build:
|
||||
name: Build
|
||||
@@ -312,29 +288,28 @@ jobs:
|
||||
run: docker compose exec -T appwrite vars
|
||||
|
||||
- name: Run Unit Tests
|
||||
timeout-minutes: 15
|
||||
run: >-
|
||||
docker compose exec
|
||||
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}"
|
||||
appwrite test /usr/src/code/tests/unit
|
||||
uses: itznotabug/php-retry@v3
|
||||
with:
|
||||
max_attempts: 2
|
||||
retry_wait_seconds: 60
|
||||
timeout_minutes: 15
|
||||
job_id: ${{ job.check_run_id }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
test_dir: tests/unit
|
||||
command: >-
|
||||
docker compose exec
|
||||
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}"
|
||||
appwrite test /usr/src/code/tests/unit
|
||||
|
||||
e2e_general:
|
||||
name: Tests / E2E / ${{ matrix.database.name }} (${{ matrix.mode }}) / General
|
||||
name: Tests / E2E / General
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build, matrix]
|
||||
env:
|
||||
COMPOSE_FILE: ${{ matrix.database.compose }}
|
||||
COMPOSE_ENV_FILES: ${{ matrix.database.env }}
|
||||
needs: build
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
database: ${{ fromJSON(needs.matrix.outputs.databases) }}
|
||||
mode: ${{ fromJSON(needs.matrix.outputs.modes) }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
- name: checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Download Docker Image
|
||||
@@ -351,10 +326,6 @@ jobs:
|
||||
|
||||
- name: Load and Start Appwrite
|
||||
timeout-minutes: 5
|
||||
env:
|
||||
_APP_BROWSER_HOST: http://invalid-browser/v1
|
||||
_APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }}
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker compose pull --quiet --ignore-buildable
|
||||
@@ -368,12 +339,19 @@ jobs:
|
||||
sleep 1
|
||||
done
|
||||
|
||||
- name: Run tests
|
||||
timeout-minutes: 20
|
||||
run: >-
|
||||
docker compose exec -T
|
||||
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}"
|
||||
appwrite vendor/bin/paratest --processes $(nproc) --functional --testsuite GeneralGroup --exclude-group abuseEnabled --exclude-group screenshots
|
||||
- name: Run General Tests
|
||||
uses: itznotabug/php-retry@v3
|
||||
with:
|
||||
max_attempts: 2
|
||||
retry_wait_seconds: 60
|
||||
timeout_minutes: 15
|
||||
job_id: ${{ job.check_run_id }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
test_dir: tests/e2e/General
|
||||
command: >-
|
||||
docker compose exec -T
|
||||
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}"
|
||||
appwrite test /usr/src/code/tests/e2e/General
|
||||
|
||||
- name: Failure Logs
|
||||
if: failure()
|
||||
@@ -382,12 +360,9 @@ jobs:
|
||||
docker compose logs
|
||||
|
||||
e2e_service:
|
||||
name: Tests / E2E / ${{ matrix.database.name }} (${{ matrix.mode }}) / ${{ matrix.service.name }}
|
||||
runs-on: ${{ matrix.service.runner || 'ubuntu-latest' }}
|
||||
name: Tests / E2E / ${{ matrix.database }} (${{ matrix.mode }}) / ${{ matrix.service }}
|
||||
runs-on: ${{ matrix.runner || 'ubuntu-latest' }}
|
||||
needs: [build, matrix]
|
||||
env:
|
||||
COMPOSE_FILE: ${{ matrix.database.compose }}
|
||||
COMPOSE_ENV_FILES: ${{ matrix.database.env }}
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
@@ -396,7 +371,45 @@ jobs:
|
||||
matrix:
|
||||
database: ${{ fromJSON(needs.matrix.outputs.databases) }}
|
||||
mode: ${{ fromJSON(needs.matrix.outputs.modes) }}
|
||||
service: ${{ fromJSON(needs.matrix.outputs.services) }}
|
||||
service: [
|
||||
Account,
|
||||
Avatars,
|
||||
Console,
|
||||
Databases,
|
||||
TablesDB,
|
||||
Functions,
|
||||
FunctionsSchedule,
|
||||
GraphQL,
|
||||
Health,
|
||||
Locale,
|
||||
Projects,
|
||||
Realtime,
|
||||
Sites,
|
||||
Proxy,
|
||||
Storage,
|
||||
Tokens,
|
||||
Teams,
|
||||
Users,
|
||||
ProjectWebhooks,
|
||||
Webhooks,
|
||||
VCS,
|
||||
Messaging,
|
||||
Migrations,
|
||||
Project
|
||||
]
|
||||
include:
|
||||
- service: Databases
|
||||
runner: blacksmith-4vcpu-ubuntu-2404
|
||||
- service: Sites
|
||||
runner: blacksmith-4vcpu-ubuntu-2404
|
||||
- service: Functions
|
||||
runner: blacksmith-4vcpu-ubuntu-2404
|
||||
- service: Avatars
|
||||
runner: blacksmith-4vcpu-ubuntu-2404
|
||||
- service: Realtime
|
||||
runner: blacksmith-4vcpu-ubuntu-2404
|
||||
- service: TablesDB
|
||||
runner: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
@@ -407,6 +420,25 @@ jobs:
|
||||
name: ${{ env.IMAGE }}
|
||||
path: /tmp
|
||||
|
||||
- name: Set database environment
|
||||
run: |
|
||||
if [ "${{ matrix.database }}" = "MariaDB" ]; then
|
||||
echo "COMPOSE_PROFILES=mariadb" >> $GITHUB_ENV
|
||||
echo "_APP_DB_ADAPTER=mariadb" >> $GITHUB_ENV
|
||||
echo "_APP_DB_HOST=mariadb" >> $GITHUB_ENV
|
||||
echo "_APP_DB_PORT=3306" >> $GITHUB_ENV
|
||||
elif [ "${{ matrix.database }}" = "MongoDB" ]; then
|
||||
echo "COMPOSE_PROFILES=mongodb" >> $GITHUB_ENV
|
||||
echo "_APP_DB_ADAPTER=mongodb" >> $GITHUB_ENV
|
||||
echo "_APP_DB_HOST=mongodb" >> $GITHUB_ENV
|
||||
echo "_APP_DB_PORT=27017" >> $GITHUB_ENV
|
||||
elif [ "${{ matrix.database }}" = "PostgreSQL" ]; then
|
||||
echo "COMPOSE_PROFILES=postgresql" >> $GITHUB_ENV
|
||||
echo "_APP_DB_ADAPTER=postgresql" >> $GITHUB_ENV
|
||||
echo "_APP_DB_HOST=postgresql" >> $GITHUB_ENV
|
||||
echo "_APP_DB_PORT=5432" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
@@ -433,11 +465,26 @@ jobs:
|
||||
done
|
||||
|
||||
- name: Run tests
|
||||
timeout-minutes: 20
|
||||
run: |
|
||||
docker compose exec -T \
|
||||
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \
|
||||
appwrite vendor/bin/paratest --processes $(nproc) ${{ matrix.service.parallel && '--functional' || '' }} --testsuite ${{ matrix.service.name }} --exclude-group abuseEnabled --exclude-group screenshots
|
||||
uses: itznotabug/php-retry@v3
|
||||
with:
|
||||
max_attempts: 2
|
||||
retry_wait_seconds: 60
|
||||
timeout_minutes: 20
|
||||
job_id: ${{ job.check_run_id }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
test_dir: tests/e2e/Services/${{ matrix.service }}
|
||||
command: |
|
||||
SERVICE_PATH="/usr/src/code/tests/e2e/Services/${{ matrix.service }}"
|
||||
|
||||
# Services that rely on sequential test method execution (shared static state)
|
||||
FUNCTIONAL_FLAG="--functional"
|
||||
case "${{ matrix.service }}" in
|
||||
Databases|TablesDB|Functions|Realtime) FUNCTIONAL_FLAG="" ;;
|
||||
esac
|
||||
|
||||
docker compose exec -T \
|
||||
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \
|
||||
appwrite vendor/bin/paratest --processes $(nproc) $FUNCTIONAL_FLAG "$SERVICE_PATH" --exclude-group abuseEnabled --exclude-group screenshots --log-junit tests/e2e/Services/${{ matrix.service }}/junit.xml
|
||||
|
||||
- name: Failure Logs
|
||||
if: failure()
|
||||
@@ -484,11 +531,18 @@ jobs:
|
||||
docker compose up -d --quiet-pull --wait
|
||||
|
||||
- name: Run tests
|
||||
timeout-minutes: 15
|
||||
run: >-
|
||||
docker compose exec -T
|
||||
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}"
|
||||
appwrite test /usr/src/code/tests/e2e --group=abuseEnabled
|
||||
uses: itznotabug/php-retry@v3
|
||||
with:
|
||||
max_attempts: 2
|
||||
retry_wait_seconds: 60
|
||||
timeout_minutes: 15
|
||||
job_id: ${{ job.check_run_id }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
test_dir: tests/e2e
|
||||
command: >-
|
||||
docker compose exec -T
|
||||
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}"
|
||||
appwrite test /usr/src/code/tests/e2e --group=abuseEnabled
|
||||
|
||||
- name: Failure Logs
|
||||
if: failure()
|
||||
@@ -542,11 +596,18 @@ jobs:
|
||||
done
|
||||
|
||||
- name: Run tests
|
||||
timeout-minutes: 15
|
||||
run: >-
|
||||
docker compose exec -T
|
||||
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}"
|
||||
appwrite test /usr/src/code/tests/e2e/Services/Sites --group=screenshots
|
||||
uses: itznotabug/php-retry@v3
|
||||
with:
|
||||
max_attempts: 2
|
||||
retry_wait_seconds: 60
|
||||
timeout_minutes: 15
|
||||
job_id: ${{ job.check_run_id }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
test_dir: tests/e2e/Services/Sites
|
||||
command: >-
|
||||
docker compose exec -T
|
||||
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}"
|
||||
appwrite test /usr/src/code/tests/e2e/Services/Sites --group=screenshots
|
||||
|
||||
- name: Failure Logs
|
||||
if: failure()
|
||||
|
||||
@@ -1,120 +1,107 @@
|
||||
# Appwrite
|
||||
# AGENTS.md
|
||||
|
||||
Self-hosted Backend-as-a-Service platform. Hybrid monolithic-microservice architecture built with PHP 8.3+ on Swoole, delivered as Docker containers.
|
||||
Appwrite is an end-to-end backend server for web, mobile, native, and backend apps. This guide provides context and instructions for AI coding agents working on the Appwrite codebase.
|
||||
|
||||
## Commands
|
||||
## Project Overview
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `docker compose up -d --force-recreate --build` | Build and start all services |
|
||||
| `docker compose exec appwrite test tests/e2e/Services/[Service]` | Run E2E tests for a service |
|
||||
| `docker compose exec appwrite test tests/e2e/Services/[Service] --filter=[Method]` | Run a single test method |
|
||||
| `docker compose exec appwrite test tests/unit/` | Run unit tests |
|
||||
| `composer format` | Auto-format code (Pint, PSR-12) |
|
||||
| `composer format <file>` | Format a specific file |
|
||||
| `composer lint <file>` | Check formatting of a file |
|
||||
| `composer analyze` | Static analysis (PHPStan level 3) |
|
||||
| `composer check` | Same as `analyze` |
|
||||
Appwrite is a self-hosted Backend-as-a-Service (BaaS) platform that provides developers with a set of APIs and tools to build secure, scalable applications. The project uses a hybrid monolithic-microservice architecture built with PHP, running on Swoole for high performance.
|
||||
|
||||
## Stack
|
||||
**Key Technologies:**
|
||||
- **Backend:** PHP 8.3+, Swoole
|
||||
- **Libraries:** Utopia PHP
|
||||
- **Database:** MariaDB, Redis
|
||||
- **Cache:** Redis
|
||||
- **Queue:** Redis
|
||||
- **Containers:** Docker
|
||||
|
||||
- PHP 8.3+, Swoole 6.x (async runtime, replaces PHP-FPM)
|
||||
- Utopia PHP framework (HTTP routing, CLI, DI, queue)
|
||||
- MongoDB (default), MariaDB, PostgreSQL (adapters via utopia-php/database; use `compose/mariadb.yml` or `compose/postgresql.yml` overlays to switch)
|
||||
- Redis (cache, queue, pub/sub)
|
||||
- Docker + Traefik (reverse proxy)
|
||||
- PHPUnit 12, Pint (PSR-12), PHPStan level 3
|
||||
## Development Commands
|
||||
|
||||
## Project layout
|
||||
```bash
|
||||
# Run Appwrite
|
||||
docker compose up -d --force-recreate --build
|
||||
|
||||
- **src/Appwrite/Platform/Modules/** -- feature modules (Account, Avatars, Compute, Console, Databases, Functions, Health, Project, Projects, Proxy, Sites, Storage, Teams, Tokens, VCS, Webhooks)
|
||||
- **src/Appwrite/Platform/Workers/** -- background job workers
|
||||
- **src/Appwrite/Platform/Tasks/** -- CLI tasks
|
||||
- **app/init.php** -- bootstrap (registers services, resources, listeners)
|
||||
- **app/init/** -- configs, constants, locales, models, registers, resources, span, database filters/formats
|
||||
- **bin/** -- CLI entry points: `worker-*` (14 workers), `schedule-*`, `queue-*`, plus `doctor`, `install`, `migrate`, `realtime`, `upgrade`, `ssl`, `vars`, `maintenance`, `interval`, `specs`, `sdks`, etc.
|
||||
- **tests/e2e/** -- end-to-end tests per service
|
||||
- **tests/unit/** -- unit tests
|
||||
- **public/** -- static assets and generated SDKs
|
||||
# Run specific test
|
||||
docker compose exec appwrite test /usr/src/code/tests/e2e/Services/[ServiceName] --filter=[FunctionName]
|
||||
|
||||
## Module structure
|
||||
|
||||
Each module under `src/Appwrite/Platform/Modules/{Name}/` contains:
|
||||
|
||||
```
|
||||
Module.php -- registers all services for the module
|
||||
Services/Http.php -- registers HTTP endpoints
|
||||
Services/Workers.php -- registers background workers
|
||||
Services/Tasks.php -- registers CLI tasks
|
||||
Http/{Service}/ -- endpoint actions (Create.php, Get.php, Update.php, Delete.php, XList.php)
|
||||
Workers/ -- worker implementations
|
||||
Tasks/ -- CLI task implementations
|
||||
# Format code
|
||||
composer format
|
||||
```
|
||||
|
||||
HTTP endpoint nesting reflects the URL path. Sub-resources get subdirectories. For example, within the Functions module:
|
||||
`Http/Deployments/Template/Create.php` -> `POST /v1/functions/:functionId/deployments/template`
|
||||
## Code Style Guidelines
|
||||
|
||||
File names in Http directories must only be `Get.php`, `Create.php`, `Update.php`, `Delete.php`, or `XList.php`. For non-CRUD operations, model the endpoint as a property update. For example, updating a team membership status lives at `Teams/Http/Memberships/Status/Update.php` (`PATCH /v1/teams/:teamId/memberships/:membershipId/status`).
|
||||
- Follow [PSR-12](https://www.php-fig.org/psr/psr-12/) coding standard
|
||||
- Use PSR-4 autoloading
|
||||
- Strict type declarations where applicable
|
||||
- Comprehensive PHPDoc comments
|
||||
|
||||
Register new modules in `src/Appwrite/Platform/Appwrite.php`. Detailed module guide: `src/Appwrite/Platform/AGENTS.md`.
|
||||
### Naming Conventions
|
||||
|
||||
## Action pattern (HTTP endpoints)
|
||||
#### `resourceType` Naming Rule
|
||||
|
||||
When a collection has a combination of `resourceType`, `resourceId`, and/or `resourceInternalId`, the value of `resourceType` MUST always be **plural** - for example: `functions`, `sites`, `deployments`.
|
||||
|
||||
Examples:
|
||||
```php
|
||||
class Create extends Action
|
||||
{
|
||||
public static function getName(): string { return 'createTeam'; }
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
|
||||
->setHttpPath('/v1/teams')
|
||||
->desc('Create team')
|
||||
->groups(['api', 'teams'])
|
||||
->label('event', 'teams.[teamId].create')
|
||||
->label('scope', 'teams.write')
|
||||
->param('teamId', '', new CustomId(), 'Team ID.')
|
||||
->param('name', null, new Text(128), 'Team name.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(
|
||||
string $teamId,
|
||||
string $name,
|
||||
Response $response,
|
||||
Database $dbForProject,
|
||||
Event $queueForEvents,
|
||||
): void {
|
||||
// implementation
|
||||
}
|
||||
}
|
||||
'resourceType' => 'functions'
|
||||
'resourceType' => 'sites'
|
||||
'resourceType' => 'deployments'
|
||||
```
|
||||
|
||||
Common injections: `$response`, `$request`, `$dbForProject`, `$dbForPlatform`, `$user`, `$project`, `$queueForEvents`, `$queueForMails`, `$queueForDeletes`.
|
||||
## Performance Patterns
|
||||
|
||||
## Conventions
|
||||
### Document Update Optimization
|
||||
|
||||
- PSR-12 formatting enforced by Pint. PSR-4 autoloading.
|
||||
- `resourceType` values are always **plural**: `'functions'`, `'sites'`, `'deployments'`.
|
||||
- When updating documents, pass only changed attributes as a sparse Document:
|
||||
```php
|
||||
// correct
|
||||
$dbForProject->updateDocument('users', $user->getId(), new Document([
|
||||
'name' => $name,
|
||||
]));
|
||||
// incorrect -- passing full document is inefficient
|
||||
$user->setAttribute('name', $name);
|
||||
$dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
```
|
||||
Exceptions: migrations, `array_merge()` with `getArrayCopy()`, updates where nearly all attributes change, complex nested relationship logic requiring full document state.
|
||||
- Avoid introducing dependencies outside the `utopia-php` ecosystem.
|
||||
- Never hardcode credentials -- use environment variables.
|
||||
- Code changes may require container restart. No central log location -- check relevant containers.
|
||||
When updating documents, always pass only the changed attributes as a sparse `Document` rather than the full document. This is more efficient because `updateDocument()` internally performs `array_merge($old, $new)`.
|
||||
|
||||
## Cross-repo context
|
||||
**Correct Pattern:**
|
||||
```php
|
||||
// Good: Pass only changed attributes directly
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), new Document([
|
||||
'name' => $name,
|
||||
'email' => $email,
|
||||
]));
|
||||
```
|
||||
|
||||
Appwrite is the base server for `appwrite/cloud`. Changes to the Action pattern, module structure, DI system, or response models affect cloud. The `feat-dedicated-db` feature spans cloud, edge, and console.
|
||||
**Incorrect Pattern:**
|
||||
```php
|
||||
$user->setAttribute('name', $name);
|
||||
$user->setAttribute('email', $email);
|
||||
|
||||
// Bad: Passing full document is inefficient
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
```
|
||||
|
||||
**Exceptions:**
|
||||
- Migration files (need full document updates by design)
|
||||
- Cases already using `array_merge()` with `getArrayCopy()`
|
||||
- Updates where almost all attributes of the document change at once (sparse update provides little benefit compared to passing the full document)
|
||||
- Complex nested relationship logic where full document state is required
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Critical Security Practices
|
||||
|
||||
- **Never hardcode credentials** - Use environment variables
|
||||
- **Rate limiting** - Respect abuse prevention mechanisms
|
||||
|
||||
## Dependencies
|
||||
|
||||
Avoid introducing new dependencies other than utopia-php.
|
||||
|
||||
## Adding new endpoints
|
||||
|
||||
When adding new endpoints, make sure to use modules and follow its patterns. Find instruction in [Modules AGENTS.md](src/Appwrite/Platform/AGENTS.md) file.
|
||||
|
||||
## Pull Request Guidelines
|
||||
### Before Submitting
|
||||
|
||||
- Run `composer format`
|
||||
- Update documentation if adding features
|
||||
- Add/update tests for your changes
|
||||
- Check that Docker build succeeds
|
||||
`docs/specs/authentication.drawio.svg`
|
||||
|
||||
## Known Issues and Gotchas
|
||||
|
||||
- **Hot Reload:** Code changes require container restart in some cases
|
||||
- **Logging:** There is no central place for logs, so when debugging, ensure to check all possibly relevant containers
|
||||
|
||||
+1
-1
@@ -296,7 +296,7 @@ Appwrite stack is a combination of a variety of open-source technologies and too
|
||||
### Other Technologies
|
||||
|
||||
- Redis - for managing cache and in-memory data (currently, we do not use Redis for persistent data).
|
||||
- MongoDB - default database for storage and queries (MariaDB and PostgreSQL also supported via compose overlays).
|
||||
- MariaDB - for database storage and queries.
|
||||
- InfluxDB - for managing stats and time-series based data
|
||||
- Statsd - for sending data over UDP protocol (using Telegraf)
|
||||
- ClamAV - for validating and scanning storage files.
|
||||
|
||||
@@ -64,7 +64,7 @@ return [
|
||||
[
|
||||
'$id' => ID::custom('database'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'size' => 2000,
|
||||
'size' => 128,
|
||||
'required' => false,
|
||||
'signed' => true,
|
||||
'array' => false,
|
||||
@@ -841,6 +841,28 @@ return [
|
||||
'array' => true,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('providerBranches'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 128,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => [],
|
||||
'array' => true,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('providerPaths'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 128,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => [],
|
||||
'array' => true,
|
||||
'filters' => [],
|
||||
],
|
||||
],
|
||||
'indexes' => [
|
||||
[
|
||||
@@ -1320,6 +1342,28 @@ return [
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('providerBranches'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 128,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => [],
|
||||
'array' => true,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('providerPaths'),
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 128,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => [],
|
||||
'array' => true,
|
||||
'filters' => [],
|
||||
],
|
||||
],
|
||||
'indexes' => [
|
||||
[
|
||||
|
||||
+24
-35
@@ -250,16 +250,26 @@ return [
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
APP_SDK_PLATFORM_STATIC => [
|
||||
'key' => APP_SDK_PLATFORM_STATIC,
|
||||
'name' => 'Static',
|
||||
'description' => 'SDK artifacts for Appwrite integrations that do not require a generated platform API specification.',
|
||||
'enabled' => true,
|
||||
'beta' => false,
|
||||
'sdks' => [
|
||||
[
|
||||
'key' => 'markdown',
|
||||
'name' => 'Markdown',
|
||||
'version' => '0.3.0',
|
||||
'url' => 'https://github.com/appwrite/sdk-for-md.git',
|
||||
'package' => 'https://www.npmjs.com/package/@appwrite.io/docs',
|
||||
'enabled' => true,
|
||||
'beta' => false,
|
||||
'dev' => false,
|
||||
'hidden' => false,
|
||||
'family' => APP_SDK_PLATFORM_CONSOLE,
|
||||
'prism' => 'markdown',
|
||||
'source' => \realpath(__DIR__ . '/../sdks/console-md'),
|
||||
'gitUrl' => 'git@github.com:appwrite/sdk-for-md.git',
|
||||
'gitRepoName' => 'sdk-for-md',
|
||||
'gitUserName' => 'appwrite',
|
||||
'gitBranch' => 'dev',
|
||||
'repoBranch' => 'main',
|
||||
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/md/CHANGELOG.md'),
|
||||
],
|
||||
[
|
||||
'key' => 'agent-skills',
|
||||
'name' => 'AgentSkills',
|
||||
@@ -269,10 +279,9 @@ return [
|
||||
'beta' => false,
|
||||
'dev' => false,
|
||||
'hidden' => false,
|
||||
'spec' => 'static',
|
||||
'family' => APP_SDK_PLATFORM_STATIC,
|
||||
'family' => APP_SDK_PLATFORM_CONSOLE,
|
||||
'prism' => 'agent-skills',
|
||||
'source' => \realpath(__DIR__ . '/../sdks/static-agent-skills'),
|
||||
'source' => \realpath(__DIR__ . '/../sdks/console-agent-skills'),
|
||||
'gitUrl' => 'git@github.com:appwrite/agent-skills.git',
|
||||
'gitRepoName' => 'agent-skills',
|
||||
'gitUserName' => 'appwrite',
|
||||
@@ -289,10 +298,9 @@ return [
|
||||
'beta' => false,
|
||||
'dev' => false,
|
||||
'hidden' => false,
|
||||
'spec' => 'static',
|
||||
'family' => APP_SDK_PLATFORM_STATIC,
|
||||
'family' => APP_SDK_PLATFORM_CONSOLE,
|
||||
'prism' => 'cursor-plugin',
|
||||
'source' => \realpath(__DIR__ . '/../sdks/static-cursor-plugin'),
|
||||
'source' => \realpath(__DIR__ . '/../sdks/console-cursor-plugin'),
|
||||
'gitUrl' => 'git@github.com:appwrite/cursor-plugin.git',
|
||||
'gitRepoName' => 'cursor-plugin',
|
||||
'gitUserName' => 'appwrite',
|
||||
@@ -486,25 +494,6 @@ return [
|
||||
'gitBranch' => 'dev',
|
||||
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/swift/CHANGELOG.md'),
|
||||
],
|
||||
[
|
||||
'key' => 'rust',
|
||||
'name' => 'Rust',
|
||||
'version' => '0.1.0',
|
||||
'url' => 'https://github.com/appwrite/sdk-for-rust',
|
||||
'package' => 'https://crates.io/crates/appwrite',
|
||||
'enabled' => true,
|
||||
'beta' => true,
|
||||
'dev' => true,
|
||||
'hidden' => false,
|
||||
'family' => APP_SDK_PLATFORM_SERVER,
|
||||
'prism' => 'rust',
|
||||
'source' => \realpath(__DIR__ . '/../sdks/server-rust'),
|
||||
'gitUrl' => 'git@github.com:appwrite/sdk-for-rust.git',
|
||||
'gitRepoName' => 'sdk-for-rust',
|
||||
'gitUserName' => 'appwrite',
|
||||
'gitBranch' => 'dev',
|
||||
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/rust/CHANGELOG.md'),
|
||||
],
|
||||
[
|
||||
'key' => 'graphql',
|
||||
'name' => 'GraphQL',
|
||||
|
||||
@@ -28,18 +28,12 @@ use Utopia\Validator\Text;
|
||||
Http::init()
|
||||
->groups(['graphql'])
|
||||
->inject('project')
|
||||
->inject('user')
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('authorization')
|
||||
->action(function (Document $project, User $user, Request $request, Response $response, Authorization $authorization) {
|
||||
$response->setUser($user);
|
||||
$request->setUser($user);
|
||||
|
||||
->action(function (Document $project, Authorization $authorization) {
|
||||
if (
|
||||
array_key_exists('graphql', $project->getAttribute('apis', []))
|
||||
&& !$project->getAttribute('apis', [])['graphql']
|
||||
&& !($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles()))
|
||||
&& !(User::isPrivileged($authorization->getRoles()) || User::isApp($authorization->getRoles()))
|
||||
) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
|
||||
}
|
||||
|
||||
@@ -1270,16 +1270,7 @@ Http::error()
|
||||
* If not a publishable error, track usage stats. Publishable errors are >= 500 or those explicitly marked as publish=true in errors.php
|
||||
*/
|
||||
if (!$publish && $project->getId() !== 'console') {
|
||||
$errorUser = new DBUser();
|
||||
try {
|
||||
$resolvedUser = $utopia->getResource('user');
|
||||
if ($resolvedUser instanceof DBUser) {
|
||||
$errorUser = $resolvedUser;
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
// User resource may not be available in error context
|
||||
}
|
||||
if (!$errorUser->isPrivileged($authorization->getRoles())) {
|
||||
if (!DBUser::isPrivileged($authorization->getRoles())) {
|
||||
$bus->dispatch(new RequestCompleted(
|
||||
project: $project->getArrayCopy(),
|
||||
request: $request,
|
||||
|
||||
@@ -96,7 +96,7 @@ Http::init()
|
||||
->inject('team')
|
||||
->inject('apiKey')
|
||||
->inject('authorization')
|
||||
->action(function (Http $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, User $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization) {
|
||||
->action(function (Http $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, Document $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey, Authorization $authorization) {
|
||||
$route = $utopia->getRoute();
|
||||
|
||||
/**
|
||||
@@ -419,7 +419,7 @@ Http::init()
|
||||
if (
|
||||
array_key_exists($namespace, $project->getAttribute('services', []))
|
||||
&& ! $project->getAttribute('services', [])[$namespace]
|
||||
&& ! ($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles()))
|
||||
&& ! (User::isPrivileged($authorization->getRoles()) || User::isApp($authorization->getRoles()))
|
||||
) {
|
||||
throw new Exception(Exception::GENERAL_SERVICE_DISABLED);
|
||||
}
|
||||
@@ -483,10 +483,7 @@ Http::init()
|
||||
->inject('telemetry')
|
||||
->inject('platform')
|
||||
->inject('authorization')
|
||||
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $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) {
|
||||
|
||||
$response->setUser($user);
|
||||
$request->setUser($user);
|
||||
->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();
|
||||
@@ -499,7 +496,7 @@ Http::init()
|
||||
if (
|
||||
array_key_exists('rest', $project->getAttribute('apis', []))
|
||||
&& ! $project->getAttribute('apis', [])['rest']
|
||||
&& ! ($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles()))
|
||||
&& ! (User::isPrivileged($authorization->getRoles()) || User::isApp($authorization->getRoles()))
|
||||
) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
|
||||
}
|
||||
@@ -531,8 +528,8 @@ Http::init()
|
||||
$closestLimit = null;
|
||||
|
||||
$roles = $authorization->getRoles();
|
||||
$isPrivilegedUser = $user->isPrivileged($roles);
|
||||
$isAppUser = $user->isApp($roles);
|
||||
$isPrivilegedUser = User::isPrivileged($roles);
|
||||
$isAppUser = User::isApp($roles);
|
||||
|
||||
foreach ($timeLimitArray as $timeLimit) {
|
||||
foreach ($request->getParams() as $key => $value) { // Set request params as potential abuse keys
|
||||
@@ -614,7 +611,7 @@ Http::init()
|
||||
if ($useCache) {
|
||||
$route = $utopia->match($request);
|
||||
$isImageTransformation = $route->getPath() === '/v1/storage/buckets/:bucketId/files/:fileId/preview';
|
||||
$isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && ! $user->isPrivileged($authorization->getRoles());
|
||||
$isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && ! User::isPrivileged($authorization->getRoles());
|
||||
|
||||
$key = $request->cacheIdentifier();
|
||||
$cacheLog = $authorization->skip(fn () => $dbForProject->getDocument('cache', $key));
|
||||
@@ -633,7 +630,7 @@ Http::init()
|
||||
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
|
||||
$isToken = ! $resourceToken->isEmpty() && $resourceToken->getAttribute('bucketInternalId') === $bucket->getSequence();
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
|
||||
if ($bucket->isEmpty() || (! $bucket->getAttribute('enabled') && ! $isAppUser && ! $isPrivilegedUser)) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
@@ -666,7 +663,7 @@ Http::init()
|
||||
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
|
||||
}
|
||||
// Do not update transformedAt if it's a console user
|
||||
if (! $user->isPrivileged($authorization->getRoles())) {
|
||||
if (! User::isPrivileged($authorization->getRoles())) {
|
||||
$transformedAt = $file->getAttribute('transformedAt', '');
|
||||
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $transformedAt) {
|
||||
$file->setAttribute('transformedAt', DateTime::now());
|
||||
@@ -700,7 +697,7 @@ Http::init()
|
||||
->groups(['session'])
|
||||
->inject('user')
|
||||
->inject('request')
|
||||
->action(function (User $user, Request $request) {
|
||||
->action(function (Document $user, Request $request) {
|
||||
if (\str_contains($request->getURI(), 'oauth2')) {
|
||||
return;
|
||||
}
|
||||
@@ -987,7 +984,7 @@ Http::shutdown()
|
||||
}
|
||||
|
||||
if ($project->getId() !== 'console') {
|
||||
if (! $user->isPrivileged($authorization->getRoles())) {
|
||||
if (! User::isPrivileged($authorization->getRoles())) {
|
||||
$bus->dispatch(new RequestCompleted(
|
||||
project: $project->getArrayCopy(),
|
||||
request: $request,
|
||||
|
||||
@@ -36,9 +36,8 @@ Http::init()
|
||||
->inject('request')
|
||||
->inject('project')
|
||||
->inject('geodb')
|
||||
->inject('user')
|
||||
->inject('authorization')
|
||||
->action(function (Http $utopia, Request $request, Document $project, Reader $geodb, User $user, Authorization $authorization) {
|
||||
->action(function (Http $utopia, Request $request, Document $project, Reader $geodb, Authorization $authorization) {
|
||||
$denylist = System::getEnv('_APP_CONSOLE_COUNTRIES_DENYLIST', '');
|
||||
if (!empty($denylist && $project->getId() === 'console')) {
|
||||
$countries = explode(',', $denylist);
|
||||
@@ -51,8 +50,8 @@ Http::init()
|
||||
|
||||
$route = $utopia->match($request);
|
||||
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
$isAppUser = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
|
||||
$isAppUser = User::isApp($authorization->getRoles());
|
||||
|
||||
if ($isAppUser || $isPrivilegedUser) { // Skip limits for app and console devs
|
||||
return;
|
||||
|
||||
+1
-1
@@ -103,7 +103,7 @@ function dispatch(Server $server, int $fd, int $type, $data = null): int
|
||||
$lines = explode("\n", $data, 3);
|
||||
$request = $lines[0];
|
||||
if (count($lines) > 1) {
|
||||
$domain = trim(explode('Host: ', $lines[1])[1] ?? '');
|
||||
$domain = trim(explode('Host: ', $lines[1])[1]);
|
||||
}
|
||||
|
||||
// Sync executions are considered risky
|
||||
|
||||
@@ -97,7 +97,6 @@ const APP_COMPUTE_DEPLOYMENT_MAX_RETENTION = 100 * 365; // 100 years
|
||||
const APP_SDK_PLATFORM_SERVER = 'server';
|
||||
const APP_SDK_PLATFORM_CLIENT = 'client';
|
||||
const APP_SDK_PLATFORM_CONSOLE = 'console';
|
||||
const APP_SDK_PLATFORM_STATIC = 'static';
|
||||
const APP_VCS_GITHUB_USERNAME = 'Appwrite';
|
||||
const APP_VCS_GITHUB_EMAIL = 'team@appwrite.io';
|
||||
const APP_VCS_GITHUB_URL = 'https://github.com/TeamAppwrite';
|
||||
|
||||
@@ -432,10 +432,8 @@ Http::setResource('user', function (string $mode, Document $project, Document $c
|
||||
$jwtUserId = $payload['userId'] ?? '';
|
||||
if (! empty($jwtUserId)) {
|
||||
if ($mode === APP_MODE_ADMIN) {
|
||||
/** @var User $user */
|
||||
$user = $dbForPlatform->getDocument('users', $jwtUserId);
|
||||
} else {
|
||||
/** @var User $user */
|
||||
$user = $dbForProject->getDocument('users', $jwtUserId);
|
||||
}
|
||||
}
|
||||
@@ -455,7 +453,6 @@ Http::setResource('user', function (string $mode, Document $project, Document $c
|
||||
throw new Exception(Exception::USER_API_KEY_AND_SESSION_SET);
|
||||
}
|
||||
|
||||
/** @var User $accountKeyUser */
|
||||
$accountKeyUser = $dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->getDocument('users', $accountKeyUserId));
|
||||
if (! $accountKeyUser->isEmpty()) {
|
||||
$key = $accountKeyUser->find(
|
||||
|
||||
+6
-6
@@ -518,7 +518,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
||||
$project = $consoleDatabase->getAuthorization()->skip(fn () => $consoleDatabase->getDocument('projects', $projectId));
|
||||
$database = getProjectDB($project);
|
||||
|
||||
/** @var User $user */
|
||||
/** @var Appwrite\Utopia\Database\Documents\User $user */
|
||||
$user = $database->getDocument('users', $userId);
|
||||
|
||||
$roles = $user->getRoles($database->getAuthorization());
|
||||
@@ -642,14 +642,10 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, 'Missing or unknown project ID');
|
||||
}
|
||||
|
||||
$timelimit = $app->getResource('timelimit');
|
||||
$user = $app->getResource('user'); /** @var User $user */
|
||||
$logUser = $user;
|
||||
|
||||
if (
|
||||
array_key_exists('realtime', $project->getAttribute('apis', []))
|
||||
&& !$project->getAttribute('apis', [])['realtime']
|
||||
&& !($user->isPrivileged($authorization->getRoles()) || $user->isApp($authorization->getRoles()))
|
||||
&& !(User::isPrivileged($authorization->getRoles()) || User::isApp($authorization->getRoles()))
|
||||
) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
|
||||
}
|
||||
@@ -660,6 +656,10 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'Project is not accessible in this region. Please make sure you are using the correct endpoint');
|
||||
}
|
||||
|
||||
$timelimit = $app->getResource('timelimit');
|
||||
$user = $app->getResource('user'); /** @var User $user */
|
||||
$logUser = $user;
|
||||
|
||||
/*
|
||||
* Abuse Check
|
||||
*
|
||||
|
||||
@@ -13,7 +13,7 @@ $organization = $this->getParam('organization', '');
|
||||
$image = $this->getParam('image', '');
|
||||
$enableAssistant = $this->getParam('enableAssistant', false);
|
||||
$dbService = $this->getParam('database', 'mongodb');
|
||||
$allowedDbServices = ['mariadb', 'mongodb'];
|
||||
$allowedDbServices = ['mariadb', 'mongodb', 'postgresql'];
|
||||
if (!\in_array($dbService, $allowedDbServices, true)) {
|
||||
$dbService = 'mongodb';
|
||||
}
|
||||
@@ -194,7 +194,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
|
||||
appwrite-console:
|
||||
<<: *x-logging
|
||||
container_name: appwrite-console
|
||||
image: <?php echo $organization; ?>/console:7.8.26
|
||||
image: <?php echo $organization; ?>/console:7.6.4
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- appwrite
|
||||
|
||||
@@ -13,7 +13,7 @@ $enabledDatabases = $enabledDatabases ?? ['mongodb', 'mariadb', 'postgresql'];
|
||||
$isLocalInstall = $isLocalInstall ?? false;
|
||||
|
||||
|
||||
$cardStep = ($step === 5) ? 4 : $step;
|
||||
$cardStep = min(4, $step);
|
||||
$stepFile = __DIR__ . "/installer/templates/steps/step-{$cardStep}.phtml";
|
||||
if (!is_file($stepFile)) {
|
||||
$stepFile = __DIR__ . "/installer/templates/steps/step-1.phtml";
|
||||
|
||||
@@ -478,10 +478,6 @@ body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.installer-page[data-upgrade='true'] .installer-step {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.action-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -695,19 +691,6 @@ body {
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
.install-counter {
|
||||
margin-left: auto;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
color: var(--fgcolor-neutral-secondary);
|
||||
}
|
||||
|
||||
.install-row[data-status='in-progress'] .install-counter:not(:empty) {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.install-row-toggle {
|
||||
margin-left: auto;
|
||||
width: 32px;
|
||||
@@ -914,17 +897,6 @@ body {
|
||||
gap: var(--gap-m);
|
||||
}
|
||||
|
||||
.install-global-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: var(--gap-m);
|
||||
padding: var(--space-4) 0;
|
||||
}
|
||||
|
||||
.install-global-actions.is-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.install-error-details .button {
|
||||
align-self: center;
|
||||
margin-top: 0;
|
||||
@@ -1812,92 +1784,3 @@ body {
|
||||
gap: var(--gap-s);
|
||||
}
|
||||
}
|
||||
|
||||
.migration-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--gap-l);
|
||||
padding: var(--space-6);
|
||||
background: var(--bgcolor-neutral-default);
|
||||
border-radius: var(--border-radius-m);
|
||||
outline: var(--border-width-s) solid var(--border-neutral);
|
||||
outline-offset: calc(var(--border-width-s) * -1);
|
||||
cursor: pointer;
|
||||
transition: outline-color 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.migration-option:hover {
|
||||
outline-color: var(--border-neutral-stronger);
|
||||
}
|
||||
|
||||
.migration-option-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.migration-switch {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.migration-switch-track {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 32px;
|
||||
height: 20px;
|
||||
border-radius: 10px;
|
||||
background: var(--bgcolor-neutral-invert-weaker);
|
||||
transition: background 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.migration-switch-thumb {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: var(--bgcolor-neutral-primary);
|
||||
transition: transform 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
#run-migration:checked ~ .migration-switch-track {
|
||||
background: var(--bgcolor-neutral-invert-weak);
|
||||
}
|
||||
|
||||
#run-migration:checked ~ .migration-switch-track .migration-switch-thumb {
|
||||
transform: translateX(12px);
|
||||
}
|
||||
|
||||
#run-migration:focus-visible ~ .migration-switch-track {
|
||||
box-shadow: 0 0 0 var(--border-width-l) var(--border-focus);
|
||||
}
|
||||
|
||||
.migration-hint {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--gap-s);
|
||||
padding: 0 var(--space-2);
|
||||
}
|
||||
|
||||
.migration-hint-icon {
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--fgcolor-neutral-tertiary);
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.migration-hint-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.migration-code {
|
||||
padding: 1px 4px;
|
||||
border-radius: var(--border-radius-xs, 4px);
|
||||
background: var(--bgcolor-neutral-secondary);
|
||||
font-family: monospace;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
const { validateInstallRequest } = window.InstallerStepsProgress || {};
|
||||
|
||||
const isUpgrade = document.body?.dataset.upgrade === 'true';
|
||||
const stepFlow = isUpgrade ? [1, 6, 4, 5] : [1, 2, 3, 4, 5];
|
||||
const stepFlow = isUpgrade ? [1, 4, 5] : [1, 2, 3, 4, 5];
|
||||
const cardSteps = stepFlow.filter((step) => step !== 5);
|
||||
|
||||
const normalizeStep = (step) => {
|
||||
@@ -53,7 +53,7 @@
|
||||
let pendingStep = null;
|
||||
let pendingPushState = false;
|
||||
|
||||
const clampStep = (step) => Math.max(1, Math.min(6, step));
|
||||
const clampStep = (step) => Math.max(1, Math.min(5, step));
|
||||
const isInstallLocked = () => Boolean(window.InstallerSteps?.isInstallLocked?.());
|
||||
|
||||
const scrollToFirstError = (panel) => {
|
||||
@@ -399,18 +399,11 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
if (action === 'next' && String(target) === '5') {
|
||||
if (typeof validateInstallRequest === 'function') {
|
||||
const isValid = await validateInstallRequest();
|
||||
if (!isValid) {
|
||||
return;
|
||||
}
|
||||
if (action === 'next' && String(target) === '5' && typeof validateInstallRequest === 'function') {
|
||||
const isValid = await validateInstallRequest();
|
||||
if (!isValid) {
|
||||
return;
|
||||
}
|
||||
// Clear stale install data from previous runs so initStep5
|
||||
// starts a fresh install instead of trying to resume.
|
||||
const { clearInstallLock, clearInstallId } = window.InstallerStepsState || {};
|
||||
clearInstallLock?.();
|
||||
clearInstallId?.();
|
||||
}
|
||||
if (isInstallLocked() && Number(target) !== 5) {
|
||||
requestStep(5, true);
|
||||
|
||||
@@ -13,10 +13,7 @@
|
||||
DOCKER_COMPOSE: 'docker-compose',
|
||||
ENV_VARS: 'env-vars',
|
||||
DOCKER_CONTAINERS: 'docker-containers',
|
||||
ACCOUNT_SETUP: 'account-setup',
|
||||
MIGRATION: 'migration',
|
||||
SSL_CERTIFICATE: 'ssl-certificate',
|
||||
REDIRECT: 'redirect'
|
||||
ACCOUNT_SETUP: 'account-setup'
|
||||
});
|
||||
|
||||
const STATUS = Object.freeze({
|
||||
@@ -53,11 +50,6 @@
|
||||
id: STEP_IDS.DOCKER_CONTAINERS,
|
||||
inProgress: 'Restarting Docker containers...',
|
||||
done: 'Docker containers restarted'
|
||||
},
|
||||
{
|
||||
id: STEP_IDS.MIGRATION,
|
||||
inProgress: 'Running database migration...',
|
||||
done: 'Database migration completed'
|
||||
}
|
||||
] : [
|
||||
{
|
||||
@@ -83,7 +75,7 @@
|
||||
{
|
||||
id: STEP_IDS.ACCOUNT_SETUP,
|
||||
inProgress: 'Creating Appwrite account...',
|
||||
done: 'Appwrite account created'
|
||||
done: 'Appwrite account created (redirecting...)'
|
||||
}
|
||||
]);
|
||||
|
||||
@@ -101,7 +93,7 @@
|
||||
const clampStep = (step) => {
|
||||
const numeric = Number(step);
|
||||
if (Number.isNaN(numeric)) return 1;
|
||||
return Math.max(1, Math.min(6, numeric));
|
||||
return Math.max(1, Math.min(5, numeric));
|
||||
};
|
||||
|
||||
window.InstallerStepsContext = Object.freeze({
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
storeInstallId,
|
||||
clearInstallId
|
||||
} = window.InstallerStepsState || {};
|
||||
const { extractHostname, isLocalHost, isIPAddress } = window.InstallerStepsValidation || {};
|
||||
const { extractHostname, isLocalHost } = window.InstallerStepsValidation || {};
|
||||
const { generateSecretKey } = window.InstallerStepsUI || {};
|
||||
const { showToast } = window.InstallerToast || {};
|
||||
|
||||
@@ -111,10 +111,10 @@
|
||||
return normalized.summary || 'Installation failed.';
|
||||
}
|
||||
if (status === STATUS.COMPLETED) return step.done;
|
||||
return message || step.inProgress;
|
||||
return step.inProgress;
|
||||
};
|
||||
|
||||
const updateInstallRow = (row, step, status, message, details) => {
|
||||
const updateInstallRow = (row, step, status, message) => {
|
||||
if (!row || !step) return;
|
||||
row.dataset.status = status;
|
||||
row.dataset.step = step.id;
|
||||
@@ -138,15 +138,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
const counter = row.querySelector('[data-install-counter]');
|
||||
if (counter) {
|
||||
const started = details?.containerStarted ?? 0;
|
||||
const total = details?.containerTotal;
|
||||
counter.textContent = (status === STATUS.IN_PROGRESS && total > 0 && started < total)
|
||||
? `${started}/${total}`
|
||||
: '';
|
||||
}
|
||||
|
||||
// Show/hide "Navigate to Console" button for account setup errors
|
||||
const consoleBtn = row.querySelector('[data-install-console]');
|
||||
if (consoleBtn) {
|
||||
@@ -260,7 +251,7 @@
|
||||
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('');
|
||||
};
|
||||
|
||||
const buildRedirectUrl = (protocol) => {
|
||||
const buildRedirectUrl = () => {
|
||||
const dataset = getBodyDataset?.() ?? {};
|
||||
const rawDomain = (formState?.appDomain || dataset.defaultAppDomain || '').trim();
|
||||
if (!rawDomain) return '';
|
||||
@@ -275,53 +266,22 @@
|
||||
} else if (normalizedHost === 'traefik') {
|
||||
host = rawDomain.replace(hostForProtocol, 'localhost');
|
||||
}
|
||||
const port = protocol === 'https' ? httpsPort : httpPort;
|
||||
const defaultPort = protocol === 'https' ? '443' : '80';
|
||||
if (!hasPort && port && port !== defaultPort) {
|
||||
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 normalizeHostname = (rawDomain) => {
|
||||
const hostname = extractHostname?.(rawDomain)?.toLowerCase?.() ?? '';
|
||||
if (hostname === '0.0.0.0' || hostname === 'traefik') return 'localhost';
|
||||
return hostname;
|
||||
};
|
||||
|
||||
const canUseHttps = () => {
|
||||
const dataset = getBodyDataset?.() ?? {};
|
||||
const rawDomain = (formState?.appDomain || dataset.defaultAppDomain || '').trim();
|
||||
const httpsPort = (formState?.httpsPort || dataset.defaultHttpsPort || '').trim();
|
||||
if (!httpsPort || httpsPort === '0') return false;
|
||||
const hostname = normalizeHostname(rawDomain);
|
||||
return !isLocalHost?.(hostname) && !isIPAddress?.(hostname);
|
||||
};
|
||||
|
||||
const pollCertificate = async (domain, port, maxAttempts, intervalMs) => {
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/install/certificate?domain=${encodeURIComponent(domain)}&port=${encodeURIComponent(port)}`,
|
||||
{ cache: 'no-store' }
|
||||
);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.ready) return true;
|
||||
}
|
||||
} catch {
|
||||
// Installer server may have shut down
|
||||
}
|
||||
if (i < maxAttempts - 1) {
|
||||
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const redirectToApp = (protocol) => {
|
||||
const url = buildRedirectUrl(protocol);
|
||||
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;
|
||||
};
|
||||
@@ -358,7 +318,7 @@
|
||||
const normalizedDomain = (formState?.appDomain || '').trim() || 'localhost';
|
||||
const normalizedHttpPort = (formState?.httpPort || '').trim() || '80';
|
||||
const normalizedHttpsPort = (formState?.httpsPort || '').trim() || '443';
|
||||
const normalizedEmail = (formState?.emailCertificates || '').trim() || (formState?.accountEmail || '').trim();
|
||||
const normalizedEmail = (formState?.emailCertificates || '').trim();
|
||||
const normalizedAssistantKey = (formState?.assistantOpenAIKey || '').trim();
|
||||
const normalizedAccountEmail = (formState?.accountEmail || '').trim();
|
||||
const normalizedAccountPassword = (formState?.accountPassword || '').trim();
|
||||
@@ -373,8 +333,7 @@
|
||||
opensslKey: (formState?.opensslKey || '').trim(),
|
||||
assistantOpenAIKey: normalizedAssistantKey,
|
||||
accountEmail: normalizedAccountEmail,
|
||||
accountPassword: normalizedAccountPassword,
|
||||
migrate: formState?.migrate ?? false
|
||||
accountPassword: normalizedAccountPassword
|
||||
};
|
||||
};
|
||||
|
||||
@@ -447,7 +406,6 @@
|
||||
|
||||
const initStep5 = (root) => {
|
||||
if (!root) return;
|
||||
let resolvedProtocol = 'http';
|
||||
|
||||
if (activeInstall?.controller) {
|
||||
activeInstall.controller.abort();
|
||||
@@ -539,7 +497,7 @@
|
||||
if (!state) return;
|
||||
const row = ensureRow(step);
|
||||
if (row) {
|
||||
updateInstallRow(row, step, state.status || STATUS.IN_PROGRESS, state.message, state.details);
|
||||
updateInstallRow(row, step, state.status || STATUS.IN_PROGRESS, state.message);
|
||||
if (state.status === STATUS?.ERROR) {
|
||||
updateInstallErrorDetails(row, {
|
||||
message: state.message,
|
||||
@@ -589,9 +547,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
if (payload.status === STATUS.ERROR) {
|
||||
showGlobalActions();
|
||||
}
|
||||
scheduleFallback();
|
||||
};
|
||||
|
||||
@@ -629,7 +584,6 @@
|
||||
|
||||
const applySnapshot = (snapshot) => {
|
||||
if (!snapshot || !snapshot.steps) return;
|
||||
let hasErrors = false;
|
||||
INSTALLATION_STEPS.forEach((step) => {
|
||||
const detail = snapshot.steps[step.id];
|
||||
if (!detail) return;
|
||||
@@ -638,14 +592,8 @@
|
||||
message: detail.message,
|
||||
details: snapshot.details?.[step.id]
|
||||
});
|
||||
if (detail.status === STATUS.ERROR) {
|
||||
hasErrors = true;
|
||||
}
|
||||
});
|
||||
renderProgress();
|
||||
if (hasErrors) {
|
||||
showGlobalActions();
|
||||
}
|
||||
};
|
||||
|
||||
const checkAllCompleted = () => {
|
||||
@@ -657,7 +605,9 @@
|
||||
const accountState = progressState.get(STEP_IDS.ACCOUNT_SETUP);
|
||||
const sessionDetails = sseSessionDetails || accountState?.details;
|
||||
finalizeInstall();
|
||||
startSslCheck(sessionDetails);
|
||||
notifyInstallComplete(activeInstall?.installId, sessionDetails).finally(() => {
|
||||
setTimeout(() => redirectToApp(), TIMINGS?.redirectDelay ?? 0);
|
||||
});
|
||||
};
|
||||
|
||||
const startPolling = () => {
|
||||
@@ -694,78 +644,6 @@
|
||||
}
|
||||
stopSyncedSpinnerRotation();
|
||||
setUnloadGuard(false);
|
||||
clearInstallLock?.();
|
||||
};
|
||||
|
||||
const SSL_STEP = {
|
||||
id: STEP_IDS.SSL_CERTIFICATE,
|
||||
inProgress: 'Generating SSL certificate...',
|
||||
done: 'SSL certificate verified'
|
||||
};
|
||||
|
||||
const REDIRECT_STEP = {
|
||||
id: STEP_IDS.REDIRECT,
|
||||
inProgress: 'Redirecting to console...',
|
||||
done: 'Redirecting to console...'
|
||||
};
|
||||
|
||||
const showRedirectStep = (sessionDetails, protocol) => {
|
||||
animatePanelHeight(() => {
|
||||
progressState.set(REDIRECT_STEP.id, {
|
||||
status: STATUS.IN_PROGRESS,
|
||||
message: REDIRECT_STEP.inProgress
|
||||
});
|
||||
const row = ensureRow(REDIRECT_STEP);
|
||||
if (row) {
|
||||
updateInstallRow(row, REDIRECT_STEP, STATUS.IN_PROGRESS, REDIRECT_STEP.inProgress);
|
||||
}
|
||||
});
|
||||
startSyncedSpinnerRotation(list);
|
||||
|
||||
const completeId = activeInstall?.installId || getStoredInstallId?.();
|
||||
notifyInstallComplete(completeId, sessionDetails).finally(() => {
|
||||
setTimeout(() => redirectToApp(protocol), TIMINGS?.redirectDelay ?? 0);
|
||||
});
|
||||
};
|
||||
|
||||
const startSslCheck = (sessionDetails) => {
|
||||
if (!canUseHttps()) {
|
||||
showRedirectStep(sessionDetails, 'http');
|
||||
return;
|
||||
}
|
||||
|
||||
animatePanelHeight(() => {
|
||||
progressState.set(SSL_STEP.id, {
|
||||
status: STATUS.IN_PROGRESS,
|
||||
message: SSL_STEP.inProgress
|
||||
});
|
||||
const row = ensureRow(SSL_STEP);
|
||||
if (row) {
|
||||
updateInstallRow(row, SSL_STEP, STATUS.IN_PROGRESS, SSL_STEP.inProgress);
|
||||
}
|
||||
});
|
||||
startSyncedSpinnerRotation(list);
|
||||
|
||||
const dataset = getBodyDataset?.() ?? {};
|
||||
const rawDomain = (formState?.appDomain || dataset.defaultAppDomain || '').trim();
|
||||
const httpsPort = (formState?.httpsPort || dataset.defaultHttpsPort || '443').trim();
|
||||
const domain = normalizeHostname(rawDomain);
|
||||
pollCertificate(domain, httpsPort, 15, 2000).then((ready) => {
|
||||
stopSyncedSpinnerRotation();
|
||||
const certMessage = ready ? SSL_STEP.done : 'Certificate not ready, continuing over HTTP';
|
||||
animatePanelHeight(() => {
|
||||
progressState.set(SSL_STEP.id, {
|
||||
status: STATUS.COMPLETED,
|
||||
message: certMessage
|
||||
});
|
||||
const row = ensureRow(SSL_STEP);
|
||||
if (row) {
|
||||
updateInstallRow(row, SSL_STEP, STATUS.COMPLETED, certMessage);
|
||||
}
|
||||
});
|
||||
resolvedProtocol = ready ? 'https' : 'http';
|
||||
showRedirectStep(sessionDetails, resolvedProtocol);
|
||||
});
|
||||
};
|
||||
|
||||
const startInstallStream = async (installId, options = {}) => {
|
||||
@@ -868,7 +746,9 @@
|
||||
const accountState = progressState.get(STEP_IDS.ACCOUNT_SETUP);
|
||||
const sessionDetails = sseSessionDetails || accountState?.details;
|
||||
finalizeInstall();
|
||||
startSslCheck(sessionDetails);
|
||||
notifyInstallComplete(activeInstall?.installId, sessionDetails).finally(() => {
|
||||
setTimeout(() => redirectToApp(), TIMINGS?.redirectDelay ?? 0);
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (event === SSE_EVENTS.ERROR) {
|
||||
@@ -912,29 +792,9 @@
|
||||
}
|
||||
};
|
||||
|
||||
const isSnapshotTerminal = (snapshot) => {
|
||||
if (!snapshot?.steps) return 'empty';
|
||||
const stepEntries = Object.values(snapshot.steps);
|
||||
if (stepEntries.length === 0) return 'empty';
|
||||
const hasError = stepEntries.some((s) => s.status === STATUS.ERROR);
|
||||
if (hasError) return 'error';
|
||||
const allCompleted = INSTALLATION_STEPS.every((step) => {
|
||||
const detail = snapshot.steps[step.id];
|
||||
return detail && detail.status === STATUS.COMPLETED;
|
||||
});
|
||||
if (allCompleted) return 'completed';
|
||||
return false;
|
||||
};
|
||||
|
||||
const resumeInstall = async (installId) => {
|
||||
const snapshot = await fetchInstallStatus(installId);
|
||||
const terminal = isSnapshotTerminal(snapshot);
|
||||
if (!snapshot || terminal) {
|
||||
if (terminal === 'completed') {
|
||||
return 'completed';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (!snapshot) return false;
|
||||
activeInstall = {
|
||||
installId,
|
||||
controller: new AbortController(),
|
||||
@@ -997,7 +857,7 @@
|
||||
const retryButton = event.target.closest('[data-install-retry]');
|
||||
|
||||
if (consoleButton) {
|
||||
redirectToApp(resolvedProtocol);
|
||||
redirectToApp();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1008,60 +868,6 @@
|
||||
}
|
||||
});
|
||||
|
||||
const globalActions = root.querySelector('[data-install-global-actions]');
|
||||
|
||||
const showGlobalActions = () => {
|
||||
if (globalActions) {
|
||||
globalActions.classList.remove('is-hidden');
|
||||
}
|
||||
};
|
||||
|
||||
const performReset = async (hard) => {
|
||||
const installId = activeInstall?.installId || getInstallLock?.()?.installId || getStoredInstallId?.();
|
||||
|
||||
try {
|
||||
const res = await fetch('/install/reset', {
|
||||
method: 'POST',
|
||||
headers: withCsrfHeader({ 'Content-Type': 'application/json' }),
|
||||
body: JSON.stringify({ installId: installId || '', hard })
|
||||
});
|
||||
if (hard && !res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
showToast?.({
|
||||
status: 'error',
|
||||
title: 'Reset failed',
|
||||
description: data?.message || 'Could not stop containers. Try running "docker compose down -v" manually.',
|
||||
dismissible: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Reset request failed:', e);
|
||||
}
|
||||
|
||||
clearInstallLock?.();
|
||||
clearInstallId?.();
|
||||
cleanupInstallFlow();
|
||||
window.location.href = '/?step=1';
|
||||
};
|
||||
|
||||
const startOverButton = root.querySelector('[data-install-start-over]');
|
||||
if (startOverButton) {
|
||||
startOverButton.addEventListener('click', () => performReset(false));
|
||||
}
|
||||
|
||||
const hardResetButton = root.querySelector('[data-install-hard-reset]');
|
||||
if (hardResetButton) {
|
||||
hardResetButton.addEventListener('click', () => {
|
||||
const confirmed = window.confirm(
|
||||
'This will stop all containers, remove all volumes (including database data, uploads, and certificates), and delete configuration files.\n\nThis action cannot be undone. Continue?'
|
||||
);
|
||||
if (confirmed) {
|
||||
performReset(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// When the user switches back to this tab, check if installation
|
||||
// finished while the tab was in the background.
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
@@ -1070,45 +876,22 @@
|
||||
}
|
||||
});
|
||||
|
||||
const startFreshInstall = () => {
|
||||
clearInstallId?.();
|
||||
clearInstallLock?.();
|
||||
const newInstallId = generateInstallId();
|
||||
storeInstallId?.(newInstallId);
|
||||
startInstallStream(newInstallId);
|
||||
};
|
||||
|
||||
const recoverToLastStep = () => {
|
||||
clearInstallId?.();
|
||||
clearInstallLock?.();
|
||||
const url = new URL(window.location.href);
|
||||
const lastStep = url.searchParams.get('step');
|
||||
// Stay on the current URL so the user keeps their place;
|
||||
// only navigate away if we're already on step 5 (the
|
||||
// progress screen) since there's nothing to show.
|
||||
if (!lastStep || String(lastStep) === '5') {
|
||||
window.location.href = '/?step=1';
|
||||
}
|
||||
};
|
||||
|
||||
const lock = getInstallLock?.();
|
||||
const existingInstallId = lock?.installId || getStoredInstallId?.();
|
||||
if (existingInstallId) {
|
||||
resumeInstall(existingInstallId).then((result) => {
|
||||
if (result === 'completed') {
|
||||
// Install already finished — redirect to console
|
||||
// instead of bouncing back to step 1.
|
||||
stopSyncedSpinnerRotation();
|
||||
setUnloadGuard(false);
|
||||
clearInstallLock?.();
|
||||
resumeInstall(existingInstallId).then((resumed) => {
|
||||
if (!resumed) {
|
||||
clearInstallId?.();
|
||||
startSslCheck(null);
|
||||
} else if (!result) {
|
||||
recoverToLastStep();
|
||||
clearInstallLock?.();
|
||||
const newInstallId = generateInstallId();
|
||||
storeInstallId?.(newInstallId);
|
||||
startInstallStream(newInstallId);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
startFreshInstall();
|
||||
const newInstallId = generateInstallId();
|
||||
storeInstallId?.(newInstallId);
|
||||
startInstallStream(newInstallId);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
|
||||
const INSTALL_LOCK_KEY = 'appwrite-install-lock';
|
||||
const INSTALL_ID_KEY = 'appwrite-install-id';
|
||||
const INSTALL_LOCK_LOCAL_KEY = 'appwrite-install-lock-backup';
|
||||
const INSTALL_ID_LOCAL_KEY = 'appwrite-install-id-backup';
|
||||
|
||||
const formState = {
|
||||
appDomain: null,
|
||||
@@ -57,24 +55,13 @@
|
||||
const getInstallLock = () => {
|
||||
try {
|
||||
const raw = sessionStorage.getItem(INSTALL_LOCK_KEY);
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (parsed && typeof parsed === 'object') return parsed;
|
||||
}
|
||||
} catch (error) {}
|
||||
|
||||
try {
|
||||
const raw = localStorage.getItem(INSTALL_LOCK_LOCAL_KEY);
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (parsed && typeof parsed === 'object') {
|
||||
sessionStorage.setItem(INSTALL_LOCK_KEY, raw);
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
} catch (error) {}
|
||||
|
||||
return null;
|
||||
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) => {
|
||||
@@ -92,9 +79,6 @@
|
||||
try {
|
||||
sessionStorage.setItem(INSTALL_LOCK_KEY, JSON.stringify(lock));
|
||||
} catch (error) {}
|
||||
try {
|
||||
localStorage.setItem(INSTALL_LOCK_LOCAL_KEY, JSON.stringify(lock));
|
||||
} catch (error) {}
|
||||
if (document.body) {
|
||||
document.body.dataset.installLocked = 'true';
|
||||
}
|
||||
@@ -105,9 +89,6 @@
|
||||
try {
|
||||
sessionStorage.removeItem(INSTALL_LOCK_KEY);
|
||||
} catch (error) {}
|
||||
try {
|
||||
localStorage.removeItem(INSTALL_LOCK_LOCAL_KEY);
|
||||
} catch (error) {}
|
||||
if (document.body) {
|
||||
delete document.body.dataset.installLocked;
|
||||
}
|
||||
@@ -140,31 +121,22 @@
|
||||
|
||||
const getStoredInstallId = () => {
|
||||
try {
|
||||
const val = sessionStorage.getItem(INSTALL_ID_KEY);
|
||||
if (val) return val;
|
||||
} catch (error) {}
|
||||
try {
|
||||
return localStorage.getItem(INSTALL_ID_LOCAL_KEY);
|
||||
} catch (error) {}
|
||||
return null;
|
||||
return sessionStorage.getItem(INSTALL_ID_KEY);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const storeInstallId = (installId) => {
|
||||
try {
|
||||
sessionStorage.setItem(INSTALL_ID_KEY, installId);
|
||||
} catch (error) {}
|
||||
try {
|
||||
localStorage.setItem(INSTALL_ID_LOCAL_KEY, installId);
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
const clearInstallId = () => {
|
||||
try {
|
||||
sessionStorage.removeItem(INSTALL_ID_KEY);
|
||||
} catch (error) {}
|
||||
try {
|
||||
localStorage.removeItem(INSTALL_ID_LOCAL_KEY);
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
window.InstallerStepsState = {
|
||||
|
||||
@@ -240,9 +240,6 @@
|
||||
if (key === 'database') {
|
||||
value = toDatabaseLabel(formState?.database);
|
||||
}
|
||||
if (key === 'emailCertificates' && !value) {
|
||||
value = formState?.accountEmail;
|
||||
}
|
||||
if (value) {
|
||||
node.textContent = value;
|
||||
}
|
||||
|
||||
@@ -106,18 +106,12 @@
|
||||
return LOCAL_HOSTS.has(normalized);
|
||||
};
|
||||
|
||||
const isIPAddress = (host) => {
|
||||
if (!host) return false;
|
||||
return isValidIPv4(host) || isValidIPv6(host);
|
||||
};
|
||||
|
||||
window.InstallerStepsValidation = {
|
||||
isValidEmail,
|
||||
isValidPort,
|
||||
isValidPassword,
|
||||
isValidHostnameInput,
|
||||
extractHostname,
|
||||
isLocalHost,
|
||||
isIPAddress
|
||||
isLocalHost
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -329,30 +329,6 @@
|
||||
}
|
||||
};
|
||||
|
||||
const initStep6 = (root) => {
|
||||
if (!root) return;
|
||||
syncInstallLockFlag?.();
|
||||
applyLockPayload?.();
|
||||
applyBodyDefaults?.();
|
||||
|
||||
const checkbox = root.querySelector('#run-migration');
|
||||
if (checkbox) {
|
||||
if (formState.migrate !== undefined) {
|
||||
checkbox.checked = formState.migrate;
|
||||
} else {
|
||||
formState.migrate = checkbox.checked;
|
||||
}
|
||||
checkbox.addEventListener('change', () => {
|
||||
formState.migrate = checkbox.checked;
|
||||
dispatchStateChange?.('migrate');
|
||||
});
|
||||
}
|
||||
|
||||
if (isInstallLocked?.()) {
|
||||
disableControls?.(root);
|
||||
}
|
||||
};
|
||||
|
||||
const initStep = (step, container) => {
|
||||
if (!container) return;
|
||||
const root = container.querySelector('.step-layout') || container;
|
||||
@@ -370,7 +346,6 @@
|
||||
if (normalized === 3) initStep3(root);
|
||||
if (normalized === 4) initStep4(root);
|
||||
if (normalized === 5) Progress.initStep5?.(root);
|
||||
if (normalized === 6) initStep6(root);
|
||||
};
|
||||
|
||||
window.InstallerSteps = {
|
||||
@@ -415,7 +390,10 @@
|
||||
if (!parsePort(httpPort, 'HTTP')) valid = false;
|
||||
if (!parsePort(httpsPort, 'HTTPS')) valid = false;
|
||||
|
||||
if (sslEmail && sslEmail.value.trim() && !isValidEmail?.(sslEmail.value.trim())) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -62,14 +62,12 @@ $badgeClass = $defaultSecretKey !== '' ? 'badge-success' : 'badge-warning';
|
||||
<span class="badge badge-neutral typography-text-xs-400" data-review-assistant-badge>Disabled</span>
|
||||
<div class="review-label typography-text-xs-400 text-neutral-tertiary">Appwrite Assistant</div>
|
||||
</div>
|
||||
<?php if (!$isUpgrade) { ?>
|
||||
<div class="review-row">
|
||||
<span class="badge <?php echo $badgeClass; ?> typography-text-xs-400" data-review-badge>
|
||||
<?php echo htmlspecialchars((string) $badgeLabel, ENT_QUOTES, 'UTF-8'); ?>
|
||||
</span>
|
||||
<div class="review-label typography-text-xs-400 text-neutral-tertiary">Secret API key</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@ $isUpgrade = $isUpgrade ?? false;
|
||||
<div class="install-panel">
|
||||
<div class="install-header">
|
||||
<div class="typography-text-m-400 text-neutral-primary">
|
||||
<?php echo $isUpgrade ? 'Updating Appwrite…' : 'Installing Appwrite…'; ?>
|
||||
<?php echo $isUpgrade ? 'Updating your app…' : 'Installing your app…'; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="install-list" data-install-list></div>
|
||||
@@ -30,7 +30,6 @@ $isUpgrade = $isUpgrade ?? false;
|
||||
</span>
|
||||
<span class="install-text typography-text-m-400 text-neutral-primary" data-install-text></span>
|
||||
</div>
|
||||
<span class="install-counter typography-text-xs-400" data-install-counter></span>
|
||||
<button type="button" class="install-row-toggle" aria-expanded="false" data-install-toggle>
|
||||
<?php include __DIR__ . '/../../icons/chevron-down.svg'; ?>
|
||||
</button>
|
||||
@@ -51,13 +50,4 @@ $isUpgrade = $isUpgrade ?? false;
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="install-global-actions is-hidden" data-install-global-actions>
|
||||
<button type="button" class="button secondary" data-install-start-over>
|
||||
<span class="button-text typography-text-m-500">Start Over</span>
|
||||
</button>
|
||||
<button type="button" class="button secondary" data-install-hard-reset>
|
||||
<span class="button-text typography-text-m-500">Reset Everything</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
<?php
|
||||
$isUpgrade = $isUpgrade ?? false;
|
||||
?>
|
||||
<div class="step-layout" data-step="6">
|
||||
<div class="stack-xl">
|
||||
<div class="stack-xxxs">
|
||||
<h1 class="typography-title-s text-neutral-primary">Database migration</h1>
|
||||
<p class="typography-text-m-400 text-neutral-secondary">
|
||||
Run database migration after the update to apply schema changes.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="stack-xl">
|
||||
<label class="migration-option" for="run-migration">
|
||||
<span class="migration-option-content">
|
||||
<span class="typography-text-m-500 text-neutral-primary">Run migration automatically</span>
|
||||
<span class="typography-text-xs-400 text-neutral-tertiary">Recommended when upgrading to a new version</span>
|
||||
</span>
|
||||
<span class="migration-switch">
|
||||
<input type="checkbox" id="run-migration" name="migrate" class="sr-only" checked>
|
||||
<span class="migration-switch-track" aria-hidden="true">
|
||||
<span class="migration-switch-thumb"></span>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div class="migration-hint">
|
||||
<span class="migration-hint-icon">
|
||||
<?php include __DIR__ . '/../../icons/info.svg'; ?>
|
||||
</span>
|
||||
<span class="typography-text-xs-400 text-neutral-tertiary">
|
||||
To run manually later: <code class="migration-code">docker compose exec appwrite migrate</code>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,3 +0,0 @@
|
||||
_APP_DB_ADAPTER=mariadb
|
||||
_APP_DB_HOST=mariadb
|
||||
_APP_DB_PORT=3306
|
||||
@@ -1,36 +0,0 @@
|
||||
x-logging: &x-logging
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-file: "5"
|
||||
max-size: "10m"
|
||||
|
||||
services:
|
||||
mongodb:
|
||||
scale: 0
|
||||
|
||||
mariadb:
|
||||
image: mariadb:10.11 # fix issues when upgrading using: mysql_upgrade -u root -p
|
||||
container_name: appwrite-mariadb
|
||||
<<: *x-logging
|
||||
networks:
|
||||
- appwrite
|
||||
volumes:
|
||||
- appwrite-mariadb:/var/lib/mysql:rw
|
||||
ports:
|
||||
- "3306:3306"
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=${_APP_DB_ROOT_PASS}
|
||||
- MYSQL_DATABASE=${_APP_DB_SCHEMA}
|
||||
- MYSQL_USER=${_APP_DB_USER}
|
||||
- MYSQL_PASSWORD=${_APP_DB_PASS}
|
||||
- MARIADB_AUTO_UPGRADE=1
|
||||
command: "mysqld --innodb-flush-method=fsync"
|
||||
healthcheck:
|
||||
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
|
||||
volumes:
|
||||
appwrite-mariadb:
|
||||
@@ -1,3 +0,0 @@
|
||||
_APP_DB_ADAPTER=postgresql
|
||||
_APP_DB_HOST=postgresql
|
||||
_APP_DB_PORT=5432
|
||||
@@ -1,3 +0,0 @@
|
||||
services:
|
||||
mongodb:
|
||||
scale: 0
|
||||
Generated
+46
-46
@@ -1226,16 +1226,16 @@
|
||||
},
|
||||
{
|
||||
"name": "open-telemetry/api",
|
||||
"version": "1.9.0",
|
||||
"version": "1.8.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/opentelemetry-php/api.git",
|
||||
"reference": "6f8d237ce2c304ca85f31970f788e7f074d147be"
|
||||
"reference": "df5197c6fd0ddd8e9883b87de042d9341300e2ad"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/opentelemetry-php/api/zipball/6f8d237ce2c304ca85f31970f788e7f074d147be",
|
||||
"reference": "6f8d237ce2c304ca85f31970f788e7f074d147be",
|
||||
"url": "https://api.github.com/repos/opentelemetry-php/api/zipball/df5197c6fd0ddd8e9883b87de042d9341300e2ad",
|
||||
"reference": "df5197c6fd0ddd8e9883b87de042d9341300e2ad",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1292,20 +1292,20 @@
|
||||
"issues": "https://github.com/open-telemetry/opentelemetry-php/issues",
|
||||
"source": "https://github.com/open-telemetry/opentelemetry-php"
|
||||
},
|
||||
"time": "2026-02-25T13:24:05+00:00"
|
||||
"time": "2026-01-21T04:14:03+00:00"
|
||||
},
|
||||
{
|
||||
"name": "open-telemetry/context",
|
||||
"version": "1.5.0",
|
||||
"version": "1.4.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/opentelemetry-php/context.git",
|
||||
"reference": "3c414b246e0dabb7d6145404e6a5e4536ca18d07"
|
||||
"reference": "d4c4470b541ce72000d18c339cfee633e4c8e0cf"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/opentelemetry-php/context/zipball/3c414b246e0dabb7d6145404e6a5e4536ca18d07",
|
||||
"reference": "3c414b246e0dabb7d6145404e6a5e4536ca18d07",
|
||||
"url": "https://api.github.com/repos/opentelemetry-php/context/zipball/d4c4470b541ce72000d18c339cfee633e4c8e0cf",
|
||||
"reference": "d4c4470b541ce72000d18c339cfee633e4c8e0cf",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1347,11 +1347,11 @@
|
||||
],
|
||||
"support": {
|
||||
"chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V",
|
||||
"docs": "https://opentelemetry.io/docs/languages/php",
|
||||
"docs": "https://opentelemetry.io/docs/php",
|
||||
"issues": "https://github.com/open-telemetry/opentelemetry-php/issues",
|
||||
"source": "https://github.com/open-telemetry/opentelemetry-php"
|
||||
},
|
||||
"time": "2025-10-19T06:44:33+00:00"
|
||||
"time": "2025-09-19T00:05:49+00:00"
|
||||
},
|
||||
{
|
||||
"name": "open-telemetry/exporter-otlp",
|
||||
@@ -1419,16 +1419,16 @@
|
||||
},
|
||||
{
|
||||
"name": "open-telemetry/gen-otlp-protobuf",
|
||||
"version": "1.9.0",
|
||||
"version": "1.8.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/opentelemetry-php/gen-otlp-protobuf.git",
|
||||
"reference": "a229cf161d42001d64c8f21e8f678581fe1c66b9"
|
||||
"reference": "673af5b06545b513466081884b47ef15a536edde"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/opentelemetry-php/gen-otlp-protobuf/zipball/a229cf161d42001d64c8f21e8f678581fe1c66b9",
|
||||
"reference": "a229cf161d42001d64c8f21e8f678581fe1c66b9",
|
||||
"url": "https://api.github.com/repos/opentelemetry-php/gen-otlp-protobuf/zipball/673af5b06545b513466081884b47ef15a536edde",
|
||||
"reference": "673af5b06545b513466081884b47ef15a536edde",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1474,30 +1474,30 @@
|
||||
],
|
||||
"support": {
|
||||
"chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V",
|
||||
"docs": "https://opentelemetry.io/docs/languages/php",
|
||||
"docs": "https://opentelemetry.io/docs/php",
|
||||
"issues": "https://github.com/open-telemetry/opentelemetry-php/issues",
|
||||
"source": "https://github.com/open-telemetry/opentelemetry-php"
|
||||
},
|
||||
"time": "2025-10-19T06:44:33+00:00"
|
||||
"time": "2025-09-17T23:10:12+00:00"
|
||||
},
|
||||
{
|
||||
"name": "open-telemetry/sdk",
|
||||
"version": "1.14.0",
|
||||
"version": "1.13.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/opentelemetry-php/sdk.git",
|
||||
"reference": "6e3d0ce93e76555dd5e2f1d19443ff45b990e410"
|
||||
"reference": "c76f91203bf7ef98ab3f4e0a82ca21699af185e1"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/6e3d0ce93e76555dd5e2f1d19443ff45b990e410",
|
||||
"reference": "6e3d0ce93e76555dd5e2f1d19443ff45b990e410",
|
||||
"url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/c76f91203bf7ef98ab3f4e0a82ca21699af185e1",
|
||||
"reference": "c76f91203bf7ef98ab3f4e0a82ca21699af185e1",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"nyholm/psr7-server": "^1.1",
|
||||
"open-telemetry/api": "^1.8",
|
||||
"open-telemetry/api": "^1.7",
|
||||
"open-telemetry/context": "^1.4",
|
||||
"open-telemetry/sem-conv": "^1.0",
|
||||
"php": "^8.1",
|
||||
@@ -1575,7 +1575,7 @@
|
||||
"issues": "https://github.com/open-telemetry/opentelemetry-php/issues",
|
||||
"source": "https://github.com/open-telemetry/opentelemetry-php"
|
||||
},
|
||||
"time": "2026-03-21T11:50:01+00:00"
|
||||
"time": "2026-01-28T11:38:11+00:00"
|
||||
},
|
||||
{
|
||||
"name": "open-telemetry/sem-conv",
|
||||
@@ -3403,16 +3403,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/agents",
|
||||
"version": "1.3.0",
|
||||
"version": "1.2.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/agents.git",
|
||||
"reference": "06064fd9fb19b77ae45a12ec7bcbc17670912c30"
|
||||
"reference": "052227953678a30ecc4b5467401fcb0b2386471e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/agents/zipball/06064fd9fb19b77ae45a12ec7bcbc17670912c30",
|
||||
"reference": "06064fd9fb19b77ae45a12ec7bcbc17670912c30",
|
||||
"url": "https://api.github.com/repos/utopia-php/agents/zipball/052227953678a30ecc4b5467401fcb0b2386471e",
|
||||
"reference": "052227953678a30ecc4b5467401fcb0b2386471e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3450,9 +3450,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/agents/issues",
|
||||
"source": "https://github.com/utopia-php/agents/tree/1.3.0"
|
||||
"source": "https://github.com/utopia-php/agents/tree/1.2.1"
|
||||
},
|
||||
"time": "2026-03-26T03:51:11+00:00"
|
||||
"time": "2026-02-24T06:03:55+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/analytics",
|
||||
@@ -4002,16 +4002,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/dns",
|
||||
"version": "1.6.6",
|
||||
"version": "1.6.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/dns.git",
|
||||
"reference": "917901ecfe5f09a540e4f689b6cbb80b9f55035d"
|
||||
"reference": "574327f0f5fabefa7048030c5634cde33ad10640"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/dns/zipball/917901ecfe5f09a540e4f689b6cbb80b9f55035d",
|
||||
"reference": "917901ecfe5f09a540e4f689b6cbb80b9f55035d",
|
||||
"url": "https://api.github.com/repos/utopia-php/dns/zipball/574327f0f5fabefa7048030c5634cde33ad10640",
|
||||
"reference": "574327f0f5fabefa7048030c5634cde33ad10640",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4053,9 +4053,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/dns/issues",
|
||||
"source": "https://github.com/utopia-php/dns/tree/1.6.6"
|
||||
"source": "https://github.com/utopia-php/dns/tree/1.6.5"
|
||||
},
|
||||
"time": "2026-03-27T11:13:50+00:00"
|
||||
"time": "2026-02-19T16:06:46+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/domains",
|
||||
@@ -5439,16 +5439,16 @@
|
||||
"packages-dev": [
|
||||
{
|
||||
"name": "appwrite/sdk-generator",
|
||||
"version": "1.14.0",
|
||||
"version": "1.11.14",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/appwrite/sdk-generator.git",
|
||||
"reference": "7e7e257b10a8c1384a237e7d8d73452e2108901e"
|
||||
"reference": "ed4faf10fafa1930ed0be3dfe43e41561f2de75b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/7e7e257b10a8c1384a237e7d8d73452e2108901e",
|
||||
"reference": "7e7e257b10a8c1384a237e7d8d73452e2108901e",
|
||||
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/ed4faf10fafa1930ed0be3dfe43e41561f2de75b",
|
||||
"reference": "ed4faf10fafa1930ed0be3dfe43e41561f2de75b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5484,9 +5484,9 @@
|
||||
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
|
||||
"support": {
|
||||
"issues": "https://github.com/appwrite/sdk-generator/issues",
|
||||
"source": "https://github.com/appwrite/sdk-generator/tree/1.14.0"
|
||||
"source": "https://github.com/appwrite/sdk-generator/tree/1.11.14"
|
||||
},
|
||||
"time": "2026-03-26T12:50:11+00:00"
|
||||
"time": "2026-03-20T10:55:13+00:00"
|
||||
},
|
||||
{
|
||||
"name": "brianium/paratest",
|
||||
@@ -6195,11 +6195,11 @@
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan",
|
||||
"version": "2.1.44",
|
||||
"version": "2.1.42",
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/4a88c083c668b2c364a425c9b3171b2d9ea5d218",
|
||||
"reference": "4a88c083c668b2c364a425c9b3171b2d9ea5d218",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/1279e1ce86ba768f0780c9d889852b4e02ff40d0",
|
||||
"reference": "1279e1ce86ba768f0780c9d889852b4e02ff40d0",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -6244,7 +6244,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-03-25T17:34:21+00:00"
|
||||
"time": "2026-03-17T14:58:32+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-code-coverage",
|
||||
@@ -8456,5 +8456,5 @@
|
||||
"platform-dev": {
|
||||
"ext-fileinfo": "*"
|
||||
},
|
||||
"plugin-api-version": "2.9.0"
|
||||
"plugin-api-version": "2.6.0"
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
services:
|
||||
appwrite-mongo-express:
|
||||
profiles: ["mongodb"]
|
||||
image: mongo-express
|
||||
container_name: appwrite-mongo-express
|
||||
networks:
|
||||
|
||||
+38
-14
@@ -254,7 +254,7 @@ services:
|
||||
appwrite-console:
|
||||
<<: *x-logging
|
||||
container_name: appwrite-console
|
||||
image: appwrite/console:7.8.26
|
||||
image: appwrite/console:7.5.7
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- appwrite
|
||||
@@ -1261,27 +1261,28 @@ services:
|
||||
retries: 20
|
||||
start_period: 5s
|
||||
|
||||
postgresql:
|
||||
image: appwrite/postgres:0.1.0
|
||||
container_name: appwrite-postgresql
|
||||
mariadb:
|
||||
image: mariadb:10.11 # fix issues when upgrading using: mysql_upgrade -u root -p
|
||||
container_name: appwrite-mariadb
|
||||
<<: *x-logging
|
||||
networks:
|
||||
- appwrite
|
||||
volumes:
|
||||
- appwrite-postgresql:/var/lib/postgresql/18/data:rw
|
||||
- appwrite-mariadb:/var/lib/mysql:rw
|
||||
ports:
|
||||
- "5432:5432"
|
||||
- "3306:3306"
|
||||
environment:
|
||||
- POSTGRES_DB=${_APP_DB_SCHEMA}
|
||||
- POSTGRES_USER=${_APP_DB_USER}
|
||||
- POSTGRES_PASSWORD=${_APP_DB_PASS}
|
||||
- MYSQL_ROOT_PASSWORD=${_APP_DB_ROOT_PASS}
|
||||
- MYSQL_DATABASE=${_APP_DB_SCHEMA}
|
||||
- MYSQL_USER=${_APP_DB_USER}
|
||||
- MYSQL_PASSWORD=${_APP_DB_PASS}
|
||||
- MARIADB_AUTO_UPGRADE=1
|
||||
command: "mysqld --innodb-flush-method=fsync"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${_APP_DB_USER} -d ${_APP_DB_SCHEMA}"]
|
||||
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 10s
|
||||
command: "postgres"
|
||||
retries: 12
|
||||
|
||||
mongodb:
|
||||
image: mongo:8.2.5
|
||||
@@ -1319,6 +1320,28 @@ services:
|
||||
retries: 10
|
||||
start_period: 30s
|
||||
|
||||
postgresql:
|
||||
image: appwrite/postgres:0.1.0
|
||||
container_name: appwrite-postgresql
|
||||
<<: *x-logging
|
||||
networks:
|
||||
- appwrite
|
||||
volumes:
|
||||
- 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}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${_APP_DB_USER} -d ${_APP_DB_SCHEMA}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 10s
|
||||
command: "postgres"
|
||||
|
||||
ollama:
|
||||
image: appwrite/ollama:0.1.1
|
||||
container_name: ollama
|
||||
@@ -1441,9 +1464,10 @@ networks:
|
||||
|
||||
|
||||
volumes:
|
||||
appwrite-postgresql:
|
||||
appwrite-mariadb:
|
||||
appwrite-mongodb:
|
||||
appwrite-mongodb-keyfile:
|
||||
appwrite-postgresql:
|
||||
appwrite-redis:
|
||||
appwrite-cache:
|
||||
appwrite-uploads:
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
Create multiple operations in a single transaction.
|
||||
@@ -1 +0,0 @@
|
||||
Create a new transaction.
|
||||
@@ -1 +0,0 @@
|
||||
Delete a transaction by its unique ID.
|
||||
@@ -1 +0,0 @@
|
||||
Get a transaction by its unique ID.
|
||||
@@ -1 +0,0 @@
|
||||
List transactions across all databases.
|
||||
@@ -1 +0,0 @@
|
||||
Update a transaction, to either commit or roll back its operations.
|
||||
@@ -1 +0,0 @@
|
||||
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.
|
||||
@@ -1 +0,0 @@
|
||||
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.
|
||||
@@ -1 +0,0 @@
|
||||
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.
|
||||
@@ -1,2 +0,0 @@
|
||||
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 +0,0 @@
|
||||
Create multiple operations in a single transaction.
|
||||
@@ -1 +0,0 @@
|
||||
Create a new transaction.
|
||||
@@ -1 +0,0 @@
|
||||
Create a new Database.
|
||||
@@ -1 +0,0 @@
|
||||
Decrement a specific column of a row by a given value.
|
||||
@@ -1 +0,0 @@
|
||||
Delete a collection by its unique ID. Only users with write permissions have access to delete this resource.
|
||||
@@ -1 +0,0 @@
|
||||
Delete a document by its unique ID.
|
||||
@@ -1 +0,0 @@
|
||||
Bulk delete documents using queries, if no queries are passed then all documents are deleted.
|
||||
@@ -1 +0,0 @@
|
||||
Delete an index.
|
||||
@@ -1 +0,0 @@
|
||||
Delete a transaction by its unique ID.
|
||||
@@ -1 +0,0 @@
|
||||
Delete a database by its unique ID. Only API keys with with databases.write scope can delete a database.
|
||||
@@ -1 +0,0 @@
|
||||
Get the collection activity logs list by its unique ID.
|
||||
@@ -1 +0,0 @@
|
||||
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.
|
||||
@@ -1 +0,0 @@
|
||||
Get a collection by its unique ID. This endpoint response returns a JSON object with the collection metadata.
|
||||
@@ -1 +0,0 @@
|
||||
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.
|
||||
@@ -1 +0,0 @@
|
||||
Get the document activity logs list by its unique ID.
|
||||
@@ -1 +0,0 @@
|
||||
Get a document by its unique ID. This endpoint response returns a JSON object with the document data.
|
||||
@@ -1 +0,0 @@
|
||||
Get index by ID.
|
||||
@@ -1 +0,0 @@
|
||||
Get the database activity logs list by its unique ID.
|
||||
@@ -1 +0,0 @@
|
||||
Get a transaction by its unique ID.
|
||||
@@ -1 +0,0 @@
|
||||
Get a database by its unique ID. This endpoint response returns a JSON object with the database metadata.
|
||||
@@ -1 +0,0 @@
|
||||
Increment a specific column of a row by a given value.
|
||||
@@ -1 +0,0 @@
|
||||
List attributes in the collection.
|
||||
@@ -1 +0,0 @@
|
||||
Get a list of all collections that belong to the provided databaseId. You can use the search parameter to filter your results.
|
||||
@@ -1 +0,0 @@
|
||||
Get a list of all the user's documents in a given collection. You can use the query params to filter your results.
|
||||
@@ -1 +0,0 @@
|
||||
List indexes in the collection.
|
||||
@@ -1 +0,0 @@
|
||||
List transactions across all databases.
|
||||
@@ -1 +0,0 @@
|
||||
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 +0,0 @@
|
||||
Get a list of all databases from the current Appwrite project. You can use the search parameter to filter your results.
|
||||
@@ -1 +0,0 @@
|
||||
Update a collection by its unique ID.
|
||||
@@ -1 +0,0 @@
|
||||
Update a document by its unique ID. Using the patch method you can pass only specific fields that will get updated.
|
||||
@@ -1 +0,0 @@
|
||||
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 +0,0 @@
|
||||
Update a transaction, to either commit or roll back its operations.
|
||||
@@ -1 +0,0 @@
|
||||
Update a database by its unique ID.
|
||||
@@ -1 +0,0 @@
|
||||
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.
|
||||
@@ -1 +0,0 @@
|
||||
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.
|
||||
@@ -0,0 +1,17 @@
|
||||
# Change Log
|
||||
|
||||
## 0.3.0
|
||||
|
||||
* Add `bytesMax` and `bytesUsed` properties to Collection and Table documentation
|
||||
* Add `queries` parameter to `listKeys` and `keyId` parameter to `createKey` documentation
|
||||
* Add `dart-3.10` and `flutter-3.38` runtimes
|
||||
* Fix Teams membership docs to use `string[]` instead of `Roles[]`
|
||||
|
||||
## 0.2.0
|
||||
|
||||
* Document array-based enum parameters in Markdown examples (e.g., `permissions: BrowserPermission[]`).
|
||||
* Breaking change: `Output` enum has been removed; use `ImageFormat` instead.
|
||||
|
||||
## 0.1.0
|
||||
|
||||
* Initial release
|
||||
@@ -1,5 +0,0 @@
|
||||
# Change Log
|
||||
|
||||
## 0.1.0
|
||||
|
||||
* Initial release
|
||||
@@ -1,68 +0,0 @@
|
||||
## Getting Started
|
||||
|
||||
### Init your SDK
|
||||
Initialize your SDK with your Appwrite server API endpoint and project ID which can be found on your project settings page and your new API secret Key from project's API keys section.
|
||||
|
||||
```rust
|
||||
use appwrite::client::Client;
|
||||
|
||||
let client = Client::new()
|
||||
.set_endpoint("https://[HOSTNAME_OR_IP]/v1") // Your API Endpoint
|
||||
.set_project("5df5acd0d48c2") // Your project ID
|
||||
.set_key("919c2d18fb5d4...a2ae413da83346ad2") // Your secret API key
|
||||
.set_self_signed(true); // Use only on dev mode with a self-signed SSL cert
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
```rust
|
||||
use appwrite::client::Client;
|
||||
use appwrite::services::users::Users;
|
||||
use appwrite::id::ID;
|
||||
|
||||
let client = Client::new()
|
||||
.set_endpoint("https://[HOSTNAME_OR_IP]/v1")
|
||||
.set_project("5df5acd0d48c2")
|
||||
.set_key("919c2d18fb5d4...a2ae413da83346ad2")
|
||||
.set_self_signed(true);
|
||||
|
||||
let users = Users::new(&client);
|
||||
|
||||
let user = users.create(
|
||||
ID::unique(),
|
||||
Some("email@example.com"),
|
||||
Some("+123456789"),
|
||||
Some("password"),
|
||||
Some("Walter O'Brien"),
|
||||
).await?;
|
||||
|
||||
println!("{}", user.name);
|
||||
println!("{}", user.email);
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
The Appwrite Rust SDK returns `Result` types. You can handle errors using standard Rust error handling patterns. Below is an example.
|
||||
|
||||
```rust
|
||||
use appwrite::error::AppwriteError;
|
||||
|
||||
match users.create(
|
||||
ID::unique(),
|
||||
Some("email@example.com"),
|
||||
Some("+123456789"),
|
||||
Some("password"),
|
||||
Some("Walter O'Brien"),
|
||||
).await {
|
||||
Ok(user) => println!("{}", user.name),
|
||||
Err(AppwriteError { message, code, .. }) => {
|
||||
eprintln!("Error {}: {}", code, message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Learn more
|
||||
You can use the following resources to learn more and get help
|
||||
- 🚀 [Getting Started Tutorial](https://appwrite.io/docs/getting-started-for-server)
|
||||
- 📜 [Appwrite Docs](https://appwrite.io/docs)
|
||||
- 💬 [Discord Community](https://appwrite.io/discord)
|
||||
@@ -114,6 +114,12 @@ parameters:
|
||||
count: 1
|
||||
path: app/controllers/mock.php
|
||||
|
||||
-
|
||||
message: '#^Call to an undefined method Utopia\\Database\\Document\:\:getRoles\(\)\.$#'
|
||||
identifier: method.notFound
|
||||
count: 1
|
||||
path: app/controllers/shared/api.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$register might not be defined\.$#'
|
||||
identifier: variable.undefined
|
||||
@@ -186,6 +192,108 @@ parameters:
|
||||
count: 3
|
||||
path: app/worker.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @return with type string is incompatible with native type int\.$#'
|
||||
identifier: return.phpDocType
|
||||
count: 1
|
||||
path: src/Appwrite/Auth/OAuth2.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @param references unknown parameter\: \$token$#'
|
||||
identifier: parameter.notFound
|
||||
count: 1
|
||||
path: src/Appwrite/Auth/OAuth2/Disqus.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @param references unknown parameter\: \$value$#'
|
||||
identifier: parameter.notFound
|
||||
count: 1
|
||||
path: src/Appwrite/Auth/Validator/PersonalData.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @param has invalid value \(DeviceDetector\)\: Unexpected token "\\n ", expected variable at offset 32 on line 2$#'
|
||||
identifier: phpDoc.parseError
|
||||
count: 1
|
||||
path: src/Appwrite/Detector/Detector.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @param has invalid value \(string\)\: Unexpected token "\\n ", expected variable at offset 24 on line 2$#'
|
||||
identifier: phpDoc.parseError
|
||||
count: 1
|
||||
path: src/Appwrite/Detector/Detector.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @var above a method has no effect\.$#'
|
||||
identifier: varTag.misplaced
|
||||
count: 1
|
||||
path: src/Appwrite/Docker/Compose.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @var above a method has no effect\.$#'
|
||||
identifier: varTag.misplaced
|
||||
count: 1
|
||||
path: src/Appwrite/Docker/Compose/Service.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @var above a method has no effect\.$#'
|
||||
identifier: varTag.misplaced
|
||||
count: 1
|
||||
path: src/Appwrite/Docker/Env.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @param has invalid value \(int port\)\: Unexpected token "port", expected variable at offset 50 on line 4$#'
|
||||
identifier: phpDoc.parseError
|
||||
count: 1
|
||||
path: src/Appwrite/Event/Mail.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @param references unknown parameter\: \$password$#'
|
||||
identifier: parameter.notFound
|
||||
count: 1
|
||||
path: src/Appwrite/Event/Mail.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @return with type string is incompatible with native type Appwrite\\Event\\Mail\.$#'
|
||||
identifier: return.phpDocType
|
||||
count: 1
|
||||
path: src/Appwrite/Event/Mail.php
|
||||
|
||||
-
|
||||
message: '#^Method Appwrite\\Event\\Message\\Usage\:\:fromArray\(\) should return static\(Appwrite\\Event\\Message\\Usage\) but returns Appwrite\\Event\\Message\\Usage\.$#'
|
||||
identifier: return.type
|
||||
count: 1
|
||||
path: src/Appwrite/Event/Message/Usage.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @param references unknown parameter\: \$message$#'
|
||||
identifier: parameter.notFound
|
||||
count: 1
|
||||
path: src/Appwrite/Event/Messaging.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @return with type string is incompatible with native type Utopia\\Database\\Document\.$#'
|
||||
identifier: return.phpDocType
|
||||
count: 1
|
||||
path: src/Appwrite/Event/Messaging.php
|
||||
|
||||
-
|
||||
message: '#^Method Appwrite\\Functions\\EventProcessor\:\:getFunctionsEvents\(\) should return array\<string, bool\> but returns array\<int\<0, max\>\>\.$#'
|
||||
identifier: return.type
|
||||
count: 1
|
||||
path: src/Appwrite/Functions/EventProcessor.php
|
||||
|
||||
-
|
||||
message: '#^Method Appwrite\\Functions\\EventProcessor\:\:getWebhooksEvents\(\) should return array\<string, bool\> but returns array\<int\<0, max\>\>\.$#'
|
||||
identifier: return.type
|
||||
count: 1
|
||||
path: src/Appwrite/Functions/EventProcessor.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$hostname on left side of \?\? always exists and is not nullable\.$#'
|
||||
identifier: nullCoalesce.variable
|
||||
count: 1
|
||||
path: src/Appwrite/Functions/EventProcessor.php
|
||||
|
||||
-
|
||||
message: '#^Anonymous function has an unused use \$context\.$#'
|
||||
identifier: closure.unusedUse
|
||||
@@ -258,6 +366,30 @@ parameters:
|
||||
count: 1
|
||||
path: src/Appwrite/GraphQL/Types/Mapper.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Appwrite\\GraphQL\\Types\\Mapper\:\:\$models through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 3
|
||||
path: src/Appwrite/GraphQL/Types/Mapper.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe call to private method Appwrite\\GraphQL\\Types\\Mapper\:\:getColumnImplementation\(\) through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateMethod
|
||||
count: 2
|
||||
path: src/Appwrite/GraphQL/Types/Mapper.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe call to private method Appwrite\\GraphQL\\Types\\Mapper\:\:getHashOptionsImplementation\(\) through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateMethod
|
||||
count: 1
|
||||
path: src/Appwrite/GraphQL/Types/Mapper.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe call to private method Appwrite\\GraphQL\\Types\\Mapper\:\:getUnionImplementation\(\) through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateMethod
|
||||
count: 1
|
||||
path: src/Appwrite/GraphQL/Types/Mapper.php
|
||||
|
||||
-
|
||||
message: '#^Call to an undefined method Appwrite\\Migration\\Version\\V15\:\:documentsIterator\(\)\.$#'
|
||||
identifier: method.notFound
|
||||
@@ -324,6 +456,12 @@ parameters:
|
||||
count: 1
|
||||
path: src/Appwrite/OpenSSL/OpenSSL.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @param references unknown parameter\: \$projectId$#'
|
||||
identifier: parameter.notFound
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Action.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$output in empty\(\) always exists and is not falsy\.$#'
|
||||
identifier: empty.variable
|
||||
@@ -456,6 +594,12 @@ parameters:
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$currentDocumentId on left side of \?\? always exists and is always null\.$#'
|
||||
identifier: nullCoalesce.variable
|
||||
count: 1
|
||||
path: src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php
|
||||
|
||||
-
|
||||
message: '#^Offset ''deviceBrand'' does not exist on int\.$#'
|
||||
identifier: offsetAccess.notFound
|
||||
@@ -1020,6 +1164,12 @@ parameters:
|
||||
count: 1
|
||||
path: src/Appwrite/SDK/Specification/Format/Swagger2.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @var above a method has no effect\.$#'
|
||||
identifier: varTag.misplaced
|
||||
count: 2
|
||||
path: src/Appwrite/Template/Template.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @param has invalid value \(Document \$this\)\: Unexpected token "\$this", expected variable at offset 69 on line 4$#'
|
||||
identifier: phpDoc.parseError
|
||||
@@ -1032,6 +1182,35 @@ parameters:
|
||||
count: 1
|
||||
path: src/Appwrite/Utopia/Database/Documents/User.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe call to private method Appwrite\\Utopia\\Request\\Filters\\V17\:\:appendSymbol\(\) through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateMethod
|
||||
count: 4
|
||||
path: src/Appwrite/Utopia/Request/Filters/V17.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe call to private method Appwrite\\Utopia\\Request\\Filters\\V17\:\:isSpecialChar\(\) through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateMethod
|
||||
count: 1
|
||||
path: src/Appwrite/Utopia/Request/Filters/V17.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @param has invalid value \(callable The callback to show sensitive information for\)\: Unexpected token "The", expected variable at offset 91 on line 4$#'
|
||||
identifier: phpDoc.parseError
|
||||
count: 1
|
||||
path: src/Appwrite/Utopia/Response.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @return with type Appwrite\\Utopia\\Response\\Filter is incompatible with native type array\.$#'
|
||||
identifier: return.phpDocType
|
||||
count: 1
|
||||
path: src/Appwrite/Utopia/Response.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @return with type string is incompatible with native type Utopia\\Database\\Document\.$#'
|
||||
identifier: return.phpDocType
|
||||
count: 1
|
||||
path: src/Appwrite/Utopia/Response/Model/User.php
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\Databases\\Legacy\\DatabasesStringTypesTest\:\:\$setupCache through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
@@ -1067,12 +1246,108 @@ parameters:
|
||||
count: 8
|
||||
path: tests/e2e/Services/Functions/FunctionsCustomServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\FunctionsClientTest\:\:\$cachedDeployment through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 3
|
||||
path: tests/e2e/Services/GraphQL/FunctionsClientTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\FunctionsClientTest\:\:\$cachedExecution through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 4
|
||||
path: tests/e2e/Services/GraphQL/FunctionsClientTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\FunctionsClientTest\:\:\$cachedFunction through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 3
|
||||
path: tests/e2e/Services/GraphQL/FunctionsClientTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\FunctionsServerTest\:\:\$cachedDeployment through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 4
|
||||
path: tests/e2e/Services/GraphQL/FunctionsServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\FunctionsServerTest\:\:\$cachedExecution through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 5
|
||||
path: tests/e2e/Services/GraphQL/FunctionsServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\FunctionsServerTest\:\:\$cachedFunction through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 4
|
||||
path: tests/e2e/Services/GraphQL/FunctionsServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Binary operation "\+" between string and 1 results in an error\.$#'
|
||||
identifier: binaryOp.invalid
|
||||
count: 1
|
||||
path: tests/e2e/Services/GraphQL/Legacy/AbuseTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\Legacy\\DatabaseClientTest\:\:\$bulkData through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 4
|
||||
path: tests/e2e/Services/GraphQL/Legacy/DatabaseClientTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\Legacy\\DatabaseClientTest\:\:\$collection through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 4
|
||||
path: tests/e2e/Services/GraphQL/Legacy/DatabaseClientTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\Legacy\\DatabaseClientTest\:\:\$database through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 4
|
||||
path: tests/e2e/Services/GraphQL/Legacy/DatabaseClientTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\Legacy\\DatabaseClientTest\:\:\$document through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 4
|
||||
path: tests/e2e/Services/GraphQL/Legacy/DatabaseClientTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\MessagingTest\:\:\$cachedEmail through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 2
|
||||
path: tests/e2e/Services/GraphQL/MessagingTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\MessagingTest\:\:\$cachedProviders through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 8
|
||||
path: tests/e2e/Services/GraphQL/MessagingTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\MessagingTest\:\:\$cachedPush through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 4
|
||||
path: tests/e2e/Services/GraphQL/MessagingTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\MessagingTest\:\:\$cachedSms through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 4
|
||||
path: tests/e2e/Services/GraphQL/MessagingTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\MessagingTest\:\:\$cachedSubscriber through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 5
|
||||
path: tests/e2e/Services/GraphQL/MessagingTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\MessagingTest\:\:\$cachedTopic through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 9
|
||||
path: tests/e2e/Services/GraphQL/MessagingTest.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$from in empty\(\) is never defined\.$#'
|
||||
identifier: empty.variable
|
||||
@@ -1085,18 +1360,174 @@ parameters:
|
||||
count: 1
|
||||
path: tests/e2e/Services/GraphQL/StorageClientTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\StorageClientTest\:\:\$cachedBucket through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 3
|
||||
path: tests/e2e/Services/GraphQL/StorageClientTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\StorageClientTest\:\:\$cachedFile through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 5
|
||||
path: tests/e2e/Services/GraphQL/StorageClientTest.php
|
||||
|
||||
-
|
||||
message: '#^Method Tests\\E2E\\Services\\GraphQL\\StorageServerTest\:\:testGetFileDownload\(\) should return array but return statement is missing\.$#'
|
||||
identifier: return.missing
|
||||
count: 1
|
||||
path: tests/e2e/Services/GraphQL/StorageServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\StorageServerTest\:\:\$cachedBucket through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 4
|
||||
path: tests/e2e/Services/GraphQL/StorageServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\StorageServerTest\:\:\$cachedFile through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 6
|
||||
path: tests/e2e/Services/GraphQL/StorageServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Binary operation "\+" between string and 1 results in an error\.$#'
|
||||
identifier: binaryOp.invalid
|
||||
count: 1
|
||||
path: tests/e2e/Services/GraphQL/TablesDB/AbuseTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedBooleanColumnData through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 5
|
||||
path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedBulkData through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 4
|
||||
path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedDatabase through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 4
|
||||
path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedDatetimeColumnData through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 5
|
||||
path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedEmailColumnData through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 5
|
||||
path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedEnumColumnData through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 5
|
||||
path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedFloatColumnData through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 5
|
||||
path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedIPColumnData through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 5
|
||||
path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedIndexData through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 7
|
||||
path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedIntegerColumnData through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 5
|
||||
path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedRelationshipColumnData through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 5
|
||||
path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedRowData through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 5
|
||||
path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedStringColumnData through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 5
|
||||
path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedTableData through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 4
|
||||
path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TablesDB\\DatabaseServerTest\:\:\$cachedURLColumnData through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 5
|
||||
path: tests/e2e/Services/GraphQL/TablesDB/DatabaseServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TeamsClientTest\:\:\$cachedMembership through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 4
|
||||
path: tests/e2e/Services/GraphQL/TeamsClientTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TeamsClientTest\:\:\$cachedTeam through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 3
|
||||
path: tests/e2e/Services/GraphQL/TeamsClientTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TeamsServerTest\:\:\$cachedMembership through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 4
|
||||
path: tests/e2e/Services/GraphQL/TeamsServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TeamsServerTest\:\:\$cachedTeam through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 3
|
||||
path: tests/e2e/Services/GraphQL/TeamsServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\TeamsServerTest\:\:\$cachedTeamWithPrefs through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 3
|
||||
path: tests/e2e/Services/GraphQL/TeamsServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\UsersTest\:\:\$cachedUser through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 3
|
||||
path: tests/e2e/Services/GraphQL/UsersTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\GraphQL\\UsersTest\:\:\$cachedUserTarget through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 5
|
||||
path: tests/e2e/Services/GraphQL/UsersTest.php
|
||||
|
||||
-
|
||||
message: '#^Variable \$from in empty\(\) is never defined\.$#'
|
||||
identifier: empty.variable
|
||||
@@ -1168,6 +1599,36 @@ parameters:
|
||||
identifier: method.notFound
|
||||
count: 1
|
||||
path: tests/e2e/Services/TablesDB/Permissions/TablesDBPermissionsTeamTest.php
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\Tokens\\TokensConsoleClientTest\:\:\$bucketAndFileData through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 4
|
||||
path: tests/e2e/Services/Tokens/TokensConsoleClientTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\Tokens\\TokensConsoleClientTest\:\:\$tokenData through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 4
|
||||
path: tests/e2e/Services/Tokens/TokensConsoleClientTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\Tokens\\TokensCustomClientTest\:\:\$bucketAndFileData through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 4
|
||||
path: tests/e2e/Services/Tokens/TokensCustomClientTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\Tokens\\TokensCustomServerTest\:\:\$bucketAndFileData through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 4
|
||||
path: tests/e2e/Services/Tokens/TokensCustomServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\Tokens\\TokensCustomServerTest\:\:\$tokenData through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
count: 4
|
||||
path: tests/e2e/Services/Tokens/TokensCustomServerTest.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe access to private property Tests\\E2E\\Services\\Users\\UsersCustomServerTest\:\:\$cachedHashedPasswordUsers through static\:\:\.$#'
|
||||
identifier: staticClassAccess.privateProperty
|
||||
@@ -1227,3 +1688,75 @@ parameters:
|
||||
identifier: method.notFound
|
||||
count: 1
|
||||
path: tests/unit/Event/EventTest.php
|
||||
|
||||
-
|
||||
message: '#^Call to method parse\(\) on an unknown class Tests\\Unit\\Utopia\\Response\\Filters\\Filter\.$#'
|
||||
identifier: class.notFound
|
||||
count: 6
|
||||
path: tests/unit/Utopia/Response/Filters/V16Test.php
|
||||
|
||||
-
|
||||
message: '#^Property Tests\\Unit\\Utopia\\Response\\Filters\\V16Test\:\:\$filter \(Tests\\Unit\\Utopia\\Response\\Filters\\Filter\) does not accept Appwrite\\Utopia\\Response\\Filters\\V16\.$#'
|
||||
identifier: assign.propertyType
|
||||
count: 1
|
||||
path: tests/unit/Utopia/Response/Filters/V16Test.php
|
||||
|
||||
-
|
||||
message: '#^Property Tests\\Unit\\Utopia\\Response\\Filters\\V16Test\:\:\$filter has unknown class Tests\\Unit\\Utopia\\Response\\Filters\\Filter as its type\.$#'
|
||||
identifier: class.notFound
|
||||
count: 1
|
||||
path: tests/unit/Utopia/Response/Filters/V16Test.php
|
||||
|
||||
-
|
||||
message: '#^Call to method parse\(\) on an unknown class Tests\\Unit\\Utopia\\Response\\Filters\\Filter\.$#'
|
||||
identifier: class.notFound
|
||||
count: 5
|
||||
path: tests/unit/Utopia/Response/Filters/V17Test.php
|
||||
|
||||
-
|
||||
message: '#^Property Tests\\Unit\\Utopia\\Response\\Filters\\V17Test\:\:\$filter \(Tests\\Unit\\Utopia\\Response\\Filters\\Filter\) does not accept Appwrite\\Utopia\\Response\\Filters\\V17\.$#'
|
||||
identifier: assign.propertyType
|
||||
count: 1
|
||||
path: tests/unit/Utopia/Response/Filters/V17Test.php
|
||||
|
||||
-
|
||||
message: '#^Property Tests\\Unit\\Utopia\\Response\\Filters\\V17Test\:\:\$filter has unknown class Tests\\Unit\\Utopia\\Response\\Filters\\Filter as its type\.$#'
|
||||
identifier: class.notFound
|
||||
count: 1
|
||||
path: tests/unit/Utopia/Response/Filters/V17Test.php
|
||||
|
||||
-
|
||||
message: '#^Call to method parse\(\) on an unknown class Tests\\Unit\\Utopia\\Response\\Filters\\Filter\.$#'
|
||||
identifier: class.notFound
|
||||
count: 4
|
||||
path: tests/unit/Utopia/Response/Filters/V18Test.php
|
||||
|
||||
-
|
||||
message: '#^Property Tests\\Unit\\Utopia\\Response\\Filters\\V18Test\:\:\$filter \(Tests\\Unit\\Utopia\\Response\\Filters\\Filter\) does not accept Appwrite\\Utopia\\Response\\Filters\\V18\.$#'
|
||||
identifier: assign.propertyType
|
||||
count: 1
|
||||
path: tests/unit/Utopia/Response/Filters/V18Test.php
|
||||
|
||||
-
|
||||
message: '#^Property Tests\\Unit\\Utopia\\Response\\Filters\\V18Test\:\:\$filter has unknown class Tests\\Unit\\Utopia\\Response\\Filters\\Filter as its type\.$#'
|
||||
identifier: class.notFound
|
||||
count: 1
|
||||
path: tests/unit/Utopia/Response/Filters/V18Test.php
|
||||
|
||||
-
|
||||
message: '#^Call to method parse\(\) on an unknown class Tests\\Unit\\Utopia\\Response\\Filters\\Filter\.$#'
|
||||
identifier: class.notFound
|
||||
count: 11
|
||||
path: tests/unit/Utopia/Response/Filters/V19Test.php
|
||||
|
||||
-
|
||||
message: '#^Property Tests\\Unit\\Utopia\\Response\\Filters\\V19Test\:\:\$filter \(Tests\\Unit\\Utopia\\Response\\Filters\\Filter\) does not accept Appwrite\\Utopia\\Response\\Filters\\V19\.$#'
|
||||
identifier: assign.propertyType
|
||||
count: 1
|
||||
path: tests/unit/Utopia/Response/Filters/V19Test.php
|
||||
|
||||
-
|
||||
message: '#^Property Tests\\Unit\\Utopia\\Response\\Filters\\V19Test\:\:\$filter has unknown class Tests\\Unit\\Utopia\\Response\\Filters\\Filter as its type\.$#'
|
||||
identifier: class.notFound
|
||||
count: 1
|
||||
path: tests/unit/Utopia/Response/Filters/V19Test.php
|
||||
|
||||
+10
-105
@@ -16,123 +16,28 @@
|
||||
<testsuite name="unit">
|
||||
<directory>./tests/unit</directory>
|
||||
</testsuite>
|
||||
<testsuite name="General">
|
||||
<directory>./tests/e2e/General</directory>
|
||||
<directory>./tests/e2e/Scopes</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Console">
|
||||
<directory>./tests/e2e/Services/Console</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Health">
|
||||
<directory>./tests/e2e/Services/Health</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Locale">
|
||||
<directory>./tests/e2e/Services/Locale</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Tokens">
|
||||
<directory>./tests/e2e/Services/Tokens</directory>
|
||||
</testsuite>
|
||||
<testsuite name="VCS">
|
||||
<directory>./tests/e2e/Services/VCS</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Webhooks">
|
||||
<directory>./tests/e2e/Services/Webhooks</directory>
|
||||
</testsuite>
|
||||
<testsuite name="GeneralGroup">
|
||||
<directory>./tests/e2e/General</directory>
|
||||
<directory>./tests/e2e/Scopes</directory>
|
||||
<directory>./tests/e2e/Services/Console</directory>
|
||||
<directory>./tests/e2e/Services/Health</directory>
|
||||
<directory>./tests/e2e/Services/Locale</directory>
|
||||
<directory>./tests/e2e/Services/Messaging</directory>
|
||||
<directory>./tests/e2e/Services/Proxy</directory>
|
||||
<directory>./tests/e2e/Services/Storage</directory>
|
||||
<directory>./tests/e2e/Services/Tokens</directory>
|
||||
<directory>./tests/e2e/Services/VCS</directory>
|
||||
<directory>./tests/e2e/Services/Webhooks</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Account">
|
||||
<directory>./tests/e2e/Services/Account</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Avatars">
|
||||
<directory>./tests/e2e/Services/Avatars</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Databases">
|
||||
<directory>./tests/e2e/Services/Databases</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Functions">
|
||||
<file>./tests/e2e/Services/Functions/FunctionsBase.php</file>
|
||||
<file>./tests/e2e/Services/Functions/FunctionsCustomServerTest.php</file>
|
||||
<file>./tests/e2e/Services/Functions/FunctionsCustomClientTest.php</file>
|
||||
</testsuite>
|
||||
<testsuite name="FunctionsSchedule">
|
||||
<directory>./tests/e2e/Services/FunctionsSchedule</directory>
|
||||
</testsuite>
|
||||
<testsuite name="GraphQL">
|
||||
<directory>./tests/e2e/Services/GraphQL</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Messaging">
|
||||
<directory>./tests/e2e/Services/Messaging</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Migrations">
|
||||
<directory>./tests/e2e/Services/Migrations</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Project">
|
||||
<directory>./tests/e2e/Services/Project</directory>
|
||||
</testsuite>
|
||||
<testsuite name="ProjectWebhooks">
|
||||
<directory>./tests/e2e/Services/ProjectWebhooks</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Projects">
|
||||
<directory>./tests/e2e/Services/Projects</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Proxy">
|
||||
<directory>./tests/e2e/Services/Proxy</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Realtime">
|
||||
<directory>./tests/e2e/Services/Realtime</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Sites">
|
||||
<directory>./tests/e2e/Services/Sites</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Storage">
|
||||
<directory>./tests/e2e/Services/Storage</directory>
|
||||
</testsuite>
|
||||
<testsuite name="TablesDB">
|
||||
<directory>./tests/e2e/Services/TablesDB</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Teams">
|
||||
<directory>./tests/e2e/Services/Teams</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Users">
|
||||
<directory>./tests/e2e/Services/Users</directory>
|
||||
</testsuite>
|
||||
<testsuite name="e2e">
|
||||
<file>./tests/e2e/Client.php</file>
|
||||
<directory>./tests/e2e/General</directory>
|
||||
<directory>./tests/e2e/Scopes</directory>
|
||||
<directory>./tests/e2e/Services/Teams</directory>
|
||||
<directory>./tests/e2e/Services/Realtime</directory>
|
||||
<directory>./tests/e2e/Services/Account</directory>
|
||||
<directory>./tests/e2e/Services/Avatars</directory>
|
||||
<directory>./tests/e2e/Services/Users</directory>
|
||||
<directory>./tests/e2e/Services/Console</directory>
|
||||
<directory>./tests/e2e/Services/Avatars</directory>
|
||||
<directory>./tests/e2e/Services/Databases</directory>
|
||||
<directory>./tests/e2e/Services/FunctionsSchedule</directory>
|
||||
<directory>./tests/e2e/Services/GraphQL</directory>
|
||||
<directory>./tests/e2e/Services/Health</directory>
|
||||
<directory>./tests/e2e/Services/Locale</directory>
|
||||
<directory>./tests/e2e/Services/Projects</directory>
|
||||
<directory>./tests/e2e/Services/Storage</directory>
|
||||
<directory>./tests/e2e/Services/Tokens</directory>
|
||||
<directory>./tests/e2e/Services/Webhooks</directory>
|
||||
<directory>./tests/e2e/Services/ProjectWebhooks</directory>
|
||||
<directory>./tests/e2e/Services/Messaging</directory>
|
||||
<directory>./tests/e2e/Services/Migrations</directory>
|
||||
<directory>./tests/e2e/Services/Project</directory>
|
||||
<directory>./tests/e2e/Services/ProjectWebhooks</directory>
|
||||
<directory>./tests/e2e/Services/Projects</directory>
|
||||
<directory>./tests/e2e/Services/Proxy</directory>
|
||||
<directory>./tests/e2e/Services/Realtime</directory>
|
||||
<directory>./tests/e2e/Services/Sites</directory>
|
||||
<directory>./tests/e2e/Services/Storage</directory>
|
||||
<directory>./tests/e2e/Services/TablesDB</directory>
|
||||
<directory>./tests/e2e/Services/Teams</directory>
|
||||
<directory>./tests/e2e/Services/Tokens</directory>
|
||||
<directory>./tests/e2e/Services/Users</directory>
|
||||
<directory>./tests/e2e/Services/VCS</directory>
|
||||
<directory>./tests/e2e/Services/Webhooks</directory>
|
||||
<file>./tests/e2e/Services/Functions/FunctionsBase.php</file>
|
||||
<file>./tests/e2e/Services/Functions/FunctionsCustomServerTest.php</file>
|
||||
<file>./tests/e2e/Services/Functions/FunctionsCustomClientTest.php</file>
|
||||
|
||||
@@ -155,7 +155,7 @@ abstract class OAuth2
|
||||
/**
|
||||
* @param string $code
|
||||
*
|
||||
* @return int
|
||||
* @return string
|
||||
*/
|
||||
public function getAccessTokenExpiry(string $code): int
|
||||
{
|
||||
|
||||
@@ -108,7 +108,7 @@ class Disqus extends OAuth2
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $accessToken
|
||||
* @param string $token
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
|
||||
@@ -33,7 +33,7 @@ class PersonalData extends Password
|
||||
/**
|
||||
* Is valid.
|
||||
*
|
||||
* @param mixed $password
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
|
||||
@@ -6,8 +6,14 @@ use DeviceDetector\DeviceDetector;
|
||||
|
||||
class Detector
|
||||
{
|
||||
/**
|
||||
* @param string
|
||||
*/
|
||||
protected $userAgent = '';
|
||||
|
||||
/**
|
||||
* @param DeviceDetector
|
||||
*/
|
||||
protected $detctor;
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,6 +12,9 @@ class Compose
|
||||
*/
|
||||
protected $compose = [];
|
||||
|
||||
/**
|
||||
* @var string $data
|
||||
*/
|
||||
public function __construct(string $data)
|
||||
{
|
||||
$this->compose = yaml_parse($data);
|
||||
|
||||
@@ -11,6 +11,9 @@ class Service
|
||||
*/
|
||||
protected $service = [];
|
||||
|
||||
/**
|
||||
* @var string $path
|
||||
*/
|
||||
public function __construct(array $service)
|
||||
{
|
||||
$this->service = $service;
|
||||
|
||||
@@ -9,6 +9,9 @@ class Env
|
||||
*/
|
||||
protected $vars = [];
|
||||
|
||||
/**
|
||||
* @var string $data
|
||||
*/
|
||||
public function __construct(string $data)
|
||||
{
|
||||
$data = explode("\n", $data);
|
||||
|
||||
@@ -101,8 +101,7 @@ class Mail extends Event
|
||||
/**
|
||||
* Sets preview for the mail event.
|
||||
*
|
||||
* @param string $preview
|
||||
* @return self
|
||||
* @return string
|
||||
*/
|
||||
public function setPreview(string $preview): self
|
||||
{
|
||||
@@ -116,7 +115,7 @@ class Mail extends Event
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPreview(): string
|
||||
public function getPreview(string $preview): string
|
||||
{
|
||||
return $this->preview;
|
||||
}
|
||||
@@ -182,7 +181,7 @@ class Mail extends Event
|
||||
/**
|
||||
* Set SMTP port
|
||||
*
|
||||
* @param int $port
|
||||
* @param int port
|
||||
* @return self
|
||||
*/
|
||||
public function setSmtpPort(int $port): self
|
||||
@@ -218,7 +217,7 @@ class Mail extends Event
|
||||
/**
|
||||
* Set SMTP secure
|
||||
*
|
||||
* @param string $secure
|
||||
* @param string $password
|
||||
* @return self
|
||||
*/
|
||||
public function setSmtpSecure(string $secure): self
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace Appwrite\Event\Message;
|
||||
|
||||
use Utopia\Database\Document;
|
||||
|
||||
final class Usage extends Base
|
||||
class Usage extends Base
|
||||
{
|
||||
/**
|
||||
* @param Document $project
|
||||
@@ -40,7 +40,7 @@ final class Usage extends Base
|
||||
*/
|
||||
public static function fromArray(array $data): static
|
||||
{
|
||||
return new static(
|
||||
return new self(
|
||||
project: new Document($data['project'] ?? []),
|
||||
metrics: $data['metrics'] ?? [],
|
||||
reduce: array_map(fn (array $doc) => new Document($doc), $data['reduce'] ?? []),
|
||||
|
||||
@@ -86,7 +86,7 @@ class Messaging extends Event
|
||||
/**
|
||||
* Returns message document for the messaging event.
|
||||
*
|
||||
* @return Document
|
||||
* @return string
|
||||
*/
|
||||
public function getMessage(): Document
|
||||
{
|
||||
@@ -96,7 +96,7 @@ class Messaging extends Event
|
||||
/**
|
||||
* Sets message ID for the messaging event.
|
||||
*
|
||||
* @param string $messageId
|
||||
* @param string $message
|
||||
* @return self
|
||||
*/
|
||||
public function setMessageId(string $messageId): self
|
||||
|
||||
@@ -8,18 +8,6 @@ use Utopia\Database\Query;
|
||||
|
||||
class EventProcessor
|
||||
{
|
||||
/**
|
||||
* @param array<mixed> $events
|
||||
* @return array<string, bool>
|
||||
*/
|
||||
private function getEventMap(array $events): array
|
||||
{
|
||||
return \array_fill_keys(
|
||||
\array_map('strval', \array_unique($events)),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get function events for a project, using Redis cache
|
||||
* @param Document|null $project
|
||||
@@ -38,7 +26,7 @@ class EventProcessor
|
||||
$cacheKey = \sprintf(
|
||||
'%s-cache-%s:%s:%s:project:%s:functions:events',
|
||||
$dbForProject->getCacheName(),
|
||||
$hostname,
|
||||
$hostname ?? '',
|
||||
$dbForProject->getNamespace(),
|
||||
$dbForProject->getTenant(),
|
||||
$project->getId()
|
||||
@@ -48,9 +36,7 @@ class EventProcessor
|
||||
$cachedFunctionEvents = $dbForProject->getCache()->load($cacheKey, $ttl);
|
||||
|
||||
if ($cachedFunctionEvents !== false) {
|
||||
$decoded = \json_decode($cachedFunctionEvents, true);
|
||||
|
||||
return \is_array($decoded) ? $this->getEventMap(\array_keys($decoded)) : [];
|
||||
return \json_decode($cachedFunctionEvents, true) ?? [];
|
||||
}
|
||||
|
||||
$events = [];
|
||||
@@ -77,7 +63,7 @@ class EventProcessor
|
||||
}
|
||||
}
|
||||
|
||||
$uniqueEvents = $this->getEventMap($events);
|
||||
$uniqueEvents = \array_flip(\array_unique($events));
|
||||
$dbForProject->getCache()->save($cacheKey, \json_encode($uniqueEvents));
|
||||
|
||||
return $uniqueEvents;
|
||||
@@ -111,6 +97,6 @@ class EventProcessor
|
||||
}
|
||||
}
|
||||
|
||||
return $this->getEventMap($events);
|
||||
return \array_flip(\array_unique($events));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,16 +101,16 @@ class Mapper
|
||||
|
||||
if (\is_array($modelName)) {
|
||||
foreach ($modelName as $name) {
|
||||
$models[] = self::$models[$name];
|
||||
$models[] = static::$models[$name];
|
||||
}
|
||||
} else {
|
||||
$models[] = self::$models[$modelName];
|
||||
$models[] = static::$models[$modelName];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If single response, get its model and wrap in array
|
||||
$modelName = $responses->getModel();
|
||||
$models = [self::$models[$modelName]];
|
||||
$models = [static::$models[$modelName]];
|
||||
}
|
||||
|
||||
foreach ($models as $model) {
|
||||
@@ -425,7 +425,7 @@ class Mapper
|
||||
'name' => $unionName,
|
||||
'types' => $types,
|
||||
'resolveType' => static function ($object) use ($unionName) {
|
||||
return self::getUnionImplementation($unionName, $object);
|
||||
return static::getUnionImplementation($unionName, $object);
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -440,11 +440,11 @@ class Mapper
|
||||
|
||||
switch ($name) {
|
||||
case 'Attributes':
|
||||
return self::getColumnImplementation($object);
|
||||
return static::getColumnImplementation($object);
|
||||
case 'Columns':
|
||||
return self::getColumnImplementation($object, true);
|
||||
return static::getColumnImplementation($object, true);
|
||||
case 'HashOptions':
|
||||
return self::getHashOptionsImplementation($object);
|
||||
return static::getHashOptionsImplementation($object);
|
||||
}
|
||||
|
||||
throw new Exception('Unknown union type: ' . $name);
|
||||
|
||||
@@ -37,7 +37,7 @@ class Action extends UtopiaAction
|
||||
* Foreach Document
|
||||
* Call provided callback for each document in the collection
|
||||
*
|
||||
* @param Database $database
|
||||
* @param string $projectId
|
||||
* @param string $collection
|
||||
* @param array $queries
|
||||
* @param callable $callback
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Installer\Http\Installer\Certificate;
|
||||
|
||||
use Appwrite\Platform\Installer\Validator\AppDomain;
|
||||
use Utopia\Http\Adapter\Swoole\Response;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Validator\Range;
|
||||
|
||||
class Get extends Action
|
||||
{
|
||||
private const int CONNECTION_TIMEOUT_SECONDS = 5;
|
||||
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'installerCertificateGet';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/install/certificate')
|
||||
->desc('Check if SSL certificate is ready for a domain')
|
||||
->param('domain', '', new AppDomain(), 'Domain to check')
|
||||
->param('port', 443, new Range(1, 65535), 'HTTPS port to check', true)
|
||||
->inject('response')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
public function action(string $domain, int $port, Response $response): void
|
||||
{
|
||||
$domain = trim($domain);
|
||||
if ($domain === '') {
|
||||
$response->json(['ready' => false]);
|
||||
return;
|
||||
}
|
||||
|
||||
$ready = $this->checkHttps($domain, $port);
|
||||
$response->json(['ready' => $ready]);
|
||||
}
|
||||
|
||||
private function checkHttps(string $domain, int $port): bool
|
||||
{
|
||||
$gateway = $this->getDockerGateway();
|
||||
|
||||
$ch = curl_init();
|
||||
$options = [
|
||||
CURLOPT_URL => 'https://' . $domain . ':' . $port . '/',
|
||||
CURLOPT_NOBODY => true,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_CONNECTTIMEOUT => self::CONNECTION_TIMEOUT_SECONDS,
|
||||
CURLOPT_TIMEOUT => self::CONNECTION_TIMEOUT_SECONDS,
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
CURLOPT_SSL_VERIFYHOST => 2,
|
||||
];
|
||||
|
||||
if ($gateway !== '') {
|
||||
$options[CURLOPT_RESOLVE] = [$domain . ':' . $port . ':' . $gateway];
|
||||
}
|
||||
|
||||
curl_setopt_array($ch, $options);
|
||||
curl_exec($ch);
|
||||
$errno = curl_errno($ch);
|
||||
curl_close($ch);
|
||||
|
||||
return $errno === 0;
|
||||
}
|
||||
|
||||
private function getDockerGateway(): string
|
||||
{
|
||||
$route = @file_get_contents('/proc/net/route');
|
||||
if ($route === false) {
|
||||
return '';
|
||||
}
|
||||
|
||||
foreach (explode("\n", $route) as $line) {
|
||||
$fields = preg_split('/\s+/', trim($line));
|
||||
if (isset($fields[1]) && $fields[1] === '00000000' && isset($fields[2])) {
|
||||
$hex = $fields[2];
|
||||
if (strlen($hex) !== 8) {
|
||||
continue;
|
||||
}
|
||||
$ip = long2ip((int) hexdec($hex[6] . $hex[7] . $hex[4] . $hex[5] . $hex[2] . $hex[3] . $hex[0] . $hex[1]));
|
||||
return $ip;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -48,10 +48,9 @@ class Complete extends Action
|
||||
|
||||
@touch(Server::INSTALLER_COMPLETE_FILE);
|
||||
|
||||
$progressData = ($installId !== '') ? $state->readProgressFile($installId) : [];
|
||||
|
||||
if (!$sessionSecret) {
|
||||
$details = $progressData['details'][Server::STEP_ACCOUNT_SETUP] ?? [];
|
||||
if (!$sessionSecret && $installId !== '') {
|
||||
$data = $state->readProgressFile($installId);
|
||||
$details = $data['details'][Server::STEP_ACCOUNT_SETUP] ?? [];
|
||||
if (!empty($details['sessionSecret'])) {
|
||||
$sessionSecret = $details['sessionSecret'];
|
||||
$sessionId = $sessionId ?: ($details['sessionId'] ?? '');
|
||||
@@ -69,11 +68,8 @@ class Complete extends Action
|
||||
$expires = $timestamp;
|
||||
}
|
||||
}
|
||||
$appDomain = $progressData['payload']['appDomain'] ?? '';
|
||||
$cookieDomain = $this->buildCookieDomain($appDomain ?: $request->getHostname());
|
||||
|
||||
$response->addCookie('a_session_console', $sessionSecret, $expires, '/', $cookieDomain, $isHttps, true, $sameSite);
|
||||
$response->addCookie('a_session_console_legacy', $sessionSecret, $expires, '/', $cookieDomain, $isHttps, true, $sameSite);
|
||||
$response->addCookie('a_session_console', $sessionSecret, $expires, '/', '', $isHttps, true, $sameSite);
|
||||
$response->addCookie('a_session_console_legacy', $sessionSecret, $expires, '/', '', $isHttps, true, $sameSite);
|
||||
if ($sessionId) {
|
||||
$response->addHeader('X-Appwrite-Session', $sessionId);
|
||||
}
|
||||
@@ -83,42 +79,4 @@ class Complete extends Action
|
||||
|
||||
$response->json(['success' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the cookie domain to match Appwrite's convention in general.php.
|
||||
*
|
||||
* For localhost and IP addresses the domain is left empty (host-only cookie).
|
||||
* For real hostnames, the domain is prefixed with a dot so the cookie matches
|
||||
* Appwrite's default `'.' . $request->getHostname()` behaviour and lives in
|
||||
* the same cookie-jar slot — preventing stale ghost cookies after logout.
|
||||
*/
|
||||
private function buildCookieDomain(string $raw): string
|
||||
{
|
||||
$hostname = $this->extractHostname($raw);
|
||||
if ($hostname === '' || $hostname === 'localhost' || $hostname === '0.0.0.0' || $hostname === 'traefik') {
|
||||
return '';
|
||||
}
|
||||
if (filter_var($hostname, FILTER_VALIDATE_IP) !== false) {
|
||||
return '';
|
||||
}
|
||||
return '.' . $hostname;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the bare hostname from an appDomain value, stripping any port
|
||||
* suffix or IPv6 bracket notation.
|
||||
*/
|
||||
private function extractHostname(string $domain): string
|
||||
{
|
||||
$domain = trim($domain);
|
||||
if ($domain === '') {
|
||||
return '';
|
||||
}
|
||||
if (str_starts_with($domain, '[')) {
|
||||
$end = strpos($domain, ']');
|
||||
return $end !== false ? substr($domain, 1, $end - 1) : '';
|
||||
}
|
||||
$parts = explode(':', $domain);
|
||||
return count($parts) <= 2 ? strtolower($parts[0]) : strtolower($domain);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ class Install extends Action
|
||||
->param('appDomain', '', new AppDomain(), 'Application domain (hostname, IP, or bracket IPv6 with optional port)')
|
||||
->param('httpPort', 80, new Range(1, 65535), 'HTTP port')
|
||||
->param('httpsPort', 443, new Range(1, 65535), 'HTTPS port')
|
||||
->param('emailCertificates', '', new Email(allowEmpty: true), 'Email for SSL certificates', true)
|
||||
->param('emailCertificates', '', new Email(), 'Email for SSL certificates')
|
||||
->param('opensslKey', '', new Text(64, 0), 'Secret API key', true)
|
||||
->param('assistantOpenAIKey', '', new Text(256, 0), 'OpenAI API key for assistant', true)
|
||||
->param('accountEmail', '', new Email(allowEmpty: true), 'Account email address', true)
|
||||
@@ -43,7 +43,6 @@ class Install extends Action
|
||||
->param('database', '', new WhiteList(['mongodb', 'mariadb', 'postgresql']), 'Database adapter', true)
|
||||
->param('installId', '', new Text(64, 0), 'Installation ID', true)
|
||||
->param('retryStep', null, new Nullable(new WhiteList([Server::STEP_DOCKER_COMPOSE, Server::STEP_ENV_VARS, Server::STEP_DOCKER_CONTAINERS], true)), 'Retry from step', true)
|
||||
->param('migrate', false, new \Utopia\Validator\Boolean(true), 'Run database migration after upgrade', true)
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('swooleResponse')
|
||||
@@ -65,7 +64,6 @@ class Install extends Action
|
||||
string $database,
|
||||
string $installId,
|
||||
?string $retryStep,
|
||||
bool $migrate,
|
||||
Request $request,
|
||||
Response $response,
|
||||
SwooleResponse $swooleResponse,
|
||||
@@ -92,9 +90,6 @@ class Install extends Action
|
||||
|
||||
$appDomain = trim($appDomain);
|
||||
$emailCertificates = trim($emailCertificates);
|
||||
if ($emailCertificates === '') {
|
||||
$emailCertificates = trim($accountEmail);
|
||||
}
|
||||
$opensslKey = trim($opensslKey);
|
||||
$assistantOpenAIKey = trim($assistantOpenAIKey);
|
||||
|
||||
@@ -145,8 +140,6 @@ class Install extends Action
|
||||
|
||||
@unlink(Server::INSTALLER_COMPLETE_FILE);
|
||||
|
||||
$state->clearStaleLockIfNeeded();
|
||||
|
||||
try {
|
||||
$lockResult = $state->reserveGlobalLock($installId);
|
||||
} catch (\Throwable $e) {
|
||||
@@ -182,23 +175,15 @@ class Install extends Action
|
||||
if (file_exists($existingPath)) {
|
||||
$existing = $state->readProgressFile($installId);
|
||||
if (!empty($existing['steps']) && $retryStep === null) {
|
||||
$previousHadError = isset($existing['error']);
|
||||
$allCompleted = !$previousHadError && $this->allStepsCompleted($existing['steps']);
|
||||
|
||||
if ($previousHadError || $allCompleted) {
|
||||
@unlink($existingPath);
|
||||
$existing = null;
|
||||
$state->updateGlobalLock($installId, Server::STATUS_ERROR);
|
||||
if ($wantsStream) {
|
||||
$this->writeSseEvent($swooleResponse, Server::STATUS_ERROR, ['message' => 'Installation already started']);
|
||||
$swooleResponse->end();
|
||||
} else {
|
||||
$state->updateGlobalLock($installId, Server::STATUS_ERROR);
|
||||
if ($wantsStream) {
|
||||
$this->writeSseEvent($swooleResponse, Server::STATUS_ERROR, ['message' => 'Installation already started']);
|
||||
$swooleResponse->end();
|
||||
} else {
|
||||
$response->setStatusCode(Response::STATUS_CODE_CONFLICT);
|
||||
$response->json(['success' => false, 'message' => 'Installation already started']);
|
||||
}
|
||||
return;
|
||||
$response->setStatusCode(Response::STATUS_CODE_CONFLICT);
|
||||
$response->json(['success' => false, 'message' => 'Installation already started']);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,8 +207,7 @@ class Install extends Action
|
||||
'_APP_ASSISTANT_OPENAI_API_KEY' => $assistantOpenAIKey,
|
||||
];
|
||||
|
||||
$previousHadError = is_array($existing) && isset($existing['error']);
|
||||
if ($this->hasPayload($existing) && !$previousHadError) {
|
||||
if ($this->hasPayload($existing)) {
|
||||
$stored = $existing['payload'];
|
||||
$inputValues = [
|
||||
'httpPort' => (string) $httpPort,
|
||||
@@ -323,28 +307,6 @@ class Install extends Action
|
||||
}
|
||||
};
|
||||
|
||||
$responseSent = false;
|
||||
$onComplete = function () use ($wantsStream, $swooleResponse, $response, $installId, $state, &$responseSent) {
|
||||
if ($responseSent) {
|
||||
return;
|
||||
}
|
||||
$responseSent = true;
|
||||
$state->updateGlobalLock($installId, Server::STATUS_COMPLETED);
|
||||
if ($wantsStream) {
|
||||
$this->writeSseEvent($swooleResponse, 'done', ['installId' => $installId, 'success' => true]);
|
||||
usleep(self::SSE_KEEPALIVE_DELAY_MICROSECONDS);
|
||||
$swooleResponse->write(": keepalive\n\n");
|
||||
usleep(self::SSE_KEEPALIVE_DELAY_MICROSECONDS);
|
||||
$swooleResponse->end();
|
||||
} else {
|
||||
$response->json([
|
||||
'success' => true,
|
||||
'installId' => $installId,
|
||||
'message' => 'Installation completed successfully',
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
$installer->performInstallation(
|
||||
$httpPort ?: $config->getDefaultHttpPort(),
|
||||
$httpsPort ?: $config->getDefaultHttpsPort(),
|
||||
@@ -355,12 +317,23 @@ class Install extends Action
|
||||
$progress,
|
||||
$retryStep,
|
||||
$config->isUpgrade(),
|
||||
$account,
|
||||
$onComplete,
|
||||
$migrate,
|
||||
$account
|
||||
);
|
||||
|
||||
$onComplete();
|
||||
if ($wantsStream) {
|
||||
$this->writeSseEvent($swooleResponse, 'done', ['installId' => $installId, 'success' => true]);
|
||||
usleep(self::SSE_KEEPALIVE_DELAY_MICROSECONDS);
|
||||
$swooleResponse->write(": keepalive\n\n");
|
||||
usleep(self::SSE_KEEPALIVE_DELAY_MICROSECONDS);
|
||||
$swooleResponse->end();
|
||||
} else {
|
||||
$response->json([
|
||||
'success' => true,
|
||||
'installId' => $installId,
|
||||
'message' => 'Installation completed successfully',
|
||||
]);
|
||||
}
|
||||
$state->updateGlobalLock($installId, Server::STATUS_COMPLETED);
|
||||
} catch (\Throwable $e) {
|
||||
$this->handleInstallationError($e, $installId, $wantsStream, $response, $swooleResponse, $state);
|
||||
}
|
||||
@@ -395,6 +368,8 @@ class Install extends Action
|
||||
$state->updateGlobalLock($installId, Server::STATUS_ERROR);
|
||||
}
|
||||
|
||||
@unlink(Server::INSTALLER_CONFIG_FILE);
|
||||
|
||||
if ($wantsStream) {
|
||||
$this->writeSseEvent($swooleResponse, Server::STATUS_ERROR, [
|
||||
'message' => $e->getMessage(),
|
||||
@@ -417,16 +392,6 @@ class Install extends Action
|
||||
return is_array($data) && isset($data['payload']) && is_array($data['payload']);
|
||||
}
|
||||
|
||||
private function allStepsCompleted(array $steps): bool
|
||||
{
|
||||
foreach ($steps as $step) {
|
||||
if (($step['status'] ?? '') !== Server::STATUS_COMPLETED) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private function deriveNameFromEmail(string $email): string
|
||||
{
|
||||
$parts = explode('@', $email);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user