Compare commits

..
Author SHA1 Message Date
Darshan 94c511f39a lint. 2026-03-05 14:03:30 +05:30
Darshan 7d72fd77e5 fix: test. 2026-03-05 13:58:42 +05:30
Darshan 7477b43653 fix: correct tag. 2026-03-05 13:38:14 +05:30
Darshan 569505c54d readd: local source mounts. 2026-03-05 13:21:49 +05:30
DarshanandGitHub e0d2db0c24 Merge pull request #11458 from appwrite/simplify-dev-merge 2026-03-05 13:13:43 +05:30
Darshan dc66be4b2c Merge branch 'feat-installer' into 'simplify-dev'. 2026-03-05 13:12:15 +05:30
Darshan ccd43c9c2a add: timeout back with correct fix. 2026-02-09 15:54:06 +05:30
Darshan fa6e8b8158 add: timeout back. 2026-02-09 15:41:45 +05:30
Darshan 96d94c9452 update: cleanup on redirects. 2026-02-09 14:57:37 +05:30
Darshan 6454ef55ad fix: router bug for in network connection. 2026-02-09 14:47:47 +05:30
Darshan d502320a94 update: simpler dev testing. 2026-02-09 14:47:26 +05:30
Darshan 850bd81511 fix: local image tagging. 2026-02-04 17:54:26 +05:30
246 changed files with 6975 additions and 29925 deletions
+2
View File
@@ -5,6 +5,8 @@ on:
types: [opened, edited]
issue_comment:
types: [created, edited]
pull_request:
types: [opened, edited]
pull_request_review:
types: [submitted, edited]
pull_request_review_comment:
+123
View File
@@ -0,0 +1,123 @@
name: Benchmark
concurrency:
group: '${{ github.workflow }}-${{ github.ref }}'
cancel-in-progress: true
env:
COMPOSE_FILE: docker-compose.yml
IMAGE: appwrite-dev
CACHE_KEY: 'appwrite-dev-${{ github.event.pull_request.head.sha }}'
'on':
- pull_request
jobs:
setup:
name: Setup & Build Appwrite Image
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
submodules: recursive
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build Appwrite
uses: docker/build-push-action@v6
with:
context: .
push: false
tags: '${{ env.IMAGE }}'
load: true
cache-from: type=gha
cache-to: 'type=gha,mode=max'
outputs: 'type=docker,dest=/tmp/${{ env.IMAGE }}.tar'
target: development
build-args: |
DEBUG=false
TESTING=true
VERSION=dev
- name: Cache Docker Image
uses: actions/cache@v4
with:
key: '${{ env.CACHE_KEY }}'
path: '/tmp/${{ env.IMAGE }}.tar'
benchmarking:
name: Benchmark
runs-on: ubuntu-latest
needs: setup
permissions:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Load Cache
uses: actions/cache@v4
with:
key: '${{ env.CACHE_KEY }}'
path: '/tmp/${{ env.IMAGE }}.tar'
fail-on-cache-miss: true
- name: Load and Start Appwrite
run: |
sed -i 's/traefik/localhost/g' .env
docker load --input /tmp/${{ env.IMAGE }}.tar
docker compose up -d
sleep 10
- name: Install Oha
run: |
echo "deb [signed-by=/usr/share/keyrings/azlux-archive-keyring.gpg] http://packages.azlux.fr/debian/ stable main" | sudo tee /etc/apt/sources.list.d/azlux.list
sudo wget -O /usr/share/keyrings/azlux-archive-keyring.gpg https://azlux.fr/repo.gpg
sudo apt update
sudo apt install oha
oha --version
- name: Benchmark PR
run: 'oha -z 180s http://localhost/v1/health/version --output-format json > benchmark.json'
- name: Cleaning
run: docker compose down -v
- name: Installing latest version
run: |
rm docker-compose.yml
rm .env
curl https://appwrite.io/install/compose -o docker-compose.yml
curl https://appwrite.io/install/env -o .env
sed -i 's/_APP_OPTIONS_ABUSE=enabled/_APP_OPTIONS_ABUSE=disabled/g' .env
docker compose up -d
sleep 10
- name: Benchmark Latest
run: oha -z 180s http://localhost/v1/health/version --output-format json > benchmark-latest.json
- name: Prepare comment
run: |
echo '## :sparkles: Benchmark results' > benchmark.txt
echo ' ' >> benchmark.txt
echo "- Requests per second: $(jq -r '.summary.requestsPerSec|tonumber?|floor|tostring|[while(length>0;.[:-3])|.[-3:]]|reverse|join(",")' benchmark.json)" >> benchmark.txt
echo "- Requests with 200 status code: $(jq -r '.statusCodeDistribution."200"|tostring|[while(length>0;.[:-3])|.[-3:]]|reverse|join(",")' benchmark.json)" >> benchmark.txt
echo "- P99 latency: $(jq -r '.latencyPercentiles.p99' benchmark.json )" >> benchmark.txt
echo " " >> benchmark.txt
echo " " >> benchmark.txt
echo "## :zap: Benchmark Comparison" >> benchmark.txt
echo " " >> benchmark.txt
echo "| Metric | This PR | Latest version | " >> benchmark.txt
echo "| --- | --- | --- | " >> benchmark.txt
echo "| RPS | $(jq -r '.summary.requestsPerSec|tonumber?|floor|tostring|[while(length>0;.[:-3])|.[-3:]]|reverse|join(",")' benchmark.json) | $(jq -r '.summary.requestsPerSec|tonumber|floor|tostring|[while(length>0;.[:-3])|.[-3:]]|reverse|join(",")' benchmark-latest.json) | " >> benchmark.txt
echo "| 200 | $(jq -r '.statusCodeDistribution."200"|tostring|[while(length>0;.[:-3])|.[-3:]]|reverse|join(",")' benchmark.json) | $(jq -r '.statusCodeDistribution."200"|tostring|[while(length>0;.[:-3])|.[-3:]]|reverse|join(",")' benchmark-latest.json) | " >> benchmark.txt
echo "| P99 | $(jq -r '.latencyPercentiles.p99' benchmark.json ) | $(jq -r '.latencyPercentiles.p99' benchmark-latest.json ) | " >> benchmark.txt
- name: Save results
uses: actions/upload-artifact@v6
if: '${{ !cancelled() }}'
with:
name: benchmark.json
path: benchmark.json
retention-days: 7
- name: Find Comment
if: github.event.pull_request.head.repo.full_name == github.repository
uses: peter-evans/find-comment@v3
id: fc
with:
issue-number: '${{ github.event.pull_request.number }}'
comment-author: 'github-actions[bot]'
body-includes: Benchmark results
- name: Comment on PR
if: github.event.pull_request.head.repo.full_name == github.repository
uses: peter-evans/create-or-update-comment@v4
with:
comment-id: '${{ steps.fc.outputs.comment-id }}'
issue-number: '${{ github.event.pull_request.number }}'
body-path: benchmark.txt
edit-mode: replace
+19
View File
@@ -0,0 +1,19 @@
name: Check dependencies
# Adapted from https://google.github.io/osv-scanner/github-action/#scan-on-pull-request
on:
pull_request:
branches: [main, 1.*.x]
merge_group:
branches: [main, 1.*.x]
permissions:
# Require writing security events to upload SARIF file to security tab
security-events: write
# Only need to read contents
contents: read
jobs:
scan-pr:
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@v1.7.1"
-707
View File
@@ -1,707 +0,0 @@
name: CI
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
env:
COMPOSE_FILE: docker-compose.yml
IMAGE: appwrite-dev
CACHE_KEY: appwrite-dev-${{ github.event.pull_request.head.sha }}
on:
pull_request:
workflow_dispatch:
inputs:
response_format:
description: 'Response format version to test (e.g., 1.5.0, 1.4.0)'
required: false
type: string
default: ''
jobs:
dependencies:
name: Checks / Dependencies
if: github.event_name == 'pull_request'
permissions:
actions: read
security-events: write
contents: read
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@v2.3.3"
security:
name: Checks / Image
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- name: Check out code
uses: actions/checkout@v6
with:
fetch-depth: 0
submodules: 'recursive'
- name: Build the Docker image
uses: docker/build-push-action@v6
with:
context: .
push: false
load: true
tags: pr_image:${{ github.sha }}
target: production
- name: Run Trivy vulnerability scanner on image
uses: aquasecurity/trivy-action@0.35.0
with:
image-ref: 'pr_image:${{ github.sha }}'
format: 'sarif'
output: 'trivy-image-results.sarif'
severity: 'CRITICAL,HIGH'
- name: Run Trivy vulnerability scanner on source code
uses: aquasecurity/trivy-action@0.35.0
with:
scan-type: 'fs'
scan-ref: '.'
format: 'sarif'
output: 'trivy-fs-results.sarif'
severity: 'CRITICAL,HIGH'
skip-setup-trivy: true
- name: Upload image scan results
uses: github/codeql-action/upload-sarif@v4
if: always() && hashFiles('trivy-image-results.sarif') != ''
with:
sarif_file: 'trivy-image-results.sarif'
category: 'trivy-image'
- name: Upload source code scan results
uses: github/codeql-action/upload-sarif@v4
if: always() && hashFiles('trivy-fs-results.sarif') != ''
with:
sarif_file: 'trivy-fs-results.sarif'
category: 'trivy-source'
composer:
name: Checks / Composer
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
tools: composer:v2
coverage: none
- name: Validate
run: composer validate
- name: Install dependencies
run: composer install --prefer-dist --no-progress --ignore-platform-reqs
- name: Audit
env:
COMPOSER_NO_AUDIT: 0
run: composer audit
format:
name: Checks / Format
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 2
- run: git checkout HEAD^2
if: github.event_name == 'pull_request'
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
tools: composer:v2
coverage: none
- name: Install dependencies
run: composer install --prefer-dist --no-progress --ignore-platform-reqs
- name: Run Linter
run: composer lint
analyze:
name: Checks / Analyze
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v6
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
tools: composer:v2
coverage: none
- name: Install dependencies
run: composer install --prefer-dist --no-progress --ignore-platform-reqs
- name: Run PHPStan
run: composer analyze
locale:
name: Checks / Locale
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v6
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '24'
- name: Run Locale check
run: node .github/workflows/static-analysis/locale/index.js
matrix:
name: Tests / Matrix
runs-on: ubuntu-latest
outputs:
databases: ${{ steps.generate.outputs.databases }}
modes: ${{ steps.generate.outputs.modes }}
steps:
- name: Generate matrix
id: generate
uses: actions/github-script@v8
with:
script: |
const allDatabases = ['MariaDB', 'PostgreSQL', 'MongoDB'];
const allModes = ['dedicated', 'shared_v1', 'shared_v2'];
const defaultDatabases = ['MongoDB'];
const defaultModes = ['dedicated'];
const pr = context.payload.pull_request;
if (!pr) {
core.setOutput('databases', JSON.stringify(allDatabases));
core.setOutput('modes', JSON.stringify(allModes));
return;
}
const getContent = (ref) => github.rest.repos.getContent({
owner: context.repo.owner,
repo: context.repo.repo,
path: 'composer.lock',
ref,
});
const getDbVersion = (lock) => lock.packages?.find(p => p.name === 'utopia-php/database')?.version;
const [{ data: base }, { data: head }] = await Promise.all([
getContent(pr.base.sha),
getContent(pr.head.sha),
]);
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 ? allDatabases : defaultDatabases));
core.setOutput('modes', JSON.stringify(databaseChanged ? allModes : defaultModes));
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
submodules: recursive
- name: Login to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Build Appwrite
uses: docker/build-push-action@v6
with:
context: .
push: false
tags: ${{ env.IMAGE }}
load: true
cache-from: type=gha
cache-to: type=gha,mode=max
outputs: type=docker,dest=/tmp/${{ env.IMAGE }}.tar
target: development
build-args: |
DEBUG=false
TESTING=true
VERSION=dev
- name: Cache Docker Image
uses: actions/cache@v5
with:
key: ${{ env.CACHE_KEY }}
path: /tmp/${{ env.IMAGE }}.tar
unit:
name: Tests / Unit
runs-on: ubuntu-latest
needs: build
permissions:
contents: read
pull-requests: write
steps:
- name: checkout
uses: actions/checkout@v6
- name: Load Cache
uses: actions/cache@v5
with:
key: ${{ env.CACHE_KEY }}
path: /tmp/${{ env.IMAGE }}.tar
fail-on-cache-miss: true
- name: Login to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Load and Start Appwrite
timeout-minutes: 5
run: |
docker load --input /tmp/${{ env.IMAGE }}.tar
docker compose pull --quiet --ignore-buildable
docker compose up -d --quiet-pull --wait
- name: Environment Variables
run: docker compose exec -T appwrite vars
- name: Run Unit 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/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 / General
runs-on: ubuntu-latest
needs: build
permissions:
contents: read
pull-requests: write
steps:
- name: checkout
uses: actions/checkout@v6
- name: Load Cache
uses: actions/cache@v5
with:
key: ${{ env.CACHE_KEY }}
path: /tmp/${{ env.IMAGE }}.tar
fail-on-cache-miss: true
- name: Login to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Load and Start Appwrite
timeout-minutes: 5
run: |
docker load --input /tmp/${{ env.IMAGE }}.tar
docker compose pull --quiet --ignore-buildable
docker compose up -d --quiet-pull --wait
- name: Wait for Open Runtimes
timeout-minutes: 3
run: |
while ! docker compose logs openruntimes-executor | grep -q "Executor is ready."; do
echo "Waiting for Executor to come online"
sleep 1
done
- 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()
run: |
echo "=== Appwrite Logs ==="
docker compose logs
e2e_service:
name: Tests / E2E / ${{ matrix.database }} (${{ matrix.mode }}) / ${{ matrix.service }}
runs-on: ubuntu-latest
needs: [build, matrix]
permissions:
contents: read
pull-requests: write
strategy:
fail-fast: false
matrix:
database: ${{ fromJSON(needs.matrix.outputs.databases) }}
mode: ${{ fromJSON(needs.matrix.outputs.modes) }}
service: [
Account,
Avatars,
Console,
Databases,
TablesDB,
Functions,
FunctionsSchedule,
GraphQL,
Health,
Locale,
Projects,
Realtime,
Sites,
Proxy,
Storage,
Tokens,
Teams,
Users,
ProjectWebhooks,
Webhooks,
VCS,
Messaging,
Migrations
]
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Load Cache
uses: actions/cache@v5
with:
key: ${{ env.CACHE_KEY }}
path: /tmp/${{ env.IMAGE }}.tar
fail-on-cache-miss: true
- name: Set database environment
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:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- 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
docker compose up -d --quiet-pull --wait
- name: Wait for Open Runtimes
timeout-minutes: 3
run: |
while ! docker compose logs openruntimes-executor | grep -q "Executor is ready."; do
echo "Waiting for Executor to come online"
sleep 1
done
- name: Run tests
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()
run: |
echo "=== Appwrite Logs ==="
docker compose logs
e2e_abuse:
name: Tests / E2E / Abuse (${{ matrix.mode }})
runs-on: ubuntu-latest
needs: [build, matrix]
permissions:
contents: read
pull-requests: write
strategy:
fail-fast: false
matrix:
mode: ${{ fromJSON(needs.matrix.outputs.modes) }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Load Cache
uses: actions/cache@v5
with:
key: ${{ env.CACHE_KEY }}
path: /tmp/${{ env.IMAGE }}.tar
fail-on-cache-miss: true
- name: Login to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Load and Start Appwrite
timeout-minutes: 5
env:
_APP_OPTIONS_ABUSE: enabled
_APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }}
_APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }}
run: |
docker load --input /tmp/${{ env.IMAGE }}.tar
docker compose pull --quiet --ignore-buildable
docker compose up -d --quiet-pull --wait
- name: Run 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
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()
run: |
echo "=== Appwrite Logs ==="
docker compose logs
e2e_screenshots:
name: Tests / E2E / Screenshots (${{ matrix.mode }})
runs-on: ubuntu-latest
needs: [build, matrix]
permissions:
contents: read
pull-requests: write
strategy:
fail-fast: false
matrix:
mode: ${{ fromJSON(needs.matrix.outputs.modes) }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Load Cache
uses: actions/cache@v5
with:
key: ${{ env.CACHE_KEY }}
path: /tmp/${{ env.IMAGE }}.tar
fail-on-cache-miss: true
- name: Login to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Load and Start Appwrite
timeout-minutes: 5
env:
_APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }}
_APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }}
run: |
docker load --input /tmp/${{ env.IMAGE }}.tar
docker compose pull --quiet --ignore-buildable
docker compose up -d --quiet-pull --wait
- name: Wait for Open Runtimes
timeout-minutes: 3
run: |
while ! docker compose logs openruntimes-executor | grep -q "Executor is ready."; do
echo "Waiting for Executor to come online"
sleep 1
done
- name: Run 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/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()
run: |
echo "=== Appwrite Logs ==="
docker compose logs
benchmark:
name: Benchmark
runs-on: ubuntu-latest
needs: build
permissions:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Load Cache
uses: actions/cache@v5
with:
key: ${{ env.CACHE_KEY }}
path: /tmp/${{ env.IMAGE }}.tar
fail-on-cache-miss: true
- name: Login to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Load and Start Appwrite
run: |
sed -i 's/traefik/localhost/g' .env
docker load --input /tmp/${{ env.IMAGE }}.tar
docker compose up -d
sleep 10
- name: Install Oha
run: |
echo "deb [signed-by=/usr/share/keyrings/azlux-archive-keyring.gpg] http://packages.azlux.fr/debian/ stable main" | sudo tee /etc/apt/sources.list.d/azlux.list
sudo wget -O /usr/share/keyrings/azlux-archive-keyring.gpg https://azlux.fr/repo.gpg
sudo apt update
sudo apt install oha
oha --version
- name: Benchmark PR
run: 'oha -z 180s http://localhost/v1/health/version --output-format json > benchmark.json'
- name: Cleaning
run: docker compose down -v
- name: Installing latest version
run: |
rm docker-compose.yml
rm .env
curl https://appwrite.io/install/compose -o docker-compose.yml
curl https://appwrite.io/install/env -o .env
sed -i 's/_APP_OPTIONS_ABUSE=enabled/_APP_OPTIONS_ABUSE=disabled/g' .env
docker compose up -d
sleep 10
- name: Benchmark Latest
run: oha -z 180s http://localhost/v1/health/version --output-format json > benchmark-latest.json
- name: Prepare comment
run: |
echo '## :sparkles: Benchmark results' > benchmark.txt
echo ' ' >> benchmark.txt
echo "- Requests per second: $(jq -r '.summary.requestsPerSec|tonumber?|floor|tostring|[while(length>0;.[:-3])|.[-3:]]|reverse|join(",")' benchmark.json)" >> benchmark.txt
echo "- Requests with 200 status code: $(jq -r '.statusCodeDistribution."200"|tostring|[while(length>0;.[:-3])|.[-3:]]|reverse|join(",")' benchmark.json)" >> benchmark.txt
echo "- P99 latency: $(jq -r '.latencyPercentiles.p99' benchmark.json )" >> benchmark.txt
echo " " >> benchmark.txt
echo " " >> benchmark.txt
echo "## :zap: Benchmark Comparison" >> benchmark.txt
echo " " >> benchmark.txt
echo "| Metric | This PR | Latest version | " >> benchmark.txt
echo "| --- | --- | --- | " >> benchmark.txt
echo "| RPS | $(jq -r '.summary.requestsPerSec|tonumber?|floor|tostring|[while(length>0;.[:-3])|.[-3:]]|reverse|join(",")' benchmark.json) | $(jq -r '.summary.requestsPerSec|tonumber|floor|tostring|[while(length>0;.[:-3])|.[-3:]]|reverse|join(",")' benchmark-latest.json) | " >> benchmark.txt
echo "| 200 | $(jq -r '.statusCodeDistribution."200"|tostring|[while(length>0;.[:-3])|.[-3:]]|reverse|join(",")' benchmark.json) | $(jq -r '.statusCodeDistribution."200"|tostring|[while(length>0;.[:-3])|.[-3:]]|reverse|join(",")' benchmark-latest.json) | " >> benchmark.txt
echo "| P99 | $(jq -r '.latencyPercentiles.p99' benchmark.json ) | $(jq -r '.latencyPercentiles.p99' benchmark-latest.json ) | " >> benchmark.txt
- name: Save results
uses: actions/upload-artifact@v7
if: ${{ !cancelled() }}
with:
name: benchmark.json
path: benchmark.json
retention-days: 7
- name: Find Comment
if: github.event.pull_request.head.repo.full_name == github.repository
uses: peter-evans/find-comment@v3
id: fc
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: 'github-actions[bot]'
body-includes: Benchmark results
- name: Comment on PR
if: github.event.pull_request.head.repo.full_name == github.repository
uses: peter-evans/create-or-update-comment@v4
with:
comment-id: ${{ steps.fc.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
body-path: benchmark.txt
edit-mode: replace
+28
View File
@@ -0,0 +1,28 @@
name: "Linter"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on: [pull_request]
jobs:
lint:
name: Linter
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 2
- run: git checkout HEAD^2
- name: Validate composer.json and composer.lock
run: |
docker run --rm -v $PWD:/app composer:2.8 sh -c \
"composer validate"
- name: Run Linter
run: |
docker run --rm -v $PWD:/app composer:2.8 sh -c \
"composer install --profile --ignore-platform-reqs && composer lint"
+106
View File
@@ -0,0 +1,106 @@
name: PR Security Scan
on:
pull_request_target:
types: [opened, synchronize, reopened]
jobs:
scan:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- name: Check out code
uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
submodules: 'recursive'
- name: Build the Docker image
uses: docker/build-push-action@v6
with:
context: .
push: false
load: true
tags: pr_image:${{ github.sha }}
target: production
- name: Run Trivy vulnerability scanner on image
uses: aquasecurity/trivy-action@0.20.0
with:
image-ref: 'pr_image:${{ github.sha }}'
format: 'json'
output: 'trivy-image-results.json'
severity: 'CRITICAL,HIGH'
- name: Run Trivy vulnerability scanner on source code
uses: aquasecurity/trivy-action@0.20.0
with:
scan-type: 'fs'
scan-ref: '.'
format: 'json'
output: 'trivy-fs-results.json'
severity: 'CRITICAL,HIGH'
- name: Process Trivy scan results
id: process-results
uses: actions/github-script@v8
with:
script: |
const fs = require('fs');
let commentBody = '## Security Scan Results for PR\n\n';
function processResults(results, title) {
let sectionBody = `### ${title}\n\n`;
if (results.Results && results.Results.some(result => result.Vulnerabilities && result.Vulnerabilities.length > 0)) {
sectionBody += '| Package | Version | Vulnerability | Severity |\n';
sectionBody += '|---------|---------|----------------|----------|\n';
const uniqueVulns = new Set();
results.Results.forEach(result => {
if (result.Vulnerabilities) {
result.Vulnerabilities.forEach(vuln => {
const vulnKey = `${vuln.PkgName}-${vuln.InstalledVersion}-${vuln.VulnerabilityID}`;
if (!uniqueVulns.has(vulnKey)) {
uniqueVulns.add(vulnKey);
sectionBody += `| ${vuln.PkgName} | ${vuln.InstalledVersion} | [${vuln.VulnerabilityID}](https://nvd.nist.gov/vuln/detail/${vuln.VulnerabilityID}) | ${vuln.Severity} |\n`;
}
});
}
});
} else {
sectionBody += '🎉 No vulnerabilities found!\n';
}
return sectionBody;
}
try {
const imageResults = JSON.parse(fs.readFileSync('trivy-image-results.json', 'utf8'));
const fsResults = JSON.parse(fs.readFileSync('trivy-fs-results.json', 'utf8'));
commentBody += processResults(imageResults, "Docker Image Scan Results");
commentBody += '\n';
commentBody += processResults(fsResults, "Source Code Scan Results");
} catch (error) {
commentBody += `There was an error while running the security scan: ${error.message}\n`;
commentBody += 'Please contact the core team for assistance.';
}
core.setOutput('comment-body', commentBody);
- name: Find Comment
uses: peter-evans/find-comment@v3
id: fc
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: 'github-actions[bot]'
body-includes: Security Scan Results for PR
- name: Create or update comment
uses: peter-evans/create-or-update-comment@v3
with:
issue-number: ${{ github.event.pull_request.number }}
comment-id: ${{ steps.fc.outputs.comment-id }}
body: ${{ steps.process-results.outputs.comment-body }}
edit-mode: replace
+1 -1
View File
@@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v10
- uses: actions/stale@v9
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: "This issue has been labeled as a 'question', indicating that it requires additional information from the requestor. It has been inactive for 7 days. If no further activity occurs, this issue will be closed in 14 days."
+21
View File
@@ -0,0 +1,21 @@
name: "Static code analysis"
on: [pull_request]
jobs:
lint:
name: CodeQL
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v6
- name: Run CodeQL
run: |
docker run --rm -v $PWD:/app composer:2.8 sh -c \
"composer install --profile --ignore-platform-reqs && composer check"
- name: Run Locale check
run: |
docker run --rm -v $PWD:/app node:24-alpine sh -c \
"cd /app/.github/workflows/static-analysis/locale && node index.js"
+691
View File
@@ -0,0 +1,691 @@
name: "Tests"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
COMPOSE_FILE: docker-compose.yml
IMAGE: appwrite-dev
CACHE_KEY: appwrite-dev-${{ github.event.pull_request.head.sha }}
on:
pull_request:
workflow_dispatch:
inputs:
response_format:
description: 'Response format version to test (e.g., 1.5.0, 1.4.0)'
required: false
type: string
default: ''
jobs:
check_database_changes:
name: Check if utopia-php/database changed
runs-on: ubuntu-latest
outputs:
database_changed: ${{ steps.check.outputs.database_changed }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Fetch base branch
run: git fetch origin ${{ github.event.pull_request.base.ref }}
- name: Check for utopia-php/database changes
id: check
run: |
if git diff origin/${{ github.event.pull_request.base.ref }} HEAD -- composer.lock | grep -q '"name": "utopia-php/database"'; then
echo "Database version changed, going to run all mode tests."
echo "database_changed=true" >> "$GITHUB_ENV"
echo "database_changed=true" >> "$GITHUB_OUTPUT"
else
echo "database_changed=false" >> "$GITHUB_ENV"
echo "database_changed=false" >> "$GITHUB_OUTPUT"
fi
setup:
name: Setup & Build Appwrite Image
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
submodules: recursive
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build Appwrite
uses: docker/build-push-action@v6
with:
context: .
push: false
tags: ${{ env.IMAGE }}
load: true
cache-from: type=gha
cache-to: type=gha,mode=max
outputs: type=docker,dest=/tmp/${{ env.IMAGE }}.tar
target: development
build-args: |
DEBUG=false
TESTING=true
VERSION=dev
- name: Cache Docker Image
uses: actions/cache@v4
with:
key: ${{ env.CACHE_KEY }}
path: /tmp/${{ env.IMAGE }}.tar
unit_test:
name: Unit Test
runs-on: ubuntu-latest
needs: setup
permissions:
contents: read
pull-requests: write
steps:
- name: checkout
uses: actions/checkout@v6
- name: Load Cache
uses: actions/cache@v4
with:
key: ${{ env.CACHE_KEY }}
path: /tmp/${{ env.IMAGE }}.tar
fail-on-cache-miss: true
- name: Load and Start Appwrite
timeout-minutes: 3
run: |
docker load --input /tmp/${{ env.IMAGE }}.tar
docker compose up -d
until docker compose exec -T appwrite doctor > /dev/null 2>&1; do
echo "Waiting for Appwrite to be ready..."
sleep 2
done
- name: Environment Variables
run: docker compose exec -T appwrite vars
- name: Run Unit Tests
uses: itznotabug/php-retry@v3
with:
max_attempts: 2
retry_wait_seconds: 300
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_test:
name: E2E General Test
runs-on: ubuntu-latest
needs: setup
permissions:
contents: read
pull-requests: write
steps:
- name: checkout
uses: actions/checkout@v6
- name: Load Cache
uses: actions/cache@v4
with:
key: ${{ env.CACHE_KEY }}
path: /tmp/${{ env.IMAGE }}.tar
fail-on-cache-miss: true
- name: Load and Start Appwrite
timeout-minutes: 3
run: |
docker load --input /tmp/${{ env.IMAGE }}.tar
docker compose up -d
until docker compose exec -T appwrite doctor > /dev/null 2>&1; do
echo "Waiting for Appwrite to be ready..."
sleep 2
done
- name: Wait for Open Runtimes
timeout-minutes: 3
run: |
while ! docker compose logs openruntimes-executor | grep -q "Executor is ready."; do
echo "Waiting for Executor to come online"
sleep 1
done
- name: Run General Tests
uses: itznotabug/php-retry@v3
with:
max_attempts: 2
retry_wait_seconds: 300
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 --debug
- name: Failure Logs
if: failure()
run: |
echo "=== Appwrite Logs ==="
docker compose logs
e2e_service_test:
name: E2E Service Test
runs-on: ubuntu-latest
needs: setup
permissions:
contents: read
pull-requests: write
strategy:
fail-fast: false
matrix:
db_adapter: [
MARIADB,
POSTGRESQL,
MONGODB
]
service: [
Account,
Avatars,
Console,
Databases,
Functions,
FunctionsSchedule,
GraphQL,
Health,
Locale,
Projects,
Realtime,
Sites,
Proxy,
Storage,
Tokens,
Teams,
Users,
Webhooks,
VCS,
Messaging,
Migrations
]
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Load Cache
uses: actions/cache@v4
with:
key: ${{ env.CACHE_KEY }}
path: /tmp/${{ env.IMAGE }}.tar
fail-on-cache-miss: true
- name: Set DB Adapter environment
id: set-db-env
run: |
DB_ADAPTER_LOWER=$(echo "${{ matrix.db_adapter }}" | tr 'A-Z' 'a-z')
echo "COMPOSE_PROFILES=${DB_ADAPTER_LOWER}" >> $GITHUB_ENV
if [ "${{ matrix.db_adapter }}" = "MARIADB" ]; then
echo "_APP_DB_ADAPTER=mariadb" >> $GITHUB_ENV
echo "_APP_DB_HOST=mariadb" >> $GITHUB_ENV
echo "_APP_DB_PORT=3306" >> $GITHUB_ENV
elif [ "${{ matrix.db_adapter }}" = "MONGODB" ]; then
echo "_APP_DB_ADAPTER=mongodb" >> $GITHUB_ENV
echo "_APP_DB_HOST=mongodb" >> $GITHUB_ENV
echo "_APP_DB_PORT=27017" >> $GITHUB_ENV
elif [ "${{ matrix.db_adapter }}" = "POSTGRESQL" ]; then
echo "_APP_DB_ADAPTER=postgresql" >> $GITHUB_ENV
echo "_APP_DB_HOST=postgresql" >> $GITHUB_ENV
echo "_APP_DB_PORT=5432" >> $GITHUB_ENV
fi
- name: Load and Start Appwrite
timeout-minutes: 3
env:
_APP_BROWSER_HOST: http://invalid-browser/v1
run: |
docker load --input /tmp/${{ env.IMAGE }}.tar
docker compose up -d
until docker compose exec -T appwrite doctor > /dev/null 2>&1; do
echo "Waiting for Appwrite to be ready..."
sleep 2
done
- name: Wait for Open Runtimes
timeout-minutes: 3
run: |
while ! docker compose logs openruntimes-executor | grep -q "Executor is ready."; do
echo "Waiting for Executor to come online"
sleep 1
done
- name: Run ${{ matrix.service }} tests with Project table mode
uses: itznotabug/php-retry@v3
with:
max_attempts: 2
retry_wait_seconds: 300
timeout_minutes: 20
job_id: ${{ job.check_run_id }}
github_token: ${{ secrets.GITHUB_TOKEN }}
test_dir: tests/e2e/Services/${{ matrix.service }}
command: |
echo "Using project tables"
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|Functions|Realtime) FUNCTIONAL_FLAG="" ;;
esac
echo "Running with paratest (parallel) for: ${{ matrix.service }} ${FUNCTIONAL_FLAG:+(functional)}"
docker compose exec -T \
-e _APP_DATABASE_SHARED_TABLES="" \
-e _APP_DATABASE_SHARED_TABLES_V1="" \
-e _APP_DB_ADAPTER="${{ env._APP_DB_ADAPTER }}" \
-e _APP_DB_HOST="${{ env._APP_DB_HOST }}" \
-e _APP_DB_PORT="${{ env._APP_DB_PORT }}" \
-e _APP_DB_SCHEMA=appwrite \
-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 --exclude-group ciIgnore --log-junit tests/e2e/Services/${{ matrix.service }}/junit.xml
- name: Failure Logs
if: failure()
run: |
echo "=== Appwrite Logs ==="
docker compose logs
e2e_shared_mode_test:
name: E2E Shared Mode Service Test
runs-on: ubuntu-latest
needs: [ setup, check_database_changes ]
if: needs.check_database_changes.outputs.database_changed == 'true'
permissions:
contents: read
pull-requests: write
strategy:
fail-fast: false
matrix:
service:
[
Account,
Avatars,
Console,
Databases,
Functions,
FunctionsSchedule,
GraphQL,
Health,
Locale,
Projects,
Realtime,
Sites,
Proxy,
Storage,
Teams,
Users,
Webhooks,
VCS,
Messaging,
Migrations,
Tokens
]
tables-mode: [
'Shared V1',
'Shared V2',
]
steps:
- name: checkout
uses: actions/checkout@v6
- name: Load Cache
uses: actions/cache@v4
with:
key: ${{ env.CACHE_KEY }}
path: /tmp/${{ env.IMAGE }}.tar
fail-on-cache-miss: true
- name: Load and Start Appwrite
timeout-minutes: 3
run: |
docker load --input /tmp/${{ env.IMAGE }}.tar
docker compose up -d
until docker compose exec -T appwrite doctor > /dev/null 2>&1; do
echo "Waiting for Appwrite to be ready..."
sleep 2
done
- name: Wait for Open Runtimes
timeout-minutes: 3
run: |
while ! docker compose logs openruntimes-executor | grep -q "Executor is ready."; do
echo "Waiting for Executor to come online"
sleep 1
done
- name: Run ${{ matrix.service }} tests with ${{ matrix.tables-mode }} table mode
uses: itznotabug/php-retry@v3
with:
max_attempts: 2
retry_wait_seconds: 300
timeout_minutes: 20
job_id: ${{ job.check_run_id }}
github_token: ${{ secrets.GITHUB_TOKEN }}
test_dir: tests/e2e/Services/${{ matrix.service }}
command: |
if [ "${{ matrix.tables-mode }}" == "Shared V1" ]; then
echo "Using shared tables V1"
export _APP_DATABASE_SHARED_TABLES=database_db_main
export _APP_DATABASE_SHARED_TABLES_V1=database_db_main
elif [ "${{ matrix.tables-mode }}" == "Shared V2" ]; then
echo "Using shared tables V2"
export _APP_DATABASE_SHARED_TABLES=database_db_main
export _APP_DATABASE_SHARED_TABLES_V1=
fi
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|Functions|Realtime) FUNCTIONAL_FLAG="" ;;
esac
docker compose exec -T \
-e _APP_DATABASE_SHARED_TABLES \
-e _APP_DATABASE_SHARED_TABLES_V1 \
-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 --exclude-group ciIgnore --log-junit tests/e2e/Services/${{ matrix.service }}/junit.xml
- name: Failure Logs
if: failure()
run: |
echo "=== Appwrite Worker Builds Logs ==="
docker compose logs appwrite-worker-builds
echo "=== OpenRuntimes Executor Logs ==="
docker compose logs openruntimes-executor
e2e_abuse_enabled:
name: E2E Service Test (Abuse enabled)
runs-on: ubuntu-latest
needs: setup
permissions:
contents: read
pull-requests: write
steps:
- name: checkout
uses: actions/checkout@v6
- name: Load Cache
uses: actions/cache@v4
with:
key: ${{ env.CACHE_KEY }}
path: /tmp/${{ env.IMAGE }}.tar
fail-on-cache-miss: true
- name: Load and Start Appwrite
timeout-minutes: 3
run: |
docker load --input /tmp/${{ env.IMAGE }}.tar
sed -i 's/_APP_OPTIONS_ABUSE=disabled/_APP_OPTIONS_ABUSE=enabled/' .env
docker compose up -d
until docker compose exec -T appwrite doctor > /dev/null 2>&1; do
echo "Waiting for Appwrite to be ready..."
sleep 2
done
- name: Run Projects tests in dedicated table mode
uses: itznotabug/php-retry@v3
with:
max_attempts: 2
retry_wait_seconds: 300
timeout_minutes: 15
job_id: ${{ job.check_run_id }}
github_token: ${{ secrets.GITHUB_TOKEN }}
test_dir: tests/e2e/Services/Projects
command: |
echo "Using project tables"
export _APP_DATABASE_SHARED_TABLES=
export _APP_DATABASE_SHARED_TABLES_V1=
docker compose exec -T \
-e _APP_DATABASE_SHARED_TABLES \
-e _APP_DATABASE_SHARED_TABLES_V1 \
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \
appwrite vendor/bin/paratest --processes $(nproc) /usr/src/code/tests/e2e/Services/Projects --group abuseEnabled
- name: Failure Logs
if: failure()
run: |
echo "=== Appwrite Worker Builds Logs ==="
docker compose logs appwrite-worker-builds
echo "=== OpenRuntimes Executor Logs ==="
docker compose logs openruntimes-executor
e2e_abuse_enabled_shared_mode:
name: E2E Shared Mode Service Test (Abuse enabled)
runs-on: ubuntu-latest
needs: [ setup, check_database_changes ]
if: needs.check_database_changes.outputs.database_changed == 'true'
permissions:
contents: read
pull-requests: write
strategy:
fail-fast: false
matrix:
tables-mode: [
'Shared V1',
'Shared V2',
]
steps:
- name: checkout
uses: actions/checkout@v6
- name: Load Cache
uses: actions/cache@v4
with:
key: ${{ env.CACHE_KEY }}
path: /tmp/${{ env.IMAGE }}.tar
fail-on-cache-miss: true
- name: Load and Start Appwrite
timeout-minutes: 3
run: |
docker load --input /tmp/${{ env.IMAGE }}.tar
sed -i 's/_APP_OPTIONS_ABUSE=disabled/_APP_OPTIONS_ABUSE=enabled/' .env
docker compose up -d
until docker compose exec -T appwrite doctor > /dev/null 2>&1; do
echo "Waiting for Appwrite to be ready..."
sleep 2
done
- name: Run Projects tests in ${{ matrix.tables-mode }} table mode
uses: itznotabug/php-retry@v3
with:
max_attempts: 2
retry_wait_seconds: 300
timeout_minutes: 15
job_id: ${{ job.check_run_id }}
github_token: ${{ secrets.GITHUB_TOKEN }}
test_dir: tests/e2e/Services/Projects
command: |
if [ "${{ matrix.tables-mode }}" == "Shared V1" ]; then
echo "Using shared tables V1"
export _APP_DATABASE_SHARED_TABLES=database_db_main
export _APP_DATABASE_SHARED_TABLES_V1=database_db_main
elif [ "${{ matrix.tables-mode }}" == "Shared V2" ]; then
echo "Using shared tables V2"
export _APP_DATABASE_SHARED_TABLES=database_db_main
export _APP_DATABASE_SHARED_TABLES_V1=
fi
docker compose exec -T \
-e _APP_DATABASE_SHARED_TABLES \
-e _APP_DATABASE_SHARED_TABLES_V1 \
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \
appwrite vendor/bin/paratest --processes $(nproc) /usr/src/code/tests/e2e/Services/Projects --group abuseEnabled
- name: Failure Logs
if: failure()
run: |
echo "=== Appwrite Worker Builds Logs ==="
docker compose logs appwrite-worker-builds
echo "=== OpenRuntimes Executor Logs ==="
docker compose logs openruntimes-executor
e2e_screenshots:
name: E2E Service Test (Site Screenshots)
runs-on: ubuntu-latest
needs: setup
permissions:
contents: read
pull-requests: write
steps:
- name: checkout
uses: actions/checkout@v6
- name: Load Cache
uses: actions/cache@v4
with:
key: ${{ env.CACHE_KEY }}
path: /tmp/${{ env.IMAGE }}.tar
fail-on-cache-miss: true
- name: Load and Start Appwrite
timeout-minutes: 3
run: |
docker load --input /tmp/${{ env.IMAGE }}.tar
sed -i 's/_APP_OPTIONS_ABUSE=disabled/_APP_OPTIONS_ABUSE=enabled/' .env
docker compose up -d
until docker compose exec -T appwrite doctor > /dev/null 2>&1; do
echo "Waiting for Appwrite to be ready..."
sleep 2
done
- name: Wait for Open Runtimes
timeout-minutes: 3
run: |
while ! docker compose logs openruntimes-executor | grep -q "Executor is ready."; do
echo "Waiting for Executor to come online"
sleep 1
done
- name: Run Site tests with browser connected in dedicated table mode
uses: itznotabug/php-retry@v3
with:
max_attempts: 2
retry_wait_seconds: 300
timeout_minutes: 15
job_id: ${{ job.check_run_id }}
github_token: ${{ secrets.GITHUB_TOKEN }}
test_dir: tests/e2e/Services/Sites
command: |
echo "Keeping original value of _APP_BROWSER_HOST"
echo "Using project tables"
export _APP_DATABASE_SHARED_TABLES=
export _APP_DATABASE_SHARED_TABLES_V1=
docker compose exec -T \
-e _APP_DATABASE_SHARED_TABLES \
-e _APP_DATABASE_SHARED_TABLES_V1 \
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \
appwrite vendor/bin/paratest --processes $(nproc) /usr/src/code/tests/e2e/Services/Sites --group screenshots
- name: Failure Logs
if: failure()
run: |
echo "=== Appwrite Worker Builds Logs ==="
docker compose logs appwrite-worker-builds
echo "=== OpenRuntimes Executor Logs ==="
docker compose logs openruntimes-executor
e2e_screenshots_shared_mode:
name: E2E Shared Mode Service Test (Site Screenshots)
runs-on: ubuntu-latest
needs: [ setup, check_database_changes ]
if: needs.check_database_changes.outputs.database_changed == 'true'
permissions:
contents: read
pull-requests: write
strategy:
fail-fast: false
matrix:
tables-mode: [
'Shared V1',
'Shared V2',
]
steps:
- name: checkout
uses: actions/checkout@v6
- name: Load Cache
uses: actions/cache@v4
with:
key: ${{ env.CACHE_KEY }}
path: /tmp/${{ env.IMAGE }}.tar
fail-on-cache-miss: true
- name: Load and Start Appwrite
timeout-minutes: 3
run: |
docker load --input /tmp/${{ env.IMAGE }}.tar
sed -i 's/_APP_OPTIONS_ABUSE=disabled/_APP_OPTIONS_ABUSE=enabled/' .env
docker compose up -d
until docker compose exec -T appwrite doctor > /dev/null 2>&1; do
echo "Waiting for Appwrite to be ready..."
sleep 2
done
- name: Wait for Open Runtimes
timeout-minutes: 3
run: |
while ! docker compose logs openruntimes-executor | grep -q "Executor is ready."; do
echo "Waiting for Executor to come online"
sleep 1
done
- name: Run Site tests with browser connected in ${{ matrix.tables-mode }} table mode
uses: itznotabug/php-retry@v3
with:
max_attempts: 2
retry_wait_seconds: 300
timeout_minutes: 15
job_id: ${{ job.check_run_id }}
github_token: ${{ secrets.GITHUB_TOKEN }}
test_dir: tests/e2e/Services/Sites
command: |
echo "Keeping original value of _APP_BROWSER_HOST"
if [ "${{ matrix.tables-mode }}" == "Shared V1" ]; then
echo "Using shared tables V1"
export _APP_DATABASE_SHARED_TABLES=database_db_main
export _APP_DATABASE_SHARED_TABLES_V1=database_db_main
elif [ "${{ matrix.tables-mode }}" == "Shared V2" ]; then
echo "Using shared tables V2"
export _APP_DATABASE_SHARED_TABLES=database_db_main
export _APP_DATABASE_SHARED_TABLES_V1=
fi
docker compose exec -T \
-e _APP_DATABASE_SHARED_TABLES \
-e _APP_DATABASE_SHARED_TABLES_V1 \
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \
appwrite vendor/bin/paratest --processes $(nproc) /usr/src/code/tests/e2e/Services/Sites --group screenshots
- name: Failure Logs
if: failure()
run: |
echo "=== Appwrite Worker Builds Logs ==="
docker compose logs appwrite-worker-builds
echo "=== OpenRuntimes Executor Logs ==="
docker compose logs openruntimes-executor
+5
View File
@@ -26,4 +26,9 @@ test-results
docker-compose.web-installer.yml
.env.web-installer
docker-compose.web-installer.yml.**.backup
# local installer output dir
appwrite/
# screenshots
tests/playwright/screenshots
-30
View File
@@ -47,36 +47,6 @@ Examples:
'resourceType' => 'deployments'
```
## Performance Patterns
### Document Update Optimization
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)`.
**Correct Pattern:**
```php
// Good: Pass only changed attributes directly
$user = $dbForProject->updateDocument('users', $user->getId(), new Document([
'name' => $name,
'email' => $email,
]));
```
**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
-1
View File
@@ -1 +0,0 @@
@AGENTS.md
+23 -36
View File
@@ -111,7 +111,7 @@ $ git push origin [name_of_your_new_branch]
## Setup From Source
To set up a working **development environment**, just fork the project git repository and install the backend and frontend dependencies using the proper package manager and run the docker-compose stack.
To set up a working **development environment**, just fork the project git repository and install the backend and frontend dependencies using the proper package manager and create run the docker-compose stack.
> If you just want to install Appwrite for day-to-day use and not as a contributor, you can reference the [installation guide](https://github.com/appwrite/appwrite#installation), the [getting started guide](https://appwrite.io/docs/quick-starts), or the main [README](README.md) file.
@@ -173,12 +173,12 @@ Learn more at our [Technology Stack](#technology-stack) section.
- [MVVM](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel) - Appwrite console architecture
##### Container Namespace Conventions
To keep our services easy to understand within Docker we follow a naming convention for all our containers depending on its intended use.
To keep our services easy to understand within Docker we follow a naming convention for all our containers depending on it's intended use.
`appwrite-worker-X` - Workers (`src/Appwrite/Platform/Workers/*`)
`appwrite-task-X` - Tasks (`src/Appwrite/Platform/Tasks/*`)
Other containers should be named the same as their service, for example `redis` should just be called `redis`.
Other containes should be named the same as their service, for example `redis` should just be called `redis`.
##### Security
@@ -189,7 +189,7 @@ Other containers should be named the same as their service, for example `redis`
## Modules
As Appwrite grows, we noticed approach of having all service endpoints in `app/controllers/api/[service].php` is not maintainable. Not only it creates massive files, it also doesn't contain all product's features such as workers or tasks. While there might still be some occurrences of those controller files, we avoid it in all new development, and gradually migrate existing controllers to **HTTP modules**.
As Appwrite grows, we noticed approach of having all service endpoints in `app/controllers/api/[service].php` is not maintainable. Not only it creates massive files, it also doesnt contain all product's features such as workers or tasks. While there might still be some occurances of those controller files, we avoid it in all new development, and gradually migrate existing controllers to **HTTP modules**.
### HTTP Endpoints
@@ -204,7 +204,7 @@ Tips and tricks:
1. If endpoint doesn't have resource, use service name as resource name too
> Example: `Modules/Sites/Http/Sites/Get.php`
2. If there are multiple resources, use them all in folder structure
2. If there are multiple resources, use then all in folder structure
> Example: `Modules/Sites/Http/Deployments/Builds/Create.php`
3. Action can only be `Get`, `Create`, `Update`, `Delete` or `XList`
@@ -395,7 +395,7 @@ These are the current metrics we collect usage stats for:
> Note: The curly brackets in the metric name represents a template and is replaced with a value when the metric is processed.
Metrics are collected within 3 scopes Daily, monthly, and infinity. Adding new usage metric in order to aggregate usage stats is very simple, but very much dependent on where do you want to collect
Metrics are collected within 3 scopes Daily, monthly, an infinity. Adding new usage metric in order to aggregate usage stats is very simple, but very much dependent on where do you want to collect
statistics ,via API or via background worker. For both cases you will need to add a `const` variable in `app/init.php` under the usage metrics list using the naming convention `METRIC_<RESOURCE_NAME>` as shown below.
```php
@@ -409,16 +409,14 @@ Next follow the appropriate steps below depending on whether you're adding the m
**API**
In file `app/controllers/shared/api.php` On the database listener, add to an existing or create a new switch case. Accumulate metrics in the usage context like so:
In file `app/controllers/shared/api.php` On the database listener, add to an existing or create a new switch case. Add a call to the usage worker with your new metric const like so:
```php
case $document->getCollection() === 'teams':
$usage->addMetric(METRIC_TEAMS, $value); // per project
$queueForStatsUsage
->addMetric(METRIC_TEAMS, $value); // per project
break;
```
The metrics will be automatically published by the shutdown hook at the end of the request. There is no need to manually trigger or publish.
There are cases when you need to handle metric that has a parent entity, like buckets.
Files are linked to a parent bucket, you should verify you remove the files stats when you delete a bucket.
@@ -427,13 +425,14 @@ In that case you need also to handle children removal using addReduce() method c
```php
case $document->getCollection() === 'buckets': //buckets
$usage->addMetric(METRIC_BUCKETS, $value); // per project
$queueForStatsUsage
->addMetric(METRIC_BUCKETS, $value); // per project
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$usage
$queueForStatsUsage
->addReduce($document);
}
break;
```
In addition, you will also need to add some logic to the `reduce()` method of the Usage worker located in `/src/Appwrite/Platform/Workers/Usage.php`, like so:
@@ -461,12 +460,8 @@ case $document->getCollection() === 'buckets':
**Background worker**
You need to inject the usage context and publisher in the desired worker on the constructor method
You need to inject the usage queue in the desired worker on the constructor method
```php
use Appwrite\Usage\Context;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Event\Message\Usage as UsageMessage;
/**
* @throws Exception
*/
@@ -479,32 +474,24 @@ public function __construct()
->inject('dbForProject')
->inject('queueForFunctions')
->inject('queueForEvents')
->inject('usage')
->inject('publisherForUsage')
->inject('queueForStatsUsage')
->inject('log')
->callback(fn (Message $message, Database $dbForProject, Func $queueForFunctions, Event $queueForEvents, Context $usage, UsagePublisher $publisherForUsage, Log $log) => $this->action($message, $dbForProject, $queueForFunctions, $queueForEvents, $usage, $publisherForUsage, $log));
->callback(fn (Message $message, Database $dbForProject, Func $queueForFunctions, Event $queueForEvents, StatsUsage $queueForStatsUsage, Log $log) => $this->action($message, $dbForProject, $queueForFunctions, $queueForEvents, $queueForStatsUsage, $log));
}
```
and then accumulate metrics, create a message, and publish like so:
and then trigger the queue with the new metric like so:
```php
$usage
$queueForStatsUsage
->addMetric(METRIC_BUILDS, 1)
->addMetric(METRIC_BUILDS_STORAGE, $build->getAttribute('size', 0))
->addMetric(METRIC_BUILDS_COMPUTE, (int)$build->getAttribute('duration', 0) * 1000)
->addMetric(str_replace('{functionInternalId}', $function->getSequence(), METRIC_FUNCTION_ID_BUILDS), 1)
->addMetric(str_replace('{functionInternalId}', $function->getSequence(), METRIC_FUNCTION_ID_BUILDS), 1)
->addMetric(str_replace('{functionInternalId}', $function->getSequence(), METRIC_FUNCTION_ID_BUILDS_STORAGE), $build->getAttribute('size', 0))
->addMetric(str_replace('{functionInternalId}', $function->getSequence(), METRIC_FUNCTION_ID_BUILDS_COMPUTE), (int)$build->getAttribute('duration', 0) * 1000);
// Publish the accumulated metrics (workers don't have shutdown hooks)
$message = new UsageMessage(
project: $project,
metrics: $usage->getMetrics(),
reduce: $usage->getReduce()
);
$publisherForUsage->enqueue($message);
$usage->reset();
->addMetric(str_replace('{functionInternalId}', $function->getSequence(), METRIC_FUNCTION_ID_BUILDS_COMPUTE), (int)$build->getAttribute('duration', 0) * 1000)
->setProject($project)
->trigger();
```
@@ -674,7 +661,7 @@ docker compose exec redis redis-cli FLUSHALL
## Using preview domains locally
Appwrite Functions are automatically given a domain you can visit to execute the function. This domain has format `[SOMETHING].functions.localhost` unless you changed `_APP_DOMAIN_FUNCTIONS` environment variable. This default value works great when running Appwrite locally, but it can be impossible to use preview domains with Cloud workspaces such as Gitpod or GitHub Codespaces.
Appwrite Functions are automatically given a domain you can visit to execute the function. This domain has format `[SOMETHING].functions.localhost` unless you changed `_APP_DOMAIN_FUNCTIONS` environment variable. This default value works great when running Appwrite locally, but it can be impossible to use preview domains with Cloud woekspaces such as Gitpod or GitHub Codespaces.
To use preview domains on Cloud workspaces, you can visit hostname provided by them, and supply function's preview domain as URL parameter:
-1
View File
@@ -70,7 +70,6 @@ RUN chmod +x /usr/local/bin/doctor && \
chmod +x /usr/local/bin/sdks && \
chmod +x /usr/local/bin/specs && \
chmod +x /usr/local/bin/ssl && \
chmod +x /usr/local/bin/task-time-travel && \
chmod +x /usr/local/bin/screenshot && \
chmod +x /usr/local/bin/test && \
chmod +x /usr/local/bin/upgrade && \
+16 -25
View File
@@ -4,13 +4,11 @@ require_once __DIR__ . '/init.php';
use Appwrite\Event\Certificate;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Event\StatsResources;
use Appwrite\Event\StatsUsage;
use Appwrite\Platform\Appwrite;
use Appwrite\Runtimes\Runtimes;
use Appwrite\Usage\Context as UsageContext;
use Appwrite\Utopia\Database\Documents\User;
use Executor\Executor;
use Swoole\Runtime;
@@ -31,7 +29,6 @@ use Utopia\Platform\Service;
use Utopia\Pools\Group;
use Utopia\Queue\Broker\Pool as BrokerPool;
use Utopia\Queue\Publisher;
use Utopia\Queue\Queue;
use Utopia\Registry\Registry;
use Utopia\System\System;
use Utopia\Telemetry\Adapter\None as NoTelemetry;
@@ -50,7 +47,7 @@ $platform = new Appwrite();
$args = $platform->getEnv('argv');
\array_shift($args);
if (! isset($args[0])) {
if (!isset($args[0])) {
Console::error('Missing task name');
Console::exit(1);
}
@@ -88,7 +85,6 @@ $setResource('pools', function (Registry $register) {
$setResource('authorization', function () {
$authorization = new Authorization();
$authorization->disable();
return $authorization;
}, []);
@@ -117,7 +113,7 @@ $setResource('dbForPlatform', function ($pools, $cache, $authorization) {
$collections = Config::getParam('collections', [])['console'];
$last = \array_key_last($collections);
if (! ($dbForPlatform->exists($dbForPlatform->getDatabase(), $last))) { /** TODO cache ready variable using registry */
if (!($dbForPlatform->exists($dbForPlatform->getDatabase(), $last))) { /** TODO cache ready variable using registry */
throw new Exception('Tables not ready yet.');
}
@@ -126,10 +122,10 @@ $setResource('dbForPlatform', function ($pools, $cache, $authorization) {
Console::warning($err->getMessage());
sleep($sleep);
}
} while ($attempts < $maxAttempts && ! $ready);
} while ($attempts < $maxAttempts && !$ready);
if (! $ready) {
throw new Exception('Console is not ready yet. Please try again later.');
if (!$ready) {
throw new Exception("Console is not ready yet. Please try again later.");
}
return $dbForPlatform;
@@ -167,7 +163,7 @@ $setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $c
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getSequence())
->setTenant((int)$project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
@@ -188,7 +184,7 @@ $setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $c
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getSequence())
->setTenant((int)$project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
@@ -212,7 +208,7 @@ $setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $a
return function (?Document $project = null) use ($pools, $cache, $database, $authorization) {
if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant($project->getSequence());
$database->setTenant((int)$project->getSequence());
return $database;
}
@@ -229,7 +225,7 @@ $setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $a
// set tenant
if ($project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant($project->getSequence());
$database->setTenant((int)$project->getSequence());
}
return $database;
@@ -247,16 +243,15 @@ $setResource('publisherFunctions', function (BrokerPool $publisher) {
$setResource('publisherMigrations', function (BrokerPool $publisher) {
return $publisher;
}, ['publisher']);
$setResource('publisherStatsUsage', function (BrokerPool $publisher) {
return $publisher;
}, ['publisher']);
$setResource('publisherMessaging', function (BrokerPool $publisher) {
return $publisher;
}, ['publisher']);
$setResource('usage', function () {
return new UsageContext();
}, []);
$setResource('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher(
$publisher,
new Queue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME))
), ['publisher']);
$setResource('queueForStatsUsage', function (Publisher $publisher) {
return new StatsUsage($publisher);
}, ['publisher']);
$setResource('queueForStatsResources', function (Publisher $publisher) {
return new StatsResources($publisher);
}, ['publisher']);
@@ -323,10 +318,6 @@ $setResource('logError', function (Registry $register) {
$setResource('executor', fn () => new Executor(), []);
$setResource('bus', function (Registry $register) use ($cli) {
return $register->get('bus')->setResolver(fn (string $name) => $cli->getResource($name));
}, ['register']);
$setResource('telemetry', fn () => new NoTelemetry(), []);
$cli
+7 -1
View File
@@ -777,7 +777,6 @@ return [
'filters' => [],
],
[
// At the moment, always empty (no runtime supports it yet)
'array' => false,
'$id' => ID::custom('startCommand'),
'type' => Database::VAR_STRING,
@@ -2191,6 +2190,13 @@ return [
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_function_internal_id'),
'type' => Database::INDEX_KEY,
'attributes' => ['resourceInternalId'],
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_resourceType'),
'type' => Database::INDEX_KEY,
+3 -8
View File
@@ -27,7 +27,7 @@ return [
Exception::GENERAL_RESOURCE_BLOCKED => [
'name' => Exception::GENERAL_RESOURCE_BLOCKED,
'description' => 'Access to this resource is blocked.',
'code' => 403,
'code' => 401,
],
Exception::GENERAL_UNKNOWN_ORIGIN => [
'name' => Exception::GENERAL_UNKNOWN_ORIGIN,
@@ -168,8 +168,8 @@ return [
],
Exception::USER_BLOCKED => [
'name' => Exception::USER_BLOCKED,
'description' => 'The current user has been blocked.',
'code' => 403,
'description' => 'The current user has been blocked. You can unblock the user by making a request to the User API\'s "Update User Status" endpoint or in the Appwrite Console\'s Auth section.',
'code' => 401,
],
Exception::USER_INVALID_TOKEN => [
'name' => Exception::USER_INVALID_TOKEN,
@@ -1144,11 +1144,6 @@ return [
'description' => 'Webhook with the requested ID could not be found.',
'code' => 404,
],
Exception::WEBHOOK_ALREADY_EXISTS => [
'name' => Exception::WEBHOOK_ALREADY_EXISTS,
'description' => 'Webhook with the same ID already exists. Try again with a different ID.',
'code' => 409,
],
Exception::KEY_NOT_FOUND => [
'name' => Exception::KEY_NOT_FOUND,
'description' => 'Key with the requested ID could not be found.',
-8
View File
@@ -172,12 +172,4 @@ return [ // List of publicly visible scopes
'tokens.write' => [
'description' => 'Access to create, update, and delete your project\'s tokens',
],
"webhooks.read" => [
"description" =>
"Access to read project\'s webhooks",
],
"webhooks.write" => [
"description" =>
"Access to create, update, and delete project\'s webhooks",
],
];
+1 -1
View File
@@ -231,7 +231,7 @@ return [
'url' => 'https://github.com/appwrite/sdk-for-cli',
'package' => 'https://www.npmjs.com/package/appwrite-cli',
'enabled' => true,
'beta' => false,
'beta' => true,
'dev' => false,
'hidden' => false,
'family' => APP_SDK_PLATFORM_CONSOLE,
+32 -46
View File
@@ -14,6 +14,7 @@ use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception;
use Appwrite\Hooks\Hooks;
use Appwrite\Network\Validator\Email as EmailValidator;
@@ -27,7 +28,6 @@ use Appwrite\SDK\MethodType;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Template\Template;
use Appwrite\URL\URL as URLParser;
use Appwrite\Usage\Context;
use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Database\Validator\Queries\Identities;
@@ -288,10 +288,7 @@ $createSession = function (string $userId, string $secret, Request $request, Res
}
try {
$dbForProject->updateDocument('users', $user->getId(), new Document([
'emailVerification' => $user->getAttribute('emailVerification'),
'phoneVerification' => $user->getAttribute('phoneVerification'),
]));
$dbForProject->updateDocument('users', $user->getId(), $user);
} catch (\Throwable $th) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed saving user to DB');
}
@@ -1035,11 +1032,7 @@ Http::post('/v1/account/sessions/email')
->setAttribute('password', $proofForPasswordUpdated->hash($password))
->setAttribute('hash', $proofForPasswordUpdated->getHash()->getName())
->setAttribute('hashOptions', $proofForPasswordUpdated->getHash()->getOptions());
$dbForProject->updateDocument('users', $user->getId(), new Document([
'password' => $user->getAttribute('password'),
'hash' => $user->getAttribute('hash'),
'hashOptions' => $user->getAttribute('hashOptions'),
]));
$dbForProject->updateDocument('users', $user->getId(), $user);
}
$dbForProject->purgeCachedDocument('users', $user->getId());
@@ -1829,11 +1822,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
->setAttribute('providerAccessToken', $accessToken)
->setAttribute('providerRefreshToken', $refreshToken)
->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int) $accessTokenExpiry));
$dbForProject->updateDocument('identities', $identity->getId(), new Document([
'providerAccessToken' => $identity->getAttribute('providerAccessToken'),
'providerRefreshToken' => $identity->getAttribute('providerRefreshToken'),
'providerAccessTokenExpiry' => $identity->getAttribute('providerAccessTokenExpiry'),
]));
$dbForProject->updateDocument('identities', $identity->getId(), $identity);
}
if (empty($user->getAttribute('email'))) {
@@ -1971,10 +1960,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
->setAttribute('sessionId', $session->getId())
->setAttribute('sessionInternalId', $session->getSequence());
$dbForProject->updateDocument('targets', $target->getId(), new Document([
'sessionId' => $target->getAttribute('sessionId'),
'sessionInternalId' => $target->getAttribute('sessionInternalId'),
]));
$dbForProject->updateDocument('targets', $target->getId(), $target);
}
}
@@ -2801,12 +2787,12 @@ Http::post('/v1/account/tokens/phone')
->inject('queueForMessaging')
->inject('locale')
->inject('timelimit')
->inject('usage')
->inject('queueForStatsUsage')
->inject('plan')
->inject('store')
->inject('proofForCode')
->inject('authorization')
->action(function (string $userId, string $phone, Request $request, Response $response, User $user, Document $project, array $platform, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Locale $locale, callable $timelimit, Context $usage, array $plan, Store $store, ProofsCode $proofForCode, Authorization $authorization) {
->action(function (string $userId, string $phone, Request $request, Response $response, User $user, Document $project, array $platform, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Locale $locale, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan, Store $store, ProofsCode $proofForCode, Authorization $authorization) {
if (empty(System::getEnv('_APP_SMS_PROVIDER'))) {
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
}
@@ -2955,12 +2941,16 @@ Http::post('/v1/account/tokens/phone')
$countryCode = $helper->parse($phone)->getCountryCode();
if (!empty($countryCode)) {
$usage->addMetric(str_replace('{countryCode}', $countryCode, METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE), 1);
$queueForStatsUsage
->addMetric(str_replace('{countryCode}', $countryCode, METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE), 1);
}
} catch (NumberParseException $e) {
// Ignore invalid phone number for country code stats
}
$usage->addMetric(METRIC_AUTH_METHOD_PHONE, 1);
$queueForStatsUsage
->addMetric(METRIC_AUTH_METHOD_PHONE, 1)
->setProject($project)
->trigger();
}
$token->setAttribute('secret', $secret);
@@ -3155,9 +3145,7 @@ Http::patch('/v1/account/name')
$user->setAttribute('name', $name);
$user = $dbForProject->updateDocument('users', $user->getId(), new Document([
'name' => $user->getAttribute('name'),
]));
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
$queueForEvents->setParam('userId', $user->getId());
@@ -3810,15 +3798,13 @@ Http::put('/v1/account/recovery')
$hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, true]);
$profile = $dbForProject->updateDocument('users', $profile->getId(), new Document(
[
'password' => $newPassword,
'passwordHistory' => $history,
'passwordUpdate' => DateTime::now(),
'hash' => $proofForPassword->getHash()->getName(),
'hashOptions' => $proofForPassword->getHash()->getOptions(),
'emailVerification' => true]
));
$profile = $dbForProject->updateDocument('users', $profile->getId(), $profile
->setAttribute('password', $newPassword)
->setAttribute('passwordHistory', $history)
->setAttribute('passwordUpdate', DateTime::now())
->setAttribute('hash', $proofForPassword->getHash()->getName())
->setAttribute('hashOptions', $proofForPassword->getHash()->getOptions())
->setAttribute('emailVerification', true));
$user->setAttributes($profile->getArrayCopy());
@@ -4140,7 +4126,7 @@ Http::put('/v1/account/verifications/email')
$authorization->addRole(Role::user($profile->getId())->toString());
$profile = $dbForProject->updateDocument('users', $profile->getId(), new Document(['emailVerification' => true]));
$profile = $dbForProject->updateDocument('users', $profile->getId(), $profile->setAttribute('emailVerification', true));
$user->setAttributes($profile->getArrayCopy());
@@ -4195,11 +4181,11 @@ Http::post('/v1/account/verifications/phone')
->inject('project')
->inject('locale')
->inject('timelimit')
->inject('usage')
->inject('queueForStatsUsage')
->inject('plan')
->inject('proofForCode')
->inject('authorization')
->action(function (Request $request, Response $response, User $user, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Document $project, Locale $locale, callable $timelimit, Context $usage, array $plan, ProofsCode $proofForCode, Authorization $authorization) {
->action(function (Request $request, Response $response, User $user, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Document $project, Locale $locale, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan, ProofsCode $proofForCode, Authorization $authorization) {
if (empty(System::getEnv('_APP_SMS_PROVIDER'))) {
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
}
@@ -4284,12 +4270,16 @@ Http::post('/v1/account/verifications/phone')
$countryCode = $helper->parse($phone)->getCountryCode();
if (!empty($countryCode)) {
$usage->addMetric(str_replace('{countryCode}', $countryCode, METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE), 1);
$queueForStatsUsage
->addMetric(str_replace('{countryCode}', $countryCode, METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE), 1);
}
} catch (NumberParseException $e) {
// Ignore invalid phone number for country code stats
}
$usage->addMetric(METRIC_AUTH_METHOD_PHONE, 1);
$queueForStatsUsage
->addMetric(METRIC_AUTH_METHOD_PHONE, 1)
->setProject($project)
->trigger();
}
$verification->setAttribute('secret', $secret);
@@ -4352,7 +4342,7 @@ Http::put('/v1/account/verifications/phone')
$authorization->addRole(Role::user($profile->getId())->toString());
$profile = $dbForProject->updateDocument('users', $profile->getId(), new Document(['phoneVerification' => true]));
$profile = $dbForProject->updateDocument('users', $profile->getId(), $profile->setAttribute('phoneVerification', true));
$user->setAttributes($profile->getArrayCopy());
@@ -4510,11 +4500,7 @@ Http::put('/v1/account/targets/:targetId/push')
$target->setAttribute('name', "{$device['deviceBrand']} {$device['deviceModel']}");
$target = $dbForProject->updateDocument('targets', $target->getId(), new Document([
'identifier' => $target->getAttribute('identifier'),
'expired' => $target->getAttribute('expired'),
'name' => $target->getAttribute('name'),
]));
$target = $dbForProject->updateDocument('targets', $target->getId(), $target);
$dbForProject->purgeCachedDocument('users', $user->getId());
+49 -20
View File
@@ -717,14 +717,22 @@ Http::get('/v1/migrations/appwrite/report')
->inject('project')
->inject('user')
->action(function (array $resources, string $endpoint, string $projectID, string $key, Response $response) {
$appwrite = new Appwrite($projectID, $endpoint, $key);
try {
$appwrite = new Appwrite($projectID, $endpoint, $key);
$report = $appwrite->report($resources);
} catch (\Throwable $e) {
throw new Exception(
Exception::MIGRATION_PROVIDER_ERROR,
'Unable to connect to the migration source. Please verify your credentials and ensure the source is reachable from this server. Check for network restrictions such as firewalls, IP allowlists, or outbound connectivity limits.'
);
switch ($e->getCode()) {
case 401:
throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, 'Source Error: ' . $e->getMessage());
case 429:
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Source Error: Rate Limit Exceeded, Is your Cloud Provider blocking Appwrite\'s IP?');
case 500:
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage());
}
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage());
}
$response
@@ -763,14 +771,21 @@ Http::get('/v1/migrations/firebase/report')
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Invalid Service Account JSON');
}
$firebase = new Firebase($serviceAccount);
try {
$firebase = new Firebase($serviceAccount);
$report = $firebase->report($resources);
} catch (\Throwable $e) {
throw new Exception(
Exception::MIGRATION_PROVIDER_ERROR,
'Unable to connect to the migration source. Please verify your credentials and ensure the source is reachable from this server. Check for network restrictions such as firewalls, IP allowlists, or outbound connectivity limits.'
);
switch ($e->getCode()) {
case 401:
throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, 'Source Error: ' . $e->getMessage());
case 429:
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Source Error: Rate Limit Exceeded, Is your Cloud Provider blocking Appwrite\'s IP?');
case 500:
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage());
}
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage());
}
$response
@@ -805,14 +820,21 @@ Http::get('/v1/migrations/supabase/report')
->inject('response')
->inject('dbForProject')
->action(function (array $resources, string $endpoint, string $apiKey, string $databaseHost, string $username, string $password, int $port, Response $response) {
$supabase = new Supabase($endpoint, $apiKey, $databaseHost, 'postgres', $username, $password, $port);
try {
$supabase = new Supabase($endpoint, $apiKey, $databaseHost, 'postgres', $username, $password, $port);
$report = $supabase->report($resources);
} catch (\Throwable $e) {
throw new Exception(
Exception::MIGRATION_PROVIDER_ERROR,
'Unable to connect to the migration source. Please verify your credentials and ensure the source is reachable from this server. Check for network restrictions such as firewalls, IP allowlists, or outbound connectivity limits.'
);
switch ($e->getCode()) {
case 401:
throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, 'Source Error: ' . $e->getMessage());
case 429:
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Source Error: Rate Limit Exceeded, Is your Cloud Provider blocking Appwrite\'s IP?');
case 500:
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage());
}
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage());
}
$response
@@ -847,14 +869,21 @@ Http::get('/v1/migrations/nhost/report')
->param('port', 5432, new Integer(true), 'Source\'s Database Port.', true)
->inject('response')
->action(function (array $resources, string $subdomain, string $region, string $adminSecret, string $database, string $username, string $password, int $port, Response $response) {
$nhost = new NHost($subdomain, $region, $adminSecret, $database, $username, $password, $port);
try {
$nhost = new NHost($subdomain, $region, $adminSecret, $database, $username, $password, $port);
$report = $nhost->report($resources);
} catch (\Throwable $e) {
throw new Exception(
Exception::MIGRATION_PROVIDER_ERROR,
'Unable to connect to the migration source. Please verify your credentials and ensure the source is reachable from this server. Check for network restrictions such as firewalls, IP allowlists, or outbound connectivity limits.'
);
switch ($e->getCode()) {
case 401:
throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, 'Source Error: ' . $e->getMessage());
case 429:
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Source Error: Rate Limit Exceeded, Is your Cloud Provider blocking Appwrite\'s IP?');
case 500:
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage());
}
throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage());
}
$response
+20 -51
View File
@@ -1161,7 +1161,7 @@ Http::patch('/v1/users/:userId/status')
throw new Exception(Exception::USER_NOT_FOUND);
}
$user = $dbForProject->updateDocument('users', $user->getId(), new Document(['status' => (bool) $status]));
$user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('status', (bool) $status));
$queueForEvents
->setParam('userId', $user->getId());
@@ -1204,7 +1204,7 @@ Http::put('/v1/users/:userId/labels')
$user->setAttribute('labels', (array) \array_values(\array_unique($labels)));
$user = $dbForProject->updateDocument('users', $user->getId(), new Document(['labels' => $user->getAttribute('labels')]));
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
$queueForEvents
->setParam('userId', $user->getId());
@@ -1245,7 +1245,7 @@ Http::patch('/v1/users/:userId/verification/phone')
throw new Exception(Exception::USER_NOT_FOUND);
}
$user = $dbForProject->updateDocument('users', $user->getId(), new Document(['phoneVerification' => $phoneVerification]));
$user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('phoneVerification', $phoneVerification));
$queueForEvents
->setParam('userId', $user->getId());
@@ -1289,7 +1289,7 @@ Http::patch('/v1/users/:userId/name')
$user->setAttribute('name', $name);
$user = $dbForProject->updateDocument('users', $user->getId(), new Document(['name' => $user->getAttribute('name')]));
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
$queueForEvents->setParam('userId', $user->getId());
@@ -1344,10 +1344,7 @@ Http::patch('/v1/users/:userId/password')
->setAttribute('password', '')
->setAttribute('passwordUpdate', DateTime::now());
$user = $dbForProject->updateDocument('users', $user->getId(), new Document([
'password' => $user->getAttribute('password'),
'passwordUpdate' => $user->getAttribute('passwordUpdate'),
]));
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
$queueForEvents->setParam('userId', $user->getId());
$response->dynamic($user, Response::MODEL_USER);
}
@@ -1380,13 +1377,7 @@ Http::patch('/v1/users/:userId/password')
->setAttribute('hash', $hasher->getName())
->setAttribute('hashOptions', $hasher->getOptions());
$user = $dbForProject->updateDocument('users', $user->getId(), new Document([
'password' => $user->getAttribute('password'),
'passwordHistory' => $user->getAttribute('passwordHistory'),
'passwordUpdate' => $user->getAttribute('passwordUpdate'),
'hash' => $user->getAttribute('hash'),
'hashOptions' => $user->getAttribute('hashOptions'),
]));
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
$sessions = $user->getAttribute('sessions', []);
$invalidate = $project->getAttribute('auths', default: [])['invalidateSessions'] ?? false;
@@ -1478,15 +1469,7 @@ Http::patch('/v1/users/:userId/email')
;
try {
$user = $dbForProject->updateDocument('users', $user->getId(), new Document([
'email' => $user->getAttribute('email'),
'emailVerification' => $user->getAttribute('emailVerification'),
'emailCanonical' => $user->getAttribute('emailCanonical'),
'emailIsCanonical' => $user->getAttribute('emailIsCanonical'),
'emailIsCorporate' => $user->getAttribute('emailIsCorporate'),
'emailIsDisposable' => $user->getAttribute('emailIsDisposable'),
'emailIsFree' => $user->getAttribute('emailIsFree'),
]));
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
/**
* @var Document $oldTarget
*/
@@ -1494,8 +1477,7 @@ Http::patch('/v1/users/:userId/email')
if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) {
if (\strlen($email) !== 0) {
$dbForProject->updateDocument('targets', $oldTarget->getId(), new Document(['identifier' => $email]));
$oldTarget->setAttribute('identifier', $email);
$dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $email));
} else {
$dbForProject->deleteDocument('targets', $oldTarget->getId());
}
@@ -1560,15 +1542,12 @@ Http::patch('/v1/users/:userId/phone')
$oldPhone = $user->getAttribute('phone');
// Store null instead of empty string so unique constraint allows multiple users without phone
$phoneValue = $number !== '' ? $number : null;
$user
->setAttribute('phone', $phoneValue)
->setAttribute('phone', $number)
->setAttribute('phoneVerification', false)
;
if ($number !== '') {
if (\strlen($number) !== 0) {
$target = $dbForProject->findOne('targets', [
Query::equal('identifier', [$number]),
]);
@@ -1579,24 +1558,20 @@ Http::patch('/v1/users/:userId/phone')
}
try {
$user = $dbForProject->updateDocument('users', $user->getId(), new Document([
'phone' => $phoneValue,
'phoneVerification' => $user->getAttribute('phoneVerification'),
]));
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
/**
* @var Document $oldTarget
*/
$oldTarget = $user->find('identifier', $oldPhone, 'targets');
if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) {
if ($number !== '') {
$dbForProject->updateDocument('targets', $oldTarget->getId(), new Document(['identifier' => $number]));
$oldTarget->setAttribute('identifier', $number);
if (\strlen($number) !== 0) {
$dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $number));
} else {
$dbForProject->deleteDocument('targets', $oldTarget->getId());
}
} else {
if ($number !== '') {
if (\strlen($number) !== 0) {
$target = $dbForProject->createDocument('targets', new Document([
'$permissions' => [
Permission::read(Role::user($user->getId())),
@@ -1655,7 +1630,7 @@ Http::patch('/v1/users/:userId/verification')
throw new Exception(Exception::USER_NOT_FOUND);
}
$user = $dbForProject->updateDocument('users', $user->getId(), new Document(['emailVerification' => $emailVerification]));
$user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('emailVerification', $emailVerification));
$queueForEvents->setParam('userId', $user->getId());
@@ -1693,7 +1668,7 @@ Http::patch('/v1/users/:userId/prefs')
throw new Exception(Exception::USER_NOT_FOUND);
}
$user = $dbForProject->updateDocument('users', $user->getId(), new Document(['prefs' => $prefs]));
$user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('prefs', $prefs));
$queueForEvents
->setParam('userId', $user->getId());
@@ -1793,13 +1768,7 @@ Http::patch('/v1/users/:userId/targets/:targetId')
$target->setAttribute('name', $name);
}
$target = $dbForProject->updateDocument('targets', $target->getId(), new Document([
'identifier' => $target->getAttribute('identifier'),
'expired' => $target->getAttribute('expired'),
'providerId' => $target->getAttribute('providerId'),
'providerInternalId' => $target->getAttribute('providerInternalId'),
'name' => $target->getAttribute('name'),
]));
$target = $dbForProject->updateDocument('targets', $target->getId(), $target);
$dbForProject->purgeCachedDocument('users', $user->getId());
$queueForEvents
@@ -1867,7 +1836,7 @@ Http::patch('/v1/users/:userId/mfa')
$user->setAttribute('mfa', $mfa);
$user = $dbForProject->updateDocument('users', $user->getId(), new Document(['mfa' => $user->getAttribute('mfa')]));
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
$queueForEvents->setParam('userId', $user->getId());
@@ -2055,7 +2024,7 @@ Http::patch('/v1/users/:userId/mfa/recovery-codes')
$mfaRecoveryCodes = Type::generateBackupCodes();
$user->setAttribute('mfaRecoveryCodes', $mfaRecoveryCodes);
$dbForProject->updateDocument('users', $user->getId(), new Document(['mfaRecoveryCodes' => $mfaRecoveryCodes]));
$dbForProject->updateDocument('users', $user->getId(), $user);
$queueForEvents->setParam('userId', $user->getId());
@@ -2127,7 +2096,7 @@ Http::put('/v1/users/:userId/mfa/recovery-codes')
$mfaRecoveryCodes = Type::generateBackupCodes();
$user->setAttribute('mfaRecoveryCodes', $mfaRecoveryCodes);
$dbForProject->updateDocument('users', $user->getId(), new Document(['mfaRecoveryCodes' => $mfaRecoveryCodes]));
$dbForProject->updateDocument('users', $user->getId(), $user);
$queueForEvents->setParam('userId', $user->getId());
+122 -71
View File
@@ -5,11 +5,11 @@ require_once __DIR__ . '/../init.php';
use Ahc\Jwt\JWT;
use Ahc\Jwt\JWTException;
use Appwrite\Auth\Key;
use Appwrite\Bus\Events\ExecutionCompleted;
use Appwrite\Bus\Events\RequestCompleted;
use Appwrite\Event\Certificate;
use Appwrite\Event\Delete as DeleteEvent;
use Appwrite\Event\Event;
use Appwrite\Event\Execution;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Network\Cors;
use Appwrite\Platform\Appwrite;
@@ -30,14 +30,11 @@ use Appwrite\Utopia\Response\Filters\V16 as ResponseV16;
use Appwrite\Utopia\Response\Filters\V17 as ResponseV17;
use Appwrite\Utopia\Response\Filters\V18 as ResponseV18;
use Appwrite\Utopia\Response\Filters\V19 as ResponseV19;
use Appwrite\Utopia\Response\Filters\V20 as ResponseV20;
use Appwrite\Utopia\Response\Filters\V21 as ResponseV21;
use Appwrite\Utopia\View;
use Executor\Executor;
use MaxMind\Db\Reader;
use Swoole\Http\Request as SwooleRequest;
use Swoole\Table;
use Utopia\Bus\Bus;
use Utopia\Config\Config;
use Utopia\Console;
use Utopia\Database\Database;
@@ -65,7 +62,7 @@ Config::setParam('domainVerification', false);
Config::setParam('cookieDomain', 'localhost');
Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE);
function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Authorization $authorization, ?Key $apiKey, DeleteEvent $queueForDeletes, int $executionsRetentionCount)
function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, StatsUsage $queueForStatsUsage, Execution $queueForExecutions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Authorization $authorization, ?Key $apiKey, DeleteEvent $queueForDeletes, int $executionsRetentionCount)
{
$host = $request->getHostname() ?? '';
if (!empty($previewHostname)) {
@@ -118,7 +115,8 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
}
}
if (!in_array($host, $platformHostnames)) {
$routerProtectionEnabled = System::getEnv('_APP_OPTIONS_ROUTER_PROTECTION', 'disabled') === 'enabled';
if ($routerProtectionEnabled && !in_array($host, $platformHostnames)) {
throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'Router protection does not allow accessing Appwrite over this domain. Please add it as custom domain to your project or disable _APP_OPTIONS_ROUTER_PROTECTION environment variable.', view: $errorView);
}
@@ -134,9 +132,8 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
if (!$project->isEmpty() && $project->getId() !== 'console') {
$accessedAt = $project->getAttribute('accessedAt', 0);
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) {
$authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), new Document([
'accessedAt' => DateTime::now()
])));
$project->setAttribute('accessedAt', DateTime::now());
$authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $project));
}
/**
@@ -324,7 +321,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
};
$runtimes = Config::getParam($version === 'v2' ? 'runtimes-v2' : 'runtimes', []);
$spec = Config::getParam('specifications')[$resource->getAttribute('runtimeSpecification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
$spec = Config::getParam('specifications')[$resource->getAttribute('specification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
$runtime = match ($type) {
'function' => $runtimes[$resource->getAttribute('runtime')] ?? null,
@@ -554,10 +551,6 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
}
}
if (!empty($deployment->getAttribute('startCommand', ''))) {
$startCommand = 'cd /usr/local/server/src/function/ && ' . str_replace(['"', '`', '$'], ['\\"', '\\`', '\\$'], $deployment->getAttribute('startCommand', ''));
}
$runtimeEntrypoint = match ($version) {
'v2' => '',
default => "cp /tmp/code.$extension /mnt/code/code.$extension && nohup helpers/start.sh \"$startCommand\"",
@@ -714,12 +707,10 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
}
} finally {
if ($type === 'function' || $type === 'site') {
$bus->dispatch(new ExecutionCompleted(
execution: $execution->getArrayCopy(),
project: $project->getArrayCopy(),
spec: $spec,
resource: $resource->getArrayCopy(),
));
$queueForExecutions
->setExecution($execution)
->setProject($project)
->trigger();
}
}
@@ -764,12 +755,70 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
->setStatusCode($execution['responseStatusCode'] ?? 200)
->send($body);
$bus->dispatch(new RequestCompleted(
project: $project->getArrayCopy(),
request: $request,
response: $response,
deployment: $deployment->getArrayCopy(),
));
$fileSize = 0;
$file = $request->getFiles('file');
if (!empty($file)) {
$fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size'];
}
if (!empty($apiKey) && !empty($apiKey->getDisabledMetrics())) {
foreach ($apiKey->getDisabledMetrics() as $key) {
$queueForStatsUsage->disableMetric($key);
}
}
$metricTypeExecutions = str_replace(['{resourceType}'], [$deployment->getAttribute('resourceType')], METRIC_RESOURCE_TYPE_EXECUTIONS);
$metricTypeIdExecutions = str_replace(['{resourceType}', '{resourceInternalId}'], [$deployment->getAttribute('resourceType'), $resource->getSequence()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS);
$metricTypeExecutionsCompute = str_replace(['{resourceType}'], [$deployment->getAttribute('resourceType')], METRIC_RESOURCE_TYPE_EXECUTIONS_COMPUTE);
$metricTypeIdExecutionsCompute = str_replace(['{resourceType}', '{resourceInternalId}'], [$deployment->getAttribute('resourceType'), $resource->getSequence()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS_COMPUTE);
$metricTypeExecutionsMbSeconds = str_replace(['{resourceType}'], [$deployment->getAttribute('resourceType')], METRIC_RESOURCE_TYPE_EXECUTIONS_MB_SECONDS);
$metricTypeIdExecutionsMBSeconds = str_replace(['{resourceType}', '{resourceInternalId}'], [$deployment->getAttribute('resourceType'), $resource->getSequence()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS_MB_SECONDS);
if ($deployment->getAttribute('resourceType') === 'sites') {
$queueForStatsUsage
->disableMetric(METRIC_NETWORK_REQUESTS)
->disableMetric(METRIC_NETWORK_INBOUND)
->disableMetric(METRIC_NETWORK_OUTBOUND);
if ($resource->getAttribute('adapter') !== 'ssr') {
$queueForStatsUsage
->disableMetric(METRIC_EXECUTIONS)
->disableMetric(METRIC_EXECUTIONS_COMPUTE)
->disableMetric(METRIC_EXECUTIONS_MB_SECONDS)
->disableMetric($metricTypeExecutions)
->disableMetric($metricTypeIdExecutions)
->disableMetric($metricTypeExecutionsCompute)
->disableMetric($metricTypeIdExecutionsCompute)
->disableMetric($metricTypeExecutionsMbSeconds)
->disableMetric($metricTypeIdExecutionsMBSeconds);
}
$queueForStatsUsage
->addMetric(METRIC_SITES_REQUESTS, 1)
->addMetric(METRIC_SITES_INBOUND, $request->getSize() + $fileSize)
->addMetric(METRIC_SITES_OUTBOUND, $response->getSize())
->addMetric(str_replace('{siteInternalId}', $resource->getSequence(), METRIC_SITES_ID_REQUESTS), 1)
->addMetric(str_replace('{siteInternalId}', $resource->getSequence(), METRIC_SITES_ID_INBOUND), $request->getSize() + $fileSize)
->addMetric(str_replace('{siteInternalId}', $resource->getSequence(), METRIC_SITES_ID_OUTBOUND), $response->getSize())
;
}
$compute = (int)($execution->getAttribute('duration') * 1000);
$mbSeconds = (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT));
$queueForStatsUsage
->addMetric(METRIC_NETWORK_REQUESTS, 1)
->addMetric(METRIC_NETWORK_INBOUND, $request->getSize() + $fileSize)
->addMetric(METRIC_NETWORK_OUTBOUND, $response->getSize())
->addMetric(METRIC_EXECUTIONS, 1)
->addMetric($metricTypeExecutions, 1)
->addMetric($metricTypeIdExecutions, 1)
->addMetric(METRIC_EXECUTIONS_COMPUTE, $compute) // per project
->addMetric($metricTypeExecutionsCompute, $compute) // per function
->addMetric($metricTypeIdExecutionsCompute, $compute) // per function
->addMetric(METRIC_EXECUTIONS_MB_SECONDS, $mbSeconds)
->addMetric($metricTypeExecutionsMbSeconds, $mbSeconds)
->addMetric($metricTypeIdExecutionsMBSeconds, $mbSeconds)
->setProject($project)
->trigger();
/* cleanup */
if ($executionsRetentionCount > 0 && ENABLE_EXECUTIONS_LIMIT_ON_ROUTE) {
@@ -833,8 +882,9 @@ Http::init()
->inject('locale')
->inject('localeCodes')
->inject('geodb')
->inject('queueForStatsUsage')
->inject('queueForEvents')
->inject('bus')
->inject('queueForExecutions')
->inject('executor')
->inject('platform')
->inject('isResourceBlocked')
@@ -845,7 +895,7 @@ Http::init()
->inject('authorization')
->inject('queueForDeletes')
->inject('executionsRetentionCount')
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, Reader $geodb, Event $queueForEvents, Bus $bus, Executor $executor, array $platform, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, Reader $geodb, StatsUsage $queueForStatsUsage, Event $queueForEvents, Execution $queueForExecutions, Executor $executor, array $platform, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
/*
* Appwrite Router
*/
@@ -853,7 +903,7 @@ Http::init()
$platformHostnames = $platform['hostnames'] ?? [];
// Only run Router when external domain
if (!\in_array($hostname, $platformHostnames) || !empty($previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForExecutions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -939,23 +989,17 @@ Http::init()
*/
$responseFormat = $request->getHeader('x-appwrite-response-format', System::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
if ($responseFormat) {
if (version_compare($responseFormat, '1.9.0', '<')) {
$response->addFilter(new ResponseV21());
}
if (version_compare($responseFormat, '1.8.0', '<')) {
$response->addFilter(new ResponseV20());
}
if (version_compare($responseFormat, '1.7.0', '<')) {
$response->addFilter(new ResponseV19());
}
if (version_compare($responseFormat, '1.6.0', '<')) {
$response->addFilter(new ResponseV18());
if (version_compare($responseFormat, '1.4.0', '<')) {
$response->addFilter(new ResponseV16());
}
if (version_compare($responseFormat, '1.5.0', '<')) {
$response->addFilter(new ResponseV17());
}
if (version_compare($responseFormat, '1.4.0', '<')) {
$response->addFilter(new ResponseV16());
if (version_compare($responseFormat, '1.6.0', '<')) {
$response->addFilter(new ResponseV18());
}
if (version_compare($responseFormat, '1.7.0', '<')) {
$response->addFilter(new ResponseV19());
}
if (version_compare($responseFormat, APP_VERSION_STABLE, '>')) {
$warnings[] = "The current SDK is built for Appwrite " . $responseFormat . ". However, the current Appwrite server version is " . APP_VERSION_STABLE . ". Please downgrade your SDK to match the Appwrite version: https://appwrite.io/docs/sdks";
@@ -1135,7 +1179,8 @@ Http::options()
->inject('dbForPlatform')
->inject('getProjectDB')
->inject('queueForEvents')
->inject('bus')
->inject('queueForStatsUsage')
->inject('queueForExecutions')
->inject('executor')
->inject('geodb')
->inject('isResourceBlocked')
@@ -1148,14 +1193,14 @@ Http::options()
->inject('authorization')
->inject('queueForDeletes')
->inject('executionsRetentionCount')
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Execution $queueForExecutions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
/*
* Appwrite Router
*/
$platformHostnames = $platform['hostnames'] ?? [];
// Only run Router when external domain
if (!in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForExecutions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -1171,11 +1216,12 @@ Http::options()
/** OPTIONS requests in utopia do not execute shutdown handlers, as a result we need to track the OPTIONS requests explicitly
* @see https://github.com/utopia-php/http/blob/0.33.16/src/App.php#L825-L855
*/
$bus->dispatch(new RequestCompleted(
project: $project->getArrayCopy(),
request: $request,
response: $response,
));
$queueForStatsUsage
->addMetric(METRIC_NETWORK_REQUESTS, 1)
->addMetric(METRIC_NETWORK_INBOUND, $request->getSize())
->addMetric(METRIC_NETWORK_OUTBOUND, $response->getSize())
->setProject($project)
->trigger();
});
Http::error()
@@ -1186,10 +1232,10 @@ Http::error()
->inject('project')
->inject('logger')
->inject('log')
->inject('bus')
->inject('queueForStatsUsage')
->inject('devKey')
->inject('authorization')
->action(function (Throwable $error, Http $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, Bus $bus, Document $devKey, Authorization $authorization) {
->action(function (Throwable $error, Http $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, StatsUsage $queueForStatsUsage, Document $devKey, Authorization $authorization) {
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
$route = $utopia->getRoute();
$class = \get_class($error);
@@ -1262,12 +1308,21 @@ Http::error()
*/
if (!$publish && $project->getId() !== 'console') {
if (!DBUser::isPrivileged($authorization->getRoles())) {
$bus->dispatch(new RequestCompleted(
project: $project->getArrayCopy(),
request: $request,
response: $response,
));
$fileSize = 0;
$file = $request->getFiles('file');
if (!empty($file)) {
$fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size'];
}
$queueForStatsUsage
->addMetric(METRIC_NETWORK_REQUESTS, 1)
->addMetric(METRIC_NETWORK_INBOUND, $request->getSize() + $fileSize)
->addMetric(METRIC_NETWORK_OUTBOUND, $response->getSize());
}
$queueForStatsUsage
->setProject($project)
->trigger();
}
if ($logger && $publish) {
@@ -1399,9 +1454,6 @@ Http::error()
$sdk = $route?->getLabel("sdk", false);
$action = 'UNKNOWN_NAMESPACE.UNKNOWN.METHOD';
if (!empty($sdk)) {
if (\is_array($sdk)) {
$sdk = $sdk[0];
}
/** @var \Appwrite\SDK\Method $sdk */
$action = $sdk->getNamespace() . '.' . $sdk->getMethodName();
} elseif ($route === null) {
@@ -1517,7 +1569,8 @@ Http::get('/robots.txt')
->inject('dbForPlatform')
->inject('getProjectDB')
->inject('queueForEvents')
->inject('bus')
->inject('queueForStatsUsage')
->inject('queueForExecutions')
->inject('executor')
->inject('geodb')
->inject('isResourceBlocked')
@@ -1527,13 +1580,13 @@ Http::get('/robots.txt')
->inject('authorization')
->inject('queueForDeletes')
->inject('executionsRetentionCount')
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Execution $queueForExecutions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
$platformHostnames = $platform['hostnames'] ?? [];
if (in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
$template = new View(__DIR__ . '/../views/general/robots.phtml');
$response->text($template->render(false));
} else {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForExecutions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -1551,7 +1604,8 @@ Http::get('/humans.txt')
->inject('dbForPlatform')
->inject('getProjectDB')
->inject('queueForEvents')
->inject('bus')
->inject('queueForStatsUsage')
->inject('queueForExecutions')
->inject('executor')
->inject('geodb')
->inject('isResourceBlocked')
@@ -1561,13 +1615,13 @@ Http::get('/humans.txt')
->inject('authorization')
->inject('queueForDeletes')
->inject('executionsRetentionCount')
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Execution $queueForExecutions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeleteEvent $queueForDeletes, int $executionsRetentionCount) {
$platformHostnames = $platform['hostnames'] ?? [];
if (in_array($request->getHostname(), $platformHostnames) || !empty($previewHostname)) {
$template = new View(__DIR__ . '/../views/general/humans.phtml');
$response->text($template->render(false));
} else {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForExecutions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $queueForDeletes, $executionsRetentionCount)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -1665,10 +1719,7 @@ Http::get('/v1/ping')
->setAttribute('pingedAt', $pingedAt);
$authorization->skip(function () use ($dbForPlatform, $project) {
$dbForPlatform->updateDocument('projects', $project->getId(), new Document([
'pingCount' => $project->getAttribute('pingCount'),
'pingedAt' => $project->getAttribute('pingedAt')
]));
$dbForPlatform->updateDocument('projects', $project->getId(), $project);
});
$queueForEvents
+103 -126
View File
@@ -2,7 +2,6 @@
use Appwrite\Auth\Key;
use Appwrite\Auth\MFA\Type\TOTP;
use Appwrite\Bus\Events\RequestCompleted;
use Appwrite\Event\Audit;
use Appwrite\Event\Build;
use Appwrite\Event\Database as EventDatabase;
@@ -10,21 +9,18 @@ use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Message\Usage as UsageMessage;
use Appwrite\Event\Messaging;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Event\Realtime;
use Appwrite\Event\StatsUsage;
use Appwrite\Event\Webhook;
use Appwrite\Extend\Exception;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Functions\EventProcessor;
use Appwrite\SDK\Method;
use Appwrite\Usage\Context;
use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Utopia\Abuse\Abuse;
use Utopia\Bus\Bus;
use Utopia\Cache\Adapter\Filesystem;
use Utopia\Cache\Cache;
use Utopia\Config\Config;
@@ -55,7 +51,7 @@ $parseLabel = function (string $label, array $responsePayload, array $requestPar
$replace = $parts[1] ?? '';
$params = match ($namespace) {
'user' => (array) $user,
'user' => (array)$user,
'request' => $requestParams,
default => $responsePayload,
};
@@ -63,13 +59,13 @@ $parseLabel = function (string $label, array $responsePayload, array $requestPar
if (array_key_exists($replace, $params)) {
$replacement = $params[$replace];
// Convert to string if it's not already a string
if (! is_string($replacement)) {
if (!is_string($replacement)) {
if (is_array($replacement)) {
$replacement = json_encode($replacement);
} elseif (is_object($replacement) && method_exists($replacement, '__toString')) {
$replacement = (string) $replacement;
$replacement = (string)$replacement;
} elseif (is_scalar($replacement)) {
$replacement = (string) $replacement;
$replacement = (string)$replacement;
} else {
throw new Exception(Exception::GENERAL_SERVER_ERROR, "The server encountered an error while parsing the label: $label. Please create an issue on GitHub to allow us to investigate further https://github.com/appwrite/appwrite/issues/new/choose");
}
@@ -77,7 +73,6 @@ $parseLabel = function (string $label, array $responsePayload, array $requestPar
$label = \str_replace($find, $replacement, $label);
}
}
return $label;
};
@@ -163,7 +158,7 @@ Http::init()
$scopes = $roles[$role]['scopes'];
// Step 5: API Key Authentication
if (! empty($apiKey)) {
if (!empty($apiKey)) {
// Check if key is expired
if ($apiKey->isExpired()) {
throw new Exception(Exception::PROJECT_KEY_EXPIRED);
@@ -173,6 +168,7 @@ Http::init()
$role = $apiKey->getRole();
$scopes = $apiKey->getScopes();
// Handle special app role case
if ($apiKey->getRole() === User::ROLE_APPS) {
// Disable authorization checks for project API keys
@@ -195,19 +191,19 @@ Http::init()
// For standard keys, update last accessed time
if (\in_array($apiKey->getType(), [API_KEY_STANDARD, API_KEY_ORGANIZATION, API_KEY_ACCOUNT])) {
$dbKey = null;
if (! empty($apiKey->getProjectId())) {
if (!empty($apiKey->getProjectId())) {
$dbKey = $project->find(
key: 'secret',
find: $request->getHeader('x-appwrite-key', ''),
subject: 'keys'
);
} elseif (! empty($apiKey->getUserId())) {
} elseif (!empty($apiKey->getUserId())) {
$dbKey = $user->find(
key: 'secret',
find: $request->getHeader('x-appwrite-key', ''),
subject: 'keys'
);
} elseif (! empty($apiKey->getTeamId())) {
} elseif (!empty($apiKey->getTeamId())) {
$dbKey = $team->find(
key: 'secret',
find: $request->getHeader('x-appwrite-key', ''),
@@ -233,7 +229,7 @@ Http::init()
if ($sdk !== 'UNKNOWN' && $sdkValidator->isValid($sdk)) {
$sdks = $dbKey->getAttribute('sdks', []);
if (! in_array($sdk, $sdks)) {
if (!in_array($sdk, $sdks)) {
$sdks[] = $sdk;
$updates->setAttribute('sdks', $sdks);
@@ -241,14 +237,14 @@ Http::init()
}
}
if (! $updates->isEmpty()) {
if (!$updates->isEmpty()) {
$dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->updateDocument('keys', $dbKey->getId(), $updates));
if (! empty($apiKey->getProjectId())) {
if (!empty($apiKey->getProjectId())) {
$dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->purgeCachedDocument('projects', $project->getId()));
} elseif (! empty($apiKey->getUserId())) {
} elseif (!empty($apiKey->getUserId())) {
$dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->purgeCachedDocument('users', $user->getId()));
} elseif (! empty($apiKey->getTeamId())) {
} elseif (!empty($apiKey->getTeamId())) {
$dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->purgeCachedDocument('teams', $team->getId()));
}
}
@@ -285,7 +281,7 @@ Http::init()
}
}
} // Admin User Authentication
elseif (($project->getId() === 'console' && ! $team->isEmpty() && ! $user->isEmpty()) || ($project->getId() !== 'console' && ! $user->isEmpty() && $mode === APP_MODE_ADMIN)) {
elseif (($project->getId() === 'console' && !$team->isEmpty() && !$user->isEmpty()) || ($project->getId() !== 'console' && !$user->isEmpty() && $mode === APP_MODE_ADMIN)) {
$teamId = $team->getId();
$adminRoles = [];
$memberships = $user->getAttribute('memberships', []);
@@ -310,7 +306,7 @@ Http::init()
// Useful for those who have project-specific roles but don't have team-wide role.
$scopes = ['teams.read', 'projects.read'];
foreach ($adminRoles as $adminRole) {
$isTeamWideRole = ! str_starts_with($adminRole, 'project-');
$isTeamWideRole = !str_starts_with($adminRole, 'project-');
$isProjectSpecificRole = $projectId !== 'console' && str_starts_with($adminRole, 'project-' . $projectId);
if ($isTeamWideRole || $isProjectSpecificRole) {
@@ -348,39 +344,34 @@ Http::init()
* But, for actions on resources (sites, functions, etc.) in a non-console project, we explicitly check
* whether the admin user has necessary permission on the project (sites, functions, etc. don't have permissions associated to them).
*/
if (empty($apiKey) && ! $user->isEmpty() && $project->getId() !== 'console' && $mode === APP_MODE_ADMIN) {
if (empty($apiKey) && !$user->isEmpty() && $project->getId() !== 'console' && $mode === APP_MODE_ADMIN) {
$input = new Input(Database::PERMISSION_READ, $project->getPermissionsByType(Database::PERMISSION_READ));
$initialStatus = $authorization->getStatus();
$authorization->enable();
if (! $authorization->isValid($input)) {
if (!$authorization->isValid($input)) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$authorization->setStatus($initialStatus);
}
// Step 6: Update project and user last activity
if (! $project->isEmpty() && $project->getId() !== 'console') {
if (!$project->isEmpty() && $project->getId() !== 'console') {
$accessedAt = $project->getAttribute('accessedAt', 0);
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) {
$authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), new Document([
'accessedAt' => DateTime::now()
])));
$project->setAttribute('accessedAt', DateTime::now());
$authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $project));
}
}
if (! empty($user->getId())) {
if (!empty($user->getId())) {
$accessedAt = $user->getAttribute('accessedAt', 0);
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_USER_ACCESS)) > $accessedAt) {
$user->setAttribute('accessedAt', DateTime::now());
if ($project->getId() !== 'console' && $mode !== APP_MODE_ADMIN) {
$dbForProject->updateDocument('users', $user->getId(), new Document([
'accessedAt' => $user->getAttribute('accessedAt')
]));
if ($project->getId() !== 'console' && APP_MODE_ADMIN !== $mode) {
$dbForProject->updateDocument('users', $user->getId(), $user);
} else {
$authorization->skip(fn () => $dbForPlatform->updateDocument('users', $user->getId(), new Document([
'accessedAt' => $user->getAttribute('accessedAt')
])));
$authorization->skip(fn () => $dbForPlatform->updateDocument('users', $user->getId(), $user));
}
}
}
@@ -397,26 +388,26 @@ Http::init()
$method = $method[0];
}
if (! empty($method)) {
if (!empty($method)) {
$namespace = $method->getNamespace();
if (
array_key_exists($namespace, $project->getAttribute('services', []))
&& ! $project->getAttribute('services', [])[$namespace]
&& ! (User::isPrivileged($authorization->getRoles()) || User::isApp($authorization->getRoles()))
&& !$project->getAttribute('services', [])[$namespace]
&& !(User::isPrivileged($authorization->getRoles()) || User::isApp($authorization->getRoles()))
) {
throw new Exception(Exception::GENERAL_SERVICE_DISABLED);
}
}
// Step 9: Validate scope permissions
$allowed = (array) $route->getLabel('scope', 'none');
$allowed = (array)$route->getLabel('scope', 'none');
if (empty(\array_intersect($allowed, $scopes))) {
throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, $user->getAttribute('email', 'User') . ' (role: ' . \strtolower($roles[$role]['label']) . ') missing scopes (' . \json_encode($allowed) . ')');
}
// Step 10: Check if user is blocked
if ($user->getAttribute('status') === false) { // Account is blocked
if (false === $user->getAttribute('status')) { // Account is blocked
throw new Exception(Exception::USER_BLOCKED);
}
@@ -434,7 +425,7 @@ Http::init()
$minimumFactors = ($mfaEnabled && $hasMoreFactors) ? 2 : 1;
// Step 13: Handle Multi-Factor Authentication
if (! in_array('mfa', $route->getGroups())) {
if (!in_array('mfa', $route->getGroups())) {
if ($session && \count($session->getAttribute('factors', [])) < $minimumFactors) {
throw new Exception(Exception::USER_MORE_FACTORS_REQUIRED);
}
@@ -454,7 +445,7 @@ Http::init()
->inject('queueForDeletes')
->inject('queueForDatabase')
->inject('queueForBuilds')
->inject('usage')
->inject('queueForStatsUsage')
->inject('queueForFunctions')
->inject('queueForMails')
->inject('dbForProject')
@@ -467,14 +458,14 @@ Http::init()
->inject('telemetry')
->inject('platform')
->inject('authorization')
->action(function (Http $utopia, Request $request, Response $response, Document $project, Document $user, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Context $usage, Func $queueForFunctions, Mail $queueForMails, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry, array $platform, Authorization $authorization) {
->action(function (Http $utopia, Request $request, Response $response, Document $project, Document $user, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, StatsUsage $queueForStatsUsage, 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();
if (
array_key_exists('rest', $project->getAttribute('apis', []))
&& ! $project->getAttribute('apis', [])['rest']
&& ! (User::isPrivileged($authorization->getRoles()) || User::isApp($authorization->getRoles()))
&& !$project->getAttribute('apis', [])['rest']
&& !(User::isPrivileged($authorization->getRoles()) || User::isApp($authorization->getRoles()))
) {
throw new AppwriteException(AppwriteException::GENERAL_API_DISABLED);
}
@@ -486,7 +477,7 @@ Http::init()
$abuseKeyLabel = $route->getLabel('abuse-key', 'url:{url},ip:{ip}');
$timeLimitArray = [];
$abuseKeyLabel = (! is_array($abuseKeyLabel)) ? [$abuseKeyLabel] : $abuseKeyLabel;
$abuseKeyLabel = (!is_array($abuseKeyLabel)) ? [$abuseKeyLabel] : $abuseKeyLabel;
foreach ($abuseKeyLabel as $abuseKey) {
$start = $request->getContentRangeStart();
@@ -499,7 +490,7 @@ Http::init()
->setParam('{ip}', $request->getIP())
->setParam('{url}', $request->getHostname() . $route->getPath())
->setParam('{method}', $request->getMethod())
->setParam('{chunkId}', (int) ($start / ($end + 1 - $start)));
->setParam('{chunkId}', (int)($start / ($end + 1 - $start)));
$timeLimitArray[] = $timeLimit;
}
@@ -511,7 +502,7 @@ Http::init()
foreach ($timeLimitArray as $timeLimit) {
foreach ($request->getParams() as $key => $value) { // Set request params as potential abuse keys
if (! empty($value)) {
if (!empty($value)) {
$timeLimit->setParam('{param-' . $key . '}', (\is_array($value)) ? \json_encode($value) : $value);
}
}
@@ -534,8 +525,8 @@ Http::init()
if (
$enabled // Abuse is enabled
&& ! $isAppUser // User is not API key
&& ! $isPrivilegedUser // User is not an admin
&& !$isAppUser // User is not API key
&& !$isPrivilegedUser // User is not an admin
&& $devKey->isEmpty() // request doesn't not contain development key
&& $abuse->check() // Route is rate-limited
) {
@@ -564,13 +555,19 @@ Http::init()
->setProject($project);
/* If a session exists, use the user associated with the session */
if (! $user->isEmpty()) {
if (!$user->isEmpty()) {
$userClone = clone $user;
// $user doesn't support `type` and can cause unintended effects.
$userClone->setAttribute('type', ACTIVITY_TYPE_USER);
$queueForAudits->setUser($userClone);
}
if (!empty($apiKey) && !empty($apiKey->getDisabledMetrics())) {
foreach ($apiKey->getDisabledMetrics() as $key) {
$queueForStatsUsage->disableMetric($key);
}
}
/* Auto-set projects */
$queueForDeletes->setProject($project);
$queueForDatabase->setProject($project);
@@ -584,70 +581,73 @@ Http::init()
$queueForBuilds->setPlatform($platform);
$queueForMails->setPlatform($platform);
$useCache = $route->getLabel('cache', false);
$storageCacheOperationsCounter = $telemetry->createCounter('storage.cache.operations.load');
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));
$cacheLog = $authorization->skip(fn () => $dbForProject->getDocument('cache', $key));
$cache = new Cache(
new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId())
);
$timestamp = 60 * 60 * 24 * 180; // Temporarily increase the TTL to 180 day to ensure files in the cache are still fetched.
$data = $cache->load($key, $timestamp);
if (! empty($data) && ! $cacheLog->isEmpty()) {
if (!empty($data) && !$cacheLog->isEmpty()) {
$usageMetric = $route->getLabel('usage.metric', null);
if ($usageMetric === METRIC_AVATARS_SCREENSHOTS_GENERATED) {
$queueForStatsUsage->disableMetric(METRIC_AVATARS_SCREENSHOTS_GENERATED);
}
$parts = explode('/', $cacheLog->getAttribute('resourceType', ''));
$type = $parts[0] ?? null;
if ($type === 'bucket' && (! $isImageTransformation || ! $isDisabled)) {
if ($type === 'bucket' && (!$isImageTransformation || !$isDisabled)) {
$bucketId = $parts[1] ?? null;
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isToken = ! $resourceToken->isEmpty() && $resourceToken->getAttribute('bucketInternalId') === $bucket->getSequence();
$isToken = !$resourceToken->isEmpty() && $resourceToken->getAttribute('bucketInternalId') === $bucket->getSequence();
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
if ($bucket->isEmpty() || (! $bucket->getAttribute('enabled') && ! $isAppUser && ! $isPrivilegedUser)) {
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAppUser && !$isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
if (! $bucket->getAttribute('transformations', true) && ! $isAppUser && ! $isPrivilegedUser) {
if (!$bucket->getAttribute('transformations', true) && !$isAppUser && !$isPrivilegedUser) {
throw new Exception(Exception::STORAGE_BUCKET_TRANSFORMATIONS_DISABLED);
}
$fileSecurity = $bucket->getAttribute('fileSecurity', false);
$valid = $authorization->isValid(new Input(Database::PERMISSION_READ, $bucket->getRead()));
if (! $fileSecurity && ! $valid && ! $isToken) {
if (!$fileSecurity && !$valid && !$isToken) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
$parts = explode('/', $cacheLog->getAttribute('resource'));
$fileId = $parts[1] ?? null;
if ($fileSecurity && ! $valid && ! $isToken) {
if ($fileSecurity && !$valid && !$isToken) {
$file = $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId);
} else {
$file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId));
}
if (! $resourceToken->isEmpty() && $resourceToken->getAttribute('fileInternalId') !== $file->getSequence()) {
if (!$resourceToken->isEmpty() && $resourceToken->getAttribute('fileInternalId') !== $file->getSequence()) {
throw new Exception(Exception::USER_UNAUTHORIZED);
}
if ($file->isEmpty()) {
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
}
// Do not update transformedAt if it's a console user
if (! User::isPrivileged($authorization->getRoles())) {
//Do not update transformedAt if it's a console user
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());
$authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $file->getAttribute('bucketInternalId'), $file->getId(), new Document([
'transformedAt' => $file->getAttribute('transformedAt')
])));
$authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $file->getAttribute('bucketInternalId'), $file->getId(), $file));
}
}
}
@@ -657,7 +657,7 @@ Http::init()
->addHeader('X-Appwrite-Cache', 'hit')
->setContentType($cacheLog->getAttribute('mimeType'));
$storageCacheOperationsCounter->add(1, ['result' => 'hit']);
if (! $isImageTransformation || ! $isDisabled) {
if (!$isImageTransformation || !$isDisabled) {
$response->send($data);
}
} else {
@@ -680,7 +680,7 @@ Http::init()
return;
}
if (! $user->isEmpty()) {
if (!$user->isEmpty()) {
throw new Exception(Exception::USER_SESSION_ALREADY_EXISTS);
}
});
@@ -734,8 +734,7 @@ Http::shutdown()
->inject('user')
->inject('queueForEvents')
->inject('queueForAudits')
->inject('usage')
->inject('publisherForUsage')
->inject('queueForStatsUsage')
->inject('queueForDeletes')
->inject('queueForDatabase')
->inject('queueForBuilds')
@@ -747,13 +746,11 @@ Http::shutdown()
->inject('authorization')
->inject('timelimit')
->inject('eventProcessor')
->inject('bus')
->inject('apiKey')
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Audit $queueForAudits, Context $usage, UsagePublisher $publisherForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor, Bus $bus, ?Key $apiKey) use ($parseLabel) {
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Audit $queueForAudits, StatsUsage $queueForStatsUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor) use ($parseLabel) {
$responsePayload = $response->getPayload();
if (! empty($queueForEvents->getEvent())) {
if (!empty($queueForEvents->getEvent())) {
if (empty($queueForEvents->getPayload())) {
$queueForEvents->setPayload($responsePayload);
}
@@ -775,7 +772,7 @@ Http::shutdown()
}
// Only trigger functions if there are matching function events
if (! empty($functionsEvents)) {
if (!empty($functionsEvents)) {
foreach ($generatedEvents as $event) {
if (isset($functionsEvents[$event])) {
$queueForFunctions
@@ -787,7 +784,7 @@ Http::shutdown()
}
// Only trigger webhooks if there are matching webhook events
if (! empty($webhooksEvents)) {
if (!empty($webhooksEvents)) {
foreach ($generatedEvents as $event) {
if (isset($webhooksEvents[$event])) {
$queueForWebhooks
@@ -811,7 +808,7 @@ Http::shutdown()
if ($abuseEnabled && \count($abuseResetCode) > 0 && \in_array($response->getStatusCode(), $abuseResetCode)) {
$abuseKeyLabel = $route->getLabel('abuse-key', 'url:{url},ip:{ip}');
$abuseKeyLabel = (! is_array($abuseKeyLabel)) ? [$abuseKeyLabel] : $abuseKeyLabel;
$abuseKeyLabel = (!is_array($abuseKeyLabel)) ? [$abuseKeyLabel] : $abuseKeyLabel;
foreach ($abuseKeyLabel as $abuseKey) {
$start = $request->getContentRangeStart();
@@ -824,10 +821,10 @@ Http::shutdown()
->setParam('{ip}', $request->getIP())
->setParam('{url}', $request->getHostname() . $route->getPath())
->setParam('{method}', $request->getMethod())
->setParam('{chunkId}', (int) ($start / ($end + 1 - $start)));
->setParam('{chunkId}', (int)($start / ($end + 1 - $start)));
foreach ($request->getParams() as $key => $value) { // Set request params as potential abuse keys
if (! empty($value)) {
if (!empty($value)) {
$timeLimit->setParam('{param-' . $key . '}', (\is_array($value)) ? \json_encode($value) : $value);
}
}
@@ -841,14 +838,14 @@ Http::shutdown()
* Audit labels
*/
$pattern = $route->getLabel('audits.resource', null);
if (! empty($pattern)) {
if (!empty($pattern)) {
$resource = $parseLabel($pattern, $responsePayload, $requestParams, $user);
if (! empty($resource) && $resource !== $pattern) {
if (!empty($resource) && $resource !== $pattern) {
$queueForAudits->setResource($resource);
}
}
if (! $user->isEmpty()) {
if (!$user->isEmpty()) {
$userClone = clone $user;
// $user doesn't support `type` and can cause unintended effects.
$userClone->setAttribute('type', ACTIVITY_TYPE_USER);
@@ -874,13 +871,13 @@ Http::shutdown()
$queueForAudits->setUser($user);
}
if (! empty($queueForAudits->getResource()) && ! $queueForAudits->getUser()->isEmpty()) {
if (!empty($queueForAudits->getResource()) && !$queueForAudits->getUser()->isEmpty()) {
/**
* audits.payload is switched to default true
* in order to auto audit payload for all endpoints
*/
$pattern = $route->getLabel('audits.payload', true);
if (! empty($pattern)) {
if (!empty($pattern)) {
$queueForAudits->setPayload($responsePayload);
}
@@ -891,19 +888,19 @@ Http::shutdown()
$queueForAudits->trigger();
}
if (! empty($queueForDeletes->getType())) {
if (!empty($queueForDeletes->getType())) {
$queueForDeletes->trigger();
}
if (! empty($queueForDatabase->getType())) {
if (!empty($queueForDatabase->getType())) {
$queueForDatabase->trigger();
}
if (! empty($queueForBuilds->getType())) {
if (!empty($queueForBuilds->getType())) {
$queueForBuilds->trigger();
}
if (! empty($queueForMessaging->getType())) {
if (!empty($queueForMessaging->getType())) {
$queueForMessaging->trigger();
}
@@ -912,14 +909,14 @@ Http::shutdown()
if ($useCache) {
$resource = $resourceType = null;
$data = $response->getPayload();
if (! empty($data['payload'])) {
if (!empty($data['payload'])) {
$pattern = $route->getLabel('cache.resource', null);
if (! empty($pattern)) {
if (!empty($pattern)) {
$resource = $parseLabel($pattern, $responsePayload, $requestParams, $user);
}
$pattern = $route->getLabel('cache.resourceType', null);
if (! empty($pattern)) {
if (!empty($pattern)) {
$resourceType = $parseLabel($pattern, $responsePayload, $requestParams, $user);
}
@@ -929,7 +926,7 @@ Http::shutdown()
$key = $request->cacheIdentifier();
$signature = md5($data['payload']);
$cacheLog = $authorization->skip(fn () => $dbForProject->getDocument('cache', $key));
$cacheLog = $authorization->skip(fn () => $dbForProject->getDocument('cache', $key));
$accessedAt = $cacheLog->getAttribute('accessedAt', 0);
$now = DateTime::now();
if ($cacheLog->isEmpty()) {
@@ -948,9 +945,7 @@ Http::shutdown()
}
} elseif (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_CACHE_UPDATE)) > $accessedAt) {
$cacheLog->setAttribute('accessedAt', $now);
$authorization->skip(fn () => $dbForProject->updateDocument('cache', $cacheLog->getId(), new Document([
'accessedAt' => $cacheLog->getAttribute('accessedAt')
])));
$authorization->skip(fn () => $dbForProject->updateDocument('cache', $cacheLog->getId(), $cacheLog));
// Overwrite the file every APP_CACHE_UPDATE seconds to update the file modified time that is used in the TTL checks in cache->load()
$cache->save($key, $data['payload']);
}
@@ -962,40 +957,22 @@ Http::shutdown()
}
if ($project->getId() !== 'console') {
if (! User::isPrivileged($authorization->getRoles())) {
$bus->dispatch(new RequestCompleted(
project: $project->getArrayCopy(),
request: $request,
response: $response,
));
}
// Publish usage metrics if context has data
if (! $usage->isEmpty()) {
$metrics = $usage->getMetrics();
// Filter out API key disabled metrics using suffix pattern matching
$disabledMetrics = $apiKey?->getDisabledMetrics() ?? [];
if (! empty($disabledMetrics)) {
$metrics = array_values(array_filter($metrics, function ($metric) use ($disabledMetrics) {
foreach ($disabledMetrics as $pattern) {
if (str_ends_with($metric['key'], $pattern)) {
return false;
}
}
return true;
}));
if (!User::isPrivileged($authorization->getRoles())) {
$fileSize = 0;
$file = $request->getFiles('file');
if (!empty($file)) {
$fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size'];
}
$message = new UsageMessage(
project: $project,
metrics: $metrics,
reduce: $usage->getReduce()
);
$publisherForUsage->enqueue($message);
$queueForStatsUsage
->addMetric(METRIC_NETWORK_REQUESTS, 1)
->addMetric(METRIC_NETWORK_INBOUND, $request->getSize() + $fileSize)
->addMetric(METRIC_NETWORK_OUTBOUND, $response->getSize());
}
$queueForStatsUsage
->setProject($project)
->trigger();
}
});
-7
View File
@@ -188,10 +188,6 @@ $http->on(Constant::EVENT_AFTER_RELOAD, function ($server) {
Console::success('Reload completed...');
});
Http::setResource('bus', function ($register, $utopia) {
return $register->get('bus')->setResolver(fn (string $name) => $utopia->getResource($name));
}, ['register', 'utopia']);
include __DIR__ . '/controllers/general.php';
function createDatabase(Http $app, string $resourceKey, string $dbName, array $collections, mixed $pools, ?callable $extraSetup = null): void
@@ -581,9 +577,6 @@ $http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, Swool
$action = 'UNKNOWN_NAMESPACE.UNKNOWN.METHOD';
if (!empty($sdk)) {
if (\is_array($sdk)) {
$sdk = $sdk[0];
}
/** @var Appwrite\SDK\Method $sdk */
$action = $sdk->getNamespace() . '.' . $sdk->getMethodName();
} elseif ($route === null) {
+3 -7
View File
@@ -93,7 +93,6 @@ const APP_SOCIAL_YOUTUBE = 'https://www.youtube.com/c/appwrite?sub_confirmation=
const APP_COMPUTE_CPUS_DEFAULT = 0.5;
const APP_COMPUTE_MEMORY_DEFAULT = 512;
const APP_COMPUTE_SPECIFICATION_DEFAULT = Specification::S_1VCPU_512MB;
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';
@@ -362,12 +361,6 @@ const METRIC_AVATARS_SCREENSHOTS_GENERATED = 'avatars.screenshotsGenerated';
const METRIC_FUNCTIONS_RUNTIME = 'functions.runtimes.{runtime}';
const METRIC_SITES_FRAMEWORK = 'sites.frameworks.{framework}';
// Realtime metrics
const METRIC_REALTIME_CONNECTIONS = 'realtime.connections';
const METRIC_REALTIME_CONNECTIONS_MESSAGES_SENT = 'realtime.messages.sent';
const METRIC_REALTIME_INBOUND = 'realtime.inbound';
const METRIC_REALTIME_OUTBOUND = 'realtime.outbound';
// Resource types
const RESOURCE_TYPE_PROJECTS = 'projects';
const RESOURCE_TYPE_FUNCTIONS = 'functions';
@@ -399,5 +392,8 @@ const COOKIE_NAME_PREVIEW = 'a_jwt_console';
const CACHE_RECONNECT_MAX_RETRIES = 2;
const CACHE_RECONNECT_RETRY_DELAY = 1000;
/* Web installer */
const LOCAL_API_TIMEOUT = 30 * 1000; // 30 seconds
// Project status
const PROJECT_STATUS_ACTIVE = 'active';
+1 -1
View File
@@ -185,7 +185,7 @@ Response::setModel(new BaseList('Runtimes List', Response::MODEL_RUNTIME_LIST, '
Response::setModel(new BaseList('Deployments List', Response::MODEL_DEPLOYMENT_LIST, 'deployments', Response::MODEL_DEPLOYMENT));
Response::setModel(new BaseList('Executions List', Response::MODEL_EXECUTION_LIST, 'executions', Response::MODEL_EXECUTION));
Response::setModel(new BaseList('Projects List', Response::MODEL_PROJECT_LIST, 'projects', Response::MODEL_PROJECT, true, false));
Response::setModel(new BaseList('Webhooks List', Response::MODEL_WEBHOOK_LIST, 'webhooks', Response::MODEL_WEBHOOK, true, true));
Response::setModel(new BaseList('Webhooks List', Response::MODEL_WEBHOOK_LIST, 'webhooks', Response::MODEL_WEBHOOK, true, false));
Response::setModel(new BaseList('API Keys List', Response::MODEL_KEY_LIST, 'keys', Response::MODEL_KEY, true, true));
Response::setModel(new BaseList('Dev Keys List', Response::MODEL_DEV_KEY_LIST, 'devKeys', Response::MODEL_DEV_KEY, true, false));
Response::setModel(new BaseList('Auth Providers List', Response::MODEL_AUTH_PROVIDER_LIST, 'platforms', Response::MODEL_AUTH_PROVIDER, true, false));
-9
View File
@@ -420,7 +420,6 @@ $register->set('smtp', function () {
$mail->Password = $password;
$mail->SMTPSecure = System::getEnv('_APP_SMTP_SECURE', '');
$mail->SMTPAutoTLS = false;
$mail->SMTPKeepAlive = true;
$mail->CharSet = 'UTF-8';
$mail->Timeout = 10; /* Connection timeout */
$mail->getSMTPInstance()->Timelimit = 30; /* Timeout for each individual SMTP command (e.g. HELO, EHLO, etc.) */
@@ -450,11 +449,3 @@ $register->set('promiseAdapter', function () {
$register->set('hooks', function () {
return new Hooks();
});
$listeners = require __DIR__ . '/../listeners.php';
$register->set('bus', function () use ($listeners) {
$bus = new \Utopia\Bus\Bus();
foreach ($listeners as $listener) {
$bus->subscribe($listener);
}
return $bus;
});
+120 -131
View File
@@ -10,14 +10,15 @@ use Appwrite\Event\Certificate;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Execution;
use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\Migration;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Event\Realtime;
use Appwrite\Event\Screenshot;
use Appwrite\Event\StatsResources;
use Appwrite\Event\StatsUsage;
use Appwrite\Event\Webhook;
use Appwrite\Extend\Exception;
use Appwrite\Functions\EventProcessor;
@@ -26,7 +27,6 @@ use Appwrite\Network\Cors;
use Appwrite\Network\Platform;
use Appwrite\Network\Validator\Origin;
use Appwrite\Network\Validator\Redirect;
use Appwrite\Usage\Context as UsageContext;
use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
@@ -58,7 +58,6 @@ use Utopia\Logger\Log;
use Utopia\Pools\Group;
use Utopia\Queue\Broker\Pool as BrokerPool;
use Utopia\Queue\Publisher;
use Utopia\Queue\Queue;
use Utopia\Storage\Device;
use Utopia\Storage\Device\AWS;
use Utopia\Storage\Device\Backblaze;
@@ -90,7 +89,6 @@ Http::setResource('register', fn () => $register);
Http::setResource('locale', function () {
$locale = new Locale(System::getEnv('_APP_LOCALE', 'en'));
$locale->setFallback(System::getEnv('_APP_LOCALE', 'en'));
return $locale;
});
@@ -111,6 +109,9 @@ Http::setResource('publisherFunctions', function (Publisher $publisher) {
Http::setResource('publisherMigrations', function (Publisher $publisher) {
return $publisher;
}, ['publisher']);
Http::setResource('publisherStatsUsage', function (Publisher $publisher) {
return $publisher;
}, ['publisher']);
Http::setResource('publisherMails', function (Publisher $publisher) {
return $publisher;
}, ['publisher']);
@@ -150,19 +151,18 @@ Http::setResource('queueForWebhooks', function (Publisher $publisher) {
Http::setResource('queueForRealtime', function () {
return new Realtime();
}, []);
Http::setResource('usage', function () {
return new UsageContext();
}, []);
Http::setResource('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher(
$publisher,
new Queue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME))
), ['publisher']);
Http::setResource('queueForStatsUsage', function (Publisher $publisher) {
return new StatsUsage($publisher);
}, ['publisher']);
Http::setResource('queueForAudits', function (Publisher $publisher) {
return new AuditEvent($publisher);
}, ['publisher']);
Http::setResource('queueForFunctions', function (Publisher $publisher) {
return new Func($publisher);
}, ['publisher']);
Http::setResource('queueForExecutions', function (Publisher $publisher) {
return new Execution($publisher);
}, ['publisher']);
Http::setResource('eventProcessor', function () {
return new EventProcessor();
}, []);
@@ -190,14 +190,14 @@ Http::setResource('allowedHostnames', function (array $platform, Document $proje
$allowed = [...($platform['hostnames'] ?? [])];
/* Add platform configured hostnames */
if (! $project->isEmpty() && $project->getId() !== 'console') {
if (!$project->isEmpty() && $project->getId() !== 'console') {
$platforms = $project->getAttribute('platforms', []);
$hostnames = Platform::getHostnames($platforms);
$allowed = [...$allowed, ...$hostnames];
}
/* Add the request hostname if a dev key is found */
if (! $devKey->isEmpty()) {
if (!$devKey->isEmpty()) {
$allowed[] = $request->getHostname();
}
@@ -215,12 +215,12 @@ Http::setResource('allowedHostnames', function (array $platform, Document $proje
}
/* Allow the request origin of rule */
if (! $rule->isEmpty() && ! empty($rule->getAttribute('domain', ''))) {
if (!$rule->isEmpty() && !empty($rule->getAttribute('domain', ''))) {
$allowed[] = $rule->getAttribute('domain', '');
}
/* Allow the request origin if a dev key is found */
if (! $devKey->isEmpty() && ! empty($hostname)) {
if (!$devKey->isEmpty() && !empty($hostname)) {
$allowed[] = $hostname;
}
@@ -233,7 +233,7 @@ Http::setResource('allowedHostnames', function (array $platform, Document $proje
Http::setResource('allowedSchemes', function (array $platform, Document $project) {
$allowed = [...($platform['schemas'] ?? [])];
if (! $project->isEmpty() && $project->getId() !== 'console') {
if (!$project->isEmpty() && $project->getId() !== 'console') {
/* Add hardcoded schemes */
$allowed[] = 'exp';
$allowed[] = 'appwrite-callback-' . $project->getId();
@@ -277,7 +277,7 @@ Http::setResource('rule', function (Request $request, Database $dbForPlatform, D
// Temporary implementation until custom wildcard domains are an official feature
// Allow trusted projects; Used for Console (website) previews
if (! $permitsCurrentProject && ! $rule->isEmpty() && ! empty($rule->getAttribute('projectId', ''))) {
if (!$permitsCurrentProject && !$rule->isEmpty() && !empty($rule->getAttribute('projectId', ''))) {
$trustedProjects = [];
foreach (\explode(',', System::getEnv('_APP_CONSOLE_TRUSTED_PROJECTS', '')) as $trustedProject) {
if (empty($trustedProject)) {
@@ -290,7 +290,7 @@ Http::setResource('rule', function (Request $request, Database $dbForPlatform, D
}
}
if (! $permitsCurrentProject) {
if (!$permitsCurrentProject) {
return new Document();
}
@@ -313,18 +313,16 @@ Http::setResource('cors', function (array $allowedHostnames) {
}, ['allowedHostnames']);
Http::setResource('originValidator', function (Document $devKey, array $allowedHostnames, array $allowedSchemes) {
if (! $devKey->isEmpty()) {
if (!$devKey->isEmpty()) {
return new URL();
}
return new Origin($allowedHostnames, $allowedSchemes);
}, ['devKey', 'allowedHostnames', 'allowedSchemes']);
Http::setResource('redirectValidator', function (Document $devKey, array $allowedHostnames, array $allowedSchemes) {
if (! $devKey->isEmpty()) {
if (!$devKey->isEmpty()) {
return new URL();
}
return new Redirect($allowedHostnames, $allowedSchemes);
}, ['devKey', 'allowedHostnames', 'allowedSchemes']);
@@ -348,11 +346,12 @@ Http::setResource('user', function (string $mode, Document $project, Document $c
* overwriting the previous value.
* 7. If account API key is passed, use user of the account API key as long as user ID header matches too
*/
$authorization->setDefaultStatus(true);
$store->setKey('a_session_' . $project->getId());
if ($mode === APP_MODE_ADMIN) {
if (APP_MODE_ADMIN === $mode) {
$store->setKey('a_session_' . $console->getId());
}
@@ -367,7 +366,7 @@ Http::setResource('user', function (string $mode, Document $project, Document $c
if (empty($store->getProperty('id', '')) && empty($store->getProperty('secret', ''))) {
$sessionHeader = $request->getHeader('x-appwrite-session', '');
if (! empty($sessionHeader)) {
if (!empty($sessionHeader)) {
$store->decode($sessionHeader);
}
}
@@ -387,14 +386,14 @@ Http::setResource('user', function (string $mode, Document $project, Document $c
}
$user = null;
if ($mode === APP_MODE_ADMIN) {
if (APP_MODE_ADMIN === $mode) {
/** @var User $user */
$user = $dbForPlatform->getDocument('users', $store->getProperty('id', ''));
} else {
if ($project->isEmpty()) {
$user = new User([]);
} else {
if (! empty($store->getProperty('id', ''))) {
if (!empty($store->getProperty('id', ''))) {
if ($project->getId() === 'console') {
/** @var User $user */
$user = $dbForPlatform->getDocument('users', $store->getProperty('id', ''));
@@ -407,16 +406,16 @@ Http::setResource('user', function (string $mode, Document $project, Document $c
}
if (
! $user ||
!$user ||
$user->isEmpty() // Check a document has been found in the DB
|| ! $user->sessionVerify($store->getProperty('secret', ''), $proofForToken)
|| !$user->sessionVerify($store->getProperty('secret', ''), $proofForToken)
) { // Validate user has valid login token
$user = new User([]);
}
$authJWT = $request->getHeader('x-appwrite-jwt', '');
if (! empty($authJWT) && ! $project->isEmpty()) { // JWT authentication
if (! $user->isEmpty()) {
if (!empty($authJWT) && !$project->isEmpty()) { // JWT authentication
if (!$user->isEmpty()) {
throw new Exception(Exception::USER_JWT_AND_COOKIE_SET);
}
@@ -428,7 +427,7 @@ Http::setResource('user', function (string $mode, Document $project, Document $c
}
$jwtUserId = $payload['userId'] ?? '';
if (! empty($jwtUserId)) {
if (!empty($jwtUserId)) {
if ($mode === APP_MODE_ADMIN) {
$user = $dbForPlatform->getDocument('users', $jwtUserId);
} else {
@@ -436,7 +435,7 @@ Http::setResource('user', function (string $mode, Document $project, Document $c
}
}
$jwtSessionId = $payload['sessionId'] ?? '';
if (! empty($jwtSessionId)) {
if (!empty($jwtSessionId)) {
if (empty($user->find('$id', $jwtSessionId, 'sessions'))) { // Match JWT to active token
$user = new User([]);
}
@@ -446,22 +445,22 @@ Http::setResource('user', function (string $mode, Document $project, Document $c
// Account based on account API key
$accountKey = $request->getHeader('x-appwrite-key', '');
$accountKeyUserId = $request->getHeader('x-appwrite-user', '');
if (! empty($accountKeyUserId) && ! empty($accountKey)) {
if (! $user->isEmpty()) {
if (!empty($accountKeyUserId) && !empty($accountKey)) {
if (!$user->isEmpty()) {
throw new Exception(Exception::USER_API_KEY_AND_SESSION_SET);
}
$accountKeyUser = $dbForPlatform->getAuthorization()->skip(fn () => $dbForPlatform->getDocument('users', $accountKeyUserId));
if (! $accountKeyUser->isEmpty()) {
if (!$accountKeyUser->isEmpty()) {
$key = $accountKeyUser->find(
key: 'secret',
find: $accountKey,
subject: 'keys'
);
if (! empty($key)) {
if (!empty($key)) {
$expire = $key->getAttribute('expire');
if (! empty($expire) && $expire < DatabaseDateTime::formatTz(DatabaseDateTime::now())) {
if (!empty($expire) && $expire < DatabaseDateTime::formatTz(DatabaseDateTime::now())) {
throw new Exception(Exception::ACCOUNT_KEY_EXPIRED);
}
@@ -480,9 +479,10 @@ Http::setResource('project', function ($dbForPlatform, $request, $console, $auth
/** @var Appwrite\Utopia\Request $request */
/** @var Utopia\Database\Database $dbForPlatform */
/** @var Utopia\Database\Document $console */
$projectId = $request->getParam('project', $request->getHeader('x-appwrite-project', ''));
// Realtime channel "project" can send project=Query array
if (! \is_string($projectId)) {
if (!\is_string($projectId)) {
$projectId = $request->getHeader('x-appwrite-project', '');
}
@@ -503,7 +503,7 @@ Http::setResource('session', function (User $user, Store $store, Token $proofFor
$sessions = $user->getAttribute('sessions', []);
$sessionId = $user->sessionVerify($store->getProperty('secret', ''), $proofForToken);
if (! $sessionId) {
if (!$sessionId) {
return;
}
foreach ($sessions as $session) {
@@ -513,6 +513,7 @@ Http::setResource('session', function (User $user, Store $store, Token $proofFor
}
}
return;
}, ['user', 'store', 'proofForToken']);
Http::setResource('store', function (): Store {
@@ -536,14 +537,12 @@ Http::setResource('proofForPassword', function (): Password {
Http::setResource('proofForToken', function (): Token {
$token = new Token();
$token->setHash(new Sha());
return $token;
});
Http::setResource('proofForCode', function (): Code {
$code = new Code();
$code->setHash(new Sha());
return $code;
});
@@ -555,7 +554,7 @@ Http::setResource('authorization', function () {
return new Authorization();
}, []);
Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatform, Cache $cache, Document $project, Response $response, Publisher $publisher, Publisher $publisherFunctions, Publisher $publisherWebhooks, Event $queueForEvents, Func $queueForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime, UsageContext $usage, Authorization $authorization) {
Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatform, Cache $cache, Document $project, Response $response, Publisher $publisher, Publisher $publisherFunctions, Publisher $publisherWebhooks, Event $queueForEvents, Func $queueForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime, StatsUsage $queueForStatsUsage, Authorization $authorization) {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForPlatform;
}
@@ -589,7 +588,7 @@ Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatfor
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getSequence())
->setTenant((int) $project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
@@ -620,8 +619,9 @@ Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatfor
->from($queueForEvents)
->trigger();
/** Trigger webhooks events only if a project has them enabled */
if (! empty($project->getAttribute('webhooks'))) {
if (!empty($project->getAttribute('webhooks'))) {
$queueForWebhooks
->from($queueForEvents)
->trigger();
@@ -640,6 +640,7 @@ Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatfor
*/
$functionsEventsCacheListener = function (string $event, Document $document, Document $project, Database $dbForProject) {
if ($document->getCollection() !== 'functions') {
return;
}
@@ -661,7 +662,7 @@ Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatfor
$dbForProject->getCache()->purge($cacheKey);
};
$usageDatabaseListener = function (string $event, Document $document, UsageContext $usage) {
$usageDatabaseListener = function (string $event, Document $document, StatsUsage $queueForStatsUsage) {
$value = 1;
switch ($event) {
@@ -681,78 +682,81 @@ Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatfor
switch (true) {
case $document->getCollection() === 'teams':
$usage->addMetric(METRIC_TEAMS, $value); // per project
$queueForStatsUsage->addMetric(METRIC_TEAMS, $value); // per project
break;
case $document->getCollection() === 'users':
$usage->addMetric(METRIC_USERS, $value); // per project
$queueForStatsUsage->addMetric(METRIC_USERS, $value); // per project
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$usage->addReduce($document);
$queueForStatsUsage->addReduce($document);
}
break;
case $document->getCollection() === 'sessions': // sessions
$usage->addMetric(METRIC_SESSIONS, $value); // per project
$queueForStatsUsage->addMetric(METRIC_SESSIONS, $value); //per project
break;
case $document->getCollection() === 'databases': // databases
$usage->addMetric(METRIC_DATABASES, $value); // per project
$queueForStatsUsage->addMetric(METRIC_DATABASES, $value); // per project
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$usage->addReduce($document);
$queueForStatsUsage->addReduce($document);
}
break;
case str_starts_with($document->getCollection(), 'database_') && ! str_contains($document->getCollection(), 'collection'): // collections
case str_starts_with($document->getCollection(), 'database_') && !str_contains($document->getCollection(), 'collection'): //collections
$parts = explode('_', $document->getCollection());
$databaseInternalId = $parts[1] ?? 0;
$usage
$queueForStatsUsage
->addMetric(METRIC_COLLECTIONS, $value) // per project
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_COLLECTIONS), $value);
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$usage->addReduce($document);
$queueForStatsUsage->addReduce($document);
}
break;
case str_starts_with($document->getCollection(), 'database_') && str_contains($document->getCollection(), '_collection_'): // documents
case str_starts_with($document->getCollection(), 'database_') && str_contains($document->getCollection(), '_collection_'): //documents
$parts = explode('_', $document->getCollection());
$databaseInternalId = $parts[1] ?? 0;
$databaseInternalId = $parts[1] ?? 0;
$collectionInternalId = $parts[3] ?? 0;
$usage
$queueForStatsUsage
->addMetric(METRIC_DOCUMENTS, $value) // per project
->addMetric(str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_DOCUMENTS), $value) // per database
->addMetric(str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $collectionInternalId], METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS), $value); // per collection
break;
case $document->getCollection() === 'buckets': // buckets
$usage->addMetric(METRIC_BUCKETS, $value); // per project
case $document->getCollection() === 'buckets': //buckets
$queueForStatsUsage
->addMetric(METRIC_BUCKETS, $value); // per project
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$usage
$queueForStatsUsage
->addReduce($document);
}
break;
case str_starts_with($document->getCollection(), 'bucket_'): // files
$parts = explode('_', $document->getCollection());
$bucketInternalId = $parts[1];
$usage
$bucketInternalId = $parts[1];
$queueForStatsUsage
->addMetric(METRIC_FILES, $value) // per project
->addMetric(METRIC_FILES_STORAGE, $document->getAttribute('sizeOriginal') * $value) // per project
->addMetric(str_replace('{bucketInternalId}', $bucketInternalId, METRIC_BUCKET_ID_FILES), $value) // per bucket
->addMetric(str_replace('{bucketInternalId}', $bucketInternalId, METRIC_BUCKET_ID_FILES_STORAGE), $document->getAttribute('sizeOriginal') * $value); // per bucket
break;
case $document->getCollection() === 'functions':
$usage->addMetric(METRIC_FUNCTIONS, $value); // per project
$queueForStatsUsage
->addMetric(METRIC_FUNCTIONS, $value); // per project
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$usage
$queueForStatsUsage
->addReduce($document);
}
break;
case $document->getCollection() === 'sites':
$usage->addMetric(METRIC_SITES, $value); // per project
$queueForStatsUsage
->addMetric(METRIC_SITES, $value); // per project
if ($event === Database::EVENT_DOCUMENT_DELETE) {
$usage
$queueForStatsUsage
->addReduce($document);
}
break;
case $document->getCollection() === 'deployments':
$usage
$queueForStatsUsage
->addMetric(METRIC_DEPLOYMENTS, $value) // per project
->addMetric(METRIC_DEPLOYMENTS_STORAGE, $document->getAttribute('size') * $value) // per project
->addMetric(str_replace(['{resourceType}'], [$document->getAttribute('resourceType')], METRIC_RESOURCE_TYPE_DEPLOYMENTS), $value) // per function
@@ -772,27 +776,30 @@ Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatfor
$queueForWebhooks = new Webhook($publisherWebhooks);
$queueForRealtime = new Realtime();
$database
->on(Database::EVENT_DOCUMENT_CREATE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $usage))
->on(Database::EVENT_DOCUMENT_DELETE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $usage))
->on(Database::EVENT_DOCUMENTS_CREATE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $usage))
->on(Database::EVENT_DOCUMENTS_DELETE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $usage))
->on(Database::EVENT_DOCUMENTS_UPSERT, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $usage))
->on(Database::EVENT_DOCUMENT_CREATE, 'create-trigger-events', fn ($event, $document) => $eventDatabaseListener(
$project,
$document,
$response,
$queueForEventsClone->from($queueForEvents),
$queueForFunctions->from($queueForEvents),
$queueForWebhooks->from($queueForEvents),
$queueForRealtime->from($queueForEvents)
))
->on(Database::EVENT_DOCUMENT_CREATE, 'purge-function-events-cache', fn ($event, $document) => $functionsEventsCacheListener($event, $document, $project, $database))
->on(Database::EVENT_DOCUMENT_UPDATE, 'purge-function-events-cache', fn ($event, $document) => $functionsEventsCacheListener($event, $document, $project, $database))
->on(Database::EVENT_DOCUMENT_DELETE, 'purge-function-events-cache', fn ($event, $document) => $functionsEventsCacheListener($event, $document, $project, $database));
->on(Database::EVENT_DOCUMENT_CREATE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $queueForStatsUsage))
->on(Database::EVENT_DOCUMENT_DELETE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $queueForStatsUsage))
->on(Database::EVENT_DOCUMENTS_CREATE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $queueForStatsUsage))
->on(Database::EVENT_DOCUMENTS_DELETE, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $queueForStatsUsage))
->on(Database::EVENT_DOCUMENTS_UPSERT, 'calculate-usage', fn ($event, $document) => $usageDatabaseListener($event, $document, $queueForStatsUsage))
->on(Database::EVENT_DOCUMENT_CREATE, 'create-trigger-events', fn ($event, $document) => $eventDatabaseListener(
$project,
$document,
$response,
$queueForEventsClone->from($queueForEvents),
$queueForFunctions->from($queueForEvents),
$queueForWebhooks->from($queueForEvents),
$queueForRealtime->from($queueForEvents)
))
->on(Database::EVENT_DOCUMENT_CREATE, 'purge-function-events-cache', fn ($event, $document) => $functionsEventsCacheListener($event, $document, $project, $database))
->on(Database::EVENT_DOCUMENT_UPDATE, 'purge-function-events-cache', fn ($event, $document) => $functionsEventsCacheListener($event, $document, $project, $database))
->on(Database::EVENT_DOCUMENT_DELETE, 'purge-function-events-cache', fn ($event, $document) => $functionsEventsCacheListener($event, $document, $project, $database))
;
return $database;
}, ['pools', 'dbForPlatform', 'cache', 'project', 'response', 'publisher', 'publisherFunctions', 'publisherWebhooks', 'queueForEvents', 'queueForFunctions', 'queueForWebhooks', 'queueForRealtime', 'usage', 'authorization']);
}, ['pools', 'dbForPlatform', 'cache', 'project', 'response', 'publisher', 'publisherFunctions', 'publisherWebhooks', 'queueForEvents', 'queueForFunctions', 'queueForWebhooks', 'queueForRealtime', 'queueForStatsUsage', 'authorization']);
Http::setResource('dbForPlatform', function (Group $pools, Cache $cache, Authorization $authorization) {
@@ -841,14 +848,15 @@ Http::setResource('getProjectDB', function (Group $pools, Database $dbForPlatfor
->setMetadata('project', $project->getId())
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_API)
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES)
->setDocumentType('users', User::class);
->setDocumentType('users', User::class)
;
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getSequence())
->setTenant((int) $project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
@@ -861,7 +869,6 @@ Http::setResource('getProjectDB', function (Group $pools, Database $dbForPlatfor
if (isset($databases[$dsn->getHost()])) {
$database = $databases[$dsn->getHost()];
$configure($database);
return $database;
}
@@ -879,7 +886,7 @@ Http::setResource('getLogsDB', function (Group $pools, Cache $cache, Authorizati
return function (?Document $project = null) use ($pools, $cache, $authorization, &$database) {
if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant($project->getSequence());
$database->setTenant((int) $project->getSequence());
return $database;
}
@@ -896,7 +903,7 @@ Http::setResource('getLogsDB', function (Group $pools, Cache $cache, Authorizati
// set tenant
if ($project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant($project->getSequence());
$database->setTenant((int) $project->getSequence());
}
return $database;
@@ -905,7 +912,6 @@ Http::setResource('getLogsDB', function (Group $pools, Cache $cache, Authorizati
Http::setResource('audit', function ($dbForProject) {
$adapter = new AdapterDatabase($dbForProject);
return new Audit($adapter);
}, ['dbForProject']);
@@ -921,7 +927,6 @@ Http::setResource('cache', function (Group $pools, Telemetry $telemetry) {
$cache = new Cache(new Sharding($adapters));
$cache->setTelemetry($telemetry);
return $cache;
}, ['pools', 'telemetry']);
@@ -967,9 +972,9 @@ Http::setResource('deviceForBuilds', function ($project, Telemetry $telemetry) {
function getDevice(string $root, string $connection = ''): Device
{
$connection = ! empty($connection) ? $connection : System::getEnv('_APP_CONNECTIONS_STORAGE', '');
$connection = !empty($connection) ? $connection : System::getEnv('_APP_CONNECTIONS_STORAGE', '');
if (! empty($connection)) {
if (!empty($connection)) {
$acl = 'private';
$device = Storage::DEVICE_LOCAL;
$accessKey = '';
@@ -991,9 +996,8 @@ function getDevice(string $root, string $connection = ''): Device
switch ($device) {
case Storage::DEVICE_S3:
if (! empty($url)) {
$bucketRoot = (! empty($bucket) ? $bucket . '/' : '') . \ltrim($root, '/');
if (!empty($url)) {
$bucketRoot = (!empty($bucket) ? $bucket . '/' : '') . \ltrim($root, '/');
return new S3($bucketRoot, $accessKey, $accessSecret, $url, $region, $acl);
} else {
return new AWS($root, $accessKey, $accessSecret, $bucket, $region, $acl);
@@ -1002,7 +1006,6 @@ function getDevice(string $root, string $connection = ''): Device
case STORAGE::DEVICE_DO_SPACES:
$device = new DOSpaces($root, $accessKey, $accessSecret, $bucket, $region, $acl);
$device->setHttpVersion(S3::HTTP_VERSION_1_1);
return $device;
case Storage::DEVICE_BACKBLAZE:
return new Backblaze($root, $accessKey, $accessSecret, $bucket, $region, $acl);
@@ -1026,9 +1029,8 @@ function getDevice(string $root, string $connection = ''): Device
$s3Bucket = System::getEnv('_APP_STORAGE_S3_BUCKET', '');
$s3Acl = 'private';
$s3EndpointUrl = System::getEnv('_APP_STORAGE_S3_ENDPOINT', '');
if (! empty($s3EndpointUrl)) {
$bucketRoot = (! empty($s3Bucket) ? $s3Bucket . '/' : '') . \ltrim($root, '/');
if (!empty($s3EndpointUrl)) {
$bucketRoot = (!empty($s3Bucket) ? $s3Bucket . '/' : '') . \ltrim($root, '/');
return new S3($bucketRoot, $s3AccessKey, $s3SecretKey, $s3EndpointUrl, $s3Region, $s3Acl);
} else {
return new AWS($root, $s3AccessKey, $s3SecretKey, $s3Bucket, $s3Region, $s3Acl);
@@ -1042,7 +1044,6 @@ function getDevice(string $root, string $connection = ''): Device
$doSpacesAcl = 'private';
$device = new DOSpaces($root, $doSpacesAccessKey, $doSpacesSecretKey, $doSpacesBucket, $doSpacesRegion, $doSpacesAcl);
$device->setHttpVersion(S3::HTTP_VERSION_1_1);
return $device;
case Storage::DEVICE_BACKBLAZE:
$backblazeAccessKey = System::getEnv('_APP_STORAGE_BACKBLAZE_ACCESS_KEY', '');
@@ -1050,7 +1051,6 @@ function getDevice(string $root, string $connection = ''): Device
$backblazeRegion = System::getEnv('_APP_STORAGE_BACKBLAZE_REGION', '');
$backblazeBucket = System::getEnv('_APP_STORAGE_BACKBLAZE_BUCKET', '');
$backblazeAcl = 'private';
return new Backblaze($root, $backblazeAccessKey, $backblazeSecretKey, $backblazeBucket, $backblazeRegion, $backblazeAcl);
case Storage::DEVICE_LINODE:
$linodeAccessKey = System::getEnv('_APP_STORAGE_LINODE_ACCESS_KEY', '');
@@ -1058,7 +1058,6 @@ function getDevice(string $root, string $connection = ''): Device
$linodeRegion = System::getEnv('_APP_STORAGE_LINODE_REGION', '');
$linodeBucket = System::getEnv('_APP_STORAGE_LINODE_BUCKET', '');
$linodeAcl = 'private';
return new Linode($root, $linodeAccessKey, $linodeSecretKey, $linodeBucket, $linodeRegion, $linodeAcl);
case Storage::DEVICE_WASABI:
$wasabiAccessKey = System::getEnv('_APP_STORAGE_WASABI_ACCESS_KEY', '');
@@ -1066,7 +1065,6 @@ function getDevice(string $root, string $connection = ''): Device
$wasabiRegion = System::getEnv('_APP_STORAGE_WASABI_REGION', '');
$wasabiBucket = System::getEnv('_APP_STORAGE_WASABI_BUCKET', '');
$wasabiAcl = 'private';
return new Wasabi($root, $wasabiAccessKey, $wasabiSecretKey, $wasabiBucket, $wasabiRegion, $wasabiAcl);
}
}
@@ -1093,6 +1091,7 @@ Http::setResource('passwordsDictionary', function ($register) {
return $register->get('passwordsDictionary');
}, ['register']);
Http::setResource('servers', function () {
$platforms = Config::getParam('sdks');
$server = $platforms[APP_SDK_PLATFORM_SERVER];
@@ -1200,17 +1199,16 @@ Http::setResource('gitHub', function (Cache $cache) {
}, ['cache']);
Http::setResource('requestTimestamp', function ($request) {
// TODO: Move this to the Request class itself
//TODO: Move this to the Request class itself
$timestampHeader = $request->getHeader('x-appwrite-timestamp');
$requestTimestamp = null;
if (! empty($timestampHeader)) {
if (!empty($timestampHeader)) {
try {
$requestTimestamp = new \DateTime($timestampHeader);
} catch (\Throwable $e) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Invalid X-Appwrite-Timestamp header value');
}
}
return $requestTimestamp;
}, ['request']);
@@ -1227,13 +1225,13 @@ Http::setResource('devKey', function (Request $request, Document $project, array
// Check if given key match project's development keys
$key = $project->find('secret', $devKey, 'devKeys');
if (! $key) {
if (!$key) {
return new Document([]);
}
// check expiration
$expire = $key->getAttribute('expire');
if (! empty($expire) && $expire < DatabaseDateTime::formatTz(DatabaseDateTime::now())) {
if (!empty($expire) && $expire < DatabaseDateTime::formatTz(DatabaseDateTime::now())) {
return new Document([]);
}
@@ -1241,9 +1239,7 @@ Http::setResource('devKey', function (Request $request, Document $project, array
$accessedAt = $key->getAttribute('accessedAt', 0);
if (empty($accessedAt) || DatabaseDateTime::formatTz(DatabaseDateTime::addSeconds(new \DateTime(), -APP_KEY_ACCESS)) > $accessedAt) {
$key->setAttribute('accessedAt', DatabaseDateTime::now());
$authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), new Document([
'accessedAt' => $key->getAttribute('accessedAt')
])));
$authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), $key));
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
}
@@ -1254,16 +1250,13 @@ Http::setResource('devKey', function (Request $request, Document $project, array
if ($sdk !== 'UNKNOWN' && $sdkValidator->isValid($sdk)) {
$sdks = $key->getAttribute('sdks', []);
if (! in_array($sdk, $sdks)) {
if (!in_array($sdk, $sdks)) {
$sdks[] = $sdk;
$key->setAttribute('sdks', $sdks);
/** Update access time as well */
$key->setAttribute('accessedAt', DatabaseDateTime::now());
$key = $authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), new Document([
'sdks' => $key->getAttribute('sdks'),
'accessedAt' => $key->getAttribute('accessedAt')
])));
$key = $authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), $key));
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
}
}
@@ -1277,7 +1270,7 @@ Http::setResource('team', function (Document $project, Database $dbForPlatform,
$teamInternalId = $project->getAttribute('teamInternalId', '');
} else {
$route = $utopia->match($request);
$path = ! empty($route) ? $route->getPath() : $request->getURI();
$path = !empty($route) ? $route->getPath() : $request->getURI();
$orgHeader = $request->getHeader('x-appwrite-organization', '');
if (str_starts_with($path, '/v1/projects/:projectId')) {
$uri = $request->getURI();
@@ -1292,9 +1285,8 @@ Http::setResource('team', function (Document $project, Database $dbForPlatform,
}
$team = $authorization->skip(fn () => $dbForPlatform->getDocument('teams', $teamId));
return $team;
} elseif (! empty($orgHeader)) {
} elseif (!empty($orgHeader)) {
return $authorization->skip(fn () => $dbForPlatform->getDocument('teams', $orgHeader));
}
}
@@ -1324,13 +1316,13 @@ Http::setResource('previewHostname', function (Request $request, ?Key $apiKey) {
if (Http::isDevelopment()) {
$allowed = true;
} elseif (! \is_null($apiKey) && $apiKey->getHostnameOverride() === true) {
} elseif (!\is_null($apiKey) && $apiKey->getHostnameOverride() === true) {
$allowed = true;
}
if ($allowed) {
$host = $request->getQuery('appwrite-hostname', $request->getHeader('x-appwrite-hostname', '')) ?? '';
if (! empty($host)) {
if (!empty($host)) {
return $host;
}
}
@@ -1351,19 +1343,19 @@ Http::setResource('apiKey', function (Request $request, Document $project, Docum
$organizationHeader = $request->getHeader('x-appwrite-organization');
$projectHeader = $request->getHeader('x-appwrite-project');
if (! empty($key->getProjectId())) {
if (!empty($key->getProjectId())) {
if (empty($projectHeader) || $projectHeader !== $key->getProjectId()) {
throw new Exception(Exception::PROJECT_ID_MISSING);
}
}
if (! empty($key->getUserId())) {
if (!empty($key->getUserId())) {
if (empty($userHeader) || $userHeader !== $key->getUserId()) {
throw new Exception(Exception::USER_ID_MISSING);
}
}
if (! empty($key->getTeamId())) {
if (!empty($key->getTeamId())) {
if (empty($organizationHeader) || $organizationHeader !== $key->getTeamId()) {
throw new Exception(Exception::ORGANIZATION_ID_MISSING);
}
@@ -1377,7 +1369,7 @@ Http::setResource('executor', fn () => new Executor());
Http::setResource('resourceToken', function ($project, $dbForProject, $request, Authorization $authorization) {
$tokenJWT = $request->getParam('token');
if (! empty($tokenJWT) && ! $project->isEmpty()) { // JWT authentication
if (!empty($tokenJWT) && !$project->isEmpty()) { // JWT authentication
// Use a large but reasonable maxAge to avoid auto-exp when token has no expiry
$jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), RESOURCE_TOKEN_ALGORITHM, RESOURCE_TOKEN_MAX_AGE, RESOURCE_TOKEN_LEEWAY); // Instantiate with key, algo, maxAge and leeway.
@@ -1421,9 +1413,7 @@ Http::setResource('resourceToken', function ($project, $dbForProject, $request,
$accessedAt = $token->getAttribute('accessedAt', 0);
if (empty($accessedAt) || DatabaseDateTime::formatTz(DatabaseDateTime::addSeconds(new \DateTime(), -APP_RESOURCE_TOKEN_ACCESS)) > $accessedAt) {
$token->setAttribute('accessedAt', DatabaseDateTime::now());
$authorization->skip(fn () => $dbForProject->updateDocument('resourceTokens', $token->getId(), new Document([
'accessedAt' => $token->getAttribute('accessedAt')
])));
$authorization->skip(fn () => $dbForProject->updateDocument('resourceTokens', $token->getId(), $token));
}
return new Document([
@@ -1437,7 +1427,6 @@ Http::setResource('resourceToken', function ($project, $dbForProject, $request,
default => throw new Exception(Exception::TOKEN_RESOURCE_TYPE_INVALID),
};
}
return new Document([]);
}, ['project', 'dbForProject', 'request', 'authorization']);
+1 -6
View File
@@ -5,9 +5,4 @@ use Utopia\Span\Span;
use Utopia\Span\Storage;
Span::setStorage(new Storage\Coroutine());
Span::addExporter(new Exporter\Pretty(), function (Span $span): bool {
if (\str_starts_with($span->getAction(), 'listener.')) {
return $span->getError() !== null;
}
return true;
});
Span::addExporter(new Exporter\Pretty());
-9
View File
@@ -1,9 +0,0 @@
<?php
use Appwrite\Bus\Listeners\Log;
use Appwrite\Bus\Listeners\Usage;
return [
new Log(),
new Usage(),
];
+17 -174
View File
@@ -50,16 +50,6 @@ require_once __DIR__ . '/init.php';
Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
// Log uncaught exceptions in one line instead of relying on Swoole's full backtrace dump
set_exception_handler(function (\Throwable $e) {
Console::error(sprintf(
'Realtime uncaught exception: %s in %s:%d',
$e->getMessage(),
$e->getFile(),
$e->getLine()
));
});
// Allows overriding
if (!function_exists('getConsoleDB')) {
function getConsoleDB(): Database
@@ -125,7 +115,7 @@ if (!function_exists('getProjectDB')) {
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getSequence())
->setTenant((int)$project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
@@ -234,13 +224,6 @@ if (!function_exists('getTelemetry')) {
}
}
if (!function_exists('triggerStats')) {
function triggerStats(array $event, string $projectId): void
{
return;
}
}
$realtime = getRealtime();
/**
@@ -252,8 +235,6 @@ $stats->column('teamId', Table::TYPE_STRING, 64);
$stats->column('connections', Table::TYPE_INT);
$stats->column('connectionsTotal', Table::TYPE_INT);
$stats->column('messages', Table::TYPE_INT);
$stats->column('inbound', Table::TYPE_INT);
$stats->column('outbound', Table::TYPE_INT);
$stats->create();
$containerId = uniqid();
@@ -375,10 +356,7 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume
->setAttribute('timestamp', DateTime::now())
->setAttribute('value', json_encode($payload));
$database->getAuthorization()->skip(fn () => $database->updateDocument('realtime', $statsDocument->getId(), new Document([
'timestamp' => $statsDocument->getAttribute('timestamp'),
'value' => $statsDocument->getAttribute('value')
])));
$database->getAuthorization()->skip(fn () => $database->updateDocument('realtime', $statsDocument->getId(), $statsDocument));
} catch (Throwable $th) {
logError($th, "updateWorkerDocument");
}
@@ -398,7 +376,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
$attempts = 0;
$start = time();
Timer::tick(5000, function () use ($server, $register, $realtime, $stats, $workerId) {
Timer::tick(5000, function () use ($server, $register, $realtime, $stats) {
/**
* Sending current connections to project channels on the console project every 5 seconds.
*/
@@ -487,56 +465,6 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
]));
}
}
/** Push only deltas to usage queue (messages, inbound, outbound). Reset after so next tick sends only new activity. */
$consoleDatabase = getConsoleDB();
foreach ($stats as $projectId => $row) {
$messages = (int) ($row['messages'] ?? 0);
$inbound = (int) ($row['inbound'] ?? 0);
$outbound = (int) ($row['outbound'] ?? 0);
if ($messages === 0 && $inbound === 0 && $outbound === 0) {
continue;
}
$project = $consoleDatabase
->getAuthorization()
->skip(fn () => $consoleDatabase->getDocument('projects', $projectId));
if ($project->isEmpty()) {
continue;
}
$queueForStatsUsage = getQueueForStatsUsageForProject($project);
if ($messages > 0) {
$queueForStatsUsage->addMetric(METRIC_REALTIME_CONNECTIONS_MESSAGES_SENT, $messages);
}
if ($inbound > 0) {
$queueForStatsUsage->addMetric(METRIC_REALTIME_INBOUND, $inbound);
}
if ($outbound > 0) {
$queueForStatsUsage->addMetric(METRIC_REALTIME_OUTBOUND, $outbound);
}
if (Http::isDevelopment()) {
Console::info("[Debug][Worker {$workerId}] Project: {$projectId}, Inbound: {$inbound}, Outbound: {$outbound}, Messages: {$messages}");
}
$queueForStatsUsage->trigger();
// Reset so next tick only sends new deltas (avoids double-counting)
if ($messages > 0) {
$stats->decr($projectId, 'messages', $messages);
}
if ($inbound > 0) {
$stats->decr($projectId, 'inbound', $inbound);
}
if ($outbound > 0) {
$stats->decr($projectId, 'outbound', $outbound);
}
}
});
while ($attempts < 300) {
@@ -617,41 +545,20 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
}
$total = 0;
$outboundBytes = 0;
foreach ($groups as $group) {
$data = $event['data'];
$data['subscriptions'] = $group['subscriptions'];
$payloadJson = json_encode([
$server->send($group['ids'], json_encode([
'type' => 'event',
'data' => $data
]);
$server->send($group['ids'], $payloadJson);
$count = count($group['ids']);
$total += $count;
$outboundBytes += strlen($payloadJson) * $count;
]));
$total += count($group['ids']);
}
if ($total > 0) {
$register->get('telemetry.messageSentCounter')->add($total);
$stats->incr($event['project'], 'messages', $total);
$projectId = $event['project'] ?? null;
if (!empty($projectId)) {
$metrics = [
METRIC_REALTIME_CONNECTIONS_MESSAGES_SENT => $total,
];
if ($outboundBytes > 0) {
$metrics[METRIC_REALTIME_OUTBOUND] = $outboundBytes;
}
triggerStats($metrics, $projectId);
}
}
});
} catch (Throwable $th) {
@@ -728,12 +635,6 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
throw new Exception(Exception::REALTIME_TOO_MANY_MESSAGES, 'Too many requests');
}
$rawSize = $request->getSize();
triggerStats([
METRIC_REALTIME_INBOUND => $rawSize,
], $project->getId());
/*
* Validate Client Domain - Check to avoid CSRF attack.
* Adding Appwrite API domains to allow XDOMAIN communication.
@@ -788,16 +689,14 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
$user = empty($user->getId()) ? null : $response->output($user, Response::MODEL_ACCOUNT);
$connectedPayloadJson = json_encode([
$server->send([$connection], json_encode([
'type' => 'connected',
'data' => [
'channels' => $names,
'subscriptions' => $mapping,
'user' => $user
]
]);
$server->send([$connection], $connectedPayloadJson);
]));
$register->get('telemetry.connectionCounter')->add(1);
$register->get('telemetry.connectionCreatedCounter')->add(1);
@@ -808,12 +707,6 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
]);
$stats->incr($project->getId(), 'connections');
$stats->incr($project->getId(), 'connectionsTotal');
$connectedOutboundBytes = \strlen($connectedPayloadJson);
triggerStats([METRIC_REALTIME_CONNECTIONS => 1, METRIC_REALTIME_OUTBOUND => $connectedOutboundBytes], $project->getId());
} catch (Throwable $th) {
logError($th, 'realtime', project: $project, user: $logUser, authorization: $authorization);
@@ -850,12 +743,11 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
}
});
$server->onMessage(function (int $connection, string $message) use ($server, $register, $realtime, $containerId, $stats) {
$server->onMessage(function (int $connection, string $message) use ($server, $register, $realtime, $containerId) {
$project = null;
$authorization = null;
try {
$rawSize = \strlen($message);
$response = new Response(new SwooleResponse());
$projectId = $realtime->connections[$connection]['projectId'] ?? null;
@@ -868,7 +760,7 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
$database = getConsoleDB();
$database->setAuthorization($authorization);
if (!empty($projectId) && $projectId !== 'console') {
if ($projectId !== 'console') {
$project = $authorization->skip(fn () => $database->getDocument('projects', $projectId));
$database = getProjectDB($project);
@@ -894,41 +786,17 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
throw new Exception(Exception::REALTIME_TOO_MANY_MESSAGES, 'Too many messages.');
}
// Record realtime inbound bytes for this project
if ($project !== null && !$project->isEmpty()) {
triggerStats([
METRIC_REALTIME_INBOUND => $rawSize,
], $project->getId());
}
$message = json_decode($message, true);
if (is_null($message) || (!array_key_exists('type', $message) && !array_key_exists('data', $message))) {
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Message format is not valid.');
}
// Ping does not require project context; other messages do (e.g. after unsubscribe during auth)
if (empty($projectId) && ($message['type'] ?? '') !== 'ping') {
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, 'Missing project context. Reconnect to the project first.');
}
switch ($message['type']) {
case 'ping':
$pongPayloadJson = json_encode([
$server->send([$connection], json_encode([
'type' => 'pong'
]);
$server->send([$connection], $pongPayloadJson);
if ($project !== null && !$project->isEmpty()) {
$pongOutboundBytes = \strlen($pongPayloadJson);
if ($pongOutboundBytes > 0) {
triggerStats([
METRIC_REALTIME_OUTBOUND => $pongOutboundBytes,
], $project->getId());
}
}
]));
break;
case 'authentication':
@@ -989,27 +857,14 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
}
$user = $response->output($user, Response::MODEL_ACCOUNT);
$authResponsePayloadJson = json_encode([
$server->send([$connection], json_encode([
'type' => 'response',
'data' => [
'to' => 'authentication',
'success' => true,
'user' => $user
]
]);
$server->send([$connection], $authResponsePayloadJson);
if ($project !== null && !$project->isEmpty()) {
$authOutboundBytes = \strlen($authResponsePayloadJson);
if ($authOutboundBytes > 0) {
triggerStats([
METRIC_REALTIME_OUTBOUND => $authOutboundBytes,
], $project->getId());
}
}
]));
break;
@@ -1047,21 +902,9 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
});
$server->onClose(function (int $connection) use ($realtime, $stats, $register) {
try {
if (array_key_exists($connection, $realtime->connections)) {
$stats->decr($realtime->connections[$connection]['projectId'], 'connectionsTotal');
$register->get('telemetry.connectionCounter')->add(-1);
$projectId = $realtime->connections[$connection]['projectId'];
triggerStats([
METRIC_REALTIME_CONNECTIONS => -1,
], $projectId);
}
} catch (\Throwable $th) {
// Log only; do not rethrow. If we let this bubble, Swoole dumps full coroutine
// backtraces and unsubscribe() below would never run (connection cleanup would fail).
Console::error('Realtime onClose error: ' . $th->getMessage());
if (array_key_exists($connection, $realtime->connections)) {
$stats->decr($realtime->connections[$connection]['projectId'], 'connectionsTotal');
$register->get('telemetry.connectionCounter')->add(-1);
}
$realtime->unsubscribe($connection);
+39 -89
View File
@@ -68,7 +68,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- traefik.http.routers.appwrite_api_https.tls=true
volumes:
<?php if ($version === 'local' && !empty($hostPath)): ?>
- "<?php echo $hostPath; ?>:/usr/src/code:rw"
- <?php echo $hostPath; ?>:/usr/src/code:rw
<?php endif; ?>
- appwrite-uploads:/storage/uploads:rw
- appwrite-imports:/storage/imports:rw
@@ -79,10 +79,8 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- appwrite-sites:/storage/sites:rw
- appwrite-builds:/storage/builds:rw
depends_on:
redis:
condition: service_healthy
<?= $dbService ?>:
condition: service_healthy
- redis
- <?= $dbService . "\n" ?>
# - clamav
environment:
- _APP_ENV
@@ -236,10 +234,8 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
networks:
- appwrite
depends_on:
redis:
condition: service_healthy
<?= $dbService ?>:
condition: service_healthy
- redis
- <?= $dbService . "\n" ?>
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -269,10 +265,8 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
networks:
- appwrite
depends_on:
redis:
condition: service_healthy
<?= $dbService ?>:
condition: service_healthy
- redis
- <?= $dbService . "\n" ?>
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -299,10 +293,8 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
networks:
- appwrite
depends_on:
redis:
condition: service_healthy
<?= $dbService ?>:
condition: service_healthy
- redis
- <?= $dbService . "\n" ?>
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -331,10 +323,8 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
networks:
- appwrite
depends_on:
redis:
condition: service_healthy
<?= $dbService ?>:
condition: service_healthy
- redis
- <?= $dbService . "\n" ?>
volumes:
- appwrite-uploads:/storage/uploads:rw
- appwrite-cache:/storage/cache:rw
@@ -398,10 +388,8 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
networks:
- appwrite
depends_on:
redis:
condition: service_healthy
<?= $dbService ?>:
condition: service_healthy
- redis
- <?= $dbService . "\n" ?>
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -428,10 +416,8 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
networks:
- appwrite
depends_on:
redis:
condition: service_healthy
<?= $dbService ?>:
condition: service_healthy
- redis
- <?= $dbService . "\n" ?>
volumes:
- appwrite-functions:/storage/functions:rw
- appwrite-sites:/storage/sites:rw
@@ -500,10 +486,8 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
networks:
- appwrite
depends_on:
redis:
condition: service_healthy
<?= $dbService ?>:
condition: service_healthy
- redis
- <?= $dbService . "\n" ?>
volumes:
- appwrite-config:/storage/config:rw
- appwrite-certificates:/storage/certificates:rw
@@ -541,12 +525,9 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
networks:
- appwrite
depends_on:
redis:
condition: service_healthy
<?= $dbService ?>:
condition: service_healthy
openruntimes-executor:
condition: service_started
- redis
- <?= $dbService . "\n" ?>
- openruntimes-executor
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -585,10 +566,8 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
networks:
- appwrite
depends_on:
redis:
condition: service_healthy
<?= $dbService ?>:
condition: service_healthy
- redis
- <?= $dbService . "\n" ?>
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -626,10 +605,8 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
volumes:
- appwrite-uploads:/storage/uploads:rw
depends_on:
redis:
condition: service_healthy
<?= $dbService ?>:
condition: service_healthy
- redis
- <?= $dbService . "\n" ?>
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -682,8 +659,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
volumes:
- appwrite-imports:/storage/imports:rw
depends_on:
<?= $dbService ?>:
condition: service_healthy
- <?= $dbService . "\n" ?>
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -719,10 +695,8 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
networks:
- appwrite
depends_on:
redis:
condition: service_healthy
<?= $dbService ?>:
condition: service_healthy
- redis
- <?= $dbService . "\n" ?>
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -763,10 +737,8 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
networks:
- appwrite
depends_on:
redis:
condition: service_healthy
<?= $dbService ?>:
condition: service_healthy
- redis
- <?= $dbService . "\n" ?>
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -796,10 +768,8 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
networks:
- appwrite
depends_on:
redis:
condition: service_healthy
<?= $dbService ?>:
condition: service_healthy
- redis
- <?= $dbService . "\n" ?>
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -828,10 +798,8 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
networks:
- appwrite
depends_on:
redis:
condition: service_healthy
<?= $dbService ?>:
condition: service_healthy
- redis
- <?= $dbService . "\n" ?>
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -860,10 +828,8 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
networks:
- appwrite
depends_on:
<?= $dbService ?>:
condition: service_healthy
redis:
condition: service_healthy
- <?= $dbService . "\n" ?>
- redis
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -889,10 +855,8 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
networks:
- appwrite
depends_on:
<?= $dbService ?>:
condition: service_healthy
redis:
condition: service_healthy
- <?= $dbService . "\n" ?>
- redis
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -918,10 +882,8 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
networks:
- appwrite
depends_on:
<?= $dbService ?>:
condition: service_healthy
redis:
condition: service_healthy
- <?= $dbService . "\n" ?>
- redis
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
@@ -1026,12 +988,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- MYSQL_PASSWORD=${_APP_DB_PASS}
- MARIADB_AUTO_UPGRADE=1
command: 'mysqld --innodb-flush-method=fsync'
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
timeout: 10s
retries: 10
start_period: 30s
<?php elseif ($dbService === 'mongodb'): ?>
@@ -1144,12 +1100,6 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- appwrite
volumes:
- appwrite-redis:/data:rw
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
# clamav:
# image: appwrite/clamav:1.2.0
+1 -5
View File
@@ -3,10 +3,6 @@
$vars = $this->getParam('vars');
foreach ($vars as $key => $value) {
if ($value === null || $value === '') {
echo $key . "=\n";
} else {
echo $key . '="' . addcslashes((string) $value, '"\\$`') . '"' . "\n";
}
echo $key.'='.$value."\n";
}
?>
+4 -2
View File
@@ -7,6 +7,8 @@ $defaultHttpsPort = $defaultHttpsPort ?? '443';
$defaultAppDomain = $vars['_APP_DOMAIN']['default'] ?? 'localhost';
$defaultAppDomain = ($defaultAppDomain === 'traefik') ? 'localhost' : $defaultAppDomain;
$defaultEmailCertificates = $defaultEmailCertificates ?? '';
$defaultSecretKey = $vars['_APP_OPENSSL_KEY_V1']['default'] ?? '';
$defaultAssistantOpenAIKey = $vars['_APP_ASSISTANT_OPENAI_API_KEY']['default'] ?? '';
$defaultDatabase = $vars['_APP_DB_ADAPTER']['default'] ?? 'mongodb';
$lockedDatabase = $isUpgrade && empty($lockedDatabase) ? $defaultDatabase : $lockedDatabase;
$isLocalInstall = $isLocalInstall ?? false;
@@ -61,8 +63,8 @@ $installerVersion = @filemtime(__DIR__ . '/installer/js/installer.js') ?: time()
data-default-https-port="<?php echo htmlspecialchars((string) $defaultHttpsPort, ENT_QUOTES, 'UTF-8'); ?>"
data-default-app-domain="<?php echo htmlspecialchars((string) $defaultAppDomain, ENT_QUOTES, 'UTF-8'); ?>"
data-default-email-certificates="<?php echo htmlspecialchars((string) $defaultEmailCertificates, ENT_QUOTES, 'UTF-8'); ?>"
data-default-secret-key=""
data-default-assistant-openai-key=""
data-default-secret-key="<?php echo htmlspecialchars((string) $defaultSecretKey, ENT_QUOTES, 'UTF-8'); ?>"
data-default-assistant-openai-key="<?php echo htmlspecialchars((string) $defaultAssistantOpenAIKey, ENT_QUOTES, 'UTF-8'); ?>"
data-default-database="<?php echo htmlspecialchars((string) $defaultDatabase, ENT_QUOTES, 'UTF-8'); ?>"
<?php if ($isLocalInstall) { ?>
data-dev-mode="true"
@@ -3,7 +3,6 @@
INSTALLATION_STEPS,
TIMINGS,
getBodyDataset,
isUpgradeMode,
STEP_IDS,
STATUS,
SSE_EVENTS
@@ -311,7 +310,7 @@
const buildInstallPayload = (installId) => {
const normalizedSecret = (formState?.opensslKey || '').trim();
if (!normalizedSecret && generateSecretKey && !isUpgradeMode?.()) {
if (!normalizedSecret && generateSecretKey) {
formState.opensslKey = generateSecretKey();
}
const normalizedDomain = (formState?.appDomain || '').trim() || 'localhost';
@@ -599,9 +598,8 @@
});
if (!allDone) return;
const accountState = progressState.get(STEP_IDS.ACCOUNT_SETUP);
const sessionDetails = accountState?.details;
finalizeInstall();
notifyInstallComplete(activeInstall?.installId, sessionDetails).finally(() => {
notifyInstallComplete(activeInstall?.installId, accountState?.details).finally(() => {
setTimeout(() => redirectToApp(), TIMINGS?.redirectDelay ?? 0);
});
};
@@ -725,22 +723,22 @@
});
renderProgress();
// If any step ended in error (e.g. account creation
// failed), stay on the progress screen so the user can
// see the error and choose to retry or navigate to the
// console manually — don't auto-redirect.
const hasErrors = INSTALLATION_STEPS.some((step) => {
const state = progressState.get(step.id);
return state && state.status === STATUS.ERROR;
});
if (hasErrors) {
const accountState = progressState.get(STEP_IDS.ACCOUNT_SETUP);
const accountRequired = INSTALLATION_STEPS.some((step) => step.id === STEP_IDS.ACCOUNT_SETUP);
if (accountRequired && accountState?.status === STATUS.ERROR) {
finalizeInstall();
return;
}
const accountState = progressState.get(STEP_IDS.ACCOUNT_SETUP);
const sessionDetails = accountState?.details;
if (accountRequired && !(sessionDetails?.sessionSecret || sessionDetails?.secret)) {
handleProgress({
step: STEP_IDS.ACCOUNT_SETUP,
status: STATUS.ERROR,
message: 'Account creation did not complete. Please retry.'
});
finalizeInstall();
return;
}
finalizeInstall();
notifyInstallComplete(activeInstall?.installId, sessionDetails).finally(() => {
setTimeout(() => redirectToApp(), TIMINGS?.redirectDelay ?? 0);
@@ -853,6 +851,14 @@
const retryButton = event.target.closest('[data-install-retry]');
if (consoleButton) {
fetch('/install/cleanup', {
method: 'POST',
headers: withCsrfHeader({
'Content-Type': 'application/json'
})
}).catch(() => {});
// Redirect immediately
redirectToApp();
return;
}
@@ -65,15 +65,9 @@
};
const setInstallLock = (installId, payload) => {
const sanitizedPayload = payload ? { ...payload } : null;
if (sanitizedPayload) {
delete sanitizedPayload.opensslKey;
delete sanitizedPayload.accountPassword;
delete sanitizedPayload.assistantOpenAIKey;
}
const lock = {
installId,
payload: sanitizedPayload,
payload: payload || null,
startedAt: Date.now()
};
try {
@@ -116,7 +110,10 @@
setStateIfEmpty('httpPort', payload.httpPort);
setStateIfEmpty('httpsPort', payload.httpsPort);
setStateIfEmpty('emailCertificates', payload.emailCertificates);
setStateIfEmpty('opensslKey', payload.opensslKey);
setStateIfEmpty('assistantOpenAIKey', payload.assistantOpenAIKey);
setStateIfEmpty('accountEmail', payload.accountEmail);
setStateIfEmpty('accountPassword', payload.accountPassword);
};
const getStoredInstallId = () => {
+2 -3
View File
@@ -8,8 +8,7 @@
const {
INSTALLATION_STEPS,
clampStep,
isUpgradeMode
clampStep
} = Context;
const {
@@ -195,7 +194,7 @@
applyLockPayload?.();
applyBodyDefaults?.();
hydrateStep2State(root);
if (!isUpgradeMode?.() && (!formState.opensslKey || !formState.opensslKey.trim())) {
if (!formState.opensslKey || !formState.opensslKey.trim()) {
formState.opensslKey = generateSecretKey?.();
dispatchStateChange?.('opensslKey');
}
+30 -32
View File
@@ -9,16 +9,16 @@ use Appwrite\Event\Certificate;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Execution;
use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\Migration;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Event\Realtime;
use Appwrite\Event\Screenshot;
use Appwrite\Event\StatsUsage;
use Appwrite\Event\Webhook;
use Appwrite\Platform\Appwrite;
use Appwrite\Usage\Context;
use Appwrite\Utopia\Database\Documents\User;
use Executor\Executor;
use Swoole\Runtime;
@@ -43,7 +43,6 @@ use Utopia\Pools\Group;
use Utopia\Queue\Broker\Pool as BrokerPool;
use Utopia\Queue\Message;
use Utopia\Queue\Publisher;
use Utopia\Queue\Queue;
use Utopia\Queue\Server;
use Utopia\Registry\Registry;
use Utopia\Storage\Device\Telemetry as TelemetryDevice;
@@ -60,8 +59,7 @@ Server::setResource('register', fn () => $register);
Server::setResource('authorization', function () {
$authorization = new Authorization();
$authorization->disable();
return $authorization;
return $authorization;
}, []);
Server::setResource('dbForPlatform', function (Cache $cache, Registry $register, Authorization $authorization) {
@@ -73,7 +71,9 @@ Server::setResource('dbForPlatform', function (Cache $cache, Registry $register,
->setDatabase(APP_DATABASE)
->setAuthorization($authorization)
->setNamespace('_console')
->setDocumentType('users', User::class);
->setDocumentType('users', User::class)
;
return $dbForPlatform;
}, ['cache', 'register', 'authorization']);
@@ -112,7 +112,7 @@ Server::setResource('dbForProject', function (Cache $cache, Registry $register,
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getSequence())
->setTenant((int)$project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
@@ -152,7 +152,7 @@ Server::setResource('getProjectDB', function (Group $pools, Database $dbForPlatf
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getSequence())
->setTenant((int)$project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
@@ -174,7 +174,7 @@ Server::setResource('getProjectDB', function (Group $pools, Database $dbForPlatf
if (\in_array($dsn->getHost(), $sharedTables)) {
$database
->setSharedTables(true)
->setTenant($project->getSequence())
->setTenant((int)$project->getSequence())
->setNamespace($dsn->getParam('namespace'));
} else {
$database
@@ -194,10 +194,9 @@ Server::setResource('getProjectDB', function (Group $pools, Database $dbForPlatf
Server::setResource('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
$database = null;
return function (?Document $project = null) use ($pools, $cache, $database, $authorization) {
if ($database !== null && $project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant($project->getSequence());
$database->setTenant((int)$project->getSequence());
return $database;
}
@@ -212,8 +211,9 @@ Server::setResource('getLogsDB', function (Group $pools, Cache $cache, Authoriza
->setTimeout(APP_DATABASE_TIMEOUT_MILLISECONDS_WORKER)
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES_WORKER);
// set tenant
if ($project !== null && !$project->isEmpty() && $project->getId() !== 'console') {
$database->setTenant($project->getSequence());
$database->setTenant((int)$project->getSequence());
}
return $database;
@@ -228,7 +228,6 @@ Server::setResource('auditRetention', function (Document $project) {
if ($project->getId() === 'console') {
return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE', 15778800)); // 6 months
}
return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 1209600)); // 14 days
}, ['project']);
@@ -254,7 +253,7 @@ Server::setResource('redis', function () {
$pass = System::getEnv('_APP_REDIS_PASS', '');
$redis = new \Redis();
@$redis->pconnect($host, (int) $port);
@$redis->pconnect($host, (int)$port);
if ($pass) {
$redis->auth($pass);
}
@@ -271,6 +270,7 @@ Server::setResource('timelimit', function (\Redis $redis) {
Server::setResource('log', fn () => new Log());
Server::setResource('publisher', function (Group $pools) {
return new BrokerPool(publisher: $pools->get('publisher'));
}, ['pools']);
@@ -287,6 +287,10 @@ Server::setResource('publisherMigrations', function (BrokerPool $publisher) {
return $publisher;
}, ['publisher']);
Server::setResource('publisherStatsUsage', function (BrokerPool $publisher) {
return $publisher;
}, ['publisher']);
Server::setResource('publisherMessaging', function (BrokerPool $publisher) {
return $publisher;
}, ['publisher']);
@@ -307,13 +311,9 @@ Server::setResource('consumerStatsUsage', function (BrokerPool $consumer) {
return $consumer;
}, ['consumer']);
Server::setResource('usage', function () {
return new Context();
}, []);
Server::setResource('publisherForUsage', fn (Publisher $publisher) => new UsagePublisher(
$publisher,
new Queue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME))
), ['publisher']);
Server::setResource('queueForStatsUsage', function (Publisher $publisher) {
return new StatsUsage($publisher);
}, ['publisher']);
Server::setResource('queueForDatabase', function (Publisher $publisher) {
return new EventDatabase($publisher);
@@ -355,6 +355,10 @@ Server::setResource('queueForFunctions', function (Publisher $publisher) {
return new Func($publisher);
}, ['publisher']);
Server::setResource('queueForExecutions', function (Publisher $publisher) {
return new Execution($publisher);
}, ['publisher']);
Server::setResource('queueForRealtime', function () {
return new Realtime();
}, []);
@@ -484,13 +488,11 @@ Server::setResource('getAudit', function (Database $dbForPlatform, callable $get
return function (Document $project) use ($dbForPlatform, $getProjectDB) {
if ($project->isEmpty() || $project->getId() === 'console') {
$adapter = new AdapterDatabase($dbForPlatform);
return new UtopiaAudit($adapter);
}
$dbForProject = $getProjectDB($project);
$adapter = new AdapterDatabase($dbForProject);
return new UtopiaAudit($adapter);
};
}, ['dbForPlatform', 'getProjectDB']);
@@ -507,7 +509,7 @@ $pools = $register->get('pools');
$platform = new Appwrite();
$args = $platform->getEnv('argv');
if (! isset($args[1])) {
if (!isset($args[1])) {
Console::error('Missing worker name');
Console::exit(1);
}
@@ -532,18 +534,14 @@ try {
'workersNum' => System::getEnv('_APP_WORKERS_NUM', 1),
'connection' => $pools->get('consumer')->pop()->getResource(),
'workerName' => strtolower($workerName) ?? null,
'queueName' => $queueName,
'queueName' => $queueName
]);
} catch (\Throwable $e) {
Console::error($e->getMessage() . ', File: ' . $e->getFile() . ', Line: ' . $e->getLine());
Console::error($e->getMessage() . ', File: ' . $e->getFile() . ', Line: ' . $e->getLine());
}
$worker = $platform->getWorker();
Server::setResource('bus', function ($register) use ($worker) {
return $register->get('bus')->setResolver(fn (string $name) => $worker->getResource($name));
}, ['register']);
$worker
->error()
->inject('error')
@@ -552,11 +550,11 @@ $worker
->inject('pools')
->inject('project')
->inject('authorization')
->action(function (Throwable $error, ?Logger $logger, Log $log, Group $pools, Document $project, Authorization $authorization) use ($queueName) {
->action(function (Throwable $error, ?Logger $logger, Log $log, Group $pools, Document $project, Authorization $authorization) use ($worker, $queueName) {
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
if ($logger) {
$log->setNamespace('appwrite-worker');
$log->setNamespace("appwrite-worker");
$log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname()));
$log->setVersion($version);
$log->setType(Log::TYPE_ERROR);
-3
View File
@@ -1,3 +0,0 @@
#!/bin/sh
exec php /usr/src/code/app/cli.php time-travel "$@"
+8 -17
View File
@@ -13,17 +13,16 @@
"test": "vendor/bin/phpunit",
"lint": "vendor/bin/pint --test --config pint.json",
"format": "vendor/bin/pint --config pint.json",
"analyze": "./vendor/bin/phpstan analyse -c phpstan.neon --memory-limit=1G",
"bench": "vendor/bin/phpbench run --report=benchmark",
"check": "./vendor/bin/phpstan analyse -c phpstan.neon",
"installer:clean": "php src/Appwrite/Platform/Installer/Server.php --clean",
"installer:dev": "docker compose build && composer installer:clean && php src/Appwrite/Platform/Installer/Server.php --docker"
"installer:dev": "docker compose build && composer installer:clean && php src/Appwrite/Platform/Installer/Server.php --docker",
"installer:dev-upgrade": "docker compose build && php src/Appwrite/Platform/Installer/Server.php --docker --upgrade"
},
"autoload": {
"psr-4": {
"Appwrite\\": "src/Appwrite",
"Executor\\": "src/Executor",
"Utopia\\Bus\\": "src/Utopia/Bus"
"Executor\\": "src/Executor"
}
},
"autoload-dev": {
@@ -73,13 +72,12 @@
"utopia-php/locale": "0.8.*",
"utopia-php/logger": "0.6.*",
"utopia-php/messaging": "0.20.*",
"utopia-php/migration": "1.7.*",
"utopia-php/migration": "1.6.*",
"utopia-php/platform": "0.7.*",
"utopia-php/pools": "1.*",
"utopia-php/span": "1.1.*",
"utopia-php/preloader": "0.2.*",
"utopia-php/queue": "0.15.*",
"utopia-php/servers": "0.2.5",
"utopia-php/registry": "0.5.*",
"utopia-php/storage": "1.0.*",
"utopia-php/system": "0.10.*",
@@ -94,25 +92,18 @@
"spomky-labs/otphp": "11.*",
"webonyx/graphql-php": "14.11.*",
"league/csv": "9.14.*",
"enshrined/svg-sanitize": "0.22.*",
"utopia-php/di": "0.1.0"
"enshrined/svg-sanitize": "0.22.*"
},
"repositories": [
{
"type": "vcs",
"url": "https://github.com/utopia-php/database"
}
],
"require-dev": {
"ext-fileinfo": "*",
"appwrite/sdk-generator": "*",
"brianium/paratest": "7.*",
"phpunit/phpunit": "12.*",
"swoole/ide-helper": "6.*",
"phpstan/phpstan": "^2.0",
"phpstan/phpstan": "1.12.*",
"textalk/websocket": "1.5.*",
"czproject/git-php": "4.*",
"laravel/pint": "1.*"
"laravel/pint": "1.*",
"phpbench/phpbench": "1.*"
},
"provide": {
"ext-phpiredis": "*"
Generated
+766 -188
View File
File diff suppressed because it is too large Load Diff
-144
View File
@@ -1,144 +0,0 @@
# Dev tools for local development only.
# This file is automatically loaded by `docker compose` alongside docker-compose.yml.
# CI sets COMPOSE_FILE=docker-compose.yml explicitly, so these services are excluded from test runs.
services:
appwrite-mongo-express:
profiles: ["mongodb"]
image: mongo-express
container_name: appwrite-mongo-express
networks:
- appwrite
ports:
- "8082:8081"
environment:
ME_CONFIG_MONGODB_URL: "mongodb://root:${_APP_DB_ROOT_PASS}@appwrite-mongodb:27017/?replicaSet=rs0&directConnection=true"
ME_CONFIG_BASICAUTH_USERNAME: ${_APP_DB_USER}
ME_CONFIG_BASICAUTH_PASSWORD: ${_APP_DB_PASS}
depends_on:
- mongodb
adminer:
image: adminer
container_name: appwrite-adminer
restart: always
ports:
- 9506:8080
networks:
- appwrite
- gateway
environment:
- ADMINER_DESIGN=pepa-linha
- ADMINER_DEFAULT_SERVER=mariadb
- ADMINER_DEFAULT_USERNAME=root
- ADMINER_DEFAULT_PASSWORD=rootsecretpassword
- ADMINER_DEFAULT_DB=appwrite
configs:
- source: adminer-index.php
target: /var/www/html/index.php
mode: 0755
labels:
- "traefik.enable=true"
- "traefik.constraint-label-stack=appwrite"
- "traefik.docker.network=gateway"
- "traefik.http.services.appwrite_adminer.loadbalancer.server.port=8080"
- "traefik.http.routers.appwrite_adminer_http.entrypoints=appwrite_web"
- "traefik.http.routers.appwrite_adminer_http.rule=Host(`mysql.localhost`)"
- "traefik.http.routers.appwrite_adminer_http.service=appwrite_adminer"
- "traefik.http.routers.appwrite_adminer_https.entrypoints=appwrite_websecure"
- "traefik.http.routers.appwrite_adminer_https.rule=Host(`mysql.localhost`)"
- "traefik.http.routers.appwrite_adminer_https.service=appwrite_adminer"
- "traefik.http.routers.appwrite_adminer_https.tls=true"
redis-insight:
image: redis/redisinsight:latest
restart: unless-stopped
networks:
- appwrite
- gateway
environment:
- RI_PRE_SETUP_DATABASES_PATH=/mnt/connections.json
configs:
- source: redisinsight-connections.json
target: /mnt/connections.json
mode: 0755
labels:
- "traefik.enable=true"
- "traefik.constraint-label-stack=appwrite"
- "traefik.docker.network=gateway"
- "traefik.http.services.appwrite_redisinsight.loadbalancer.server.port=5540"
- "traefik.http.routers.appwrite_redisinsight_http.entrypoints=appwrite_web"
- "traefik.http.routers.appwrite_redisinsight_http.rule=Host(`redis.localhost`)"
- "traefik.http.routers.appwrite_redisinsight_http.service=appwrite_redisinsight"
- "traefik.http.routers.appwrite_redisinsight_https.entrypoints=appwrite_websecure"
- "traefik.http.routers.appwrite_redisinsight_https.rule=Host(`redis.localhost`)"
- "traefik.http.routers.appwrite_redisinsight_https.service=appwrite_redisinsight"
- "traefik.http.routers.appwrite_redisinsight_https.tls=true"
ports:
- "8081:5540"
graphql-explorer:
container_name: appwrite-graphql-explorer
image: appwrite/altair:0.3.0
restart: unless-stopped
networks:
- appwrite
ports:
- "9509:3000"
environment:
- SERVER_URL=http://localhost/v1/graphql
configs:
redisinsight-connections.json:
content: |
[
{
"compressor": "NONE",
"id": "104dc90a-21ef-4d5e-8912-b30baabb152f",
"host": "redis",
"port": 6379,
"name": "redis:6379",
"db": 0,
"username": "default",
"password": null,
"connectionType": "STANDALONE",
"nameFromProvider": null,
"provider": "REDIS",
"lastConnection": "2025-10-16T09:22:02.591Z",
"modules": [
{
"name": "ReJSON",
"version": 20808,
"semanticVersion": "2.8.8"
},
{
"name": "search",
"version": 21015,
"semanticVersion": "2.10.15"
}
],
"tls": false,
"tlsServername": null,
"verifyServerCert": null,
"caCert": null,
"clientCert": null,
"ssh": false,
"sshOptions": null,
"forceStandalone": false,
"tags": []
}
]
adminer-index.php:
content: |
<?php
if(!count($$_GET)) {
$$_POST['auth'] = [
'server' => $$_ENV['ADMINER_DEFAULT_SERVER'],
'driver' => 'server', /* seems to autodetect the driver from server settings */
'username' => $$_ENV['ADMINER_DEFAULT_USERNAME'],
'password' => $$_ENV['ADMINER_DEFAULT_PASSWORD'],
'db' => $$_ENV['ADMINER_DEFAULT_DB'],
];
}
include './adminer.php';
+190 -59
View File
@@ -10,15 +10,6 @@ x-logging: &x-logging
max-file: "5"
max-size: "10m"
x-build: &x-build
build:
context: .
target: development
args:
DEBUG: false
TESTING: true
VERSION: dev
services:
traefik:
image: traefik:3.6
@@ -59,13 +50,15 @@ services:
appwrite:
container_name: appwrite
<<: [*x-logging, *x-build]
<<: *x-logging
image: appwrite-dev
healthcheck:
test: ["CMD", "doctor"]
interval: 5s
timeout: 5s
retries: 12
build:
context: .
target: development
args:
DEBUG: false
TESTING: true
VERSION: dev
ports:
- 9501:80
networks:
@@ -108,10 +101,10 @@ services:
- ./dev:/usr/src/code/dev
depends_on:
redis:
condition: service_healthy
coredns:
condition: service_started
- ${_APP_DB_HOST:-mongodb}
- redis
- coredns
# - clamav
entrypoint:
- php
- -e
@@ -159,6 +152,7 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_SMTP_HOST
- _APP_SMTP_PORT
- _APP_SMTP_SECURE
@@ -267,7 +261,7 @@ services:
appwrite-realtime:
entrypoint: realtime
<<: [*x-logging, *x-build]
<<: *x-logging
container_name: appwrite-realtime
image: appwrite-dev
restart: unless-stopped
@@ -311,6 +305,7 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_USAGE_STATS
- _APP_LOGGING_CONFIG
- _APP_LOGGING_CONFIG_REALTIME
@@ -319,7 +314,7 @@ services:
appwrite-worker-audits:
entrypoint: worker-audits
<<: [*x-logging, *x-build]
<<: *x-logging
container_name: appwrite-worker-audits
image: appwrite-dev
networks:
@@ -345,12 +340,13 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_LOGGING_CONFIG
- _APP_DATABASE_SHARED_TABLES
appwrite-worker-webhooks:
entrypoint: worker-webhooks
<<: [*x-logging, *x-build]
<<: *x-logging
container_name: appwrite-worker-webhooks
image: appwrite-dev
networks:
@@ -375,6 +371,7 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
@@ -385,7 +382,7 @@ services:
appwrite-worker-deletes:
entrypoint: worker-deletes
<<: [*x-logging, *x-build]
<<: *x-logging
container_name: appwrite-worker-deletes
image: appwrite-dev
networks:
@@ -417,6 +414,7 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_STORAGE_DEVICE
- _APP_STORAGE_S3_ACCESS_KEY
- _APP_STORAGE_S3_SECRET
@@ -450,7 +448,7 @@ services:
appwrite-worker-databases:
entrypoint: worker-databases
<<: [*x-logging, *x-build]
<<: *x-logging
container_name: appwrite-worker-databases
image: appwrite-dev
networks:
@@ -476,6 +474,7 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_LOGGING_CONFIG
- _APP_WORKERS_NUM
- _APP_QUEUE_NAME
@@ -483,7 +482,7 @@ services:
appwrite-worker-builds:
entrypoint: worker-builds
<<: [*x-logging, *x-build]
<<: *x-logging
container_name: appwrite-worker-builds
image: appwrite-dev
networks:
@@ -514,6 +513,7 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_LOGGING_CONFIG
- _APP_VCS_GITHUB_APP_NAME
- _APP_VCS_GITHUB_PRIVATE_KEY
@@ -558,7 +558,7 @@ services:
appwrite-worker-screenshots:
entrypoint: worker-screenshots
<<: [*x-logging, *x-build]
<<: *x-logging
container_name: appwrite-worker-screenshots
image: appwrite-dev
networks:
@@ -621,7 +621,7 @@ services:
appwrite-worker-certificates:
entrypoint: worker-certificates
<<: [*x-logging, *x-build]
<<: *x-logging
container_name: appwrite-worker-certificates
image: appwrite-dev
networks:
@@ -658,12 +658,13 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_LOGGING_CONFIG
- _APP_DATABASE_SHARED_TABLES
appwrite-worker-executions:
entrypoint: worker-executions
<<: [*x-logging, *x-build]
<<: *x-logging
container_name: appwrite-worker-executions
image: appwrite-dev
networks:
@@ -693,7 +694,7 @@ services:
appwrite-worker-functions:
entrypoint: worker-functions
<<: [*x-logging, *x-build]
<<: *x-logging
container_name: appwrite-worker-functions
image: appwrite-dev
networks:
@@ -721,6 +722,7 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_FUNCTIONS_TIMEOUT
- _APP_SITES_TIMEOUT
- _APP_COMPUTE_BUILD_TIMEOUT
@@ -737,7 +739,7 @@ services:
appwrite-worker-mails:
entrypoint: worker-mails
<<: [*x-logging, *x-build]
<<: *x-logging
container_name: appwrite-worker-mails
image: appwrite-dev
networks:
@@ -779,7 +781,7 @@ services:
appwrite-worker-messaging:
entrypoint: worker-messaging
<<: [*x-logging, *x-build]
<<: *x-logging
container_name: appwrite-worker-messaging
restart: unless-stopped
image: appwrite-dev
@@ -806,6 +808,7 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_LOGGING_CONFIG
- _APP_SMS_FROM
- _APP_SMS_PROVIDER
@@ -836,7 +839,7 @@ services:
appwrite-worker-migrations:
entrypoint: worker-migrations
<<: [*x-logging, *x-build]
<<: *x-logging
container_name: appwrite-worker-migrations
restart: unless-stopped
image: appwrite-dev
@@ -872,6 +875,7 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_LOGGING_CONFIG
- _APP_MIGRATIONS_FIREBASE_CLIENT_ID
- _APP_MIGRATIONS_FIREBASE_CLIENT_SECRET
@@ -881,7 +885,7 @@ services:
appwrite-task-maintenance:
entrypoint: maintenance
<<: [*x-logging, *x-build]
<<: *x-logging
container_name: appwrite-task-maintenance
image: appwrite-dev
networks:
@@ -914,6 +918,7 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_MAINTENANCE_INTERVAL
- _APP_MAINTENANCE_RETENTION_EXECUTION
- _APP_MAINTENANCE_RETENTION_CACHE
@@ -927,7 +932,7 @@ services:
appwrite-task-interval:
entrypoint: interval
<<: [*x-logging, *x-build]
<<: *x-logging
container_name: appwrite-task-interval
image: appwrite-dev
networks:
@@ -968,7 +973,7 @@ services:
appwrite-task-stats-resources:
container_name: appwrite-task-stats-resources
entrypoint: stats-resources
<<: [*x-logging, *x-build]
<<: *x-logging
image: appwrite-dev
networks:
- appwrite
@@ -989,6 +994,7 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
@@ -1000,7 +1006,7 @@ services:
appwrite-worker-stats-resources:
entrypoint: worker-stats-resources
<<: [*x-logging, *x-build]
<<: *x-logging
container_name: appwrite-worker-stats-resources
image: appwrite-dev
networks:
@@ -1022,6 +1028,7 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
@@ -1033,7 +1040,7 @@ services:
appwrite-worker-stats-usage:
entrypoint: worker-stats-usage
<<: [*x-logging, *x-build]
<<: *x-logging
container_name: appwrite-worker-stats-usage
image: appwrite-dev
networks:
@@ -1055,6 +1062,7 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
@@ -1066,7 +1074,7 @@ services:
appwrite-task-scheduler-functions:
entrypoint: schedule-functions
<<: [*x-logging, *x-build]
<<: *x-logging
container_name: appwrite-task-scheduler-functions
image: appwrite-dev
networks:
@@ -1092,11 +1100,12 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_DATABASE_SHARED_TABLES
appwrite-task-scheduler-executions:
entrypoint: schedule-executions
<<: [*x-logging, *x-build]
<<: *x-logging
container_name: appwrite-task-scheduler-executions
image: appwrite-dev
networks:
@@ -1122,11 +1131,11 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DATABASE_SHARED_TABLES
- _APP_DB_ADAPTER
appwrite-task-scheduler-messages:
entrypoint: schedule-messages
<<: [*x-logging, *x-build]
<<: *x-logging
container_name: appwrite-task-scheduler-messages
image: appwrite-dev
networks:
@@ -1152,6 +1161,7 @@ services:
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_DB_ADAPTER
- _APP_DATABASE_SHARED_TABLES
appwrite-assistant:
@@ -1245,11 +1255,6 @@ services:
- 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
mongodb:
profiles: ["mongodb"]
@@ -1288,7 +1293,20 @@ services:
retries: 10
start_period: 30s
appwrite-mongo-express:
profiles: ["mongodb"]
image: mongo-express
container_name: appwrite-mongo-express
networks:
- appwrite
ports:
- "8082:8081"
environment:
ME_CONFIG_MONGODB_URL: "mongodb://root:${_APP_DB_ROOT_PASS}@appwrite-mongodb:27017/?replicaSet=rs0&directConnection=true"
ME_CONFIG_BASICAUTH_USERNAME: ${_APP_DB_USER}
ME_CONFIG_BASICAUTH_PASSWORD: ${_APP_DB_PASS}
depends_on:
- mongodb
postgresql:
profiles: ["postgresql"]
@@ -1309,11 +1327,6 @@ services:
- POSTGRES_USER=${_APP_DB_USER}
- POSTGRES_PASSWORD=${_APP_DB_PASS}
command: "postgres"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${_APP_DB_USER}"]
interval: 5s
timeout: 5s
retries: 12
redis:
image: redis:7.4.7-alpine
@@ -1330,11 +1343,6 @@ services:
- appwrite
volumes:
- appwrite-redis:/data:rw
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 12
coredns: # DNS server for testing purposes (Proxy APIs)
image: coredns/coredns:1.12.4
@@ -1406,8 +1414,78 @@ services:
networks:
- appwrite
# Dev tools (adminer, redis-insight, mongo-express, graphql-explorer)
# are defined in docker-compose.override.yml
adminer:
image: adminer
container_name: appwrite-adminer
<<: *x-logging
restart: always
ports:
- 9506:8080
networks:
- appwrite
- gateway
environment:
- ADMINER_DESIGN=pepa-linha
- ADMINER_DEFAULT_SERVER=mariadb
- ADMINER_DEFAULT_USERNAME=root
- ADMINER_DEFAULT_PASSWORD=rootsecretpassword
- ADMINER_DEFAULT_DB=appwrite
configs:
- source: adminer-index.php
target: /var/www/html/index.php
mode: 0755
labels:
- "traefik.enable=true"
- "traefik.constraint-label-stack=appwrite"
- "traefik.docker.network=gateway"
- "traefik.http.services.appwrite_adminer.loadbalancer.server.port=8080"
- "traefik.http.routers.appwrite_adminer_http.entrypoints=appwrite_web"
- "traefik.http.routers.appwrite_adminer_http.rule=Host(`mysql.localhost`)"
- "traefik.http.routers.appwrite_adminer_http.service=appwrite_adminer"
- "traefik.http.routers.appwrite_adminer_https.entrypoints=appwrite_websecure"
- "traefik.http.routers.appwrite_adminer_https.rule=Host(`mysql.localhost`)"
- "traefik.http.routers.appwrite_adminer_https.service=appwrite_adminer"
- "traefik.http.routers.appwrite_adminer_https.tls=true"
redis-insight:
image: redis/redisinsight:latest
restart: unless-stopped
networks:
- appwrite
- gateway
environment:
- RI_PRE_SETUP_DATABASES_PATH=/mnt/connections.json
configs:
- source: redisinsight-connections.json
target: /mnt/connections.json
mode: 0755
labels:
- "traefik.enable=true"
- "traefik.constraint-label-stack=appwrite"
- "traefik.docker.network=gateway"
- "traefik.http.services.appwrite_redisinsight.loadbalancer.server.port=5540"
- "traefik.http.routers.appwrite_redisinsight_http.entrypoints=appwrite_web"
- "traefik.http.routers.appwrite_redisinsight_http.rule=Host(`redis.localhost`)"
- "traefik.http.routers.appwrite_redisinsight_http.service=appwrite_redisinsight"
- "traefik.http.routers.appwrite_redisinsight_https.entrypoints=appwrite_websecure"
- "traefik.http.routers.appwrite_redisinsight_https.rule=Host(`redis.localhost`)"
- "traefik.http.routers.appwrite_redisinsight_https.service=appwrite_redisinsight"
- "traefik.http.routers.appwrite_redisinsight_https.tls=true"
ports:
- "8081:5540"
graphql-explorer:
container_name: appwrite-graphql-explorer
image: appwrite/altair:0.3.0
restart: unless-stopped
networks:
- appwrite
ports:
- "9509:3000"
environment:
- SERVER_URL=http://localhost/v1/graphql
# Dev Tools End ------------------------------------------------------------------------------------------
networks:
gateway:
@@ -1420,7 +1498,60 @@ networks:
runtimes:
name: runtimes
configs:
redisinsight-connections.json:
content: |
[
{
"compressor": "NONE",
"id": "104dc90a-21ef-4d5e-8912-b30baabb152f",
"host": "redis",
"port": 6379,
"name": "redis:6379",
"db": 0,
"username": "default",
"password": null,
"connectionType": "STANDALONE",
"nameFromProvider": null,
"provider": "REDIS",
"lastConnection": "2025-10-16T09:22:02.591Z",
"modules": [
{
"name": "ReJSON",
"version": 20808,
"semanticVersion": "2.8.8"
},
{
"name": "search",
"version": 21015,
"semanticVersion": "2.10.15"
}
],
"tls": false,
"tlsServername": null,
"verifyServerCert": null,
"caCert": null,
"clientCert": null,
"ssh": false,
"sshOptions": null,
"forceStandalone": false,
"tags": []
}
]
adminer-index.php:
content: |
<?php
if(!count($$_GET)) {
$$_POST['auth'] = [
'server' => $$_ENV['ADMINER_DEFAULT_SERVER'],
'driver' => 'server', /* seems to autodetect the driver from server settings */
'username' => $$_ENV['ADMINER_DEFAULT_USERNAME'],
'password' => $$_ENV['ADMINER_DEFAULT_PASSWORD'],
'db' => $$_ENV['ADMINER_DEFAULT_DB'],
];
}
include './adminer.php';
volumes:
appwrite-mariadb:
+131
View File
@@ -0,0 +1,131 @@
# Appwrite Web Installer
The Appwrite Web Installer provides a modern, user-friendly web interface for installing Appwrite. It runs in a
temporary Docker container with its own web server before the Appwrite stack is created.
## Architecture
The web installer runs as part of the installation task using a Swoole HTTP server with Utopia PHP routing:
```
docker run (temporary container)
└── Server.php
├── Swoole HTTP server (0.0.0.0:20080)
├── Module / Service / Action routes
├── State (progress tracking)
└── Config (installation settings)
```
## Features
- Retry failed steps with payload validation
- Step-by-step guided installation with real-time validation
- Progress tracking with Server-Sent Events and polling fallback
## Usage
### Docker
```bash
docker run -it --rm \
--name appwrite-installer \
-p 20080:20080 \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
--entrypoint="install" \
appwrite/appwrite:<version>
```
Then open http://localhost:20080 in your browser.
### Composer (Development)
```bash
composer installer:dev # Start web installer in Docker
composer installer:dev-upgrade # Start web installer in upgrade mode
composer installer:clean # Clean up installer containers and temp files
```
### CLI Mode
```bash
docker run -it --rm \
--name appwrite-installer \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
--entrypoint="install" \
appwrite/appwrite:<version> \
--interactive N
```
## Files
### Frontend
- `app/views/install/installer.phtml` - Main layout
- `app/views/install/installer/` - UI components (templates, CSS, JS, icons)
### Backend
- `src/Appwrite/Platform/Installer/**/*.php` - Installer backend (Server, Module, Actions, State, Config)
- `src/Appwrite/Platform/Tasks/Install.php` - Installation task execution
- `app/views/install/*.phtml` - Docker Compose and env templates
## Technical Details
### Endpoints
- `GET /` - Installer UI
- `POST /install` - Start installation (SSE stream or JSON)
- `GET /install/status?installId=...` - Progress polling
- `POST /install/validate` - CSRF validation
- `POST /install/complete` - Notify completion
### URL Parameters
- `?step=1..5` - Jump to specific installation step
### Installation Flow
1. User submits form → CSRF validation
2. Acquire global lock (prevents concurrent installs)
3. Stream progress via SSE (or poll if SSE fails)
4. Generate `docker-compose.yml` and `.env`
5. Run `docker compose up -d`
6. Redirect to console, shutdown server
## Security
- **CSRF**: Cookie tokens with timing-safe comparison
- **CSP**: Strict headers prevent XSS
- **Validation**: Ports (1-65535), emails, domains, env var names
- **Escaping**: All shell commands use `escapeshellarg()`
- **Path protection**: `realpath()` + `str_starts_with()` checks
- **File permissions**: Config files created with mode 0600
- **Global lock**: File-based with exception-safe cleanup
## Troubleshooting
### ERR_EMPTY_RESPONSE
Server binds to `0.0.0.0` (not `localhost`) for Docker port mapping. If `localhost` resolves to IPv6 `::1`, connection
will fail from host.
### Can't access localhost:20080
Ensure `-p 20080:20080` is in docker run command. Try `http://127.0.0.1:20080` instead.
### Installation fails
- Check Docker is running
- Verify write permissions for appwrite directory
- Check browser console for errors
- Review Docker logs: `docker logs appwrite-installer`
## Support
For issues or questions:
- [GitHub Issues](https://github.com/appwrite/appwrite/issues)
- [Discord Community](https://appwrite.io/discord)
- [Documentation](https://appwrite.io/docs)
+3 -63
View File
@@ -20,16 +20,10 @@ client = Client()
### Make Your First Request
Once your SDK object is set, create any of the Appwrite service objects and choose any request to send. Full documentation for any service method you would like to use can be found in your SDK documentation or in the [API References](https://appwrite.io/docs) section.
All service methods return typed Pydantic models, so you can access response fields as attributes:
```python
users = Users(client)
user = users.create(ID.unique(), email = "email@example.com", phone = "+123456789", password = "password", name = "Walter O'Brien")
print(user.name) # "Walter O'Brien"
print(user.email) # "email@example.com"
print(user.id) # The generated user ID
result = users.create(ID.unique(), email = "email@example.com", phone = "+123456789", password = "password", name = "Walter O'Brien")
```
### Full Example
@@ -49,60 +43,7 @@ client = Client()
users = Users(client)
user = users.create(ID.unique(), email = "email@example.com", phone = "+123456789", password = "password", name = "Walter O'Brien")
print(user.name) # Access fields as attributes
print(user.to_dict()) # Convert to dictionary if needed
```
### Type Safety with Models
The Appwrite Python SDK provides type safety when working with database rows through generic methods. Methods like `get_row`, `list_rows`, and others accept a `model_type` parameter that allows you to specify your custom Pydantic model for full type safety.
```python
from pydantic import BaseModel
from datetime import datetime
from typing import Optional
from appwrite.client import Client
from appwrite.services.tables_db import TablesDB
# Define your custom model matching your table schema
class Post(BaseModel):
postId: int
authorId: int
title: str
content: str
createdAt: datetime
updatedAt: datetime
isPublished: bool
excerpt: Optional[str] = None
client = Client()
# ... configure your client ...
tables_db = TablesDB(client)
# Fetch a single row with type safety
row = tables_db.get_row(
database_id="your-database-id",
table_id="your-table-id",
row_id="your-row-id",
model_type=Post # Pass your custom model type
)
print(row.data.title) # Fully typed - IDE autocomplete works
print(row.data.postId) # int type, not Any
print(row.data.createdAt) # datetime type
# Fetch multiple rows with type safety
result = tables_db.list_rows(
database_id="your-database-id",
table_id="your-table-id",
model_type=Post
)
for row in result.rows:
print(f"{row.data.title} by {row.data.authorId}")
result = users.create(ID.unique(), email = "email@example.com", phone = "+123456789", password = "password", name = "Walter O'Brien")
```
### Error Handling
@@ -111,8 +52,7 @@ The Appwrite Python SDK raises `AppwriteException` object with `message`, `code`
```python
users = Users(client)
try:
user = users.create(ID.unique(), email = "email@example.com", phone = "+123456789", password = "password", name = "Walter O'Brien")
print(user.name)
result = users.create(ID.unique(), email = "email@example.com", phone = "+123456789", password = "password", name = "Walter O'Brien")
except AppwriteException as e:
print(e.message)
```
+4 -1
View File
@@ -1,10 +1,13 @@
{
"name": "@appwrite.io/repo",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@appwrite.io/repo"
"name": "@appwrite.io/repo",
"version": "1.0.0",
"license": "ISC"
}
}
}
+16 -1
View File
@@ -8,5 +8,20 @@
"files": [
"docs/examples",
"app/config/specs"
]
],
"version": "1.0.0",
"description": "> We just announced Timestamp Overrides for Appwrite Databases - [Learn more](https://appwrite.io/blog/post/announcing-timestamp-overrides)",
"main": "index.js",
"directories": {
"doc": "docs",
"test": "tests"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"bugs": {
"url": "https://github.com/appwrite/appwrite/issues"
},
"homepage": "https://github.com/appwrite/appwrite#readme"
}
+6
View File
@@ -0,0 +1,6 @@
{
"$schema":"vendor/phpbench/phpbench/phpbench.schema.json",
"runner.bootstrap": "vendor/autoload.php",
"runner.path": "tests",
"runner.file_pattern": "*Bench.php"
}
File diff suppressed because it is too large Load Diff
+3 -11
View File
@@ -1,17 +1,9 @@
includes:
- phpstan-baseline.neon
parameters:
level: 3
level: 8
paths:
- src
- app
- bin
- tests
bootstrapFiles:
- app/init/constants.php
- src/Appwrite/Transformation
scanDirectories:
- vendor/swoole/ide-helper
excludePaths:
- tests/resources
- app/sdks
-2
View File
@@ -6,7 +6,6 @@
colors="true"
processIsolation="false"
stopOnFailure="false"
stopOnError="false"
cacheDirectory=".phpunit.cache"
>
<extensions>
@@ -34,7 +33,6 @@
<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>
<file>./tests/e2e/Services/Functions/FunctionsBase.php</file>
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="64" height="64" viewBox="0 0 45.025 44.217"><defs><clipPath id="A"><use xlink:href="#B"/></clipPath><path id="B" d="M0 0h191.356v57.344H0z"/></defs><path clip-path="url(#A)" d="M75.198 5.328c-1.872.047-2.977.863-3.34 2.42.48.418 1.126.6 1.77.572 1.415-.06 2.817-1.138 2.42-2.965-.298-.025-.582-.035-.85-.027M84.84 0h-.167c-1.335.043-1.542.767-4.425 1.477-2.912.717-5.725.028-8.86 1.344C62.025 6.748 60.03 20.293 51.52 25.1c-6.36 3.593-12.512 5.188-18.28 6.995-3.792 1.188-7.552 2.556-10.988 5.516-2.667 2.3-3.4 4.085-6.177 6.97-2.835 2.933-11.553-2.258-15.8 1.54a.84.84 0 0 0 .014 1.253c.92.805 2.67 2.722 4.336 3.314-.528 1-3.513 3.946-2.904 5.42.64 1.55 8.04.497 14.88-3.633 3.185-1.924 5.723-4.696 10.685-5.357 6.42-.855 13.817.55 21.25 1.62-1.103 3.286-3.315 5.473-5.088 8.1-.55.592 1.103.658 2.986.3 3.4-.838 5.832-1.513 8.4-3 3.143-1.83 3.62-6.52 7.474-7.534 1.864 2.865 6.5 3.83 10.074 2.3.198-.084.392-.177.582-.275.43-.375.178-.813-.086-1.135a6.21 6.21 0 0 1-.362-.364c-2.012-2.162-2.472-7.266-1.58-9.748 1.017-2.824 2.022-7.343 3.046-11.075 1.1-4 1.506-9.063 2.837-11.106 2.002-3.072 4.215-4.127 6.136-5.86s3.68-3.42 3.62-7.384C86.547.734 85.946.03 84.84 0m.204 4.163C84.647 6.897 82.9 8.4 80.836 9.85c-1.8 1.27-3.794 2.493-5.07 4.477-1.305 2.03-2.134 8.985-4.16 15.852-1.68 5.698-4.184 11.335-8.467 14.05-.278.176-.645.015-.704-.3-.3-1.592-.24-4.5-.734-3.56-.62 1.77-1.322 3.458-2.143 5.027-2.52 4.816-6.166 8.505-12.057 9.95a.53.53 0 0 1-.552-.82c2.718-3.77 5.15-7.825 5.447-14.014.025-.534-.646-.778-.983-.364-1.284 1.583-1.6 5.347-3.477 6.506-1.474.16-2.967.16-4.47.07-6.17-.37-12.502-2.226-18.274-.373-3.93 1.262-8.057 4.85-11.386 6.293C9.894 54.33 8.04 55.93 3.1 55.82c-.505-.68 2.062-3.623 2.934-4.893.278-.407-.317-.835-.874-1.1-1.338-.614-2.68-2.28-4.107-2.93.183-.337.83-.674 1.187-.88 3.24-1.88 11.832 2.124 14.14-.143 1.425-1.398 2.385-2.626 3.353-4.05.94-1.38 2.368-2.838 3.847-4.047a31.3 31.3 0 0 1 1.94-1.435c2.52-1.724 3.907-1.852 7.17-3.064 4.152-1.544 9.293-2.898 13.747-4.6 2.752-1.053 5.744-2.35 8.183-4.17.58-.432 1.127-.893 1.634-1.386 6.964-6.8 8.345-18.766 19.2-19.882 1.314-.135 2.248-.192 3.228-.223 1.096-.03 2.18-.3 3.263-.818.334-.146 2.324-1.35 2.996-.768.448.388.115 2.492.086 2.72" transform="matrix(.52012 0 0 .52012 0 7.19564)" fill="#444b5e"/></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 32 32"><path d="M15.9.087l.854 1.604c.192.296.4.558.645.802.715.715 1.394 1.464 2.004 2.266 1.447 1.9 2.423 4.01 3.12 6.292.418 1.394.645 2.824.662 4.27.07 4.323-1.412 8.035-4.4 11.12-.488.488-1.01.94-1.57 1.342-.296 0-.436-.227-.558-.436-.227-.383-.366-.82-.436-1.255-.105-.523-.174-1.046-.14-1.586v-.244C16.057 24.21 15.796.21 15.9.087z" fill="#599636"/><path d="M15.9.034c-.035-.07-.07-.017-.105.017.017.35-.105.662-.296.96-.21.296-.488.523-.767.767-1.55 1.342-2.77 2.963-3.747 4.776-1.3 2.44-1.97 5.055-2.16 7.808-.087.993.314 4.497.627 5.508.854 2.684 2.388 4.933 4.375 6.885.488.47 1.01.906 1.55 1.325.157 0 .174-.14.21-.244a4.78 4.78 0 0 0 .157-.68l.35-2.614L15.9.034z" fill="#6cac48"/><path d="M16.754 28.845c.035-.4.227-.732.436-1.063-.21-.087-.366-.26-.488-.453-.105-.174-.192-.383-.26-.575-.244-.732-.296-1.5-.366-2.248v-.453c-.087.07-.105.662-.105.75a17.37 17.37 0 0 1-.314 2.353c-.052.314-.087.627-.28.906 0 .035 0 .07.017.122.314.924.4 1.865.453 2.824v.35c0 .418-.017.33.33.47.14.052.296.07.436.174.105 0 .122-.087.122-.157l-.052-.575v-1.604c-.017-.28.035-.558.07-.82z" fill="#c2bfbf"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -1,22 +0,0 @@
<?php
namespace Appwrite\Bus\Events;
use Utopia\Bus\Event;
class ExecutionCompleted implements Event
{
/**
* @param array<string, mixed> $execution
* @param array<string, mixed> $project
* @param array<string, mixed> $spec
* @param array<string, mixed> $resource
*/
public function __construct(
public readonly array $execution,
public readonly array $project,
public readonly array $spec = [],
public readonly array $resource = [],
) {
}
}
@@ -1,22 +0,0 @@
<?php
namespace Appwrite\Bus\Events;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use Utopia\Bus\Event;
class RequestCompleted implements Event
{
/**
* @param array<string, mixed> $project
* @param array<string, mixed> $deployment
*/
public function __construct(
public readonly array $project,
public readonly Request $request,
public readonly Response $response,
public readonly array $deployment = [],
) {
}
}
-39
View File
@@ -1,39 +0,0 @@
<?php
namespace Appwrite\Bus\Listeners;
use Appwrite\Bus\Events\ExecutionCompleted;
use Appwrite\Event\Execution;
use Utopia\Bus\Listener;
use Utopia\Database\Document;
use Utopia\Queue\Publisher;
class Log extends Listener
{
public static function getName(): string
{
return 'log';
}
public static function getEvents(): array
{
return [ExecutionCompleted::class];
}
public function __construct()
{
$this
->desc('Persists execution logs to database via queue')
->inject('publisher')
->callback($this->handle(...));
}
public function handle(ExecutionCompleted $event, Publisher $publisher): void
{
$queueForExecutions = new Execution($publisher);
$queueForExecutions
->setExecution(new Document($event->execution))
->setProject(new Document($event->project))
->trigger();
}
}
-115
View File
@@ -1,115 +0,0 @@
<?php
namespace Appwrite\Bus\Listeners;
use Appwrite\Bus\Events\ExecutionCompleted;
use Appwrite\Bus\Events\RequestCompleted;
use Appwrite\Event\Message\Usage as UsageMessage;
use Appwrite\Event\Publisher\Usage as Publisher;
use Appwrite\Usage\Context;
use Utopia\Bus\Event;
use Utopia\Bus\Listener;
use Utopia\Database\Document;
class Usage extends Listener
{
public static function getName(): string
{
return 'usage';
}
public static function getEvents(): array
{
return [
ExecutionCompleted::class,
RequestCompleted::class,
];
}
public function __construct()
{
$this
->desc('Records usage metrics')
->inject('publisherForUsage')
->inject('usage')
->callback($this->handle(...));
}
public function handle(Event $event, Publisher $publisherForUsage, Context $usage): void
{
match (true) {
$event instanceof ExecutionCompleted => $this->handleExecutionCompleted($event, $publisherForUsage),
$event instanceof RequestCompleted => $this->handleRequestCompleted($event, $usage),
default => null,
};
}
private function handleExecutionCompleted(ExecutionCompleted $event, Publisher $publisherForUsage): void
{
$execution = new Document($event->execution);
$resource = new Document($event->resource);
// Non-SSR sites don't record execution metrics
if ($execution->getAttribute('resourceType') === 'sites' && $resource->getAttribute('adapter') !== 'ssr') {
return;
}
$project = new Document($event->project);
$spec = $event->spec;
$resourceType = $execution->getAttribute('resourceType', '');
$resourceInternalId = $execution->getAttribute('resourceInternalId', '');
$duration = $execution->getAttribute('duration', 0);
$compute = (int)($duration * 1000);
$mbSeconds = (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $duration * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT));
$context = (new Context())
->addMetric(METRIC_EXECUTIONS, 1)
->addMetric(str_replace(['{resourceType}'], [$resourceType], METRIC_RESOURCE_TYPE_EXECUTIONS), 1)
->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [$resourceType, $resourceInternalId], METRIC_RESOURCE_TYPE_ID_EXECUTIONS), 1)
->addMetric(METRIC_EXECUTIONS_COMPUTE, $compute)
->addMetric(str_replace(['{resourceType}'], [$resourceType], METRIC_RESOURCE_TYPE_EXECUTIONS_COMPUTE), $compute)
->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [$resourceType, $resourceInternalId], METRIC_RESOURCE_TYPE_ID_EXECUTIONS_COMPUTE), $compute)
->addMetric(METRIC_EXECUTIONS_MB_SECONDS, $mbSeconds)
->addMetric(str_replace(['{resourceType}'], [$resourceType], METRIC_RESOURCE_TYPE_EXECUTIONS_MB_SECONDS), $mbSeconds)
->addMetric(str_replace(['{resourceType}', '{resourceInternalId}'], [$resourceType, $resourceInternalId], METRIC_RESOURCE_TYPE_ID_EXECUTIONS_MB_SECONDS), $mbSeconds);
$message = new UsageMessage(
project: $project,
metrics: $context->getMetrics(),
reduce: $context->getReduce()
);
$publisherForUsage->enqueue($message);
}
private function handleRequestCompleted(RequestCompleted $event, Context $usage): void
{
$fileSize = 0;
$file = $event->request->getFiles('file');
if (!empty($file)) {
$fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size'];
}
$deployment = new Document($event->deployment);
$inbound = $event->request->getSize() + $fileSize;
$outbound = $event->response->getSize();
if ($deployment->getAttribute('resourceType') === 'sites') {
$siteInternalId = $deployment->getAttribute('resourceInternalId', '');
$usage
->addMetric(METRIC_SITES_REQUESTS, 1)
->addMetric(METRIC_SITES_INBOUND, $inbound)
->addMetric(METRIC_SITES_OUTBOUND, $outbound)
->addMetric(str_replace('{siteInternalId}', $siteInternalId, METRIC_SITES_ID_REQUESTS), 1)
->addMetric(str_replace('{siteInternalId}', $siteInternalId, METRIC_SITES_ID_INBOUND), $inbound)
->addMetric(str_replace('{siteInternalId}', $siteInternalId, METRIC_SITES_ID_OUTBOUND), $outbound);
} else {
$usage
->addMetric(METRIC_NETWORK_REQUESTS, 1)
->addMetric(METRIC_NETWORK_INBOUND, $inbound)
->addMetric(METRIC_NETWORK_OUTBOUND, $outbound);
}
}
}
-21
View File
@@ -1,21 +0,0 @@
<?php
namespace Appwrite\Event\Message;
abstract class Base
{
/**
* Serialize message to array for queue
*
* @return array
*/
abstract public function toArray(): array;
/**
* Deserialize message from array
*
* @param array $data
* @return static
*/
abstract public static function fromArray(array $data): static;
}
-49
View File
@@ -1,49 +0,0 @@
<?php
namespace Appwrite\Event\Message;
use Utopia\Database\Document;
class Usage extends Base
{
/**
* @param Document $project
* @param array<array{key: string, value: int}> $metrics
* @param array<Document> $reduce
*/
public function __construct(
public readonly Document $project,
public readonly array $metrics,
public readonly array $reduce = [],
) {
}
/**
* @return array
*/
public function toArray(): array
{
return [
'project' => [
'$id' => $this->project->getId(),
'$sequence' => $this->project->getSequence(),
'database' => $this->project->getAttribute('database', ''),
],
'metrics' => $this->metrics,
'reduce' => array_map(fn (Document $doc) => $doc->getArrayCopy(), $this->reduce),
];
}
/**
* @param array $data
* @return static
*/
public static function fromArray(array $data): static
{
return new self(
project: new Document($data['project'] ?? []),
metrics: $data['metrics'] ?? [],
reduce: array_map(fn (array $doc) => new Document($doc), $data['reduce'] ?? []),
);
}
}
-33
View File
@@ -1,33 +0,0 @@
<?php
namespace Appwrite\Event\Publisher;
use Appwrite\Event\Message\Base as BaseMessage;
use Utopia\Queue\Publisher;
use Utopia\Queue\Queue;
readonly class Base
{
public function __construct(
protected Publisher $publisher
) {
}
/**
* Publish a message to the queue
*/
public function publish(Queue $queue, BaseMessage $message): string|bool
{
$payload = $message->toArray();
return $this->publisher->enqueue($queue, $payload);
}
/**
* Get the size of a queue
*/
public function getQueueSize(Queue $queue, bool $failed = false): int
{
return $this->publisher->getQueueSize($queue, $failed);
}
}
-39
View File
@@ -1,39 +0,0 @@
<?php
namespace Appwrite\Event\Publisher;
use Appwrite\Event\Message\Usage as UsageMessage;
use Utopia\Console;
use Utopia\Queue\Publisher;
use Utopia\Queue\Queue;
readonly class Usage extends Base
{
public function __construct(
Publisher $publisher,
protected Queue $queue
) {
parent::__construct($publisher);
}
/**
* Enqueue a usage message
*/
public function enqueue(UsageMessage $message): string|bool
{
try {
return $this->publish($this->queue, $message);
} catch (\Throwable $th) {
Console::error('[Usage] Failed to publish usage message: ' . $th->getMessage());
return false;
}
}
/**
* Get the size of the usage queue
*/
public function getSize(bool $failed = false): int
{
return $this->getQueueSize($this->queue, $failed);
}
}
+96
View File
@@ -0,0 +1,96 @@
<?php
namespace Appwrite\Event;
use Utopia\Database\Document;
use Utopia\Queue\Publisher;
use Utopia\System\System;
class StatsUsage extends Event
{
protected array $metrics = [];
protected array $reduce = [];
protected array $disabled = [];
protected bool $critical = false;
public function __construct(protected Publisher $publisher)
{
parent::__construct($publisher);
$this
->setQueue(System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME))
->setClass(System::getEnv('_APP_STATS_USAGE_CLASS_NAME', Event::STATS_USAGE_CLASS_NAME));
}
/**
* Add reduce.
*
* @param Document $document
* @return self
*/
public function addReduce(Document $document): self
{
$this->reduce[] = $document;
return $this;
}
/**
* Add metric.
*
* @param string $key
* @param int $value
* @return self
*/
public function addMetric(string $key, int $value): self
{
$this->metrics[] = [
'key' => $key,
'value' => $value,
];
return $this;
}
/**
* Set disabled metrics.
*
* @param string $key
* @return self
*/
public function disableMetric(string $key): self
{
$this->disabled[] = $key;
return $this;
}
/**
* Prepare the payload for the event
*
* @return array
*/
protected function preparePayload(): array
{
return [
'project' => $this->getProject(),
'reduce' => $this->reduce,
'metrics' => \array_filter($this->metrics, function ($metric) {
foreach ($this->disabled as $disabledMetric) {
if (\str_ends_with($metric['key'], $disabledMetric)) {
return false;
}
}
return true;
}),
];
}
public function reset(): Event
{
$this->metrics = [];
parent::reset();
return $this;
}
}
-1
View File
@@ -307,7 +307,6 @@ class Exception extends \Exception
/** Webhooks */
public const string WEBHOOK_NOT_FOUND = 'webhook_not_found';
public const string WEBHOOK_ALREADY_EXISTS = 'webhook_already_exists';
/** Router */
public const string ROUTER_HOST_NOT_FOUND = 'router_host_not_found';
-2
View File
@@ -16,7 +16,6 @@ use Appwrite\Platform\Modules\Storage;
use Appwrite\Platform\Modules\Teams;
use Appwrite\Platform\Modules\Tokens;
use Appwrite\Platform\Modules\VCS;
use Appwrite\Platform\Modules\Webhooks;
use Utopia\Platform\Platform;
class Appwrite extends Platform
@@ -37,6 +36,5 @@ class Appwrite extends Platform
$this->addModule(new Tokens\Module());
$this->addModule(new Storage\Module());
$this->addModule(new VCS\Module());
$this->addModule(new Webhooks\Module());
}
}
@@ -0,0 +1,42 @@
<?php
namespace Appwrite\Platform\Installer\Http\Installer;
use Appwrite\Platform\Installer\Server;
use Utopia\Http\Adapter\Swoole\Request;
use Utopia\Http\Adapter\Swoole\Response;
use Utopia\Platform\Action;
class Cleanup extends Action
{
public static function getName(): string
{
return 'installerCleanup';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/install/cleanup')
->desc('Cleanup installer container')
->inject('request')
->inject('response')
->callback($this->action(...));
}
public function action(Request $request, Response $response): void
{
if (!Validate::validateCsrf($request)) {
$response->setStatusCode(Response::STATUS_CODE_BAD_REQUEST);
$response->json(['success' => false, 'message' => 'Invalid CSRF token']);
return;
}
// Remove the installer container
$container = escapeshellarg(Server::DEFAULT_CONTAINER);
@exec("docker rm -f $container >/dev/null 2>&1");
$response->json(['success' => true]);
}
}
@@ -36,7 +36,7 @@ class Install extends Action
->param('httpPort', 80, new Range(1, 65535), 'HTTP port')
->param('httpsPort', 443, new Range(1, 65535), 'HTTPS port')
->param('emailCertificates', '', new Email(), 'Email for SSL certificates')
->param('opensslKey', '', new Text(64, 0), 'Secret API key', true)
->param('opensslKey', '', new Text(64, 1), 'Secret API key')
->param('assistantOpenAIKey', '', new Text(256, 0), 'OpenAI API key for assistant', true)
->param('accountEmail', '', new Email(allowEmpty: true), 'Account email address', true)
->param('accountPassword', '', new Password(allowEmpty: true), 'Account password', true)
@@ -93,11 +93,6 @@ class Install extends Action
$opensslKey = trim($opensslKey);
$assistantOpenAIKey = trim($assistantOpenAIKey);
if ($opensslKey === '' && !$config->isUpgrade()) {
$this->sendBadRequest($response, $swooleResponse, $wantsStream, 'Secret key is required');
return;
}
$account = [];
if (!$config->isUpgrade()) {
$accountEmail = trim($accountEmail);
@@ -4,7 +4,6 @@ namespace Appwrite\Platform\Installer\Http\Installer;
use Swoole\Http\Server as SwooleServer;
use Swoole\Timer;
use Utopia\Http\Adapter\Swoole\Request;
use Utopia\Http\Adapter\Swoole\Response;
use Utopia\Platform\Action;
@@ -23,20 +22,13 @@ class Shutdown extends Action
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/install/shutdown')
->desc('Shutdown installer server')
->inject('request')
->inject('response')
->inject('swooleServer')
->callback($this->action(...));
}
public function action(Request $request, Response $response, ?SwooleServer $swooleServer): void
public function action(Response $response, ?SwooleServer $swooleServer): void
{
if (!Validate::validateCsrf($request)) {
$response->setStatusCode(Response::STATUS_CODE_BAD_REQUEST);
$response->json(['success' => false, 'message' => 'Invalid CSRF token']);
return;
}
$response->json(['success' => true]);
if ($swooleServer) {
@@ -51,15 +51,6 @@ class Status extends Action
$data['payload']['assistantOpenAIKeyHash'],
);
}
// Strip sensitive data from step details
if (is_array($data) && isset($data['details']) && is_array($data['details'])) {
foreach ($data['details'] as $stepKey => &$stepDetails) {
if (is_array($stepDetails)) {
unset($stepDetails['sessionSecret'], $stepDetails['trace']);
}
}
unset($stepDetails);
}
$response->json(['success' => true, 'progress' => $data]);
}
}
@@ -309,7 +309,7 @@ class State
$data['updatedAt'] = $payload['updatedAt'] ?? time();
file_put_contents($this->progressFilePath($installId), json_encode($data), LOCK_EX);
file_put_contents($this->progressFilePath($installId), json_encode($data));
}
private function parseEnvFile(string $contents): array
+23 -8
View File
@@ -2,6 +2,8 @@
namespace Appwrite\Platform\Installer;
require_once __DIR__ . '/../../../../vendor/autoload.php';
use Appwrite\Platform\Installer\Http\Installer\Error;
use Appwrite\Platform\Installer\Runtime\State;
use Swoole\Http\Server as SwooleServer;
@@ -196,14 +198,27 @@ class Server
private function cleanupWebInstallerFiles(): void
{
$cwd = getcwd();
if ($cwd === false) {
return;
$baseDir = null;
try {
$cfg = $this->state->buildConfig();
if ($cfg->isLocal() && !empty($cfg->getHostPath())) {
$baseDir = rtrim($cfg->getHostPath(), '/') . '/appwrite';
}
} catch (\Throwable) {
// Fall back to cwd
}
if ($baseDir === null) {
$cwd = getcwd();
if ($cwd === false) {
return;
}
$baseDir = $cwd;
}
$filesToRemove = [
$cwd . '/.env.web-installer',
$cwd . '/docker-compose.web-installer.yml',
$baseDir . '/.env.web-installer',
$baseDir . '/docker-compose.web-installer.yml',
];
foreach ($filesToRemove as $file) {
@@ -239,7 +254,7 @@ class Server
}
}
private function ensureLocalInstallerTag(string $source, string $target): void
private function ensureCorrectTag(string $source, string $target): void
{
$sourceArg = escapeshellarg($source);
$targetArg = escapeshellarg($target);
@@ -257,7 +272,7 @@ class Server
if (!$this->dockerImageExists($image)) {
$this->buildDockerInstallerImage($image);
}
$this->ensureLocalInstallerTag($image, 'appwrite/appwrite:local');
$this->ensureCorrectTag($image, 'appwrite/appwrite:local');
$port = (string)self::INSTALLER_WEB_PORT;
$entrypoint = isset($opts['upgrade']) ? 'upgrade' : 'install';
@@ -288,6 +303,7 @@ class Server
'-p', "127.0.0.1:$port:" . self::INSTALLER_WEB_PORT,
'--volume', '/var/run/docker.sock:/var/run/docker.sock',
'--volume', "$volumePath:/usr/src/code:rw",
'--volume', "$volumePath:$volumePath:rw",
];
$args[] = '-e';
$args[] = 'APPWRITE_INSTALLER_CONFIG=' . $configJson;
@@ -309,7 +325,6 @@ function shouldRunInstallerServer(): bool
}
if (shouldRunInstallerServer()) {
require_once __DIR__ . '/../../../../vendor/autoload.php';
$server = new Server();
$server->run();
}
@@ -2,6 +2,7 @@
namespace Appwrite\Platform\Installer\Services;
use Appwrite\Platform\Installer\Http\Installer\Cleanup;
use Appwrite\Platform\Installer\Http\Installer\Complete;
use Appwrite\Platform\Installer\Http\Installer\Install;
use Appwrite\Platform\Installer\Http\Installer\Shutdown;
@@ -19,6 +20,7 @@ class Http extends Service
$this->addAction(View::getName(), new View());
$this->addAction(Status::getName(), new Status());
$this->addAction(Validate::getName(), new Validate());
$this->addAction(Cleanup::getName(), new Cleanup());
$this->addAction(Complete::getName(), new Complete());
$this->addAction(Shutdown::getName(), new Shutdown());
$this->addAction(Install::getName(), new Install());
@@ -119,7 +119,7 @@ class Update extends Action
$authenticator->setAttribute('verified', true);
$dbForProject->updateDocument('authenticators', $authenticator->getId(), new Document(['verified' => true]));
$dbForProject->updateDocument('authenticators', $authenticator->getId(), $authenticator);
$dbForProject->purgeCachedDocument('users', $user->getId());
$factors = $session->getAttribute('factors', []);
@@ -127,7 +127,7 @@ class Update extends Action
$factors = \array_values(\array_unique($factors));
$session->setAttribute('factors', $factors);
$dbForProject->updateDocument('sessions', $session->getId(), new Document(['factors' => $factors]));
$dbForProject->updateDocument('sessions', $session->getId(), $session);
$queueForEvents->setParam('userId', $user->getId());
@@ -7,6 +7,7 @@ use Appwrite\Detector\Detector;
use Appwrite\Event\Event;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@@ -14,7 +15,6 @@ use Appwrite\SDK\Deprecated;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Template\Template;
use Appwrite\Usage\Context;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
use libphonenumber\NumberParseException;
@@ -104,7 +104,7 @@ class Create extends Action
->inject('queueForMessaging')
->inject('queueForMails')
->inject('timelimit')
->inject('usage')
->inject('queueForStatsUsage')
->inject('plan')
->inject('proofForToken')
->inject('proofForCode')
@@ -124,7 +124,7 @@ class Create extends Action
Messaging $queueForMessaging,
Mail $queueForMails,
callable $timelimit,
Context $usage,
StatsUsage $queueForStatsUsage,
array $plan,
ProofsToken $proofForToken,
ProofsCode $proofForCode
@@ -201,12 +201,16 @@ class Create extends Action
$countryCode = $helper->parse($phone)->getCountryCode();
if (!empty($countryCode)) {
$usage->addMetric(str_replace('{countryCode}', $countryCode, METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE), 1);
$queueForStatsUsage
->addMetric(str_replace('{countryCode}', $countryCode, METRIC_AUTH_METHOD_PHONE_COUNTRY_CODE), 1);
}
} catch (NumberParseException $e) {
// Ignore invalid phone number for country code stats
}
$usage->addMetric(METRIC_AUTH_METHOD_PHONE, 1);
$queueForStatsUsage
->addMetric(METRIC_AUTH_METHOD_PHONE, 1)
->setProject($project)
->trigger();
break;
case Type::EMAIL:
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
@@ -117,7 +117,7 @@ class Update extends Action
$mfaRecoveryCodes = \array_diff($mfaRecoveryCodes, [$otp]);
$mfaRecoveryCodes = \array_values($mfaRecoveryCodes);
$user->setAttribute('mfaRecoveryCodes', $mfaRecoveryCodes);
$dbForProject->updateDocument('users', $user->getId(), new Document(['mfaRecoveryCodes' => $mfaRecoveryCodes]));
$dbForProject->updateDocument('users', $user->getId(), $user);
return true;
}
@@ -147,13 +147,11 @@ class Update extends Action
$factors[] = $type;
$factors = \array_values(\array_unique($factors));
$mfaUpdatedAt = DateTime::now();
$session
->setAttribute('factors', $factors)
->setAttribute('mfaUpdatedAt', $mfaUpdatedAt);
->setAttribute('mfaUpdatedAt', DateTime::now());
$dbForProject->updateDocument('sessions', $session->getId(), new Document(['factors' => $factors, 'mfaUpdatedAt' => $mfaUpdatedAt]));
$dbForProject->updateDocument('sessions', $session->getId(), $session);
$queueForEvents
->setParam('userId', $user->getId())
@@ -93,7 +93,7 @@ class Create extends Action
$mfaRecoveryCodes = Type::generateBackupCodes();
$user->setAttribute('mfaRecoveryCodes', $mfaRecoveryCodes);
$dbForProject->updateDocument('users', $user->getId(), new Document(['mfaRecoveryCodes' => $mfaRecoveryCodes]));
$dbForProject->updateDocument('users', $user->getId(), $user);
$queueForEvents->setParam('userId', $user->getId());
@@ -91,7 +91,7 @@ class Update extends Action
$mfaRecoveryCodes = Type::generateBackupCodes();
$user->setAttribute('mfaRecoveryCodes', $mfaRecoveryCodes);
$dbForProject->updateDocument('users', $user->getId(), new Document(['mfaRecoveryCodes' => $mfaRecoveryCodes]));
$dbForProject->updateDocument('users', $user->getId(), $user);
$queueForEvents->setParam('userId', $user->getId());
@@ -72,7 +72,7 @@ class Update extends Action
): void {
$user->setAttribute('mfa', $mfa);
$user = $dbForProject->updateDocument('users', $user->getId(), new Document(['mfa' => $mfa]));
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
if ($mfa) {
$factors = $session->getAttribute('factors', []);
@@ -89,7 +89,7 @@ class Update extends Action
$factors = \array_values(\array_unique($factors));
$session->setAttribute('factors', $factors);
$dbForProject->updateDocument('sessions', $session->getId(), new Document(['factors' => $factors]));
$dbForProject->updateDocument('sessions', $session->getId(), $session);
}
$queueForEvents->setParam('userId', $user->getId());
@@ -2,6 +2,7 @@
namespace Appwrite\Platform\Modules\Avatars\Http\Screenshots;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Avatars\Http\Action;
use Appwrite\SDK\AuthType;
@@ -9,7 +10,6 @@ use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\MethodType;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Usage\Context;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Domains\Domain;
@@ -84,11 +84,11 @@ class Get extends Action
->param('quality', -1, new Range(-1, 100), 'Screenshot quality. Pass an integer between 0 to 100. Defaults to keep existing image quality.', true, example: '85')
->param('output', '', new WhiteList(\array_keys(Config::getParam('storage-outputs')), true), 'Output format type (jpeg, jpg, png, gif and webp).', true, example: 'jpeg')
->inject('response')
->inject('usage')
->inject('queueForStatsUsage')
->callback($this->action(...));
}
public function action(string $url, array $headers, int $viewportWidth, int $viewportHeight, float $scale, string $theme, string $userAgent, bool $fullpage, string $locale, string $timezone, float $latitude, float $longitude, float $accuracy, bool $touch, array $permissions, int $sleep, int $width, int $height, int $quality, string $output, Response $response, Context $usage)
public function action(string $url, array $headers, int $viewportWidth, int $viewportHeight, float $scale, string $theme, string $userAgent, bool $fullpage, string $locale, string $timezone, float $latitude, float $longitude, float $accuracy, bool $touch, array $permissions, int $sleep, int $width, int $height, int $quality, string $output, Response $response, StatsUsage $queueForStatsUsage)
{
if (!\extension_loaded('imagick')) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing');
@@ -210,7 +210,7 @@ class Get extends Action
$outputs = Config::getParam('storage-outputs');
$contentType = $outputs[$output] ?? $outputs['png'];
$usage->addMetric(METRIC_AVATARS_SCREENSHOTS_GENERATED, 1);
$queueForStatsUsage->addMetric(METRIC_AVATARS_SCREENSHOTS_GENERATED, 1);
$response
->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days
+2 -12
View File
@@ -143,12 +143,7 @@ class Base extends Action
->setAttribute('latestDeploymentInternalId', $deployment->getSequence())
->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt())
->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
$dbForProject->updateDocument('functions', $function->getId(), new Document([
'latestDeploymentId' => $deployment->getId(),
'latestDeploymentInternalId' => $deployment->getSequence(),
'latestDeploymentCreatedAt' => $deployment->getCreatedAt(),
'latestDeploymentStatus' => $deployment->getAttribute('status', ''),
]));
$dbForProject->updateDocument('functions', $function->getId(), $function);
$queueForBuilds
->setType(BUILD_TYPE_DEPLOYMENT)
@@ -254,12 +249,7 @@ class Base extends Action
->setAttribute('latestDeploymentInternalId', $deployment->getSequence())
->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt())
->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
$dbForProject->updateDocument('sites', $site->getId(), new Document([
'latestDeploymentId' => $deployment->getId(),
'latestDeploymentInternalId' => $deployment->getSequence(),
'latestDeploymentCreatedAt' => $deployment->getCreatedAt(),
'latestDeploymentStatus' => $deployment->getAttribute('status', ''),
]));
$dbForProject->updateDocument('sites', $site->getId(), $site);
$sitesDomain = $platform['sitesDomain'];
$domain = ID::unique() . "." . $sitesDomain;
@@ -12,7 +12,6 @@ use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\IndexDependency as IndexDependencyValidator;
use Utopia\Database\Validator\Key;
@@ -99,8 +98,7 @@ class Delete extends Action
}
if ($attribute->getAttribute('status') === 'available') {
$attribute->setAttribute('status', 'deleting');
$attribute = $dbForProject->updateDocument('attributes', $attribute->getId(), new Document(['status' => 'deleting']));
$attribute = $dbForProject->updateDocument('attributes', $attribute->getId(), $attribute->setAttribute('status', 'deleting'));
}
$dbForProject->purgeCachedDocument('database_' . $db->getSequence(), $collectionId);
@@ -120,8 +118,7 @@ class Delete extends Action
}
if ($relatedAttribute->getAttribute('status') === 'available') {
$relatedAttribute->setAttribute('status', 'deleting');
$dbForProject->updateDocument('attributes', $relatedAttribute->getId(), new Document(['status' => 'deleting']));
$dbForProject->updateDocument('attributes', $relatedAttribute->getId(), $relatedAttribute->setAttribute('status', 'deleting'));
}
$dbForProject->purgeCachedDocument('database_' . $db->getSequence(), $options['relatedCollection']);
@@ -3,6 +3,7 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Attribute;
use Appwrite\Event\Event;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Action;
use Appwrite\SDK\AuthType;
@@ -10,7 +11,6 @@ use Appwrite\SDK\ContentType;
use Appwrite\SDK\Deprecated;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Usage\Context;
use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Response as UtopiaResponse;
use InvalidArgumentException;
@@ -83,13 +83,13 @@ class Decrement extends Action
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->inject('usage')
->inject('queueForStatsUsage')
->inject('plan')
->inject('authorization')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $min, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, Context $usage, array $plan, Authorization $authorization): void
public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $min, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage, array $plan, Authorization $authorization): void
{
$isAPIKey = User::isApp($authorization->getRoles());
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
@@ -200,7 +200,7 @@ class Decrement extends Action
)
);
$usage
$queueForStatsUsage
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, 1)
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), 1);
@@ -3,6 +3,7 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Attribute;
use Appwrite\Event\Event;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Action;
use Appwrite\SDK\AuthType;
@@ -10,7 +11,6 @@ use Appwrite\SDK\ContentType;
use Appwrite\SDK\Deprecated;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Usage\Context;
use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Response as UtopiaResponse;
use InvalidArgumentException;
@@ -83,13 +83,13 @@ class Increment extends Action
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->inject('usage')
->inject('queueForStatsUsage')
->inject('plan')
->inject('authorization')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $max, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, Context $usage, array $plan, Authorization $authorization): void
public function action(string $databaseId, string $collectionId, string $documentId, string $attribute, int|float $value, int|float|null $max, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage, array $plan, Authorization $authorization): void
{
$isAPIKey = User::isApp($authorization->getRoles());
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
@@ -200,7 +200,7 @@ class Increment extends Action
)
);
$usage
$queueForStatsUsage
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, 1)
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), 1);
@@ -3,6 +3,7 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Bulk;
use Appwrite\Event\Event;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception;
use Appwrite\Functions\EventProcessor;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Action;
@@ -11,7 +12,6 @@ use Appwrite\SDK\ContentType;
use Appwrite\SDK\Deprecated;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Usage\Context;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
@@ -76,7 +76,7 @@ class Delete extends Action
->param('transactionId', null, fn (Database $dbForProject) => new Nullable(new UID($dbForProject->getAdapter()->getMaxUIDLength())), 'Transaction ID for staging the operation.', true, ['dbForProject'])
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('queueForStatsUsage')
->inject('queueForEvents')
->inject('queueForRealtime')
->inject('queueForFunctions')
@@ -86,7 +86,7 @@ class Delete extends Action
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Context $usage, Event $queueForEvents, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan, EventProcessor $eventProcessor): void
public function action(string $databaseId, string $collectionId, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, Event $queueForEvents, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan, EventProcessor $eventProcessor): void
{
$database = $dbForProject->getDocument('databases', $databaseId);
if ($database->isEmpty()) {
@@ -185,10 +185,10 @@ class Delete extends Action
foreach ($documents as $document) {
$document->setAttribute('$databaseId', $database->getId());
$document->setAttribute('$' . $this->getCollectionsEventsContext() . 'Id', $collection->getId());
$document->setAttribute('$'.$this->getCollectionsEventsContext().'Id', $collection->getId());
}
$usage
$queueForStatsUsage
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, \max(1, $modified))
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), \max(1, $modified));
@@ -3,6 +3,7 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Bulk;
use Appwrite\Event\Event;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception;
use Appwrite\Functions\EventProcessor;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Action;
@@ -11,7 +12,6 @@ use Appwrite\SDK\ContentType;
use Appwrite\SDK\Deprecated;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Usage\Context;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
@@ -80,7 +80,7 @@ class Update extends Action
->param('transactionId', null, fn (Database $dbForProject) => new Nullable(new UID($dbForProject->getAdapter()->getMaxUIDLength())), 'Transaction ID for staging the operation.', true, ['dbForProject'])
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('queueForStatsUsage')
->inject('queueForEvents')
->inject('queueForRealtime')
->inject('queueForFunctions')
@@ -90,7 +90,7 @@ class Update extends Action
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string|array $data, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Context $usage, Event $queueForEvents, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan, EventProcessor $eventProcessor): void
public function action(string $databaseId, string $collectionId, string|array $data, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, Event $queueForEvents, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan, EventProcessor $eventProcessor): void
{
$data = \is_string($data)
? \json_decode($data, true)
@@ -216,10 +216,10 @@ class Update extends Action
foreach ($documents as $document) {
$document->setAttribute('$databaseId', $database->getId());
$document->setAttribute('$' . $this->getCollectionsEventsContext() . 'Id', $collection->getId());
$document->setAttribute('$'.$this->getCollectionsEventsContext().'Id', $collection->getId());
}
$usage
$queueForStatsUsage
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, \max(1, $modified))
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), \max(1, $modified));
@@ -3,6 +3,7 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Bulk;
use Appwrite\Event\Event;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception;
use Appwrite\Functions\EventProcessor;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Action;
@@ -11,7 +12,6 @@ use Appwrite\SDK\ContentType;
use Appwrite\SDK\Deprecated;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Usage\Context;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
@@ -78,7 +78,7 @@ class Upsert extends Action
->param('transactionId', null, fn (Database $dbForProject) => new Nullable(new UID($dbForProject->getAdapter()->getMaxUIDLength())), 'Transaction ID for staging the operation.', true, ['dbForProject'])
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('queueForStatsUsage')
->inject('queueForEvents')
->inject('queueForRealtime')
->inject('queueForFunctions')
@@ -88,7 +88,7 @@ class Upsert extends Action
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, array $documents, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Context $usage, Event $queueForEvents, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan, EventProcessor $eventProcessor): void
public function action(string $databaseId, string $collectionId, array $documents, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, Event $queueForEvents, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan, EventProcessor $eventProcessor): void
{
$database = $dbForProject->getDocument('databases', $databaseId);
if ($database->isEmpty()) {
@@ -106,7 +106,7 @@ class Upsert extends Action
);
if ($hasRelationships) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk upsert is not supported for ' . $this->getSDKNamespace() . ' with relationship attributes');
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk upsert is not supported for ' . $this->getSDKNamespace() . ' with relationship attributes');
}
foreach ($documents as $key => $document) {
@@ -191,10 +191,10 @@ class Upsert extends Action
foreach ($upserted as $document) {
$document->setAttribute('$databaseId', $database->getId());
$document->setAttribute('$' . $this->getCollectionsEventsContext() . 'Id', $collection->getId());
$document->setAttribute('$'.$this->getCollectionsEventsContext().'Id', $collection->getId());
}
$usage
$queueForStatsUsage
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, \max(1, $modified))
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), \max(1, $modified));
@@ -3,6 +3,7 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents;
use Appwrite\Event\Event;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception;
use Appwrite\Functions\EventProcessor;
use Appwrite\SDK\AuthType;
@@ -11,7 +12,6 @@ use Appwrite\SDK\Deprecated;
use Appwrite\SDK\Method;
use Appwrite\SDK\Parameter;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Usage\Context;
use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Response as UtopiaResponse;
@@ -129,7 +129,7 @@ class Create extends Action
->inject('dbForProject')
->inject('user')
->inject('queueForEvents')
->inject('usage')
->inject('queueForStatsUsage')
->inject('queueForRealtime')
->inject('queueForFunctions')
->inject('queueForWebhooks')
@@ -138,7 +138,7 @@ class Create extends Action
->inject('eventProcessor')
->callback($this->action(...));
}
public function action(string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, ?array $documents, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Document $user, Event $queueForEvents, Context $usage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan, Authorization $authorization, EventProcessor $eventProcessor): void
public function action(string $databaseId, string $documentId, string $collectionId, string|array $data, ?array $permissions, ?array $documents, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Document $user, Event $queueForEvents, StatsUsage $queueForStatsUsage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, array $plan, Authorization $authorization, EventProcessor $eventProcessor): void
{
$data = \is_string($data)
? \json_decode($data, true)
@@ -205,7 +205,7 @@ class Create extends Action
);
if ($isBulk && $hasRelationships) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk create is not supported for ' . $this->getSDKNamespace() . ' with relationship ' . $this->getStructureContext());
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk create is not supported for ' . $this->getSDKNamespace() .' with relationship ' . $this->getStructureContext());
}
$setPermissions = function (Document $document, ?array $permissions) use ($user, $isAPIKey, $isPrivilegedUser, $isBulk, $dbForProject, $authorization) {
@@ -489,7 +489,7 @@ class Create extends Action
);
}
$usage
$queueForStatsUsage
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, \max(1, $operations))
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), \max(1, $operations)); // per collection
@@ -4,13 +4,13 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documen
use Appwrite\Databases\TransactionState;
use Appwrite\Event\Event;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Deprecated;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Usage\Context;
use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
@@ -80,7 +80,7 @@ class Delete extends Action
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->inject('usage')
->inject('queueForStatsUsage')
->inject('transactionState')
->inject('plan')
->inject('authorization')
@@ -96,7 +96,7 @@ class Delete extends Action
UtopiaResponse $response,
Database $dbForProject,
Event $queueForEvents,
Context $usage,
StatsUsage $queueForStatsUsage,
TransactionState $transactionState,
array $plan,
Authorization $authorization
@@ -210,7 +210,7 @@ class Delete extends Action
authorization: $authorization
);
$usage
$queueForStatsUsage
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, 1)
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), 1); // per collection
@@ -3,13 +3,13 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents;
use Appwrite\Databases\TransactionState;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Deprecated;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Usage\Context;
use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
@@ -68,13 +68,13 @@ class Get extends Action
->param('transactionId', null, fn (Database $dbForProject) => new Nullable(new UID($dbForProject->getAdapter()->getMaxUIDLength())), 'Transaction ID to read uncommitted changes within the transaction.', true, ['dbForProject'])
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('queueForStatsUsage')
->inject('transactionState')
->inject('authorization')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $documentId, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, Context $usage, TransactionState $transactionState, Authorization $authorization): void
public function action(string $databaseId, string $collectionId, string $documentId, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, TransactionState $transactionState, Authorization $authorization): void
{
$isAPIKey = User::isApp($authorization->getRoles());
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
@@ -128,7 +128,7 @@ class Get extends Action
operations: $operations
);
$usage
$queueForStatsUsage
->addMetric(METRIC_DATABASES_OPERATIONS_READS, max($operations, 1))
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_READS), $operations);
@@ -4,13 +4,13 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documen
use Appwrite\Databases\TransactionState;
use Appwrite\Event\Event;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Deprecated;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Usage\Context;
use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
@@ -84,14 +84,14 @@ class Update extends Action
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->inject('usage')
->inject('queueForStatsUsage')
->inject('transactionState')
->inject('plan')
->inject('authorization')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, Context $usage, TransactionState $transactionState, array $plan, Authorization $authorization): void
public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage, TransactionState $transactionState, array $plan, Authorization $authorization): void
{
$data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array
@@ -246,7 +246,7 @@ class Update extends Action
$setCollection($collection, $newDocument);
$usage
$queueForStatsUsage
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, max($operations, 1))
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), $operations);
@@ -4,13 +4,13 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documen
use Appwrite\Databases\TransactionState;
use Appwrite\Event\Event;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Deprecated;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Usage\Context;
use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Response as UtopiaResponse;
@@ -88,14 +88,14 @@ class Upsert extends Action
->inject('user')
->inject('dbForProject')
->inject('queueForEvents')
->inject('usage')
->inject('queueForStatsUsage')
->inject('transactionState')
->inject('plan')
->inject('authorization')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Document $user, Database $dbForProject, Event $queueForEvents, Context $usage, TransactionState $transactionState, array $plan, Authorization $authorization): void
public function action(string $databaseId, string $collectionId, string $documentId, string|array $data, ?array $permissions, ?string $transactionId, ?\DateTime $requestTimestamp, UtopiaResponse $response, Document $user, Database $dbForProject, Event $queueForEvents, StatsUsage $queueForStatsUsage, TransactionState $transactionState, array $plan, Authorization $authorization): void
{
$data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array
@@ -256,7 +256,7 @@ class Upsert extends Action
$setCollection($collection, $newDocument);
$usage
$queueForStatsUsage
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, \max(1, $operations))
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_WRITES), \max(1, $operations));
@@ -3,13 +3,13 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents;
use Appwrite\Databases\TransactionState;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Deprecated;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Usage\Context;
use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
@@ -76,13 +76,13 @@ class XList extends Action
->inject('response')
->inject('dbForProject')
->inject('user')
->inject('usage')
->inject('queueForStatsUsage')
->inject('transactionState')
->inject('authorization')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, array $queries, ?string $transactionId, bool $includeTotal, int $ttl, UtopiaResponse $response, Database $dbForProject, Document $user, Context $usage, TransactionState $transactionState, Authorization $authorization): void
public function action(string $databaseId, string $collectionId, array $queries, ?string $transactionId, bool $includeTotal, int $ttl, UtopiaResponse $response, Database $dbForProject, Document $user, StatsUsage $queueForStatsUsage, TransactionState $transactionState, Authorization $authorization): void
{
$isAPIKey = User::isApp($authorization->getRoles());
$isPrivilegedUser = User::isPrivileged($authorization->getRoles());
@@ -231,7 +231,7 @@ class XList extends Action
);
}
$usage
$queueForStatsUsage
->addMetric(METRIC_DATABASES_OPERATIONS_READS, max($operations, 1))
->addMetric(str_replace('{databaseInternalId}', $database->getSequence(), METRIC_DATABASE_ID_OPERATIONS_READS), $operations);
@@ -12,7 +12,6 @@ use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
@@ -97,8 +96,7 @@ class Delete extends Action
// Only update status if removing available index
if ($index->getAttribute('status') === 'available') {
$index->setAttribute('status', 'deleting');
$index = $dbForProject->updateDocument('indexes', $index->getId(), new Document(['status' => 'deleting']));
$index = $dbForProject->updateDocument('indexes', $index->getId(), $index->setAttribute('status', 'deleting'));
}
$dbForProject->purgeCachedDocument('database_' . $db->getSequence(), $collectionId);
@@ -60,6 +60,7 @@ class Delete extends Action
->inject('dbForProject')
->inject('queueForDatabase')
->inject('queueForEvents')
->inject('queueForStatsUsage')
->callback($this->action(...));
}
@@ -5,13 +5,13 @@ namespace Appwrite\Platform\Modules\Databases\Http\Databases\Transactions;
use Appwrite\Databases\TransactionState;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception;
use Appwrite\Functions\EventProcessor;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Usage\Context;
use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
@@ -73,7 +73,7 @@ class Update extends Action
->inject('transactionState')
->inject('queueForDeletes')
->inject('queueForEvents')
->inject('usage')
->inject('queueForStatsUsage')
->inject('queueForRealtime')
->inject('queueForFunctions')
->inject('queueForWebhooks')
@@ -92,7 +92,7 @@ class Update extends Action
* @param TransactionState $transactionState
* @param Delete $queueForDeletes
* @param Event $queueForEvents
* @param Context $usage
* @param StatsUsage $queueForStatsUsage
* @param Event $queueForRealtime
* @param Event $queueForFunctions
* @param Event $queueForWebhooks
@@ -106,7 +106,7 @@ class Update extends Action
* @throws Structure
* @throws \Utopia\Http\Exception
*/
public function action(string $transactionId, bool $commit, bool $rollback, UtopiaResponse $response, Database $dbForProject, Document $user, TransactionState $transactionState, Delete $queueForDeletes, Event $queueForEvents, Context $usage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, Authorization $authorization, EventProcessor $eventProcessor): void
public function action(string $transactionId, bool $commit, bool $rollback, UtopiaResponse $response, Database $dbForProject, Document $user, TransactionState $transactionState, Delete $queueForDeletes, Event $queueForEvents, StatsUsage $queueForStatsUsage, Event $queueForRealtime, Event $queueForFunctions, Event $queueForWebhooks, Authorization $authorization, EventProcessor $eventProcessor): void
{
if (!$commit && !$rollback) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Either commit or rollback must be true');
@@ -142,7 +142,7 @@ class Update extends Action
$currentDocumentId = null;
try {
$dbForProject->withTransaction(function () use ($dbForProject, $transactionState, $queueForDeletes, $transactionId, &$transaction, &$operations, &$totalOperations, &$databaseOperations, &$currentDocumentId, $queueForEvents, $usage, $queueForRealtime, $queueForFunctions, $queueForWebhooks, $authorization) {
$dbForProject->withTransaction(function () use ($dbForProject, $transactionState, $queueForDeletes, $transactionId, &$transaction, &$operations, &$totalOperations, &$databaseOperations, &$currentDocumentId, $queueForEvents, $queueForStatsUsage, $queueForRealtime, $queueForFunctions, $queueForWebhooks, $authorization) {
$authorization->skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([
'status' => 'committing',
])));
@@ -279,10 +279,11 @@ class Update extends Action
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$usage->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, $totalOperations);
$queueForStatsUsage
->addMetric(METRIC_DATABASES_OPERATIONS_WRITES, $totalOperations);
foreach ($databaseOperations as $sequence => $count) {
$usage->addMetric(
$queueForStatsUsage->addMetric(
str_replace('{databaseInternalId}', $sequence, METRIC_DATABASE_ID_OPERATIONS_WRITES),
$count
);
@@ -50,6 +50,7 @@ class Delete extends DatabaseDelete
->inject('dbForProject')
->inject('queueForDatabase')
->inject('queueForEvents')
->inject('queueForStatsUsage')
->callback($this->action(...));
}
}
@@ -61,7 +61,7 @@ class Delete extends DocumentsDelete
->param('transactionId', null, fn (Database $dbForProject) => new Nullable(new UID($dbForProject->getAdapter()->getMaxUIDLength())), 'Transaction ID for staging the operation.', true, ['dbForProject'])
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('queueForStatsUsage')
->inject('queueForEvents')
->inject('queueForRealtime')
->inject('queueForFunctions')
@@ -63,7 +63,7 @@ class Update extends DocumentsUpdate
->param('transactionId', null, fn (Database $dbForProject) => new Nullable(new UID($dbForProject->getAdapter()->getMaxUIDLength())), 'Transaction ID for staging the operation.', true, ['dbForProject'])
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('queueForStatsUsage')
->inject('queueForEvents')
->inject('queueForRealtime')
->inject('queueForFunctions')
@@ -63,7 +63,7 @@ class Upsert extends DocumentsUpsert
->param('transactionId', null, fn (Database $dbForProject) => new Nullable(new UID($dbForProject->getAdapter()->getMaxUIDLength())), 'Transaction ID for staging the operation.', true, ['dbForProject'])
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('queueForStatsUsage')
->inject('queueForEvents')
->inject('queueForRealtime')
->inject('queueForFunctions')
@@ -66,7 +66,7 @@ class Decrement extends DecrementDocumentAttribute
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->inject('usage')
->inject('queueForStatsUsage')
->inject('plan')
->inject('authorization')
->callback($this->action(...));

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