diff --git a/.github/workflows/ai-moderator.yml b/.github/workflows/ai-moderator.yml
index 483f3dbeee..948fa6c0c1 100644
--- a/.github/workflows/ai-moderator.yml
+++ b/.github/workflows/ai-moderator.yml
@@ -25,6 +25,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: AI Moderator
- uses: github/ai-moderator@v1
+ uses: github/ai-moderator@81159c370785e295c97461ade67d7c33576e9319 # v1.1.4
with:
token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/auto-label-issue.yml b/.github/workflows/auto-label-issue.yml
index e0eb0de98d..0151c2f9c1 100644
--- a/.github/workflows/auto-label-issue.yml
+++ b/.github/workflows/auto-label-issue.yml
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Issue Labeler
- uses: github/issue-labeler@v3.4
+ uses: github/issue-labeler@c1b0f9f52a63158c4adc09425e858e87b32e9685 # v3.4
with:
configuration-path: .github/labeler.yml
enable-versioned-regex: false
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 8cc3b3e113..ac16c80264 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -32,7 +32,7 @@ jobs:
actions: read
security-events: write
contents: read
- uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@v2.3.3"
+ uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@c51854704019a247608d928f370c98740469d4b5" # v2.3.5
security:
name: Checks / Image
@@ -43,13 +43,13 @@ jobs:
security-events: write
steps:
- name: Check out code
- uses: actions/checkout@v6
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
submodules: 'recursive'
- name: Build the Docker image
- uses: docker/build-push-action@v6
+ uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
push: false
@@ -58,7 +58,7 @@ jobs:
target: production
- name: Run Trivy vulnerability scanner on image
- uses: aquasecurity/trivy-action@0.35.0
+ uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
image-ref: 'pr_image:${{ github.sha }}'
format: 'sarif'
@@ -66,7 +66,7 @@ jobs:
severity: 'CRITICAL,HIGH'
- name: Run Trivy vulnerability scanner on source code
- uses: aquasecurity/trivy-action@0.35.0
+ uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
scan-type: 'fs'
scan-ref: '.'
@@ -76,14 +76,14 @@ jobs:
skip-setup-trivy: true
- name: Upload image scan results
- uses: github/codeql-action/upload-sarif@v4
+ uses: github/codeql-action/upload-sarif@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
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
+ uses: github/codeql-action/upload-sarif@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
if: always() && hashFiles('trivy-fs-results.sarif') != ''
with:
sarif_file: 'trivy-fs-results.sarif'
@@ -94,10 +94,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
- uses: actions/checkout@v6
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup PHP
- uses: shivammathur/setup-php@v2
+ uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # 2.37.0
with:
php-version: '8.3'
tools: composer:v2
@@ -119,7 +119,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
- uses: actions/checkout@v6
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 2
@@ -127,7 +127,7 @@ jobs:
if: github.event_name == 'pull_request'
- name: Setup PHP
- uses: shivammathur/setup-php@v2
+ uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # 2.37.0
with:
php-version: '8.3'
tools: composer:v2
@@ -144,10 +144,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out the repo
- uses: actions/checkout@v6
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup PHP
- uses: shivammathur/setup-php@v2
+ uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # 2.37.0
with:
php-version: '8.3'
tools: composer:v2
@@ -157,7 +157,7 @@ jobs:
run: composer install --prefer-dist --no-progress --ignore-platform-reqs
- name: Cache PHPStan result cache
- uses: actions/cache@v4
+ uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: .phpstan-cache
key: phpstan-${{ github.sha }}
@@ -172,10 +172,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out the repo
- uses: actions/checkout@v6
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup PHP
- uses: shivammathur/setup-php@v2
+ uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # 2.37.0
with:
php-version: '8.3'
extensions: swoole
@@ -193,10 +193,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out the repo
- uses: actions/checkout@v6
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Node
- uses: actions/setup-node@v4
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '24'
@@ -212,7 +212,7 @@ jobs:
steps:
- name: Generate matrix
id: generate
- uses: actions/github-script@v8
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const allDatabases = ['MariaDB', 'PostgreSQL', 'MongoDB'];
@@ -253,28 +253,28 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
- uses: actions/checkout@v6
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- name: Login to Docker Hub
- uses: docker/login-action@v4
+ uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
- uses: docker/login-action@v4
+ uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v4
+ uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Build and push Appwrite
- uses: docker/build-push-action@v6
+ uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
push: true
@@ -297,16 +297,16 @@ jobs:
packages: read
steps:
- name: checkout
- uses: actions/checkout@v6
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Login to Docker Hub
- uses: docker/login-action@v4
+ uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
- uses: docker/login-action@v4
+ uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -327,7 +327,7 @@ jobs:
run: docker compose exec -T appwrite vars
- name: Run Unit Tests
- uses: itznotabug/php-retry@v3
+ uses: itznotabug/php-retry@d6bef45a8bff490babfb613e33b00d133e4062f0 # v3
with:
max_attempts: 2
retry_wait_seconds: 60
@@ -350,16 +350,16 @@ jobs:
packages: read
steps:
- name: checkout
- uses: actions/checkout@v6
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Login to Docker Hub
- uses: docker/login-action@v4
+ uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
- uses: docker/login-action@v4
+ uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -385,7 +385,7 @@ jobs:
done
- name: Run General Tests
- uses: itznotabug/php-retry@v3
+ uses: itznotabug/php-retry@d6bef45a8bff490babfb613e33b00d133e4062f0 # v3
with:
max_attempts: 2
retry_wait_seconds: 60
@@ -406,7 +406,7 @@ jobs:
e2e_service:
name: Tests / E2E / ${{ matrix.database }} (${{ matrix.mode }}) / ${{ matrix.service }}
- runs-on: ${{ matrix.runner || 'ubuntu-latest' }}
+ runs-on: ${{ matrix.runner || format('runs-on={0}/runner=4cpu-linux-x64/volume=120g/spot=false', github.run_id) }}
needs: [build, matrix]
permissions:
contents: read
@@ -427,6 +427,7 @@ jobs:
FunctionsSchedule,
GraphQL,
Health,
+ Advisor,
Locale,
Projects,
Realtime,
@@ -445,26 +446,18 @@ jobs:
]
include:
- service: Databases
- runner: blacksmith-4vcpu-ubuntu-2404
+ runner: runs-on=${{ github.run_id }}/runner=8cpu-linux-x64/volume=120g/spot=false
paratest_processes: 3
timeout_minutes: 30
- - service: Sites
- runner: blacksmith-4vcpu-ubuntu-2404
- - service: Functions
- runner: blacksmith-4vcpu-ubuntu-2404
- - service: Avatars
- runner: blacksmith-4vcpu-ubuntu-2404
- - service: Realtime
- runner: blacksmith-4vcpu-ubuntu-2404
- service: TablesDB
- runner: blacksmith-4vcpu-ubuntu-2404
+ runner: runs-on=${{ github.run_id }}/runner=8cpu-linux-x64/volume=120g/spot=false
paratest_processes: 3
timeout_minutes: 30
- service: Migrations
paratest_processes: 1
steps:
- name: Checkout repository
- uses: actions/checkout@v6
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set environment
run: |
@@ -488,13 +481,13 @@ jobs:
fi
- name: Login to Docker Hub
- uses: docker/login-action@v4
+ uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
- uses: docker/login-action@v4
+ uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -525,7 +518,7 @@ jobs:
done
- name: Run tests
- uses: itznotabug/php-retry@v3
+ uses: itznotabug/php-retry@d6bef45a8bff490babfb613e33b00d133e4062f0 # v3
with:
max_attempts: 2
retry_wait_seconds: 60
@@ -573,18 +566,18 @@ jobs:
mode: ${{ fromJSON(needs.matrix.outputs.modes) }}
steps:
- name: Checkout repository
- uses: actions/checkout@v6
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 1
- name: Login to Docker Hub
- uses: docker/login-action@v4
+ uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
- uses: docker/login-action@v4
+ uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -607,7 +600,7 @@ jobs:
docker compose up -d --quiet-pull --wait
- name: Run tests
- uses: itznotabug/php-retry@v3
+ uses: itznotabug/php-retry@d6bef45a8bff490babfb613e33b00d133e4062f0 # v3
with:
max_attempts: 2
retry_wait_seconds: 60
@@ -640,16 +633,16 @@ jobs:
mode: ${{ fromJSON(needs.matrix.outputs.modes) }}
steps:
- name: Checkout repository
- uses: actions/checkout@v6
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Login to Docker Hub
- uses: docker/login-action@v4
+ uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
- uses: docker/login-action@v4
+ uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -679,7 +672,7 @@ jobs:
done
- name: Run tests
- uses: itznotabug/php-retry@v3
+ uses: itznotabug/php-retry@d6bef45a8bff490babfb613e33b00d133e4062f0 # v3
with:
max_attempts: 2
retry_wait_seconds: 60
@@ -711,18 +704,18 @@ jobs:
packages: read
steps:
- name: Checkout repository
- uses: actions/checkout@v6
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 1
- name: Login to Docker Hub
- uses: docker/login-action@v4
+ uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
- uses: docker/login-action@v4
+ uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -735,7 +728,7 @@ jobs:
docker tag ${{ env.REGISTRY_IMAGE }}:${{ github.sha }} ${{ env.IMAGE }}:after
- name: Setup k6
- uses: grafana/setup-k6-action@ffe7d7290dfa715e48c2ccc924d068444c94bde2
+ uses: grafana/setup-k6-action@db07bd9765aac508ef18982e52ab937fe633a065 # v1.2.1
with:
k6-version: ${{ env.K6_VERSION }}
@@ -774,7 +767,7 @@ jobs:
- name: Benchmark before
if: steps.benchmark_before_start.outcome == 'success'
continue-on-error: true
- uses: grafana/run-k6-action@a15e2072ede004e8d46141e33d7f7dad8ad08d9d
+ uses: grafana/run-k6-action@de51a7390bdf0ac85a3bef493691bd71d4c7c158 # v1.4.0
env:
APPWRITE_ENDPOINT: 'http://localhost/v1'
APPWRITE_BENCHMARK_ITERATIONS: '5'
@@ -826,7 +819,7 @@ jobs:
- name: Benchmark after
id: benchmark_after
continue-on-error: true
- uses: grafana/run-k6-action@a15e2072ede004e8d46141e33d7f7dad8ad08d9d
+ uses: grafana/run-k6-action@de51a7390bdf0ac85a3bef493691bd71d4c7c158 # v1.4.0
env:
APPWRITE_ENDPOINT: 'http://localhost/v1'
APPWRITE_BENCHMARK_ITERATIONS: '5'
@@ -846,7 +839,7 @@ jobs:
- name: Comment on PR
if: always()
- uses: actions/github-script@v8
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
BENCHMARK_BASE_REF: ${{ github.event.pull_request.base.ref }}
BENCHMARK_HEAD_REF: ${{ github.event.pull_request.head.ref }}
@@ -856,7 +849,7 @@ jobs:
await comment({ github, context, core });
- name: Save results
- uses: actions/upload-artifact@v7
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: ${{ !cancelled() }}
with:
name: benchmark-results
diff --git a/.github/workflows/cleanup-cache.yml b/.github/workflows/cleanup-cache.yml
index 4b6b13d35d..e4f28816be 100644
--- a/.github/workflows/cleanup-cache.yml
+++ b/.github/workflows/cleanup-cache.yml
@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code
- uses: actions/checkout@v6
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Cleanup
run: |
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 7edfde0aae..cb9b09b496 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -34,7 +34,7 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v6
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
@@ -47,14 +47,14 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@v2
+ uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
with:
languages: ${{ matrix.language }}
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
- uses: github/codeql-action/autobuild@v2
+ uses: github/codeql-action/autobuild@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
# âšī¸ Command-line programs to run using the OS shell.
# đ https://git.io/JvXDl
@@ -68,4 +68,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v2
+ uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml
index c4289678bb..0a49f658ac 100644
--- a/.github/workflows/nightly.yml
+++ b/.github/workflows/nightly.yml
@@ -10,13 +10,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code
- uses: actions/checkout@v6
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- name: Build the Docker image
run: DOCKER_BUILDKIT=1 docker build . --target production -t appwrite_image:latest
- name: Run Trivy vulnerability scanner on image
- uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0
+ uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
image-ref: 'appwrite_image:latest'
format: 'sarif'
@@ -24,7 +24,7 @@ jobs:
ignore-unfixed: 'false'
severity: 'CRITICAL,HIGH'
- name: Upload Docker Image Scan Results
- uses: github/codeql-action/upload-sarif@v4
+ uses: github/codeql-action/upload-sarif@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
if: always() && hashFiles('trivy-image-results.sarif') != ''
with:
sarif_file: 'trivy-image-results.sarif'
@@ -35,16 +35,16 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code
- uses: actions/checkout@v6
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Run Trivy vulnerability scanner on filesystem
- uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0
+ uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
scan-type: 'fs'
format: 'sarif'
output: 'trivy-fs-results.sarif'
severity: 'CRITICAL,HIGH'
- name: Upload Code Scan Results
- uses: github/codeql-action/upload-sarif@v4
+ uses: github/codeql-action/upload-sarif@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
if: always() && hashFiles('trivy-fs-results.sarif') != ''
with:
sarif_file: 'trivy-fs-results.sarif'
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 692861d44d..68ab657213 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -12,33 +12,33 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v6
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 2
submodules: recursive
- name: Set up QEMU
- uses: docker/setup-qemu-action@v3
+ uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
+ uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Login to Docker Hub
- uses: docker/login-action@v3
+ uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
- uses: docker/metadata-action@v4
+ uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
with:
images: appwrite/cloud
tags: |
type=ref,event=tag
- name: Build & Publish to DockerHub
- uses: docker/build-push-action@v6
+ uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
platforms: linux/amd64,linux/arm64
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 84fc4c9fba..ed4e46d811 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -11,7 +11,7 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v6
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
@@ -20,20 +20,20 @@ jobs:
submodules: recursive
- name: Set up QEMU
- uses: docker/setup-qemu-action@v3
+ uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
+ uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Login to Docker Hub
- uses: docker/login-action@v3
+ uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
- uses: docker/metadata-action@v4
+ uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
with:
images: appwrite/appwrite
tags: |
@@ -42,7 +42,7 @@ jobs:
type=semver,pattern={{major}}
- name: Build and push
- uses: docker/build-push-action@v6
+ uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
platforms: linux/amd64,linux/arm64
diff --git a/.github/workflows/sdk-preview.yml b/.github/workflows/sdk-preview.yml
index f81346a7d1..dacc37a64a 100644
--- a/.github/workflows/sdk-preview.yml
+++ b/.github/workflows/sdk-preview.yml
@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
- uses: actions/checkout@v6
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set SDK type
id: set-sdk
@@ -49,7 +49,7 @@ jobs:
docker compose exec appwrite sdks --platform=${{ steps.set-sdk.outputs.platform }} --sdk=${{ steps.set-sdk.outputs.sdk_type }} --version=latest --git=no
sudo chown -R $USER:$USER ./app/sdks/${{ steps.set-sdk.outputs.platform }}-${{ steps.set-sdk.outputs.sdk_type }}
- - uses: actions/setup-node@v4
+ - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 20
diff --git a/.github/workflows/specs.yml b/.github/workflows/specs.yml
index 6f377354d5..85c76bacd3 100644
--- a/.github/workflows/specs.yml
+++ b/.github/workflows/specs.yml
@@ -31,7 +31,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
- uses: actions/checkout@v6
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
index 6e4a8ba73b..73b767aafe 100644
--- a/.github/workflows/stale.yml
+++ b/.github/workflows/stale.yml
@@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/stale@v10
+ - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
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."
diff --git a/AGENTS.md b/AGENTS.md
index 4c5db871d7..b84bc89c3b 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -115,6 +115,14 @@ Common injections: `$response`, `$request`, `$dbForProject`, `$dbForPlatform`, `
- Never hardcode credentials -- use environment variables.
- Code changes may require container restart. No central log location -- check relevant containers.
+## Tracing with Utopia Span
+
+In handlers, only call `Span::add($key, $value)`. **Never** call `Span::init`, `Span::error`, or `Span::finish` -- lifecycle is owned by the entry-point harness (`app/http.php`, `app/worker.php`, `app/realtime.php`, `Bus::dispatch`). For selective export, filter in the sampler in `app/init/span.php`.
+
+Keys are `snake_case` with dots only for child relationships: `project.id` (id of project), `storage.bucket.id`. No dot otherwise: `inbound_bytes`, not `inbound.bytes`. No camelCase, no bare top-level keys (`function.id`, not `functionId`).
+
+Cross-cutting identifiers (`project.id`, `function.id`, `user.id`) live at the top level, not under a subsystem (no `realtime.project.id`). The trace sampler and downstream filters look them up by the canonical key.
+
## Patch release process
For bumping patch versions (e.g., `1.9.0` -> `1.9.1`), follow the checklist in `.claude/skills/patch-release-checklist/SKILL.md`. It covers the 4 files that must be updated, console image bumps, CHANGES.md updates, and common pitfalls to avoid.
diff --git a/app/cli.php b/app/cli.php
index ada155c4dc..9ad223a3ff 100644
--- a/app/cli.php
+++ b/app/cli.php
@@ -2,10 +2,11 @@
require_once __DIR__ . '/init.php';
-use Appwrite\Event\Delete;
use Appwrite\Event\Event;
-use Appwrite\Event\Func;
use Appwrite\Event\Publisher\Certificate as CertificatePublisher;
+use Appwrite\Event\Publisher\Database as DatabasePublisher;
+use Appwrite\Event\Publisher\Delete as DeletePublisher;
+use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Event\Publisher\StatsResources as StatsResourcesPublisher;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Platform\Appwrite;
@@ -281,12 +282,18 @@ $container->set('publisherForStatsResources', fn (Publisher $publisher) => new S
$publisher,
new Queue(System::getEnv('_APP_STATS_RESOURCES_QUEUE_NAME', Event::STATS_RESOURCES_QUEUE_NAME))
), ['publisher']);
-$container->set('queueForFunctions', function (Publisher $publisher) {
- return new Func($publisher);
-}, ['publisher']);
-$container->set('queueForDeletes', function (Publisher $publisher) {
- return new Delete($publisher);
-}, ['publisher']);
+$container->set('publisherForFunctions', fn (Publisher $publisher) => new FunctionPublisher(
+ $publisher,
+ new Queue(System::getEnv('_APP_FUNCTIONS_QUEUE_NAME', Event::FUNCTIONS_QUEUE_NAME), 'utopia-queue', Event::FUNCTIONS_QUEUE_TTL)
+), ['publisher']);
+$container->set('publisherForDatabase', fn (Publisher $publisherDatabases) => new DatabasePublisher(
+ $publisherDatabases,
+ new Queue(System::getEnv('_APP_DATABASE_QUEUE_NAME', Event::DATABASE_QUEUE_NAME))
+), ['publisherDatabases']);
+$container->set('publisherForDeletes', fn (Publisher $publisher) => new DeletePublisher(
+ $publisher,
+ new Queue(System::getEnv('_APP_DELETE_QUEUE_NAME', Event::DELETE_QUEUE_NAME))
+), ['publisher']);
$container->set('logError', function (Registry $register) {
return function (Throwable $error, string $namespace, string $action) use ($register) {
Console::error('[Error] Timestamp: ' . date('c', time()));
diff --git a/app/config/collections/common.php b/app/config/collections/common.php
index 80bb717423..37fbcc8ca3 100644
--- a/app/config/collections/common.php
+++ b/app/config/collections/common.php
@@ -1523,6 +1523,13 @@ return [
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
+ [
+ '$id' => ID::custom('_key_team_confirm'),
+ 'type' => Database::INDEX_KEY,
+ 'attributes' => ['teamInternalId', 'confirm'],
+ 'lengths' => [],
+ 'orders' => [],
+ ],
],
],
diff --git a/app/config/collections/platform.php b/app/config/collections/platform.php
index 6195c11724..7496b7a9a7 100644
--- a/app/config/collections/platform.php
+++ b/app/config/collections/platform.php
@@ -404,6 +404,13 @@ $platformCollections = [
'lengths' => [],
'orders' => [],
],
+ [
+ '$id' => ID::custom('_key_teamInternalId'),
+ 'type' => Database::INDEX_KEY,
+ 'attributes' => ['teamInternalId'],
+ 'lengths' => [Database::LENGTH_KEY],
+ 'orders' => [Database::ORDER_ASC],
+ ],
],
],
@@ -635,6 +642,13 @@ $platformCollections = [
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
+ [
+ '$id' => ID::custom('_key_project_id'),
+ 'type' => Database::INDEX_KEY,
+ 'attributes' => ['projectId'],
+ 'lengths' => [Database::LENGTH_KEY],
+ 'orders' => [Database::ORDER_ASC],
+ ],
],
],
@@ -1007,7 +1021,14 @@ $platformCollections = [
'attributes' => ['projectInternalId'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
- ]
+ ],
+ [
+ '$id' => ID::custom('_key_project_id'),
+ 'type' => Database::INDEX_KEY,
+ 'attributes' => ['projectId'],
+ 'lengths' => [Database::LENGTH_KEY],
+ 'orders' => [Database::ORDER_ASC],
+ ],
],
],
@@ -1935,6 +1956,440 @@ $platformCollections = [
'attributes' => [],
'indexes' => []
],
+
+ 'reports' => [
+ '$collection' => ID::custom(Database::METADATA),
+ '$id' => ID::custom('reports'),
+ 'name' => 'Reports',
+ 'attributes' => [
+ [
+ '$id' => ID::custom('projectInternalId'),
+ 'type' => Database::VAR_ID,
+ 'format' => '',
+ 'size' => 0,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('projectId'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => Database::LENGTH_KEY,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('appInternalId'),
+ 'type' => Database::VAR_ID,
+ 'format' => '',
+ 'size' => 0,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('appId'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => Database::LENGTH_KEY,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('type'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 64,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('title'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 256,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('summary'),
+ 'type' => Database::VAR_TEXT,
+ 'format' => '',
+ 'size' => 65535,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => '',
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ // Resource type the report is about. Plural noun, e.g. databases, sites, urls.
+ '$id' => ID::custom('targetType'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 64,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ // Free-form target identifier (URL for lighthouse, resource ID for db).
+ // Indexed by `_key_project_target` with an explicit prefix length.
+ '$id' => ID::custom('target'),
+ 'type' => Database::VAR_TEXT,
+ 'format' => '',
+ 'size' => 65535,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ // Category strings, e.g. 'performance', 'accessibility'. Native array
+ // column â we never query on individual entries (MySQL JSON-array
+ // indexes are weak), this is read+rewrite only.
+ '$id' => ID::custom('categories'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 64,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => null,
+ 'array' => true,
+ 'filters' => [],
+ ],
+ [
+ // Virtual attribute â insights live in the `insights` collection
+ // back-referenced by `reportInternalId`. The subQuery filter joins
+ // them at read time.
+ '$id' => ID::custom('insights'),
+ 'type' => Database::VAR_TEXT,
+ 'format' => '',
+ 'size' => 65535,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => ['subQueryReportInsights'],
+ ],
+ [
+ '$id' => ID::custom('analyzedAt'),
+ 'type' => Database::VAR_DATETIME,
+ 'format' => '',
+ 'size' => 0,
+ 'signed' => false,
+ 'required' => false,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => ['datetime'],
+ ],
+ ],
+ 'indexes' => [
+ [
+ '$id' => ID::custom('_key_project_app_type'),
+ 'type' => Database::INDEX_KEY,
+ 'attributes' => ['projectInternalId', 'appInternalId', 'type'],
+ 'lengths' => [],
+ 'orders' => [],
+ ],
+ [
+ '$id' => ID::custom('_key_project_target'),
+ 'type' => Database::INDEX_KEY,
+ 'attributes' => ['projectInternalId', 'appInternalId', 'targetType', 'target'],
+ 'lengths' => [null, null, null, 700],
+ 'orders' => [],
+ ],
+ ],
+ ],
+
+ 'insights' => [
+ '$collection' => ID::custom(Database::METADATA),
+ '$id' => ID::custom('insights'),
+ 'name' => 'Insights',
+ 'attributes' => [
+ [
+ '$id' => ID::custom('projectInternalId'),
+ 'type' => Database::VAR_ID,
+ 'format' => '',
+ 'size' => 0,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('projectId'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => Database::LENGTH_KEY,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('reportInternalId'),
+ 'type' => Database::VAR_ID,
+ 'format' => '',
+ 'size' => 0,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('reportId'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => Database::LENGTH_KEY,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => '',
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('type'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 64,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('severity'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 16,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('status'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 16,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => 'active',
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('resourceType'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 64,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('resourceId'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => Database::LENGTH_KEY,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('resourceInternalId'),
+ 'type' => Database::VAR_ID,
+ 'format' => '',
+ 'size' => 0,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('parentResourceType'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 64,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => '',
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('parentResourceId'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => Database::LENGTH_KEY,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => '',
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('parentResourceInternalId'),
+ 'type' => Database::VAR_ID,
+ 'format' => '',
+ 'size' => 0,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('title'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => 256,
+ 'signed' => true,
+ 'required' => true,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('summary'),
+ 'type' => Database::VAR_TEXT,
+ 'format' => '',
+ 'size' => 65535,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => '',
+ 'array' => false,
+ 'filters' => [],
+ ],
+ [
+ '$id' => ID::custom('ctas'),
+ 'type' => Database::VAR_TEXT,
+ 'format' => '',
+ 'size' => 65535,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => ['json'],
+ ],
+ [
+ '$id' => ID::custom('analyzedAt'),
+ 'type' => Database::VAR_DATETIME,
+ 'format' => '',
+ 'size' => 0,
+ 'signed' => false,
+ 'required' => false,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => ['datetime'],
+ ],
+ [
+ '$id' => ID::custom('dismissedAt'),
+ 'type' => Database::VAR_DATETIME,
+ 'format' => '',
+ 'size' => 0,
+ 'signed' => false,
+ 'required' => false,
+ 'default' => null,
+ 'array' => false,
+ 'filters' => ['datetime'],
+ ],
+ [
+ '$id' => ID::custom('dismissedBy'),
+ 'type' => Database::VAR_STRING,
+ 'format' => '',
+ 'size' => Database::LENGTH_KEY,
+ 'signed' => true,
+ 'required' => false,
+ 'default' => '',
+ 'array' => false,
+ 'filters' => [],
+ ],
+ ],
+ 'indexes' => [
+ [
+ '$id' => ID::custom('_key_project_report'),
+ 'type' => Database::INDEX_KEY,
+ 'attributes' => ['projectInternalId', 'reportInternalId'],
+ 'lengths' => [],
+ 'orders' => [],
+ ],
+ [
+ '$id' => ID::custom('_key_project_resource'),
+ 'type' => Database::INDEX_KEY,
+ 'attributes' => ['projectInternalId', 'resourceType', 'resourceId'],
+ 'lengths' => [],
+ 'orders' => [],
+ ],
+ [
+ '$id' => ID::custom('_key_project_parent_resource'),
+ 'type' => Database::INDEX_KEY,
+ 'attributes' => ['projectInternalId', 'parentResourceType', 'parentResourceId'],
+ 'lengths' => [],
+ 'orders' => [],
+ ],
+ [
+ '$id' => ID::custom('_key_project_type'),
+ 'type' => Database::INDEX_KEY,
+ 'attributes' => ['projectInternalId', 'type'],
+ 'lengths' => [],
+ 'orders' => [],
+ ],
+ [
+ '$id' => ID::custom('_key_project_severity'),
+ 'type' => Database::INDEX_KEY,
+ 'attributes' => ['projectInternalId', 'severity'],
+ 'lengths' => [],
+ 'orders' => [],
+ ],
+ [
+ '$id' => ID::custom('_key_project_status'),
+ 'type' => Database::INDEX_KEY,
+ 'attributes' => ['projectInternalId', 'status'],
+ 'lengths' => [],
+ 'orders' => [],
+ ],
+ [
+ '$id' => ID::custom('_key_project_dismissedAt'),
+ 'type' => Database::INDEX_KEY,
+ 'attributes' => ['projectInternalId', 'dismissedAt'],
+ 'lengths' => [],
+ 'orders' => [Database::ORDER_ASC, Database::ORDER_DESC],
+ ],
+ ],
+ ],
+
];
// Organization API keys subquery
diff --git a/app/config/errors.php b/app/config/errors.php
index fa112bcb6f..42ce9ac91b 100644
--- a/app/config/errors.php
+++ b/app/config/errors.php
@@ -623,6 +623,11 @@ return [
'description' => 'Synchronous function execution timed out. Use asynchronous execution instead, or ensure the execution duration doesn\'t exceed 30 seconds.',
'code' => 408,
],
+ Exception::FUNCTION_ASYNCHRONOUS_TIMEOUT => [
+ 'name' => Exception::FUNCTION_ASYNCHRONOUS_TIMEOUT,
+ 'description' => 'Asynchronous function execution timed out. Ensure the execution duration doesn\'t exceed the configured function timeout.',
+ 'code' => 408,
+ ],
Exception::FUNCTION_TEMPLATE_NOT_FOUND => [
'name' => Exception::FUNCTION_TEMPLATE_NOT_FOUND,
'description' => 'Function Template with the requested ID could not be found.',
@@ -687,6 +692,11 @@ return [
'description' => 'Build with the requested ID failed. Please check the logs for more information.',
'code' => 400,
],
+ Exception::BUILD_TIMEOUT => [
+ 'name' => Exception::BUILD_TIMEOUT,
+ 'description' => 'Build timed out. Increase the build timeout via the `_APP_COMPUTE_BUILD_TIMEOUT` environment variable, or simplify the build to complete within the limit.',
+ 'code' => 408,
+ ],
/** Deployments */
Exception::DEPLOYMENT_NOT_FOUND => [
@@ -1236,6 +1246,26 @@ return [
'description' => 'The specified database type is not supported for CSV import or export operations.',
'code' => 400,
],
+ Exception::MIGRATION_SOURCE_PROJECT_ID_REQUIRED => [
+ 'name' => Exception::MIGRATION_SOURCE_PROJECT_ID_REQUIRED,
+ 'description' => 'A source projectId is required for Appwrite migrations. Provide it in the migration credentials.',
+ 'code' => 400,
+ ],
+ Exception::MIGRATION_SOURCE_PROJECT_NOT_FOUND => [
+ 'name' => Exception::MIGRATION_SOURCE_PROJECT_NOT_FOUND,
+ 'description' => 'The source project for the provided projectId was not found. Verify the projectId and the API key has access to it.',
+ 'code' => 404,
+ ],
+ Exception::MIGRATION_SOURCE_TYPE_INVALID => [
+ 'name' => Exception::MIGRATION_SOURCE_TYPE_INVALID,
+ 'description' => 'The migration source type is invalid. Use one of the supported source types.',
+ 'code' => 400,
+ ],
+ Exception::MIGRATION_DESTINATION_TYPE_INVALID => [
+ 'name' => Exception::MIGRATION_DESTINATION_TYPE_INVALID,
+ 'description' => 'The migration destination type is invalid. Use one of the supported destination types.',
+ 'code' => 400,
+ ],
/** Realtime */
Exception::REALTIME_MESSAGE_FORMAT_INVALID => [
@@ -1423,4 +1453,28 @@ return [
'description' => 'The maximum number of mock phones for this project has been reached.',
'code' => 400,
],
+
+ /** Advisor */
+ Exception::INSIGHT_NOT_FOUND => [
+ 'name' => Exception::INSIGHT_NOT_FOUND,
+ 'description' => 'Insight with the requested ID could not be found.',
+ 'code' => 404,
+ ],
+ Exception::INSIGHT_ALREADY_EXISTS => [
+ 'name' => Exception::INSIGHT_ALREADY_EXISTS,
+ 'description' => 'Insight with the requested ID already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
+ 'code' => 409,
+ ],
+
+ /** Reports */
+ Exception::REPORT_NOT_FOUND => [
+ 'name' => Exception::REPORT_NOT_FOUND,
+ 'description' => 'Report with the requested ID could not be found.',
+ 'code' => 404,
+ ],
+ Exception::REPORT_ALREADY_EXISTS => [
+ 'name' => Exception::REPORT_ALREADY_EXISTS,
+ 'description' => 'Report with the requested ID already exists. Try again with a different ID or use ID.unique() to generate a unique ID.',
+ 'code' => 409,
+ ],
];
diff --git a/app/config/events.php b/app/config/events.php
index 11dc2e0e4a..2825562ab7 100644
--- a/app/config/events.php
+++ b/app/config/events.php
@@ -426,5 +426,33 @@ return [
'update' => [
'$description' => 'This event triggers when a proxy rule is updated.',
]
- ]
+ ],
+ 'reports' => [
+ '$model' => Response::MODEL_REPORT,
+ '$resource' => true,
+ '$description' => 'This event triggers on any report event.',
+ 'create' => [
+ '$description' => 'This event triggers when a report is created.',
+ ],
+ 'update' => [
+ '$description' => 'This event triggers when a report is updated.',
+ ],
+ 'delete' => [
+ '$description' => 'This event triggers when a report is deleted.',
+ ],
+ 'insights' => [
+ '$model' => Response::MODEL_INSIGHT,
+ '$resource' => true,
+ '$description' => 'This event triggers on any insight event.',
+ 'create' => [
+ '$description' => 'This event triggers when an insight is created.',
+ ],
+ 'update' => [
+ '$description' => 'This event triggers when an insight is updated.',
+ ],
+ 'delete' => [
+ '$description' => 'This event triggers when an insight is deleted.',
+ ],
+ ],
+ ],
];
diff --git a/app/config/roles.php b/app/config/roles.php
index 8fba27e503..cb4b178a29 100644
--- a/app/config/roles.php
+++ b/app/config/roles.php
@@ -59,8 +59,8 @@ $admins = [
'oauth2.write',
'mocks.read',
'mocks.write',
- 'policies.read',
- 'policies.write',
+ 'project.policies.read',
+ 'project.policies.write',
'templates.read',
'templates.write',
'projects.write',
@@ -103,6 +103,10 @@ $admins = [
'tokens.write',
'schedules.read',
'schedules.write',
+ 'insights.read',
+ 'insights.write',
+ 'reports.read',
+ 'reports.write',
];
return [
diff --git a/app/config/scopes/organization.php b/app/config/scopes/organization.php
index 228a1437f2..d74452f259 100644
--- a/app/config/scopes/organization.php
+++ b/app/config/scopes/organization.php
@@ -4,17 +4,23 @@
return [
"projects.read" => [
- "description" => 'Access to read organization\'s projects',
+ "description" => 'Access to read organization projects',
+ "category" => "Projects",
],
"projects.write" => [
"description" =>
- "Access to create, update, and delete projects in organization",
+ "Access to create, update, and delete organization projects",
+ "category" => "Projects",
],
"devKeys.read" => [
"description" => 'Access to read project\'s development keys',
+ "category" => "Other",
+ "deprecated" => true,
],
"devKeys.write" => [
"description" =>
"Access to create, update, and delete project\'s development keys",
+ "category" => "Other",
+ "deprecated" => true,
],
];
diff --git a/app/config/scopes/project.php b/app/config/scopes/project.php
index a048920de9..3d8998fb2f 100644
--- a/app/config/scopes/project.php
+++ b/app/config/scopes/project.php
@@ -44,11 +44,23 @@ return [
"category" => "Project",
],
"policies.read" => [
+ "description" =>
+ "Access to read project\'s policies. Replaced by \'project.policies.read\' for more granular control",
+ "category" => "Project",
+ 'deprecated' => true,
+ ],
+ "policies.write" => [
+ "description" =>
+ "Access to update project\'s policies. Replaces by \'project.policies.write\' for more granular control",
+ "category" => "Project",
+ 'deprecated' => true,
+ ],
+ "project.policies.read" => [
"description" =>
"Access to read project\'s policies",
"category" => "Project",
],
- "policies.write" => [
+ "project.policies.write" => [
"description" =>
"Access to update project\'s policies",
"category" => "Project",
@@ -286,6 +298,16 @@ return [
'category' => 'Messaging',
],
+ // Proxy
+ 'rules.read' => [
+ 'description' => 'Access to read proxy rules.',
+ 'category' => 'Proxy',
+ ],
+ 'rules.write' => [
+ 'description' => 'Access to create, update, and delete proxy rules.',
+ 'category' => 'Proxy',
+ ],
+
// Other
"webhooks.read" => [
"description" =>
@@ -339,12 +361,22 @@ return [
'description' => 'Access to create, update, and delete resources under VCS service.',
'category' => 'Other',
],
- 'rules.read' => [
- 'description' => 'Access to read proxy rules.',
- 'category' => 'Other',
+
+ // Advisor
+ 'insights.read' => [
+ 'description' => 'Access to read insights under Advisor service.',
+ 'category' => 'Advisor',
],
- 'rules.write' => [
- 'description' => 'Access to create, update, and delete proxy rules.',
- 'category' => 'Other',
+ 'insights.write' => [
+ 'description' => 'Reserved for Advisor insight ingestion outside CE.',
+ 'category' => 'Advisor',
+ ],
+ 'reports.read' => [
+ 'description' => 'Access to read reports under Advisor service.',
+ 'category' => 'Advisor',
+ ],
+ 'reports.write' => [
+ 'description' => 'Access to delete reports under Advisor service.',
+ 'category' => 'Advisor',
],
];
diff --git a/app/config/sdks.php b/app/config/sdks.php
index e89265b05e..e29b28690f 100644
--- a/app/config/sdks.php
+++ b/app/config/sdks.php
@@ -320,6 +320,26 @@ return [
'repoBranch' => 'main',
'changelog' => \realpath(__DIR__ . '/../../docs/sdks/claude-plugin/CHANGELOG.md'),
],
+ [
+ 'key' => 'codex-plugin',
+ 'name' => 'CodexPlugin',
+ 'version' => '0.1.1',
+ 'url' => 'https://github.com/appwrite/codex-plugin.git',
+ 'enabled' => true,
+ 'beta' => false,
+ 'dev' => false,
+ 'hidden' => false,
+ 'spec' => 'static',
+ 'family' => APP_SDK_PLATFORM_STATIC,
+ 'prism' => 'codex-plugin',
+ 'source' => \realpath(__DIR__ . '/../sdks/static-codex-plugin'),
+ 'gitUrl' => 'git@github.com:appwrite/codex-plugin.git',
+ 'gitRepoName' => 'codex-plugin',
+ 'gitUserName' => 'appwrite',
+ 'gitBranch' => 'dev',
+ 'repoBranch' => 'main',
+ 'changelog' => \realpath(__DIR__ . '/../../docs/sdks/codex-plugin/CHANGELOG.md'),
+ ],
],
],
diff --git a/app/config/services.php b/app/config/services.php
index cf2714f8c5..f829937623 100644
--- a/app/config/services.php
+++ b/app/config/services.php
@@ -308,5 +308,19 @@ return [
'optional' => true,
'icon' => '/images/services/messaging.png',
'platforms' => ['client', 'server', 'console'],
- ]
+ ],
+ 'advisor' => [
+ 'key' => 'advisor',
+ 'name' => 'Advisor',
+ 'subtitle' => 'The Advisor service surfaces actionable reports about your project resources, with CTA descriptors for one-click remediation in the console.',
+ 'description' => '/docs/services/advisor.md',
+ 'controller' => '', // Uses modules
+ 'sdk' => true,
+ 'docs' => true,
+ 'docsUrl' => 'https://appwrite.io/docs/server/advisor',
+ 'tests' => true,
+ 'optional' => true,
+ 'icon' => '/images/services/insights.png',
+ 'platforms' => ['server', 'console'],
+ ],
];
diff --git a/app/config/variables.php b/app/config/variables.php
index c834656ff4..90df9b4518 100644
--- a/app/config/variables.php
+++ b/app/config/variables.php
@@ -1336,6 +1336,15 @@ return [
'category' => 'Migrations',
'description' => '',
'variables' => [
+ [
+ 'name' => '_APP_MIGRATION_HOST',
+ 'description' => 'Internal hostname the migrations worker uses to reach this instance\'s API (for migrations and CSV/JSON imports & exports). Defaults to \'appwrite\', the API service name in the standard Docker Compose setup. Only change this for non-standard deployments.',
+ 'introduction' => '1.9.0',
+ 'default' => 'appwrite',
+ 'required' => false,
+ 'question' => '',
+ 'filter' => ''
+ ],
[
'name' => '_APP_MIGRATIONS_FIREBASE_CLIENT_ID',
'description' => 'Google OAuth client ID. You can find it in your GCP application settings.',
diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php
index c6a5fd6f97..e12263a009 100644
--- a/app/controllers/api/account.php
+++ b/app/controllers/api/account.php
@@ -11,10 +11,13 @@ use Appwrite\Auth\Validator\PersonalData;
use Appwrite\Auth\Validator\Phone;
use Appwrite\Bus\Events\SessionCreated;
use Appwrite\Detector\Detector;
-use Appwrite\Event\Delete;
use Appwrite\Event\Event;
-use Appwrite\Event\Mail;
-use Appwrite\Event\Messaging;
+use Appwrite\Event\Message\Delete as DeleteMessage;
+use Appwrite\Event\Message\Mail as MailMessage;
+use Appwrite\Event\Message\Messaging as MessagingMessage;
+use Appwrite\Event\Publisher\Delete as DeletePublisher;
+use Appwrite\Event\Publisher\Mail as MailPublisher;
+use Appwrite\Event\Publisher\Messaging as MessagingPublisher;
use Appwrite\Extend\Exception;
use Appwrite\Hooks\Hooks;
use Appwrite\Network\Validator\Redirect;
@@ -332,15 +335,15 @@ Http::post('/v1/account')
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
}
- if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
+ if ((($project->getId() === 'console') || ($plan['supportsDisposableEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
throw new Exception(Exception::USER_EMAIL_DISPOSABLE);
}
- if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
+ if ((($project->getId() === 'console') || ($plan['supportsCanonicalEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL);
}
- if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
+ if ((($project->getId() === 'console') || ($plan['supportsFreeEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
throw new Exception(Exception::USER_EMAIL_FREE);
}
@@ -470,9 +473,9 @@ Http::delete('/v1/account')
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
- ->inject('queueForDeletes')
+ ->inject('publisherForDeletes')
->inject('authorization')
- ->action(function (Document $user, Document $project, Response $response, Database $dbForProject, Event $queueForEvents, Delete $queueForDeletes, Authorization $authorization) {
+ ->action(function (Document $user, Document $project, Response $response, Database $dbForProject, Event $queueForEvents, DeletePublisher $publisherForDeletes, Authorization $authorization) {
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
@@ -496,9 +499,11 @@ Http::delete('/v1/account')
$dbForProject->deleteDocument('users', $user->getId());
- $queueForDeletes
- ->setType(DELETE_TYPE_DOCUMENT)
- ->setDocument($user);
+ $publisherForDeletes->enqueue(new DeleteMessage(
+ project: $project,
+ type: DELETE_TYPE_DOCUMENT,
+ document: $user,
+ ));
$queueForEvents
->setParam('userId', $user->getId())
@@ -580,12 +585,12 @@ Http::delete('/v1/account/sessions')
->inject('dbForProject')
->inject('locale')
->inject('queueForEvents')
- ->inject('queueForDeletes')
+ ->inject('publisherForDeletes')
->inject('store')
->inject('proofForToken')
->inject('domainVerification')
->inject('cookieDomain')
- ->action(function (Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain) {
+ ->action(function (Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, DeletePublisher $publisherForDeletes, Store $store, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain) {
$protocol = $request->getProtocol();
$sessions = $user->getAttribute('sessions', []);
@@ -615,10 +620,11 @@ Http::delete('/v1/account/sessions')
$queueForEvents
->setPayload($response->output($session, Response::MODEL_SESSION));
- $queueForDeletes
- ->setType(DELETE_TYPE_SESSION_TARGETS)
- ->setDocument($session)
- ->trigger();
+ $publisherForDeletes->enqueue(new DeleteMessage(
+ project: $queueForEvents->getProject(),
+ type: DELETE_TYPE_SESSION_TARGETS,
+ document: $session,
+ ));
}
}
@@ -712,12 +718,12 @@ Http::delete('/v1/account/sessions/:sessionId')
->inject('dbForProject')
->inject('locale')
->inject('queueForEvents')
- ->inject('queueForDeletes')
+ ->inject('publisherForDeletes')
->inject('store')
->inject('proofForToken')
->inject('domainVerification')
->inject('cookieDomain')
- ->action(function (?string $sessionId, ?\DateTime $requestTimestamp, Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Delete $queueForDeletes, Store $store, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain) {
+ ->action(function (?string $sessionId, ?\DateTime $requestTimestamp, Request $request, Response $response, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, DeletePublisher $publisherForDeletes, Store $store, ProofsToken $proofForToken, bool $domainVerification, ?string $cookieDomain) {
$protocol = $request->getProtocol();
$sessionId = ($sessionId === 'current')
@@ -759,10 +765,11 @@ Http::delete('/v1/account/sessions/:sessionId')
->setParam('sessionId', $session->getId())
->setPayload($response->output($session, Response::MODEL_SESSION));
- $queueForDeletes
- ->setType(DELETE_TYPE_SESSION_TARGETS)
- ->setDocument($session)
- ->trigger();
+ $publisherForDeletes->enqueue(new DeleteMessage(
+ project: $queueForEvents->getProject(),
+ type: DELETE_TYPE_SESSION_TARGETS,
+ document: $session,
+ ));
$response->noContent();
return;
@@ -830,11 +837,11 @@ Http::patch('/v1/account/sessions/:sessionId')
$refreshToken = $session->getAttribute('providerRefreshToken', '');
$oAuthProviders = Config::getParam('oAuthProviders') ?? [];
$className = $oAuthProviders[$provider]['class'] ?? null;
- if (!empty($provider) && ($className === null || !\class_exists($className))) {
+ if (!empty($refreshToken) && ($className === null || !\class_exists($className))) {
throw new Exception(Exception::PROJECT_PROVIDER_UNSUPPORTED);
}
- if (!empty($provider) && \class_exists($className)) {
+ if ($className !== null && \class_exists($className)) {
$appId = $project->getAttribute('oAuthProviders', [])[$provider . 'Appid'] ?? '';
$appSecret = $project->getAttribute('oAuthProviders', [])[$provider . 'Secret'] ?? '{}';
@@ -1676,15 +1683,15 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
$failureRedirect(Exception::GENERAL_INVALID_EMAIL);
}
- if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
+ if ((($project->getId() === 'console') || ($plan['supportsDisposableEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
$failureRedirect(Exception::USER_EMAIL_DISPOSABLE);
}
- if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
+ if ((($project->getId() === 'console') || ($plan['supportsCanonicalEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
$failureRedirect(Exception::USER_EMAIL_NOT_CANONICAL);
}
- if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
+ if ((($project->getId() === 'console') || ($plan['supportsFreeEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
$failureRedirect(Exception::USER_EMAIL_FREE);
}
@@ -1817,15 +1824,15 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
$failureRedirect(Exception::GENERAL_INVALID_EMAIL);
}
- if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
+ if ((($project->getId() === 'console') || ($plan['supportsDisposableEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
$failureRedirect(Exception::USER_EMAIL_DISPOSABLE);
}
- if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
+ if ((($project->getId() === 'console') || ($plan['supportsCanonicalEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
$failureRedirect(Exception::USER_EMAIL_NOT_CANONICAL);
}
- if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
+ if ((($project->getId() === 'console') || ($plan['supportsFreeEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
$failureRedirect(Exception::USER_EMAIL_FREE);
}
@@ -2113,12 +2120,12 @@ Http::post('/v1/account/tokens/magic-url')
->inject('dbForProject')
->inject('locale')
->inject('queueForEvents')
- ->inject('queueForMails')
+ ->inject('publisherForMails')
->inject('plan')
->inject('proofForPassword')
->inject('platform')
->inject('authorization')
- ->action(function (string $userId, string $email, string $url, bool $phrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, array $plan, ProofsPassword $proofForPassword, array $platform, Authorization $authorization) {
+ ->action(function (string $userId, string $email, string $url, bool $phrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, MailPublisher $publisherForMails, array $plan, ProofsPassword $proofForPassword, array $platform, Authorization $authorization) {
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled');
}
@@ -2175,15 +2182,15 @@ Http::post('/v1/account/tokens/magic-url')
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
}
- if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
+ if ((($project->getId() === 'console') || ($plan['supportsDisposableEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
throw new Exception(Exception::USER_EMAIL_DISPOSABLE);
}
- if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
+ if ((($project->getId() === 'console') || ($plan['supportsCanonicalEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL);
}
- if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
+ if ((($project->getId() === 'console') || ($plan['supportsFreeEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
throw new Exception(Exception::USER_EMAIL_FREE);
}
@@ -2304,6 +2311,7 @@ Http::post('/v1/account/tokens/magic-url')
$senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server');
$replyToEmail = '';
$replyToName = '';
+ $smtpConfig = [];
if ($smtpEnabled) {
if (!empty($smtp['senderEmail'])) {
@@ -2321,13 +2329,6 @@ Http::post('/v1/account/tokens/magic-url')
$replyToName = $smtp['replyToName'];
}
- $queueForMails
- ->setSmtpHost($smtp['host'] ?? '')
- ->setSmtpPort($smtp['port'] ?? '')
- ->setSmtpUsername($smtp['username'] ?? '')
- ->setSmtpPassword($smtp['password'] ?? '')
- ->setSmtpSecure($smtp['secure'] ?? '');
-
if (!empty($customTemplate)) {
if (!empty($customTemplate['senderEmail'])) {
$senderEmail = $customTemplate['senderEmail'];
@@ -2348,11 +2349,17 @@ Http::post('/v1/account/tokens/magic-url')
$subject = $customTemplate['subject'] ?? $subject;
}
- $queueForMails
- ->setSmtpReplyToEmail($replyToEmail)
- ->setSmtpReplyToName($replyToName)
- ->setSmtpSenderEmail($senderEmail)
- ->setSmtpSenderName($senderName);
+ $smtpConfig = [
+ 'host' => $smtp['host'] ?? '',
+ 'port' => $smtp['port'] ?? '',
+ 'username' => $smtp['username'] ?? '',
+ 'password' => $smtp['password'] ?? '',
+ 'secure' => $smtp['secure'] ?? '',
+ 'replyToEmail' => $replyToEmail,
+ 'replyToName' => $replyToName,
+ 'senderEmail' => $senderEmail,
+ 'senderName' => $senderName,
+ ];
}
$projectName = $project->getAttribute('name');
@@ -2374,18 +2381,17 @@ Http::post('/v1/account/tokens/magic-url')
'team' => '',
];
- $queueForMails
- ->setSubject($subject)
- ->setPreview($preview)
- ->setBody($body)
- ->appendVariables($emailVariables)
- ->setRecipient($email);
-
- if ($project->getId() === 'console') {
- $queueForMails->setSenderName($platform['emailSenderName']);
- }
-
- $queueForMails->trigger();
+ $publisherForMails->enqueue(new MailMessage(
+ project: $project,
+ recipient: $email,
+ subject: $subject,
+ body: $body,
+ preview: $preview,
+ smtp: $smtpConfig,
+ variables: $emailVariables,
+ customMailOptions: $project->getId() === 'console' ? ['senderName' => $platform['emailSenderName']] : [],
+ platform: $platform,
+ ));
$token->setAttribute('secret', $tokenSecret);
@@ -2436,12 +2442,12 @@ Http::post('/v1/account/tokens/email')
->inject('dbForProject')
->inject('locale')
->inject('queueForEvents')
- ->inject('queueForMails')
+ ->inject('publisherForMails')
->inject('plan')
->inject('proofForPassword')
->inject('proofForCode')
->inject('authorization')
- ->action(function (string $userId, string $email, bool $phrase, Request $request, Response $response, User $user, Document $project, array $platform, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, array $plan, ProofsPassword $proofForPassword, ProofsCode $proofForCode, Authorization $authorization) {
+ ->action(function (string $userId, string $email, bool $phrase, Request $request, Response $response, User $user, Document $project, array $platform, Database $dbForProject, Locale $locale, Event $queueForEvents, MailPublisher $publisherForMails, array $plan, ProofsPassword $proofForPassword, ProofsCode $proofForCode, Authorization $authorization) {
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled');
}
@@ -2496,15 +2502,15 @@ Http::post('/v1/account/tokens/email')
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
}
- if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
+ if ((($project->getId() === 'console') || ($plan['supportsDisposableEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
throw new Exception(Exception::USER_EMAIL_DISPOSABLE);
}
- if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
+ if ((($project->getId() === 'console') || ($plan['supportsCanonicalEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL);
}
- if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
+ if ((($project->getId() === 'console') || ($plan['supportsFreeEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
throw new Exception(Exception::USER_EMAIL_FREE);
}
@@ -2633,6 +2639,7 @@ Http::post('/v1/account/tokens/email')
$senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server');
$replyToEmail = '';
$replyToName = '';
+ $smtpConfig = [];
if ($smtpEnabled) {
if (!empty($smtp['senderEmail'])) {
@@ -2650,13 +2657,6 @@ Http::post('/v1/account/tokens/email')
$replyToName = $smtp['replyToName'];
}
- $queueForMails
- ->setSmtpHost($smtp['host'] ?? '')
- ->setSmtpPort($smtp['port'] ?? '')
- ->setSmtpUsername($smtp['username'] ?? '')
- ->setSmtpPassword($smtp['password'] ?? '')
- ->setSmtpSecure($smtp['secure'] ?? '');
-
if (!empty($customTemplate)) {
if (!empty($customTemplate['senderEmail'])) {
$senderEmail = $customTemplate['senderEmail'];
@@ -2677,11 +2677,17 @@ Http::post('/v1/account/tokens/email')
$subject = $customTemplate['subject'] ?? $subject;
}
- $queueForMails
- ->setSmtpReplyToEmail($replyToEmail)
- ->setSmtpReplyToName($replyToName)
- ->setSmtpSenderEmail($senderEmail)
- ->setSmtpSenderName($senderName);
+ $smtpConfig = [
+ 'host' => $smtp['host'] ?? '',
+ 'port' => $smtp['port'] ?? '',
+ 'username' => $smtp['username'] ?? '',
+ 'password' => $smtp['password'] ?? '',
+ 'secure' => $smtp['secure'] ?? '',
+ 'replyToEmail' => $replyToEmail,
+ 'replyToName' => $replyToName,
+ 'senderEmail' => $senderEmail,
+ 'senderName' => $senderName,
+ ];
}
$projectName = $project->getAttribute('name');
@@ -2717,20 +2723,18 @@ Http::post('/v1/account/tokens/email')
]);
}
- $queueForMails
- ->setSubject($subject)
- ->setPreview($preview)
- ->setBody($body)
- ->setBodyTemplate($bodyTemplate)
- ->appendVariables($emailVariables)
- ->setRecipient($email);
-
- // since this is console project, set email sender name!
- if ($smtpBaseTemplate === APP_BRANDED_EMAIL_BASE_TEMPLATE) {
- $queueForMails->setSenderName($platform['emailSenderName']);
- }
-
- $queueForMails->trigger();
+ $publisherForMails->enqueue(new MailMessage(
+ project: $project,
+ recipient: $email,
+ subject: $subject,
+ bodyTemplate: $bodyTemplate,
+ body: $body,
+ preview: $preview,
+ smtp: $smtpConfig,
+ variables: $emailVariables,
+ customMailOptions: $smtpBaseTemplate === APP_BRANDED_EMAIL_BASE_TEMPLATE ? ['senderName' => $platform['emailSenderName']] : [],
+ platform: $platform,
+ ));
$token->setAttribute('secret', $tokenSecret);
@@ -2880,7 +2884,7 @@ Http::post('/v1/account/tokens/phone')
->inject('platform')
->inject('dbForProject')
->inject('queueForEvents')
- ->inject('queueForMessaging')
+ ->inject('publisherForMessaging')
->inject('locale')
->inject('timelimit')
->inject('usage')
@@ -2888,7 +2892,7 @@ Http::post('/v1/account/tokens/phone')
->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, MessagingPublisher $publisherForMessaging, Locale $locale, callable $timelimit, Context $usage, 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');
}
@@ -3021,11 +3025,13 @@ Http::post('/v1/account/tokens/phone')
],
]);
- $queueForMessaging
- ->setType(MESSAGE_SEND_TYPE_INTERNAL)
- ->setMessage($messageDoc)
- ->setRecipients([$phone])
- ->setProviderType(MESSAGE_TYPE_SMS);
+ $publisherForMessaging->enqueue(new MessagingMessage(
+ type: MESSAGE_SEND_TYPE_INTERNAL,
+ project: $project,
+ message: $messageDoc,
+ recipients: [$phone],
+ providerType: MESSAGE_TYPE_SMS,
+ ));
$helper = PhoneNumberUtil::getInstance();
try {
@@ -3417,15 +3423,15 @@ Http::patch('/v1/account/email')
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
}
- if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
+ if ((($project->getId() === 'console') || ($plan['supportsDisposableEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && $emailMetadata['emailIsDisposable']) {
throw new Exception(Exception::USER_EMAIL_DISPOSABLE);
}
- if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
+ if ((($project->getId() === 'console') || ($plan['supportsCanonicalEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && $emailMetadata['emailIsCanonical'] === false) {
throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL);
}
- if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
+ if ((($project->getId() === 'console') || ($plan['supportsFreeEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && $emailMetadata['emailIsFree']) {
throw new Exception(Exception::USER_EMAIL_FREE);
}
@@ -3681,11 +3687,11 @@ Http::post('/v1/account/recovery')
->inject('project')
->inject('platform')
->inject('locale')
- ->inject('queueForMails')
+ ->inject('publisherForMails')
->inject('queueForEvents')
->inject('proofForToken')
->inject('authorization')
- ->action(function (string $email, string $url, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, Mail $queueForMails, Event $queueForEvents, ProofsToken $proofForToken, Authorization $authorization) {
+ ->action(function (string $email, string $url, Request $request, Response $response, User $user, Database $dbForProject, Document $project, array $platform, Locale $locale, MailPublisher $publisherForMails, Event $queueForEvents, ProofsToken $proofForToken, Authorization $authorization) {
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP Disabled');
@@ -3768,6 +3774,7 @@ Http::post('/v1/account/recovery')
$senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server');
$replyToEmail = '';
$replyToName = '';
+ $smtpConfig = [];
if ($smtpEnabled) {
if (!empty($smtp['senderEmail'])) {
@@ -3785,13 +3792,6 @@ Http::post('/v1/account/recovery')
$replyToName = $smtp['replyToName'];
}
- $queueForMails
- ->setSmtpHost($smtp['host'] ?? '')
- ->setSmtpPort($smtp['port'] ?? '')
- ->setSmtpUsername($smtp['username'] ?? '')
- ->setSmtpPassword($smtp['password'] ?? '')
- ->setSmtpSecure($smtp['secure'] ?? '');
-
if (!empty($customTemplate)) {
if (!empty($customTemplate['senderEmail'])) {
$senderEmail = $customTemplate['senderEmail'];
@@ -3812,11 +3812,17 @@ Http::post('/v1/account/recovery')
$subject = $customTemplate['subject'] ?? $subject;
}
- $queueForMails
- ->setSmtpReplyToEmail($replyToEmail)
- ->setSmtpReplyToName($replyToName)
- ->setSmtpSenderEmail($senderEmail)
- ->setSmtpSenderName($senderName);
+ $smtpConfig = [
+ 'host' => $smtp['host'] ?? '',
+ 'port' => $smtp['port'] ?? '',
+ 'username' => $smtp['username'] ?? '',
+ 'password' => $smtp['password'] ?? '',
+ 'secure' => $smtp['secure'] ?? '',
+ 'replyToEmail' => $replyToEmail,
+ 'replyToName' => $replyToName,
+ 'senderEmail' => $senderEmail,
+ 'senderName' => $senderName,
+ ];
}
$emailVariables = [
@@ -3829,19 +3835,18 @@ Http::post('/v1/account/recovery')
'team' => ''
];
- $queueForMails
- ->setRecipient($profile->getAttribute('email', ''))
- ->setName($profile->getAttribute('name', ''))
- ->setBody($body)
- ->appendVariables($emailVariables)
- ->setSubject($subject)
- ->setPreview($preview);
-
- if ($project->getId() === 'console') {
- $queueForMails->setSenderName($platform['emailSenderName']);
- }
-
- $queueForMails->trigger();
+ $publisherForMails->enqueue(new MailMessage(
+ project: $project,
+ recipient: $profile->getAttribute('email', ''),
+ name: $profile->getAttribute('name', ''),
+ subject: $subject,
+ body: $body,
+ preview: $preview,
+ smtp: $smtpConfig,
+ variables: $emailVariables,
+ customMailOptions: $project->getId() === 'console' ? ['senderName' => $platform['emailSenderName']] : [],
+ platform: $platform,
+ ));
$recovery->setAttribute('secret', $secret);
@@ -4009,10 +4014,10 @@ Http::post('/v1/account/verifications/email')
->inject('dbForProject')
->inject('locale')
->inject('queueForEvents')
- ->inject('queueForMails')
+ ->inject('publisherForMails')
->inject('proofForToken')
->inject('authorization')
- ->action(function (string $url, Request $request, Response $response, Document $project, array $platform, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsToken $proofForToken, Authorization $authorization) {
+ ->action(function (string $url, Request $request, Response $response, Document $project, array $platform, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, MailPublisher $publisherForMails, ProofsToken $proofForToken, Authorization $authorization) {
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP Disabled');
@@ -4099,6 +4104,7 @@ Http::post('/v1/account/verifications/email')
$senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server');
$replyToEmail = '';
$replyToName = '';
+ $smtpConfig = [];
if ($smtpEnabled) {
if (!empty($smtp['senderEmail'])) {
@@ -4116,13 +4122,6 @@ Http::post('/v1/account/verifications/email')
$replyToName = $smtp['replyToName'];
}
- $queueForMails
- ->setSmtpHost($smtp['host'] ?? '')
- ->setSmtpPort($smtp['port'] ?? '')
- ->setSmtpUsername($smtp['username'] ?? '')
- ->setSmtpPassword($smtp['password'] ?? '')
- ->setSmtpSecure($smtp['secure'] ?? '');
-
if (!empty($customTemplate)) {
if (!empty($customTemplate['senderEmail'])) {
$senderEmail = $customTemplate['senderEmail'];
@@ -4143,11 +4142,17 @@ Http::post('/v1/account/verifications/email')
$subject = $customTemplate['subject'] ?? $subject;
}
- $queueForMails
- ->setSmtpReplyToEmail($replyToEmail)
- ->setSmtpReplyToName($replyToName)
- ->setSmtpSenderEmail($senderEmail)
- ->setSmtpSenderName($senderName);
+ $smtpConfig = [
+ 'host' => $smtp['host'] ?? '',
+ 'port' => $smtp['port'] ?? '',
+ 'username' => $smtp['username'] ?? '',
+ 'password' => $smtp['password'] ?? '',
+ 'secure' => $smtp['secure'] ?? '',
+ 'replyToEmail' => $replyToEmail,
+ 'replyToName' => $replyToName,
+ 'senderEmail' => $senderEmail,
+ 'senderName' => $senderName,
+ ];
}
$emailVariables = [
@@ -4174,20 +4179,19 @@ Http::post('/v1/account/verifications/email')
]);
}
- $queueForMails
- ->setSubject($subject)
- ->setPreview($preview)
- ->setBody($body)
- ->setBodyTemplate($bodyTemplate)
- ->appendVariables($emailVariables)
- ->setRecipient($user->getAttribute('email'))
- ->setName($user->getAttribute('name') ?? '');
-
- if ($project->getId() === 'console') {
- $queueForMails->setSenderName($platform['emailSenderName']);
- }
-
- $queueForMails->trigger();
+ $publisherForMails->enqueue(new MailMessage(
+ project: $project,
+ recipient: $user->getAttribute('email'),
+ name: $user->getAttribute('name') ?? '',
+ subject: $subject,
+ bodyTemplate: $bodyTemplate,
+ body: $body,
+ preview: $preview,
+ smtp: $smtpConfig,
+ variables: $emailVariables,
+ customMailOptions: $project->getId() === 'console' ? ['senderName' => $platform['emailSenderName']] : [],
+ platform: $platform,
+ ));
$verification->setAttribute('secret', $verificationSecret);
@@ -4321,7 +4325,7 @@ Http::post('/v1/account/verifications/phone')
->inject('user')
->inject('dbForProject')
->inject('queueForEvents')
- ->inject('queueForMessaging')
+ ->inject('publisherForMessaging')
->inject('project')
->inject('locale')
->inject('timelimit')
@@ -4329,7 +4333,7 @@ Http::post('/v1/account/verifications/phone')
->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, MessagingPublisher $publisherForMessaging, Document $project, Locale $locale, callable $timelimit, Context $usage, array $plan, ProofsCode $proofForCode, Authorization $authorization) {
if (empty(System::getEnv('_APP_SMS_PROVIDER'))) {
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
}
@@ -4398,11 +4402,13 @@ Http::post('/v1/account/verifications/phone')
],
]);
- $queueForMessaging
- ->setType(MESSAGE_SEND_TYPE_INTERNAL)
- ->setMessage($messageDoc)
- ->setRecipients([$user->getAttribute('phone')])
- ->setProviderType(MESSAGE_TYPE_SMS);
+ $publisherForMessaging->enqueue(new MessagingMessage(
+ type: MESSAGE_SEND_TYPE_INTERNAL,
+ project: $project,
+ message: $messageDoc,
+ recipients: [$user->getAttribute('phone')],
+ providerType: MESSAGE_TYPE_SMS,
+ ));
$helper = PhoneNumberUtil::getInstance();
try {
@@ -4674,13 +4680,13 @@ Http::delete('/v1/account/targets/:targetId/push')
))
->param('targetId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Target ID.', false, ['dbForProject'])
->inject('queueForEvents')
- ->inject('queueForDeletes')
+ ->inject('publisherForDeletes')
->inject('user')
->inject('request')
->inject('response')
->inject('dbForProject')
->inject('authorization')
- ->action(function (string $targetId, Event $queueForEvents, Delete $queueForDeletes, Document $user, Request $request, Response $response, Database $dbForProject, Authorization $authorization) {
+ ->action(function (string $targetId, Event $queueForEvents, DeletePublisher $publisherForDeletes, Document $user, Request $request, Response $response, Database $dbForProject, Authorization $authorization) {
$target = $authorization->skip(fn () => $dbForProject->getDocument('targets', $targetId));
if ($target->isEmpty()) {
@@ -4695,9 +4701,11 @@ Http::delete('/v1/account/targets/:targetId/push')
$dbForProject->purgeCachedDocument('users', $user->getId());
- $queueForDeletes
- ->setType(DELETE_TYPE_TARGET)
- ->setDocument($target);
+ $publisherForDeletes->enqueue(new DeleteMessage(
+ project: $queueForEvents->getProject(),
+ type: DELETE_TYPE_TARGET,
+ document: $target,
+ ));
$queueForEvents
->setParam('userId', $user->getId())
diff --git a/app/controllers/api/messaging.php b/app/controllers/api/messaging.php
index 58c6a2c29e..d1ffa2e478 100644
--- a/app/controllers/api/messaging.php
+++ b/app/controllers/api/messaging.php
@@ -3,9 +3,11 @@
use Ahc\Jwt\JWT;
use Appwrite\Auth\Validator\Phone;
use Appwrite\Detector\Detector;
-use Appwrite\Event\Delete;
use Appwrite\Event\Event;
-use Appwrite\Event\Messaging;
+use Appwrite\Event\Message\Delete as DeleteMessage;
+use Appwrite\Event\Message\Messaging as MessagingMessage;
+use Appwrite\Event\Publisher\Delete as DeletePublisher;
+use Appwrite\Event\Publisher\Messaging as MessagingPublisher;
use Appwrite\Extend\Exception;
use Appwrite\Messaging\Status as MessageStatus;
use Appwrite\Permission;
@@ -2727,9 +2729,9 @@ Http::delete('/v1/messaging/topics/:topicId')
->param('topicId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Topic ID.', false, ['dbForProject'])
->inject('queueForEvents')
->inject('dbForProject')
- ->inject('queueForDeletes')
+ ->inject('publisherForDeletes')
->inject('response')
- ->action(function (string $topicId, Event $queueForEvents, Database $dbForProject, Delete $queueForDeletes, Response $response) {
+ ->action(function (string $topicId, Event $queueForEvents, Database $dbForProject, DeletePublisher $publisherForDeletes, Response $response) {
$topic = $dbForProject->getDocument('topics', $topicId);
if ($topic->isEmpty()) {
@@ -2738,9 +2740,11 @@ Http::delete('/v1/messaging/topics/:topicId')
$dbForProject->deleteDocument('topics', $topicId);
- $queueForDeletes
- ->setType(DELETE_TYPE_TOPIC)
- ->setDocument($topic);
+ $publisherForDeletes->enqueue(new DeleteMessage(
+ project: $queueForEvents->getProject(),
+ type: DELETE_TYPE_TOPIC,
+ document: $topic,
+ ));
$queueForEvents
->setParam('topicId', $topic->getId());
@@ -3187,9 +3191,9 @@ Http::post('/v1/messaging/messages/email')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('project')
- ->inject('queueForMessaging')
+ ->inject('publisherForMessaging')
->inject('response')
- ->action(function (string $messageId, string $subject, string $content, ?array $topics, ?array $users, ?array $targets, ?array $cc, ?array $bcc, ?array $attachments, bool $draft, bool $html, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) {
+ ->action(function (string $messageId, string $subject, string $content, ?array $topics, ?array $users, ?array $targets, ?array $cc, ?array $bcc, ?array $attachments, bool $draft, bool $html, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, MessagingPublisher $publisherForMessaging, Response $response) {
$messageId = $messageId == 'unique()'
? ID::unique()
: $messageId;
@@ -3274,9 +3278,11 @@ Http::post('/v1/messaging/messages/email')
switch ($status) {
case MessageStatus::PROCESSING:
- $queueForMessaging
- ->setType(MESSAGE_SEND_TYPE_EXTERNAL)
- ->setMessageId($message->getId());
+ $publisherForMessaging->enqueue(new MessagingMessage(
+ type: MESSAGE_SEND_TYPE_EXTERNAL,
+ project: $project,
+ messageId: $message->getId(),
+ ));
break;
case MessageStatus::SCHEDULED:
$schedule = $dbForPlatform->createDocument('schedules', new Document([
@@ -3362,9 +3368,9 @@ Http::post('/v1/messaging/messages/sms')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('project')
- ->inject('queueForMessaging')
+ ->inject('publisherForMessaging')
->inject('response')
- ->action(function (string $messageId, string $content, ?array $topics, ?array $users, ?array $targets, bool $draft, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) {
+ ->action(function (string $messageId, string $content, ?array $topics, ?array $users, ?array $targets, bool $draft, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, MessagingPublisher $publisherForMessaging, Response $response) {
$messageId = $messageId == 'unique()'
? ID::unique()
: $messageId;
@@ -3418,9 +3424,11 @@ Http::post('/v1/messaging/messages/sms')
switch ($status) {
case MessageStatus::PROCESSING:
- $queueForMessaging
- ->setType(MESSAGE_SEND_TYPE_EXTERNAL)
- ->setMessageId($message->getId());
+ $publisherForMessaging->enqueue(new MessagingMessage(
+ type: MESSAGE_SEND_TYPE_EXTERNAL,
+ project: $project,
+ messageId: $message->getId(),
+ ));
break;
case MessageStatus::SCHEDULED:
$schedule = $dbForPlatform->createDocument('schedules', new Document([
@@ -3498,10 +3506,10 @@ Http::post('/v1/messaging/messages/push')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('project')
- ->inject('queueForMessaging')
+ ->inject('publisherForMessaging')
->inject('response')
->inject('platform')
- ->action(function (string $messageId, string $title, string $body, ?array $topics, ?array $users, ?array $targets, ?array $data, string $action, string $image, string $icon, string $sound, string $color, string $tag, int $badge, bool $draft, ?string $scheduledAt, bool $contentAvailable, bool $critical, string $priority, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response, array $platform) {
+ ->action(function (string $messageId, string $title, string $body, ?array $topics, ?array $users, ?array $targets, ?array $data, string $action, string $image, string $icon, string $sound, string $color, string $tag, int $badge, bool $draft, ?string $scheduledAt, bool $contentAvailable, bool $critical, string $priority, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, MessagingPublisher $publisherForMessaging, Response $response, array $platform) {
$messageId = $messageId == 'unique()'
? ID::unique()
: $messageId;
@@ -3638,9 +3646,11 @@ Http::post('/v1/messaging/messages/push')
switch ($status) {
case MessageStatus::PROCESSING:
- $queueForMessaging
- ->setType(MESSAGE_SEND_TYPE_EXTERNAL)
- ->setMessageId($message->getId());
+ $publisherForMessaging->enqueue(new MessagingMessage(
+ type: MESSAGE_SEND_TYPE_EXTERNAL,
+ project: $project,
+ messageId: $message->getId(),
+ ));
break;
case MessageStatus::SCHEDULED:
$schedule = $dbForPlatform->createDocument('schedules', new Document([
@@ -3983,9 +3993,9 @@ Http::patch('/v1/messaging/messages/email/:messageId')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('project')
- ->inject('queueForMessaging')
+ ->inject('publisherForMessaging')
->inject('response')
- ->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $subject, ?string $content, ?bool $draft, ?bool $html, ?array $cc, ?array $bcc, ?string $scheduledAt, ?array $attachments, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) {
+ ->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $subject, ?string $content, ?bool $draft, ?bool $html, ?array $cc, ?array $bcc, ?string $scheduledAt, ?array $attachments, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, MessagingPublisher $publisherForMessaging, Response $response) {
$message = $dbForProject->getDocument('messages', $messageId);
if ($message->isEmpty()) {
@@ -4141,9 +4151,11 @@ Http::patch('/v1/messaging/messages/email/:messageId')
$message = $dbForProject->updateDocument('messages', $message->getId(), $message);
if ($status === MessageStatus::PROCESSING) {
- $queueForMessaging
- ->setType(MESSAGE_SEND_TYPE_EXTERNAL)
- ->setMessageId($message->getId());
+ $publisherForMessaging->enqueue(new MessagingMessage(
+ type: MESSAGE_SEND_TYPE_EXTERNAL,
+ project: $project,
+ messageId: $message->getId(),
+ ));
}
$queueForEvents
@@ -4205,9 +4217,9 @@ Http::patch('/v1/messaging/messages/sms/:messageId')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('project')
- ->inject('queueForMessaging')
+ ->inject('publisherForMessaging')
->inject('response')
- ->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $content, ?bool $draft, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) {
+ ->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $content, ?bool $draft, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, MessagingPublisher $publisherForMessaging, Response $response) {
$message = $dbForProject->getDocument('messages', $messageId);
if ($message->isEmpty()) {
@@ -4323,9 +4335,11 @@ Http::patch('/v1/messaging/messages/sms/:messageId')
$message = $dbForProject->updateDocument('messages', $message->getId(), $message);
if ($status === MessageStatus::PROCESSING) {
- $queueForMessaging
- ->setType(MESSAGE_SEND_TYPE_EXTERNAL)
- ->setMessageId($message->getId());
+ $publisherForMessaging->enqueue(new MessagingMessage(
+ type: MESSAGE_SEND_TYPE_EXTERNAL,
+ project: $project,
+ messageId: $message->getId(),
+ ));
}
$queueForEvents
@@ -4379,10 +4393,10 @@ Http::patch('/v1/messaging/messages/push/:messageId')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('project')
- ->inject('queueForMessaging')
+ ->inject('publisherForMessaging')
->inject('response')
->inject('platform')
- ->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $title, ?string $body, ?array $data, ?string $action, ?string $image, ?string $icon, ?string $sound, ?string $color, ?string $tag, ?int $badge, ?bool $draft, ?string $scheduledAt, ?bool $contentAvailable, ?bool $critical, ?string $priority, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response, array $platform) {
+ ->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $title, ?string $body, ?array $data, ?string $action, ?string $image, ?string $icon, ?string $sound, ?string $color, ?string $tag, ?int $badge, ?bool $draft, ?string $scheduledAt, ?bool $contentAvailable, ?bool $critical, ?string $priority, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, MessagingPublisher $publisherForMessaging, Response $response, array $platform) {
$message = $dbForProject->getDocument('messages', $messageId);
if ($message->isEmpty()) {
@@ -4584,9 +4598,11 @@ Http::patch('/v1/messaging/messages/push/:messageId')
$message = $dbForProject->updateDocument('messages', $message->getId(), $message);
if ($status === MessageStatus::PROCESSING) {
- $queueForMessaging
- ->setType(MESSAGE_SEND_TYPE_EXTERNAL)
- ->setMessageId($message->getId());
+ $publisherForMessaging->enqueue(new MessagingMessage(
+ type: MESSAGE_SEND_TYPE_EXTERNAL,
+ project: $project,
+ messageId: $message->getId(),
+ ));
}
$queueForEvents
diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php
index 494aa11150..8ab30fac99 100644
--- a/app/controllers/api/projects.php
+++ b/app/controllers/api/projects.php
@@ -2,9 +2,6 @@
use Appwrite\Auth\Validator\MockNumber;
use Appwrite\Extend\Exception;
-use Appwrite\SDK\AuthType;
-use Appwrite\SDK\Method;
-use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Database;
@@ -27,37 +24,6 @@ Http::init()
}
});
-Http::get('/v1/projects/:projectId')
- ->desc('Get project')
- ->groups(['api', 'projects'])
- ->label('scope', 'projects.read')
- ->label('sdk', new Method(
- namespace: 'projects',
- group: 'projects',
- name: 'get',
- description: '/docs/references/projects/get.md',
- auth: [AuthType::ADMIN],
- responses: [
- new SDKResponse(
- code: Response::STATUS_CODE_OK,
- model: Response::MODEL_PROJECT,
- )
- ]
- ))
- ->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
- ->inject('response')
- ->inject('dbForPlatform')
- ->action(function (string $projectId, Response $response, Database $dbForPlatform) {
-
- $project = $dbForPlatform->getDocument('projects', $projectId);
-
- if ($project->isEmpty()) {
- throw new Exception(Exception::PROJECT_NOT_FOUND);
- }
-
- $response->dynamic($project, Response::MODEL_PROJECT);
- });
-
// Backwards compatibility
Http::patch('/v1/projects/:projectId/oauth2')
->desc('Update project OAuth2')
diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php
index abcecac396..ccd7cf4661 100644
--- a/app/controllers/api/users.php
+++ b/app/controllers/api/users.php
@@ -11,8 +11,9 @@ use Appwrite\Auth\Validator\Phone;
use Appwrite\Deletes\Identities as DeleteIdentities;
use Appwrite\Deletes\Targets as DeleteTargets;
use Appwrite\Detector\Detector;
-use Appwrite\Event\Delete;
use Appwrite\Event\Event;
+use Appwrite\Event\Message\Delete as DeleteMessage;
+use Appwrite\Event\Publisher\Delete as DeletePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Hooks\Hooks;
use Appwrite\SDK\AuthType;
@@ -131,15 +132,15 @@ function createUser(Hash $hash, string $userId, ?string $email, ?string $passwor
} catch (\Throwable) {
}
- if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
+ if ((($project->getId() === 'console') || ($plan['supportsDisposableEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
throw new Exception(Exception::USER_EMAIL_DISPOSABLE);
}
- if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
+ if ((($project->getId() === 'console') || ($plan['supportsCanonicalEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL);
}
- if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
+ if ((($project->getId() === 'console') || ($plan['supportsFreeEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
throw new Exception(Exception::USER_EMAIL_FREE);
}
@@ -1563,15 +1564,15 @@ Http::patch('/v1/users/:userId/email')
} catch (\Throwable) {
}
- if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
+ if ((($project->getId() === 'console') || ($plan['supportsDisposableEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
throw new Exception(Exception::USER_EMAIL_DISPOSABLE);
}
- if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
+ if ((($project->getId() === 'console') || ($plan['supportsCanonicalEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL);
}
- if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
+ if ((($project->getId() === 'console') || ($plan['supportsFreeEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
throw new Exception(Exception::USER_EMAIL_FREE);
}
@@ -2592,8 +2593,8 @@ Http::delete('/v1/users/:userId')
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
- ->inject('queueForDeletes')
- ->action(function (string $userId, Response $response, Database $dbForProject, Event $queueForEvents, Delete $queueForDeletes) {
+ ->inject('publisherForDeletes')
+ ->action(function (string $userId, Response $response, Database $dbForProject, Event $queueForEvents, DeletePublisher $publisherForDeletes) {
$user = $dbForProject->getDocument('users', $userId);
@@ -2608,9 +2609,11 @@ Http::delete('/v1/users/:userId')
DeleteIdentities::delete($dbForProject, Query::equal('userInternalId', [$user->getSequence()]));
DeleteTargets::delete($dbForProject, Query::equal('userInternalId', [$user->getSequence()]));
- $queueForDeletes
- ->setType(DELETE_TYPE_DOCUMENT)
- ->setDocument($clone);
+ $publisherForDeletes->enqueue(new DeleteMessage(
+ project: $queueForEvents->getProject(),
+ type: DELETE_TYPE_DOCUMENT,
+ document: $clone,
+ ));
$queueForEvents
->setParam('userId', $user->getId())
@@ -2643,10 +2646,10 @@ Http::delete('/v1/users/:userId/targets/:targetId')
->param('userId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'User ID.', false, ['dbForProject'])
->param('targetId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Target ID.', false, ['dbForProject'])
->inject('queueForEvents')
- ->inject('queueForDeletes')
+ ->inject('publisherForDeletes')
->inject('response')
->inject('dbForProject')
- ->action(function (string $userId, string $targetId, Event $queueForEvents, Delete $queueForDeletes, Response $response, Database $dbForProject) {
+ ->action(function (string $userId, string $targetId, Event $queueForEvents, DeletePublisher $publisherForDeletes, Response $response, Database $dbForProject) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty()) {
@@ -2666,9 +2669,11 @@ Http::delete('/v1/users/:userId/targets/:targetId')
$dbForProject->deleteDocument('targets', $target->getId());
$dbForProject->purgeCachedDocument('users', $user->getId());
- $queueForDeletes
- ->setType(DELETE_TYPE_TARGET)
- ->setDocument($target);
+ $publisherForDeletes->enqueue(new DeleteMessage(
+ project: $queueForEvents->getProject(),
+ type: DELETE_TYPE_TARGET,
+ document: $target,
+ ));
$queueForEvents
->setParam('userId', $user->getId())
diff --git a/app/controllers/general.php b/app/controllers/general.php
index eb4899a3d8..6ca0a63ee2 100644
--- a/app/controllers/general.php
+++ b/app/controllers/general.php
@@ -7,9 +7,10 @@ use Ahc\Jwt\JWTException;
use Appwrite\Auth\Key;
use Appwrite\Bus\Events\ExecutionCompleted;
use Appwrite\Bus\Events\RequestCompleted;
-use Appwrite\Event\Delete as DeleteEvent;
use Appwrite\Event\Event;
+use Appwrite\Event\Message\Delete as DeleteMessage;
use Appwrite\Event\Publisher\Certificate;
+use Appwrite\Event\Publisher\Delete as DeletePublisher;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Network\Cors;
use Appwrite\Platform\Appwrite;
@@ -28,6 +29,8 @@ use Appwrite\Utopia\Request\Filters\V21 as RequestV21;
use Appwrite\Utopia\Request\Filters\V22 as RequestV22;
use Appwrite\Utopia\Request\Filters\V23 as RequestV23;
use Appwrite\Utopia\Request\Filters\V24 as RequestV24;
+use Appwrite\Utopia\Request\Filters\V25 as RequestV25;
+use Appwrite\Utopia\Request\Filters\V26 as RequestV26;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Filters\V16 as ResponseV16;
use Appwrite\Utopia\Response\Filters\V17 as ResponseV17;
@@ -38,7 +41,10 @@ use Appwrite\Utopia\Response\Filters\V21 as ResponseV21;
use Appwrite\Utopia\Response\Filters\V22 as ResponseV22;
use Appwrite\Utopia\Response\Filters\V23 as ResponseV23;
use Appwrite\Utopia\Response\Filters\V24 as ResponseV24;
+use Appwrite\Utopia\Response\Filters\V25 as ResponseV25;
+use Appwrite\Utopia\Response\Filters\V26 as ResponseV26;
use Appwrite\Utopia\View;
+use Executor\Exception\Timeout as ExecutorTimeout;
use Executor\Executor;
use MaxMind\Db\Reader;
use Swoole\Http\Request as SwooleRequest;
@@ -69,7 +75,7 @@ use Utopia\Validator\Text;
Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE);
-function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Authorization $authorization, ?Key $apiKey, DeleteEvent $queueForDeletes, int $executionsRetentionCount)
+function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Authorization $authorization, ?Key $apiKey, DeletePublisher $publisherForDeletes, int $executionsRetentionCount)
{
$host = $request->getHostname();
if (!empty($previewHostname)) {
@@ -579,26 +585,30 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
'site' => '',
};
- $executionResponse = $executor->createExecution(
- projectId: $project->getId(),
- deploymentId: $deployment->getId(),
- body: \strlen($body) > 0 ? $body : null,
- variables: $vars,
- timeout: $resource->getAttribute('timeout', 30),
- image: $runtime['image'],
- source: $source,
- entrypoint: $entrypoint,
- version: $version,
- path: $path,
- method: $method,
- headers: $headers,
- runtimeEntrypoint: $runtimeEntrypoint,
- cpus: $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT,
- memory: $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT,
- logging: $resource->getAttribute('logging', true),
- requestTimeout: 30,
- responseFormat: Executor::RESPONSE_FORMAT_ARRAY_HEADERS
- );
+ try {
+ $executionResponse = $executor->createExecution(
+ projectId: $project->getId(),
+ deploymentId: $deployment->getId(),
+ body: \strlen($body) > 0 ? $body : null,
+ variables: $vars,
+ timeout: $resource->getAttribute('timeout', 30),
+ image: $runtime['image'],
+ source: $source,
+ entrypoint: $entrypoint,
+ version: $version,
+ path: $path,
+ method: $method,
+ headers: $headers,
+ runtimeEntrypoint: $runtimeEntrypoint,
+ cpus: $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT,
+ memory: $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT,
+ logging: $resource->getAttribute('logging', true),
+ requestTimeout: 30,
+ responseFormat: Executor::RESPONSE_FORMAT_ARRAY_HEADERS
+ );
+ } catch (ExecutorTimeout $th) {
+ throw new AppwriteException(AppwriteException::FUNCTION_SYNCHRONOUS_TIMEOUT, previous: $th);
+ }
$headerOverrides = [];
@@ -781,12 +791,12 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
? RESOURCE_TYPE_FUNCTIONS
: RESOURCE_TYPE_SITES;
- $queueForDeletes
- ->setProject($project)
- ->setResourceType($resourceType)
- ->setResource($resource->getSequence())
- ->setType(DELETE_TYPE_EXECUTIONS_LIMIT)
- ->trigger();
+ $publisherForDeletes->enqueue(new DeleteMessage(
+ project: $project,
+ type: DELETE_TYPE_EXECUTIONS_LIMIT,
+ resource: (string) $resource->getSequence(),
+ resourceType: $resourceType,
+ ));
}
return true;
@@ -847,9 +857,9 @@ Http::init()
->inject('apiKey')
->inject('cors')
->inject('authorization')
- ->inject('queueForDeletes')
+ ->inject('publisherForDeletes')
->inject('executionsRetentionCount')
- ->action(function (Http $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, Reader $geodb, Event $queueForEvents, Bus $bus, Executor $executor, array $platform, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, 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, Event $queueForEvents, Bus $bus, Executor $executor, array $platform, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeletePublisher $publisherForDeletes, int $executionsRetentionCount) {
/*
* Appwrite Router
*/
@@ -857,7 +867,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, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $publisherForDeletes, $executionsRetentionCount)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -904,6 +914,12 @@ Http::init()
if (version_compare($requestFormat, '1.9.3', '<')) {
$request->addFilter(new RequestV24());
}
+ if (version_compare($requestFormat, '1.9.4', '<')) {
+ $request->addFilter(new RequestV25());
+ }
+ if (version_compare($requestFormat, '1.9.5', '<')) {
+ $request->addFilter(new RequestV26());
+ }
}
$localeParam = (string) $request->getParam('locale', $request->getHeader('x-appwrite-locale', ''));
@@ -928,6 +944,12 @@ Http::init()
*/
$responseFormat = $request->getHeader('x-appwrite-response-format', System::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
if ($responseFormat) {
+ if (version_compare($responseFormat, '1.9.5', '<')) {
+ $response->addFilter(new ResponseV26());
+ }
+ if (version_compare($responseFormat, '1.9.4', '<')) {
+ $response->addFilter(new ResponseV25());
+ }
if (version_compare($responseFormat, '1.9.3', '<')) {
$response->addFilter(new ResponseV24());
}
@@ -1146,16 +1168,16 @@ Http::options()
->inject('apiKey')
->inject('cors')
->inject('authorization')
- ->inject('queueForDeletes')
+ ->inject('publisherForDeletes')
->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, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey, Cors $cors, Authorization $authorization, DeletePublisher $publisherForDeletes, 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, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $publisherForDeletes, $executionsRetentionCount)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -1261,7 +1283,7 @@ Http::error()
if (!$publish && $project->getId() !== 'console') {
$errorUser = new DBUser();
try {
- $resolvedUser = $utopia->getResource('user');
+ $resolvedUser = $utopia->context()->get('user');
if ($resolvedUser instanceof DBUser) {
$errorUser = $resolvedUser;
}
@@ -1280,7 +1302,7 @@ Http::error()
if ($logger && $publish) {
try {
/** @var Utopia\Database\Document $user */
- $user = $utopia->getResource('user');
+ $user = $utopia->context()->get('user');
} catch (\Throwable) {
// All good, user is optional information for logger
}
@@ -1481,7 +1503,7 @@ Http::error()
// the cors resource (which depends on rule -> DB) would cascade.
// Uses override:true to avoid duplicate headers if init() already set them.
try {
- $cors = $utopia->getResource('cors');
+ $cors = $utopia->context()->get('cors');
foreach ($cors->headers($request->getOrigin()) as $name => $value) {
$response
->removeHeader($name)
@@ -1548,15 +1570,15 @@ Http::get('/robots.txt')
->inject('previewHostname')
->inject('apiKey')
->inject('authorization')
- ->inject('queueForDeletes')
+ ->inject('publisherForDeletes')
->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, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeletePublisher $publisherForDeletes, 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, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $publisherForDeletes, $executionsRetentionCount)) {
$utopia->getRoute()?->label('router', true);
}
}
@@ -1582,15 +1604,15 @@ Http::get('/humans.txt')
->inject('previewHostname')
->inject('apiKey')
->inject('authorization')
- ->inject('queueForDeletes')
+ ->inject('publisherForDeletes')
->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, Bus $bus, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, Authorization $authorization, DeletePublisher $publisherForDeletes, 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, $bus, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $authorization, $apiKey, $publisherForDeletes, $executionsRetentionCount)) {
$utopia->getRoute()?->label('router', true);
}
}
diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php
index c9e4f8b47d..6e5167660a 100644
--- a/app/controllers/shared/api.php
+++ b/app/controllers/shared/api.php
@@ -3,23 +3,21 @@
use Appwrite\Auth\Key;
use Appwrite\Auth\MFA\Type\TOTP;
use Appwrite\Bus\Events\RequestCompleted;
-use Appwrite\Event\Build;
use Appwrite\Event\Context\Audit as AuditContext;
-use Appwrite\Event\Database as EventDatabase;
-use Appwrite\Event\Delete;
use Appwrite\Event\Event;
-use Appwrite\Event\Func;
-use Appwrite\Event\Mail;
use Appwrite\Event\Message\Audit as AuditMessage;
+use Appwrite\Event\Message\Func as FunctionMessage;
use Appwrite\Event\Message\Usage as UsageMessage;
-use Appwrite\Event\Messaging;
use Appwrite\Event\Publisher\Audit;
+use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Event\Realtime;
use Appwrite\Event\Webhook;
use Appwrite\Extend\Exception;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Functions\EventProcessor;
+use Appwrite\Platform\Modules\Storage\Config\CacheControl;
+use Appwrite\Platform\Modules\Storage\Config\StorageCacheControl;
use Appwrite\SDK\Method;
use Appwrite\Usage\Context;
use Appwrite\Utopia\Database\Documents\User;
@@ -484,26 +482,99 @@ Http::init()
->inject('response')
->inject('project')
->inject('user')
- ->inject('queueForEvents')
- ->inject('queueForMessaging')
- ->inject('auditContext')
- ->inject('queueForDeletes')
- ->inject('queueForDatabase')
- ->inject('queueForBuilds')
- ->inject('usage')
- ->inject('queueForFunctions')
- ->inject('queueForMails')
- ->inject('dbForProject')
->inject('timelimit')
+ ->inject('devKey')
+ ->inject('authorization')
+ ->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, callable $timelimit, Document $devKey, Authorization $authorization) {
+ $response->setUser($user);
+ $request->setUser($user);
+
+ $roles = $authorization->getRoles();
+ $shouldCheckAbuse = System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled'
+ && ! $user->isApp($roles)
+ && ! $user->isPrivileged($roles)
+ && $devKey->isEmpty();
+
+ $route = $utopia->getRoute();
+ if ($route === null) {
+ throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND);
+ }
+
+ $abuseKeyLabel = $route->getLabel('abuse-key', 'url:{url},ip:{ip}');
+ $abuseKeyLabel = (! is_array($abuseKeyLabel)) ? [$abuseKeyLabel] : $abuseKeyLabel;
+ $closestLimit = null;
+
+ foreach ($abuseKeyLabel as $abuseKey) {
+ $isRateLimited = false;
+
+ try {
+ $start = $request->getContentRangeStart();
+ $end = $request->getContentRangeEnd();
+ $timeLimit = $timelimit($abuseKey, $route->getLabel('abuse-limit', 0), $route->getLabel('abuse-time', 3600));
+ $timeLimit
+ ->setParam('{projectId}', $project->getId())
+ ->setParam('{userId}', $user->getId())
+ ->setParam('{userAgent}', $request->getUserAgent(''))
+ ->setParam('{ip}', $request->getIP())
+ ->setParam('{url}', $request->getHostname() . $route->getPath())
+ ->setParam('{method}', $request->getMethod())
+ ->setParam('{chunkId}', (int) ($start / ($end + 1 - $start)));
+
+ foreach ($request->getParams() as $key => $value) {
+ if (! empty($value)) {
+ $timeLimit->setParam('{param-' . $key . '}', (\is_array($value)) ? \json_encode($value) : $value);
+ }
+ }
+
+ $abuse = new Abuse($timeLimit);
+ $remaining = $timeLimit->remaining();
+ $limit = $timeLimit->limit();
+ $time = $timeLimit->time() + $route->getLabel('abuse-time', 3600);
+
+ if ($limit && ($remaining < $closestLimit || is_null($closestLimit))) {
+ $closestLimit = $remaining;
+ $response
+ ->addHeader('X-RateLimit-Limit', $limit)
+ ->addHeader('X-RateLimit-Remaining', $remaining)
+ ->addHeader('X-RateLimit-Reset', $time);
+ }
+
+ if ($shouldCheckAbuse) {
+ $isRateLimited = $abuse->check();
+ }
+ } catch (\Throwable $th) {
+ \error_log((string) $th);
+
+ continue;
+ }
+
+ if ($isRateLimited) {
+ throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED);
+ }
+ }
+ });
+
+Http::init()
+ ->groups(['api'])
+ ->inject('utopia')
+ ->inject('request')
+ ->inject('response')
+ ->inject('project')
+ ->inject('user')
+ ->inject('queueForEvents')
+ ->inject('auditContext')
+ ->inject('usage')
+ ->inject('publisherForFunctions')
+ ->inject('dbForProject')
->inject('resourceToken')
->inject('mode')
->inject('apiKey')
->inject('plan')
- ->inject('devKey')
->inject('telemetry')
->inject('platform')
->inject('authorization')
- ->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Messaging $queueForMessaging, AuditContext $auditContext, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Context $usage, Func $queueForFunctions, Mail $queueForMails, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry, array $platform, Authorization $authorization) {
+ ->inject('cacheControlForStorage')
+ ->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Context $usage, FunctionPublisher $publisherForFunctions, Database $dbForProject, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Telemetry $telemetry, array $platform, Authorization $authorization, callable $cacheControlForStorage) {
$response->setUser($user);
$request->setUser($user);
@@ -520,70 +591,6 @@ Http::init()
default => '',
};
- /*
- * Abuse Check
- */
-
- $abuseKeyLabel = $route->getLabel('abuse-key', 'url:{url},ip:{ip}');
- $timeLimitArray = [];
-
- $abuseKeyLabel = (! is_array($abuseKeyLabel)) ? [$abuseKeyLabel] : $abuseKeyLabel;
-
- foreach ($abuseKeyLabel as $abuseKey) {
- $start = $request->getContentRangeStart();
- $end = $request->getContentRangeEnd();
- $timeLimit = $timelimit($abuseKey, $route->getLabel('abuse-limit', 0), $route->getLabel('abuse-time', 3600));
- $timeLimit
- ->setParam('{projectId}', $project->getId())
- ->setParam('{userId}', $user->getId())
- ->setParam('{userAgent}', $request->getUserAgent(''))
- ->setParam('{ip}', $request->getIP())
- ->setParam('{url}', $request->getHostname() . $route->getPath())
- ->setParam('{method}', $request->getMethod())
- ->setParam('{chunkId}', (int) ($start / ($end + 1 - $start)));
- $timeLimitArray[] = $timeLimit;
- }
-
- $closestLimit = null;
-
- $roles = $authorization->getRoles();
- $isPrivilegedUser = $user->isPrivileged($roles);
- $isAppUser = $user->isApp($roles);
-
- foreach ($timeLimitArray as $timeLimit) {
- foreach ($request->getParams() as $key => $value) { // Set request params as potential abuse keys
- if (! empty($value)) {
- $timeLimit->setParam('{param-' . $key . '}', (\is_array($value)) ? \json_encode($value) : $value);
- }
- }
-
- $abuse = new Abuse($timeLimit);
- $remaining = $timeLimit->remaining();
-
- $limit = $timeLimit->limit();
- $time = $timeLimit->time() + $route->getLabel('abuse-time', 3600);
-
- if ($limit && ($remaining < $closestLimit || is_null($closestLimit))) {
- $closestLimit = $remaining;
- $response
- ->addHeader('X-RateLimit-Limit', $limit)
- ->addHeader('X-RateLimit-Remaining', $remaining)
- ->addHeader('X-RateLimit-Reset', $time);
- }
-
- $enabled = System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled';
-
- if (
- $enabled // Abuse is enabled
- && ! $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
- ) {
- throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED);
- }
- }
-
/**
* TODO: (@loks0n)
* Avoid mutating the message across file boundaries - it's difficult to reason about at scale.
@@ -613,25 +620,14 @@ Http::init()
$auditContext->user = $userClone;
}
- /* Auto-set projects */
- $queueForDeletes->setProject($project);
- $queueForDatabase->setProject($project);
- $queueForMessaging->setProject($project);
- $queueForFunctions->setProject($project);
- $queueForBuilds->setProject($project);
- $queueForMails->setProject($project);
-
- /* Auto-set platforms */
- $queueForFunctions->setPlatform($platform);
- $queueForBuilds->setPlatform($platform);
- $queueForMails->setPlatform($platform);
-
$useCache = $route->getLabel('cache', false);
$storageCacheOperationsCounter = $telemetry->createCounter('storage.cache.operations.load');
if ($useCache) {
$route = $utopia->match($request);
+ $roles = $authorization->getRoles();
+ $isAppUser = $user->isApp($roles);
$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($roles);
$key = $request->cacheIdentifier();
Span::add('storage.cache.key', $key);
@@ -643,6 +639,7 @@ Http::init()
$data = $cache->load($key, $timestamp);
if (! empty($data) && ! $cacheLog->isEmpty()) {
+ $cacheControl = \sprintf('private, max-age=%d', $timestamp);
$parts = explode('/', $cacheLog->getAttribute('resourceType', ''));
$type = $parts[0];
@@ -651,7 +648,7 @@ Http::init()
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isToken = ! $resourceToken->isEmpty() && $resourceToken->getAttribute('bucketInternalId') === $bucket->getSequence();
- $isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
+ $isPrivilegedUser = $user->isPrivileged($roles);
if ($bucket->isEmpty() || (! $bucket->getAttribute('enabled') && ! $isAppUser && ! $isPrivilegedUser)) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
@@ -695,6 +692,21 @@ Http::init()
])));
}
}
+
+ if ($isImageTransformation) {
+ $cacheControl = $cacheControlForStorage(new StorageCacheControl(
+ source: CacheControl::SOURCE_CACHE,
+ user: $user,
+ maxAge: $timestamp,
+ project: $project,
+ bucket: $bucket,
+ file: $file,
+ resourceToken: $resourceToken,
+ fileSecurity: $fileSecurity,
+ cacheLog: $cacheLog,
+ route: $route,
+ ));
+ }
}
$accessedAt = $cacheLog->getAttribute('accessedAt', '');
@@ -707,7 +719,7 @@ Http::init()
}
$response
- ->addHeader('Cache-Control', sprintf('private, max-age=%d', $timestamp))
+ ->addHeader('Cache-Control', $cacheControl)
->addHeader('X-Appwrite-Cache', 'hit')
->setContentType($cacheLog->getAttribute('mimeType'));
$storageCacheOperationsCounter->add(1, ['result' => 'hit']);
@@ -798,11 +810,7 @@ Http::shutdown()
->inject('publisherForAudits')
->inject('usage')
->inject('publisherForUsage')
- ->inject('queueForDeletes')
- ->inject('queueForDatabase')
- ->inject('queueForBuilds')
- ->inject('queueForMessaging')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('queueForRealtime')
->inject('dbForProject')
@@ -812,7 +820,7 @@ Http::shutdown()
->inject('bus')
->inject('apiKey')
->inject('mode')
- ->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Audit $publisherForAudits, Context $usage, UsagePublisher $publisherForUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor, Bus $bus, ?Key $apiKey, string $mode) use ($parseLabel) {
+ ->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Audit $publisherForAudits, Context $usage, UsagePublisher $publisherForUsage, FunctionPublisher $publisherForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor, Bus $bus, ?Key $apiKey, string $mode) use ($parseLabel) {
$responsePayload = $response->getPayload();
@@ -841,9 +849,15 @@ Http::shutdown()
if (! empty($functionsEvents)) {
foreach ($generatedEvents as $event) {
if (isset($functionsEvents[$event])) {
- $queueForFunctions
- ->from($queueForEvents)
- ->trigger();
+ $publisherForFunctions->enqueue(FunctionMessage::fromEvent(
+ event: $queueForEvents->getEvent(),
+ params: $queueForEvents->getParams(),
+ project: $queueForEvents->getProject(),
+ user: $queueForEvents->getUser(),
+ userId: $queueForEvents->getUserId(),
+ payload: $queueForEvents->getPayload(),
+ platform: $queueForEvents->getPlatform(),
+ ));
break;
}
}
@@ -953,22 +967,6 @@ Http::shutdown()
$publisherForAudits->enqueue(AuditMessage::fromContext($auditContext));
}
- if (! empty($queueForDeletes->getType())) {
- $queueForDeletes->trigger();
- }
-
- if (! empty($queueForDatabase->getType())) {
- $queueForDatabase->trigger();
- }
-
- if (! empty($queueForBuilds->getType())) {
- $queueForBuilds->trigger();
- }
-
- if (! empty($queueForMessaging->getType())) {
- $queueForMessaging->trigger();
- }
-
// Cache label
$useCache = $route->getLabel('cache', false);
if ($useCache) {
diff --git a/app/http.php b/app/http.php
index b72f3b7f34..6dc415f000 100644
--- a/app/http.php
+++ b/app/http.php
@@ -3,7 +3,7 @@
require_once __DIR__ . '/init.php';
require_once __DIR__ . '/init/span.php';
-$registerRequestResources = require __DIR__ . '/init/resources/request.php';
+$setRequestContext = require __DIR__ . '/init/resources/request.php';
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
@@ -26,6 +26,7 @@ use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
+use Utopia\DI\Container;
use Utopia\Http\Adapter\Swoole\Server;
use Utopia\Http\Files;
use Utopia\Http\Http;
@@ -57,7 +58,7 @@ $container->set('pools', function ($register) {
$payloadSize = 12 * (1024 * 1024); // 12MB - adding slight buffer for headers and other data that might be sent with the payload - update later with valid testing
$totalWorkers = intval(System::getEnv('_APP_CPU_NUM', swoole_cpu_num())) * intval(System::getEnv('_APP_WORKER_PER_CORE', 6));
-$swooleAdapter = new Server(
+$swoole = new Server(
host: "0.0.0.0",
port: System::getEnv('PORT', 80),
settings: [
@@ -69,10 +70,10 @@ $swooleAdapter = new Server(
Constant::OPTION_OUTPUT_BUFFER_SIZE => $payloadSize,
Constant::OPTION_TASK_WORKER_NUM => 1, // required for the task to fetch domains background
],
- container: $container,
+ resources: $container,
);
-$http = $swooleAdapter->getServer();
+$http = $swoole->getServer();
/**
* Assigns HTTP requests to worker threads by analyzing its payload/content.
@@ -190,13 +191,11 @@ $http->on(Constant::EVENT_AFTER_RELOAD, function ($server) {
Console::success('Reload completed...');
});
-$container->set('bus', function ($register) use ($swooleAdapter) {
- return $register->get('bus')->setResolver(fn (string $name) => $swooleAdapter->getContainer()->get($name));
-}, ['register']);
+$container->set('bus', fn ($register) => $register->get('bus')->setResolver(fn (string $name) => $swoole->context()->get($name)), ['register']);
include __DIR__ . '/controllers/general.php';
-function createDatabase(Http $app, string $resourceKey, string $dbName, array $collections, mixed $pools, ?callable $extraSetup = null): void
+function createDatabase(Container $resources, string $resourceKey, string $dbName, array $collections, mixed $pools, ?callable $extraSetup = null): void
{
$max = 15;
$sleep = 2;
@@ -205,7 +204,7 @@ function createDatabase(Http $app, string $resourceKey, string $dbName, array $c
while (true) {
try {
$attempts++;
- $resource = $app->getResource($resourceKey);
+ $resource = $resources->get($resourceKey);
/* @var $database Database */
$database = is_callable($resource) ? $resource() : $resource;
break; // exit loop on success
@@ -288,23 +287,21 @@ function createDatabase(Http $app, string $resourceKey, string $dbName, array $c
Span::current()?->finish();
}
-$http->on(Constant::EVENT_START, function ($http) use ($payloadSize, $totalWorkers, $swooleAdapter) {
- $app = new Http($swooleAdapter, 'UTC');
-
+$http->on(Constant::EVENT_START, function ($http) use ($payloadSize, $totalWorkers, $container) {
/** @var \Utopia\Pools\Group $pools */
- $pools = $app->getResource('pools');
+ $pools = $container->get('pools');
- go(function () use ($app, $pools) {
+ go(function () use ($container, $pools) {
/** @var array $collections */
$collections = Config::getParam('collections', []);
// create logs database first, `getLogsDB` is a callable.
- createDatabase($app, 'getLogsDB', 'logs', $collections['logs'], $pools);
+ createDatabase($container, 'getLogsDB', 'logs', $collections['logs'], $pools);
// create appwrite database, `dbForPlatform` is a direct access call.
- createDatabase($app, 'dbForPlatform', 'appwrite', $collections['console'], $pools, function (Database $dbForPlatform) use ($collections, $app) {
- $authorization = $app->getResource('authorization');
+ createDatabase($container, 'dbForPlatform', 'appwrite', $collections['console'], $pools, function (Database $dbForPlatform) use ($collections, $container) {
+ $authorization = $container->get('authorization');
if ($dbForPlatform->getCollection(AuditAdapterSQL::COLLECTION)->isEmpty()) {
$adapter = new AdapterDatabase($dbForPlatform);
@@ -416,7 +413,7 @@ $http->on(Constant::EVENT_START, function ($http) use ($payloadSize, $totalWorke
$documentsSharedTables = \explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES', ''));
$vectorSharedTables = \explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES', ''));
- $cache = $app->getResource('cache');
+ $cache = $container->get('cache');
// All shared tables pools that need project metadata collections
$allSharedTables = \array_values(\array_unique(\array_filter([
@@ -502,7 +499,7 @@ $http->on(Constant::EVENT_START, function ($http) use ($payloadSize, $totalWorke
});
});
-$swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files, $swooleAdapter, $registerRequestResources) {
+$swoole->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files, $swoole, $setRequestContext) {
Span::init('http.request');
$request = new Request($utopiaRequest->getSwooleRequest());
@@ -522,21 +519,18 @@ $swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files
return;
}
- $requestContainer = $swooleAdapter->getContainer();
- $requestContainer->set('container', fn () => $requestContainer);
- $requestContainer->set('request', fn () => $request);
- $requestContainer->set('response', fn () => $response);
+ $app = new Http($swoole, 'UTC');
+ $app->context()->set('request', fn () => $request);
+ $app->context()->set('response', fn () => $response);
+ $app->context()->set('utopia', fn () => $app);
- $app = new Http($swooleAdapter, 'UTC');
- $requestContainer->set('utopia', fn () => $app);
-
- $registerRequestResources($requestContainer);
+ $setRequestContext($app->context());
$app->setCompression(System::getEnv('_APP_COMPRESSION_ENABLED', 'enabled') === 'enabled');
$app->setCompressionMinSize(intval(System::getEnv('_APP_COMPRESSION_MIN_SIZE_BYTES', '1024'))); // 1KB
try {
- $authorization = $app->getResource('authorization');
+ $authorization = $app->context()->get('authorization');
$request->setAuthorization($authorization);
$response->setAuthorization($authorization);
@@ -552,18 +546,18 @@ $swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
- $logger = $app->getResource("logger");
+ $logger = $app->context()->get("logger");
if ($logger) {
try {
/** @var Utopia\Database\Document $user */
- $user = $app->getResource('user');
+ $user = $app->context()->get('user');
} catch (\Throwable $_th) {
// All good, user is optional information for logger
}
$route = $app->getRoute();
- $log = $app->getResource("log");
+ $log = $app->context()->get("log");
if (isset($user) && !$user->isEmpty()) {
$log->setUser(new User($user->getId()));
@@ -642,18 +636,16 @@ $swooleAdapter->onRequest(function ($utopiaRequest, $utopiaResponse) use ($files
});
// Fetch domains every `DOMAIN_SYNC_TIMER` seconds and update in the memory
-$http->on(Constant::EVENT_TASK, function () use ($swooleAdapter) {
+$http->on(Constant::EVENT_TASK, function () use ($container) {
$lastSyncUpdate = null;
- $app = new Http($swooleAdapter, 'UTC');
-
/** @var Utopia\Database\Database $dbForPlatform */
- $dbForPlatform = $app->getResource('dbForPlatform');
+ $dbForPlatform = $container->get('dbForPlatform');
/** @var \Swoole\Table $riskyDomains */
- $riskyDomains = $app->getResource('riskyDomains');
+ $riskyDomains = $container->get('riskyDomains');
- Timer::tick(DOMAIN_SYNC_TIMER * 1000, function () use ($dbForPlatform, $riskyDomains, &$lastSyncUpdate, $app) {
+ Timer::tick(DOMAIN_SYNC_TIMER * 1000, function () use ($dbForPlatform, $riskyDomains, &$lastSyncUpdate, $container) {
try {
$time = DateTime::now();
$limit = 1000;
@@ -670,7 +662,7 @@ $http->on(Constant::EVENT_TASK, function () use ($swooleAdapter) {
}
$results = [];
try {
- $authorization = $app->getResource('authorization');
+ $authorization = $container->get('authorization');
$results = $authorization->skip(fn () => $dbForPlatform->find('rules', $queries));
} catch (Throwable $th) {
Console::error('rules ' . $th->getMessage());
@@ -720,4 +712,4 @@ $http->on(Constant::EVENT_TASK, function () use ($swooleAdapter) {
});
});
-$swooleAdapter->start();
+$swoole->start();
diff --git a/app/init/constants.php b/app/init/constants.php
index f27d0c7c70..bdc8e67fae 100644
--- a/app/init/constants.php
+++ b/app/init/constants.php
@@ -1,5 +1,11 @@
value, // legacy databases.createIndex
+ InsightType::TABLES_DB_INDEX->value, // tablesDB.createIndex
+ InsightType::DOCUMENTS_DB_INDEX->value, // documentsDB.createIndex
+ InsightType::VECTORS_DB_INDEX->value, // vectorsDB.createIndex
+ InsightType::DATABASE_PERFORMANCE->value,
+ InsightType::SITE_PERFORMANCE->value,
+ InsightType::SITE_ACCESSIBILITY->value,
+ InsightType::SITE_SEO->value,
+ InsightType::FUNCTION_PERFORMANCE->value,
+];
+
+// Public API services (SDK namespaces) that an insight CTA's `service` can reference.
+// Analyzers must pick the one matching the engine the resource lives in.
+const ADVISOR_CTA_SERVICES = [
+ InsightCTAService::DATABASES->value, // legacy
+ InsightCTAService::TABLES_DB->value,
+ InsightCTAService::DOCUMENTS_DB->value,
+ InsightCTAService::VECTORS_DB->value,
+];
+
+// Public API method names that an insight CTA's `method` can reference for index suggestions.
+const ADVISOR_CTA_METHODS = [
+ InsightCTAMethod::CREATE_INDEX->value,
+];
+
+// Insight severities
+const ADVISOR_SEVERITIES = [
+ InsightSeverity::INFO->value,
+ InsightSeverity::WARNING->value,
+ InsightSeverity::CRITICAL->value,
+];
+
+// Insight statuses
+const ADVISOR_STATUSES = [
+ InsightStatus::ACTIVE->value,
+ InsightStatus::DISMISSED->value,
+];
+
+// Report types
+const ADVISOR_REPORT_TYPES = [
+ ReportType::LIGHTHOUSE->value,
+ ReportType::AUDIT->value,
+ ReportType::DATABASE_ANALYZER->value,
+];
// Resource types for Tokens
const TOKENS_RESOURCE_TYPE_FILES = 'files';
@@ -457,3 +514,7 @@ const CSV_ALLOWED_DATABASE_TYPES = [
DATABASE_TYPE_TABLESDB,
DATABASE_TYPE_VECTORSDB
];
+
+const VCS_DEPLOYMENT_SKIP_PATTERNS = [
+ '[skip ci]',
+];
diff --git a/app/init/database/filters.php b/app/init/database/filters.php
index 5a65479424..e171805c47 100644
--- a/app/init/database/filters.php
+++ b/app/init/database/filters.php
@@ -475,3 +475,17 @@ Database::addFilter(
]));
}
);
+
+Database::addFilter(
+ 'subQueryReportInsights',
+ function (mixed $value) {
+ return;
+ },
+ function (mixed $value, Document $document, Database $database) {
+ return $database->getAuthorization()->skip(fn () => $database->find('insights', [
+ Query::equal('projectInternalId', [$document->getAttribute('projectInternalId')]),
+ Query::equal('reportInternalId', [$document->getSequence()]),
+ Query::limit(APP_LIMIT_SUBQUERY),
+ ]));
+ }
+);
diff --git a/app/init/database/formats.php b/app/init/database/formats.php
index 29a4f0c7d4..9ecf07716a 100644
--- a/app/init/database/formats.php
+++ b/app/init/database/formats.php
@@ -36,6 +36,13 @@ Structure::addFormat(APP_DATABASE_ATTRIBUTE_INT_RANGE, function ($attribute) {
return new Range($min, $max, Range::TYPE_INTEGER);
}, Database::VAR_INTEGER);
+// BigInt uses a dedicated bigintRange format name to avoid clobbering `intRange`.
+Structure::addFormat(APP_DATABASE_ATTRIBUTE_BIGINT_RANGE, function ($attribute) {
+ $min = $attribute['formatOptions']['min'] ?? -INF;
+ $max = $attribute['formatOptions']['max'] ?? INF;
+ return new Range($min, $max, Range::TYPE_INTEGER);
+}, Database::VAR_BIGINT);
+
Structure::addFormat(APP_DATABASE_ATTRIBUTE_FLOAT_RANGE, function ($attribute) {
$min = $attribute['formatOptions']['min'] ?? -INF;
$max = $attribute['formatOptions']['max'] ?? INF;
diff --git a/app/init/models.php b/app/init/models.php
index 9530b4b98b..521a3b77cd 100644
--- a/app/init/models.php
+++ b/app/init/models.php
@@ -11,6 +11,7 @@ use Appwrite\Utopia\Response\Model\AlgoScryptModified;
use Appwrite\Utopia\Response\Model\AlgoSha;
use Appwrite\Utopia\Response\Model\Any;
use Appwrite\Utopia\Response\Model\Attribute;
+use Appwrite\Utopia\Response\Model\AttributeBigInt;
use Appwrite\Utopia\Response\Model\AttributeBoolean;
use Appwrite\Utopia\Response\Model\AttributeDatetime;
use Appwrite\Utopia\Response\Model\AttributeEmail;
@@ -37,6 +38,7 @@ use Appwrite\Utopia\Response\Model\Branch;
use Appwrite\Utopia\Response\Model\Bucket;
use Appwrite\Utopia\Response\Model\Collection;
use Appwrite\Utopia\Response\Model\Column;
+use Appwrite\Utopia\Response\Model\ColumnBigInt;
use Appwrite\Utopia\Response\Model\ColumnBoolean;
use Appwrite\Utopia\Response\Model\ColumnDatetime;
use Appwrite\Utopia\Response\Model\ColumnEmail;
@@ -90,6 +92,8 @@ use Appwrite\Utopia\Response\Model\HealthTime;
use Appwrite\Utopia\Response\Model\HealthVersion;
use Appwrite\Utopia\Response\Model\Identity;
use Appwrite\Utopia\Response\Model\Index;
+use Appwrite\Utopia\Response\Model\Insight;
+use Appwrite\Utopia\Response\Model\InsightCTA;
use Appwrite\Utopia\Response\Model\Installation;
use Appwrite\Utopia\Response\Model\JWT;
use Appwrite\Utopia\Response\Model\Key;
@@ -171,12 +175,16 @@ use Appwrite\Utopia\Response\Model\PolicySessionLimit;
use Appwrite\Utopia\Response\Model\PolicyUserLimit;
use Appwrite\Utopia\Response\Model\Preferences;
use Appwrite\Utopia\Response\Model\Project;
+use Appwrite\Utopia\Response\Model\ProjectAuthMethod;
+use Appwrite\Utopia\Response\Model\ProjectProtocol;
+use Appwrite\Utopia\Response\Model\ProjectService;
use Appwrite\Utopia\Response\Model\Provider;
use Appwrite\Utopia\Response\Model\ProviderRepository;
use Appwrite\Utopia\Response\Model\ProviderRepositoryFramework;
use Appwrite\Utopia\Response\Model\ProviderRepositoryFrameworkList;
use Appwrite\Utopia\Response\Model\ProviderRepositoryRuntime;
use Appwrite\Utopia\Response\Model\ProviderRepositoryRuntimeList;
+use Appwrite\Utopia\Response\Model\Report;
use Appwrite\Utopia\Response\Model\ResourceToken;
use Appwrite\Utopia\Response\Model\Row;
use Appwrite\Utopia\Response\Model\Rule;
@@ -286,6 +294,8 @@ Response::setModel(new BaseList('Specifications List', Response::MODEL_SPECIFICA
Response::setModel(new BaseList('VCS Content List', Response::MODEL_VCS_CONTENT_LIST, 'contents', Response::MODEL_VCS_CONTENT));
Response::setModel(new BaseList('VectorsDB Collections List', Response::MODEL_VECTORSDB_COLLECTION_LIST, 'collections', Response::MODEL_VECTORSDB_COLLECTION));
Response::setModel(new BaseList('Embedding list', Response::MODEL_EMBEDDING_LIST, 'embeddings', Response::MODEL_EMBEDDING));
+Response::setModel(new BaseList('Insights List', Response::MODEL_INSIGHT_LIST, 'insights', Response::MODEL_INSIGHT));
+Response::setModel(new BaseList('Reports List', Response::MODEL_REPORT_LIST, 'reports', Response::MODEL_REPORT));
// Entities
Response::setModel(new Database());
@@ -297,6 +307,7 @@ Response::setModel(new Attribute());
Response::setModel(new AttributeList());
Response::setModel(new AttributeString());
Response::setModel(new AttributeInteger());
+Response::setModel(new AttributeBigInt());
Response::setModel(new AttributeFloat());
Response::setModel(new AttributeBoolean());
Response::setModel(new AttributeEmail());
@@ -330,6 +341,7 @@ Response::setModel(new Column());
Response::setModel(new ColumnList());
Response::setModel(new ColumnString());
Response::setModel(new ColumnInteger());
+Response::setModel(new ColumnBigInt());
Response::setModel(new ColumnFloat());
Response::setModel(new ColumnBoolean());
Response::setModel(new ColumnEmail());
@@ -393,6 +405,9 @@ Response::setModel(new FrameworkAdapter());
Response::setModel(new Deployment());
Response::setModel(new Execution());
Response::setModel(new Project());
+Response::setModel(new ProjectAuthMethod());
+Response::setModel(new ProjectService());
+Response::setModel(new ProjectProtocol());
Response::setModel(new Webhook());
Response::setModel(new Key());
Response::setModel(new EphemeralKey());
@@ -505,6 +520,9 @@ Response::setModel(new Target());
Response::setModel(new Migration());
Response::setModel(new MigrationReport());
Response::setModel(new MigrationFirebaseProject());
+Response::setModel(new Insight());
+Response::setModel(new InsightCTA());
+Response::setModel(new Report());
// Tests (keep last)
Response::setModel(new Mock());
diff --git a/app/init/resources.php b/app/init/resources.php
index 96457294de..92b581157f 100644
--- a/app/init/resources.php
+++ b/app/init/resources.php
@@ -2,12 +2,19 @@
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Audit as AuditPublisher;
+use Appwrite\Event\Publisher\Build as BuildPublisher;
use Appwrite\Event\Publisher\Certificate as CertificatePublisher;
+use Appwrite\Event\Publisher\Database as DatabasePublisher;
+use Appwrite\Event\Publisher\Delete as DeletePublisher;
use Appwrite\Event\Publisher\Execution as ExecutionPublisher;
+use Appwrite\Event\Publisher\Func as FunctionPublisher;
+use Appwrite\Event\Publisher\Mail as MailPublisher;
+use Appwrite\Event\Publisher\Messaging as MessagingPublisher;
use Appwrite\Event\Publisher\Migration as MigrationPublisher;
use Appwrite\Event\Publisher\Screenshot as ScreenshotPublisher;
use Appwrite\Event\Publisher\StatsResources as StatsResourcesPublisher;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
+use Appwrite\Platform\Modules\Storage\Config\StorageCacheControl;
use Appwrite\Utopia\Database\Documents\User;
use Executor\Executor;
use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis;
@@ -104,6 +111,10 @@ $container->set('publisherForExecutions', fn (Publisher $publisher) => new Execu
$publisher,
new Queue(System::getEnv('_APP_EXECUTIONS_QUEUE_NAME', Event::EXECUTIONS_QUEUE_NAME))
), ['publisher']);
+$container->set('publisherForFunctions', fn (Publisher $publisher) => new FunctionPublisher(
+ $publisher,
+ new Queue(System::getEnv('_APP_FUNCTIONS_QUEUE_NAME', Event::FUNCTIONS_QUEUE_NAME), 'utopia-queue', Event::FUNCTIONS_QUEUE_TTL)
+), ['publisher']);
$container->set('publisherForMigrations', fn (Publisher $publisher) => new MigrationPublisher(
$publisher,
new Queue(System::getEnv('_APP_MIGRATIONS_QUEUE_NAME', Event::MIGRATIONS_QUEUE_NAME))
@@ -112,6 +123,26 @@ $container->set('publisherForStatsResources', fn (Publisher $publisher) => new S
$publisher,
new Queue(System::getEnv('_APP_STATS_RESOURCES_QUEUE_NAME', Event::STATS_RESOURCES_QUEUE_NAME))
), ['publisher']);
+$container->set('publisherForBuilds', fn (Publisher $publisher) => new BuildPublisher(
+ $publisher,
+ new Queue(System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::BUILDS_QUEUE_NAME))
+), ['publisher']);
+$container->set('publisherForDatabase', fn (Publisher $publisherDatabases) => new DatabasePublisher(
+ $publisherDatabases,
+ new Queue(System::getEnv('_APP_DATABASE_QUEUE_NAME', Event::DATABASE_QUEUE_NAME))
+), ['publisherDatabases']);
+$container->set('publisherForDeletes', fn (Publisher $publisher) => new DeletePublisher(
+ $publisher,
+ new Queue(System::getEnv('_APP_DELETE_QUEUE_NAME', Event::DELETE_QUEUE_NAME))
+), ['publisher']);
+$container->set('publisherForMails', fn (Publisher $publisher) => new MailPublisher(
+ $publisher,
+ new Queue(System::getEnv('_APP_MAILS_QUEUE_NAME', Event::MAILS_QUEUE_NAME))
+), ['publisher']);
+$container->set('publisherForMessaging', fn (Publisher $publisher) => new MessagingPublisher(
+ $publisher,
+ new Queue(System::getEnv('_APP_MESSAGING_QUEUE_NAME', Event::MESSAGING_QUEUE_NAME))
+), ['publisher']);
/**
* Platform configuration
@@ -198,6 +229,10 @@ $container->set('cache', function (Group $pools, Telemetry $telemetry) {
return $cache;
}, ['pools', 'telemetry']);
+$container->set('cacheControlForStorage', fn () => function (StorageCacheControl $config): string {
+ return \sprintf('private, max-age=%d', $config->maxAge);
+});
+
$container->set('redis', function () {
$host = System::getEnv('_APP_REDIS_HOST', 'localhost');
$port = System::getEnv('_APP_REDIS_PORT', 6379);
diff --git a/app/init/resources/request.php b/app/init/resources/request.php
index 70d691370d..85d8db3698 100644
--- a/app/init/resources/request.php
+++ b/app/init/resources/request.php
@@ -4,14 +4,10 @@ use Ahc\Jwt\JWT;
use Ahc\Jwt\JWTException;
use Appwrite\Auth\Key;
use Appwrite\Databases\TransactionState;
-use Appwrite\Event\Build;
use Appwrite\Event\Context\Audit as AuditContext;
-use Appwrite\Event\Database as EventDatabase;
-use Appwrite\Event\Delete;
use Appwrite\Event\Event;
-use Appwrite\Event\Func;
-use Appwrite\Event\Mail;
-use Appwrite\Event\Messaging;
+use Appwrite\Event\Message\Func as FunctionMessage;
+use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Event\Realtime;
use Appwrite\Event\Webhook;
use Appwrite\Extend\Exception;
@@ -51,6 +47,7 @@ use Utopia\Locale\Locale;
use Utopia\Logger\Log;
use Utopia\Pools\Group;
use Utopia\Queue\Publisher;
+use Utopia\Queue\Queue;
use Utopia\Storage\Device;
use Utopia\System\System;
use Utopia\Telemetry\Adapter as Telemetry;
@@ -62,26 +59,18 @@ use Utopia\Validator\WhiteList;
* These resources depend (directly or transitively) on request/response
* and must be fresh for each HTTP request.
*/
-return function (Container $container): void {
- $container->set('utopia:graphql', function ($utopia) {
- return $utopia;
- }, ['utopia']);
+return function (Container $context): void {
+ $context->set('utopia:graphql', fn ($utopia) => $utopia, ['utopia']);
- $container->set('log', fn () => new Log(), []);
+ $context->set('log', fn () => new Log(), []);
- $container->set('logger', function ($register) {
- return $register->get('logger');
- }, ['register']);
+ $context->set('logger', fn ($register) => $register->get('logger'), ['register']);
- $container->set('authorization', function () {
- return new Authorization();
- }, []);
+ $context->set('authorization', fn () => new Authorization(), []);
- $container->set('store', function (): Store {
- return new Store();
- }, []);
+ $context->set('store', fn (): Store => new Store(), []);
- $container->set('proofForPassword', function (): Password {
+ $context->set('proofForPassword', function (): Password {
$hash = new Argon2();
$hash
->setMemoryCost(7168)
@@ -95,21 +84,21 @@ return function (Container $container): void {
return $password;
});
- $container->set('proofForToken', function (): Token {
+ $context->set('proofForToken', function (): Token {
$token = new Token();
$token->setHash(new Sha());
return $token;
});
- $container->set('proofForCode', function (): Code {
+ $context->set('proofForCode', function (): Code {
$code = new Code();
$code->setHash(new Sha());
return $code;
});
- $container->set('locale', function () {
+ $context->set('locale', function () {
$locale = new Locale(System::getEnv('_APP_LOCALE', 'en'));
$locale->setFallback(System::getEnv('_APP_LOCALE', 'en'));
@@ -117,41 +106,17 @@ return function (Container $container): void {
});
// Per-request queue resources (stateful, accumulate event data during request)
- $container->set('queueForMessaging', function (Publisher $publisher) {
- return new Messaging($publisher);
- }, ['publisher']);
- $container->set('queueForMails', function (Publisher $publisher) {
- return new Mail($publisher);
- }, ['publisher']);
- $container->set('queueForBuilds', function (Publisher $publisher) {
- return new Build($publisher);
- }, ['publisher']);
- $container->set('queueForDatabase', function (Publisher $publisher) {
- return new EventDatabase($publisher);
- }, ['publisher']);
- $container->set('queueForDeletes', function (Publisher $publisher) {
- return new Delete($publisher);
- }, ['publisher']);
- $container->set('queueForEvents', function (Publisher $publisher) {
- return new Event($publisher);
- }, ['publisher']);
- $container->set('queueForWebhooks', function (Publisher $publisher) {
- return new Webhook($publisher);
- }, ['publisher']);
- $container->set('queueForRealtime', function () {
- return new Realtime();
- }, []);
- $container->set('usage', function () {
- return new UsageContext();
- }, []);
- $container->set('auditContext', fn () => new AuditContext(), []);
- $container->set('queueForFunctions', function (Publisher $publisher) {
- return new Func($publisher);
- }, ['publisher']);
- $container->set('eventProcessor', function () {
- return new EventProcessor();
- }, []);
- $container->set('dbForPlatform', function (Group $pools, Cache $cache, Authorization $authorization) {
+ $context->set('queueForEvents', fn (Publisher $publisher) => new Event($publisher), ['publisher']);
+ $context->set('queueForWebhooks', fn (Publisher $publisher) => new Webhook($publisher), ['publisher']);
+ $context->set('queueForRealtime', fn () => new Realtime(), []);
+ $context->set('usage', fn () => new UsageContext(), []);
+ $context->set('auditContext', fn () => new AuditContext(), []);
+ $context->set('publisherForFunctions', fn (Publisher $publisher) => new FunctionPublisher(
+ $publisher,
+ new Queue(System::getEnv('_APP_FUNCTIONS_QUEUE_NAME', Event::FUNCTIONS_QUEUE_NAME), 'utopia-queue', Event::FUNCTIONS_QUEUE_TTL)
+ ), ['publisher']);
+ $context->set('eventProcessor', fn () => new EventProcessor(), []);
+ $context->set('dbForPlatform', function (Group $pools, Cache $cache, Authorization $authorization) {
$adapter = new DatabasePool($pools->get('console'));
$database = new Database($adapter, $cache);
@@ -169,7 +134,7 @@ return function (Container $container): void {
return $database;
}, ['pools', 'cache', 'authorization']);
- $container->set('getProjectDB', function (Group $pools, Database $dbForPlatform, Cache $cache, Authorization $authorization) {
+ $context->set('getProjectDB', function (Group $pools, Database $dbForPlatform, Cache $cache, Authorization $authorization) {
$adapters = [];
return function (Document $project) use ($pools, $dbForPlatform, $cache, $authorization, &$adapters) {
@@ -226,7 +191,7 @@ return function (Container $container): void {
};
}, ['pools', 'dbForPlatform', 'cache', 'authorization']);
- $container->set('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
+ $context->set('getLogsDB', function (Group $pools, Cache $cache, Authorization $authorization) {
$adapter = null;
return function (?Document $project = null) use ($pools, $cache, $authorization, &$adapter) {
@@ -258,7 +223,7 @@ return function (Container $container): void {
/**
* List of allowed request hostnames for the request.
*/
- $container->set('allowedHostnames', function (array $platform, Document $project, Document $rule, Document $devKey, Request $request) {
+ $context->set('allowedHostnames', function (array $platform, Document $project, Document $rule, Document $devKey, Request $request) {
$allowed = [...($platform['hostnames'] ?? [])];
/* Add platform configured hostnames */
@@ -302,7 +267,7 @@ return function (Container $container): void {
/**
* List of allowed request schemes for the request.
*/
- $container->set('allowedSchemes', function (array $platform, Document $project) {
+ $context->set('allowedSchemes', function (array $platform, Document $project) {
$allowed = [...($platform['schemas'] ?? [])];
if (! $project->isEmpty() && $project->getId() !== 'console') {
@@ -322,7 +287,7 @@ return function (Container $container): void {
/**
* Whether the request origin is verified against the request hostname.
*/
- $container->set('domainVerification', function (Request $request) {
+ $context->set('domainVerification', function (Request $request) {
$origin = \parse_url($request->getOrigin($request->getReferer('')), PHP_URL_HOST);
$selfDomain = new Domain($request->getHostname());
$endDomain = new Domain((string) $origin);
@@ -334,7 +299,7 @@ return function (Container $container): void {
/**
* Cookie domain for the current request.
*/
- $container->set('cookieDomain', function (Request $request, Document $project) {
+ $context->set('cookieDomain', function (Request $request, Document $project) {
$localHosts = ['localhost', 'localhost:' . $request->getPort()];
$migrationHost = System::getEnv('_APP_MIGRATION_HOST');
@@ -368,7 +333,7 @@ return function (Container $container): void {
/**
* Rule associated with a request origin.
*/
- $container->set('rule', function (Request $request, Database $dbForPlatform, Document $project, Authorization $authorization) {
+ $context->set('rule', function (Request $request, Database $dbForPlatform, Document $project, Authorization $authorization) {
$domain = \parse_url($request->getOrigin(), PHP_URL_HOST);
if (empty($domain)) {
@@ -418,7 +383,7 @@ return function (Container $container): void {
/**
* CORS service
*/
- $container->set('cors', function (array $allowedHostnames) {
+ $context->set('cors', function (array $allowedHostnames) {
$corsConfig = Config::getParam('cors');
return new Cors(
@@ -430,23 +395,23 @@ return function (Container $container): void {
);
}, ['allowedHostnames']);
- $container->set('originValidator', function (Document $devKey, array $allowedHostnames, array $allowedSchemes) {
- if (! $devKey->isEmpty()) {
- return new URL();
- }
+ $context->set(
+ 'originValidator',
+ fn (Document $devKey, array $allowedHostnames, array $allowedSchemes) => $devKey->isEmpty()
+ ? new Origin($allowedHostnames, $allowedSchemes)
+ : new URL(),
+ ['devKey', 'allowedHostnames', 'allowedSchemes']
+ );
- return new Origin($allowedHostnames, $allowedSchemes);
- }, ['devKey', 'allowedHostnames', 'allowedSchemes']);
+ $context->set(
+ 'redirectValidator',
+ fn (Document $devKey, array $allowedHostnames, array $allowedSchemes) => $devKey->isEmpty()
+ ? new Redirect($allowedHostnames, $allowedSchemes)
+ : new URL(),
+ ['devKey', 'allowedHostnames', 'allowedSchemes']
+ );
- $container->set('redirectValidator', function (Document $devKey, array $allowedHostnames, array $allowedSchemes) {
- if (! $devKey->isEmpty()) {
- return new URL();
- }
-
- return new Redirect($allowedHostnames, $allowedSchemes);
- }, ['devKey', 'allowedHostnames', 'allowedSchemes']);
-
- $container->set('user', function (string $mode, Document $project, Document $console, Request $request, Response $response, Database $dbForProject, Database $dbForPlatform, Store $store, Token $proofForToken, $authorization) {
+ $context->set('user', function (string $mode, Document $project, Document $console, Request $request, Response $response, Database $dbForProject, Database $dbForPlatform, Store $store, Token $proofForToken, $authorization) {
/**
* Handles user authentication and session validation.
*
@@ -617,7 +582,7 @@ return function (Container $container): void {
return $user;
}, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForPlatform', 'store', 'proofForToken', 'authorization']);
- $container->set('project', function ($dbForPlatform, $request, $console, $authorization, Http $utopia) {
+ $context->set('project', function ($dbForPlatform, $request, $console, $authorization, Http $utopia) {
/** @var Appwrite\Utopia\Request $request */
/** @var Utopia\Database\Database $dbForPlatform */
/** @var Utopia\Database\Document $console */
@@ -650,7 +615,7 @@ return function (Container $container): void {
return $project;
}, ['dbForPlatform', 'request', 'console', 'authorization', 'utopia']);
- $container->set('session', function (User $user, Store $store, Token $proofForToken) {
+ $context->set('session', function (User $user, Store $store, Token $proofForToken) {
if ($user->isEmpty()) {
return;
}
@@ -671,7 +636,7 @@ return function (Container $container): void {
return;
}, ['user', 'store', 'proofForToken']);
- $container->set('dbForProject', function (Group $pools, Database $dbForPlatform, Cache $cache, Document $project, Response $response, Publisher $publisher, Publisher $publisherFunctions, Publisher $publisherWebhooks, Event $queueForEvents, Func $queueForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime, UsageContext $usage, Authorization $authorization, Request $request) {
+ $context->set('dbForProject', function (Group $pools, Database $dbForPlatform, Cache $cache, Document $project, Response $response, Publisher $publisher, Publisher $publisherFunctions, Publisher $publisherWebhooks, Event $queueForEvents, FunctionPublisher $publisherForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime, UsageContext $usage, Authorization $authorization, Request $request) {
if ($project->isEmpty() || $project->getId() === 'console') {
return $dbForPlatform;
}
@@ -727,7 +692,7 @@ return function (Container $container): void {
* Accounts can be created in many ways beyond `createAccount`
* (anonymous, OAuth, phone, etc.), and those flows are probably not covered in event tests; so we handle this here.
*/
- $eventDatabaseListener = function (Document $project, Document $document, Response $response, Event $queueForEvents, Func $queueForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime) {
+ $eventDatabaseListener = function (Document $project, Document $document, Response $response, Event $queueForEvents, FunctionPublisher $publisherForFunctions, Webhook $queueForWebhooks, Realtime $queueForRealtime) {
// Only trigger events for user creation with the database listener.
if ($document->getCollection() !== 'users') {
return;
@@ -739,9 +704,15 @@ return function (Container $container): void {
->setPayload($response->output($document, Response::MODEL_USER));
// Trigger functions, webhooks, and realtime events
- $queueForFunctions
- ->from($queueForEvents)
- ->trigger();
+ $publisherForFunctions->enqueue(FunctionMessage::fromEvent(
+ event: $queueForEvents->getEvent(),
+ params: $queueForEvents->getParams(),
+ project: $queueForEvents->getProject(),
+ user: $queueForEvents->getUser(),
+ userId: $queueForEvents->getUserId(),
+ payload: $queueForEvents->getPayload(),
+ platform: $queueForEvents->getPlatform(),
+ ));
/** Trigger webhooks events only if a project has them enabled */
if (! empty($project->getAttribute('webhooks'))) {
@@ -921,7 +892,6 @@ return function (Container $container): void {
// Clone the queues, to prevent events triggered by the database listener
// from overwriting the events that are supposed to be triggered in the shutdown hook.
$queueForEventsClone = new Event($publisher);
- $queueForFunctions = new Func($publisherFunctions);
$queueForWebhooks = new Webhook($publisherWebhooks);
$queueForRealtime = new Realtime();
@@ -936,7 +906,7 @@ return function (Container $container): void {
$document,
$response,
$queueForEventsClone->from($queueForEvents),
- $queueForFunctions->from($queueForEvents),
+ $publisherForFunctions,
$queueForWebhooks->from($queueForEvents),
$queueForRealtime->from($queueForEvents)
))
@@ -945,9 +915,9 @@ return function (Container $container): void {
->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', 'request']);
+ }, ['pools', 'dbForPlatform', 'cache', 'project', 'response', 'publisher', 'publisherFunctions', 'publisherWebhooks', 'queueForEvents', 'publisherForFunctions', 'queueForWebhooks', 'queueForRealtime', 'usage', 'authorization', 'request']);
- $container->set('schema', function ($utopia, $dbForProject, $authorization) {
+ $context->set('schema', function ($utopia, $dbForProject, $authorization) {
$complexity = function (int $complexity, array $args) {
$queries = Query::parseQueries($args['queries'] ?? []);
@@ -1034,13 +1004,9 @@ return function (Container $container): void {
);
}, ['utopia', 'dbForProject', 'authorization']);
- $container->set('audit', function ($dbForProject) {
- $adapter = new AdapterDatabase($dbForProject);
+ $context->set('audit', fn ($dbForProject) => new Audit(new AdapterDatabase($dbForProject)), ['dbForProject']);
- return new Audit($adapter);
- }, ['dbForProject']);
-
- $container->set('mode', function ($request, Document $project) {
+ $context->set('mode', function ($request, Document $project) {
/** @var Appwrite\Utopia\Request $request */
/**
@@ -1058,7 +1024,7 @@ return function (Container $container): void {
return $mode;
}, ['request', 'project']);
- $container->set('requestTimestamp', function ($request) {
+ $context->set('requestTimestamp', function ($request) {
// TODO: Move this to the Request class itself
$timestampHeader = $request->getHeader('x-appwrite-timestamp');
$requestTimestamp = null;
@@ -1073,7 +1039,7 @@ return function (Container $container): void {
return $requestTimestamp;
}, ['request']);
- $container->set('devKey', function (Request $request, Document $project, array $servers, Database $dbForPlatform, Authorization $authorization) {
+ $context->set('devKey', function (Request $request, Document $project, array $servers, Database $dbForPlatform, Authorization $authorization) {
$devKey = $request->getHeader('x-appwrite-dev-key', $request->getParam('devKey', ''));
// Check if given key match project's development keys
@@ -1122,7 +1088,7 @@ return function (Container $container): void {
return $key;
}, ['request', 'project', 'servers', 'dbForPlatform', 'authorization']);
- $container->set('team', function (Document $project, Database $dbForPlatform, Http $utopia, Request $request, Authorization $authorization) {
+ $context->set('team', function (Document $project, Database $dbForPlatform, Http $utopia, Request $request, Authorization $authorization) {
$teamInternalId = '';
if ($project->getId() !== 'console') {
$teamInternalId = $project->getAttribute('teamInternalId', '');
@@ -1165,7 +1131,7 @@ return function (Container $container): void {
return $team;
}, ['project', 'dbForPlatform', 'utopia', 'request', 'authorization']);
- $container->set('previewHostname', function (Request $request, ?Key $apiKey) {
+ $context->set('previewHostname', function (Request $request, ?Key $apiKey) {
$allowed = false;
if (Http::isDevelopment()) {
@@ -1184,7 +1150,7 @@ return function (Container $container): void {
return '';
}, ['request', 'apiKey']);
- $container->set('apiKey', function (Request $request, Document $project, Document $team, Document $user): ?Key {
+ $context->set('apiKey', function (Request $request, Document $project, Document $team, Document $user): ?Key {
$key = $request->getHeader('x-appwrite-key');
if (empty($key)) {
@@ -1218,7 +1184,7 @@ return function (Container $container): void {
return $key;
}, ['request', 'project', 'team', 'user']);
- $container->set('resourceToken', function ($project, $dbForProject, $request, Authorization $authorization) {
+ $context->set('resourceToken', function ($project, $dbForProject, $request, Authorization $authorization) {
$tokenJWT = $request->getParam('token');
if (! empty($tokenJWT) && ! $project->isEmpty()) { // JWT authentication
@@ -1285,10 +1251,10 @@ return function (Container $container): void {
return new Document([]);
}, ['project', 'dbForProject', 'request', 'authorization']);
- $container->set('getDatabasesDB', function (Group $pools, Cache $cache, Document $project, Request $request, UsageContext $usage, Authorization $authorization) {
+ $context->set('getDatabasesDB', function (Group $pools, Cache $cache, Document $project, Request $request, UsageContext $usage, Authorization $authorization) {
return function (Document $database) use ($pools, $cache, $project, $request, $usage, $authorization): Database {
- $databaseDSN = $database->getAttribute('database', $project->getAttribute('database', ''));
+ $databaseDSN = $database->getAttribute('database') ?: $project->getAttribute('database', '');
$databaseType = $database->getAttribute('type', '');
try {
@@ -1447,35 +1413,27 @@ return function (Container $container): void {
}, ['pools', 'cache', 'project', 'request', 'usage', 'authorization']);
- $container->set('transactionState', function (Database $dbForProject, Authorization $authorization, callable $getDatabasesDB) {
- return new TransactionState($dbForProject, $authorization, $getDatabasesDB);
- }, ['dbForProject', 'authorization', 'getDatabasesDB']);
+ $context->set(
+ 'transactionState',
+ fn (Database $dbForProject, Authorization $authorization, callable $getDatabasesDB) => new TransactionState($dbForProject, $authorization, $getDatabasesDB),
+ ['dbForProject', 'authorization', 'getDatabasesDB']
+ );
- $container->set('executionsRetentionCount', function (Document $project, array $plan) {
- if ($project->getId() === 'console' || empty($plan)) {
- return 0;
- }
+ $context->set(
+ 'executionsRetentionCount',
+ fn (Document $project, array $plan) => ($project->getId() === 'console' || empty($plan))
+ ? 0
+ : (int) ($plan['executionsRetentionCount'] ?? 100),
+ ['project', 'plan']
+ );
- return (int) ($plan['executionsRetentionCount'] ?? 100);
- }, ['project', 'plan']);
+ $context->set('deviceForFiles', fn ($project, Telemetry $telemetry) => new Device\Telemetry($telemetry, getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId())), ['project', 'telemetry']);
+ $context->set('deviceForSites', fn ($project, Telemetry $telemetry) => new Device\Telemetry($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId())), ['project', 'telemetry']);
+ $context->set('deviceForMigrations', fn ($project, Telemetry $telemetry) => new Device\Telemetry($telemetry, getDevice(APP_STORAGE_IMPORTS . '/app-' . $project->getId())), ['project', 'telemetry']);
+ $context->set('deviceForFunctions', fn ($project, Telemetry $telemetry) => new Device\Telemetry($telemetry, getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId())), ['project', 'telemetry']);
+ $context->set('deviceForBuilds', fn ($project, Telemetry $telemetry) => new Device\Telemetry($telemetry, getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId())), ['project', 'telemetry']);
- $container->set('deviceForFiles', function ($project, Telemetry $telemetry) {
- return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId()));
- }, ['project', 'telemetry']);
- $container->set('deviceForSites', function ($project, Telemetry $telemetry) {
- return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_SITES . '/app-' . $project->getId()));
- }, ['project', 'telemetry']);
- $container->set('deviceForMigrations', function ($project, Telemetry $telemetry) {
- return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_IMPORTS . '/app-' . $project->getId()));
- }, ['project', 'telemetry']);
- $container->set('deviceForFunctions', function ($project, Telemetry $telemetry) {
- return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId()));
- }, ['project', 'telemetry']);
- $container->set('deviceForBuilds', function ($project, Telemetry $telemetry) {
- return new Device\Telemetry($telemetry, getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId()));
- }, ['project', 'telemetry']);
-
- $container->set('embeddingAgent', function ($register) {
+ $context->set('embeddingAgent', function ($register) {
$adapter = new Ollama();
$adapter->setEndpoint(System::getEnv('_APP_EMBEDDING_ENDPOINT', 'http://ollama:11434/api/embed'));
$adapter->setTimeout((int) System::getEnv('_APP_EMBEDDING_TIMEOUT', '30000'));
diff --git a/app/init/span.php b/app/init/span.php
index 8afa01b2df..f6871badfa 100644
--- a/app/init/span.php
+++ b/app/init/span.php
@@ -3,11 +3,30 @@
use Utopia\Span\Exporter;
use Utopia\Span\Span;
use Utopia\Span\Storage;
+use Utopia\System\System;
Span::setStorage(new Storage\Coroutine());
-Span::addExporter(new Exporter\Pretty(), function (Span $span): bool {
+
+// Resolve trace filters once at boot to avoid repeated env lookups per span.
+$traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
+$traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
+$traceEnabled = $traceProjectId !== '' || $traceFunctionId !== '';
+
+Span::addExporter(new Exporter\Pretty(), function (Span $span) use ($traceEnabled, $traceProjectId, $traceFunctionId): bool {
if (\str_starts_with($span->getAction(), 'listener.')) {
return $span->getError() !== null;
}
+
+ // Selective tracing: when _APP_TRACE_PROJECT_ID / _APP_TRACE_FUNCTION_ID are set,
+ // only export spans tagged with matching project.id / function.id.
+ if ($traceEnabled) {
+ if ($traceProjectId !== '' && $span->get('project.id') !== $traceProjectId) {
+ return false;
+ }
+ if ($traceFunctionId !== '' && $span->get('function.id') !== $traceFunctionId) {
+ return false;
+ }
+ }
+
return true;
});
diff --git a/app/init/worker/message.php b/app/init/worker/message.php
index 17796fadcd..3585421a28 100644
--- a/app/init/worker/message.php
+++ b/app/init/worker/message.php
@@ -1,12 +1,7 @@
set('queueForDatabase', function (Publisher $publisher) {
- return new EventDatabase($publisher);
- }, ['publisher']);
-
- $container->set('queueForMessaging', function (Publisher $publisher) {
- return new Messaging($publisher);
- }, ['publisher']);
-
- $container->set('queueForMails', function (Publisher $publisher) {
- return new Mail($publisher);
- }, ['publisher']);
-
- $container->set('queueForBuilds', function (Publisher $publisher) {
- return new Build($publisher);
- }, ['publisher']);
-
- $container->set('queueForDeletes', function (Publisher $publisher) {
- return new Delete($publisher);
- }, ['publisher']);
-
$container->set('queueForEvents', function (Publisher $publisher) {
return new Event($publisher);
}, ['publisher']);
@@ -358,10 +334,10 @@ return function (Container $container): void {
return new Webhook($publisher);
}, ['publisher']);
- $container->set('queueForFunctions', function (Publisher $publisher) {
- return new Func($publisher);
- }, ['publisher']);
-
+ $container->set('publisherForFunctions', fn (Publisher $publisher) => new FunctionPublisher(
+ $publisher,
+ new Queue(System::getEnv('_APP_FUNCTIONS_QUEUE_NAME', Event::FUNCTIONS_QUEUE_NAME), 'utopia-queue', Event::FUNCTIONS_QUEUE_TTL)
+ ), ['publisher']);
$container->set('queueForRealtime', function () {
return new Realtime();
}, []);
diff --git a/app/realtime.php b/app/realtime.php
index 826d751b14..9f42d77461 100644
--- a/app/realtime.php
+++ b/app/realtime.php
@@ -728,8 +728,8 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
$success = false;
Span::init('realtime.open');
- Span::add('realtime.connectionId', $connection);
- Span::add('realtime.inboundBytes', $rawSize);
+ Span::add('realtime.connection.id', $connection);
+ Span::add('realtime.inbound_bytes', $rawSize);
if (!empty($request->getOrigin())) {
Span::add('realtime.origin', $request->getOrigin());
}
@@ -936,16 +936,16 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
Span::error($th);
} finally {
Span::add('realtime.success', $success);
- Span::add('realtime.responseCode', $responseCode);
- Span::add('realtime.subscriptionMode', $subscriptionMode);
- Span::add('realtime.channelCount', $channelCount);
- Span::add('realtime.subscriptionCount', $subscriptionCount);
- Span::add('realtime.outboundBytes', $outboundBytes);
+ Span::add('realtime.response_code', $responseCode);
+ Span::add('realtime.subscription_mode', $subscriptionMode);
+ Span::add('realtime.channel_count', $channelCount);
+ Span::add('realtime.subscription_count', $subscriptionCount);
+ Span::add('realtime.outbound_bytes', $outboundBytes);
if (!empty($project?->getId())) {
- Span::add('realtime.projectId', $project->getId());
+ Span::add('project.id', $project->getId());
}
if (!empty($logUser?->getId())) {
- Span::add('realtime.userId', $logUser->getId());
+ Span::add('user.id', $logUser->getId());
}
Span::current()?->finish();
}
@@ -965,9 +965,9 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
$success = false;
Span::init('realtime.message');
- Span::add('realtime.connectionId', $connection);
- Span::add('realtime.inboundBytes', $rawSize);
- Span::add('realtime.containerId', $containerId);
+ Span::add('realtime.connection.id', $connection);
+ Span::add('realtime.inbound_bytes', $rawSize);
+ Span::add('realtime.container.id', $containerId);
try {
$response = new Response(new SwooleResponse());
@@ -1352,15 +1352,15 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
Span::error($th);
} finally {
Span::add('realtime.success', $success);
- Span::add('realtime.responseCode', $responseCode);
- Span::add('realtime.subscriptionDelta', $subscriptionDelta);
- Span::add('realtime.subscriptionsRequested', $subscriptionsRequested);
- Span::add('realtime.subscriptionsRemoved', $subscriptionsRemoved);
- Span::add('realtime.subscribe.subscriptionsCount', $subscriptionsRequested);
- Span::add('realtime.outboundBytes', $outboundBytes);
- Span::add('realtime.projectId', $project?->getId() ?? $projectId);
- Span::add('realtime.userId', $realtime->connections[$connection]['userId'] ?? null);
- Span::add('realtime.messageType', $messageType);
+ Span::add('realtime.response_code', $responseCode);
+ Span::add('realtime.subscription_delta', $subscriptionDelta);
+ Span::add('realtime.subscriptions_requested', $subscriptionsRequested);
+ Span::add('realtime.subscriptions_removed', $subscriptionsRemoved);
+ Span::add('realtime.subscribe.subscriptions_count', $subscriptionsRequested);
+ Span::add('realtime.outbound_bytes', $outboundBytes);
+ Span::add('project.id', $project?->getId() ?? $projectId);
+ Span::add('user.id', $realtime->connections[$connection]['userId'] ?? null);
+ Span::add('realtime.message_type', $messageType);
Span::current()?->finish();
}
});
@@ -1372,7 +1372,7 @@ $server->onClose(function (int $connection) use ($realtime, $stats, $register) {
$success = false;
Span::init('realtime.close');
- Span::add('realtime.connectionId', $connection);
+ Span::add('realtime.connection.id', $connection);
if (array_key_exists($connection, $realtime->connections)) {
$projectId = $realtime->connections[$connection]['projectId'] ?? null;
@@ -1411,12 +1411,12 @@ $server->onClose(function (int $connection) use ($realtime, $stats, $register) {
Span::add('realtime.success', $success);
if (!empty($projectId)) {
- Span::add('realtime.projectId', $projectId);
+ Span::add('project.id', $projectId);
}
if (!empty($userId)) {
- Span::add('realtime.userId', $userId);
+ Span::add('user.id', $userId);
}
- Span::add('realtime.subscriptionsBeforeClose', $subscriptionsBeforeClose);
+ Span::add('realtime.subscriptions_before_close', $subscriptionsBeforeClose);
Span::current()?->finish();
}
diff --git a/app/views/install/compose.phtml b/app/views/install/compose.phtml
index 1bf36b7f6d..6ce1fb5cea 100644
--- a/app/views/install/compose.phtml
+++ b/app/views/install/compose.phtml
@@ -881,6 +881,13 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_ENV
- _APP_WORKER_PER_CORE
- _APP_OPENSSL_KEY_V1
+ - _APP_OPTIONS_FORCE_HTTPS
+ - _APP_DOMAIN
+ - _APP_CONSOLE_DOMAIN
+ - _APP_DOMAIN_FUNCTIONS
+ - _APP_DOMAIN_SITES
+ - _APP_MIGRATION_HOST
+ - _APP_CONSOLE_SCHEMA
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
@@ -909,6 +916,13 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_ENV
- _APP_WORKER_PER_CORE
- _APP_OPENSSL_KEY_V1
+ - _APP_OPTIONS_FORCE_HTTPS
+ - _APP_DOMAIN
+ - _APP_CONSOLE_DOMAIN
+ - _APP_DOMAIN_FUNCTIONS
+ - _APP_DOMAIN_SITES
+ - _APP_MIGRATION_HOST
+ - _APP_CONSOLE_SCHEMA
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
diff --git a/app/worker.php b/app/worker.php
index 12b822c4eb..169b8e9770 100644
--- a/app/worker.php
+++ b/app/worker.php
@@ -16,6 +16,7 @@ use Utopia\Pools\Group;
use Utopia\Queue\Adapter\Swoole;
use Utopia\Queue\Broker\Pool as BrokerPool;
use Utopia\Queue\Server;
+use Utopia\Span\Span;
use Utopia\System\System;
Runtime::enableCoroutine();
@@ -91,8 +92,13 @@ $adapter = new Swoole(
$worker = new Server($adapter, $container);
try {
- $worker->init()->action(function () use ($worker, $registerWorkerMessageResources) {
+ $worker->init()->action(function () use ($worker, $registerWorkerMessageResources, $queueName) {
$registerWorkerMessageResources($worker->getContainer());
+ Span::init("worker.{$queueName}");
+ });
+
+ $worker->shutdown()->action(function () {
+ Span::current()?->finish();
});
$container->set('bus', function ($register) use ($worker) {
@@ -120,6 +126,8 @@ $worker
->action(function (Throwable $error, ?Logger $logger, Log $log, Document $project, Authorization $authorization) use ($queueName) {
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
+ Span::error($error);
+
if ($logger) {
$log->setNamespace('appwrite-worker');
$log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname()));
diff --git a/composer.json b/composer.json
index ca1919a062..fe6ec5dd0d 100644
--- a/composer.json
+++ b/composer.json
@@ -51,42 +51,42 @@
"ext-sockets": "*",
"appwrite/php-runtimes": "0.20.*",
"appwrite/php-clamav": "2.0.*",
- "utopia-php/abuse": "1.2.*",
+ "utopia-php/abuse": "1.3.*",
"utopia-php/agents": "1.2.*",
"utopia-php/analytics": "0.15.*",
- "utopia-php/audit": "2.2.*",
+ "utopia-php/audit": "2.3.*",
"utopia-php/auth": "0.5.*",
- "utopia-php/cache": "1.0.*",
+ "utopia-php/cache": "^2.1",
"utopia-php/cli": "0.23.*",
"utopia-php/compression": "0.1.*",
"utopia-php/config": "1.*",
"utopia-php/console": "0.1.*",
"utopia-php/database": "5.*",
"utopia-php/detector": "0.2.*",
- "utopia-php/domains": "1.*",
- "utopia-php/emails": "0.6.*",
- "utopia-php/dns": "1.6.*",
+ "utopia-php/domains": "2.*",
+ "utopia-php/emails": "0.7.*",
+ "utopia-php/dns": "1.7.*",
"utopia-php/dsn": "0.2.1",
- "utopia-php/http": "0.34.*",
- "utopia-php/fetch": "0.5.*",
+ "utopia-php/http": "^2.0@RC",
+ "utopia-php/fetch": "^1.1",
"utopia-php/validators": "0.2.*",
"utopia-php/image": "0.8.*",
"utopia-php/locale": "0.8.*",
"utopia-php/lock": "dev-main",
- "utopia-php/logger": "0.6.*",
+ "utopia-php/logger": "0.8.*",
"utopia-php/messaging": "0.22.*",
- "utopia-php/migration": "1.9.*",
- "utopia-php/platform": "0.13.*",
+ "utopia-php/migration": "1.*",
+ "utopia-php/platform": "1.0.0-rc2",
"utopia-php/pools": "1.*",
"utopia-php/span": "1.1.*",
"utopia-php/preloader": "0.2.*",
- "utopia-php/queue": "0.17.*",
- "utopia-php/servers": "0.3.*",
+ "utopia-php/queue": "0.18.*",
+ "utopia-php/servers": "0.4.*",
"utopia-php/registry": "0.5.*",
"utopia-php/storage": "2.*",
"utopia-php/system": "0.10.*",
"utopia-php/telemetry": "0.2.*",
- "utopia-php/vcs": "3.*",
+ "utopia-php/vcs": "4.*",
"utopia-php/websocket": "1.0.*",
"matomo/device-detector": "6.4.*",
"dragonmantank/cron-expression": "3.4.*",
diff --git a/composer.lock b/composer.lock
index 43f3dbf228..8ccfb9e744 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "acb2a97c8a37ef8145b70cc385b1091e",
+ "content-hash": "864ea52ffc77c76363c5b487f49a1c77",
"packages": [
{
"name": "adhocore/jwt",
@@ -69,25 +69,25 @@
},
{
"name": "appwrite/appwrite",
- "version": "19.1.0",
+ "version": "23.1.1",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-for-php.git",
- "reference": "8738e812062f899c85b2598eef43d6a247f08a56"
+ "reference": "fd7c0f0bf5ddf334533534b20ed967cfb400f6ea"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/8738e812062f899c85b2598eef43d6a247f08a56",
- "reference": "8738e812062f899c85b2598eef43d6a247f08a56",
+ "url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/fd7c0f0bf5ddf334533534b20ed967cfb400f6ea",
+ "reference": "fd7c0f0bf5ddf334533534b20ed967cfb400f6ea",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-json": "*",
- "php": ">=7.1.0"
+ "php": ">=8.2.0"
},
"require-dev": {
- "mockery/mockery": "^1.6.12",
+ "mockery/mockery": "1.6.12",
"phpunit/phpunit": "^10"
},
"type": "library",
@@ -100,14 +100,14 @@
"license": [
"BSD-3-Clause"
],
- "description": "Appwrite is an open-source self-hosted backend server that abstract and simplify complex and repetitive development tasks behind a very simple REST API",
+ "description": "Appwrite is an open-source self-hosted backend server that abstracts and simplifies complex and repetitive development tasks behind a very simple REST API",
"support": {
"email": "team@appwrite.io",
"issues": "https://github.com/appwrite/sdk-for-php/issues",
- "source": "https://github.com/appwrite/sdk-for-php/tree/19.1.0",
+ "source": "https://github.com/appwrite/sdk-for-php/tree/23.1.1",
"url": "https://appwrite.io/support"
},
- "time": "2025-12-18T08:07:43+00:00"
+ "time": "2026-05-12T11:03:36+00:00"
},
{
"name": "appwrite/php-clamav",
@@ -3359,24 +3359,24 @@
},
{
"name": "utopia-php/abuse",
- "version": "1.2.3",
+ "version": "1.3.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/abuse.git",
- "reference": "53f4274939353522ba331f55bcff6e6011ffc56c"
+ "reference": "5d7efbe5c6b0cf7d06003114fd86e24ba785582f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/utopia-php/abuse/zipball/53f4274939353522ba331f55bcff6e6011ffc56c",
- "reference": "53f4274939353522ba331f55bcff6e6011ffc56c",
+ "url": "https://api.github.com/repos/utopia-php/abuse/zipball/5d7efbe5c6b0cf7d06003114fd86e24ba785582f",
+ "reference": "5d7efbe5c6b0cf7d06003114fd86e24ba785582f",
"shasum": ""
},
"require": {
- "appwrite/appwrite": "19.*",
+ "appwrite/appwrite": "23.*",
"ext-curl": "*",
"ext-pdo": "*",
"ext-redis": "*",
- "php": ">=8.0",
+ "php": ">=8.2",
"utopia-php/database": "5.*"
},
"require-dev": {
@@ -3405,27 +3405,27 @@
],
"support": {
"issues": "https://github.com/utopia-php/abuse/issues",
- "source": "https://github.com/utopia-php/abuse/tree/1.2.3"
+ "source": "https://github.com/utopia-php/abuse/tree/1.3.0"
},
- "time": "2026-04-29T11:19:08+00:00"
+ "time": "2026-05-11T08:07:02+00:00"
},
{
"name": "utopia-php/agents",
- "version": "1.2.1",
+ "version": "1.2.2",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/agents.git",
- "reference": "052227953678a30ecc4b5467401fcb0b2386471e"
+ "reference": "0703f4cae02261e09a1bf0d39a4b1ce649cae634"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/utopia-php/agents/zipball/052227953678a30ecc4b5467401fcb0b2386471e",
- "reference": "052227953678a30ecc4b5467401fcb0b2386471e",
+ "url": "https://api.github.com/repos/utopia-php/agents/zipball/0703f4cae02261e09a1bf0d39a4b1ce649cae634",
+ "reference": "0703f4cae02261e09a1bf0d39a4b1ce649cae634",
"shasum": ""
},
"require": {
"php": ">=8.3",
- "utopia-php/fetch": "0.5.*"
+ "utopia-php/fetch": "^1.1.0"
},
"require-dev": {
"laravel/pint": "^1.18",
@@ -3458,9 +3458,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/agents/issues",
- "source": "https://github.com/utopia-php/agents/tree/1.2.1"
+ "source": "https://github.com/utopia-php/agents/tree/1.2.2"
},
- "time": "2026-02-24T06:03:55+00:00"
+ "time": "2026-05-08T10:38:23+00:00"
},
{
"name": "utopia-php/analytics",
@@ -3510,22 +3510,23 @@
},
{
"name": "utopia-php/audit",
- "version": "2.2.2",
+ "version": "2.3.2",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/audit.git",
- "reference": "90886c202e7983999e6b6a8201004d5ab61d4b57"
+ "reference": "e7b4049fc2ee9be34bcc18771fa593db3b0e9fe3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/utopia-php/audit/zipball/90886c202e7983999e6b6a8201004d5ab61d4b57",
- "reference": "90886c202e7983999e6b6a8201004d5ab61d4b57",
+ "url": "https://api.github.com/repos/utopia-php/audit/zipball/e7b4049fc2ee9be34bcc18771fa593db3b0e9fe3",
+ "reference": "e7b4049fc2ee9be34bcc18771fa593db3b0e9fe3",
"shasum": ""
},
"require": {
- "php": ">=8.0",
+ "php": ">=8.4",
"utopia-php/database": "5.*",
- "utopia-php/fetch": "0.5.*",
+ "utopia-php/fetch": "^1.1",
+ "utopia-php/query": "0.1.*",
"utopia-php/validators": "0.2.*"
},
"require-dev": {
@@ -3553,9 +3554,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/audit/issues",
- "source": "https://github.com/utopia-php/audit/tree/2.2.2"
+ "source": "https://github.com/utopia-php/audit/tree/2.3.2"
},
- "time": "2026-05-04T06:48:58+00:00"
+ "time": "2026-05-14T04:00:37+00:00"
},
{
"name": "utopia-php/auth",
@@ -3614,23 +3615,24 @@
},
{
"name": "utopia-php/cache",
- "version": "1.0.3",
+ "version": "2.1.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/cache.git",
- "reference": "ef52a04e8bfa314c621e3d3326ffcf50db3dfdfa"
+ "reference": "fc3b9ae33c4b83e0e2c91ecf60b4f40fb7ee8f8e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/utopia-php/cache/zipball/ef52a04e8bfa314c621e3d3326ffcf50db3dfdfa",
- "reference": "ef52a04e8bfa314c621e3d3326ffcf50db3dfdfa",
+ "url": "https://api.github.com/repos/utopia-php/cache/zipball/fc3b9ae33c4b83e0e2c91ecf60b4f40fb7ee8f8e",
+ "reference": "fc3b9ae33c4b83e0e2c91ecf60b4f40fb7ee8f8e",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-memcached": "*",
"ext-redis": "*",
- "php": ">=8.0",
+ "php": ">=8.3",
+ "utopia-php/circuit-breaker": "0.3.*",
"utopia-php/pools": "1.*",
"utopia-php/telemetry": "*"
},
@@ -3638,6 +3640,7 @@
"laravel/pint": "1.2.*",
"phpstan/phpstan": "^1.12",
"phpunit/phpunit": "^9.3",
+ "swoole/ide-helper": "^6.0",
"vimeo/psalm": "4.13.1"
},
"type": "library",
@@ -3660,27 +3663,89 @@
],
"support": {
"issues": "https://github.com/utopia-php/cache/issues",
- "source": "https://github.com/utopia-php/cache/tree/1.0.3"
+ "source": "https://github.com/utopia-php/cache/tree/2.1.0"
},
- "time": "2026-05-11T11:02:13+00:00"
+ "time": "2026-05-12T15:03:23+00:00"
},
{
- "name": "utopia-php/cli",
- "version": "0.23.2",
+ "name": "utopia-php/circuit-breaker",
+ "version": "0.3.0",
"source": {
"type": "git",
- "url": "https://github.com/utopia-php/cli.git",
- "reference": "145b91fef827853bcceaa3ab8ca2b1d6faaca2ab"
+ "url": "https://github.com/utopia-php/circuit-breaker.git",
+ "reference": "064243c1667778c00abf027ff53a735a228776de"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/utopia-php/cli/zipball/145b91fef827853bcceaa3ab8ca2b1d6faaca2ab",
- "reference": "145b91fef827853bcceaa3ab8ca2b1d6faaca2ab",
+ "url": "https://api.github.com/repos/utopia-php/circuit-breaker/zipball/064243c1667778c00abf027ff53a735a228776de",
+ "reference": "064243c1667778c00abf027ff53a735a228776de",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "laravel/pint": "^1.29",
+ "phpstan/phpstan": "^2.1",
+ "phpunit/phpunit": "^10.0",
+ "utopia-php/telemetry": "0.2.*"
+ },
+ "suggest": {
+ "ext-opentelemetry": "Required by utopia-php/telemetry when using OpenTelemetry metrics.",
+ "ext-protobuf": "Required by utopia-php/telemetry when using OpenTelemetry metrics.",
+ "ext-redis": "Required when using Utopia\\CircuitBreaker\\Adapter\\Redis with the phpredis extension.",
+ "ext-swoole": "Required when using Utopia\\CircuitBreaker\\Adapter\\SwooleTable.",
+ "utopia-php/telemetry": "Required when passing telemetry adapters or running the local telemetry demo."
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Utopia\\CircuitBreaker\\": "src/CircuitBreaker"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Team Appwrite",
+ "email": "team@appwrite.io"
+ }
+ ],
+ "description": "Light & simple Circuit Breaker for PHP to prevent cascading failures in distributed systems.",
+ "keywords": [
+ "circuit-breaker",
+ "fault-tolerance",
+ "framework",
+ "php",
+ "resilience",
+ "upf",
+ "utopia"
+ ],
+ "support": {
+ "issues": "https://github.com/utopia-php/circuit-breaker/issues",
+ "source": "https://github.com/utopia-php/circuit-breaker/tree/0.3.0"
+ },
+ "time": "2026-05-12T04:27:08+00:00"
+ },
+ {
+ "name": "utopia-php/cli",
+ "version": "0.23.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/utopia-php/cli.git",
+ "reference": "3c45ae5bcdcd3c7916e1909d74c60b8e771610db"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/utopia-php/cli/zipball/3c45ae5bcdcd3c7916e1909d74c60b8e771610db",
+ "reference": "3c45ae5bcdcd3c7916e1909d74c60b8e771610db",
"shasum": ""
},
"require": {
"php": ">=7.4",
- "utopia-php/servers": "0.3.*"
+ "utopia-php/servers": "0.4.0"
},
"require-dev": {
"laravel/pint": "1.2.*",
@@ -3711,9 +3776,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/cli/issues",
- "source": "https://github.com/utopia-php/cli/tree/0.23.2"
+ "source": "https://github.com/utopia-php/cli/tree/0.23.3"
},
- "time": "2026-04-27T09:19:04+00:00"
+ "time": "2026-05-05T04:38:59+00:00"
},
{
"name": "utopia-php/compression",
@@ -3858,16 +3923,16 @@
},
{
"name": "utopia-php/database",
- "version": "5.7.0",
+ "version": "5.8.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
- "reference": "eb35e68f7f90932d5a60bd72e70158ae7a4e0511"
+ "reference": "3391c97318f0e7f94d2c1ea0f7d09e5ba8aad696"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/utopia-php/database/zipball/eb35e68f7f90932d5a60bd72e70158ae7a4e0511",
- "reference": "eb35e68f7f90932d5a60bd72e70158ae7a4e0511",
+ "url": "https://api.github.com/repos/utopia-php/database/zipball/3391c97318f0e7f94d2c1ea0f7d09e5ba8aad696",
+ "reference": "3391c97318f0e7f94d2c1ea0f7d09e5ba8aad696",
"shasum": ""
},
"require": {
@@ -3876,7 +3941,7 @@
"ext-pdo": "*",
"ext-redis": "*",
"php": ">=8.4",
- "utopia-php/cache": "1.*",
+ "utopia-php/cache": "^2.0",
"utopia-php/console": "0.1.*",
"utopia-php/mongo": "1.*",
"utopia-php/pools": "1.*",
@@ -3912,9 +3977,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/database/issues",
- "source": "https://github.com/utopia-php/database/tree/5.7.0"
+ "source": "https://github.com/utopia-php/database/tree/5.8.0"
},
- "time": "2026-05-06T01:04:08+00:00"
+ "time": "2026-05-12T12:52:44+00:00"
},
{
"name": "utopia-php/detector",
@@ -4014,21 +4079,21 @@
},
{
"name": "utopia-php/dns",
- "version": "1.6.6",
+ "version": "1.7.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/dns.git",
- "reference": "917901ecfe5f09a540e4f689b6cbb80b9f55035d"
+ "reference": "90bf1bc4a51ceca93590d09e7365317b28d1eb89"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/utopia-php/dns/zipball/917901ecfe5f09a540e4f689b6cbb80b9f55035d",
- "reference": "917901ecfe5f09a540e4f689b6cbb80b9f55035d",
+ "url": "https://api.github.com/repos/utopia-php/dns/zipball/90bf1bc4a51ceca93590d09e7365317b28d1eb89",
+ "reference": "90bf1bc4a51ceca93590d09e7365317b28d1eb89",
"shasum": ""
},
"require": {
"php": ">=8.3",
- "utopia-php/domains": "1.0.*",
+ "utopia-php/domains": "^2.0",
"utopia-php/span": "1.1.*",
"utopia-php/telemetry": "*",
"utopia-php/validators": "0.*"
@@ -4065,27 +4130,27 @@
],
"support": {
"issues": "https://github.com/utopia-php/dns/issues",
- "source": "https://github.com/utopia-php/dns/tree/1.6.6"
+ "source": "https://github.com/utopia-php/dns/tree/1.7.0"
},
- "time": "2026-03-27T11:13:50+00:00"
+ "time": "2026-05-13T07:11:31+00:00"
},
{
"name": "utopia-php/domains",
- "version": "1.0.6",
+ "version": "2.0.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/domains.git",
- "reference": "c87ba0a1da4cbf75d2cff9d3ea0262b78f1d86f6"
+ "reference": "7f76390998359ef67fcea168f614cbd63a4001e8"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/utopia-php/domains/zipball/c87ba0a1da4cbf75d2cff9d3ea0262b78f1d86f6",
- "reference": "c87ba0a1da4cbf75d2cff9d3ea0262b78f1d86f6",
+ "url": "https://api.github.com/repos/utopia-php/domains/zipball/7f76390998359ef67fcea168f614cbd63a4001e8",
+ "reference": "7f76390998359ef67fcea168f614cbd63a4001e8",
"shasum": ""
},
"require": {
"php": ">=8.2",
- "utopia-php/cache": "1.0.*",
+ "utopia-php/cache": "^2.0",
"utopia-php/validators": "0.*"
},
"require-dev": {
@@ -4127,9 +4192,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/domains/issues",
- "source": "https://github.com/utopia-php/domains/tree/1.0.6"
+ "source": "https://github.com/utopia-php/domains/tree/2.0.0"
},
- "time": "2026-04-29T11:08:10+00:00"
+ "time": "2026-05-12T12:52:53+00:00"
},
{
"name": "utopia-php/dsn",
@@ -4180,21 +4245,21 @@
},
{
"name": "utopia-php/emails",
- "version": "0.6.10",
+ "version": "0.7.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/emails.git",
- "reference": "2e397754ce68c2ba918564b9f31d9923c0a90429"
+ "reference": "115e24aa908e2b1f06c7ff3b94434a0bdbed9107"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/utopia-php/emails/zipball/2e397754ce68c2ba918564b9f31d9923c0a90429",
- "reference": "2e397754ce68c2ba918564b9f31d9923c0a90429",
+ "url": "https://api.github.com/repos/utopia-php/emails/zipball/115e24aa908e2b1f06c7ff3b94434a0bdbed9107",
+ "reference": "115e24aa908e2b1f06c7ff3b94434a0bdbed9107",
"shasum": ""
},
"require": {
"php": ">=8.0",
- "utopia-php/domains": "^1.0",
+ "utopia-php/domains": "^2.0",
"utopia-php/validators": "0.*"
},
"require-dev": {
@@ -4235,22 +4300,22 @@
],
"support": {
"issues": "https://github.com/utopia-php/emails/issues",
- "source": "https://github.com/utopia-php/emails/tree/0.6.10"
+ "source": "https://github.com/utopia-php/emails/tree/0.7.0"
},
- "time": "2026-05-08T10:16:22+00:00"
+ "time": "2026-05-13T05:01:26+00:00"
},
{
"name": "utopia-php/fetch",
- "version": "0.5.1",
+ "version": "1.1.2",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/fetch.git",
- "reference": "a96a010e1c273f3888765449687baf58cbc61fcd"
+ "reference": "64f2b3a789480f1deb102ce684dac4217d8e98d5"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/utopia-php/fetch/zipball/a96a010e1c273f3888765449687baf58cbc61fcd",
- "reference": "a96a010e1c273f3888765449687baf58cbc61fcd",
+ "url": "https://api.github.com/repos/utopia-php/fetch/zipball/64f2b3a789480f1deb102ce684dac4217d8e98d5",
+ "reference": "64f2b3a789480f1deb102ce684dac4217d8e98d5",
"shasum": ""
},
"require": {
@@ -4259,7 +4324,8 @@
"require-dev": {
"laravel/pint": "^1.5.0",
"phpstan/phpstan": "^1.10",
- "phpunit/phpunit": "^9.5"
+ "phpunit/phpunit": "^9.5",
+ "swoole/ide-helper": "^6.0"
},
"type": "library",
"autoload": {
@@ -4274,29 +4340,29 @@
"description": "A simple library that provides an interface for making HTTP Requests.",
"support": {
"issues": "https://github.com/utopia-php/fetch/issues",
- "source": "https://github.com/utopia-php/fetch/tree/0.5.1"
+ "source": "https://github.com/utopia-php/fetch/tree/1.1.2"
},
- "time": "2025-12-18T16:25:10+00:00"
+ "time": "2026-04-29T11:19:19+00:00"
},
{
"name": "utopia-php/http",
- "version": "0.34.24",
+ "version": "2.0.0-rc1",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/http.git",
- "reference": "d1eced0627c5a9fceddf53992ed97d664b810d33"
+ "reference": "3e3b431d443844c6bf810120dee735f45880856f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/utopia-php/http/zipball/d1eced0627c5a9fceddf53992ed97d664b810d33",
- "reference": "d1eced0627c5a9fceddf53992ed97d664b810d33",
+ "url": "https://api.github.com/repos/utopia-php/http/zipball/3e3b431d443844c6bf810120dee735f45880856f",
+ "reference": "3e3b431d443844c6bf810120dee735f45880856f",
"shasum": ""
},
"require": {
"php": ">=8.3",
"utopia-php/compression": "0.1.*",
"utopia-php/di": "0.3.*",
- "utopia-php/servers": "0.3.*",
+ "utopia-php/servers": "0.4.0",
"utopia-php/telemetry": "0.2.*",
"utopia-php/validators": "0.2.*"
},
@@ -4330,9 +4396,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/http/issues",
- "source": "https://github.com/utopia-php/http/tree/0.34.24"
+ "source": "https://github.com/utopia-php/http/tree/2.0.0-rc1"
},
- "time": "2026-04-24T12:16:53+00:00"
+ "time": "2026-05-05T15:00:03+00:00"
},
{
"name": "utopia-php/image",
@@ -4507,20 +4573,21 @@
},
{
"name": "utopia-php/logger",
- "version": "0.6.2",
+ "version": "0.8.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/logger.git",
- "reference": "25b5bd2ad8bb51292f76332faa7034644fd0941d"
+ "reference": "132236c42222cd614cb882938a48f8729ef3118b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/utopia-php/logger/zipball/25b5bd2ad8bb51292f76332faa7034644fd0941d",
- "reference": "25b5bd2ad8bb51292f76332faa7034644fd0941d",
+ "url": "https://api.github.com/repos/utopia-php/logger/zipball/132236c42222cd614cb882938a48f8729ef3118b",
+ "reference": "132236c42222cd614cb882938a48f8729ef3118b",
"shasum": ""
},
"require": {
- "php": ">=8.0"
+ "php": ">=8.1",
+ "utopia-php/fetch": "^1.1"
},
"require-dev": {
"laravel/pint": "1.2.*",
@@ -4555,9 +4622,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/logger/issues",
- "source": "https://github.com/utopia-php/logger/tree/0.6.2"
+ "source": "https://github.com/utopia-php/logger/tree/0.8.0"
},
- "time": "2024-10-14T16:02:49+00:00"
+ "time": "2026-05-05T06:04:27+00:00"
},
{
"name": "utopia-php/messaging",
@@ -4612,24 +4679,24 @@
},
{
"name": "utopia-php/migration",
- "version": "1.9.7",
+ "version": "1.12.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/migration.git",
- "reference": "81b608a6871f56b70496803d12010823300aab6e"
+ "reference": "3ee6e12af256726bddc3a0402c94535132abecc6"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/utopia-php/migration/zipball/81b608a6871f56b70496803d12010823300aab6e",
- "reference": "81b608a6871f56b70496803d12010823300aab6e",
+ "url": "https://api.github.com/repos/utopia-php/migration/zipball/3ee6e12af256726bddc3a0402c94535132abecc6",
+ "reference": "3ee6e12af256726bddc3a0402c94535132abecc6",
"shasum": ""
},
"require": {
- "appwrite/appwrite": "19.*",
+ "appwrite/appwrite": "23.*",
"ext-curl": "*",
"ext-openssl": "*",
"halaxa/json-machine": "^1.2",
- "php": ">=8.1",
+ "php": ">=8.2",
"utopia-php/database": "5.*",
"utopia-php/dsn": "0.2.*",
"utopia-php/storage": "2.*"
@@ -4661,9 +4728,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/migration/issues",
- "source": "https://github.com/utopia-php/migration/tree/1.9.7"
+ "source": "https://github.com/utopia-php/migration/tree/1.12.0"
},
- "time": "2026-05-05T07:18:48+00:00"
+ "time": "2026-05-14T07:30:09+00:00"
},
{
"name": "utopia-php/mongo",
@@ -4728,26 +4795,26 @@
},
{
"name": "utopia-php/platform",
- "version": "0.13.0",
+ "version": "1.0.0-rc2",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/platform.git",
- "reference": "d23af5349a7ea9ee11f9920a13626226f985522e"
+ "reference": "a67e5037007ee7fdca5359ab4577b82917e55452"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/utopia-php/platform/zipball/d23af5349a7ea9ee11f9920a13626226f985522e",
- "reference": "d23af5349a7ea9ee11f9920a13626226f985522e",
+ "url": "https://api.github.com/repos/utopia-php/platform/zipball/a67e5037007ee7fdca5359ab4577b82917e55452",
+ "reference": "a67e5037007ee7fdca5359ab4577b82917e55452",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-redis": "*",
- "php": ">=8.1",
+ "php": ">=8.3",
"utopia-php/cli": "0.23.*",
- "utopia-php/http": "0.34.*",
- "utopia-php/queue": "0.17.*",
- "utopia-php/servers": "0.3.*"
+ "utopia-php/http": "^2.0@RC",
+ "utopia-php/queue": "0.18.*",
+ "utopia-php/servers": "0.4.*"
},
"require-dev": {
"laravel/pint": "1.2.*",
@@ -4773,9 +4840,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/platform/issues",
- "source": "https://github.com/utopia-php/platform/tree/0.13.0"
+ "source": "https://github.com/utopia-php/platform/tree/1.0.0-rc2"
},
- "time": "2026-04-17T09:57:18+00:00"
+ "time": "2026-05-15T06:19:20+00:00"
},
{
"name": "utopia-php/pools",
@@ -4884,26 +4951,71 @@
"time": "2020-10-24T07:04:59+00:00"
},
{
- "name": "utopia-php/queue",
- "version": "0.17.0",
+ "name": "utopia-php/query",
+ "version": "0.1.1",
"source": {
"type": "git",
- "url": "https://github.com/utopia-php/queue.git",
- "reference": "0fbc7d7312f5cf76ec112513fb93317000901f5f"
+ "url": "https://github.com/utopia-php/query.git",
+ "reference": "964a10ed3185490505f4c0062f2eb7b89287fb27"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/utopia-php/queue/zipball/0fbc7d7312f5cf76ec112513fb93317000901f5f",
- "reference": "0fbc7d7312f5cf76ec112513fb93317000901f5f",
+ "url": "https://api.github.com/repos/utopia-php/query/zipball/964a10ed3185490505f4c0062f2eb7b89287fb27",
+ "reference": "964a10ed3185490505f4c0062f2eb7b89287fb27",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4"
+ },
+ "require-dev": {
+ "laravel/pint": "*",
+ "phpstan/phpstan": "*",
+ "phpunit/phpunit": "^12.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Utopia\\Query\\": "src/Query"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "A simple library providing a query abstraction for filtering, ordering, and pagination",
+ "keywords": [
+ "framework",
+ "php",
+ "query",
+ "upf",
+ "utopia"
+ ],
+ "support": {
+ "issues": "https://github.com/utopia-php/query/issues",
+ "source": "https://github.com/utopia-php/query/tree/0.1.1"
+ },
+ "time": "2026-03-03T09:05:14+00:00"
+ },
+ {
+ "name": "utopia-php/queue",
+ "version": "0.18.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/utopia-php/queue.git",
+ "reference": "141aad162b90728353f3aa834684b1f2affed045"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/utopia-php/queue/zipball/141aad162b90728353f3aa834684b1f2affed045",
+ "reference": "141aad162b90728353f3aa834684b1f2affed045",
"shasum": ""
},
"require": {
"php": ">=8.3",
"php-amqplib/php-amqplib": "^3.7",
"utopia-php/di": "0.3.*",
- "utopia-php/fetch": "0.5.*",
"utopia-php/pools": "1.*",
- "utopia-php/servers": "0.3.*",
+ "utopia-php/servers": "0.4.0",
"utopia-php/telemetry": "0.2.*",
"utopia-php/validators": "0.2.*"
},
@@ -4946,9 +5058,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/queue/issues",
- "source": "https://github.com/utopia-php/queue/tree/0.17.0"
+ "source": "https://github.com/utopia-php/queue/tree/0.18.3"
},
- "time": "2026-03-23T16:21:31+00:00"
+ "time": "2026-05-14T08:53:35+00:00"
},
{
"name": "utopia-php/registry",
@@ -5004,16 +5116,16 @@
},
{
"name": "utopia-php/servers",
- "version": "0.3.0",
+ "version": "0.4.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/servers.git",
- "reference": "235be31200df9437fc96a1c270ffef4c64fafe52"
+ "reference": "7db346ef377503efe0acafe0791085270cd9ed70"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/utopia-php/servers/zipball/235be31200df9437fc96a1c270ffef4c64fafe52",
- "reference": "235be31200df9437fc96a1c270ffef4c64fafe52",
+ "url": "https://api.github.com/repos/utopia-php/servers/zipball/7db346ef377503efe0acafe0791085270cd9ed70",
+ "reference": "7db346ef377503efe0acafe0791085270cd9ed70",
"shasum": ""
},
"require": {
@@ -5052,9 +5164,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/servers/issues",
- "source": "https://github.com/utopia-php/servers/tree/0.3.0"
+ "source": "https://github.com/utopia-php/servers/tree/0.4.0"
},
- "time": "2026-03-13T11:31:42+00:00"
+ "time": "2026-05-05T04:08:30+00:00"
},
{
"name": "utopia-php/span",
@@ -5310,23 +5422,23 @@
},
{
"name": "utopia-php/vcs",
- "version": "3.2.0",
+ "version": "4.1.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/vcs.git",
- "reference": "44a84ab52b42fc12f812b4d7331286b519d39db3"
+ "reference": "2850dbe975ee69b9466ee6df385fe1679394ce78"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/utopia-php/vcs/zipball/44a84ab52b42fc12f812b4d7331286b519d39db3",
- "reference": "44a84ab52b42fc12f812b4d7331286b519d39db3",
+ "url": "https://api.github.com/repos/utopia-php/vcs/zipball/2850dbe975ee69b9466ee6df385fe1679394ce78",
+ "reference": "2850dbe975ee69b9466ee6df385fe1679394ce78",
"shasum": ""
},
"require": {
"adhocore/jwt": "^1.1",
- "php": ">=8.0",
- "utopia-php/cache": "1.0.*",
- "utopia-php/fetch": "0.5.*"
+ "php": ">=8.2",
+ "utopia-php/cache": "^2.0",
+ "utopia-php/fetch": "^1.1"
},
"require-dev": {
"laravel/pint": "1.*.*",
@@ -5353,9 +5465,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/vcs/issues",
- "source": "https://github.com/utopia-php/vcs/tree/3.2.0"
+ "source": "https://github.com/utopia-php/vcs/tree/4.1.0"
},
- "time": "2026-04-08T16:00:31+00:00"
+ "time": "2026-05-14T10:04:10+00:00"
},
{
"name": "utopia-php/websocket",
@@ -8528,6 +8640,7 @@
"aliases": [],
"minimum-stability": "dev",
"stability-flags": {
+ "utopia-php/http": 5,
"utopia-php/lock": 20
},
"prefer-stable": true,
diff --git a/docker-compose.yml b/docker-compose.yml
index da5efac438..76f06c672a 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -255,7 +255,7 @@ services:
appwrite-console:
<<: *x-logging
container_name: appwrite-console
- image: appwrite/console:7.8.45
+ image: appwrite/console:8
restart: unless-stopped
networks:
- appwrite
@@ -1114,6 +1114,13 @@ services:
- _APP_WORKER_PER_CORE
- _APP_POOL_ADAPTER
- _APP_OPENSSL_KEY_V1
+ - _APP_OPTIONS_FORCE_HTTPS
+ - _APP_DOMAIN
+ - _APP_CONSOLE_DOMAIN
+ - _APP_DOMAIN_FUNCTIONS
+ - _APP_DOMAIN_SITES
+ - _APP_MIGRATION_HOST
+ - _APP_CONSOLE_SCHEMA
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
@@ -1145,6 +1152,13 @@ services:
- _APP_WORKER_PER_CORE
- _APP_POOL_ADAPTER
- _APP_OPENSSL_KEY_V1
+ - _APP_OPTIONS_FORCE_HTTPS
+ - _APP_DOMAIN
+ - _APP_CONSOLE_DOMAIN
+ - _APP_DOMAIN_FUNCTIONS
+ - _APP_DOMAIN_SITES
+ - _APP_MIGRATION_HOST
+ - _APP_CONSOLE_SCHEMA
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
@@ -1478,4 +1492,4 @@ volumes:
appwrite-sites:
appwrite-builds:
appwrite-config:
- appwrite-models:
\ No newline at end of file
+ appwrite-models:
diff --git a/docs/references/advisor/delete-report.md b/docs/references/advisor/delete-report.md
new file mode 100644
index 0000000000..b32ba845e2
--- /dev/null
+++ b/docs/references/advisor/delete-report.md
@@ -0,0 +1 @@
+Delete an analyzer report by its unique ID. Nested insights and CTA metadata are removed asynchronously by the deletes worker.
diff --git a/docs/references/advisor/get-insight.md b/docs/references/advisor/get-insight.md
new file mode 100644
index 0000000000..7e1e795c22
--- /dev/null
+++ b/docs/references/advisor/get-insight.md
@@ -0,0 +1 @@
+Get an insight by its unique ID, scoped to its parent report.
diff --git a/docs/references/advisor/get-report.md b/docs/references/advisor/get-report.md
new file mode 100644
index 0000000000..731c10dc8a
--- /dev/null
+++ b/docs/references/advisor/get-report.md
@@ -0,0 +1 @@
+Get an analyzer report by its unique ID. The response includes the report's metadata and the nested insights it produced.
diff --git a/docs/references/advisor/list-insights.md b/docs/references/advisor/list-insights.md
new file mode 100644
index 0000000000..56d6a2fca0
--- /dev/null
+++ b/docs/references/advisor/list-insights.md
@@ -0,0 +1 @@
+List the insights produced under a single analyzer report. You can use the query params to filter your results further.
diff --git a/docs/references/advisor/list-reports.md b/docs/references/advisor/list-reports.md
new file mode 100644
index 0000000000..04b91c541a
--- /dev/null
+++ b/docs/references/advisor/list-reports.md
@@ -0,0 +1 @@
+Get a list of all the project's analyzer reports. You can use the query params to filter your results.
diff --git a/docs/references/databases/create-bigint-attribute.md b/docs/references/databases/create-bigint-attribute.md
new file mode 100644
index 0000000000..6fb607304b
--- /dev/null
+++ b/docs/references/databases/create-bigint-attribute.md
@@ -0,0 +1 @@
+Create a bigint attribute. Optionally, minimum and maximum values can be provided.
diff --git a/docs/references/databases/update-bigint-attribute.md b/docs/references/databases/update-bigint-attribute.md
new file mode 100644
index 0000000000..4a301c2216
--- /dev/null
+++ b/docs/references/databases/update-bigint-attribute.md
@@ -0,0 +1 @@
+Update a bigint attribute. Changing the `default` value will not update already existing documents.
diff --git a/docs/references/tablesdb/create-bigint-column.md b/docs/references/tablesdb/create-bigint-column.md
new file mode 100644
index 0000000000..7bbbb5aac6
--- /dev/null
+++ b/docs/references/tablesdb/create-bigint-column.md
@@ -0,0 +1 @@
+Create a bigint column. Optionally, minimum and maximum values can be provided.
diff --git a/docs/references/tablesdb/update-bigint-column.md b/docs/references/tablesdb/update-bigint-column.md
new file mode 100644
index 0000000000..0dde070f6f
--- /dev/null
+++ b/docs/references/tablesdb/update-bigint-column.md
@@ -0,0 +1 @@
+Update a bigint column. Changing the `default` value will not update already existing rows.
diff --git a/docs/references/vcs/list-repository-branches.md b/docs/references/vcs/list-repository-branches.md
index eea1795a3e..b614c2ad13 100644
--- a/docs/references/vcs/list-repository-branches.md
+++ b/docs/references/vcs/list-repository-branches.md
@@ -1 +1 @@
-Get a list of all branches from a GitHub repository in your installation. This endpoint returns the names of all branches in the repository and their total count. The GitHub installation must be properly configured and have access to the requested repository for this endpoint to work.
+Get a list of branches from a GitHub repository in your installation. This endpoint supports filtering by a search term and pagination using query strings such as `Query.limit()`, `Query.offset()`, `Query.cursorAfter()`, and `Query.cursorBefore()`. It returns branch names along with the total number of matches. The GitHub installation must be properly configured and have access to the requested repository for this endpoint to work.
diff --git a/docs/services/advisor.md b/docs/services/advisor.md
new file mode 100644
index 0000000000..2fa3943829
--- /dev/null
+++ b/docs/services/advisor.md
@@ -0,0 +1,3 @@
+The Advisor service provides read access to analyzer reports and their nested insights for a project.
+
+Use the reports endpoints to list and fetch analyzer runs, then use the insights endpoints to inspect individual findings attached to a report.
diff --git a/phpunit.xml b/phpunit.xml
index 9748c5a5c8..32e865fe35 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -38,6 +38,7 @@
./tests/e2e/Services/Messaging
./tests/e2e/Services/Migrations
./tests/e2e/Services/Project
+ ./tests/e2e/Services/Advisor
./tests/e2e/Services/Functions/FunctionsBase.php
./tests/e2e/Services/Functions/FunctionsCustomServerTest.php
./tests/e2e/Services/Functions/FunctionsCustomClientTest.php
diff --git a/src/Appwrite/Advisor/Validator/CTAs.php b/src/Appwrite/Advisor/Validator/CTAs.php
new file mode 100644
index 0000000000..14f7d788e7
--- /dev/null
+++ b/src/Appwrite/Advisor/Validator/CTAs.php
@@ -0,0 +1,83 @@
+allowedServices = $allowedServices ?? ADVISOR_CTA_SERVICES;
+ $this->allowedMethods = $allowedMethods ?? ADVISOR_CTA_METHODS;
+ }
+
+ public function getDescription(): string
+ {
+ return $this->message;
+ }
+
+ public function isArray(): bool
+ {
+ return true;
+ }
+
+ public function getType(): string
+ {
+ return self::TYPE_ARRAY;
+ }
+
+ public function isValid($value): bool
+ {
+ if (!\is_array($value)) {
+ return false;
+ }
+
+ if (\count($value) > $this->maxCount) {
+ $this->message = "A maximum of {$this->maxCount} CTAs are allowed per insight.";
+ return false;
+ }
+
+ foreach ($value as $entry) {
+ if (!\is_array($entry)) {
+ return false;
+ }
+
+ $maxLengths = ['label' => 256, 'service' => 64, 'method' => 64];
+ foreach ($maxLengths as $required => $maxLength) {
+ if (!isset($entry[$required]) || !\is_string($entry[$required]) || $entry[$required] === '') {
+ return false;
+ }
+ if (\strlen($entry[$required]) > $maxLength) {
+ $this->message = "CTA `{$required}` must not exceed {$maxLength} characters.";
+ return false;
+ }
+ }
+
+ if (!empty($this->allowedServices) && !\in_array($entry['service'], $this->allowedServices, true)) {
+ $this->message = "CTA `service` must be one of: " . \implode(', ', $this->allowedServices) . '.';
+ return false;
+ }
+
+ if (!empty($this->allowedMethods) && !\in_array($entry['method'], $this->allowedMethods, true)) {
+ $this->message = "CTA `method` must be one of: " . \implode(', ', $this->allowedMethods) . '.';
+ return false;
+ }
+
+ if (isset($entry['params']) && !\is_array($entry['params']) && !\is_object($entry['params'])) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/src/Appwrite/Auth/OAuth2/Google.php b/src/Appwrite/Auth/OAuth2/Google.php
index 79894c2422..1166a313c6 100644
--- a/src/Appwrite/Auth/OAuth2/Google.php
+++ b/src/Appwrite/Auth/OAuth2/Google.php
@@ -55,7 +55,7 @@ class Google extends OAuth2
'state' => \json_encode($this->state),
'response_type' => 'code',
'access_type' => 'offline',
- 'prompt' => 'consent'
+ 'prompt' => $this->getPrompt()
]);
}
@@ -72,7 +72,7 @@ class Google extends OAuth2
'https://oauth2.googleapis.com/token?' . \http_build_query([
'code' => $code,
'client_id' => $this->appID,
- 'client_secret' => $this->appSecret,
+ 'client_secret' => $this->getClientSecret(),
'redirect_uri' => $this->callback,
'scope' => null,
'grant_type' => 'authorization_code'
@@ -95,7 +95,7 @@ class Google extends OAuth2
'https://oauth2.googleapis.com/token?' . \http_build_query([
'refresh_token' => $refreshToken,
'client_id' => $this->appID,
- 'client_secret' => $this->appSecret,
+ 'client_secret' => $this->getClientSecret(),
'grant_type' => 'refresh_token'
])
), true);
@@ -177,4 +177,54 @@ class Google extends OAuth2
return $this->user;
}
+
+ /**
+ * Extracts the Client Secret from the JSON stored in appSecret
+ *
+ * @return string
+ */
+ protected function getClientSecret(): string
+ {
+ $secret = $this->getAppSecret();
+
+ return $secret['clientSecret'] ?? $this->appSecret;
+ }
+
+ /**
+ * Extracts the prompt values from the JSON stored in appSecret
+ *
+ * @return string
+ */
+ protected function getPrompt(): string
+ {
+ $secret = $this->getAppSecret();
+ $prompt = $secret['prompt'] ?? [];
+
+ if (empty($prompt)) {
+ $prompt = ['consent'];
+ }
+
+ return \implode(' ', $prompt);
+ }
+
+ /**
+ * Decode the JSON stored in appSecret.
+ * Falls back to treating the raw string as the client secret for backwards compatibility.
+ *
+ * @return array
+ */
+ protected function getAppSecret(): array
+ {
+ try {
+ $secret = \json_decode($this->appSecret, true, 512, JSON_THROW_ON_ERROR);
+ } catch (\Throwable $th) {
+ return ['clientSecret' => $this->appSecret];
+ }
+
+ if (!\is_array($secret)) {
+ return ['clientSecret' => $this->appSecret];
+ }
+
+ return $secret;
+ }
}
diff --git a/src/Appwrite/Bus/Listeners/Log.php b/src/Appwrite/Bus/Listeners/Log.php
index 585d4b09a7..e0376a4f81 100644
--- a/src/Appwrite/Bus/Listeners/Log.php
+++ b/src/Appwrite/Bus/Listeners/Log.php
@@ -8,7 +8,6 @@ use Appwrite\Event\Publisher\Execution as ExecutionPublisher;
use Utopia\Bus\Listener;
use Utopia\Database\Document;
use Utopia\Span\Span;
-use Utopia\System\System;
class Log extends Listener
{
@@ -34,20 +33,13 @@ class Log extends Listener
{
$project = new Document($event->project);
$execution = new Document($event->execution);
+
if ($execution->getAttribute('resourceType', '') === 'functions') {
- $traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
- $traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
- $resourceId = $execution->getAttribute('resourceId', '');
- if ($traceProjectId !== '' && $traceFunctionId !== '' && $project->getId() === $traceProjectId && $resourceId === $traceFunctionId) {
- Span::init('execution.trace.v1_executions_enqueue');
- Span::add('datetime', gmdate('c'));
- Span::add('projectId', $project->getId());
- Span::add('functionId', $resourceId);
- Span::add('executionId', $execution->getId());
- Span::add('deploymentId', $execution->getAttribute('deploymentId', ''));
- Span::add('status', $execution->getAttribute('status', ''));
- Span::current()?->finish();
- }
+ Span::add('project.id', $project->getId());
+ Span::add('function.id', $execution->getAttribute('resourceId', ''));
+ Span::add('execution.id', $execution->getId());
+ Span::add('deployment.id', $execution->getAttribute('deploymentId', ''));
+ Span::add('execution.status', $execution->getAttribute('status', ''));
}
$publisherForExecutions->enqueue(new ExecutionMessage(
diff --git a/src/Appwrite/Bus/Listeners/Mails.php b/src/Appwrite/Bus/Listeners/Mails.php
index 9b3d68519f..eb36e0d394 100644
--- a/src/Appwrite/Bus/Listeners/Mails.php
+++ b/src/Appwrite/Bus/Listeners/Mails.php
@@ -4,14 +4,14 @@ namespace Appwrite\Bus\Listeners;
use Appwrite\Auth\MFA\Type;
use Appwrite\Bus\Events\SessionCreated;
-use Appwrite\Event\Mail;
+use Appwrite\Event\Message\Mail as MailMessage;
+use Appwrite\Event\Publisher\Mail as MailPublisher;
use Appwrite\Template\Template;
use Utopia\Bus\Listener;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Locale\Locale;
-use Utopia\Queue\Publisher;
use Utopia\Storage\Validator\FileName;
use Utopia\System\System;
@@ -31,14 +31,14 @@ class Mails extends Listener
{
$this
->desc('Sends session alert emails')
- ->inject('publisher')
+ ->inject('publisherForMails')
->inject('locale')
->inject('platform')
->inject('dbForProject')
->callback($this->handle(...));
}
- public function handle(SessionCreated $event, Publisher $publisher, Locale $locale, array $platform, Database $dbForProject): void
+ public function handle(SessionCreated $event, MailPublisher $publisherForMails, Locale $locale, array $platform, Database $dbForProject): void
{
$project = new Document($event->project);
@@ -124,34 +124,32 @@ class Mails extends Listener
];
}
- $queueForMails = new Mail($publisher);
-
+ $smtpConfig = [];
if ($smtp['enabled'] ?? false) {
- $queueForMails
- ->setSmtpHost($smtp['host'] ?? '')
- ->setSmtpPort($smtp['port'] ?? '')
- ->setSmtpUsername($smtp['username'] ?? '')
- ->setSmtpPassword($smtp['password'] ?? '')
- ->setSmtpSecure($smtp['secure'] ?? '')
- ->setSmtpReplyToEmail($customTemplate['replyToEmail'] ?? $customTemplate['replyTo'] ?? $smtp['replyToEmail'] ?? $smtp['replyTo'] ?? '') // Includes backwards compatibility
- ->setSmtpReplyToName($customTemplate['replyToName'] ?? $smtp['replyToName'] ?? '')
- ->setSmtpSenderEmail($customTemplate['senderEmail'] ?? $smtp['senderEmail'] ?? System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM))
- ->setSmtpSenderName($customTemplate['senderName'] ?? $smtp['senderName'] ?? System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'));
+ $smtpConfig = [
+ 'host' => $smtp['host'] ?? '',
+ 'port' => $smtp['port'] ?? '',
+ 'username' => $smtp['username'] ?? '',
+ 'password' => $smtp['password'] ?? '',
+ 'secure' => $smtp['secure'] ?? '',
+ 'replyToEmail' => $customTemplate['replyToEmail'] ?? $customTemplate['replyTo'] ?? $smtp['replyToEmail'] ?? $smtp['replyTo'] ?? '', // Includes backwards compatibility
+ 'replyToName' => $customTemplate['replyToName'] ?? $smtp['replyToName'] ?? '',
+ 'senderEmail' => $customTemplate['senderEmail'] ?? $smtp['senderEmail'] ?? System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM),
+ 'senderName' => $customTemplate['senderName'] ?? $smtp['senderName'] ?? System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'),
+ ];
}
- $queueForMails
- ->setProject($project)
- ->setSubject($subject)
- ->setPreview($preview)
- ->setBody($body)
- ->setBodyTemplate(__DIR__ . '/../../../../app/config/locale/templates/' . $smtpBaseTemplate . '.tpl')
- ->appendVariables($emailVariables)
- ->setRecipient($event->user['email']);
-
- if ($isBranded) {
- $queueForMails->setSenderName($platform['emailSenderName']);
- }
-
- $queueForMails->trigger();
+ $publisherForMails->enqueue(new MailMessage(
+ project: $project,
+ recipient: $event->user['email'],
+ subject: $subject,
+ bodyTemplate: __DIR__ . '/../../../../app/config/locale/templates/' . $smtpBaseTemplate . '.tpl',
+ body: $body,
+ preview: $preview,
+ smtp: $smtpConfig,
+ variables: $emailVariables,
+ customMailOptions: $isBranded ? ['senderName' => $platform['emailSenderName']] : [],
+ platform: $platform,
+ ));
}
}
diff --git a/src/Appwrite/Event/Build.php b/src/Appwrite/Event/Build.php
deleted file mode 100644
index 4eaf108f15..0000000000
--- a/src/Appwrite/Event/Build.php
+++ /dev/null
@@ -1,146 +0,0 @@
-setQueue(System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::BUILDS_QUEUE_NAME))
- ->setClass(System::getEnv('_APP_BUILDS_CLASS_NAME', Event::BUILDS_CLASS_NAME));
- }
-
- /**
- * Sets template for the build event.
- *
- * @param Document $template
- * @return self
- */
- public function setTemplate(Document $template): self
- {
- $this->template = $template;
-
- return $this;
- }
-
- /**
- * Sets resource document for the build event.
- *
- * @param Document $resource
- * @return self
- */
- public function setResource(Document $resource): self
- {
- $this->resource = $resource;
-
- return $this;
- }
-
- /**
- * Returns set resource document for the build event.
- *
- * @return null|Document
- */
- public function getResource(): ?Document
- {
- return $this->resource;
- }
-
- /**
- * Sets deployment for the build event.
- *
- * @param Document $deployment
- * @return self
- */
- public function setDeployment(Document $deployment): self
- {
- $this->deployment = $deployment;
-
- return $this;
- }
-
- /**
- * Returns set deployment for the build event.
- *
- * @return null|Document
- */
- public function getDeployment(): ?Document
- {
- return $this->deployment;
- }
-
- /**
- * Sets type for the build event.
- *
- * @param string $type Can be `BUILD_TYPE_DEPLOYMENT` or `BUILD_TYPE_RETRY`.
- * @return self
- */
- public function setType(string $type): self
- {
- $this->type = $type;
-
- return $this;
- }
-
- /**
- * Returns set type for the function event.
- *
- * @return string
- */
- public function getType(): string
- {
- return $this->type;
- }
-
- /**
- * Prepare payload for queue.
- *
- * @return array
- */
- protected function preparePayload(): array
- {
- $platform = $this->platform;
- if (empty($platform)) {
- $platform = Config::getParam('platform', []);
- }
-
- return [
- 'project' => $this->project,
- 'resource' => $this->resource,
- 'deployment' => $this->deployment,
- 'type' => $this->type,
- 'template' => $this->template,
- 'platform' => $platform,
- ];
- }
-
- /**
- * Resets event.
- *
- * @return self
- */
- public function reset(): self
- {
- $this->type = '';
- $this->resource = null;
- $this->deployment = null;
- $this->template = null;
- $this->platform = [];
- parent::reset();
-
- return $this;
- }
-}
diff --git a/src/Appwrite/Event/Mail.php b/src/Appwrite/Event/Mail.php
deleted file mode 100644
index 0685586c60..0000000000
--- a/src/Appwrite/Event/Mail.php
+++ /dev/null
@@ -1,576 +0,0 @@
-setQueue(System::getEnv('_APP_MAILS_QUEUE_NAME', Event::MAILS_QUEUE_NAME))
- ->setClass(System::getEnv('_APP_MAILS_CLASS_NAME', Event::MAILS_CLASS_NAME));
- }
-
- /**
- * Sets subject for the mail event.
- *
- * @param string $subject
- * @return self
- */
- public function setSubject(string $subject): self
- {
- $this->subject = $subject;
-
- return $this;
- }
-
- /**
- * Returns subject for the mail event.
- *
- * @return string
- */
- public function getSubject(): string
- {
- return $this->subject;
- }
-
- /**
- * Sets recipient for the mail event.
- *
- * @param string $recipient
- * @return self
- */
- public function setRecipient(string $recipient): self
- {
- $this->recipient = $recipient;
-
- return $this;
- }
-
- /**
- * Returns set recipient for mail event.
- *
- * @return string
- */
- public function getRecipient(): string
- {
- return $this->recipient;
- }
-
- /**
- * Sets body for the mail event.
- *
- * @param string $body
- * @return self
- */
- public function setBody(string $body): self
- {
- $this->body = $body;
-
- return $this;
- }
-
- /**
- * Returns body for the mail event.
- *
- * @return string
- */
- public function getBody(): string
- {
- return $this->body;
- }
-
- /**
- * Sets preview for the mail event.
- *
- * @return self
- */
- public function setPreview(string $preview): self
- {
- $this->preview = $preview;
-
- return $this;
- }
-
- /**
- * Returns preview for the mail event.
- *
- * @return string
- */
- public function getPreview(): string
- {
- return $this->preview;
- }
-
- /**
- * Sets name for the mail event.
- *
- * @param string $name
- * @return self
- */
- public function setName(string $name): self
- {
- $this->name = $name;
-
- return $this;
- }
-
- /**
- * Returns set name for the mail event.
- *
- * @return string
- */
- public function getName(): string
- {
- return $this->name;
- }
-
- /**
- * Sets bodyTemplate for the mail event.
- *
- * @param string $bodyTemplate
- * @return self
- */
- public function setBodyTemplate(string $bodyTemplate): self
- {
- $this->bodyTemplate = $bodyTemplate;
-
- return $this;
- }
-
- /**
- * Returns subject for the mail event.
- *
- * @return string
- */
- public function getBodyTemplate(): string
- {
- return $this->bodyTemplate;
- }
-
- /**
- * Set SMTP Host
- *
- * @param string $host
- * @return self
- */
- public function setSmtpHost(string $host): self
- {
- $this->smtp['host'] = $host;
- return $this;
- }
-
- /**
- * Set SMTP port
- *
- * @param int $port
- * @return self
- */
- public function setSmtpPort(int $port): self
- {
- $this->smtp['port'] = $port;
- return $this;
- }
-
- /**
- * Set SMTP username
- *
- * @param string $username
- * @return self
- */
- public function setSmtpUsername(string $username): self
- {
- $this->smtp['username'] = $username;
- return $this;
- }
-
- /**
- * Set SMTP password
- *
- * @param string $password
- * @return self
- */
- public function setSmtpPassword(string $password): self
- {
- $this->smtp['password'] = $password;
- return $this;
- }
-
- /**
- * Set SMTP secure
- *
- * @param string $secure
- * @return self
- */
- public function setSmtpSecure(string $secure): self
- {
- $this->smtp['secure'] = $secure;
- return $this;
- }
-
- /**
- * Set SMTP sender email
- *
- * @param string $senderEmail
- * @return self
- */
- public function setSmtpSenderEmail(string $senderEmail): self
- {
- $this->smtp['senderEmail'] = $senderEmail;
- return $this;
- }
-
- /**
- * Set SMTP sender name
- *
- * @param string $senderName
- * @return self
- */
- public function setSmtpSenderName(string $senderName): self
- {
- $this->smtp['senderName'] = $senderName;
- return $this;
- }
-
- /**
- * Set SMTP reply-to email
- *
- * @param string $email
- * @return self
- */
- public function setSmtpReplyToEmail(string $email): self
- {
- $this->smtp['replyToEmail'] = $email;
- return $this;
- }
-
- /**
- * Set SMTP reply-to name
- *
- * @param string $name
- * @return self
- */
- public function setSmtpReplyToName(string $name): self
- {
- $this->smtp['replyToName'] = $name;
- return $this;
- }
-
- /**
- * Get SMTP
- *
- * @return string
- */
- public function getSmtpHost(): string
- {
- return $this->smtp['host'] ?? '';
- }
-
- /**
- * Get SMTP port
- *
- * @return integer
- */
- public function getSmtpPort(): int
- {
- return $this->smtp['port'] ?? 0;
- }
-
- /**
- * Get SMTP username
- *
- * @return string
- */
- public function getSmtpUsername(): string
- {
- return $this->smtp['username'] ?? '';
- }
-
- /**
- * Get SMTP password
- *
- * @return string
- */
- public function getSmtpPassword(): string
- {
- return $this->smtp['password'] ?? '';
- }
-
- /**
- * Get SMTP secure
- *
- * @return string
- */
- public function getSmtpSecure(): string
- {
- return $this->smtp['secure'] ?? '';
- }
-
- /**
- * Get SMTP sender email
- *
- * @return string
- */
- public function getSmtpSenderEmail(): string
- {
- return $this->smtp['senderEmail'] ?? '';
- }
-
- /**
- * Get SMTP sender name
- *
- * @return string
- */
- public function getSmtpSenderName(): string
- {
- return $this->smtp['senderName'] ?? '';
- }
-
- /**
- * Get SMTP reply-to email
- *
- * @return string
- */
- public function getSmtpReplyToEmail(): string
- {
- return $this->smtp['replyToEmail'] ?? '';
- }
-
- /**
- * Get SMTP reply-to name
- *
- * @return string
- */
- public function getSmtpReplyToName(): string
- {
- return $this->smtp['replyToName'] ?? '';
- }
-
- /**
- * Get Email Variables
- *
- * @return array
- */
- public function getVariables(): array
- {
- return $this->variables;
- }
-
- /**
- * Set Email Variables
- *
- * @param array $variables
- * @return self
- */
- public function setVariables(array $variables): self
- {
- $this->variables = $variables;
- return $this;
- }
-
- /**
- * Append variables to the email event.
- *
- * @param array $variables
- * @return self
- */
- public function appendVariables(array $variables): self
- {
- $this->variables = \array_merge($this->variables, $variables);
- return $this;
- }
-
- /**
- * Set attachment
- * @param string $content
- * @param string $filename
- * @param string $encoding
- * @param string $type
- * @return self
- */
- public function setAttachment(string $content, string $filename, string $encoding = 'base64', string $type = 'plain/text')
- {
- $this->attachment = [
- 'content' => base64_encode($content),
- 'filename' => $filename,
- 'encoding' => $encoding,
- 'type' => $type,
- ];
- return $this;
- }
-
- /**
- * Get attachment
- *
- * @return array
- */
- public function getAttachment(): array
- {
- return $this->attachment;
- }
-
- /**
- * Reset attachment
- *
- * @return self
- */
- public function resetAttachment(): self
- {
- $this->attachment = [];
- return $this;
- }
-
- /**
- * Set sender email
- *
- * @param string $email
- * @return self
- */
- public function setSenderEmail(string $email): self
- {
- $this->customMailOptions['senderEmail'] = $email;
- return $this;
- }
-
- /**
- * Get sender email
- *
- * @return string
- */
- public function getSenderEmail(): string
- {
- return $this->customMailOptions['senderEmail'] ?? '';
- }
-
- /**
- * Set sender name
- *
- * @param string $name
- * @return self
- */
- public function setSenderName(string $name): self
- {
- $this->customMailOptions['senderName'] = $name;
- return $this;
- }
-
- /**
- * Get sender name
- *
- * @return string
- */
- public function getSenderName(): string
- {
- return $this->customMailOptions['senderName'] ?? '';
- }
-
- /**
- * Set reply-to email
- *
- * @param string $email
- * @return self
- */
- public function setReplyToEmail(string $email): self
- {
- $this->customMailOptions['replyToEmail'] = $email;
- return $this;
- }
-
- /**
- * Get reply-to email
- *
- * @return string
- */
- public function getReplyToEmail(): string
- {
- return $this->customMailOptions['replyToEmail'] ?? '';
- }
-
- /**
- * Set reply-to name
- *
- * @param string $name
- * @return self
- */
- public function setReplyToName(string $name): self
- {
- $this->customMailOptions['replyToName'] = $name;
- return $this;
- }
-
- /**
- * Get reply-to name
- *
- * @return string
- */
- public function getReplyToName(): string
- {
- return $this->customMailOptions['replyToName'] ?? '';
- }
-
- /**
- * Reset
- *
- * @return self
- */
- public function reset(): self
- {
- $this->project = null;
- $this->recipient = '';
- $this->name = '';
- $this->subject = '';
- $this->body = '';
- $this->variables = [];
- $this->bodyTemplate = '';
- $this->attachment = [];
- $this->customMailOptions = [];
- return $this;
- }
-
- /**
- * Prepare the payload for the event
- *
- * @return array
- */
- protected function preparePayload(): array
- {
- $platform = $this->platform;
- if (empty($platform)) {
- $platform = Config::getParam('platform', []);
- }
-
- return [
- 'project' => $this->project,
- 'recipient' => $this->recipient,
- 'name' => $this->name,
- 'subject' => $this->subject,
- 'bodyTemplate' => $this->bodyTemplate,
- 'body' => $this->body,
- 'preview' => $this->preview,
- 'smtp' => $this->smtp,
- 'variables' => $this->variables,
- 'attachment' => $this->attachment,
- 'customMailOptions' => $this->customMailOptions,
- 'events' => Event::generateEvents($this->getEvent(), $this->getParams()),
- 'platform' => $platform,
- ];
- }
-}
diff --git a/src/Appwrite/Event/Message/Build.php b/src/Appwrite/Event/Message/Build.php
new file mode 100644
index 0000000000..0c8967aff6
--- /dev/null
+++ b/src/Appwrite/Event/Message/Build.php
@@ -0,0 +1,45 @@
+platform) ? $this->platform : Config::getParam('platform', []);
+
+ return [
+ 'project' => $this->project->getArrayCopy(),
+ 'resource' => $this->resource->getArrayCopy(),
+ 'deployment' => $this->deployment->getArrayCopy(),
+ 'type' => $this->type,
+ 'template' => $this->template?->getArrayCopy(),
+ 'platform' => $platform,
+ ];
+ }
+
+ public static function fromArray(array $data): static
+ {
+ return new self(
+ project: new Document($data['project'] ?? []),
+ resource: new Document($data['resource'] ?? []),
+ deployment: new Document($data['deployment'] ?? []),
+ type: $data['type'] ?? '',
+ template: !empty($data['template']) ? new Document($data['template']) : null,
+ platform: $data['platform'] ?? [],
+ );
+ }
+}
diff --git a/src/Appwrite/Event/Message/Database.php b/src/Appwrite/Event/Message/Database.php
new file mode 100644
index 0000000000..1178dcf5c7
--- /dev/null
+++ b/src/Appwrite/Event/Message/Database.php
@@ -0,0 +1,51 @@
+ $this->project?->getArrayCopy(),
+ 'user' => $this->user?->getArrayCopy(),
+ 'type' => $this->type,
+ 'table' => $this->table?->getArrayCopy(),
+ 'row' => $this->row?->getArrayCopy(),
+ 'collection' => $this->collection?->getArrayCopy(),
+ 'document' => $this->document?->getArrayCopy(),
+ 'database' => $this->database?->getArrayCopy(),
+ 'events' => $this->events,
+ ];
+ }
+
+ public static function fromArray(array $data): static
+ {
+ return new self(
+ project: !empty($data['project']) ? new Document($data['project']) : null,
+ user: !empty($data['user']) ? new Document($data['user']) : null,
+ type: $data['type'] ?? '',
+ table: !empty($data['table']) ? new Document($data['table']) : null,
+ row: !empty($data['row']) ? new Document($data['row']) : null,
+ collection: !empty($data['collection']) ? new Document($data['collection']) : null,
+ document: !empty($data['document']) ? new Document($data['document']) : null,
+ database: !empty($data['database']) ? new Document($data['database']) : null,
+ events: $data['events'] ?? [],
+ );
+ }
+}
diff --git a/src/Appwrite/Event/Message/Delete.php b/src/Appwrite/Event/Message/Delete.php
new file mode 100644
index 0000000000..6866cf3f02
--- /dev/null
+++ b/src/Appwrite/Event/Message/Delete.php
@@ -0,0 +1,45 @@
+ $this->project?->getArrayCopy(),
+ 'type' => $this->type,
+ 'document' => $this->document?->getArrayCopy(),
+ 'resource' => $this->resource,
+ 'resourceType' => $this->resourceType,
+ 'datetime' => $this->datetime,
+ 'hourlyUsageRetentionDatetime' => $this->hourlyUsageRetentionDatetime,
+ ];
+ }
+
+ public static function fromArray(array $data): static
+ {
+ return new self(
+ project: !empty($data['project']) ? new Document($data['project']) : null,
+ type: $data['type'] ?? '',
+ document: !empty($data['document']) ? new Document($data['document']) : null,
+ resource: $data['resource'] ?? null,
+ resourceType: $data['resourceType'] ?? null,
+ datetime: $data['datetime'] ?? null,
+ hourlyUsageRetentionDatetime: $data['hourlyUsageRetentionDatetime'] ?? null,
+ );
+ }
+}
diff --git a/src/Appwrite/Event/Message/Func.php b/src/Appwrite/Event/Message/Func.php
new file mode 100644
index 0000000000..2a2ae9d90f
--- /dev/null
+++ b/src/Appwrite/Event/Message/Func.php
@@ -0,0 +1,92 @@
+platform) ? $this->platform : Config::getParam('platform', []);
+
+ return [
+ 'project' => $this->project?->getArrayCopy(),
+ 'user' => $this->user?->getArrayCopy(),
+ 'userId' => $this->userId,
+ 'function' => $this->function?->getArrayCopy(),
+ 'functionId' => $this->functionId,
+ 'execution' => $this->execution?->getArrayCopy(),
+ 'type' => $this->type,
+ 'jwt' => $this->jwt,
+ 'payload' => $this->payload,
+ 'events' => $this->events,
+ 'body' => $this->body,
+ 'path' => $this->path,
+ 'headers' => $this->headers,
+ 'method' => $this->method,
+ 'platform' => $platform,
+ ];
+ }
+
+ public static function fromArray(array $data): static
+ {
+ return new self(
+ project: !empty($data['project']) ? new Document($data['project']) : null,
+ user: !empty($data['user']) ? new Document($data['user']) : null,
+ userId: $data['userId'] ?? null,
+ function: !empty($data['function']) ? new Document($data['function']) : null,
+ functionId: $data['functionId'] ?? null,
+ execution: !empty($data['execution']) ? new Document($data['execution']) : null,
+ type: $data['type'] ?? '',
+ jwt: $data['jwt'] ?? '',
+ payload: $data['payload'] ?? [],
+ events: $data['events'] ?? [],
+ body: $data['body'] ?? '',
+ path: $data['path'] ?? '',
+ headers: $data['headers'] ?? [],
+ method: $data['method'] ?? '',
+ platform: $data['platform'] ?? [],
+ );
+ }
+}
diff --git a/src/Appwrite/Event/Message/Mail.php b/src/Appwrite/Event/Message/Mail.php
new file mode 100644
index 0000000000..aeeea8a616
--- /dev/null
+++ b/src/Appwrite/Event/Message/Mail.php
@@ -0,0 +1,66 @@
+platform) ? $this->platform : Config::getParam('platform', []);
+
+ return [
+ 'project' => $this->project?->getArrayCopy(),
+ 'recipient' => $this->recipient,
+ 'name' => $this->name,
+ 'subject' => $this->subject,
+ 'bodyTemplate' => $this->bodyTemplate,
+ 'body' => $this->body,
+ 'preview' => $this->preview,
+ 'smtp' => $this->smtp,
+ 'variables' => $this->variables,
+ 'attachment' => $this->attachment,
+ 'customMailOptions' => $this->customMailOptions,
+ 'events' => $this->events,
+ 'platform' => $platform,
+ ];
+ }
+
+ public static function fromArray(array $data): static
+ {
+ return new self(
+ project: !empty($data['project']) ? new Document($data['project']) : null,
+ recipient: $data['recipient'] ?? '',
+ name: $data['name'] ?? '',
+ subject: $data['subject'] ?? '',
+ bodyTemplate: $data['bodyTemplate'] ?? '',
+ body: $data['body'] ?? '',
+ preview: $data['preview'] ?? '',
+ smtp: $data['smtp'] ?? [],
+ variables: $data['variables'] ?? [],
+ attachment: $data['attachment'] ?? [],
+ customMailOptions: $data['customMailOptions'] ?? [],
+ events: $data['events'] ?? [],
+ platform: $data['platform'] ?? [],
+ );
+ }
+}
diff --git a/src/Appwrite/Event/Message/Messaging.php b/src/Appwrite/Event/Message/Messaging.php
new file mode 100644
index 0000000000..7f0f918217
--- /dev/null
+++ b/src/Appwrite/Event/Message/Messaging.php
@@ -0,0 +1,45 @@
+ $this->type,
+ 'project' => $this->project->getArrayCopy(),
+ 'user' => $this->user?->getArrayCopy(),
+ 'messageId' => $this->messageId,
+ 'message' => $this->message?->getArrayCopy(),
+ 'recipients' => $this->recipients,
+ 'providerType' => $this->providerType,
+ ];
+ }
+
+ public static function fromArray(array $data): static
+ {
+ return new self(
+ type: $data['type'] ?? '',
+ project: new Document($data['project'] ?? []),
+ user: !empty($data['user']) ? new Document($data['user']) : null,
+ messageId: $data['messageId'] ?? null,
+ message: !empty($data['message']) ? new Document($data['message']) : null,
+ recipients: $data['recipients'] ?? null,
+ providerType: $data['providerType'] ?? null,
+ );
+ }
+}
diff --git a/src/Appwrite/Event/Messaging.php b/src/Appwrite/Event/Messaging.php
deleted file mode 100644
index 9895d52ec2..0000000000
--- a/src/Appwrite/Event/Messaging.php
+++ /dev/null
@@ -1,182 +0,0 @@
-setQueue(System::getEnv('_APP_MESSAGING_QUEUE_NAME', Event::MESSAGING_QUEUE_NAME))
- ->setClass(System::getEnv('_APP_MESSAGING_CLASS_NAME', Event::MESSAGING_CLASS_NAME));
- }
-
- /**
- * Sets type for the build event.
- *
- * @param string $type Can be `MESSAGE_SEND_TYPE_INTERNAL` or `MESSAGE_SEND_TYPE_EXTERNAL`.
- * @return self
- */
- public function setType(string $type): self
- {
- $this->type = $type;
-
- return $this;
- }
-
- /**
- * Returns set type for the function event.
- *
- * @return string
- */
- public function getType(): string
- {
- return $this->type;
- }
-
- /**
- * Sets recipient for the messaging event.
- *
- * @param string[] $recipients
- * @return self
- */
- public function setRecipients(array $recipients): self
- {
- $this->recipients = $recipients;
-
- return $this;
- }
-
- /**
- * Returns set recipient for messaging event.
- *
- * @return string[]
- */
- public function getRecipient(): array
- {
- return $this->recipients;
- }
-
- /**
- * Sets message document for the messaging event.
- *
- * @param Document $message
- * @return self
- */
- public function setMessage(Document $message): self
- {
- $this->message = $message;
-
- return $this;
- }
-
- /**
- * Returns message document for the messaging event.
- *
- * @return Document
- */
- public function getMessage(): Document
- {
- return $this->message;
- }
-
- /**
- * Sets message ID for the messaging event.
- *
- * @param string $messageId
- * @return self
- */
- public function setMessageId(string $messageId): self
- {
- $this->messageId = $messageId;
-
- return $this;
- }
-
- /**
- * Returns set message ID for the messaging event.
- *
- * @return string
- */
- public function getMessageId(): string
- {
- return $this->messageId;
- }
-
- /**
- * Sets provider type for the messaging event.
- *
- * @param string $providerType
- * @return self
- */
- public function setProviderType(string $providerType): self
- {
- $this->providerType = $providerType;
-
- return $this;
- }
-
- /**
- * Returns set provider type for the messaging event.
- *
- * @return string
- */
- public function getProviderType(): string
- {
- return $this->providerType;
- }
-
- /**
- * Sets Scheduled delivery time for the messaging event.
- *
- * @param string $scheduledAt
- * @return self
- */
- public function setScheduledAt(string $scheduledAt): self
- {
- $this->scheduledAt = $scheduledAt;
-
- return $this;
- }
-
- /**
- * Returns set Delivery Time for the messaging event.
- *
- * @return string
- */
- public function getScheduledAt(): string
- {
- return $this->scheduledAt;
- }
-
- /**
- * Prepare the payload for the event
- *
- * @return array
- */
- protected function preparePayload(): array
- {
- return [
- 'type' => $this->type,
- 'project' => $this->project,
- 'user' => $this->user,
- 'messageId' => $this->messageId,
- 'message' => $this->message,
- 'recipients' => $this->recipients,
- 'providerType' => $this->providerType,
- ];
- }
-}
diff --git a/src/Appwrite/Event/Publisher/Build.php b/src/Appwrite/Event/Publisher/Build.php
new file mode 100644
index 0000000000..9b2a3b68a0
--- /dev/null
+++ b/src/Appwrite/Event/Publisher/Build.php
@@ -0,0 +1,27 @@
+publish($queue ?? $this->queue, $message);
+ }
+
+ public function getSize(bool $failed = false, ?Queue $queue = null): int
+ {
+ return $this->getQueueSize($queue ?? $this->queue, $failed);
+ }
+}
diff --git a/src/Appwrite/Event/Publisher/Database.php b/src/Appwrite/Event/Publisher/Database.php
new file mode 100644
index 0000000000..09d5c33f03
--- /dev/null
+++ b/src/Appwrite/Event/Publisher/Database.php
@@ -0,0 +1,45 @@
+publish($queue ?? $this->getQueueFromProject($message->project), $message);
+ }
+
+ public function getSize(bool $failed = false, ?Queue $queue = null): int
+ {
+ return $this->getQueueSize($queue ?? $this->queue, $failed);
+ }
+
+ private function getQueueFromProject(?Document $project): Queue
+ {
+ $database = $project?->getAttribute('database', '');
+ if (empty($database)) {
+ return $this->queue;
+ }
+
+ try {
+ $dsn = new DSN($database);
+ } catch (\InvalidArgumentException) {
+ $dsn = new DSN('mysql://' . $database);
+ }
+
+ return new Queue($dsn->getHost());
+ }
+}
diff --git a/src/Appwrite/Event/Publisher/Delete.php b/src/Appwrite/Event/Publisher/Delete.php
new file mode 100644
index 0000000000..fb3b46c647
--- /dev/null
+++ b/src/Appwrite/Event/Publisher/Delete.php
@@ -0,0 +1,27 @@
+publish($queue ?? $this->queue, $message);
+ }
+
+ public function getSize(bool $failed = false, ?Queue $queue = null): int
+ {
+ return $this->getQueueSize($queue ?? $this->queue, $failed);
+ }
+}
diff --git a/src/Appwrite/Event/Publisher/Func.php b/src/Appwrite/Event/Publisher/Func.php
new file mode 100644
index 0000000000..46f748a59f
--- /dev/null
+++ b/src/Appwrite/Event/Publisher/Func.php
@@ -0,0 +1,27 @@
+publish($queue ?? $this->queue, $message);
+ }
+
+ public function getSize(bool $failed = false, ?Queue $queue = null): int
+ {
+ return $this->getQueueSize($queue ?? $this->queue, $failed);
+ }
+}
diff --git a/src/Appwrite/Event/Publisher/Mail.php b/src/Appwrite/Event/Publisher/Mail.php
new file mode 100644
index 0000000000..16d48be044
--- /dev/null
+++ b/src/Appwrite/Event/Publisher/Mail.php
@@ -0,0 +1,27 @@
+publish($queue ?? $this->queue, $message);
+ }
+
+ public function getSize(bool $failed = false, ?Queue $queue = null): int
+ {
+ return $this->getQueueSize($queue ?? $this->queue, $failed);
+ }
+}
diff --git a/src/Appwrite/Event/Publisher/Messaging.php b/src/Appwrite/Event/Publisher/Messaging.php
new file mode 100644
index 0000000000..69863566a1
--- /dev/null
+++ b/src/Appwrite/Event/Publisher/Messaging.php
@@ -0,0 +1,27 @@
+publish($queue ?? $this->queue, $message);
+ }
+
+ public function getSize(bool $failed = false, ?Queue $queue = null): int
+ {
+ return $this->getQueueSize($queue ?? $this->queue, $failed);
+ }
+}
diff --git a/src/Appwrite/Extend/Exception.php b/src/Appwrite/Extend/Exception.php
index 6fc3e88635..a0553d00b8 100644
--- a/src/Appwrite/Extend/Exception.php
+++ b/src/Appwrite/Extend/Exception.php
@@ -178,6 +178,7 @@ class Exception extends \Exception
public const string FUNCTION_RUNTIME_UNSUPPORTED = 'function_runtime_unsupported';
public const string FUNCTION_ENTRYPOINT_MISSING = 'function_entrypoint_missing';
public const string FUNCTION_SYNCHRONOUS_TIMEOUT = 'function_synchronous_timeout';
+ public const string FUNCTION_ASYNCHRONOUS_TIMEOUT = 'function_asynchronous_timeout';
public const string FUNCTION_TEMPLATE_NOT_FOUND = 'function_template_not_found';
public const string FUNCTION_RUNTIME_NOT_DETECTED = 'function_runtime_not_detected';
public const string FUNCTION_EXECUTE_PERMISSION_MISSING = 'function_execute_permission_missing';
@@ -192,6 +193,7 @@ class Exception extends \Exception
public const string BUILD_ALREADY_COMPLETED = 'build_already_completed';
public const string BUILD_CANCELED = 'build_canceled';
public const string BUILD_FAILED = 'build_failed';
+ public const string BUILD_TIMEOUT = 'build_timeout';
/** Execution */
public const string EXECUTION_NOT_FOUND = 'execution_not_found';
@@ -346,6 +348,10 @@ class Exception extends \Exception
public const string MIGRATION_IN_PROGRESS = 'migration_in_progress';
public const string MIGRATION_PROVIDER_ERROR = 'migration_provider_error';
public const string MIGRATION_DATABASE_TYPE_UNSUPPORTED = 'migration_database_type_unsupported';
+ public const string MIGRATION_SOURCE_PROJECT_ID_REQUIRED = 'migration_source_project_id_required';
+ public const string MIGRATION_SOURCE_PROJECT_NOT_FOUND = 'migration_source_project_not_found';
+ public const string MIGRATION_SOURCE_TYPE_INVALID = 'migration_source_type_invalid';
+ public const string MIGRATION_DESTINATION_TYPE_INVALID = 'migration_destination_type_invalid';
/** Realtime */
public const string REALTIME_MESSAGE_FORMAT_INVALID = 'realtime_message_format_invalid';
@@ -400,6 +406,14 @@ class Exception extends \Exception
public const string TOKEN_EXPIRED = 'token_expired';
public const string TOKEN_RESOURCE_TYPE_INVALID = 'token_resource_type_invalid';
+ /** Advisor */
+ public const string INSIGHT_NOT_FOUND = 'insight_not_found';
+ public const string INSIGHT_ALREADY_EXISTS = 'insight_already_exists';
+
+ /** Reports */
+ public const string REPORT_NOT_FOUND = 'report_not_found';
+ public const string REPORT_ALREADY_EXISTS = 'report_already_exists';
+
protected string $type = '';
protected array $errors = [];
protected bool $publish;
diff --git a/src/Appwrite/GraphQL/Resolvers.php b/src/Appwrite/GraphQL/Resolvers.php
index cabb357607..6e8c30b6ec 100644
--- a/src/Appwrite/GraphQL/Resolvers.php
+++ b/src/Appwrite/GraphQL/Resolvers.php
@@ -6,7 +6,6 @@ use Appwrite\GraphQL\Exception as GQLException;
use Appwrite\Promises\Swoole;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
-use Utopia\DI\Container;
use Utopia\Http\Exception;
use Utopia\Http\Http;
use Utopia\Http\Route;
@@ -53,22 +52,6 @@ class Resolvers
}
}
- /**
- * Get the current request container.
- */
- private static function getResolverContainer(Http $utopia): Container
- {
- $container = $utopia->getResource('container');
-
- if ($container instanceof Container || (\is_object($container) && \method_exists($container, 'get') && \method_exists($container, 'set'))) {
- /** @var Container $container */
- return $container;
- }
-
- /** @var callable(): Container $container */
- return $container();
- }
-
/**
* Get the request-scoped lock shared by GraphQL resolver coroutines
* for the current HTTP request.
@@ -95,9 +78,9 @@ class Resolvers
?Route $route,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(function (callable $resolve, callable $reject) use ($utopia, $route, $args) {
- $utopia = $utopia->getResource('utopia:graphql');
- $request = $utopia->getResource('request');
- $response = $utopia->getResource('response');
+ $utopia = $utopia->context()->get('utopia:graphql');
+ $request = $utopia->context()->get('request');
+ $response = $utopia->context()->get('response');
self::resolve(
$utopia,
@@ -167,9 +150,9 @@ class Resolvers
callable $url,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $args) {
- $utopia = $utopia->getResource('utopia:graphql');
- $request = $utopia->getResource('request');
- $response = $utopia->getResource('response');
+ $utopia = $utopia->context()->get('utopia:graphql');
+ $request = $utopia->context()->get('request');
+ $response = $utopia->context()->get('response');
self::resolve(
$utopia,
@@ -203,9 +186,9 @@ class Resolvers
callable $params,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
- $utopia = $utopia->getResource('utopia:graphql');
- $request = $utopia->getResource('request');
- $response = $utopia->getResource('response');
+ $utopia = $utopia->context()->get('utopia:graphql');
+ $request = $utopia->context()->get('request');
+ $response = $utopia->context()->get('response');
$beforeResolve = function ($payload) {
return $payload['documents'];
@@ -245,9 +228,9 @@ class Resolvers
callable $params,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
- $utopia = $utopia->getResource('utopia:graphql');
- $request = $utopia->getResource('request');
- $response = $utopia->getResource('response');
+ $utopia = $utopia->context()->get('utopia:graphql');
+ $request = $utopia->context()->get('request');
+ $response = $utopia->context()->get('response');
self::resolve(
$utopia,
@@ -282,9 +265,9 @@ class Resolvers
callable $params,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
- $utopia = $utopia->getResource('utopia:graphql');
- $request = $utopia->getResource('request');
- $response = $utopia->getResource('response');
+ $utopia = $utopia->context()->get('utopia:graphql');
+ $request = $utopia->context()->get('request');
+ $response = $utopia->context()->get('response');
self::resolve(
$utopia,
@@ -317,9 +300,9 @@ class Resolvers
callable $url,
): callable {
return static fn ($type, $args, $context, $info) => new Swoole(function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $args) {
- $utopia = $utopia->getResource('utopia:graphql');
- $request = $utopia->getResource('request');
- $response = $utopia->getResource('response');
+ $utopia = $utopia->context()->get('utopia:graphql');
+ $request = $utopia->context()->get('request');
+ $response = $utopia->context()->get('response');
self::resolve(
$utopia,
@@ -373,10 +356,9 @@ class Resolvers
}
/** @var Response $resolverResponse */
- $resolverResponse = clone $utopia->getResource('response');
- $container = self::getResolverContainer($utopia);
- $container->set('request', static fn () => $request);
- $container->set('response', static fn () => $resolverResponse);
+ $resolverResponse = clone $utopia->context()->get('response');
+ $utopia->context()->set('request', static fn () => $request);
+ $utopia->context()->set('response', static fn () => $resolverResponse);
$resolverResponse->setContentType(Response::CONTENT_TYPE_NULL);
$resolverResponse->setSent(false);
diff --git a/src/Appwrite/GraphQL/Schema.php b/src/Appwrite/GraphQL/Schema.php
index 4ff96fb635..a689655e31 100644
--- a/src/Appwrite/GraphQL/Schema.php
+++ b/src/Appwrite/GraphQL/Schema.php
@@ -84,7 +84,7 @@ class Schema
protected static function api(Http $utopia, callable $complexity): array
{
Mapper::init($utopia
- ->getResource('response')
+ ->context()->get('response')
->getModels());
$queries = [];
diff --git a/src/Appwrite/GraphQL/Types/Mapper.php b/src/Appwrite/GraphQL/Types/Mapper.php
index 55810fd74e..92c7753ad2 100644
--- a/src/Appwrite/GraphQL/Types/Mapper.php
+++ b/src/Appwrite/GraphQL/Types/Mapper.php
@@ -254,7 +254,7 @@ class Mapper
array $injections
): Type {
$validator = \is_callable($validator)
- ? \call_user_func_array($validator, $utopia->getResources($injections))
+ ? \call_user_func_array($validator, \array_map($utopia->context()->get(...), $injections))
: $validator;
$isNullable = $validator instanceof Nullable;
diff --git a/src/Appwrite/Messaging/Adapter/Realtime.php b/src/Appwrite/Messaging/Adapter/Realtime.php
index 5a9c02a2bd..c4cd2c08d5 100644
--- a/src/Appwrite/Messaging/Adapter/Realtime.php
+++ b/src/Appwrite/Messaging/Adapter/Realtime.php
@@ -774,6 +774,21 @@ class Realtime extends MessagingAdapter
$roles = [Role::team($project->getAttribute('teamId'))->toString()];
}
break;
+ case 'reports':
+ // Plain report event: `reports.{reportId}.{action}`
+ $channels[] = 'reports';
+ if (isset($parts[1])) {
+ $channels[] = 'reports.' . $parts[1];
+ }
+ // Nested insight event: `reports.{reportId}.insights.{insightId}.{action}`
+ if (isset($parts[2]) && $parts[2] === 'insights') {
+ $channels[] = 'reports.' . $parts[1] . '.insights';
+ if (isset($parts[3])) {
+ $channels[] = 'reports.' . $parts[1] . '.insights.' . $parts[3];
+ }
+ }
+ $roles = [Role::team($project->getAttribute('teamId'))->toString()];
+ break;
}
// Action is the last segment for plain CRUD events (e.g. `documents.X.create`),
diff --git a/src/Appwrite/Migration/Migration.php b/src/Appwrite/Migration/Migration.php
index 359925e368..77c62bce96 100644
--- a/src/Appwrite/Migration/Migration.php
+++ b/src/Appwrite/Migration/Migration.php
@@ -96,6 +96,8 @@ abstract class Migration
'1.9.1' => 'V24',
'1.9.2' => 'V24',
'1.9.3' => 'V24',
+ '1.9.4' => 'V24',
+ '1.9.5' => 'V24',
];
/**
diff --git a/src/Appwrite/Platform/Appwrite.php b/src/Appwrite/Platform/Appwrite.php
index 88788b73fc..a9cd1a8e2f 100644
--- a/src/Appwrite/Platform/Appwrite.php
+++ b/src/Appwrite/Platform/Appwrite.php
@@ -3,6 +3,7 @@
namespace Appwrite\Platform;
use Appwrite\Platform\Modules\Account;
+use Appwrite\Platform\Modules\Advisor;
use Appwrite\Platform\Modules\Avatars;
use Appwrite\Platform\Modules\Console;
use Appwrite\Platform\Modules\Core;
@@ -42,5 +43,6 @@ class Appwrite extends Platform
$this->addModule(new Webhooks\Module());
$this->addModule(new Migrations\Module());
$this->addModule(new Project\Module());
+ $this->addModule(new Advisor\Module());
}
}
diff --git a/src/Appwrite/Platform/Installer/Server.php b/src/Appwrite/Platform/Installer/Server.php
index 38d61b7d24..26d82adf24 100644
--- a/src/Appwrite/Platform/Installer/Server.php
+++ b/src/Appwrite/Platform/Installer/Server.php
@@ -154,7 +154,7 @@ class Server
$nativeServer = $adapter->getNativeServer();
- $container = $adapter->getContainer();
+ $container = $adapter->resources();
$container->set('installerState', fn () => $state);
$container->set('installerConfig', fn () => $config);
$container->set('installerPaths', fn () => $paths);
diff --git a/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Challenges/Create.php b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Challenges/Create.php
index 7bcc78e974..285875eb35 100644
--- a/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Challenges/Create.php
+++ b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Challenges/Create.php
@@ -5,8 +5,10 @@ namespace Appwrite\Platform\Modules\Account\Http\Account\MFA\Challenges;
use Appwrite\Auth\MFA\Type;
use Appwrite\Detector\Detector;
use Appwrite\Event\Event;
-use Appwrite\Event\Mail;
-use Appwrite\Event\Messaging;
+use Appwrite\Event\Message\Mail as MailMessage;
+use Appwrite\Event\Message\Messaging as MessagingMessage;
+use Appwrite\Event\Publisher\Mail as MailPublisher;
+use Appwrite\Event\Publisher\Messaging as MessagingPublisher;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@@ -101,8 +103,8 @@ class Create extends Action
->inject('platform')
->inject('request')
->inject('queueForEvents')
- ->inject('queueForMessaging')
- ->inject('queueForMails')
+ ->inject('publisherForMessaging')
+ ->inject('publisherForMails')
->inject('timelimit')
->inject('usage')
->inject('plan')
@@ -121,8 +123,8 @@ class Create extends Action
array $platform,
Request $request,
Event $queueForEvents,
- Messaging $queueForMessaging,
- Mail $queueForMails,
+ MessagingPublisher $publisherForMessaging,
+ MailPublisher $publisherForMails,
callable $timelimit,
Context $usage,
array $plan,
@@ -180,16 +182,18 @@ class Create extends Action
$message = $message->render();
$phone = $user->getAttribute('phone');
- $queueForMessaging
- ->setType(MESSAGE_SEND_TYPE_INTERNAL)
- ->setMessage(new Document([
+ $publisherForMessaging->enqueue(new MessagingMessage(
+ type: MESSAGE_SEND_TYPE_INTERNAL,
+ project: $project,
+ message: new Document([
'$id' => $challenge->getId(),
'data' => [
'content' => $code,
],
- ]))
- ->setRecipients([$phone])
- ->setProviderType(MESSAGE_TYPE_SMS);
+ ]),
+ recipients: [$phone],
+ providerType: MESSAGE_TYPE_SMS,
+ ));
$helper = PhoneNumberUtil::getInstance();
try {
@@ -252,6 +256,7 @@ class Create extends Action
$senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server');
$replyToEmail = '';
$replyToName = '';
+ $smtpConfig = [];
if ($smtpEnabled) {
if (!empty($smtp['senderEmail'])) {
@@ -269,13 +274,6 @@ class Create extends Action
$replyToName = $smtp['replyToName'];
}
- $queueForMails
- ->setSmtpHost($smtp['host'] ?? '')
- ->setSmtpPort($smtp['port'] ?? '')
- ->setSmtpUsername($smtp['username'] ?? '')
- ->setSmtpPassword($smtp['password'] ?? '')
- ->setSmtpSecure($smtp['secure'] ?? '');
-
if (!empty($customTemplate)) {
if (!empty($customTemplate['senderEmail'])) {
$senderEmail = $customTemplate['senderEmail'];
@@ -296,11 +294,17 @@ class Create extends Action
$subject = $customTemplate['subject'] ?? $subject;
}
- $queueForMails
- ->setSmtpReplyToEmail($replyToEmail)
- ->setSmtpReplyToName($replyToName)
- ->setSmtpSenderEmail($senderEmail)
- ->setSmtpSenderName($senderName);
+ $smtpConfig = [
+ 'host' => $smtp['host'] ?? '',
+ 'port' => $smtp['port'] ?? '',
+ 'username' => $smtp['username'] ?? '',
+ 'password' => $smtp['password'] ?? '',
+ 'secure' => $smtp['secure'] ?? '',
+ 'replyToEmail' => $replyToEmail,
+ 'replyToName' => $replyToName,
+ 'senderEmail' => $senderEmail,
+ 'senderName' => $senderName,
+ ];
}
$emailVariables = [
@@ -327,20 +331,18 @@ class Create extends Action
]);
}
- $queueForMails
- ->setSubject($subject)
- ->setPreview($preview)
- ->setBody($body)
- ->setBodyTemplate($bodyTemplate)
- ->appendVariables($emailVariables)
- ->setRecipient($user->getAttribute('email'));
-
- // since this is console project, set email sender name!
- if ($smtpBaseTemplate === APP_BRANDED_EMAIL_BASE_TEMPLATE) {
- $queueForMails->setSenderName($platform['emailSenderName']);
- }
-
- $queueForMails->trigger();
+ $publisherForMails->enqueue(new MailMessage(
+ project: $project,
+ recipient: $user->getAttribute('email'),
+ subject: $subject,
+ bodyTemplate: $bodyTemplate,
+ body: $body,
+ preview: $preview,
+ smtp: $smtpConfig,
+ variables: $emailVariables,
+ customMailOptions: $smtpBaseTemplate === APP_BRANDED_EMAIL_BASE_TEMPLATE ? ['senderName' => $platform['emailSenderName']] : [],
+ platform: $platform,
+ ));
break;
}
diff --git a/src/Appwrite/Platform/Modules/Advisor/Enums/InsightCTAMethod.php b/src/Appwrite/Platform/Modules/Advisor/Enums/InsightCTAMethod.php
new file mode 100644
index 0000000000..31d578a991
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Advisor/Enums/InsightCTAMethod.php
@@ -0,0 +1,8 @@
+setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
+ ->setHttpPath('/v1/reports/:reportId/insights/:insightId')
+ ->desc('Get insight')
+ ->groups(['api', 'advisor'])
+ ->label('scope', 'insights.read')
+ ->label('resourceType', RESOURCE_TYPE_INSIGHTS)
+ ->label('sdk', new Method(
+ namespace: 'advisor',
+ group: 'insights',
+ name: 'getInsight',
+ description: '/docs/references/advisor/get-insight.md',
+ auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
+ responses: [
+ new SDKResponse(
+ code: Response::STATUS_CODE_OK,
+ model: Response::MODEL_INSIGHT,
+ ),
+ ]
+ ))
+ ->param('reportId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Parent report ID.', false, ['dbForPlatform'])
+ ->param('insightId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Insight ID.', false, ['dbForPlatform'])
+ ->inject('response')
+ ->inject('project')
+ ->inject('dbForPlatform')
+ ->callback($this->action(...));
+ }
+
+ public function action(
+ string $reportId,
+ string $insightId,
+ Response $response,
+ Document $project,
+ Database $dbForPlatform
+ ) {
+ // Skip the insights subquery â we only need ownership metadata.
+ $report = $dbForPlatform->skipFilters(
+ fn () => $dbForPlatform->getDocument('reports', $reportId),
+ ['subQueryReportInsights'],
+ );
+
+ if ($report->isEmpty() || $report->getAttribute('projectInternalId') !== $project->getSequence()) {
+ throw new Exception(Exception::REPORT_NOT_FOUND);
+ }
+
+ $insight = $dbForPlatform->getDocument('insights', $insightId);
+
+ if (
+ $insight->isEmpty()
+ || $insight->getAttribute('projectInternalId') !== $project->getSequence()
+ || $insight->getAttribute('reportInternalId') !== $report->getSequence()
+ ) {
+ throw new Exception(Exception::INSIGHT_NOT_FOUND);
+ }
+
+ $response->dynamic($insight, Response::MODEL_INSIGHT);
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Advisor/Http/Insights/XList.php b/src/Appwrite/Platform/Modules/Advisor/Http/Insights/XList.php
new file mode 100644
index 0000000000..64d3676c08
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Advisor/Http/Insights/XList.php
@@ -0,0 +1,126 @@
+setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
+ ->setHttpPath('/v1/reports/:reportId/insights')
+ ->desc('List insights')
+ ->groups(['api', 'advisor'])
+ ->label('scope', 'insights.read')
+ ->label('resourceType', RESOURCE_TYPE_INSIGHTS)
+ ->label('sdk', new Method(
+ namespace: 'advisor',
+ group: 'insights',
+ name: 'listInsights',
+ description: '/docs/references/advisor/list-insights.md',
+ auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
+ responses: [
+ new SDKResponse(
+ code: Response::STATUS_CODE_OK,
+ model: Response::MODEL_INSIGHT_LIST,
+ ),
+ ]
+ ))
+ ->param('reportId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Parent report ID.', false, ['dbForPlatform'])
+ ->param('queries', [], new Insights(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Insights::ALLOWED_ATTRIBUTES), true)
+ ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
+ ->inject('response')
+ ->inject('project')
+ ->inject('dbForPlatform')
+ ->callback($this->action(...));
+ }
+
+ public function action(
+ string $reportId,
+ array $queries,
+ bool $includeTotal,
+ Response $response,
+ Document $project,
+ Database $dbForPlatform
+ ) {
+ // Skip the insights subquery â we're about to fetch a filtered, paginated slice ourselves.
+ $report = $dbForPlatform->skipFilters(
+ fn () => $dbForPlatform->getDocument('reports', $reportId),
+ ['subQueryReportInsights'],
+ );
+
+ if ($report->isEmpty() || $report->getAttribute('projectInternalId') !== $project->getSequence()) {
+ throw new Exception(Exception::REPORT_NOT_FOUND);
+ }
+
+ try {
+ $queries = Query::parseQueries($queries);
+ } catch (QueryException $e) {
+ throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
+ }
+
+ $queries[] = Query::equal('projectInternalId', [$project->getSequence()]);
+ $queries[] = Query::equal('reportInternalId', [$report->getSequence()]);
+
+ $cursor = Query::getCursorQueries($queries, false);
+ $cursor = \reset($cursor);
+
+ if ($cursor !== false) {
+ $validator = new Cursor();
+ if (!$validator->isValid($cursor)) {
+ throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
+ }
+
+ $insightId = $cursor->getValue();
+ $cursorDocument = $dbForPlatform->getDocument('insights', $insightId);
+
+ if (
+ $cursorDocument->isEmpty()
+ || $cursorDocument->getAttribute('projectInternalId') !== $project->getSequence()
+ || $cursorDocument->getAttribute('reportInternalId') !== $report->getSequence()
+ ) {
+ throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Insight '{$insightId}' for the 'cursor' value not found.");
+ }
+
+ $cursor->setValue($cursorDocument);
+ }
+
+ $filterQueries = Query::groupByType($queries)['filters'];
+
+ try {
+ $insights = $dbForPlatform->find('insights', $queries);
+ $total = $includeTotal ? $dbForPlatform->count('insights', $filterQueries, APP_LIMIT_COUNT) : 0;
+ } catch (OrderException $e) {
+ throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
+ }
+
+ $response->dynamic(new Document([
+ 'insights' => $insights,
+ 'total' => $total,
+ ]), Response::MODEL_INSIGHT_LIST);
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Advisor/Http/Reports/Delete.php b/src/Appwrite/Platform/Modules/Advisor/Http/Reports/Delete.php
new file mode 100644
index 0000000000..1efc029c17
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Advisor/Http/Reports/Delete.php
@@ -0,0 +1,100 @@
+setHttpMethod(Action::HTTP_REQUEST_METHOD_DELETE)
+ ->setHttpPath('/v1/reports/:reportId')
+ ->desc('Delete report')
+ ->groups(['api', 'advisor'])
+ ->label('scope', 'reports.write')
+ ->label('event', 'reports.[reportId].delete')
+ ->label('resourceType', RESOURCE_TYPE_REPORTS)
+ ->label('audits.event', 'report.delete')
+ ->label('audits.resource', 'report/{request.reportId}')
+ ->label('abuse-key', 'projectId:{projectId},userId:{userId}')
+ ->label('abuse-limit', APP_LIMIT_WRITE_RATE_DEFAULT)
+ ->label('abuse-time', APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT)
+ ->label('sdk', new Method(
+ namespace: 'advisor',
+ group: 'reports',
+ name: 'deleteReport',
+ description: '/docs/references/advisor/delete-report.md',
+ auth: [AuthType::ADMIN, AuthType::KEY],
+ responses: [
+ new SDKResponse(
+ code: Response::STATUS_CODE_NOCONTENT,
+ model: Response::MODEL_NONE,
+ ),
+ ],
+ contentType: ContentType::NONE
+ ))
+ ->param('reportId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Report ID.', false, ['dbForPlatform'])
+ ->inject('response')
+ ->inject('project')
+ ->inject('dbForPlatform')
+ ->inject('publisherForDeletes')
+ ->inject('queueForEvents')
+ ->callback($this->action(...));
+ }
+
+ public function action(
+ string $reportId,
+ Response $response,
+ Document $project,
+ Database $dbForPlatform,
+ DeletePublisher $publisherForDeletes,
+ Event $queueForEvents
+ ): void {
+ $report = $dbForPlatform->skipFilters(
+ fn () => $dbForPlatform->getDocument('reports', $reportId),
+ ['subQueryReportInsights'],
+ );
+
+ if ($report->isEmpty() || $report->getAttribute('projectInternalId') !== $project->getSequence()) {
+ throw new Exception(Exception::REPORT_NOT_FOUND);
+ }
+
+ if (!$dbForPlatform->deleteDocument('reports', $report->getId())) {
+ throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove report from DB');
+ }
+
+ $publisherForDeletes->enqueue(new DeleteMessage(
+ project: $project,
+ type: DELETE_TYPE_REPORT,
+ document: $report,
+ ));
+
+ $queueForEvents
+ ->setParam('reportId', $report->getId())
+ ->setPayload($response->output($report, Response::MODEL_REPORT));
+
+ $response->noContent();
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Advisor/Http/Reports/Get.php b/src/Appwrite/Platform/Modules/Advisor/Http/Reports/Get.php
new file mode 100644
index 0000000000..78885a7c5d
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Advisor/Http/Reports/Get.php
@@ -0,0 +1,80 @@
+setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
+ ->setHttpPath('/v1/reports/:reportId')
+ ->desc('Get report')
+ ->groups(['api', 'advisor'])
+ ->label('scope', 'reports.read')
+ ->label('resourceType', RESOURCE_TYPE_REPORTS)
+ ->label('sdk', new Method(
+ namespace: 'advisor',
+ group: 'reports',
+ name: 'getReport',
+ description: '/docs/references/advisor/get-report.md',
+ auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
+ responses: [
+ new SDKResponse(
+ code: Response::STATUS_CODE_OK,
+ model: Response::MODEL_REPORT,
+ ),
+ ]
+ ))
+ ->param('reportId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Report ID.', false, ['dbForPlatform'])
+ ->inject('response')
+ ->inject('project')
+ ->inject('dbForPlatform')
+ ->callback($this->action(...));
+ }
+
+ public function action(
+ string $reportId,
+ Response $response,
+ Document $project,
+ Database $dbForPlatform
+ ) {
+ $report = $dbForPlatform->skipFilters(
+ fn () => $dbForPlatform->getDocument('reports', $reportId),
+ ['subQueryReportInsights'],
+ );
+
+ if ($report->isEmpty() || $report->getAttribute('projectInternalId') !== $project->getSequence()) {
+ throw new Exception(Exception::REPORT_NOT_FOUND);
+ }
+
+ $insights = $dbForPlatform->find('insights', [
+ Query::equal('projectInternalId', [$project->getSequence()]),
+ Query::equal('reportInternalId', [$report->getSequence()]),
+ Query::limit(APP_LIMIT_SUBQUERY),
+ ]);
+
+ $report->setAttribute('insights', $insights);
+
+ $response->dynamic($report, Response::MODEL_REPORT);
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Advisor/Http/Reports/XList.php b/src/Appwrite/Platform/Modules/Advisor/Http/Reports/XList.php
new file mode 100644
index 0000000000..c5debb7f68
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Advisor/Http/Reports/XList.php
@@ -0,0 +1,133 @@
+setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
+ ->setHttpPath('/v1/reports')
+ ->desc('List reports')
+ ->groups(['api', 'advisor'])
+ ->label('scope', 'reports.read')
+ ->label('resourceType', RESOURCE_TYPE_REPORTS)
+ ->label('sdk', new Method(
+ namespace: 'advisor',
+ group: 'reports',
+ name: 'listReports',
+ description: '/docs/references/advisor/list-reports.md',
+ auth: [AuthType::ADMIN, AuthType::SESSION, AuthType::KEY, AuthType::JWT],
+ responses: [
+ new SDKResponse(
+ code: Response::STATUS_CODE_OK,
+ model: Response::MODEL_REPORT_LIST,
+ ),
+ ]
+ ))
+ ->param('queries', [], new Reports(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Reports::ALLOWED_ATTRIBUTES), true)
+ ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
+ ->inject('response')
+ ->inject('project')
+ ->inject('dbForPlatform')
+ ->callback($this->action(...));
+ }
+
+ public function action(
+ array $queries,
+ bool $includeTotal,
+ Response $response,
+ Document $project,
+ Database $dbForPlatform
+ ) {
+ try {
+ $queries = Query::parseQueries($queries);
+ } catch (QueryException $e) {
+ throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
+ }
+
+ $queries[] = Query::equal('projectInternalId', [$project->getSequence()]);
+
+ $cursor = Query::getCursorQueries($queries, false);
+ $cursor = \reset($cursor);
+
+ if ($cursor !== false) {
+ $validator = new Cursor();
+ if (!$validator->isValid($cursor)) {
+ throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
+ }
+
+ $reportId = $cursor->getValue();
+ $cursorDocument = $dbForPlatform->skipFilters(
+ fn () => $dbForPlatform->getDocument('reports', $reportId),
+ ['subQueryReportInsights'],
+ );
+
+ if ($cursorDocument->isEmpty() || $cursorDocument->getAttribute('projectInternalId') !== $project->getSequence()) {
+ throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Report '{$reportId}' for the 'cursor' value not found.");
+ }
+
+ $cursor->setValue($cursorDocument);
+ }
+
+ $filterQueries = Query::groupByType($queries)['filters'];
+
+ try {
+ $reports = $dbForPlatform->skipFilters(
+ fn () => $dbForPlatform->find('reports', $queries),
+ ['subQueryReportInsights'],
+ );
+ $total = $includeTotal ? $dbForPlatform->count('reports', $filterQueries, APP_LIMIT_COUNT) : 0;
+ } catch (OrderException $e) {
+ throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
+ }
+
+ if (!empty($reports)) {
+ $reportSequences = \array_map(fn (Document $r) => $r->getSequence(), $reports);
+
+ $insights = $dbForPlatform->find('insights', [
+ Query::equal('projectInternalId', [$project->getSequence()]),
+ Query::equal('reportInternalId', $reportSequences),
+ Query::limit(APP_LIMIT_SUBQUERY),
+ ]);
+
+ $insightsByReport = [];
+ foreach ($insights as $insight) {
+ $insightsByReport[$insight->getAttribute('reportInternalId')][] = $insight;
+ }
+
+ foreach ($reports as $report) {
+ $report->setAttribute('insights', $insightsByReport[$report->getSequence()] ?? []);
+ }
+ }
+
+ $response->dynamic(new Document([
+ 'reports' => $reports,
+ 'total' => $total,
+ ]), Response::MODEL_REPORT_LIST);
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Advisor/Module.php b/src/Appwrite/Platform/Modules/Advisor/Module.php
new file mode 100644
index 0000000000..b28a2421c2
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Advisor/Module.php
@@ -0,0 +1,14 @@
+addService('http', new Http());
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Advisor/Services/Http.php b/src/Appwrite/Platform/Modules/Advisor/Services/Http.php
new file mode 100644
index 0000000000..2558b00247
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Advisor/Services/Http.php
@@ -0,0 +1,25 @@
+type = Service::TYPE_HTTP;
+
+ $this->addAction(GetReport::getName(), new GetReport());
+ $this->addAction(ListReports::getName(), new ListReports());
+ $this->addAction(DeleteReport::getName(), new DeleteReport());
+
+ $this->addAction(GetInsight::getName(), new GetInsight());
+ $this->addAction(ListInsights::getName(), new ListInsights());
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Compute/Base.php b/src/Appwrite/Platform/Modules/Compute/Base.php
index 85dfec3cfd..b0efac3829 100644
--- a/src/Appwrite/Platform/Modules/Compute/Base.php
+++ b/src/Appwrite/Platform/Modules/Compute/Base.php
@@ -2,7 +2,8 @@
namespace Appwrite\Platform\Modules\Compute;
-use Appwrite\Event\Build;
+use Appwrite\Event\Message\Build as BuildMessage;
+use Appwrite\Event\Publisher\Build as BuildPublisher;
use Appwrite\Extend\Exception;
use Appwrite\Filter\BranchDomain as BranchDomainFilter;
use Appwrite\Platform\Action;
@@ -57,7 +58,7 @@ class Base extends Action
return $allowedSpecifications[0] ?? APP_COMPUTE_SPECIFICATION_DEFAULT;
}
- public function redeployVcsFunction(Request $request, Document $function, Document $project, Document $installation, Database $dbForProject, Build $queueForBuilds, Document $template, GitHub $github, bool $activate, string $referenceType = 'branch', string $reference = ''): Document
+ public function redeployVcsFunction(Request $request, Document $function, Document $project, Document $installation, Database $dbForProject, BuildPublisher $publisherForBuilds, Document $template, GitHub $github, bool $activate, array $platform = [], string $referenceType = 'branch', string $reference = ''): Document
{
$deploymentId = ID::unique();
$entrypoint = $function->getAttribute('entrypoint', '');
@@ -150,16 +151,19 @@ class Base extends Action
'latestDeploymentStatus' => $deployment->getAttribute('status', ''),
]));
- $queueForBuilds
- ->setType(BUILD_TYPE_DEPLOYMENT)
- ->setResource($function)
- ->setDeployment($deployment)
- ->setTemplate($template);
+ $publisherForBuilds->enqueue(new BuildMessage(
+ project: $project,
+ resource: $function,
+ deployment: $deployment,
+ type: BUILD_TYPE_DEPLOYMENT,
+ template: $template,
+ platform: $platform,
+ ));
return $deployment;
}
- public function redeployVcsSite(Request $request, Document $site, Document $project, Document $installation, Database $dbForProject, Database $dbForPlatform, Build $queueForBuilds, Document $template, GitHub $github, bool $activate, Authorization $authorization, array $platform, string $referenceType = 'branch', string $reference = ''): Document
+ public function redeployVcsSite(Request $request, Document $site, Document $project, Document $installation, Database $dbForProject, Database $dbForPlatform, BuildPublisher $publisherForBuilds, Document $template, GitHub $github, bool $activate, Authorization $authorization, array $platform, string $referenceType = 'branch', string $reference = ''): Document
{
$deploymentId = ID::unique();
$providerInstallationId = $installation->getAttribute('providerInstallationId', '');
@@ -358,11 +362,14 @@ class Base extends Action
$this->updateEmptyManualRule($project, $site, $deployment, $dbForPlatform, $authorization);
- $queueForBuilds
- ->setType(BUILD_TYPE_DEPLOYMENT)
- ->setResource($site)
- ->setDeployment($deployment)
- ->setTemplate($template);
+ $publisherForBuilds->enqueue(new BuildMessage(
+ project: $project,
+ resource: $site,
+ deployment: $deployment,
+ type: BUILD_TYPE_DEPLOYMENT,
+ template: $template,
+ platform: $platform,
+ ));
return $deployment;
}
diff --git a/src/Appwrite/Platform/Modules/Console/Http/Scopes/Organization/XList.php b/src/Appwrite/Platform/Modules/Console/Http/Scopes/Organization/XList.php
new file mode 100644
index 0000000000..4f88df6948
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Console/Http/Scopes/Organization/XList.php
@@ -0,0 +1,69 @@
+setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
+ ->setHttpPath('/v1/console/scopes/organization')
+ ->desc('List organization scopes')
+ ->groups(['api'])
+ ->label('scope', 'public')
+ ->label('sdk', new Method(
+ namespace: 'console',
+ group: 'console',
+ name: 'listOrganizationScopes',
+ description: 'List all scopes available for organization API keys, along with a description for each scope.',
+ auth: [AuthType::ADMIN],
+ responses: [
+ new SDKResponse(
+ code: Response::STATUS_CODE_OK,
+ model: Response::MODEL_CONSOLE_KEY_SCOPE_LIST,
+ )
+ ],
+ contentType: ContentType::JSON
+ ))
+ ->inject('response')
+ ->callback($this->action(...));
+ }
+
+ public function action(Response $response): void
+ {
+ $scopesConfig = Config::getParam('organizationScopes', []);
+
+ $scopes = [];
+ foreach ($scopesConfig as $scopeId => $scope) {
+ $scopes[] = new Document([
+ '$id' => $scopeId,
+ 'description' => $scope['description'] ?? '',
+ 'category' => $scope['category'] ?? '',
+ 'deprecated' => $scope['deprecated'] ?? false,
+ ]);
+ }
+
+ $response->dynamic(new Document([
+ 'total' => \count($scopes),
+ 'scopes' => $scopes,
+ ]), Response::MODEL_CONSOLE_KEY_SCOPE_LIST);
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Console/Http/Scopes/Key/XList.php b/src/Appwrite/Platform/Modules/Console/Http/Scopes/Project/XList.php
similarity index 96%
rename from src/Appwrite/Platform/Modules/Console/Http/Scopes/Key/XList.php
rename to src/Appwrite/Platform/Modules/Console/Http/Scopes/Project/XList.php
index d951e93886..3e6eceb26c 100644
--- a/src/Appwrite/Platform/Modules/Console/Http/Scopes/Key/XList.php
+++ b/src/Appwrite/Platform/Modules/Console/Http/Scopes/Project/XList.php
@@ -1,6 +1,6 @@
addAction(GetVariables::getName(), new GetVariables());
$this->addAction(ListOAuth2Providers::getName(), new ListOAuth2Providers());
$this->addAction(ListKeyScopes::getName(), new ListKeyScopes());
+ $this->addAction(ListOrganizationScopes::getName(), new ListOrganizationScopes());
$this->addAction(CreateAssistantQuery::getName(), new CreateAssistantQuery());
$this->addAction(GetResourceAvailability::getName(), new GetResourceAvailability());
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php
index 1606c7ab40..a07a4be561 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Action.php
@@ -2,8 +2,9 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes;
-use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
+use Appwrite\Event\Message\Database as DatabaseMessage;
+use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response as UtopiaResponse;
@@ -241,6 +242,10 @@ abstract class Action extends UtopiaAction
? UtopiaResponse::MODEL_ATTRIBUTE_INTEGER
: UtopiaResponse::MODEL_COLUMN_INTEGER,
+ Database::VAR_BIGINT => $isCollections
+ ? UtopiaResponse::MODEL_ATTRIBUTE_BIGINT
+ : UtopiaResponse::MODEL_COLUMN_BIGINT,
+
Database::VAR_FLOAT => $isCollections
? UtopiaResponse::MODEL_ATTRIBUTE_FLOAT
: UtopiaResponse::MODEL_COLUMN_FLOAT,
@@ -308,7 +313,7 @@ abstract class Action extends UtopiaAction
};
}
- protected function createAttribute(string $databaseId, string $collectionId, Document $attribute, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): Document
+ protected function createAttribute(string $databaseId, string $collectionId, Document $attribute, Response $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): Document
{
$key = $attribute->getAttribute('key');
$type = $attribute->getAttribute('type', '');
@@ -460,20 +465,6 @@ abstract class Action extends UtopiaAction
$dbForProject->purgeCachedCollection('database_' . $db->getSequence() . '_collection_' . $relatedCollection->getSequence());
}
- $queueForDatabase
- ->setType(DATABASE_TYPE_CREATE_ATTRIBUTE)
- ->setDatabase($db);
-
- if ($this->isCollectionsAPI()) {
- $queueForDatabase
- ->setDocument($attribute)
- ->setCollection($collection);
- } else {
- $queueForDatabase
- ->setRow($attribute)
- ->setTable($collection);
- }
-
$queueForEvents
->setContext('database', $db)
->setParam('databaseId', $databaseId)
@@ -483,6 +474,18 @@ abstract class Action extends UtopiaAction
->setParam('columnId', $attribute->getId())
->setContext($this->getCollectionsEventsContext(), $collection);
+ $publisherForDatabase->enqueue(new DatabaseMessage(
+ project: $queueForEvents->getProject(),
+ user: $queueForEvents->getUser(),
+ type: DATABASE_TYPE_CREATE_ATTRIBUTE,
+ database: $db,
+ collection: $this->isCollectionsAPI() ? $collection : null,
+ document: $this->isCollectionsAPI() ? $attribute : null,
+ table: $this->isCollectionsAPI() ? null : $collection,
+ row: $this->isCollectionsAPI() ? null : $attribute,
+ events: Event::generateEvents($queueForEvents->getEvent(), $queueForEvents->getParams()),
+ ));
+
$response->setStatusCode(SwooleResponse::STATUS_CODE_CREATED);
return $attribute;
@@ -540,6 +543,7 @@ abstract class Action extends UtopiaAction
switch ($attribute->getAttribute('format')) {
case APP_DATABASE_ATTRIBUTE_INT_RANGE:
+ case APP_DATABASE_ATTRIBUTE_BIGINT_RANGE:
case APP_DATABASE_ATTRIBUTE_FLOAT_RANGE:
$min ??= $attribute->getAttribute('formatOptions')['min'];
$max ??= $attribute->getAttribute('formatOptions')['max'];
@@ -548,14 +552,15 @@ abstract class Action extends UtopiaAction
throw new Exception($this->getInvalidValueException(), 'Minimum value must be lesser than maximum value');
}
- if ($attribute->getAttribute('format') === APP_DATABASE_ATTRIBUTE_INT_RANGE) {
- $validator = new Range($min, $max, Database::VAR_INTEGER);
- } else {
+ if ($attribute->getAttribute('format') === APP_DATABASE_ATTRIBUTE_FLOAT_RANGE) {
$validator = new Range($min, $max, Database::VAR_FLOAT);
if (!is_null($default)) {
$default = \floatval($default);
}
+ } else {
+ // intRange and bigintRange share the same integer range semantics
+ $validator = new Range($min, $max, Range::TYPE_INTEGER);
}
if (!is_null($default) && !$validator->isValid($default)) {
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Create.php
new file mode 100644
index 0000000000..11d3ada810
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Create.php
@@ -0,0 +1,117 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
+ ->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/bigint')
+ ->desc('Create bigint attribute')
+ ->groups(['api', 'database', 'schema'])
+ ->label('scope', 'collections.write')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
+ ->label('audits.event', 'attribute.create')
+ ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
+ ->label('sdk', new Method(
+ namespace: $this->getSDKNamespace(),
+ group: $this->getSDKGroup(),
+ name: self::getName(),
+ description: '/docs/references/databases/create-bigint-attribute.md',
+ auth: [AuthType::ADMIN, AuthType::KEY],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_ACCEPTED,
+ model: $this->getResponseModel(),
+ )
+ ],
+ deprecated: new Deprecated(
+ since: '1.8.0',
+ replaceWith: 'tablesDB.createBigIntColumn',
+ ),
+ ))
+ ->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject'])
+ ->param('collectionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Collection ID.', false, ['dbForProject'])
+ ->param('key', '', fn (Database $dbForProject) => new Key(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Attribute Key.', false, ['dbForProject'])
+ ->param('required', null, new Boolean(), 'Is attribute required?')
+ ->param('min', null, new Nullable(new Integer(false, 64)), 'Minimum value', true)
+ ->param('max', null, new Nullable(new Integer(false, 64)), 'Maximum value', true)
+ ->param('default', null, new Nullable(new Integer(false, 64)), 'Default value. Cannot be set when attribute is required.', true)
+ ->param('array', false, new Boolean(), 'Is attribute an array?', true)
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('publisherForDatabase')
+ ->inject('queueForEvents')
+ ->inject('authorization')
+ ->callback($this->action(...));
+ }
+
+ public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, bool $array, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void
+ {
+ $min ??= \PHP_INT_MIN;
+ $max ??= \PHP_INT_MAX;
+
+ if ($min > $max) {
+ throw new Exception($this->getInvalidValueException(), 'Minimum value must be lesser than maximum value');
+ }
+
+ $validator = new Range($min, $max, Range::TYPE_INTEGER);
+ if (!\is_null($default) && !$validator->isValid($default)) {
+ throw new Exception($this->getInvalidValueException(), $validator->getDescription());
+ }
+
+ $attribute = $this->createAttribute($databaseId, $collectionId, new Document([
+ 'key' => $key,
+ 'type' => Database::VAR_BIGINT,
+ 'size' => 8,
+ 'required' => $required,
+ 'default' => $default,
+ 'array' => $array,
+ 'format' => APP_DATABASE_ATTRIBUTE_BIGINT_RANGE,
+ 'formatOptions' => ['min' => $min, 'max' => $max],
+ ]), $response, $dbForProject, $publisherForDatabase, $queueForEvents, $authorization);
+
+ $formatOptions = $attribute->getAttribute('formatOptions', []);
+ if (!empty($formatOptions)) {
+ $attribute->setAttribute('min', \intval($formatOptions['min']));
+ $attribute->setAttribute('max', \intval($formatOptions['max']));
+ }
+
+ $response
+ ->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED)
+ ->dynamic($attribute, $this->getResponseModel());
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Update.php
new file mode 100644
index 0000000000..5d8e8bf3a5
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Update.php
@@ -0,0 +1,106 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
+ ->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/bigint/:key')
+ ->desc('Update bigint attribute')
+ ->groups(['api', 'database', 'schema'])
+ ->label('scope', 'collections.write')
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].update')
+ ->label('audits.event', 'attribute.update')
+ ->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
+ ->label('sdk', new Method(
+ namespace: $this->getSDKNamespace(),
+ group: $this->getSDKGroup(),
+ name: self::getName(),
+ description: '/docs/references/databases/update-bigint-attribute.md',
+ auth: [AuthType::ADMIN, AuthType::KEY],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_OK,
+ model: $this->getResponseModel(),
+ )
+ ],
+ contentType: ContentType::JSON,
+ deprecated: new Deprecated(
+ since: '1.8.0',
+ replaceWith: 'tablesDB.updateBigIntColumn',
+ ),
+ ))
+ ->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject'])
+ ->param('collectionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Collection ID.', false, ['dbForProject'])
+ ->param('key', '', fn (Database $dbForProject) => new Key(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Attribute Key.', false, ['dbForProject'])
+ ->param('required', null, new Boolean(), 'Is attribute required?')
+ ->param('min', null, new Nullable(new Integer(false, 64)), 'Minimum value', true)
+ ->param('max', null, new Nullable(new Integer(false, 64)), 'Maximum value', true)
+ ->param('default', null, new Nullable(new Integer(false, 64)), 'Default value. Cannot be set when attribute is required.')
+ ->param('newKey', null, fn (Database $dbForProject) => new Nullable(new Key(false, $dbForProject->getAdapter()->getMaxUIDLength())), 'New Attribute Key.', true, ['dbForProject'])
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('queueForEvents')
+ ->inject('authorization')
+ ->callback($this->action(...));
+ }
+
+ public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, ?string $newKey, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization): void
+ {
+ $attribute = $this->updateAttribute(
+ databaseId: $databaseId,
+ collectionId: $collectionId,
+ key: $key,
+ dbForProject: $dbForProject,
+ queueForEvents: $queueForEvents,
+ authorization: $authorization,
+ type: Database::VAR_BIGINT,
+ default: $default,
+ required: $required,
+ min: $min,
+ max: $max,
+ newKey: $newKey
+ );
+
+ $formatOptions = $attribute->getAttribute('formatOptions', []);
+ if (!empty($formatOptions)) {
+ $attribute->setAttribute('min', \intval($formatOptions['min']));
+ $attribute->setAttribute('max', \intval($formatOptions['max']));
+ }
+
+ $response
+ ->setStatusCode(SwooleResponse::STATUS_CODE_OK)
+ ->dynamic($attribute, $this->getResponseModel());
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Boolean/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Boolean/Create.php
index a19b1626c9..475b43f569 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Boolean/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Boolean/Create.php
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Boolean;
-use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
+use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Deprecated;
@@ -68,13 +68,13 @@ class Create extends Action
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
- public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?bool $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
+ public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?bool $default, bool $array, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void
{
$attribute = $this->createAttribute($databaseId, $collectionId, new Document([
'key' => $key,
@@ -83,7 +83,7 @@ class Create extends Action
'required' => $required,
'default' => $default,
'array' => $array,
- ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization);
+ ]), $response, $dbForProject, $publisherForDatabase, $queueForEvents, $authorization);
$response
->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED)
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Datetime/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Datetime/Create.php
index 4162b50daf..7a0776751b 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Datetime/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Datetime/Create.php
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Datetime;
-use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
+use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Deprecated;
@@ -69,13 +69,13 @@ class Create extends Action
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
- public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
+ public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void
{
$attribute = $this->createAttribute(
$databaseId,
@@ -91,7 +91,7 @@ class Create extends Action
]),
$response,
$dbForProject,
- $queueForDatabase,
+ $publisherForDatabase,
$queueForEvents,
$authorization
);
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Delete.php
index 38b96e67bc..ff1636ae60 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Delete.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Delete.php
@@ -2,8 +2,9 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes;
-use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
+use Appwrite\Event\Message\Database as DatabaseMessage;
+use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@@ -66,13 +67,13 @@ class Delete extends Action
->param('key', '', fn (Database $dbForProject) => new Key(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Attribute Key.', false, ['dbForProject'])
->inject('response')
->inject('dbForProject')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
- public function action(string $databaseId, string $collectionId, string $key, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
+ public function action(string $databaseId, string $collectionId, string $key, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void
{
$db = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($db->isEmpty()) {
@@ -129,20 +130,6 @@ class Delete extends Action
}
}
- $queueForDatabase
- ->setDatabase($db)
- ->setType(DATABASE_TYPE_DELETE_ATTRIBUTE);
-
- if ($this->isCollectionsAPI()) {
- $queueForDatabase
- ->setRow($attribute)
- ->setTable($collection);
- } else {
- $queueForDatabase
- ->setDocument($attribute)
- ->setCollection($collection);
- }
-
$type = $attribute->getAttribute('type');
$format = $attribute->getAttribute('format');
@@ -158,6 +145,18 @@ class Delete extends Action
->setPayload($response->output($attribute, $model))
->setContext($this->getCollectionsEventsContext(), $collection);
+ $publisherForDatabase->enqueue(new DatabaseMessage(
+ project: $queueForEvents->getProject(),
+ user: $queueForEvents->getUser(),
+ type: DATABASE_TYPE_DELETE_ATTRIBUTE,
+ database: $db,
+ collection: $this->isCollectionsAPI() ? null : $collection,
+ document: $this->isCollectionsAPI() ? null : $attribute,
+ table: $this->isCollectionsAPI() ? $collection : null,
+ row: $this->isCollectionsAPI() ? $attribute : null,
+ events: Event::generateEvents($queueForEvents->getEvent(), $queueForEvents->getParams()),
+ ));
+
$response->noContent();
}
}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Email/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Email/Create.php
index 6530cdb1dd..098083bea6 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Email/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Email/Create.php
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Email;
-use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
+use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Deprecated;
@@ -69,13 +69,13 @@ class Create extends Action
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
- public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
+ public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void
{
$attribute = $this->createAttribute(
$databaseId,
@@ -91,7 +91,7 @@ class Create extends Action
]),
$response,
$dbForProject,
- $queueForDatabase,
+ $publisherForDatabase,
$queueForEvents,
$authorization
);
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Enum/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Enum/Create.php
index fbc2d08cd1..602189e881 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Enum/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Enum/Create.php
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Enum;
-use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
+use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
@@ -72,13 +72,13 @@ class Create extends Action
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
- public function action(string $databaseId, string $collectionId, string $key, array $elements, ?bool $required, ?string $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
+ public function action(string $databaseId, string $collectionId, string $key, array $elements, ?bool $required, ?string $default, bool $array, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void
{
if (!is_null($default) && !\in_array($default, $elements, true)) {
throw new Exception($this->getInvalidValueException(), 'Default value not found in elements');
@@ -99,7 +99,7 @@ class Create extends Action
]),
$response,
$dbForProject,
- $queueForDatabase,
+ $publisherForDatabase,
$queueForEvents,
$authorization
);
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Float/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Float/Create.php
index e1585be169..a715b51b5a 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Float/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Float/Create.php
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Float;
-use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
+use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
@@ -73,13 +73,13 @@ class Create extends Action
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
- public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?float $min, ?float $max, ?float $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
+ public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?float $min, ?float $max, ?float $default, bool $array, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void
{
$min ??= -PHP_FLOAT_MAX;
$max ??= PHP_FLOAT_MAX;
@@ -102,7 +102,7 @@ class Create extends Action
'array' => $array,
'format' => APP_DATABASE_ATTRIBUTE_FLOAT_RANGE,
'formatOptions' => ['min' => $min, 'max' => $max],
- ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization);
+ ]), $response, $dbForProject, $publisherForDatabase, $queueForEvents, $authorization);
$formatOptions = $attribute->getAttribute('formatOptions', []);
if (!empty($formatOptions)) {
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/IP/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/IP/Create.php
index 8b02339252..9a142b1a86 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/IP/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/IP/Create.php
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\IP;
-use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
+use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Deprecated;
@@ -69,13 +69,13 @@ class Create extends Action
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
- public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
+ public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void
{
$attribute = $this->createAttribute(
$databaseId,
@@ -91,7 +91,7 @@ class Create extends Action
]),
$response,
$dbForProject,
- $queueForDatabase,
+ $publisherForDatabase,
$queueForEvents,
$authorization
);
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Integer/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Integer/Create.php
index 3d2fa68797..89aefb87e6 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Integer/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Integer/Create.php
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Integer;
-use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
+use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
@@ -73,13 +73,13 @@ class Create extends Action
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
- public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
+ public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, bool $array, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void
{
$min ??= \PHP_INT_MIN;
$max ??= \PHP_INT_MAX;
@@ -104,7 +104,7 @@ class Create extends Action
'array' => $array,
'format' => APP_DATABASE_ATTRIBUTE_INT_RANGE,
'formatOptions' => ['min' => $min, 'max' => $max],
- ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization);
+ ]), $response, $dbForProject, $publisherForDatabase, $queueForEvents, $authorization);
$formatOptions = $attribute->getAttribute('formatOptions', []);
if (!empty($formatOptions)) {
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Line/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Line/Create.php
index d2578a963f..d3f82cd109 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Line/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Line/Create.php
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Line;
-use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
+use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
@@ -69,13 +69,13 @@ class Create extends Action
->param('default', null, new Nullable(new Spatial(Database::VAR_LINESTRING)), 'Default value for attribute when not provided, two-dimensional array of coordinate pairs, [[longitude, latitude], [longitude, latitude], âĻ], listing the vertices of the line in order. Cannot be set when attribute is required.', true)
->inject('response')
->inject('dbForProject')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
- public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?array $default, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
+ public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?array $default, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void
{
if (!$dbForProject->getAdapter()->getSupportForSpatialAttributes()) {
throw new Exception(Exception::GENERAL_FEATURE_UNSUPPORTED, 'Spatial columns are not supported by this database.');
@@ -86,7 +86,7 @@ class Create extends Action
'type' => Database::VAR_LINESTRING,
'required' => $required,
'default' => $default
- ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization);
+ ]), $response, $dbForProject, $publisherForDatabase, $queueForEvents, $authorization);
$response
->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED)
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Longtext/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Longtext/Create.php
index 2fc9de8699..90591b43fb 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Longtext/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Longtext/Create.php
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Longtext;
-use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
+use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
@@ -67,7 +67,7 @@ class Create extends Action
->param('encrypt', false, new Boolean(), 'Toggle encryption for the attribute. Encryption enhances security by not storing any plain text values in the database. However, encrypted attributes cannot be queried.', true)
->inject('response')
->inject('dbForProject')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('plan')
->inject('authorization')
@@ -84,7 +84,7 @@ class Create extends Action
bool $encrypt,
UtopiaResponse $response,
Database $dbForProject,
- EventDatabase $queueForDatabase,
+ DatabasePublisher $publisherForDatabase,
Event $queueForEvents,
array $plan,
Authorization $authorization
@@ -112,7 +112,7 @@ class Create extends Action
]),
$response,
$dbForProject,
- $queueForDatabase,
+ $publisherForDatabase,
$queueForEvents,
$authorization
);
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Mediumtext/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Mediumtext/Create.php
index 5776e51917..0f7b386fd5 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Mediumtext/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Mediumtext/Create.php
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Mediumtext;
-use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
+use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
@@ -67,7 +67,7 @@ class Create extends Action
->param('encrypt', false, new Boolean(), 'Toggle encryption for the attribute. Encryption enhances security by not storing any plain text values in the database. However, encrypted attributes cannot be queried.', true)
->inject('response')
->inject('dbForProject')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('plan')
->inject('authorization')
@@ -84,7 +84,7 @@ class Create extends Action
bool $encrypt,
UtopiaResponse $response,
Database $dbForProject,
- EventDatabase $queueForDatabase,
+ DatabasePublisher $publisherForDatabase,
Event $queueForEvents,
array $plan,
Authorization $authorization
@@ -112,7 +112,7 @@ class Create extends Action
]),
$response,
$dbForProject,
- $queueForDatabase,
+ $publisherForDatabase,
$queueForEvents,
$authorization
);
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Point/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Point/Create.php
index 527b4330b9..38082b46da 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Point/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Point/Create.php
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Point;
-use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
+use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
@@ -69,13 +69,13 @@ class Create extends Action
->param('default', null, new Nullable(new Spatial(Database::VAR_POINT)), 'Default value for attribute when not provided, array of two numbers [longitude, latitude], representing a single coordinate. Cannot be set when attribute is required.', true)
->inject('response')
->inject('dbForProject')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
- public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?array $default, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
+ public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?array $default, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void
{
if (!$dbForProject->getAdapter()->getSupportForSpatialAttributes()) {
throw new Exception(Exception::GENERAL_FEATURE_UNSUPPORTED, 'Spatial columns are not supported by this database.');
@@ -86,7 +86,7 @@ class Create extends Action
'type' => Database::VAR_POINT,
'required' => $required,
'default' => $default,
- ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization);
+ ]), $response, $dbForProject, $publisherForDatabase, $queueForEvents, $authorization);
$response
->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED)
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Polygon/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Polygon/Create.php
index 4c3e725f3e..3063d1938a 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Polygon/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Polygon/Create.php
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Polygon;
-use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
+use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
@@ -69,13 +69,13 @@ class Create extends Action
->param('default', null, new Nullable(new Spatial(Database::VAR_POLYGON)), 'Default value for attribute when not provided, three-dimensional array where the outer array holds one or more linear rings, [[[longitude, latitude], âĻ], âĻ], the first ring is the exterior boundary, any additional rings are interior holes, and each ring must start and end with the same coordinate pair. Cannot be set when attribute is required.', true)
->inject('response')
->inject('dbForProject')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
- public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?array $default, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
+ public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?array $default, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void
{
if (!$dbForProject->getAdapter()->getSupportForSpatialAttributes()) {
throw new Exception(Exception::GENERAL_FEATURE_UNSUPPORTED, 'Spatial columns are not supported by this database.');
@@ -86,7 +86,7 @@ class Create extends Action
'type' => Database::VAR_POLYGON,
'required' => $required,
'default' => $default,
- ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization);
+ ]), $response, $dbForProject, $publisherForDatabase, $queueForEvents, $authorization);
$response
->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED)
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Relationship/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Relationship/Create.php
index fdd40aaa8f..ace48a5c56 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Relationship/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Relationship/Create.php
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Relationship;
-use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
+use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
@@ -81,13 +81,13 @@ class Create extends Action
], true), 'Constraints option', true)
->inject('response')
->inject('dbForProject')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
- public function action(string $databaseId, string $collectionId, string $relatedCollectionId, string $type, bool $twoWay, ?string $key, ?string $twoWayKey, string $onDelete, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
+ public function action(string $databaseId, string $collectionId, string $relatedCollectionId, string $type, bool $twoWay, ?string $key, ?string $twoWayKey, string $onDelete, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void
{
if (!$dbForProject->getAdapter()->getSupportForRelationships()) {
throw new Exception(Exception::GENERAL_FEATURE_UNSUPPORTED, 'Relationships are not supported by this database.');
@@ -159,7 +159,7 @@ class Create extends Action
'twoWayKey' => $twoWayKey,
'onDelete' => $onDelete,
]
- ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization);
+ ]), $response, $dbForProject, $publisherForDatabase, $queueForEvents, $authorization);
foreach ($attribute->getAttribute('options', []) as $k => $option) {
$attribute->setAttribute($k, $option);
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/String/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/String/Create.php
index c8917c3deb..a32a3083ab 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/String/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/String/Create.php
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\String;
-use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
+use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
@@ -75,7 +75,7 @@ class Create extends Action
->param('encrypt', false, new Boolean(), 'Toggle encryption for the attribute. Encryption enhances security by not storing any plain text values in the database. However, encrypted attributes cannot be queried.', true)
->inject('response')
->inject('dbForProject')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('plan')
->inject('authorization')
@@ -93,7 +93,7 @@ class Create extends Action
bool $encrypt,
UtopiaResponse $response,
Database $dbForProject,
- EventDatabase $queueForDatabase,
+ DatabasePublisher $publisherForDatabase,
Event $queueForEvents,
array $plan,
Authorization $authorization
@@ -134,7 +134,7 @@ class Create extends Action
]),
$response,
$dbForProject,
- $queueForDatabase,
+ $publisherForDatabase,
$queueForEvents,
$authorization
);
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Text/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Text/Create.php
index eb6b2f9691..79968d0feb 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Text/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Text/Create.php
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Text;
-use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
+use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
@@ -67,7 +67,7 @@ class Create extends Action
->param('encrypt', false, new Boolean(), 'Toggle encryption for the attribute. Encryption enhances security by not storing any plain text values in the database. However, encrypted attributes cannot be queried.', true)
->inject('response')
->inject('dbForProject')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('plan')
->inject('authorization')
@@ -84,7 +84,7 @@ class Create extends Action
bool $encrypt,
UtopiaResponse $response,
Database $dbForProject,
- EventDatabase $queueForDatabase,
+ DatabasePublisher $publisherForDatabase,
Event $queueForEvents,
array $plan,
Authorization $authorization
@@ -112,7 +112,7 @@ class Create extends Action
]),
$response,
$dbForProject,
- $queueForDatabase,
+ $publisherForDatabase,
$queueForEvents,
$authorization
);
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/URL/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/URL/Create.php
index 7ada8c7f7d..7338bdbd1d 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/URL/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/URL/Create.php
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\URL;
-use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
+use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Deprecated;
@@ -69,7 +69,7 @@ class Create extends Action
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
@@ -84,7 +84,7 @@ class Create extends Action
bool $array,
UtopiaResponse $response,
Database $dbForProject,
- EventDatabase $queueForDatabase,
+ DatabasePublisher $publisherForDatabase,
Event $queueForEvents,
Authorization $authorization
): void {
@@ -96,7 +96,7 @@ class Create extends Action
'default' => $default,
'array' => $array,
'format' => APP_DATABASE_ATTRIBUTE_URL,
- ]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization);
+ ]), $response, $dbForProject, $publisherForDatabase, $queueForEvents, $authorization);
$response
->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED)
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Varchar/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Varchar/Create.php
index 24a36725c8..89690de4e9 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Varchar/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Varchar/Create.php
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Varchar;
-use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
+use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
@@ -70,7 +70,7 @@ class Create extends Action
->param('encrypt', false, new Boolean(), 'Toggle encryption for the attribute. Encryption enhances security by not storing any plain text values in the database. However, encrypted attributes cannot be queried.', true)
->inject('response')
->inject('dbForProject')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('plan')
->inject('authorization')
@@ -88,7 +88,7 @@ class Create extends Action
bool $encrypt,
UtopiaResponse $response,
Database $dbForProject,
- EventDatabase $queueForDatabase,
+ DatabasePublisher $publisherForDatabase,
Event $queueForEvents,
array $plan,
Authorization $authorization
@@ -129,7 +129,7 @@ class Create extends Action
]),
$response,
$dbForProject,
- $queueForDatabase,
+ $publisherForDatabase,
$queueForEvents,
$authorization
);
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php
index fd309a413c..3a53a49579 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Create.php
@@ -290,13 +290,15 @@ class Create extends Action
}
if (isset($attribute['min']) || isset($attribute['max'])) {
- $format = $type === Database::VAR_INTEGER
- ? APP_DATABASE_ATTRIBUTE_INT_RANGE
- : APP_DATABASE_ATTRIBUTE_FLOAT_RANGE;
+ $format = match($type) {
+ Database::VAR_INTEGER => APP_DATABASE_ATTRIBUTE_INT_RANGE,
+ Database::VAR_BIGINT => APP_DATABASE_ATTRIBUTE_BIGINT_RANGE,
+ default => APP_DATABASE_ATTRIBUTE_FLOAT_RANGE,
+ };
$formatOptions = [
- 'min' => $attribute['min'] ?? ($type === Database::VAR_INTEGER ? \PHP_INT_MIN : -\PHP_FLOAT_MAX),
- 'max' => $attribute['max'] ?? ($type === Database::VAR_INTEGER ? \PHP_INT_MAX : \PHP_FLOAT_MAX),
+ 'min' => $attribute['min'] ?? ($type === Database::VAR_INTEGER || $type === Database::VAR_BIGINT ? \PHP_INT_MIN : -\PHP_FLOAT_MAX),
+ 'max' => $attribute['max'] ?? ($type === Database::VAR_INTEGER || $type === Database::VAR_BIGINT ? \PHP_INT_MAX : \PHP_FLOAT_MAX),
];
}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Delete.php
index 7a5b73f7db..87171fb2fe 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Delete.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Delete.php
@@ -2,8 +2,9 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections;
-use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
+use Appwrite\Event\Message\Database as DatabaseMessage;
+use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@@ -63,13 +64,13 @@ class Delete extends Action
->inject('response')
->inject('dbForProject')
->inject('getDatabasesDB')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
- public function action(string $databaseId, string $collectionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
+ public function action(string $databaseId, string $collectionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void
{
$database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
@@ -89,22 +90,22 @@ class Delete extends Action
$dbForDatabases = $getDatabasesDB($database);
$dbForDatabases->purgeCachedCollection('database_' . $database->getSequence() . '_collection_' . $collection->getSequence());
- $queueForDatabase
- ->setType(DATABASE_TYPE_DELETE_COLLECTION)
- ->setDatabase($database);
-
- if ($this->isCollectionsAPI()) {
- $queueForDatabase->setCollection($collection);
- } else {
- $queueForDatabase->setTable($collection);
- }
-
$queueForEvents
->setParam('databaseId', $databaseId)
->setContext('database', $database)
->setParam($this->getEventsParamKey(), $collection->getId())
->setPayload($response->output($collection, $this->getResponseModel()));
+ $publisherForDatabase->enqueue(new DatabaseMessage(
+ project: $queueForEvents->getProject(),
+ user: $queueForEvents->getUser(),
+ type: DATABASE_TYPE_DELETE_COLLECTION,
+ database: $database,
+ collection: $this->isCollectionsAPI() ? $collection : null,
+ table: $this->isCollectionsAPI() ? null : $collection,
+ events: Event::generateEvents($queueForEvents->getEvent(), $queueForEvents->getParams()),
+ ));
+
$response->noContent();
}
}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php
index 8100a2c51b..d62782f95e 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Action.php
@@ -3,6 +3,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents;
use Appwrite\Event\Event;
+use Appwrite\Event\Message\Func as FunctionMessage;
+use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Extend\Exception;
use Appwrite\Functions\EventProcessor;
use Appwrite\Platform\Modules\Databases\Http\Databases\Action as DatabasesAction;
@@ -421,7 +423,7 @@ abstract class Action extends DatabasesAction
* @param Document[] $documents
* @param Event $queueForEvents
* @param Event $queueForRealtime
- * @param Event $queueForFunctions
+ * @param FunctionPublisher $publisherForFunctions
* @param Event $queueForWebhooks
* @param Database $dbForProject
* @param EventProcessor $eventProcessor
@@ -434,7 +436,7 @@ abstract class Action extends DatabasesAction
array $documents,
Event $queueForEvents,
Event $queueForRealtime,
- Event $queueForFunctions,
+ FunctionPublisher $publisherForFunctions,
Event $queueForWebhooks,
Database $dbForProject,
EventProcessor $eventProcessor
@@ -472,9 +474,15 @@ abstract class Action extends DatabasesAction
if (!empty($functionsEvents)) {
foreach ($generatedEvents as $event) {
if (isset($functionsEvents[$event])) {
- $queueForFunctions
- ->from($queueForEvents)
- ->trigger();
+ $publisherForFunctions->enqueue(FunctionMessage::fromEvent(
+ event: $queueForEvents->getEvent(),
+ params: $queueForEvents->getParams(),
+ project: $queueForEvents->getProject(),
+ user: $queueForEvents->getUser(),
+ userId: $queueForEvents->getUserId(),
+ payload: $queueForEvents->getPayload(),
+ platform: $queueForEvents->getPlatform(),
+ ));
break;
}
}
@@ -494,7 +502,6 @@ abstract class Action extends DatabasesAction
$queueForEvents->reset();
$queueForRealtime->reset();
- $queueForFunctions->reset();
$queueForWebhooks->reset();
}
}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Delete.php
index 267a54adb0..2dc3100046 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Delete.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Delete.php
@@ -3,6 +3,7 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Bulk;
use Appwrite\Event\Event;
+use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Extend\Exception;
use Appwrite\Functions\EventProcessor;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Action;
@@ -80,14 +81,14 @@ class Delete extends Action
->inject('usage')
->inject('queueForEvents')
->inject('queueForRealtime')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('plan')
->inject('eventProcessor')
->callback($this->action(...));
}
- public function action(string $databaseId, string $collectionId, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, 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, callable $getDatabasesDB, Context $usage, Event $queueForEvents, Event $queueForRealtime, FunctionPublisher $publisherForFunctions, Event $queueForWebhooks, array $plan, EventProcessor $eventProcessor): void
{
$database = $dbForProject->getDocument('databases', $databaseId);
if ($database->isEmpty()) {
@@ -206,7 +207,7 @@ class Delete extends Action
$documents,
$queueForEvents,
$queueForRealtime,
- $queueForFunctions,
+ $publisherForFunctions,
$queueForWebhooks,
$dbForProject,
$eventProcessor
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Update.php
index da3adf1192..393590d1e6 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Update.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Update.php
@@ -3,6 +3,7 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Bulk;
use Appwrite\Event\Event;
+use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Extend\Exception;
use Appwrite\Functions\EventProcessor;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Action;
@@ -84,14 +85,14 @@ class Update extends Action
->inject('usage')
->inject('queueForEvents')
->inject('queueForRealtime')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('plan')
->inject('eventProcessor')
->callback($this->action(...));
}
- public function action(string $databaseId, string $collectionId, string|array $data, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, 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, callable $getDatabasesDB, Context $usage, Event $queueForEvents, Event $queueForRealtime, FunctionPublisher $publisherForFunctions, Event $queueForWebhooks, array $plan, EventProcessor $eventProcessor): void
{
$data = \is_string($data)
? \json_decode($data, true)
@@ -237,7 +238,7 @@ class Update extends Action
$documents,
$queueForEvents,
$queueForRealtime,
- $queueForFunctions,
+ $publisherForFunctions,
$queueForWebhooks,
$dbForProject,
$eventProcessor
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Upsert.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Upsert.php
index 5a5ebf48ee..d69298919b 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Upsert.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Upsert.php
@@ -3,6 +3,7 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Bulk;
use Appwrite\Event\Event;
+use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Extend\Exception;
use Appwrite\Functions\EventProcessor;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents\Action;
@@ -82,14 +83,14 @@ class Upsert extends Action
->inject('usage')
->inject('queueForEvents')
->inject('queueForRealtime')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('plan')
->inject('eventProcessor')
->callback($this->action(...));
}
- public function action(string $databaseId, string $collectionId, array $documents, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, 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, callable $getDatabasesDB, Context $usage, Event $queueForEvents, Event $queueForRealtime, FunctionPublisher $publisherForFunctions, Event $queueForWebhooks, array $plan, EventProcessor $eventProcessor): void
{
$database = $dbForProject->getDocument('databases', $databaseId);
if ($database->isEmpty()) {
@@ -212,7 +213,7 @@ class Upsert extends Action
$upserted,
$queueForEvents,
$queueForRealtime,
- $queueForFunctions,
+ $publisherForFunctions,
$queueForWebhooks,
$dbForProject,
$eventProcessor
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php
index 633a2bbc86..2ade0b2b79 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php
@@ -3,6 +3,7 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Documents;
use Appwrite\Event\Event;
+use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Extend\Exception;
use Appwrite\Functions\EventProcessor;
use Appwrite\SDK\AuthType;
@@ -137,7 +138,7 @@ class Create extends Action
->inject('queueForEvents')
->inject('usage')
->inject('queueForRealtime')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('plan')
->inject('authorization')
@@ -145,7 +146,7 @@ class Create extends Action
->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, callable $getDatabasesDB, User $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, callable $getDatabasesDB, User $user, Event $queueForEvents, Context $usage, Event $queueForRealtime, FunctionPublisher $publisherForFunctions, Event $queueForWebhooks, array $plan, Authorization $authorization, EventProcessor $eventProcessor): void
{
$data = \is_string($data)
? \json_decode($data, true)
@@ -517,7 +518,7 @@ class Create extends Action
$created,
$queueForEvents,
$queueForRealtime,
- $queueForFunctions,
+ $publisherForFunctions,
$queueForWebhooks,
$dbForProject,
$eventProcessor
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Create.php
index 7e073c95d4..6c13a5c33c 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Create.php
@@ -2,8 +2,9 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Indexes;
-use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
+use Appwrite\Event\Message\Database as DatabaseMessage;
+use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@@ -78,13 +79,13 @@ class Create extends Action
->inject('response')
->inject('dbForProject')
->inject('getDatabasesDB')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
- public function action(string $databaseId, string $collectionId, string $key, string $type, array $attributes, array $orders, array $lengths, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
+ public function action(string $databaseId, string $collectionId, string $key, string $type, array $attributes, array $orders, array $lengths, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void
{
$db = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
@@ -228,20 +229,6 @@ class Create extends Action
$dbForProject->purgeCachedDocument('database_' . $db->getSequence(), $collectionId);
- $queueForDatabase
- ->setType(DATABASE_TYPE_CREATE_INDEX)
- ->setDatabase($db);
-
- if ($this->isCollectionsAPI()) {
- $queueForDatabase
- ->setCollection($collection)
- ->setDocument($index);
- } else {
- $queueForDatabase
- ->setTable($collection)
- ->setRow($index);
- }
-
$queueForEvents
->setContext('database', $db)
->setParam('databaseId', $databaseId)
@@ -250,6 +237,18 @@ class Create extends Action
->setParam('tableId', $collection->getId())
->setContext($this->getCollectionsEventsContext(), $collection);
+ $publisherForDatabase->enqueue(new DatabaseMessage(
+ project: $queueForEvents->getProject(),
+ user: $queueForEvents->getUser(),
+ type: DATABASE_TYPE_CREATE_INDEX,
+ database: $db,
+ collection: $this->isCollectionsAPI() ? $collection : null,
+ document: $this->isCollectionsAPI() ? $index : null,
+ table: $this->isCollectionsAPI() ? null : $collection,
+ row: $this->isCollectionsAPI() ? null : $index,
+ events: Event::generateEvents($queueForEvents->getEvent(), $queueForEvents->getParams()),
+ ));
+
$response
->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED)
->dynamic($index, $this->getResponseModel());
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Delete.php
index dea62bfc16..82cada6e0d 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Delete.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Delete.php
@@ -2,8 +2,9 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Indexes;
-use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
+use Appwrite\Event\Message\Database as DatabaseMessage;
+use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@@ -69,13 +70,13 @@ class Delete extends Action
->param('key', '', fn (Database $dbForProject) => new Key(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Index Key.', false, ['dbForProject'])
->inject('response')
->inject('dbForProject')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
- public function action(string $databaseId, string $collectionId, string $key, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
+ public function action(string $databaseId, string $collectionId, string $key, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void
{
$db = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
@@ -103,20 +104,6 @@ class Delete extends Action
$dbForProject->purgeCachedDocument('database_' . $db->getSequence(), $collectionId);
- $queueForDatabase
- ->setType(DATABASE_TYPE_DELETE_INDEX)
- ->setDatabase($db);
-
- if ($this->isCollectionsAPI()) {
- $queueForDatabase
- ->setCollection($collection)
- ->setDocument($index);
- } else {
- $queueForDatabase
- ->setTable($collection)
- ->setRow($index);
- }
-
$queueForEvents
->setContext('database', $db)
->setParam('databaseId', $databaseId)
@@ -126,6 +113,18 @@ class Delete extends Action
->setContext($this->getCollectionsEventsContext(), $collection)
->setPayload($response->output($index, $this->getResponseModel()));
+ $publisherForDatabase->enqueue(new DatabaseMessage(
+ project: $queueForEvents->getProject(),
+ user: $queueForEvents->getUser(),
+ type: DATABASE_TYPE_DELETE_INDEX,
+ database: $db,
+ collection: $this->isCollectionsAPI() ? $collection : null,
+ document: $this->isCollectionsAPI() ? $index : null,
+ table: $this->isCollectionsAPI() ? null : $collection,
+ row: $this->isCollectionsAPI() ? null : $index,
+ events: Event::generateEvents($queueForEvents->getEvent(), $queueForEvents->getParams()),
+ ));
+
$response->noContent();
}
}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Delete.php
index 1046d7e566..058c48d68f 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Delete.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Delete.php
@@ -2,8 +2,9 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases;
-use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
+use Appwrite\Event\Message\Database as DatabaseMessage;
+use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@@ -58,12 +59,12 @@ class Delete extends Action
->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject'])
->inject('response')
->inject('dbForProject')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->callback($this->action(...));
}
- public function action(string $databaseId, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void
+ public function action(string $databaseId, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents): void
{
$database = $dbForProject->getDocument('databases', $databaseId);
@@ -78,14 +79,18 @@ class Delete extends Action
$dbForProject->purgeCachedDocument('databases', $database->getId());
$dbForProject->purgeCachedCollection('databases_' . $database->getSequence());
- $queueForDatabase
- ->setType(DATABASE_TYPE_DELETE_DATABASE)
- ->setDatabase($database);
-
$queueForEvents
->setParam('databaseId', $database->getId())
->setPayload($response->output($database, UtopiaResponse::MODEL_DATABASE));
+ $publisherForDatabase->enqueue(new DatabaseMessage(
+ project: $queueForEvents->getProject(),
+ user: $queueForEvents->getUser(),
+ type: DATABASE_TYPE_DELETE_DATABASE,
+ database: $database,
+ events: Event::generateEvents($queueForEvents->getEvent(), $queueForEvents->getParams()),
+ ));
+
$response->noContent();
}
}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Delete.php
index d57cebbe4a..072cb21bbc 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Delete.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Delete.php
@@ -2,7 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Transactions;
-use Appwrite\Event\Delete as DeleteEvent;
+use Appwrite\Event\Message\Delete as DeleteMessage;
+use Appwrite\Event\Publisher\Delete as DeletePublisher;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@@ -10,6 +11,7 @@ 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\UID;
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
@@ -51,11 +53,12 @@ class Delete extends Action
->param('transactionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Transaction ID.', false, ['dbForProject'])
->inject('response')
->inject('dbForProject')
- ->inject('queueForDeletes')
+ ->inject('publisherForDeletes')
+ ->inject('project')
->callback($this->action(...));
}
- public function action(string $transactionId, UtopiaResponse $response, Database $dbForProject, DeleteEvent $queueForDeletes): void
+ public function action(string $transactionId, UtopiaResponse $response, Database $dbForProject, DeletePublisher $publisherForDeletes, Document $project): void
{
$transaction = $dbForProject->getDocument('transactions', $transactionId);
@@ -65,9 +68,11 @@ class Delete extends Action
$dbForProject->deleteDocument('transactions', $transactionId);
- $queueForDeletes
- ->setType(DELETE_TYPE_DOCUMENT)
- ->setDocument($transaction);
+ $publisherForDeletes->enqueue(new DeleteMessage(
+ project: $project,
+ type: DELETE_TYPE_DOCUMENT,
+ document: $transaction,
+ ));
$response->noContent();
}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php
index c4d51e6c64..fe2ad8dbae 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Transactions/Update.php
@@ -3,8 +3,11 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Transactions;
use Appwrite\Databases\TransactionState;
-use Appwrite\Event\Delete;
use Appwrite\Event\Event;
+use Appwrite\Event\Message\Delete as DeleteMessage;
+use Appwrite\Event\Message\Func as FunctionMessage;
+use Appwrite\Event\Publisher\Delete as DeletePublisher;
+use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Extend\Exception;
use Appwrite\Functions\EventProcessor;
use Appwrite\SDK\AuthType;
@@ -73,11 +76,11 @@ class Update extends Action
->inject('getDatabasesDB')
->inject('user')
->inject('transactionState')
- ->inject('queueForDeletes')
+ ->inject('publisherForDeletes')
->inject('queueForEvents')
->inject('usage')
->inject('queueForRealtime')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('authorization')
->inject('eventProcessor')
@@ -93,11 +96,11 @@ class Update extends Action
* @param callable $getDatabasesDB
* @param User $user
* @param TransactionState $transactionState
- * @param Delete $queueForDeletes
+ * @param DeletePublisher $publisherForDeletes
* @param Event $queueForEvents
* @param Context $usage
* @param Event $queueForRealtime
- * @param Event $queueForFunctions
+ * @param FunctionPublisher $publisherForFunctions
* @param Event $queueForWebhooks
* @param EventProcessor $eventProcessor
* @return void
@@ -108,7 +111,7 @@ class Update extends Action
* @throws StructureException
* @throws \Utopia\Http\Exception
*/
- public function action(string $transactionId, bool $commit, bool $rollback, Document $project, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, User $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, Document $project, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, User $user, TransactionState $transactionState, DeletePublisher $publisherForDeletes, Event $queueForEvents, Context $usage, Event $queueForRealtime, FunctionPublisher $publisherForFunctions, Event $queueForWebhooks, Authorization $authorization, EventProcessor $eventProcessor): void
{
if (!$commit && !$rollback) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Either commit or rollback must be true');
@@ -154,9 +157,11 @@ class Update extends Action
new Document(['status' => 'committed'])
));
- $queueForDeletes
- ->setType(DELETE_TYPE_DOCUMENT)
- ->setDocument($transaction);
+ $publisherForDeletes->enqueue(new DeleteMessage(
+ project: $project,
+ type: DELETE_TYPE_DOCUMENT,
+ document: $transaction,
+ ));
$response
->setStatusCode(SwooleResponse::STATUS_CODE_OK)
@@ -293,9 +298,11 @@ class Update extends Action
new Document(['status' => 'committed'])
));
- $queueForDeletes
- ->setType(DELETE_TYPE_DOCUMENT)
- ->setDocument($transaction);
+ $publisherForDeletes->enqueue(new DeleteMessage(
+ project: $project,
+ type: DELETE_TYPE_DOCUMENT,
+ document: $transaction,
+ ));
} catch (NotFoundException $e) {
$authorization->skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([
'status' => 'failed',
@@ -461,7 +468,15 @@ class Update extends Action
if (!empty($functionsEvents)) {
foreach ($generatedEvents as $event) {
if (isset($functionsEvents[$event])) {
- $queueForFunctions->from($queueForEvents)->trigger();
+ $publisherForFunctions->enqueue(FunctionMessage::fromEvent(
+ event: $queueForEvents->getEvent(),
+ params: $queueForEvents->getParams(),
+ project: $queueForEvents->getProject(),
+ user: $queueForEvents->getUser(),
+ userId: $queueForEvents->getUserId(),
+ payload: $queueForEvents->getPayload(),
+ platform: $queueForEvents->getPlatform(),
+ ));
break;
}
}
@@ -480,7 +495,6 @@ class Update extends Action
$queueForEvents->reset();
$queueForRealtime->reset();
- $queueForFunctions->reset();
$queueForWebhooks->reset();
}
}
@@ -492,9 +506,11 @@ class Update extends Action
new Document(['status' => 'failed'])
));
- $queueForDeletes
- ->setType(DELETE_TYPE_DOCUMENT)
- ->setDocument($transaction);
+ $publisherForDeletes->enqueue(new DeleteMessage(
+ project: $project,
+ type: DELETE_TYPE_DOCUMENT,
+ document: $transaction,
+ ));
}
$response
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Delete.php
index d698b40203..043f74998d 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Delete.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Delete.php
@@ -54,7 +54,7 @@ class Delete extends CollectionDelete
->inject('response')
->inject('dbForProject')
->inject('getDatabasesDB')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Bulk/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Bulk/Delete.php
index 09ad9a5741..6b2910aac4 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Bulk/Delete.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Bulk/Delete.php
@@ -63,7 +63,7 @@ class Delete extends DocumentsDelete
->inject('usage')
->inject('queueForEvents')
->inject('queueForRealtime')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('plan')
->inject('eventProcessor')
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Bulk/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Bulk/Update.php
index c723f1bc30..f395d0b490 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Bulk/Update.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Bulk/Update.php
@@ -65,7 +65,7 @@ class Update extends DocumentsUpdate
->inject('usage')
->inject('queueForEvents')
->inject('queueForRealtime')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('plan')
->inject('eventProcessor')
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Bulk/Upsert.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Bulk/Upsert.php
index d5b62ec903..5acc4626af 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Bulk/Upsert.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Bulk/Upsert.php
@@ -65,7 +65,7 @@ class Upsert extends DocumentsUpsert
->inject('usage')
->inject('queueForEvents')
->inject('queueForRealtime')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('plan')
->inject('eventProcessor')
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Create.php
index 532ae826e2..2df96958ad 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Documents/Create.php
@@ -112,7 +112,7 @@ class Create extends DocumentCreate
->inject('queueForEvents')
->inject('usage')
->inject('queueForRealtime')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('plan')
->inject('authorization')
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Indexes/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Indexes/Create.php
index dc3ce34605..637255f16a 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Indexes/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Indexes/Create.php
@@ -65,7 +65,7 @@ class Create extends IndexCreate
->inject('response')
->inject('dbForProject')
->inject('getDatabasesDB')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Indexes/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Indexes/Delete.php
index d4464f171d..1e3c012b4f 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Indexes/Delete.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Collections/Indexes/Delete.php
@@ -59,7 +59,7 @@ class Delete extends IndexDelete
->param('key', '', new Key(), 'Index Key.')
->inject('response')
->inject('dbForProject')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Delete.php
index 1708656c98..5e63ab8a7f 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Delete.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Delete.php
@@ -48,7 +48,7 @@ class Delete extends DatabaseDelete
->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject'])
->inject('response')
->inject('dbForProject')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('usage')
->callback($this->action(...));
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Transactions/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Transactions/Delete.php
index 036f2e9600..94ff3fa214 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Transactions/Delete.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Transactions/Delete.php
@@ -49,7 +49,8 @@ class Delete extends TransactionsDelete
->param('transactionId', '', new UID(), 'Transaction ID.')
->inject('response')
->inject('dbForProject')
- ->inject('queueForDeletes')
+ ->inject('publisherForDeletes')
+ ->inject('project')
->callback($this->action(...));
}
}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Transactions/Update.php
index b4c0c2ffab..1b9cdee137 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Transactions/Update.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/DocumentsDB/Transactions/Update.php
@@ -56,11 +56,11 @@ class Update extends TransactionsUpdate
->inject('getDatabasesDB')
->inject('user')
->inject('transactionState')
- ->inject('queueForDeletes')
+ ->inject('publisherForDeletes')
->inject('queueForEvents')
->inject('usage')
->inject('queueForRealtime')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('authorization')
->inject('eventProcessor')
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Delete.php
index 7873d369e6..70dc8430f2 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Delete.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Delete.php
@@ -48,7 +48,7 @@ class Delete extends DatabaseDelete
->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject'])
->inject('response')
->inject('dbForProject')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->callback($this->action(...));
}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Create.php
new file mode 100644
index 0000000000..9d882e09a6
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Create.php
@@ -0,0 +1,70 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
+ ->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/bigint')
+ ->desc('Create bigint column')
+ ->groups(['api', 'database', 'schema'])
+ ->label('scope', ['tables.write', 'collections.write', 'columns.write', 'attributes.write'])
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].create')
+ ->label('audits.event', 'column.create')
+ ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}')
+ ->label('sdk', new Method(
+ namespace: $this->getSDKNamespace(),
+ group: $this->getSDKGroup(),
+ name: self::getName(),
+ description: '/docs/references/tablesdb/create-bigint-column.md',
+ auth: [AuthType::ADMIN, AuthType::KEY],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_ACCEPTED,
+ model: $this->getResponseModel(),
+ )
+ ]
+ ))
+ ->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject'])
+ ->param('tableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Table ID.', false, ['dbForProject'])
+ ->param('key', '', fn (Database $dbForProject) => new Key(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Column Key.', false, ['dbForProject'])
+ ->param('required', null, new Boolean(), 'Is column required?')
+ ->param('min', null, new Nullable(new Integer(false, 64)), 'Minimum value', true)
+ ->param('max', null, new Nullable(new Integer(false, 64)), 'Maximum value', true)
+ ->param('default', null, new Nullable(new Integer(false, 64)), 'Default value. Cannot be set when column is required.', true)
+ ->param('array', false, new Boolean(), 'Is column an array?', true)
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('publisherForDatabase')
+ ->inject('queueForEvents')
+ ->inject('authorization')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Update.php
new file mode 100644
index 0000000000..b2754a2b7d
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/BigInt/Update.php
@@ -0,0 +1,71 @@
+setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
+ ->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/bigint/:key')
+ ->desc('Update bigint column')
+ ->groups(['api', 'database', 'schema'])
+ ->label('scope', ['tables.write', 'collections.write', 'columns.write', 'attributes.write'])
+ ->label('resourceType', RESOURCE_TYPE_DATABASES)
+ ->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].update')
+ ->label('audits.event', 'column.update')
+ ->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}')
+ ->label('sdk', new Method(
+ namespace: $this->getSDKNamespace(),
+ group: $this->getSDKGroup(),
+ name: self::getName(),
+ description: '/docs/references/tablesdb/update-bigint-column.md',
+ auth: [AuthType::ADMIN, AuthType::KEY],
+ responses: [
+ new SDKResponse(
+ code: SwooleResponse::STATUS_CODE_OK,
+ model: $this->getResponseModel(),
+ )
+ ],
+ contentType: ContentType::JSON
+ ))
+ ->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject'])
+ ->param('tableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Table ID.', false, ['dbForProject'])
+ ->param('key', '', fn (Database $dbForProject) => new Key(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Column Key.', false, ['dbForProject'])
+ ->param('required', null, new Boolean(), 'Is column required?')
+ ->param('min', null, new Nullable(new Integer(false, 64)), 'Minimum value', true)
+ ->param('max', null, new Nullable(new Integer(false, 64)), 'Maximum value', true)
+ ->param('default', null, new Nullable(new Integer(false, 64)), 'Default value. Cannot be set when column is required.')
+ ->param('newKey', null, fn (Database $dbForProject) => new Nullable(new Key(false, $dbForProject->getAdapter()->getMaxUIDLength())), 'New Column Key.', true, ['dbForProject'])
+ ->inject('response')
+ ->inject('dbForProject')
+ ->inject('queueForEvents')
+ ->inject('authorization')
+ ->callback($this->action(...));
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Boolean/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Boolean/Create.php
index 10cd65bc98..334c8b5124 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Boolean/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Boolean/Create.php
@@ -59,7 +59,7 @@ class Create extends BooleanCreate
->param('array', false, new Boolean(), 'Is column an array?', true)
->inject('response')
->inject('dbForProject')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Datetime/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Datetime/Create.php
index 64e73e310e..922e071f35 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Datetime/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Datetime/Create.php
@@ -60,7 +60,7 @@ class Create extends DatetimeCreate
->param('array', false, new Boolean(), 'Is column an array?', true)
->inject('response')
->inject('dbForProject')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Delete.php
index f4d606637d..8e0abf211f 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Delete.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Delete.php
@@ -57,7 +57,7 @@ class Delete extends AttributesDelete
->param('key', '', fn (Database $dbForProject) => new Key(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Column Key.', false, ['dbForProject'])
->inject('response')
->inject('dbForProject')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Email/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Email/Create.php
index d0b2ed3e4b..072e334b4b 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Email/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Email/Create.php
@@ -60,7 +60,7 @@ class Create extends EmailCreate
->param('array', false, new Boolean(), 'Is column an array?', true)
->inject('response')
->inject('dbForProject')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Enum/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Enum/Create.php
index e58ae115fc..9d24f310bd 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Enum/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Enum/Create.php
@@ -62,7 +62,7 @@ class Create extends EnumCreate
->param('array', false, new Boolean(), 'Is column an array?', true)
->inject('response')
->inject('dbForProject')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Float/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Float/Create.php
index b8e81820aa..d68b3a4921 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Float/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Float/Create.php
@@ -62,7 +62,7 @@ class Create extends FloatCreate
->param('array', false, new Boolean(), 'Is column an array?', true)
->inject('response')
->inject('dbForProject')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/IP/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/IP/Create.php
index c2faec9aeb..ff5828e749 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/IP/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/IP/Create.php
@@ -60,7 +60,7 @@ class Create extends IPCreate
->param('array', false, new Boolean(), 'Is column an array?', true)
->inject('response')
->inject('dbForProject')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Integer/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Integer/Create.php
index 1a965c19dc..dec399cdb2 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Integer/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Integer/Create.php
@@ -62,7 +62,7 @@ class Create extends IntegerCreate
->param('array', false, new Boolean(), 'Is column an array?', true)
->inject('response')
->inject('dbForProject')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Line/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Line/Create.php
index c2f480d5d0..71548c74da 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Line/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Line/Create.php
@@ -59,7 +59,7 @@ class Create extends LineCreate
->param('default', null, new Nullable(new Spatial(Database::VAR_LINESTRING)), 'Default value for column when not provided, two-dimensional array of coordinate pairs, [[longitude, latitude], [longitude, latitude], âĻ], listing the vertices of the line in order. Cannot be set when column is required.', true)
->inject('response')
->inject('dbForProject')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Longtext/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Longtext/Create.php
index 8e2dbd911d..ec0f633400 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Longtext/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Longtext/Create.php
@@ -60,7 +60,7 @@ class Create extends LongtextCreate
->param('encrypt', false, new Boolean(), 'Toggle encryption for the column. Encryption enhances security by not storing any plain text values in the database. However, encrypted columns cannot be queried.', true)
->inject('response')
->inject('dbForProject')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('plan')
->inject('authorization')
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Mediumtext/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Mediumtext/Create.php
index f0b8099f02..2728caa58f 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Mediumtext/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Mediumtext/Create.php
@@ -60,7 +60,7 @@ class Create extends MediumtextCreate
->param('encrypt', false, new Boolean(), 'Toggle encryption for the column. Encryption enhances security by not storing any plain text values in the database. However, encrypted columns cannot be queried.', true)
->inject('response')
->inject('dbForProject')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('plan')
->inject('authorization')
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Point/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Point/Create.php
index 138ee482c3..601e19299b 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Point/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Point/Create.php
@@ -59,7 +59,7 @@ class Create extends PointCreate
->param('default', null, new Nullable(new Spatial(Database::VAR_POINT)), 'Default value for column when not provided, array of two numbers [longitude, latitude], representing a single coordinate. Cannot be set when column is required.', true)
->inject('response')
->inject('dbForProject')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Polygon/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Polygon/Create.php
index a03a34f310..36972d5da2 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Polygon/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Polygon/Create.php
@@ -59,7 +59,7 @@ class Create extends PolygonCreate
->param('default', null, new Nullable(new Spatial(Database::VAR_POLYGON)), 'Default value for column when not provided, three-dimensional array where the outer array holds one or more linear rings, [[[longitude, latitude], âĻ], âĻ], the first ring is the exterior boundary, any additional rings are interior holes, and each ring must start and end with the same coordinate pair. Cannot be set when column is required.', true)
->inject('response')
->inject('dbForProject')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Relationship/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Relationship/Create.php
index 87544926fe..414cf03b3d 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Relationship/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Relationship/Create.php
@@ -71,7 +71,7 @@ class Create extends RelationshipCreate
], true), 'Constraints option', true)
->inject('response')
->inject('dbForProject')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/String/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/String/Create.php
index 17f60f61c1..8151b3e8da 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/String/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/String/Create.php
@@ -69,7 +69,7 @@ class Create extends StringCreate
->param('encrypt', false, new Boolean(), 'Toggle encryption for the column. Encryption enhances security by not storing any plain text values in the database. However, encrypted columns cannot be queried.', true)
->inject('response')
->inject('dbForProject')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('plan')
->inject('authorization')
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Text/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Text/Create.php
index a8fde7d271..bffdc96001 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Text/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Text/Create.php
@@ -60,7 +60,7 @@ class Create extends TextCreate
->param('encrypt', false, new Boolean(), 'Toggle encryption for the column. Encryption enhances security by not storing any plain text values in the database. However, encrypted columns cannot be queried.', true)
->inject('response')
->inject('dbForProject')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('plan')
->inject('authorization')
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/URL/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/URL/Create.php
index 19b33594b7..2edf4a62f6 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/URL/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/URL/Create.php
@@ -60,7 +60,7 @@ class Create extends URLCreate
->param('array', false, new Boolean(), 'Is column an array?', true)
->inject('response')
->inject('dbForProject')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Varchar/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Varchar/Create.php
index 7595f16c45..307a1fd5e3 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Varchar/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Varchar/Create.php
@@ -63,7 +63,7 @@ class Create extends VarcharCreate
->param('encrypt', false, new Boolean(), 'Toggle encryption for the column. Encryption enhances security by not storing any plain text values in the database. However, encrypted columns cannot be queried.', true)
->inject('response')
->inject('dbForProject')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('plan')
->inject('authorization')
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Delete.php
index 97c5465fe3..3a6d6666f2 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Delete.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Delete.php
@@ -55,7 +55,7 @@ class Delete extends CollectionDelete
->inject('response')
->inject('dbForProject')
->inject('getDatabasesDB')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/Create.php
index d377bed184..77496fea59 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/Create.php
@@ -65,7 +65,7 @@ class Create extends IndexCreate
->inject('response')
->inject('dbForProject')
->inject('getDatabasesDB')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/Delete.php
index ca7e4fc2da..6cd5cfe78f 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/Delete.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Indexes/Delete.php
@@ -60,7 +60,7 @@ class Delete extends IndexDelete
->param('key', '', fn (Database $dbForProject) => new Key(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Index Key.', false, ['dbForProject'])
->inject('response')
->inject('dbForProject')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Delete.php
index 37a3db01db..8315a8d04b 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Delete.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Delete.php
@@ -65,7 +65,7 @@ class Delete extends DocumentsDelete
->inject('usage')
->inject('queueForEvents')
->inject('queueForRealtime')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('plan')
->inject('eventProcessor')
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Update.php
index bb839b752e..a31ebc15e0 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Update.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Update.php
@@ -67,7 +67,7 @@ class Update extends DocumentsUpdate
->inject('usage')
->inject('queueForEvents')
->inject('queueForRealtime')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('plan')
->inject('eventProcessor')
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Upsert.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Upsert.php
index 364bf4a928..543de8c4bc 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Upsert.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Upsert.php
@@ -67,7 +67,7 @@ class Upsert extends DocumentsUpsert
->inject('usage')
->inject('queueForEvents')
->inject('queueForRealtime')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('plan')
->inject('eventProcessor')
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Create.php
index 26649accfb..ea9e3e0b03 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Create.php
@@ -109,7 +109,7 @@ class Create extends DocumentCreate
->inject('queueForEvents')
->inject('usage')
->inject('queueForRealtime')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('plan')
->inject('authorization')
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Delete.php
index 9ee85ff153..988bfc3d1d 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Delete.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Delete.php
@@ -50,7 +50,8 @@ class Delete extends TransactionsDelete
->param('transactionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Transaction ID.', false, ['dbForProject'])
->inject('response')
->inject('dbForProject')
- ->inject('queueForDeletes')
+ ->inject('publisherForDeletes')
+ ->inject('project')
->callback($this->action(...));
}
}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Update.php
index 872927d533..bd06f475b2 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Update.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Transactions/Update.php
@@ -57,11 +57,11 @@ class Update extends TransactionsUpdate
->inject('getDatabasesDB')
->inject('user')
->inject('transactionState')
- ->inject('queueForDeletes')
+ ->inject('publisherForDeletes')
->inject('queueForEvents')
->inject('usage')
->inject('queueForRealtime')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('authorization')
->inject('eventProcessor')
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Delete.php
index f1188868aa..6ee83b2530 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Delete.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Delete.php
@@ -54,7 +54,7 @@ class Delete extends CollectionDelete
->inject('response')
->inject('dbForProject')
->inject('getDatabasesDB')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Bulk/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Bulk/Delete.php
index a4d640b423..4c7d97aa55 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Bulk/Delete.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Bulk/Delete.php
@@ -63,7 +63,7 @@ class Delete extends DocumentsDelete
->inject('usage')
->inject('queueForEvents')
->inject('queueForRealtime')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('plan')
->inject('eventProcessor')
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Bulk/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Bulk/Update.php
index 2784fa220a..18e441ede7 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Bulk/Update.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Bulk/Update.php
@@ -65,7 +65,7 @@ class Update extends DocumentsUpdate
->inject('usage')
->inject('queueForEvents')
->inject('queueForRealtime')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('plan')
->inject('eventProcessor')
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Bulk/Upsert.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Bulk/Upsert.php
index cfbf6c9158..c26e61d716 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Bulk/Upsert.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Bulk/Upsert.php
@@ -65,7 +65,7 @@ class Upsert extends DocumentsUpsert
->inject('usage')
->inject('queueForEvents')
->inject('queueForRealtime')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('plan')
->inject('eventProcessor')
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Create.php
index 563b5f60ef..dee8d8e85f 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Documents/Create.php
@@ -106,7 +106,7 @@ class Create extends DocumentCreate
->inject('queueForEvents')
->inject('usage')
->inject('queueForRealtime')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('plan')
->inject('authorization')
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Indexes/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Indexes/Create.php
index a535dd5724..bba7ee0579 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Indexes/Create.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Indexes/Create.php
@@ -65,7 +65,7 @@ class Create extends IndexCreate
->inject('response')
->inject('dbForProject')
->inject('getDatabasesDB')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Indexes/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Indexes/Delete.php
index 5c7fc47ee0..67e13dd26a 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Indexes/Delete.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Collections/Indexes/Delete.php
@@ -59,7 +59,7 @@ class Delete extends IndexDelete
->param('key', '', new Key(), 'Index Key.')
->inject('response')
->inject('dbForProject')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Delete.php
index c9d36904a9..a33eedccd5 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Delete.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Delete.php
@@ -47,7 +47,7 @@ class Delete extends DatabaseDelete
->param('databaseId', '', new UID(), 'Database ID.')
->inject('response')
->inject('dbForProject')
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('usage')
->callback($this->action(...));
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Transactions/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Transactions/Delete.php
index 0ac2caecba..2de71fc904 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Transactions/Delete.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Transactions/Delete.php
@@ -49,7 +49,8 @@ class Delete extends TransactionsDelete
->param('transactionId', '', new UID(), 'Transaction ID.')
->inject('response')
->inject('dbForProject')
- ->inject('queueForDeletes')
+ ->inject('publisherForDeletes')
+ ->inject('project')
->callback($this->action(...));
}
}
diff --git a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Transactions/Update.php b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Transactions/Update.php
index f4bd4d67f5..cebfcb42e8 100644
--- a/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Transactions/Update.php
+++ b/src/Appwrite/Platform/Modules/Databases/Http/VectorsDB/Transactions/Update.php
@@ -56,11 +56,11 @@ class Update extends TransactionsUpdate
->inject('getDatabasesDB')
->inject('user')
->inject('transactionState')
- ->inject('queueForDeletes')
+ ->inject('publisherForDeletes')
->inject('queueForEvents')
->inject('usage')
->inject('queueForRealtime')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('authorization')
->inject('eventProcessor')
diff --git a/src/Appwrite/Platform/Modules/Databases/Services/Registry/Legacy.php b/src/Appwrite/Platform/Modules/Databases/Services/Registry/Legacy.php
index a8d2205236..a2fba9efb3 100644
--- a/src/Appwrite/Platform/Modules/Databases/Services/Registry/Legacy.php
+++ b/src/Appwrite/Platform/Modules/Databases/Services/Registry/Legacy.php
@@ -2,6 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Services\Registry;
+use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\BigInt\Create as CreateBigIntAttribute;
+use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\BigInt\Update as UpdateBigIntAttribute;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Boolean\Create as CreateBooleanAttribute;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Boolean\Update as UpdateBooleanAttribute;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Datetime\Create as CreateDatetimeAttribute;
@@ -171,6 +173,10 @@ class Legacy extends Base
$service->addAction(CreateIntegerAttribute::getName(), new CreateIntegerAttribute());
$service->addAction(UpdateIntegerAttribute::getName(), new UpdateIntegerAttribute());
+ // Attribute: BigInt
+ $service->addAction(CreateBigIntAttribute::getName(), new CreateBigIntAttribute());
+ $service->addAction(UpdateBigIntAttribute::getName(), new UpdateBigIntAttribute());
+
// Attribute: IP
$service->addAction(CreateIPAttribute::getName(), new CreateIPAttribute());
$service->addAction(UpdateIPAttribute::getName(), new UpdateIPAttribute());
diff --git a/src/Appwrite/Platform/Modules/Databases/Services/Registry/TablesDB.php b/src/Appwrite/Platform/Modules/Databases/Services/Registry/TablesDB.php
index 965e0929fb..765fbd4421 100644
--- a/src/Appwrite/Platform/Modules/Databases/Services/Registry/TablesDB.php
+++ b/src/Appwrite/Platform/Modules/Databases/Services/Registry/TablesDB.php
@@ -5,6 +5,8 @@ namespace Appwrite\Platform\Modules\Databases\Services\Registry;
use Appwrite\Platform\Modules\Databases\Http\TablesDB\Create as CreateTablesDatabase;
use Appwrite\Platform\Modules\Databases\Http\TablesDB\Delete as DeleteTablesDatabase;
use Appwrite\Platform\Modules\Databases\Http\TablesDB\Get as GetTablesDatabase;
+use Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\BigInt\Create as CreateBigInt;
+use Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\BigInt\Update as UpdateBigInt;
use Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\Boolean\Create as CreateBoolean;
use Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\Boolean\Update as UpdateBoolean;
use Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\Datetime\Create as CreateDatetime;
@@ -151,6 +153,10 @@ class TablesDB extends Base
$service->addAction(CreateInteger::getName(), new CreateInteger());
$service->addAction(UpdateInteger::getName(), new UpdateInteger());
+ // Column: BigInt
+ $service->addAction(CreateBigInt::getName(), new CreateBigInt());
+ $service->addAction(UpdateBigInt::getName(), new UpdateBigInt());
+
// Column: IP
$service->addAction(CreateIP::getName(), new CreateIP());
$service->addAction(UpdateIP::getName(), new UpdateIP());
diff --git a/src/Appwrite/Platform/Modules/Databases/Workers/Databases.php b/src/Appwrite/Platform/Modules/Databases/Workers/Databases.php
index 39902aea53..ee8494b382 100644
--- a/src/Appwrite/Platform/Modules/Databases/Workers/Databases.php
+++ b/src/Appwrite/Platform/Modules/Databases/Workers/Databases.php
@@ -2,6 +2,7 @@
namespace Appwrite\Platform\Modules\Databases\Workers;
+use Appwrite\Event\Message\Database as DatabaseMessage;
use Appwrite\Event\Realtime;
use Exception;
use Utopia\Console;
@@ -60,10 +61,11 @@ class Databases extends Action
throw new Exception('Missing payload');
}
- $type = $payload['type'];
- $document = new Document($payload['row'] ?? $payload['document'] ?? []);
- $collection = new Document($payload['table'] ?? $payload['collection'] ?? []);
- $database = new Document($payload['database'] ?? []);
+ $databaseMessage = DatabaseMessage::fromArray($payload);
+ $type = $databaseMessage->type;
+ $document = $databaseMessage->row ?? $databaseMessage->document ?? new Document();
+ $collection = $databaseMessage->table ?? $databaseMessage->collection ?? new Document();
+ $database = $databaseMessage->database ?? new Document();
/**
* @var Database $dbForDatabases
*/
diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Create.php
index de468e1bb0..74e32e01c9 100644
--- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Create.php
+++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Create.php
@@ -2,8 +2,9 @@
namespace Appwrite\Platform\Modules\Functions\Http\Deployments;
-use Appwrite\Event\Build;
use Appwrite\Event\Event;
+use Appwrite\Event\Message\Build as BuildMessage;
+use Appwrite\Event\Publisher\Build as BuildPublisher;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@@ -89,9 +90,10 @@ class Create extends Action
->inject('project')
->inject('deviceForFunctions')
->inject('deviceForLocal')
- ->inject('queueForBuilds')
+ ->inject('publisherForBuilds')
->inject('plan')
->inject('authorization')
+ ->inject('platform')
->inject('redis')
->callback($this->action(...));
}
@@ -109,9 +111,10 @@ class Create extends Action
Document $project,
Device $deviceForFunctions,
Device $deviceForLocal,
- Build $queueForBuilds,
+ BuildPublisher $publisherForBuilds,
array $plan,
Authorization $authorization,
+ array $platform,
\Redis $redis
) {
$activate = \strval($activate) === 'true' || \strval($activate) === '1';
@@ -239,7 +242,7 @@ class Create extends Action
$type = $request->getHeader('x-sdk-language') === 'cli' ? 'cli' : 'manual';
try {
- $stateLock->withLock(function () use ($activate, &$chunks, $chunksUploaded, $commands, $dbForProject, $deploymentId, $deviceForFunctions, $entrypoint, $fileSize, &$function, $functionId, $path, &$metadata, $queueForBuilds, $queueForEvents, $response, $type): void {
+ $stateLock->withLock(function () use ($activate, &$chunks, $chunksUploaded, $commands, $dbForProject, $deploymentId, $deviceForFunctions, $entrypoint, $fileSize, &$function, $functionId, $path, &$metadata, $platform, $project, $publisherForBuilds, $queueForEvents, $response, $type): void {
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
$uploaded = 0;
@@ -315,10 +318,13 @@ class Create extends Action
}
// Start the build
- $queueForBuilds
- ->setType(BUILD_TYPE_DEPLOYMENT)
- ->setResource($function)
- ->setDeployment($deployment);
+ $publisherForBuilds->enqueue(new BuildMessage(
+ project: $project,
+ resource: $function,
+ deployment: $deployment,
+ type: BUILD_TYPE_DEPLOYMENT,
+ platform: $platform,
+ ));
} else {
if ($deployment->isEmpty()) {
$deployment = $dbForProject->createDocument('deployments', new Document([
diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Delete.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Delete.php
index 3d75919eb8..be4437ffe3 100644
--- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Delete.php
+++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Delete.php
@@ -2,8 +2,9 @@
namespace Appwrite\Platform\Modules\Functions\Http\Deployments;
-use Appwrite\Event\Delete as DeleteEvent;
use Appwrite\Event\Event;
+use Appwrite\Event\Message\Delete as DeleteMessage;
+use Appwrite\Event\Publisher\Delete as DeletePublisher;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@@ -59,7 +60,7 @@ class Delete extends Action
->param('deploymentId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Deployment ID.', false, ['dbForProject'])
->inject('response')
->inject('dbForProject')
- ->inject('queueForDeletes')
+ ->inject('publisherForDeletes')
->inject('queueForEvents')
->inject('deviceForFunctions')
->callback($this->action(...));
@@ -70,7 +71,7 @@ class Delete extends Action
string $deploymentId,
Response $response,
Database $dbForProject,
- DeleteEvent $queueForDeletes,
+ DeletePublisher $publisherForDeletes,
Event $queueForEvents,
Device $deviceForFunctions
) {
@@ -128,9 +129,11 @@ class Delete extends Action
->setParam('functionId', $function->getId())
->setParam('deploymentId', $deployment->getId());
- $queueForDeletes
- ->setType(DELETE_TYPE_DOCUMENT)
- ->setDocument($deployment);
+ $publisherForDeletes->enqueue(new DeleteMessage(
+ project: $queueForEvents->getProject(),
+ type: DELETE_TYPE_DOCUMENT,
+ document: $deployment,
+ ));
$response->noContent();
}
diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Duplicate/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Duplicate/Create.php
index 9884b12dba..76070c8bf5 100644
--- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Duplicate/Create.php
+++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Duplicate/Create.php
@@ -2,8 +2,9 @@
namespace Appwrite\Platform\Modules\Functions\Http\Deployments\Duplicate;
-use Appwrite\Event\Build;
use Appwrite\Event\Event;
+use Appwrite\Event\Message\Build as BuildMessage;
+use Appwrite\Event\Publisher\Build as BuildPublisher;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
@@ -61,8 +62,10 @@ class Create extends Action
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
- ->inject('queueForBuilds')
+ ->inject('publisherForBuilds')
->inject('deviceForFunctions')
+ ->inject('project')
+ ->inject('platform')
->callback($this->action(...));
}
@@ -73,8 +76,10 @@ class Create extends Action
Response $response,
Database $dbForProject,
Event $queueForEvents,
- Build $queueForBuilds,
- Device $deviceForFunctions
+ BuildPublisher $publisherForBuilds,
+ Device $deviceForFunctions,
+ Document $project,
+ array $platform
) {
$function = $dbForProject->getDocument('functions', $functionId);
@@ -127,10 +132,13 @@ class Create extends Action
'latestDeploymentStatus' => $function->getAttribute('latestDeploymentStatus'),
]));
- $queueForBuilds
- ->setType(BUILD_TYPE_DEPLOYMENT)
- ->setResource($function)
- ->setDeployment($deployment);
+ $publisherForBuilds->enqueue(new BuildMessage(
+ project: $project,
+ resource: $function,
+ deployment: $deployment,
+ type: BUILD_TYPE_DEPLOYMENT,
+ platform: $platform,
+ ));
$queueForEvents
->setParam('functionId', $function->getId())
diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php
index 53af82e701..f18543c60e 100644
--- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php
+++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php
@@ -2,8 +2,9 @@
namespace Appwrite\Platform\Modules\Functions\Http\Deployments\Template;
-use Appwrite\Event\Build;
use Appwrite\Event\Event;
+use Appwrite\Event\Message\Build as BuildMessage;
+use Appwrite\Event\Publisher\Build as BuildPublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
@@ -76,9 +77,10 @@ class Create extends Base
->inject('dbForPlatform')
->inject('queueForEvents')
->inject('project')
- ->inject('queueForBuilds')
+ ->inject('publisherForBuilds')
->inject('gitHub')
->inject('authorization')
+ ->inject('platform')
->callback($this->action(...));
}
@@ -96,9 +98,10 @@ class Create extends Base
Database $dbForPlatform,
Event $queueForEvents,
Document $project,
- Build $queueForBuilds,
+ BuildPublisher $publisherForBuilds,
GitHub $github,
- Authorization $authorization
+ Authorization $authorization,
+ array $platform
) {
$function = $dbForProject->getDocument('functions', $functionId);
@@ -127,10 +130,11 @@ class Create extends Base
project: $project,
installation: $installation,
dbForProject: $dbForProject,
- queueForBuilds: $queueForBuilds,
+ publisherForBuilds: $publisherForBuilds,
template: $template,
github: $github,
activate: $activate,
+ platform: $platform,
referenceType: $type,
reference: $reference
);
@@ -184,11 +188,14 @@ class Create extends Base
$this->updateEmptyManualRule($project, $function, $deployment, $dbForPlatform, $authorization);
- $queueForBuilds
- ->setType(BUILD_TYPE_DEPLOYMENT)
- ->setResource($function)
- ->setDeployment($deployment)
- ->setTemplate($template);
+ $publisherForBuilds->enqueue(new BuildMessage(
+ project: $project,
+ resource: $function,
+ deployment: $deployment,
+ type: BUILD_TYPE_DEPLOYMENT,
+ template: $template,
+ platform: $platform,
+ ));
$queueForEvents
->setParam('functionId', $function->getId())
diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Vcs/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Vcs/Create.php
index 587c09beba..a74fc12593 100644
--- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Vcs/Create.php
+++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Vcs/Create.php
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Functions\Http\Deployments\Vcs;
-use Appwrite\Event\Build;
use Appwrite\Event\Event;
+use Appwrite\Event\Publisher\Build as BuildPublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
@@ -70,8 +70,9 @@ class Create extends Base
->inject('dbForPlatform')
->inject('project')
->inject('queueForEvents')
- ->inject('queueForBuilds')
+ ->inject('publisherForBuilds')
->inject('gitHub')
+ ->inject('platform')
->callback($this->action(...));
}
@@ -86,8 +87,9 @@ class Create extends Base
Database $dbForPlatform,
Document $project,
Event $queueForEvents,
- Build $queueForBuilds,
+ BuildPublisher $publisherForBuilds,
GitHub $github,
+ array $platform,
) {
$function = $dbForProject->getDocument('functions', $functionId);
@@ -105,10 +107,11 @@ class Create extends Base
project: $project,
installation: $installation,
dbForProject: $dbForProject,
- queueForBuilds: $queueForBuilds,
+ publisherForBuilds: $publisherForBuilds,
template: $template,
github: $github,
activate: $activate,
+ platform: $platform,
reference: $reference,
referenceType: $type
);
diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php
index 9f15cf9d1e..35264730f8 100644
--- a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php
+++ b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php
@@ -3,9 +3,11 @@
namespace Appwrite\Platform\Modules\Functions\Http\Executions;
use Ahc\Jwt\JWT;
-use Appwrite\Event\Delete as DeleteEvent;
use Appwrite\Event\Event;
-use Appwrite\Event\Func;
+use Appwrite\Event\Message\Delete as DeleteMessage;
+use Appwrite\Event\Message\Func as FunctionMessage;
+use Appwrite\Event\Publisher\Delete as DeletePublisher;
+use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Extend\Exception;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Functions\Validator\Headers;
@@ -17,6 +19,7 @@ use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Usage\Context;
use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Response;
+use Executor\Exception\Timeout as ExecutorTimeout;
use Executor\Executor;
use MaxMind\Db\Reader;
use Utopia\Auth\Proofs\Token;
@@ -94,14 +97,14 @@ class Create extends Base
->inject('user')
->inject('queueForEvents')
->inject('usage')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('geodb')
->inject('store')
->inject('proofForToken')
->inject('executor')
->inject('platform')
->inject('authorization')
- ->inject('queueForDeletes')
+ ->inject('publisherForDeletes')
->inject('executionsRetentionCount')
->callback($this->action(...));
}
@@ -122,14 +125,14 @@ class Create extends Base
User $user,
Event $queueForEvents,
Context $usage,
- Func $queueForFunctions,
+ FunctionPublisher $publisherForFunctions,
Reader $geodb,
Store $store,
Token $proofForToken,
Executor $executor,
array $platform,
Authorization $authorization,
- DeleteEvent $queueForDeletes,
+ DeletePublisher $publisherForDeletes,
int $executionsRetentionCount,
) {
$async = \strval($async) === 'true' || \strval($async) === '1';
@@ -293,20 +296,19 @@ class Create extends Base
if ($async) {
if (is_null($scheduledAt)) {
$execution = $authorization->skip(fn () => $dbForProject->createDocument('executions', $execution));
- $queueForFunctions
- ->setType('http')
- ->setExecution($execution)
- ->setFunction($function)
- ->setBody($body)
- ->setHeaders($headers)
- ->setPath($path)
- ->setMethod($method)
- ->setJWT($jwt)
- ->setProject($project)
- ->setUser($user)
- ->setParam('functionId', $function->getId())
- ->setParam('executionId', $execution->getId())
- ->trigger();
+ $publisherForFunctions->enqueue(new FunctionMessage(
+ project: $project,
+ user: $user,
+ function: $function,
+ functionId: $function->getId(),
+ execution: $execution,
+ type: 'http',
+ jwt: $jwt,
+ body: $body,
+ path: $path,
+ headers: $headers,
+ method: $method,
+ ));
} else {
$data = [
'headers' => $headers,
@@ -337,12 +339,12 @@ class Create extends Base
}
if ($executionsRetentionCount > 0 && ENABLE_EXECUTIONS_LIMIT_ON_ROUTE) {
- $queueForDeletes
- ->setProject($project)
- ->setResource($function->getSequence())
- ->setResourceType(RESOURCE_TYPE_FUNCTIONS)
- ->setType(DELETE_TYPE_EXECUTIONS_LIMIT)
- ->trigger();
+ $publisherForDeletes->enqueue(new DeleteMessage(
+ project: $project,
+ type: DELETE_TYPE_EXECUTIONS_LIMIT,
+ resource: (string) $function->getSequence(),
+ resourceType: RESOURCE_TYPE_FUNCTIONS,
+ ));
}
$response->setStatusCode(Response::STATUS_CODE_ACCEPTED);
@@ -417,25 +419,29 @@ class Create extends Base
$source = $deployment->getAttribute('buildPath', '');
$extension = str_ends_with($source, '.tar') ? 'tar' : 'tar.gz';
$command = $version === 'v2' ? '' : "cp /tmp/code.$extension /mnt/code/code.$extension && nohup helpers/start.sh \"$command\"";
- $executionResponse = $executor->createExecution(
- projectId: $project->getId(),
- deploymentId: $deployment->getId(),
- body: \strlen($body) > 0 ? $body : null,
- variables: $vars,
- timeout: $function->getAttribute('timeout', 0),
- image: $runtime['image'],
- source: $source,
- entrypoint: $deployment->getAttribute('entrypoint', ''),
- version: $version,
- path: $path,
- method: $method,
- headers: $headers,
- runtimeEntrypoint: $command,
- cpus: $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT,
- memory: $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT,
- logging: $function->getAttribute('logging', true),
- requestTimeout: 30
- );
+ try {
+ $executionResponse = $executor->createExecution(
+ projectId: $project->getId(),
+ deploymentId: $deployment->getId(),
+ body: \strlen($body) > 0 ? $body : null,
+ variables: $vars,
+ timeout: $function->getAttribute('timeout', 0),
+ image: $runtime['image'],
+ source: $source,
+ entrypoint: $deployment->getAttribute('entrypoint', ''),
+ version: $version,
+ path: $path,
+ method: $method,
+ headers: $headers,
+ runtimeEntrypoint: $command,
+ cpus: $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT,
+ memory: $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT,
+ logging: $function->getAttribute('logging', true),
+ requestTimeout: 30
+ );
+ } catch (ExecutorTimeout $th) {
+ throw new AppwriteException(AppwriteException::FUNCTION_SYNCHRONOUS_TIMEOUT, previous: $th);
+ }
$headersFiltered = [];
foreach ($executionResponse['headers'] as $key => $value) {
@@ -524,12 +530,12 @@ class Create extends Base
}
if ($executionsRetentionCount > 0 && ENABLE_EXECUTIONS_LIMIT_ON_ROUTE) {
- $queueForDeletes
- ->setProject($project)
- ->setResource($function->getSequence())
- ->setResourceType(RESOURCE_TYPE_FUNCTIONS)
- ->setType(DELETE_TYPE_EXECUTIONS_LIMIT)
- ->trigger();
+ $publisherForDeletes->enqueue(new DeleteMessage(
+ project: $project,
+ type: DELETE_TYPE_EXECUTIONS_LIMIT,
+ resource: (string) $function->getSequence(),
+ resourceType: RESOURCE_TYPE_FUNCTIONS,
+ ));
}
$response
diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php
index 7b294f3f90..148f0945ac 100644
--- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php
+++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php
@@ -2,9 +2,11 @@
namespace Appwrite\Platform\Modules\Functions\Http\Functions;
-use Appwrite\Event\Build;
use Appwrite\Event\Event;
-use Appwrite\Event\Func;
+use Appwrite\Event\Message\Build as BuildMessage;
+use Appwrite\Event\Message\Func as FunctionMessage;
+use Appwrite\Event\Publisher\Build as BuildPublisher;
+use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Event\Realtime;
use Appwrite\Event\Validator\FunctionEvent;
use Appwrite\Event\Webhook;
@@ -115,10 +117,10 @@ class Create extends Base
->inject('timelimit')
->inject('project')
->inject('queueForEvents')
- ->inject('queueForBuilds')
+ ->inject('publisherForBuilds')
->inject('queueForRealtime')
->inject('queueForWebhooks')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('dbForPlatform')
->inject('request')
->inject('gitHub')
@@ -157,10 +159,10 @@ class Create extends Base
callable $timelimit,
Document $project,
Event $queueForEvents,
- Build $queueForBuilds,
+ BuildPublisher $publisherForBuilds,
Realtime $queueForRealtime,
Webhook $queueForWebhooks,
- Func $queueForFunctions,
+ FunctionPublisher $publisherForFunctions,
Database $dbForPlatform,
Request $request,
GitHub $github,
@@ -326,10 +328,11 @@ class Create extends Base
project: $project,
installation: $installation,
dbForProject: $dbForProject,
- queueForBuilds: $queueForBuilds,
+ publisherForBuilds: $publisherForBuilds,
template: $template,
github: $github,
activate: true,
+ platform: $platform,
reference: $providerBranch,
referenceType: 'branch'
);
@@ -367,11 +370,14 @@ class Create extends Base
'latestDeploymentStatus' => $deployment->getAttribute('status', ''),
]));
- $queueForBuilds
- ->setType(BUILD_TYPE_DEPLOYMENT)
- ->setResource($function)
- ->setDeployment($deployment)
- ->setTemplate($template);
+ $publisherForBuilds->enqueue(new BuildMessage(
+ project: $project,
+ resource: $function,
+ deployment: $deployment,
+ type: BUILD_TYPE_DEPLOYMENT,
+ template: $template,
+ platform: $platform,
+ ));
}
$functionsDomain = $platform['functionsDomain'];
@@ -418,9 +424,15 @@ class Create extends Base
->trigger();
/** Trigger Functions */
- $queueForFunctions
- ->from($ruleCreate)
- ->trigger();
+ $publisherForFunctions->enqueue(FunctionMessage::fromEvent(
+ event: $ruleCreate->getEvent(),
+ params: $ruleCreate->getParams(),
+ project: $ruleCreate->getProject(),
+ user: $ruleCreate->getUser(),
+ userId: $ruleCreate->getUserId(),
+ payload: $ruleCreate->getPayload(),
+ platform: $ruleCreate->getPlatform(),
+ ));
/** Trigger Realtime Events */
$queueForRealtime
diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Delete.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Delete.php
index fb45cee82f..1517ee7793 100644
--- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Delete.php
+++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Delete.php
@@ -2,8 +2,9 @@
namespace Appwrite\Platform\Modules\Functions\Http\Functions;
-use Appwrite\Event\Delete as DeleteEvent;
use Appwrite\Event\Event;
+use Appwrite\Event\Message\Delete as DeleteMessage;
+use Appwrite\Event\Publisher\Delete as DeletePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
@@ -59,7 +60,7 @@ class Delete extends Base
->param('functionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Function ID.', false, ['dbForProject'])
->inject('response')
->inject('dbForProject')
- ->inject('queueForDeletes')
+ ->inject('publisherForDeletes')
->inject('queueForEvents')
->inject('dbForPlatform')
->inject('authorization')
@@ -70,7 +71,7 @@ class Delete extends Base
string $functionId,
Response $response,
Database $dbForProject,
- DeleteEvent $queueForDeletes,
+ DeletePublisher $publisherForDeletes,
Event $queueForEvents,
Database $dbForPlatform,
Authorization $authorization
@@ -97,9 +98,11 @@ class Delete extends Base
])));
}
- $queueForDeletes
- ->setType(DELETE_TYPE_DOCUMENT)
- ->setDocument($function);
+ $publisherForDeletes->enqueue(new DeleteMessage(
+ project: $queueForEvents->getProject(),
+ type: DELETE_TYPE_DOCUMENT,
+ document: $function,
+ ));
$queueForEvents->setParam('functionId', $function->getId());
diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php
index 7d6572d336..e8713a179d 100644
--- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php
+++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Update.php
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Functions\Http\Functions;
-use Appwrite\Event\Build;
use Appwrite\Event\Event;
+use Appwrite\Event\Publisher\Build as BuildPublisher;
use Appwrite\Event\Validator\FunctionEvent;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
@@ -105,11 +105,12 @@ class Update extends Base
->inject('dbForProject')
->inject('project')
->inject('queueForEvents')
- ->inject('queueForBuilds')
+ ->inject('publisherForBuilds')
->inject('dbForPlatform')
->inject('gitHub')
->inject('executor')
->inject('authorization')
+ ->inject('platform')
->callback($this->action(...));
}
@@ -139,11 +140,12 @@ class Update extends Base
Database $dbForProject,
Document $project,
Event $queueForEvents,
- Build $queueForBuilds,
+ BuildPublisher $publisherForBuilds,
Database $dbForPlatform,
GitHub $github,
Executor $executor,
- Authorization $authorization
+ Authorization $authorization,
+ array $platform
) {
// TODO: If only branch changes, re-deploy
$function = $dbForProject->getDocument('functions', $functionId);
@@ -281,11 +283,33 @@ class Update extends Base
// Redeploy logic
if (!$isConnected && !empty($providerRepositoryId)) {
- $this->redeployVcsFunction($request, $function, $project, $installation, $dbForProject, $queueForBuilds, new Document(), $github, true);
+ $this->redeployVcsFunction($request, $function, $project, $installation, $dbForProject, $publisherForBuilds, new Document(), $github, true, $platform);
}
// Inform scheduler if function is still active
- $schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId'));
+ $schedule = $authorization->skip(fn () => $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId')));
+
+ // Re-create schedule if missing
+ if ($schedule->isEmpty()) {
+ $schedule = $authorization->skip(
+ fn () => $dbForPlatform->createDocument('schedules', new Document([
+ 'region' => $project->getAttribute('region'),
+ 'resourceType' => SCHEDULE_RESOURCE_TYPE_FUNCTION,
+ 'resourceId' => $function->getId(),
+ 'resourceInternalId' => $function->getSequence(),
+ 'resourceUpdatedAt' => DateTime::now(),
+ 'projectId' => $project->getId(),
+ 'schedule' => $function->getAttribute('schedule'),
+ 'active' => false,
+ ]))
+ );
+
+ $function = $dbForProject->updateDocument('functions', $function->getId(), new Document([
+ 'scheduleId' => $schedule->getId(),
+ 'scheduleInternalId' => $schedule->getSequence(),
+ ]));
+ }
+
$schedule
->setAttribute('resourceUpdatedAt', DateTime::now())
->setAttribute('schedule', $function->getAttribute('schedule'))
diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Variables/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Create.php
index fee5b0095d..de572cd41e 100644
--- a/src/Appwrite/Platform/Modules/Functions/Http/Variables/Create.php
+++ b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Create.php
@@ -2,11 +2,13 @@
namespace Appwrite\Platform\Modules\Functions\Http\Variables;
+use Appwrite\Event\Event as QueueEvent;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
+use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
@@ -38,6 +40,7 @@ class Create extends Base
->groups(['api', 'functions'])
->label('scope', 'functions.write')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
+ ->label('event', 'variables.[variableId].create')
->label('audits.event', 'variable.create')
->label('audits.resource', 'function/{request.functionId}')
->label('sdk', new Method(
@@ -56,10 +59,12 @@ class Create extends Base
]
))
->param('functionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Function unique ID.', false, ['dbForProject'])
+ ->param('variableId', '', fn (Database $dbForProject) => new CustomId(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Variable ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForProject'])
->param('key', null, new Text(Database::LENGTH_KEY), 'Variable key. Max length: ' . Database::LENGTH_KEY . ' chars.', false)
->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', false)
->param('secret', true, new Boolean(), 'Secret variables can be updated or deleted, but only functions can read them during build and runtime.', true)
->inject('response')
+ ->inject('queueForEvents')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('project')
@@ -69,10 +74,12 @@ class Create extends Base
public function action(
string $functionId,
+ string $variableId,
string $key,
string $value,
bool $secret,
Response $response,
+ QueueEvent $queueForEvents,
Database $dbForProject,
Database $dbForPlatform,
Document $project,
@@ -84,7 +91,7 @@ class Create extends Base
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
- $variableId = ID::unique();
+ $variableId = ($variableId === 'unique()') ? ID::unique() : $variableId;
$teamId = $project->getAttribute('teamId', '');
$variable = new Document([
@@ -120,6 +127,8 @@ class Create extends Base
'active' => $schedule->getAttribute('active'),
])));
+ $queueForEvents->setParam('variableId', $variable->getId());
+
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($variable, Response::MODEL_VARIABLE);
diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Variables/Delete.php b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Delete.php
index f6d77c2a0d..fa9f19ba8f 100644
--- a/src/Appwrite/Platform/Modules/Functions/Http/Variables/Delete.php
+++ b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Delete.php
@@ -2,6 +2,7 @@
namespace Appwrite\Platform\Modules\Functions\Http\Variables;
+use Appwrite\Event\Event as QueueEvent;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
@@ -35,6 +36,7 @@ class Delete extends Base
->groups(['api', 'functions'])
->label('scope', 'functions.write')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
+ ->label('event', 'variables.[variableId].delete')
->label('audits.event', 'variable.delete')
->label('audits.resource', 'function/{request.functionId}')
->label('sdk', new Method(
@@ -56,6 +58,7 @@ class Delete extends Base
->param('functionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Function unique ID.', false, ['dbForProject'])
->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID.', false, ['dbForProject'])
->inject('response')
+ ->inject('queueForEvents')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('authorization')
@@ -66,6 +69,7 @@ class Delete extends Base
string $functionId,
string $variableId,
Response $response,
+ QueueEvent $queueForEvents,
Database $dbForProject,
Database $dbForPlatform,
Authorization $authorization
@@ -98,6 +102,8 @@ class Delete extends Base
'active' => $schedule->getAttribute('active'),
])));
+ $queueForEvents->setParam('variableId', $variable->getId());
+
$response->noContent();
}
}
diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Variables/Update.php b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Update.php
index 54d7a647a3..6413b29f82 100644
--- a/src/Appwrite/Platform/Modules/Functions/Http/Variables/Update.php
+++ b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Update.php
@@ -2,6 +2,7 @@
namespace Appwrite\Platform\Modules\Functions\Http\Variables;
+use Appwrite\Event\Event as QueueEvent;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
@@ -38,6 +39,7 @@ class Update extends Base
->groups(['api', 'functions'])
->label('scope', 'functions.write')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
+ ->label('event', 'variables.[variableId].update')
->label('audits.event', 'variable.update')
->label('audits.resource', 'function/{request.functionId}')
->label('sdk', new Method(
@@ -57,10 +59,11 @@ class Update extends Base
))
->param('functionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Function unique ID.', false, ['dbForProject'])
->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID.', false, ['dbForProject'])
- ->param('key', null, new Text(255), 'Variable key. Max length: 255 chars.', false)
+ ->param('key', null, new Nullable(new Text(255, 0)), 'Variable key. Max length: 255 chars.', true)
->param('value', null, new Nullable(new Text(8192, 0)), 'Variable value. Max length: 8192 chars.', true)
->param('secret', null, new Nullable(new Boolean()), 'Secret variables can be updated or deleted, but only functions can read them during build and runtime.', true)
->inject('response')
+ ->inject('queueForEvents')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('authorization')
@@ -70,10 +73,11 @@ class Update extends Base
public function action(
string $functionId,
string $variableId,
- string $key,
+ ?string $key,
?string $value,
?bool $secret,
Response $response,
+ QueueEvent $queueForEvents,
Database $dbForProject,
Database $dbForPlatform,
Authorization $authorization
@@ -93,19 +97,27 @@ class Update extends Base
throw new Exception(Exception::VARIABLE_CANNOT_UNSET_SECRET);
}
- $variable
- ->setAttribute('key', $key)
- ->setAttribute('value', $value ?? $variable->getAttribute('value'))
- ->setAttribute('secret', $secret ?? $variable->getAttribute('secret'))
- ->setAttribute('search', implode(' ', [$variableId, $function->getId(), $key, 'function']));
+ if (\is_null($key) && \is_null($value) && \is_null($secret)) {
+ throw new Exception(Exception::GENERAL_ARGUMENT_INVALID);
+ }
+
+ $updates = new Document();
+
+ if (!\is_null($key)) {
+ $updates->setAttribute('key', $key);
+ $updates->setAttribute('search', implode(' ', [$variableId, $function->getId(), $key, 'function']));
+ }
+
+ if (!\is_null($value)) {
+ $updates->setAttribute('value', $value);
+ }
+
+ if (!\is_null($secret)) {
+ $updates->setAttribute('secret', $secret);
+ }
try {
- $dbForProject->updateDocument('variables', $variable->getId(), new Document([
- 'key' => $key,
- 'value' => $value ?? $variable->getAttribute('value'),
- 'secret' => $secret ?? $variable->getAttribute('secret'),
- 'search' => implode(' ', [$variableId, $function->getId(), $key, 'function']),
- ]));
+ $variable = $dbForProject->updateDocument('variables', $variable->getId(), $updates);
} catch (DuplicateException $th) {
throw new Exception(Exception::VARIABLE_ALREADY_EXISTS);
}
@@ -125,6 +137,8 @@ class Update extends Base
'active' => $schedule->getAttribute('active'),
])));
+ $queueForEvents->setParam('variableId', $variable->getId());
+
$response->dynamic($variable, Response::MODEL_VARIABLE);
}
}
diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Variables/XList.php b/src/Appwrite/Platform/Modules/Functions/Http/Variables/XList.php
index 55dea3be1e..b330812b96 100644
--- a/src/Appwrite/Platform/Modules/Functions/Http/Variables/XList.php
+++ b/src/Appwrite/Platform/Modules/Functions/Http/Variables/XList.php
@@ -7,12 +7,18 @@ use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
+use Appwrite\Utopia\Database\Validator\Queries\Variables;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
+use Utopia\Database\Exception\Order as OrderException;
+use Utopia\Database\Exception\Query as QueryException;
+use Utopia\Database\Query;
+use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
+use Utopia\Validator\Boolean;
class XList extends Base
{
@@ -51,22 +57,74 @@ class XList extends Base
)
)
->param('functionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Function unique ID.', false, ['dbForProject'])
+ ->param('queries', [], new Variables(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Variables::ALLOWED_ATTRIBUTES), true)
+ ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('response')
->inject('dbForProject')
->callback($this->action(...));
}
- public function action(string $functionId, Response $response, Database $dbForProject)
- {
+ /**
+ * @param array $queries
+ */
+ public function action(
+ string $functionId,
+ array $queries,
+ bool $includeTotal,
+ Response $response,
+ Database $dbForProject
+ ) {
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
+ try {
+ $queries = Query::parseQueries($queries);
+ } catch (QueryException $e) {
+ throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
+ }
+
+ $queries[] = Query::equal('resourceType', ['function']);
+ $queries[] = Query::equal('resourceInternalId', [$function->getSequence()]);
+ $queries[] = Query::orderAsc();
+
+ $cursor = Query::getCursorQueries($queries, false);
+ $cursor = \reset($cursor);
+
+ if ($cursor !== false) {
+ $validator = new Cursor();
+ if (!$validator->isValid($cursor)) {
+ throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
+ }
+
+ $variableId = $cursor->getValue();
+ $cursorDocument = $dbForProject->findOne('variables', [
+ Query::equal('$id', [$variableId]),
+ Query::equal('resourceType', ['function']),
+ Query::equal('resourceInternalId', [$function->getSequence()]),
+ ]);
+
+ if ($cursorDocument->isEmpty()) {
+ throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Variable '{$variableId}' for the 'cursor' value not found.");
+ }
+
+ $cursor->setValue($cursorDocument);
+ }
+
+ $filterQueries = Query::groupByType($queries)['filters'];
+
+ try {
+ $variables = $dbForProject->find('variables', $queries);
+ $total = $includeTotal ? $dbForProject->count('variables', $filterQueries, APP_LIMIT_COUNT) : 0;
+ } catch (OrderException $e) {
+ throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
+ }
+
$response->dynamic(new Document([
- 'variables' => $function->getAttribute('vars', []),
- 'total' => \count($function->getAttribute('vars', [])),
+ 'variables' => $variables,
+ 'total' => $total,
]), Response::MODEL_VARIABLE_LIST);
}
}
diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php
index 352fb56e28..5aa95d3bf2 100644
--- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php
+++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php
@@ -4,17 +4,20 @@ namespace Appwrite\Platform\Modules\Functions\Workers;
use Ahc\Jwt\JWT;
use Appwrite\Event\Event;
-use Appwrite\Event\Func;
+use Appwrite\Event\Message\Func as FunctionMessage;
use Appwrite\Event\Message\Usage as UsageMessage;
+use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Event\Publisher\Screenshot;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Event\Realtime;
use Appwrite\Event\Webhook;
+use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Filter\BranchDomain as BranchDomainFilter;
use Appwrite\Usage\Context;
use Appwrite\Utopia\Response\Model\Deployment;
use Appwrite\Vcs\Comment;
use Exception;
+use Executor\Exception\Timeout as ExecutorTimeout;
use Executor\Executor;
use Swoole\Coroutine as Co;
use Utopia\Cache\Cache;
@@ -34,6 +37,7 @@ use Utopia\Detector\Detector\Rendering;
use Utopia\Logger\Log;
use Utopia\Platform\Action;
use Utopia\Queue\Message;
+use Utopia\Span\Span;
use Utopia\Storage\Device;
use Utopia\Storage\Device\Local;
use Utopia\System\System;
@@ -60,7 +64,7 @@ class Builds extends Action
->inject('queueForEvents')
->inject('publisherForScreenshots')
->inject('queueForWebhooks')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('queueForRealtime')
->inject('usage')
->inject('publisherForUsage')
@@ -86,7 +90,7 @@ class Builds extends Action
Event $queueForEvents,
Screenshot $publisherForScreenshots,
Webhook $queueForWebhooks,
- Func $queueForFunctions,
+ FunctionPublisher $publisherForFunctions,
Realtime $queueForRealtime,
Context $usage,
UsagePublisher $publisherForUsage,
@@ -128,7 +132,7 @@ class Builds extends Action
$deviceForFiles,
$publisherForScreenshots,
$queueForWebhooks,
- $queueForFunctions,
+ $publisherForFunctions,
$queueForRealtime,
$queueForEvents,
$usage,
@@ -164,7 +168,7 @@ class Builds extends Action
Device $deviceForFiles,
Screenshot $publisherForScreenshots,
Webhook $queueForWebhooks,
- Func $queueForFunctions,
+ FunctionPublisher $publisherForFunctions,
Realtime $queueForRealtime,
Event $queueForEvents,
Context $usage,
@@ -183,6 +187,12 @@ class Builds extends Action
array $platform,
int $timeout
): void {
+ Span::add('project.id', $project->getId());
+ Span::add('resource.id', $resource->getId());
+ Span::add('resource.type', $resource->getCollection());
+ Span::add('deployment.id', $deployment->getId());
+ Span::add('build.timeout', $timeout);
+
Console::info('Deployment action started');
$startTime = DateTime::now();
@@ -223,8 +233,12 @@ class Builds extends Action
$version = $this->getVersion($resource);
$runtime = $this->getRuntime($resource, $version);
+ Span::add('build.runtime', $resource->getAttribute($resource->getCollection() === 'sites' ? 'buildRuntime' : 'runtime', ''));
+ Span::add('build.version', $version);
$spec = Config::getParam('specifications')[$resource->getAttribute('buildSpecification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
+ Span::add('build.cpus', (float) ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT));
+ Span::add('build.memory', (int) ($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT));
// Realtime preparation
$event = "{$resource->getCollection()}.[{$resourceKey}].deployments.[deploymentId].update";
@@ -557,9 +571,15 @@ class Builds extends Action
->trigger();
/** Trigger Functions */
- $queueForFunctions
- ->from($deploymentUpdate)
- ->trigger();
+ $publisherForFunctions->enqueue(FunctionMessage::fromEvent(
+ event: $deploymentUpdate->getEvent(),
+ params: $deploymentUpdate->getParams(),
+ project: $deploymentUpdate->getProject(),
+ user: $deploymentUpdate->getUser(),
+ userId: $deploymentUpdate->getUserId(),
+ payload: $deploymentUpdate->getPayload(),
+ platform: $deploymentUpdate->getPlatform(),
+ ));
/** Trigger Realtime Event */
$queueForRealtime
@@ -720,6 +740,9 @@ class Builds extends Action
);
Console::log('createRuntime finished');
+ } catch (ExecutorTimeout $error) {
+ Console::warning('createRuntime timed out');
+ $err = new AppwriteException(AppwriteException::BUILD_TIMEOUT, previous: $error);
} catch (\Throwable $error) {
Console::warning('createRuntime failed');
$err = $error;
@@ -1147,13 +1170,11 @@ class Builds extends Action
$message = \str_replace('{APPWRITE_DETECTION_SEPARATOR_START}', '', $message);
$message = \str_replace('{APPWRITE_DETECTION_SEPARATOR_END}', '', $message);
- // Combine with previous logs if deployment got past build process
- $previousLogs = '';
- if (! is_null($deployment->getAttribute('buildSize', null))) {
- $previousLogs = $deployment->getAttribute('buildLogs', '');
- if (! empty($previousLogs)) {
- $message = $previousLogs . "\n" . $message;
- }
+ // Append error to whatever build logs were already streamed
+ $deployment = $dbForProject->getDocument('deployments', $deploymentId);
+ $previousLogs = $deployment->getAttribute('buildLogs', '');
+ if (! empty($previousLogs)) {
+ $message = $previousLogs . "\n" . $message;
}
$endTime = DateTime::now();
diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Screenshots.php b/src/Appwrite/Platform/Modules/Functions/Workers/Screenshots.php
index 7d1cdc4980..c766f73929 100644
--- a/src/Appwrite/Platform/Modules/Functions/Workers/Screenshots.php
+++ b/src/Appwrite/Platform/Modules/Functions/Workers/Screenshots.php
@@ -109,9 +109,7 @@ class Screenshots extends Action
throw new \Exception("Rule for deployment not found");
}
- $client = new FetchClient();
- $client->setTimeout(\intval($site->getAttribute('timeout', '15')) * 1000);
- $client->addHeader('content-type', FetchClient::CONTENT_TYPE_APPLICATION_JSON);
+ $timeout = \intval($site->getAttribute('timeout', '15')) * 1000;
$bucket = $dbForPlatform->getDocument('buckets', 'screenshots');
@@ -162,8 +160,8 @@ class Screenshots extends Action
]);
$screenshotError = null;
- $screenshots = batch(\array_map(function ($key) use ($configs, $apiKey, $site, $client, &$screenshotError) {
- return function () use ($key, $configs, $apiKey, $site, $client, &$screenshotError) {
+ $screenshots = batch(\array_map(function ($key) use ($configs, $apiKey, $site, $timeout, &$screenshotError) {
+ return function () use ($key, $configs, $apiKey, $site, $timeout, &$screenshotError) {
try {
$config = $configs[$key];
@@ -179,6 +177,10 @@ class Screenshots extends Action
}
$browserEndpoint = System::getEnv('_APP_BROWSER_HOST', 'http://appwrite-browser:3000/v1');
+ $client = new FetchClient();
+ $client->setTimeout($timeout);
+ $client->addHeader('content-type', FetchClient::CONTENT_TYPE_APPLICATION_JSON);
+
$fetchResponse = $client->fetch(
url: $browserEndpoint . '/screenshots',
method: 'POST',
diff --git a/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Builds/Get.php b/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Builds/Get.php
index 8ae7c8687a..98e65e37e5 100644
--- a/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Builds/Get.php
+++ b/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Builds/Get.php
@@ -2,7 +2,7 @@
namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\Builds;
-use Appwrite\Event\Build;
+use Appwrite\Event\Publisher\Build as BuildPublisher;
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@@ -42,16 +42,16 @@ class Get extends Base
contentType: ContentType::JSON
))
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
- ->inject('queueForBuilds')
+ ->inject('publisherForBuilds')
->inject('response')
->callback($this->action(...));
}
- public function action(int|string $threshold, Build $queueForBuilds, Response $response): void
+ public function action(int|string $threshold, BuildPublisher $publisherForBuilds, Response $response): void
{
$threshold = (int) $threshold;
- $size = $queueForBuilds->getSize();
+ $size = $publisherForBuilds->getSize();
$this->assertQueueThreshold($size, $threshold);
diff --git a/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Databases/Get.php b/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Databases/Get.php
index 213bd8b36c..3bd42b64c6 100644
--- a/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Databases/Get.php
+++ b/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Databases/Get.php
@@ -2,7 +2,7 @@
namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\Databases;
-use Appwrite\Event\Database;
+use Appwrite\Event\Publisher\Database;
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@@ -10,6 +10,7 @@ use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Document;
+use Utopia\Queue\Queue;
use Utopia\Validator\Integer;
use Utopia\Validator\Text;
@@ -44,15 +45,15 @@ class Get extends Base
))
->param('name', 'database_db_main', new Text(256), 'Queue name for which to check the queue size', true)
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
- ->inject('queueForDatabase')
+ ->inject('publisherForDatabase')
->inject('response')
->callback($this->action(...));
}
- public function action(string $name, int|string $threshold, Database $queueForDatabase, Response $response): void
+ public function action(string $name, int|string $threshold, Database $publisherForDatabase, Response $response): void
{
$threshold = (int) $threshold;
- $size = $queueForDatabase->setQueue($name)->getSize();
+ $size = $publisherForDatabase->getSize(queue: new Queue($name));
$this->assertQueueThreshold($size, $threshold);
diff --git a/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Deletes/Get.php b/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Deletes/Get.php
index 816583fc47..c1bcc900e0 100644
--- a/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Deletes/Get.php
+++ b/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Deletes/Get.php
@@ -2,7 +2,7 @@
namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\Deletes;
-use Appwrite\Event\Delete;
+use Appwrite\Event\Publisher\Delete;
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@@ -42,16 +42,16 @@ class Get extends Base
contentType: ContentType::JSON
))
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
- ->inject('queueForDeletes')
+ ->inject('publisherForDeletes')
->inject('response')
->callback($this->action(...));
}
- public function action(int|string $threshold, Delete $queueForDeletes, Response $response): void
+ public function action(int|string $threshold, Delete $publisherForDeletes, Response $response): void
{
$threshold = (int) $threshold;
- $size = $queueForDeletes->getSize();
+ $size = $publisherForDeletes->getSize();
$this->assertQueueThreshold($size, $threshold);
diff --git a/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Failed/Get.php b/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Failed/Get.php
index 7602de45d3..d3b760d01b 100644
--- a/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Failed/Get.php
+++ b/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Failed/Get.php
@@ -2,15 +2,15 @@
namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\Failed;
-use Appwrite\Event\Build;
-use Appwrite\Event\Database;
-use Appwrite\Event\Delete;
use Appwrite\Event\Event;
-use Appwrite\Event\Func;
-use Appwrite\Event\Mail;
-use Appwrite\Event\Messaging;
use Appwrite\Event\Publisher\Audit;
+use Appwrite\Event\Publisher\Build as BuildPublisher;
use Appwrite\Event\Publisher\Certificate;
+use Appwrite\Event\Publisher\Database as DatabasePublisher;
+use Appwrite\Event\Publisher\Delete as DeletePublisher;
+use Appwrite\Event\Publisher\Func as FunctionPublisher;
+use Appwrite\Event\Publisher\Mail as MailPublisher;
+use Appwrite\Event\Publisher\Messaging as MessagingPublisher;
use Appwrite\Event\Publisher\Migration as MigrationPublisher;
use Appwrite\Event\Publisher\Screenshot;
use Appwrite\Event\Publisher\StatsResources as StatsResourcesPublisher;
@@ -74,17 +74,17 @@ class Get extends Base
]), 'The name of the queue')
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
->inject('response')
- ->inject('queueForDatabase')
- ->inject('queueForDeletes')
+ ->inject('publisherForDatabase')
+ ->inject('publisherForDeletes')
->inject('publisherForAudits')
- ->inject('queueForMails')
- ->inject('queueForFunctions')
+ ->inject('publisherForMails')
+ ->inject('publisherForFunctions')
->inject('publisherForStatsResources')
->inject('publisherForUsage')
->inject('queueForWebhooks')
->inject('publisherForCertificates')
- ->inject('queueForBuilds')
- ->inject('queueForMessaging')
+ ->inject('publisherForBuilds')
+ ->inject('publisherForMessaging')
->inject('publisherForMigrations')
->inject('publisherForScreenshots')
->callback($this->action(...));
@@ -94,35 +94,35 @@ class Get extends Base
string $name,
int|string $threshold,
Response $response,
- Database $queueForDatabase,
- Delete $queueForDeletes,
+ DatabasePublisher $publisherForDatabase,
+ DeletePublisher $publisherForDeletes,
Audit $publisherForAudits,
- Mail $queueForMails,
- Func $queueForFunctions,
+ MailPublisher $publisherForMails,
+ FunctionPublisher $publisherForFunctions,
StatsResourcesPublisher $publisherForStatsResources,
UsagePublisher $publisherForUsage,
Webhook $queueForWebhooks,
Certificate $publisherForCertificates,
- Build $queueForBuilds,
- Messaging $queueForMessaging,
+ BuildPublisher $publisherForBuilds,
+ MessagingPublisher $publisherForMessaging,
MigrationPublisher $publisherForMigrations,
Screenshot $publisherForScreenshots,
): void {
$threshold = (int) $threshold;
$queue = match ($name) {
- System::getEnv('_APP_DATABASE_QUEUE_NAME', Event::DATABASE_QUEUE_NAME) => $queueForDatabase,
- System::getEnv('_APP_DELETE_QUEUE_NAME', Event::DELETE_QUEUE_NAME) => $queueForDeletes,
+ System::getEnv('_APP_DATABASE_QUEUE_NAME', Event::DATABASE_QUEUE_NAME) => $publisherForDatabase,
+ System::getEnv('_APP_DELETE_QUEUE_NAME', Event::DELETE_QUEUE_NAME) => $publisherForDeletes,
System::getEnv('_APP_AUDITS_QUEUE_NAME', Event::AUDITS_QUEUE_NAME) => $publisherForAudits,
- System::getEnv('_APP_MAILS_QUEUE_NAME', Event::MAILS_QUEUE_NAME) => $queueForMails,
- System::getEnv('_APP_FUNCTIONS_QUEUE_NAME', Event::FUNCTIONS_QUEUE_NAME) => $queueForFunctions,
+ System::getEnv('_APP_MAILS_QUEUE_NAME', Event::MAILS_QUEUE_NAME) => $publisherForMails,
+ System::getEnv('_APP_FUNCTIONS_QUEUE_NAME', Event::FUNCTIONS_QUEUE_NAME) => $publisherForFunctions,
System::getEnv('_APP_STATS_RESOURCES_QUEUE_NAME', Event::STATS_RESOURCES_QUEUE_NAME) => $publisherForStatsResources,
System::getEnv('_APP_STATS_USAGE_QUEUE_NAME', Event::STATS_USAGE_QUEUE_NAME) => $publisherForUsage,
System::getEnv('_APP_WEBHOOK_QUEUE_NAME', Event::WEBHOOK_QUEUE_NAME) => $queueForWebhooks,
System::getEnv('_APP_CERTIFICATES_QUEUE_NAME', Event::CERTIFICATES_QUEUE_NAME) => $publisherForCertificates,
- System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::BUILDS_QUEUE_NAME) => $queueForBuilds,
+ System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::BUILDS_QUEUE_NAME) => $publisherForBuilds,
System::getEnv('_APP_SCREENSHOTS_QUEUE_NAME', Event::SCREENSHOTS_QUEUE_NAME) => $publisherForScreenshots,
- System::getEnv('_APP_MESSAGING_QUEUE_NAME', Event::MESSAGING_QUEUE_NAME) => $queueForMessaging,
+ System::getEnv('_APP_MESSAGING_QUEUE_NAME', Event::MESSAGING_QUEUE_NAME) => $publisherForMessaging,
System::getEnv('_APP_MIGRATIONS_QUEUE_NAME', Event::MIGRATIONS_QUEUE_NAME) => $publisherForMigrations,
default => throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Unknown queue name: ' . $name),
};
diff --git a/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Functions/Get.php b/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Functions/Get.php
index 1d10b8d1a0..29c7a7c859 100644
--- a/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Functions/Get.php
+++ b/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Functions/Get.php
@@ -2,7 +2,7 @@
namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\Functions;
-use Appwrite\Event\Func;
+use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@@ -42,16 +42,16 @@ class Get extends Base
contentType: ContentType::JSON
))
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('response')
->callback($this->action(...));
}
- public function action(int|string $threshold, Func $queueForFunctions, Response $response): void
+ public function action(int|string $threshold, FunctionPublisher $publisherForFunctions, Response $response): void
{
$threshold = (int) $threshold;
- $size = $queueForFunctions->getSize();
+ $size = $publisherForFunctions->getSize();
$this->assertQueueThreshold($size, $threshold);
diff --git a/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Mails/Get.php b/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Mails/Get.php
index 3b9c06b5f9..2dd36e8111 100644
--- a/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Mails/Get.php
+++ b/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Mails/Get.php
@@ -2,7 +2,7 @@
namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\Mails;
-use Appwrite\Event\Mail;
+use Appwrite\Event\Publisher\Mail as MailPublisher;
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@@ -42,16 +42,16 @@ class Get extends Base
contentType: ContentType::JSON
))
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
- ->inject('queueForMails')
+ ->inject('publisherForMails')
->inject('response')
->callback($this->action(...));
}
- public function action(int|string $threshold, Mail $queueForMails, Response $response): void
+ public function action(int|string $threshold, MailPublisher $publisherForMails, Response $response): void
{
$threshold = (int) $threshold;
- $size = $queueForMails->getSize();
+ $size = $publisherForMails->getSize();
$this->assertQueueThreshold($size, $threshold);
diff --git a/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Messaging/Get.php b/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Messaging/Get.php
index db2d7d7172..a2a829b1d5 100644
--- a/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Messaging/Get.php
+++ b/src/Appwrite/Platform/Modules/Health/Http/Health/Queue/Messaging/Get.php
@@ -2,7 +2,7 @@
namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\Messaging;
-use Appwrite\Event\Messaging;
+use Appwrite\Event\Publisher\Messaging as MessagingPublisher;
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@@ -42,16 +42,16 @@ class Get extends Base
contentType: ContentType::JSON
))
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
- ->inject('queueForMessaging')
+ ->inject('publisherForMessaging')
->inject('response')
->callback($this->action(...));
}
- public function action(int|string $threshold, Messaging $queueForMessaging, Response $response): void
+ public function action(int|string $threshold, MessagingPublisher $publisherForMessaging, Response $response): void
{
$threshold = (int) $threshold;
- $size = $queueForMessaging->getSize();
+ $size = $publisherForMessaging->getSize();
$this->assertQueueThreshold($size, $threshold);
diff --git a/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/Appwrite/Create.php b/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/Appwrite/Create.php
index 006ab3ae90..fa700877a1 100644
--- a/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/Appwrite/Create.php
+++ b/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/Appwrite/Create.php
@@ -13,6 +13,7 @@ use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\UID;
+use Utopia\Migration\Destinations\OnDuplicate;
use Utopia\Migration\Sources\Appwrite as AppwriteSource;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
@@ -57,6 +58,7 @@ class Create extends Action
->param('endpoint', '', new URL(), 'Source Appwrite endpoint')
->param('projectId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Source Project ID', false, ['dbForProject'])
->param('apiKey', '', new Text(512), 'Source API Key')
+ ->param('onDuplicate', OnDuplicate::Fail->value, new WhiteList(OnDuplicate::values()), 'Behavior when a row with an existing $id is encountered. "fail" (default): abort on first conflict. "skip": silently ignore. "overwrite": replace existing row.', true)
->inject('response')
->inject('dbForProject')
->inject('project')
@@ -71,6 +73,7 @@ class Create extends Action
string $endpoint,
string $projectId,
string $apiKey,
+ string $onDuplicate,
Response $response,
Database $dbForProject,
Document $project,
@@ -93,6 +96,9 @@ class Create extends Action
'statusCounters' => '{}',
'resourceData' => '{}',
'errors' => [],
+ 'options' => [
+ 'onDuplicate' => $onDuplicate,
+ ],
]));
$queueForEvents->setParam('migrationId', $migration->getId());
diff --git a/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/CSV/Imports/Create.php b/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/CSV/Imports/Create.php
index 5cc21241c3..4b47ed7d58 100644
--- a/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/CSV/Imports/Create.php
+++ b/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/CSV/Imports/Create.php
@@ -20,6 +20,7 @@ use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
+use Utopia\Migration\Destinations\OnDuplicate;
use Utopia\Migration\Resource;
use Utopia\Migration\Sources\Appwrite as AppwriteSource;
use Utopia\Migration\Sources\CSV;
@@ -29,6 +30,7 @@ use Utopia\Platform\Scope\HTTP;
use Utopia\Storage\Device;
use Utopia\System\System;
use Utopia\Validator\Boolean;
+use Utopia\Validator\WhiteList;
class Create extends Action
{
@@ -67,6 +69,7 @@ class Create extends Action
->param('fileId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'File ID.', false, ['dbForProject'])
->param('resourceId', null, new CompoundUID(), 'Composite ID in the format {databaseId:collectionId}, identifying a collection within a database.')
->param('internalFile', false, new Boolean(), 'Is the file stored in an internal bucket?', true)
+ ->param('onDuplicate', OnDuplicate::Fail->value, new WhiteList(OnDuplicate::values()), 'Behavior when a row with an existing $id is encountered. "fail" (default): abort on first conflict. "skip": silently ignore. "overwrite": replace existing row.', true)
->inject('response')
->inject('dbForProject')
->inject('dbForPlatform')
@@ -85,6 +88,7 @@ class Create extends Action
string $fileId,
string $resourceId,
bool $internalFile,
+ string $onDuplicate,
Response $response,
Database $dbForProject,
Database $dbForPlatform,
@@ -183,6 +187,7 @@ class Create extends Action
'options' => [
'path' => $newPath,
'size' => $fileSize,
+ 'onDuplicate' => $onDuplicate,
],
]));
diff --git a/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/JSON/Imports/Create.php b/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/JSON/Imports/Create.php
index 55081b2645..c5d936711e 100644
--- a/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/JSON/Imports/Create.php
+++ b/src/Appwrite/Platform/Modules/Migrations/Http/Migrations/JSON/Imports/Create.php
@@ -20,6 +20,7 @@ use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
+use Utopia\Migration\Destinations\OnDuplicate;
use Utopia\Migration\Resource;
use Utopia\Migration\Sources\Appwrite as AppwriteSource;
use Utopia\Migration\Sources\JSON as JSONSource;
@@ -29,6 +30,7 @@ use Utopia\Platform\Scope\HTTP;
use Utopia\Storage\Device;
use Utopia\System\System;
use Utopia\Validator\Boolean;
+use Utopia\Validator\WhiteList;
class Create extends Action
{
@@ -66,6 +68,7 @@ class Create extends Action
->param('fileId', '', new UID(), 'File ID.')
->param('resourceId', null, new CompoundUID(), 'Composite ID in the format {databaseId:collectionId}, identifying a collection within a database.')
->param('internalFile', false, new Boolean(), 'Is the file stored in an internal bucket?', true)
+ ->param('onDuplicate', OnDuplicate::Fail->value, new WhiteList(OnDuplicate::values()), 'Behavior when a row with an existing $id is encountered. "fail" (default): abort on first conflict. "skip": silently ignore. "overwrite": replace existing row.', true)
->inject('response')
->inject('dbForProject')
->inject('dbForPlatform')
@@ -84,6 +87,7 @@ class Create extends Action
string $fileId,
string $resourceId,
bool $internalFile,
+ string $onDuplicate,
Response $response,
Database $dbForProject,
Database $dbForPlatform,
@@ -183,6 +187,7 @@ class Create extends Action
'options' => [
'path' => $newPath,
'size' => $fileSize,
+ 'onDuplicate' => $onDuplicate,
],
]));
diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/AuthMethods/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/AuthMethods/Update.php
index 0d1cd83203..5335036cde 100644
--- a/src/Appwrite/Platform/Modules/Project/Http/Project/AuthMethods/Update.php
+++ b/src/Appwrite/Platform/Modules/Project/Http/Project/AuthMethods/Update.php
@@ -31,7 +31,7 @@ class Update extends Action
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/project/auth-methods/:methodId')
->httpAlias('/v1/projects/:projectId/auth/:methodId')
- ->desc('Update project auth method status. Use this endpoint to enable or disable a given auth method for this project.')
+ ->desc('Update project auth method status')
->groups(['api', 'project'])
->label('scope', 'project.write')
->label('event', 'authMethod.[methodId].update')
diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Delete.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Delete.php
index 0a60e4ce4d..201061dd62 100644
--- a/src/Appwrite/Platform/Modules/Project/Http/Project/Delete.php
+++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Delete.php
@@ -2,7 +2,8 @@
namespace Appwrite\Platform\Modules\Project\Http\Project;
-use Appwrite\Event\Delete as DeleteQueue;
+use Appwrite\Event\Message\Delete as DeleteMessage;
+use Appwrite\Event\Publisher\Delete as DeletePublisher;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@@ -33,7 +34,6 @@ class Delete extends Action
->desc('Delete project')
->groups(['api', 'project'])
->label('scope', 'project.write')
- ->label('event', 'project.delete')
->label('audits.event', 'project.delete')
->label('audits.resource', 'project/{project.$id}')
->label('sdk', new Method(
@@ -54,7 +54,7 @@ class Delete extends Action
))
->inject('response')
->inject('dbForPlatform')
- ->inject('queueForDeletes')
+ ->inject('publisherForDeletes')
->inject('authorization')
->inject('project')
->callback($this->action(...));
@@ -63,19 +63,20 @@ class Delete extends Action
public function action(
Response $response,
Database $dbForPlatform,
- DeleteQueue $queueForDeletes,
+ DeletePublisher $publisherForDeletes,
Authorization $authorization,
Document $project,
) {
- $queueForDeletes
- ->setProject($project)
- ->setType(DELETE_TYPE_DOCUMENT)
- ->setDocument($project);
-
if (!$authorization->skip(fn () => $dbForPlatform->deleteDocument('projects', $project->getId()))) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove project from DB');
}
+ $publisherForDeletes->enqueue(new DeleteMessage(
+ project: $project,
+ type: DELETE_TYPE_DOCUMENT,
+ document: $project,
+ ));
+
$response->noContent();
}
}
diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Get.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Get.php
new file mode 100644
index 0000000000..197d82ef58
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Get.php
@@ -0,0 +1,64 @@
+setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
+ ->setHttpPath('/v1/project')
+ ->httpAlias('/v1/projects/:projectId')
+ ->desc('Get project')
+ ->groups(['api', 'project'])
+ ->label('scope', 'project.read')
+ ->label('sdk', new Method(
+ namespace: 'project',
+ group: null,
+ name: 'get',
+ description: <<inject('response')
+ ->inject('project')
+ ->callback($this->action(...));
+ }
+
+ public function action(
+ Response $response,
+ Document $project,
+ ) {
+ if ($project->isEmpty()) {
+ throw new Exception(Exception::PROJECT_NOT_FOUND);
+ }
+
+ $response->dynamic($project, Response::MODEL_PROJECT);
+ }
+}
diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Ephemeral/Create.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Ephemeral/Create.php
index 7fdefca218..4130effe69 100644
--- a/src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Ephemeral/Create.php
+++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Keys/Ephemeral/Create.php
@@ -59,7 +59,7 @@ class Create extends Base
],
))
->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.', optional: false)
- ->param('duration', null, new Range(1, 3600), 'Time in seconds before ephemeral key expires. Maximum duration is 3600 seconds.', optional: false)
+ ->param('duration', null, new Range(1, 3600), 'Time in seconds before ephemeral key expires. Maximum duration is 3600 seconds.', optional: false, example: 600)
->inject('response')
->inject('queueForEvents')
->inject('project')
diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Get.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Get.php
index ae46a59c67..250a3e5df1 100644
--- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Get.php
+++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Get.php
@@ -11,7 +11,7 @@ use Utopia\Config\Config;
use Utopia\Database\Document;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
-use Utopia\Validator\Text;
+use Utopia\Validator\WhiteList;
class Get extends Action
{
@@ -86,28 +86,28 @@ class Get extends Action
)
]
))
- ->param('provider', '', new Text(128), 'OAuth2 provider key. For example: github, google, apple.')
+ ->param('providerId', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders', [])), true), 'OAuth2 provider key. For example: github, google, apple.', aliases: ['provider'])
->inject('response')
->inject('project')
->callback($this->action(...));
}
public function action(
- string $provider,
+ string $providerId,
Response $response,
Document $project,
): void {
$providers = Config::getParam('oAuthProviders', []);
- if (!\array_key_exists($provider, $providers) || !($providers[$provider]['enabled'] ?? false)) {
+ if (!\array_key_exists($providerId, $providers) || !($providers[$providerId]['enabled'] ?? false)) {
throw new Exception(Exception::PROJECT_PROVIDER_UNSUPPORTED);
}
$actions = Base::getProviderActions();
- if (!isset($actions[$provider])) {
+ if (!isset($actions[$providerId])) {
throw new Exception(Exception::PROJECT_PROVIDER_UNSUPPORTED);
}
- $updateClass = $actions[$provider];
+ $updateClass = $actions[$providerId];
$action = new $updateClass();
$response->dynamic($action->buildReadResponse($project), $updateClass::getResponseModel());
diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Google/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Google/Update.php
index 9b985f4aed..2a061d09ce 100644
--- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Google/Update.php
+++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Google/Update.php
@@ -3,8 +3,22 @@
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Google;
use Appwrite\Auth\OAuth2\Google;
+use Appwrite\Event\Event as QueueEvent;
+use Appwrite\Extend\Exception;
+use Appwrite\Platform\Action;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base;
+use Appwrite\SDK\AuthType;
+use Appwrite\SDK\Method;
+use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
+use Utopia\Database\Database;
+use Utopia\Database\Document;
+use Utopia\Database\Validator\Authorization;
+use Utopia\Validator\ArrayList;
+use Utopia\Validator\Boolean;
+use Utopia\Validator\Nullable;
+use Utopia\Validator\Text;
+use Utopia\Validator\WhiteList;
class Update extends Base
{
@@ -52,4 +66,118 @@ class Update extends Base
{
return 'GOCSPX-2k8gsR0000000000000000VNahJj';
}
+
+ public static function getParameters(): array
+ {
+ return \array_merge(parent::getParameters(), [
+ [
+ '$id' => 'prompt',
+ 'name' => 'Prompt',
+ 'example' => '["consent"]',
+ 'hint' => '',
+ ],
+ ]);
+ }
+
+ public function __construct()
+ {
+ $providerId = static::getProviderId();
+ $providerLabel = static::getProviderLabel();
+
+ $this
+ ->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
+ ->setHttpPath('/v1/project/oauth2/' . $providerId)
+ ->desc('Update project OAuth2 ' . $providerLabel)
+ ->groups(['api', 'project'])
+ ->label('scope', 'oauth2.write')
+ ->label('event', 'oauth2.[providerId].update')
+ ->label('audits.event', 'project.oauth2.[providerId].update')
+ ->label('audits.resource', 'project.oauth2/{response.$id}')
+ ->label('sdk', new Method(
+ namespace: 'project',
+ group: 'oauth2',
+ name: static::getProviderSDKMethod(),
+ description: 'Update the project OAuth2 ' . $providerLabel . ' configuration.',
+ auth: [AuthType::ADMIN, AuthType::KEY],
+ responses: [
+ new SDKResponse(
+ code: Response::STATUS_CODE_OK,
+ model: static::getResponseModel(),
+ )
+ ],
+ ))
+ ->param(static::getClientIdParamName(), null, new Nullable(new Text(256, 0)), static::getClientIdDescription(), optional: true)
+ ->param(static::getClientSecretParamName(), null, new Nullable(new Text(512, 0)), static::getClientSecretDescription(), optional: true)
+ ->param('prompt', null, new Nullable(new ArrayList(new WhiteList(['none', 'consent', 'select_account'], true), 3)), 'Array of Google OAuth2 prompt values. If "none" is included, it must be the only element. "none" means: don\'t display any authentication or consent screens. Must not be specified with other values. "consent" means: prompt the user for consent. "select_account" means: prompt the user to select an account.', optional: true)
+ ->param('enabled', null, new Nullable(new Boolean()), 'OAuth2 sign-in method status. Set to true to enable new session creation. Setting to true will trigger end-to-end credentials validation, and will throw if the credentials are invalid.', true)
+ ->inject('response')
+ ->inject('dbForPlatform')
+ ->inject('project')
+ ->inject('authorization')
+ ->inject('queueForEvents')
+ ->callback($this->handle(...));
+ }
+
+ public function buildReadResponse(Document $project): Document
+ {
+ $providerId = static::getProviderId();
+ $oAuthProviders = $project->getAttribute('oAuthProviders', []);
+ $decoded = $this->decodeStoredSecret($project);
+
+ return new Document([
+ '$id' => $providerId,
+ 'enabled' => $oAuthProviders[$providerId . 'Enabled'] ?? false,
+ static::getClientIdParamName() => $oAuthProviders[$providerId . 'Appid'] ?? '',
+ static::getClientSecretParamName() => '',
+ 'prompt' => $decoded['prompt'] ?? ['consent'],
+ ]);
+ }
+
+ /**
+ * Custom callback used instead of the parent's `action()` because Google
+ * takes an additional optional `prompt` parameter. The method is named
+ * differently to avoid an LSP-incompatible override of Base::action().
+ */
+ public function handle(
+ ?string $clientId,
+ ?string $clientSecret,
+ ?array $prompt,
+ ?bool $enabled,
+ Response $response,
+ Database $dbForPlatform,
+ Document $project,
+ Authorization $authorization,
+ QueueEvent $queueForEvents
+ ): void {
+ $providerId = static::getProviderId();
+ $queueForEvents->setParam('providerId', $providerId);
+
+ if ($prompt !== null) {
+ if (empty($prompt)) {
+ throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Prompt array cannot be empty.');
+ }
+
+ if (\in_array('none', $prompt) && \count($prompt) > 1) {
+ throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'When "none" is used as a prompt value, it must be the only element in the array.');
+ }
+ }
+
+ $storedRaw = $project->getAttribute('oAuthProviders', [])[$providerId . 'Secret'] ?? '';
+ $existing = $this->decodeStoredSecret($project);
+
+ // Backwards compatibility: secrets stored before the prompt feature
+ // were saved as plain strings. Treat the raw value as clientSecret.
+ if (!empty($storedRaw) && empty($existing)) {
+ $existing = ['clientSecret' => $storedRaw];
+ }
+
+ $encodedSecret = \json_encode([
+ 'clientSecret' => $clientSecret ?? ($existing['clientSecret'] ?? ''),
+ 'prompt' => $prompt ?? ($existing['prompt'] ?? ['consent']),
+ ]);
+
+ $project = $this->persistCredentials($project, $dbForPlatform, $authorization, $clientId, $encodedSecret, $enabled);
+
+ $response->dynamic($this->buildReadResponse($project), static::getResponseModel());
+ }
}
diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Oidc/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Oidc/Update.php
index 9598ff4c43..697b306be8 100644
--- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Oidc/Update.php
+++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Oidc/Update.php
@@ -82,13 +82,13 @@ class Update extends Base
'hint' => '',
],
[
- '$id' => 'tokenUrl',
+ '$id' => 'tokenURL',
'name' => 'Token URL',
'example' => 'https://myoauth.com/oauth2/token',
'hint' => '',
],
[
- '$id' => 'userInfoUrl',
+ '$id' => 'userInfoURL',
'name' => 'User Info URL',
'example' => 'https://myoauth.com/oauth2/userinfo',
'hint' => '',
@@ -127,8 +127,8 @@ class Update extends Base
->param(static::getClientSecretParamName(), null, new Nullable(new Text(512, 0)), static::getClientSecretDescription(), optional: true)
->param('wellKnownURL', null, new Nullable(new URL(allowEmpty: true)), 'OpenID Connect well-known configuration URL. When provided, authorization, token, and user info endpoints can be discovered automatically. For example: https://myoauth.com/.well-known/openid-configuration', optional: true)
->param('authorizationURL', null, new Nullable(new URL(allowEmpty: true)), 'OpenID Connect authorization endpoint URL. Required when wellKnownURL is not provided. For example: https://myoauth.com/oauth2/authorize', optional: true)
- ->param('tokenUrl', null, new Nullable(new URL(allowEmpty: true)), 'OpenID Connect token endpoint URL. Required when wellKnownURL is not provided. For example: https://myoauth.com/oauth2/token', optional: true)
- ->param('userInfoUrl', null, new Nullable(new URL(allowEmpty: true)), 'OpenID Connect user info endpoint URL. Required when wellKnownURL is not provided. For example: https://myoauth.com/oauth2/userinfo', optional: true)
+ ->param('tokenURL', null, new Nullable(new URL(allowEmpty: true)), 'OpenID Connect token endpoint URL. Required when wellKnownURL is not provided. For example: https://myoauth.com/oauth2/token', optional: true, aliases: ['tokenUrl'])
+ ->param('userInfoURL', null, new Nullable(new URL(allowEmpty: true)), 'OpenID Connect user info endpoint URL. Required when wellKnownURL is not provided. For example: https://myoauth.com/oauth2/userinfo', optional: true, aliases: ['userInfoUrl'])
->param('enabled', null, new Nullable(new Boolean()), 'OAuth2 sign-in method status. Set to true to enable new session creation. Setting to true will trigger end-to-end credentials validation, and will throw if the credentials are invalid.', true)
->inject('response')
->inject('dbForPlatform')
@@ -151,8 +151,8 @@ class Update extends Base
static::getClientSecretParamName() => '',
'wellKnownURL' => $decoded['wellKnownEndpoint'] ?? '',
'authorizationURL' => $decoded['authorizationEndpoint'] ?? '',
- 'tokenUrl' => $decoded['tokenEndpoint'] ?? '',
- 'userInfoUrl' => $decoded['userInfoEndpoint'] ?? '',
+ 'tokenURL' => $decoded['tokenEndpoint'] ?? '',
+ 'userInfoURL' => $decoded['userInfoEndpoint'] ?? '',
]);
}
@@ -174,8 +174,8 @@ class Update extends Base
?string $clientSecret,
?string $wellKnownURL,
?string $authorizationURL,
- ?string $tokenUrl,
- ?string $userInfoUrl,
+ ?string $tokenURL,
+ ?string $userInfoURL,
?bool $enabled,
Response $response,
Database $dbForPlatform,
@@ -201,8 +201,8 @@ class Update extends Base
'clientSecret' => $clientSecret ?? ($existing['clientSecret'] ?? ''),
'wellKnownEndpoint' => $wellKnownURL ?? ($existing['wellKnownEndpoint'] ?? ''),
'authorizationEndpoint' => $authorizationURL ?? ($existing['authorizationEndpoint'] ?? ''),
- 'tokenEndpoint' => $tokenUrl ?? ($existing['tokenEndpoint'] ?? ''),
- 'userInfoEndpoint' => $userInfoUrl ?? ($existing['userInfoEndpoint'] ?? ''),
+ 'tokenEndpoint' => $tokenURL ?? ($existing['tokenEndpoint'] ?? ''),
+ 'userInfoEndpoint' => $userInfoURL ?? ($existing['userInfoEndpoint'] ?? ''),
];
// When enabling, require either wellKnownEndpoint alone, or all three
@@ -215,7 +215,7 @@ class Update extends Base
&& !empty($merged['userInfoEndpoint']);
if (!$hasWellKnown && !$hasAllDiscovery) {
- throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Enabling OpenID Connect requires either wellKnownURL, or all of authorizationURL, tokenUrl, and userInfoUrl.');
+ throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Enabling OpenID Connect requires either wellKnownURL, or all of authorizationURL, tokenURL, and userInfoURL.');
}
}
diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/XList.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/XList.php
index d0780e4bae..6a84868286 100644
--- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/XList.php
+++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/XList.php
@@ -2,14 +2,21 @@
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2;
+use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Document;
+use Utopia\Database\Exception\Query as QueryException;
+use Utopia\Database\Query;
+use Utopia\Database\Validator\Queries;
+use Utopia\Database\Validator\Query\Limit;
+use Utopia\Database\Validator\Query\Offset;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
+use Utopia\Validator\Boolean;
class XList extends Action
{
@@ -43,15 +50,28 @@ class XList extends Action
)
]
))
+ ->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
+ ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('response')
->inject('project')
->callback($this->action(...));
}
+ /**
+ * @param array $queries
+ */
public function action(
+ array $queries,
+ bool $includeTotal,
Response $response,
Document $project,
): void {
+ try {
+ $queries = Query::parseQueries($queries);
+ } catch (QueryException $e) {
+ throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
+ }
+
$providers = Config::getParam('oAuthProviders', []);
$actions = Base::getProviderActions();
@@ -66,8 +86,16 @@ class XList extends Action
$documents[] = $action->buildReadResponse($project);
}
+ $total = $includeTotal ? \count($documents) : 0;
+
+ $grouped = Query::groupByType($queries);
+ $offset = $grouped['offset'] ?? 0;
+ $limit = $grouped['limit'] ?? null;
+
+ $documents = \array_slice($documents, $offset, $limit);
+
$response->dynamic(new Document([
- 'total' => \count($documents),
+ 'total' => $total,
'providers' => $documents,
]), Response::MODEL_OAUTH2_PROVIDER_LIST);
}
diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/Get.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/Get.php
index 3ffe30f1fa..21342332d9 100644
--- a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/Get.php
+++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/Get.php
@@ -27,7 +27,7 @@ class Get extends Action
->setHttpPath('/v1/project/policies/:policyId')
->desc('Get project policy')
->groups(['api', 'project'])
- ->label('scope', 'policies.read')
+ ->label('scope', ['policies.read', 'project.policies.read'])
->label('sdk', new Method(
namespace: 'project',
group: 'policies',
diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/MembershipPrivacy/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/MembershipPrivacy/Update.php
index c947ff225a..41a6168b07 100644
--- a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/MembershipPrivacy/Update.php
+++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/MembershipPrivacy/Update.php
@@ -31,7 +31,7 @@ class Update extends Action
->httpAlias('/v1/projects/:projectId/auth/memberships-privacy')
->desc('Update membership privacy policy')
->groups(['api', 'project'])
- ->label('scope', 'policies.write')
+ ->label('scope', ['policies.write', 'project.policies.write'])
->label('event', 'projects.[projectId].policies.[policy].update')
->label('audits.event', 'projects.[projectId].policies.[policy].update')
->label('audits.resource', 'project/{response.$id}')
diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/PasswordDictionary/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/PasswordDictionary/Update.php
index e2c678abb6..d7ee99fbfe 100644
--- a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/PasswordDictionary/Update.php
+++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/PasswordDictionary/Update.php
@@ -31,7 +31,7 @@ class Update extends Action
->httpAlias('/v1/projects/:projectId/auth/password-dictionary')
->desc('Update password dictionary policy')
->groups(['api', 'project'])
- ->label('scope', 'policies.write')
+ ->label('scope', ['policies.write', 'project.policies.write'])
->label('event', 'projects.[projectId].policies.[policy].update')
->label('audits.event', 'projects.[projectId].policies.[policy].update')
->label('audits.resource', 'project/{response.$id}')
diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/PasswordHistory/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/PasswordHistory/Update.php
index a8ae81caff..84861a19e1 100644
--- a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/PasswordHistory/Update.php
+++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/PasswordHistory/Update.php
@@ -32,7 +32,7 @@ class Update extends Action
->httpAlias('/v1/projects/:projectId/auth/password-history')
->desc('Update password history policy')
->groups(['api', 'project'])
- ->label('scope', 'policies.write')
+ ->label('scope', ['policies.write', 'project.policies.write'])
->label('event', 'projects.[projectId].policies.[policy].update')
->label('audits.event', 'projects.[projectId].policies.[policy].update')
->label('audits.resource', 'project/{response.$id}')
diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/PasswordPersonalData/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/PasswordPersonalData/Update.php
index 9db7cf0549..435f00fc39 100644
--- a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/PasswordPersonalData/Update.php
+++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/PasswordPersonalData/Update.php
@@ -31,7 +31,7 @@ class Update extends Action
->httpAlias('/v1/projects/:projectId/auth/personal-data')
->desc('Update password personal data policy')
->groups(['api', 'project'])
- ->label('scope', 'policies.write')
+ ->label('scope', ['policies.write', 'project.policies.write'])
->label('event', 'projects.[projectId].policies.[policy].update')
->label('audits.event', 'projects.[projectId].policies.[policy].update')
->label('audits.resource', 'project/{response.$id}')
diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/SessionAlert/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/SessionAlert/Update.php
index 22b7a44b04..79653d46ad 100644
--- a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/SessionAlert/Update.php
+++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/SessionAlert/Update.php
@@ -31,7 +31,7 @@ class Update extends Action
->httpAlias('/v1/projects/:projectId/auth/session-alerts')
->desc('Update session alert policy')
->groups(['api', 'project'])
- ->label('scope', 'policies.write')
+ ->label('scope', ['policies.write', 'project.policies.write'])
->label('event', 'projects.[projectId].policies.[policy].update')
->label('audits.event', 'projects.[projectId].policies.[policy].update')
->label('audits.resource', 'project/{response.$id}')
diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/SessionDuration/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/SessionDuration/Update.php
index ba72c93a6f..0a7f33218a 100644
--- a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/SessionDuration/Update.php
+++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/SessionDuration/Update.php
@@ -31,7 +31,7 @@ class Update extends Action
->httpAlias('/v1/projects/:projectId/auth/duration')
->desc('Update session duration policy')
->groups(['api', 'project'])
- ->label('scope', 'policies.write')
+ ->label('scope', ['policies.write', 'project.policies.write'])
->label('event', 'projects.[projectId].policies.[policy].update')
->label('audits.event', 'projects.[projectId].policies.[policy].update')
->label('audits.resource', 'project/{response.$id}')
diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/SessionInvalidation/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/SessionInvalidation/Update.php
index 8f8a959959..a1feb67346 100644
--- a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/SessionInvalidation/Update.php
+++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/SessionInvalidation/Update.php
@@ -31,7 +31,7 @@ class Update extends Action
->httpAlias('/v1/projects/:projectId/auth/session-invalidation')
->desc('Update session invalidation policy')
->groups(['api', 'project'])
- ->label('scope', 'policies.write')
+ ->label('scope', ['policies.write', 'project.policies.write'])
->label('event', 'projects.[projectId].policies.[policy].update')
->label('audits.event', 'projects.[projectId].policies.[policy].update')
->label('audits.resource', 'project/{response.$id}')
diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/SessionLimit/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/SessionLimit/Update.php
index 382ed6f0d9..936a541249 100644
--- a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/SessionLimit/Update.php
+++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/SessionLimit/Update.php
@@ -32,7 +32,7 @@ class Update extends Action
->httpAlias('/v1/projects/:projectId/auth/max-sessions')
->desc('Update session limit policy')
->groups(['api', 'project'])
- ->label('scope', 'policies.write')
+ ->label('scope', ['policies.write', 'project.policies.write'])
->label('event', 'projects.[projectId].policies.[policy].update')
->label('audits.event', 'projects.[projectId].policies.[policy].update')
->label('audits.resource', 'project/{response.$id}')
diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/UserLimit/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/UserLimit/Update.php
index 9129b81250..2b7e704853 100644
--- a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/UserLimit/Update.php
+++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/UserLimit/Update.php
@@ -32,7 +32,7 @@ class Update extends Action
->httpAlias('/v1/projects/:projectId/auth/limit')
->desc('Update user limit policy')
->groups(['api', 'project'])
- ->label('scope', 'policies.write')
+ ->label('scope', ['policies.write', 'project.policies.write'])
->label('event', 'projects.[projectId].policies.[policy].update')
->label('audits.event', 'projects.[projectId].policies.[policy].update')
->label('audits.resource', 'project/{response.$id}')
diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/XList.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/XList.php
index 893b28fef2..3020fa79dd 100644
--- a/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/XList.php
+++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Policies/XList.php
@@ -33,7 +33,7 @@ class XList extends Action
->setHttpPath('/v1/project/policies')
->desc('List project policies')
->groups(['api', 'project'])
- ->label('scope', 'policies.read')
+ ->label('scope', ['policies.read', 'project.policies.read'])
->label('sdk', new Method(
namespace: 'project',
group: 'policies',
diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/SMTP/Tests/Create.php b/src/Appwrite/Platform/Modules/Project/Http/Project/SMTP/Tests/Create.php
index 7095c2d2d0..8c87a41475 100644
--- a/src/Appwrite/Platform/Modules/Project/Http/Project/SMTP/Tests/Create.php
+++ b/src/Appwrite/Platform/Modules/Project/Http/Project/SMTP/Tests/Create.php
@@ -2,7 +2,8 @@
namespace Appwrite\Platform\Modules\Project\Http\Project\SMTP\Tests;
-use Appwrite\Event\Mail;
+use Appwrite\Event\Message\Mail as MailMessage;
+use Appwrite\Event\Publisher\Mail as MailPublisher;
use Appwrite\Extend\Exception as Exception;
use Appwrite\Platform\Action;
use Appwrite\SDK\AuthType;
@@ -67,7 +68,7 @@ class Create extends Action
->param('secure', '', new WhiteList(['tls', 'ssl'], true), 'Does SMTP server use secure connection', optional: true, deprecated: true) // Backwards compatibility
->inject('response')
->inject('project')
- ->inject('queueForMails')
+ ->inject('publisherForMails')
->inject('plan')
->callback($this->action(...));
}
@@ -87,7 +88,7 @@ class Create extends Action
string $paramSecure, // Backwards compatibility
Response $response,
Document $project,
- Mail $queueForMails,
+ MailPublisher $publisherForMails,
array $plan
): void {
// Backwards compatibility: use inline params if provided, otherwise fall back to project SMTP config.
@@ -153,23 +154,24 @@ class Create extends Action
->setParam('{{privacyUrl}}', $plan['privacyUrl'] ?? APP_EMAIL_PRIVACY_URL);
foreach ($emails as $email) {
- $queueForMails
- ->setSmtpHost($host)
- ->setSmtpPort($port)
- ->setSmtpUsername($username)
- ->setSmtpPassword($password)
- ->setSmtpSecure($secure)
- ->setSmtpReplyToEmail($replyToEmail)
- ->setSmtpReplyToName($replyToName)
- ->setSmtpSenderEmail($senderEmail)
- ->setSmtpSenderName($senderName)
- ->setRecipient($email)
- ->setName('')
- ->setBodyTemplate(APP_CE_CONFIG_DIR . '/locale/templates/email-base-styled.tpl')
- ->setBody($template->render())
- ->setVariables([])
- ->setSubject($subject)
- ->trigger();
+ $publisherForMails->enqueue(new MailMessage(
+ project: $project,
+ recipient: $email,
+ subject: $subject,
+ bodyTemplate: APP_CE_CONFIG_DIR . '/locale/templates/email-base-styled.tpl',
+ body: $template->render(),
+ smtp: [
+ 'host' => $host,
+ 'port' => $port,
+ 'username' => $username,
+ 'password' => $password,
+ 'secure' => $secure,
+ 'replyToEmail' => $replyToEmail,
+ 'replyToName' => $replyToName,
+ 'senderEmail' => $senderEmail,
+ 'senderName' => $senderName,
+ ],
+ ));
}
$response->noContent();
diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Create.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Create.php
index 8dbc720045..8c76ed2a8e 100644
--- a/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Create.php
+++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Create.php
@@ -53,7 +53,7 @@ class Create extends Action
)
],
))
- ->param('variableId', '', fn (Database $dbForProject) => new CustomId(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Variable ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForProject'])
+ ->param('variableId', '', fn (Database $dbForProject) => new CustomId(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForProject'])
->param('key', null, new Text(Database::LENGTH_KEY), 'Variable key. Max length: ' . Database::LENGTH_KEY . ' chars.')
->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.')
->param('secret', true, new Boolean(), 'Secret variables can be updated or deleted, but only projects can read them during build and runtime.', true)
@@ -72,7 +72,7 @@ class Create extends Action
QueueEvent $queueForEvents,
Database $dbForProject,
) {
- $variableId = ($variableId == 'unique()') ? ID::unique() : $variableId;
+ $variableId = ($variableId === 'unique()') ? ID::unique() : $variableId;
$variable = new Document([
'$id' => $variableId,
diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Delete.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Delete.php
index 2b0ae8feb1..553fb09e54 100644
--- a/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Delete.php
+++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Delete.php
@@ -51,7 +51,7 @@ class Delete extends Action
],
contentType: ContentType::NONE
))
- ->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable ID.', false, ['dbForProject'])
+ ->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID.', false, ['dbForProject'])
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Get.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Get.php
index af14148c92..d9030421d7 100644
--- a/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Get.php
+++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Get.php
@@ -44,7 +44,7 @@ class Get extends Action
)
]
))
- ->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable ID.', false, ['dbForProject'])
+ ->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID.', false, ['dbForProject'])
->inject('response')
->inject('dbForProject')
->callback($this->action(...));
diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Update.php
index 988a7c0849..6b05e19a78 100644
--- a/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Update.php
+++ b/src/Appwrite/Platform/Modules/Project/Http/Project/Variables/Update.php
@@ -52,7 +52,7 @@ class Update extends Action
)
]
))
- ->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable ID.', false, ['dbForProject'])
+ ->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID.', false, ['dbForProject'])
->param('key', null, new Nullable(new Text(255, 0)), 'Variable key. Max length: 255 chars.', true)
->param('value', null, new Nullable(new Text(8192, 0)), 'Variable value. Max length: 8192 chars.', true)
->param('secret', null, new Nullable(new Boolean()), 'Secret variables can be updated or deleted, but only projects can read them during build and runtime.', true)
diff --git a/src/Appwrite/Platform/Modules/Project/Services/Http.php b/src/Appwrite/Platform/Modules/Project/Services/Http.php
index 609de96530..3fe9f63d9e 100644
--- a/src/Appwrite/Platform/Modules/Project/Services/Http.php
+++ b/src/Appwrite/Platform/Modules/Project/Services/Http.php
@@ -5,6 +5,7 @@ namespace Appwrite\Platform\Modules\Project\Services;
use Appwrite\Platform\Modules\Project\Http\Init;
use Appwrite\Platform\Modules\Project\Http\Project\AuthMethods\Update as UpdateAuthMethod;
use Appwrite\Platform\Modules\Project\Http\Project\Delete as DeleteProject;
+use Appwrite\Platform\Modules\Project\Http\Project\Get as GetProject;
use Appwrite\Platform\Modules\Project\Http\Project\Keys\Create as CreateKey;
use Appwrite\Platform\Modules\Project\Http\Project\Keys\Delete as DeleteKey;
use Appwrite\Platform\Modules\Project\Http\Project\Keys\Ephemeral\Create as CreateEphemeralKey;
@@ -110,6 +111,7 @@ class Http extends Service
// Project
$this->addAction(DeleteProject::getName(), new DeleteProject());
+ $this->addAction(GetProject::getName(), new GetProject());
$this->addAction(UpdateProjectLabels::getName(), new UpdateProjectLabels());
$this->addAction(UpdateProjectProtocol::getName(), new UpdateProjectProtocol());
$this->addAction(UpdateProjectService::getName(), new UpdateProjectService());
@@ -139,19 +141,19 @@ class Http extends Service
$this->addAction(UpdateKey::getName(), new UpdateKey());
// Platforms
- $this->addAction(DeletePlatform::getName(), new DeletePlatform());
- $this->addAction(UpdateWebPlatform::getName(), new UpdateWebPlatform());
- $this->addAction(UpdateApplePlatform::getName(), new UpdateApplePlatform());
- $this->addAction(UpdateAndroidPlatform::getName(), new UpdateAndroidPlatform());
- $this->addAction(UpdateWindowsPlatform::getName(), new UpdateWindowsPlatform());
- $this->addAction(UpdateLinuxPlatform::getName(), new UpdateLinuxPlatform());
+ $this->addAction(ListPlatforms::getName(), new ListPlatforms());
+ $this->addAction(GetPlatform::getName(), new GetPlatform());
$this->addAction(CreateWebPlatform::getName(), new CreateWebPlatform());
$this->addAction(CreateApplePlatform::getName(), new CreateApplePlatform());
$this->addAction(CreateAndroidPlatform::getName(), new CreateAndroidPlatform());
$this->addAction(CreateWindowsPlatform::getName(), new CreateWindowsPlatform());
$this->addAction(CreateLinuxPlatform::getName(), new CreateLinuxPlatform());
- $this->addAction(GetPlatform::getName(), new GetPlatform());
- $this->addAction(ListPlatforms::getName(), new ListPlatforms());
+ $this->addAction(UpdateWebPlatform::getName(), new UpdateWebPlatform());
+ $this->addAction(UpdateApplePlatform::getName(), new UpdateApplePlatform());
+ $this->addAction(UpdateAndroidPlatform::getName(), new UpdateAndroidPlatform());
+ $this->addAction(UpdateWindowsPlatform::getName(), new UpdateWindowsPlatform());
+ $this->addAction(UpdateLinuxPlatform::getName(), new UpdateLinuxPlatform());
+ $this->addAction(DeletePlatform::getName(), new DeletePlatform());
// Mock Phones
$this->addAction(CreateMockPhone::getName(), new CreateMockPhone());
diff --git a/src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php b/src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php
index 363c99dc1f..d2c92fc65c 100644
--- a/src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php
+++ b/src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php
@@ -28,7 +28,6 @@ use Utopia\Pools\Group;
use Utopia\System\System;
use Utopia\Validator;
use Utopia\Validator\Text;
-use Utopia\Validator\URL;
use Utopia\Validator\WhiteList;
class Create extends Action
@@ -72,15 +71,6 @@ class Create extends Action
->param('name', null, new Text(128), 'Project name. Max length: 128 chars.')
->param('teamId', '', new UID(), 'Team unique ID.')
->param('region', System::getEnv('_APP_REGION', 'default'), new WhiteList(array_keys(array_filter(Config::getParam('regions'), fn ($config) => !$config['disabled']))), 'Project Region.', true)
- ->param('description', '', new Text(256), 'Project description. Max length: 256 chars.', true)
- ->param('logo', '', new Text(1024), 'Project logo.', true)
- ->param('url', '', new URL(), 'Project URL.', true)
- ->param('legalName', '', new Text(256), 'Project legal Name. Max length: 256 chars.', true)
- ->param('legalCountry', '', new Text(256), 'Project legal Country. Max length: 256 chars.', true)
- ->param('legalState', '', new Text(256), 'Project legal State. Max length: 256 chars.', true)
- ->param('legalCity', '', new Text(256), 'Project legal City. Max length: 256 chars.', true)
- ->param('legalAddress', '', new Text(256), 'Project legal Address. Max length: 256 chars.', true)
- ->param('legalTaxId', '', new Text(256), 'Project legal Tax ID. Max length: 256 chars.', true)
->inject('request')
->inject('response')
->inject('dbForPlatform')
@@ -90,7 +80,7 @@ class Create extends Action
->callback($this->action(...));
}
- public function action(string $projectId, string $name, string $teamId, string $region, string $description, string $logo, string $url, string $legalName, string $legalCountry, string $legalState, string $legalCity, string $legalAddress, string $legalTaxId, Request $request, Response $response, Database $dbForPlatform, Cache $cache, Group $pools, Hooks $hooks)
+ public function action(string $projectId, string $name, string $teamId, string $region, Request $request, Response $response, Database $dbForPlatform, Cache $cache, Group $pools, Hooks $hooks)
{
$team = $dbForPlatform->getDocument('teams', $teamId);
@@ -175,16 +165,7 @@ class Create extends Action
'teamInternalId' => $team->getSequence(),
'teamId' => $team->getId(),
'region' => $region,
- 'description' => $description,
- 'logo' => $logo,
- 'url' => $url,
'version' => APP_VERSION_STABLE,
- 'legalName' => $legalName,
- 'legalCountry' => $legalCountry,
- 'legalState' => $legalState,
- 'legalCity' => $legalCity,
- 'legalAddress' => $legalAddress,
- 'legalTaxId' => ID::custom($legalTaxId),
'services' => new \stdClass(),
'platforms' => null,
'oAuthProviders' => [],
diff --git a/src/Appwrite/Platform/Modules/Proxy/Action.php b/src/Appwrite/Platform/Modules/Proxy/Action.php
index 8baf54c790..f2ffc58568 100644
--- a/src/Appwrite/Platform/Modules/Proxy/Action.php
+++ b/src/Appwrite/Platform/Modules/Proxy/Action.php
@@ -5,7 +5,11 @@ namespace Appwrite\Platform\Modules\Proxy;
use Appwrite\Extend\Exception;
use Appwrite\Network\Validator\DNS as ValidatorDNS;
use Appwrite\Platform\Action as PlatformAction;
+use Utopia\Database\Database;
use Utopia\Database\Document;
+use Utopia\Database\Exception\Duplicate;
+use Utopia\Database\Query;
+use Utopia\Database\Validator\Authorization;
use Utopia\DNS\Message\Record;
use Utopia\Domains\Domain;
use Utopia\Logger\Log;
@@ -20,6 +24,57 @@ class Action extends PlatformAction
{
}
+ protected function createRule(Document $rule, Database $dbForPlatform, Authorization $authorization): Document
+ {
+ try {
+ return $authorization->skip(fn () => $dbForPlatform->createDocument('rules', $rule));
+ } catch (Duplicate) {
+ if (!$this->deleteOrphanedRule($rule, $dbForPlatform, $authorization)) {
+ throw new Exception(Exception::RULE_ALREADY_EXISTS);
+ }
+ }
+
+ try {
+ return $authorization->skip(fn () => $dbForPlatform->createDocument('rules', $rule));
+ } catch (Duplicate) {
+ throw new Exception(Exception::RULE_ALREADY_EXISTS);
+ }
+ }
+
+ private function deleteOrphanedRule(Document $rule, Database $dbForPlatform, Authorization $authorization): bool
+ {
+ $existingRule = $authorization->skip(function () use ($rule, $dbForPlatform) {
+ $existingRule = $dbForPlatform->findOne('rules', [
+ Query::equal('domain', [$rule->getAttribute('domain', '')]),
+ ]);
+ if (!$existingRule->isEmpty()) {
+ return $existingRule;
+ }
+
+ return $dbForPlatform->getDocument('rules', $rule->getId());
+ });
+
+ if (
+ $existingRule->isEmpty() ||
+ $existingRule->getAttribute('domain', '') !== $rule->getAttribute('domain', '')
+ ) {
+ return false;
+ }
+
+ $projectId = $existingRule->getAttribute('projectId', '');
+ if (empty($projectId)) {
+ return false;
+ }
+
+ $project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
+ if (!$project->isEmpty()) {
+ return false;
+ }
+
+ $authorization->skip(fn () => $dbForPlatform->deleteDocument('rules', $existingRule->getId()));
+ return true;
+ }
+
/**
* Ensures domain is not in the deny list and is a valid domain
*
diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/API/Create.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/API/Create.php
index a6a3e44194..9431d24cde 100644
--- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/API/Create.php
+++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/API/Create.php
@@ -12,8 +12,8 @@ use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
-use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Helpers\ID;
+use Utopia\Database\Validator\Authorization;
use Utopia\Logger\Log;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
@@ -43,12 +43,14 @@ class Create extends Action
->label('audits.resource', 'rule/{response.$id}')
->label('sdk', new Method(
namespace: 'proxy',
- group: null,
+ group: 'rules',
name: 'createAPIRule',
description: <<inject('dbForPlatform')
->inject('platform')
->inject('log')
+ ->inject('authorization')
->callback($this->action(...));
}
- public function action(string $domain, Response $response, Document $project, Certificate $publisherForCertificates, Event $queueForEvents, Database $dbForPlatform, array $platform, Log $log)
- {
+ public function action(
+ string $domain,
+ Response $response,
+ Document $project,
+ Certificate $publisherForCertificates,
+ Event $queueForEvents,
+ Database $dbForPlatform,
+ array $platform,
+ Log $log,
+ Authorization $authorization,
+ ) {
$this->validateDomainRestrictions($domain, $platform);
// TODO: (@Meldiron) Remove after 1.7.x migration
@@ -107,11 +119,7 @@ class Create extends Action
}
}
- try {
- $rule = $dbForPlatform->createDocument('rules', $rule);
- } catch (Duplicate $e) {
- throw new Exception(Exception::RULE_ALREADY_EXISTS);
- }
+ $rule = $this->createRule($rule, $dbForPlatform, $authorization);
if ($rule->getAttribute('status', '') === RULE_STATUS_CERTIFICATE_GENERATING) {
$publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate(
@@ -126,6 +134,13 @@ class Create extends Action
$queueForEvents->setParam('ruleId', $rule->getId());
+ // Rename 'created' status to 'unverified' for consistency.
+ // 'verifying' and 'verified' statuses stay as is.
+ // 'unverified' in the meaning of failed certificate generation stays as is.
+ if ($rule->getAttribute('status') === 'created') {
+ $rule->setAttribute('status', 'unverified');
+ }
+
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($rule, Response::MODEL_PROXY_RULE);
diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Delete.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Delete.php
index 1d5b770496..991b8eb006 100644
--- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Delete.php
+++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Delete.php
@@ -2,8 +2,9 @@
namespace Appwrite\Platform\Modules\Proxy\Http\Rules;
-use Appwrite\Event\Delete as DeleteEvent;
use Appwrite\Event\Event;
+use Appwrite\Event\Message\Delete as DeleteMessage;
+use Appwrite\Event\Publisher\Delete as DeletePublisher;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@@ -12,6 +13,7 @@ use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
+use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
@@ -38,12 +40,12 @@ class Delete extends Action
->label('audits.resource', 'rule/{request.ruleId}')
->label('sdk', new Method(
namespace: 'proxy',
- group: null,
+ group: 'rules',
name: 'deleteRule',
description: <<inject('response')
->inject('project')
->inject('dbForPlatform')
- ->inject('queueForDeletes')
+ ->inject('publisherForDeletes')
->inject('queueForEvents')
+ ->inject('authorization')
->callback($this->action(...));
}
@@ -66,20 +69,23 @@ class Delete extends Action
Response $response,
Document $project,
Database $dbForPlatform,
- DeleteEvent $queueForDeletes,
- Event $queueForEvents
+ DeletePublisher $publisherForDeletes,
+ Event $queueForEvents,
+ Authorization $authorization,
) {
- $rule = $dbForPlatform->getDocument('rules', $ruleId);
+ $rule = $authorization->skip(fn () => $dbForPlatform->getDocument('rules', $ruleId));
if ($rule->isEmpty() || $rule->getAttribute('projectInternalId') !== $project->getSequence()) {
throw new Exception(Exception::RULE_NOT_FOUND);
}
- $dbForPlatform->deleteDocument('rules', $rule->getId());
+ $authorization->skip(fn () => $dbForPlatform->deleteDocument('rules', $rule->getId()));
- $queueForDeletes
- ->setType(DELETE_TYPE_DOCUMENT)
- ->setDocument($rule);
+ $publisherForDeletes->enqueue(new DeleteMessage(
+ project: $project,
+ type: DELETE_TYPE_DOCUMENT,
+ document: $rule,
+ ));
$queueForEvents->setParam('ruleId', $rule->getId());
diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Function/Create.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Function/Create.php
index 4a8bd4897e..7cc8b5e59e 100644
--- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Function/Create.php
+++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Function/Create.php
@@ -12,8 +12,8 @@ use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
-use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Helpers\ID;
+use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Logger\Log;
use Utopia\Platform\Scope\HTTP;
@@ -45,12 +45,14 @@ class Create extends Action
->label('audits.resource', 'rule/{response.$id}')
->label('sdk', new Method(
namespace: 'proxy',
- group: null,
+ group: 'rules',
name: 'createFunctionRule',
description: <<inject('dbForProject')
->inject('platform')
->inject('log')
+ ->inject('authorization')
->callback($this->action(...));
}
- public function action(string $domain, string $functionId, string $branch, Response $response, Document $project, Certificate $publisherForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform, Log $log)
- {
+ public function action(
+ string $domain,
+ string $functionId,
+ string $branch,
+ Response $response,
+ Document $project,
+ Certificate $publisherForCertificates,
+ Event $queueForEvents,
+ Database $dbForPlatform,
+ Database $dbForProject,
+ array $platform,
+ Log $log,
+ Authorization $authorization,
+ ) {
+
$this->validateDomainRestrictions($domain, $platform);
$function = $dbForProject->getDocument('functions', $functionId);
@@ -125,11 +141,7 @@ class Create extends Action
}
}
- try {
- $rule = $dbForPlatform->createDocument('rules', $rule);
- } catch (Duplicate $e) {
- throw new Exception(Exception::RULE_ALREADY_EXISTS);
- }
+ $rule = $this->createRule($rule, $dbForPlatform, $authorization);
if ($rule->getAttribute('status', '') === RULE_STATUS_CERTIFICATE_GENERATING) {
$publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate(
@@ -144,6 +156,13 @@ class Create extends Action
$queueForEvents->setParam('ruleId', $rule->getId());
+ // Rename 'created' status to 'unverified' for consistency.
+ // 'verifying' and 'verified' statuses stay as is.
+ // 'unverified' in the meaning of failed certificate generation stays as is.
+ if ($rule->getAttribute('status') === 'created') {
+ $rule->setAttribute('status', 'unverified');
+ }
+
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($rule, Response::MODEL_PROXY_RULE);
diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Get.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Get.php
index b88a4ffc06..103ab1fddc 100644
--- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Get.php
+++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Get.php
@@ -10,6 +10,7 @@ use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
+use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Scope\HTTP;
@@ -34,12 +35,12 @@ class Get extends Action
->label('scope', 'rules.read')
->label('sdk', new Method(
namespace: 'proxy',
- group: null,
+ group: 'rules',
name: 'getRule',
description: <<inject('response')
->inject('project')
->inject('dbForPlatform')
+ ->inject('authorization')
->callback($this->action(...));
}
@@ -58,15 +60,16 @@ class Get extends Action
string $ruleId,
Response $response,
Document $project,
- Database $dbForPlatform
+ Database $dbForPlatform,
+ Authorization $authorization,
) {
- $rule = $dbForPlatform->getDocument('rules', $ruleId);
+ $rule = $authorization->skip(fn () => $dbForPlatform->getDocument('rules', $ruleId));
if ($rule->isEmpty() || $rule->getAttribute('projectInternalId') !== $project->getSequence()) {
throw new Exception(Exception::RULE_NOT_FOUND);
}
- $certificate = $dbForPlatform->getDocument('certificates', $rule->getAttribute('certificateId', ''));
+ $certificate = $authorization->skip(fn () => $dbForPlatform->getDocument('certificates', $rule->getAttribute('certificateId', '')));
// Give priority to certificate generation logs if present
if (!empty($certificate->getAttribute('logs', ''))) {
@@ -75,6 +78,13 @@ class Get extends Action
$rule->setAttribute('renewAt', $certificate->getAttribute('renewDate', ''));
+ // Rename 'created' status to 'unverified' for consistency.
+ // 'verifying' and 'verified' statuses stay as is.
+ // 'unverified' in the meaning of failed certificate generation stays as is.
+ if ($rule->getAttribute('status') === 'created') {
+ $rule->setAttribute('status', 'unverified');
+ }
+
$response->dynamic($rule, Response::MODEL_PROXY_RULE);
}
}
diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php
index 5964a20772..e8167b44a0 100644
--- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php
+++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php
@@ -12,8 +12,8 @@ use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
-use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Helpers\ID;
+use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Logger\Log;
use Utopia\Platform\Scope\HTTP;
@@ -46,12 +46,14 @@ class Create extends Action
->label('audits.resource', 'rule/{response.$id}')
->label('sdk', new Method(
namespace: 'proxy',
- group: null,
+ group: 'rules',
name: 'createRedirectRule',
description: <<inject('dbForProject')
->inject('platform')
->inject('log')
+ ->inject('authorization')
->callback($this->action(...));
}
- public function action(string $domain, string $url, int $statusCode, string $resourceId, string $resourceType, Response $response, Document $project, Certificate $publisherForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform, Log $log)
- {
+ public function action(
+ string $domain,
+ string $url,
+ int $statusCode,
+ string $resourceId,
+ string $resourceType,
+ Response $response,
+ Document $project,
+ Certificate $publisherForCertificates,
+ Event $queueForEvents,
+ Database $dbForPlatform,
+ Database $dbForProject,
+ array $platform,
+ Log $log,
+ Authorization $authorization,
+ ) {
+
$this->validateDomainRestrictions($domain, $platform);
$collection = match ($resourceType) {
@@ -130,11 +148,7 @@ class Create extends Action
}
}
- try {
- $rule = $dbForPlatform->createDocument('rules', $rule);
- } catch (Duplicate $e) {
- throw new Exception(Exception::RULE_ALREADY_EXISTS);
- }
+ $rule = $this->createRule($rule, $dbForPlatform, $authorization);
if ($rule->getAttribute('status', '') === RULE_STATUS_CERTIFICATE_GENERATING) {
$publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate(
@@ -149,6 +163,13 @@ class Create extends Action
$queueForEvents->setParam('ruleId', $rule->getId());
+ // Rename 'created' status to 'unverified' for consistency.
+ // 'verifying' and 'verified' statuses stay as is.
+ // 'unverified' in the meaning of failed certificate generation stays as is.
+ if ($rule->getAttribute('status') === 'created') {
+ $rule->setAttribute('status', 'unverified');
+ }
+
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($rule, Response::MODEL_PROXY_RULE);
diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Site/Create.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Site/Create.php
index a9dfa93a49..ca45d73e13 100644
--- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Site/Create.php
+++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Site/Create.php
@@ -12,8 +12,8 @@ use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
-use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Helpers\ID;
+use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Logger\Log;
use Utopia\Platform\Scope\HTTP;
@@ -45,12 +45,14 @@ class Create extends Action
->label('audits.resource', 'rule/{response.$id}')
->label('sdk', new Method(
namespace: 'proxy',
- group: null,
+ group: 'rules',
name: 'createSiteRule',
description: <<inject('dbForProject')
->inject('platform')
->inject('log')
+ ->inject('authorization')
->callback($this->action(...));
}
- public function action(string $domain, string $siteId, ?string $branch, Response $response, Document $project, Certificate $publisherForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform, Log $log)
- {
+ public function action(
+ string $domain,
+ string $siteId,
+ ?string $branch,
+ Response $response,
+ Document $project,
+ Certificate $publisherForCertificates,
+ Event $queueForEvents,
+ Database $dbForPlatform,
+ Database $dbForProject,
+ array $platform,
+ Log $log,
+ Authorization $authorization,
+ ) {
+
$this->validateDomainRestrictions($domain, $platform);
$site = $dbForProject->getDocument('sites', $siteId);
@@ -125,11 +141,7 @@ class Create extends Action
}
}
- try {
- $rule = $dbForPlatform->createDocument('rules', $rule);
- } catch (Duplicate $e) {
- throw new Exception(Exception::RULE_ALREADY_EXISTS);
- }
+ $rule = $this->createRule($rule, $dbForPlatform, $authorization);
if ($rule->getAttribute('status', '') === RULE_STATUS_CERTIFICATE_GENERATING) {
$publisherForCertificates->enqueue(new \Appwrite\Event\Message\Certificate(
@@ -144,6 +156,13 @@ class Create extends Action
$queueForEvents->setParam('ruleId', $rule->getId());
+ // Rename 'created' status to 'unverified' for consistency.
+ // 'verifying' and 'verified' statuses stay as is.
+ // 'unverified' in the meaning of failed certificate generation stays as is.
+ if ($rule->getAttribute('status') === 'created') {
+ $rule->setAttribute('status', 'unverified');
+ }
+
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($rule, Response::MODEL_PROXY_RULE);
diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Verification/Update.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Status/Update.php
similarity index 74%
rename from src/Appwrite/Platform/Modules/Proxy/Http/Rules/Verification/Update.php
rename to src/Appwrite/Platform/Modules/Proxy/Http/Rules/Status/Update.php
index 9e81f6ff18..1ad6f730b3 100644
--- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Verification/Update.php
+++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Status/Update.php
@@ -1,6 +1,6 @@
setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
- ->setHttpPath('/v1/proxy/rules/:ruleId/verification')
- ->desc('Update rule verification status')
+ ->setHttpPath('/v1/proxy/rules/:ruleId/status')
+ ->httpAlias('/v1/proxy/rules/:ruleId/verification')
+ ->desc('Update rule status')
->groups(['api', 'proxy'])
->label('scope', 'rules.write')
->label('event', 'rules.[ruleId].update')
@@ -41,12 +43,12 @@ class Update extends Action
->label('audits.resource', 'rule/{response.$id}')
->label('sdk', new Method(
namespace: 'proxy',
- group: null,
- name: 'updateRuleVerification',
+ group: 'rules',
+ name: 'updateRuleStatus',
description: <<inject('project')
->inject('dbForPlatform')
->inject('log')
+ ->inject('authorization')
->callback($this->action(...));
}
@@ -71,9 +74,10 @@ class Update extends Action
Event $queueForEvents,
Document $project,
Database $dbForPlatform,
- Log $log
+ Log $log,
+ Authorization $authorization,
) {
- $rule = $dbForPlatform->getDocument('rules', $ruleId);
+ $rule = $authorization->skip(fn () => $dbForPlatform->getDocument('rules', $ruleId));
if ($rule->isEmpty() || $rule->getAttribute('projectInternalId') !== $project->getSequence()) {
throw new Exception(Exception::RULE_NOT_FOUND);
@@ -90,22 +94,22 @@ class Update extends Action
try {
$this->verifyRule($rule, $log);
// Reset logs and status for the rule
- $rule = $dbForPlatform->updateDocument('rules', $rule->getId(), new Document([
+ $rule = $authorization->skip(fn () => $dbForPlatform->updateDocument('rules', $rule->getId(), new Document([
'logs' => '',
'status' => RULE_STATUS_CERTIFICATE_GENERATING,
- ]));
+ ])));
$certificateId = $rule->getAttribute('certificateId', '');
// Reset logs for the associated certificate.
if (!empty($certificateId)) {
- $certificate = $dbForPlatform->updateDocument('certificates', $certificateId, new Document([
+ $certificate = $authorization->skip(fn () => $dbForPlatform->updateDocument('certificates', $certificateId, new Document([
'logs' => '',
- ]));
+ ])));
}
} catch (Exception $err) {
- $dbForPlatform->updateDocument('rules', $rule->getId(), new Document([
+ $authorization->skip(fn () => $dbForPlatform->updateDocument('rules', $rule->getId(), new Document([
'$updatedAt' => DateTime::now(),
- ]));
+ ])));
throw $err;
}
diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/XList.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/XList.php
index 19daf8c8d2..999b4c8d74 100644
--- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/XList.php
+++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/XList.php
@@ -13,6 +13,7 @@ use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Query;
+use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
@@ -39,12 +40,12 @@ class XList extends Action
->label('scope', 'rules.read')
->label('sdk', new Method(
namespace: 'proxy',
- group: null,
+ group: 'rules',
name: 'listRules',
description: <<param('queries', [], new Rules(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Rules::ALLOWED_ATTRIBUTES), true)
- ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
+ ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true, deprecated: true)
->inject('response')
->inject('project')
->inject('dbForPlatform')
+ ->inject('authorization')
->callback($this->action(...));
}
public function action(
array $queries,
+ bool $total,
string $search,
- bool $includeTotal,
Response $response,
Document $project,
- Database $dbForPlatform
+ Database $dbForPlatform,
+ Authorization $authorization,
) {
try {
$queries = Query::parseQueries($queries);
@@ -91,7 +94,7 @@ class XList extends Action
}
$ruleId = $cursor->getValue();
- $cursorDocument = $dbForPlatform->getDocument('rules', $ruleId);
+ $cursorDocument = $authorization->skip(fn () => $dbForPlatform->getDocument('rules', $ruleId));
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Rule '{$ruleId}' for the 'cursor' value not found.");
@@ -102,9 +105,9 @@ class XList extends Action
$filterQueries = Query::groupByType($queries)['filters'];
- $rules = $dbForPlatform->find('rules', $queries);
+ $rules = $authorization->skip(fn () => $dbForPlatform->find('rules', $queries));
foreach ($rules as $rule) {
- $certificate = $dbForPlatform->getDocument('certificates', $rule->getAttribute('certificateId', ''));
+ $certificate = $authorization->skip(fn () => $dbForPlatform->getDocument('certificates', $rule->getAttribute('certificateId', '')));
// Give priority to certificate generation logs if present
if (!empty($certificate->getAttribute('logs', ''))) {
@@ -112,11 +115,18 @@ class XList extends Action
}
$rule->setAttribute('renewAt', $certificate->getAttribute('renewDate', ''));
+
+ // Rename 'created' status to 'unverified' for consistency.
+ // 'verifying' and 'verified' statuses stay as is.
+ // 'unverified' in the meaning of failed certificate generation stays as is.
+ if ($rule->getAttribute('status') === 'created') {
+ $rule->setAttribute('status', 'unverified');
+ }
}
$response->dynamic(new Document([
'rules' => $rules,
- 'total' => $includeTotal ? $dbForPlatform->count('rules', $filterQueries, APP_LIMIT_COUNT) : 0,
+ 'total' => $total ? $authorization->skip(fn () => $dbForPlatform->count('rules', $filterQueries, APP_LIMIT_COUNT)) : 0,
]), Response::MODEL_PROXY_RULE_LIST);
}
}
diff --git a/src/Appwrite/Platform/Modules/Proxy/Services/Http.php b/src/Appwrite/Platform/Modules/Proxy/Services/Http.php
index 980c64cc54..b2a9de1933 100644
--- a/src/Appwrite/Platform/Modules/Proxy/Services/Http.php
+++ b/src/Appwrite/Platform/Modules/Proxy/Services/Http.php
@@ -8,7 +8,7 @@ use Appwrite\Platform\Modules\Proxy\Http\Rules\Function\Create as CreateFunction
use Appwrite\Platform\Modules\Proxy\Http\Rules\Get as GetRule;
use Appwrite\Platform\Modules\Proxy\Http\Rules\Redirect\Create as CreateRedirectRule;
use Appwrite\Platform\Modules\Proxy\Http\Rules\Site\Create as CreateSiteRule;
-use Appwrite\Platform\Modules\Proxy\Http\Rules\Verification\Update as UpdateRuleVerification;
+use Appwrite\Platform\Modules\Proxy\Http\Rules\Status\Update as UpdateRuleStatus;
use Appwrite\Platform\Modules\Proxy\Http\Rules\XList as ListRules;
use Utopia\Platform\Service;
@@ -26,6 +26,6 @@ class Http extends Service
$this->addAction(GetRule::getName(), new GetRule());
$this->addAction(ListRules::getName(), new ListRules());
$this->addAction(DeleteRule::getName(), new DeleteRule());
- $this->addAction(UpdateRuleVerification::getName(), new UpdateRuleVerification());
+ $this->addAction(UpdateRuleStatus::getName(), new UpdateRuleStatus());
}
}
diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php
index 531962d33a..df61d1809d 100644
--- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php
+++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php
@@ -2,8 +2,9 @@
namespace Appwrite\Platform\Modules\Sites\Http\Deployments;
-use Appwrite\Event\Build;
use Appwrite\Event\Event;
+use Appwrite\Event\Message\Build as BuildMessage;
+use Appwrite\Event\Publisher\Build as BuildPublisher;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@@ -87,7 +88,7 @@ class Create extends Action
->inject('queueForEvents')
->inject('deviceForSites')
->inject('deviceForLocal')
- ->inject('queueForBuilds')
+ ->inject('publisherForBuilds')
->inject('plan')
->inject('authorization')
->inject('platform')
@@ -110,7 +111,7 @@ class Create extends Action
Event $queueForEvents,
Device $deviceForSites,
Device $deviceForLocal,
- Build $queueForBuilds,
+ BuildPublisher $publisherForBuilds,
array $plan,
Authorization $authorization,
array $platform,
@@ -249,7 +250,7 @@ class Create extends Action
}
try {
- $stateLock->withLock(function () use ($activate, $authorization, $commands, &$chunks, $chunksUploaded, $dbForPlatform, $dbForProject, $deploymentId, $deviceForSites, $fileSize, &$metadata, $outputDirectory, $path, $platform, $project, $queueForBuilds, $queueForEvents, $response, &$site, $siteId, $type): void {
+ $stateLock->withLock(function () use ($activate, $authorization, $commands, &$chunks, $chunksUploaded, $dbForPlatform, $dbForProject, $deploymentId, $deviceForSites, $fileSize, &$metadata, $outputDirectory, $path, $platform, $project, $publisherForBuilds, $queueForEvents, $response, &$site, $siteId, $type): void {
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
$uploaded = 0;
@@ -358,10 +359,13 @@ class Create extends Action
}
// Start the build
- $queueForBuilds
- ->setType(BUILD_TYPE_DEPLOYMENT)
- ->setResource($site)
- ->setDeployment($deployment);
+ $publisherForBuilds->enqueue(new BuildMessage(
+ project: $project,
+ resource: $site,
+ deployment: $deployment,
+ type: BUILD_TYPE_DEPLOYMENT,
+ platform: $platform,
+ ));
} else {
if ($deployment->isEmpty()) {
$deployment = $dbForProject->createDocument('deployments', new Document([
diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Delete.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Delete.php
index efea79395f..b50e9b54f4 100644
--- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Delete.php
+++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Delete.php
@@ -2,8 +2,9 @@
namespace Appwrite\Platform\Modules\Sites\Http\Deployments;
-use Appwrite\Event\Delete as DeleteEvent;
use Appwrite\Event\Event;
+use Appwrite\Event\Message\Delete as DeleteMessage;
+use Appwrite\Event\Publisher\Delete as DeletePublisher;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@@ -59,7 +60,7 @@ class Delete extends Action
->param('deploymentId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Deployment ID.', false, ['dbForProject'])
->inject('response')
->inject('dbForProject')
- ->inject('queueForDeletes')
+ ->inject('publisherForDeletes')
->inject('queueForEvents')
->inject('deviceForSites')
->callback($this->action(...));
@@ -70,7 +71,7 @@ class Delete extends Action
string $deploymentId,
Response $response,
Database $dbForProject,
- DeleteEvent $queueForDeletes,
+ DeletePublisher $publisherForDeletes,
Event $queueForEvents,
Device $deviceForSites
) {
@@ -130,9 +131,11 @@ class Delete extends Action
->setParam('siteId', $site->getId())
->setParam('deploymentId', $deployment->getId());
- $queueForDeletes
- ->setType(DELETE_TYPE_DOCUMENT)
- ->setDocument($deployment);
+ $publisherForDeletes->enqueue(new DeleteMessage(
+ project: $queueForEvents->getProject(),
+ type: DELETE_TYPE_DOCUMENT,
+ document: $deployment,
+ ));
$response->noContent();
}
diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Duplicate/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Duplicate/Create.php
index 546549604b..b3619c6017 100644
--- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Duplicate/Create.php
+++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Duplicate/Create.php
@@ -2,8 +2,9 @@
namespace Appwrite\Platform\Modules\Sites\Http\Deployments\Duplicate;
-use Appwrite\Event\Build;
use Appwrite\Event\Event;
+use Appwrite\Event\Message\Build as BuildMessage;
+use Appwrite\Event\Publisher\Build as BuildPublisher;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
@@ -63,7 +64,7 @@ class Create extends Action
->inject('dbForProject')
->inject('dbForPlatform')
->inject('queueForEvents')
- ->inject('queueForBuilds')
+ ->inject('publisherForBuilds')
->inject('deviceForSites')
->inject('authorization')
->inject('platform')
@@ -79,7 +80,7 @@ class Create extends Action
Database $dbForProject,
Database $dbForPlatform,
Event $queueForEvents,
- Build $queueForBuilds,
+ BuildPublisher $publisherForBuilds,
Device $deviceForSites,
Authorization $authorization,
array $platform
@@ -177,10 +178,13 @@ class Create extends Action
]))
);
- $queueForBuilds
- ->setType(BUILD_TYPE_DEPLOYMENT)
- ->setResource($site)
- ->setDeployment($deployment);
+ $publisherForBuilds->enqueue(new BuildMessage(
+ project: $project,
+ resource: $site,
+ deployment: $deployment,
+ type: BUILD_TYPE_DEPLOYMENT,
+ platform: $platform,
+ ));
$queueForEvents
->setParam('siteId', $site->getId())
diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php
index f648c57a83..29854d473b 100644
--- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php
+++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php
@@ -2,8 +2,9 @@
namespace Appwrite\Platform\Modules\Sites\Http\Deployments\Template;
-use Appwrite\Event\Build;
use Appwrite\Event\Event;
+use Appwrite\Event\Message\Build as BuildMessage;
+use Appwrite\Event\Publisher\Build as BuildPublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
@@ -77,7 +78,7 @@ class Create extends Base
->inject('dbForPlatform')
->inject('project')
->inject('queueForEvents')
- ->inject('queueForBuilds')
+ ->inject('publisherForBuilds')
->inject('gitHub')
->inject('authorization')
->inject('platform')
@@ -98,7 +99,7 @@ class Create extends Base
Database $dbForPlatform,
Document $project,
Event $queueForEvents,
- Build $queueForBuilds,
+ BuildPublisher $publisherForBuilds,
GitHub $github,
Authorization $authorization,
array $platform
@@ -130,7 +131,7 @@ class Create extends Base
installation: $installation,
dbForProject: $dbForProject,
dbForPlatform: $dbForPlatform,
- queueForBuilds: $queueForBuilds,
+ publisherForBuilds: $publisherForBuilds,
template: $template,
github: $github,
activate: $activate,
@@ -223,11 +224,14 @@ class Create extends Base
$this->updateEmptyManualRule($project, $site, $deployment, $dbForPlatform, $authorization);
- $queueForBuilds
- ->setType(BUILD_TYPE_DEPLOYMENT)
- ->setResource($site)
- ->setDeployment($deployment)
- ->setTemplate($template);
+ $publisherForBuilds->enqueue(new BuildMessage(
+ project: $project,
+ resource: $site,
+ deployment: $deployment,
+ type: BUILD_TYPE_DEPLOYMENT,
+ template: $template,
+ platform: $platform,
+ ));
$queueForEvents
->setParam('siteId', $site->getId())
diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Vcs/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Vcs/Create.php
index 4351dd8dd9..d34b8c4055 100644
--- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Vcs/Create.php
+++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Vcs/Create.php
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Sites\Http\Deployments\Vcs;
-use Appwrite\Event\Build;
use Appwrite\Event\Event;
+use Appwrite\Event\Publisher\Build as BuildPublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
@@ -71,7 +71,7 @@ class Create extends Base
->inject('dbForPlatform')
->inject('project')
->inject('queueForEvents')
- ->inject('queueForBuilds')
+ ->inject('publisherForBuilds')
->inject('gitHub')
->inject('authorization')
->inject('platform')
@@ -89,7 +89,7 @@ class Create extends Base
Database $dbForPlatform,
Document $project,
Event $queueForEvents,
- Build $queueForBuilds,
+ BuildPublisher $publisherForBuilds,
GitHub $github,
Authorization $authorization,
array $platform
@@ -111,7 +111,7 @@ class Create extends Base
installation: $installation,
dbForProject: $dbForProject,
dbForPlatform: $dbForPlatform,
- queueForBuilds: $queueForBuilds,
+ publisherForBuilds: $publisherForBuilds,
template: $template,
github: $github,
activate: $activate,
diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Delete.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Delete.php
index ebc192b6e6..50b070d098 100644
--- a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Delete.php
+++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Delete.php
@@ -2,8 +2,9 @@
namespace Appwrite\Platform\Modules\Sites\Http\Sites;
-use Appwrite\Event\Delete as DeleteEvent;
use Appwrite\Event\Event;
+use Appwrite\Event\Message\Delete as DeleteMessage;
+use Appwrite\Event\Publisher\Delete as DeletePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
@@ -56,7 +57,7 @@ class Delete extends Base
->param('siteId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Site ID.', false, ['dbForProject'])
->inject('response')
->inject('dbForProject')
- ->inject('queueForDeletes')
+ ->inject('publisherForDeletes')
->inject('queueForEvents')
->callback($this->action(...));
}
@@ -65,7 +66,7 @@ class Delete extends Base
string $siteId,
Response $response,
Database $dbForProject,
- DeleteEvent $queueForDeletes,
+ DeletePublisher $publisherForDeletes,
Event $queueForEvents
) {
$site = $dbForProject->getDocument('sites', $siteId);
@@ -78,9 +79,11 @@ class Delete extends Base
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove site from DB');
}
- $queueForDeletes
- ->setType(DELETE_TYPE_DOCUMENT)
- ->setDocument($site);
+ $publisherForDeletes->enqueue(new DeleteMessage(
+ project: $queueForEvents->getProject(),
+ type: DELETE_TYPE_DOCUMENT,
+ document: $site,
+ ));
$queueForEvents->setParam('siteId', $site->getId());
diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php
index 3c0d090b7b..2aee03265e 100644
--- a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php
+++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Update.php
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Sites\Http\Sites;
-use Appwrite\Event\Build;
use Appwrite\Event\Event;
+use Appwrite\Event\Publisher\Build as BuildPublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\Platform\Modules\Compute\Validator\Specification;
@@ -99,10 +99,11 @@ class Update extends Base
->inject('dbForProject')
->inject('project')
->inject('queueForEvents')
- ->inject('queueForBuilds')
+ ->inject('publisherForBuilds')
->inject('dbForPlatform')
->inject('gitHub')
->inject('executor')
+ ->inject('platform')
->callback($this->action(...));
}
@@ -133,10 +134,11 @@ class Update extends Base
Database $dbForProject,
Document $project,
Event $queueForEvents,
- Build $queueForBuilds,
+ BuildPublisher $publisherForBuilds,
Database $dbForPlatform,
GitHub $github,
- Executor $executor
+ Executor $executor,
+ array $platform
) {
if (!empty($adapter)) {
$configFramework = Config::getParam('frameworks')[$framework] ?? [];
@@ -279,7 +281,7 @@ class Update extends Base
// Redeploy logic
if (!$isConnected && !empty($providerRepositoryId)) {
- $this->redeployVcsFunction($request, $site, $project, $installation, $dbForProject, $queueForBuilds, new Document(), $github, true);
+ $this->redeployVcsFunction($request, $site, $project, $installation, $dbForProject, $publisherForBuilds, new Document(), $github, true, $platform);
}
$queueForEvents->setParam('siteId', $site->getId());
diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Variables/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Variables/Create.php
index 04b30fbc9c..edd3412b8f 100644
--- a/src/Appwrite/Platform/Modules/Sites/Http/Variables/Create.php
+++ b/src/Appwrite/Platform/Modules/Sites/Http/Variables/Create.php
@@ -2,11 +2,13 @@
namespace Appwrite\Platform\Modules\Sites\Http\Variables;
+use Appwrite\Event\Event as QueueEvent;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
+use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
@@ -36,6 +38,7 @@ class Create extends Base
->groups(['api', 'sites'])
->label('scope', 'sites.write')
->label('resourceType', RESOURCE_TYPE_SITES)
+ ->label('event', 'variables.[variableId].create')
->label('audits.event', 'variable.create')
->label('audits.resource', 'site/{request.siteId}')
->label('sdk', new Method(
@@ -54,16 +57,18 @@ class Create extends Base
]
))
->param('siteId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Site unique ID.', false, ['dbForProject'])
+ ->param('variableId', '', fn (Database $dbForProject) => new CustomId(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Variable ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForProject'])
->param('key', null, new Text(Database::LENGTH_KEY), 'Variable key. Max length: ' . Database::LENGTH_KEY . ' chars.', false)
->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', false)
->param('secret', true, new Boolean(), 'Secret variables can be updated or deleted, but only sites can read them during build and runtime.', true)
->inject('response')
+ ->inject('queueForEvents')
->inject('dbForProject')
->inject('project')
->callback($this->action(...));
}
- public function action(string $siteId, string $key, string $value, bool $secret, Response $response, Database $dbForProject, Document $project)
+ public function action(string $siteId, string $variableId, string $key, string $value, bool $secret, Response $response, QueueEvent $queueForEvents, Database $dbForProject, Document $project)
{
$site = $dbForProject->getDocument('sites', $siteId);
@@ -71,7 +76,7 @@ class Create extends Base
throw new Exception(Exception::SITE_NOT_FOUND);
}
- $variableId = ID::unique();
+ $variableId = ($variableId === 'unique()') ? ID::unique() : $variableId;
$teamId = $project->getAttribute('teamId', '');
$variable = new Document([
@@ -96,6 +101,8 @@ class Create extends Base
'live' => false,
]));
+ $queueForEvents->setParam('variableId', $variable->getId());
+
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($variable, Response::MODEL_VARIABLE);
diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Variables/Delete.php b/src/Appwrite/Platform/Modules/Sites/Http/Variables/Delete.php
index d61c9892cf..74c638bddc 100644
--- a/src/Appwrite/Platform/Modules/Sites/Http/Variables/Delete.php
+++ b/src/Appwrite/Platform/Modules/Sites/Http/Variables/Delete.php
@@ -2,6 +2,7 @@
namespace Appwrite\Platform\Modules\Sites\Http\Variables;
+use Appwrite\Event\Event as QueueEvent;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
@@ -33,6 +34,7 @@ class Delete extends Base
->groups(['api', 'sites'])
->label('scope', 'sites.write')
->label('resourceType', RESOURCE_TYPE_SITES)
+ ->label('event', 'variables.[variableId].delete')
->label('audits.event', 'variable.delete')
->label('audits.resource', 'site/{request.siteId}')
->label('sdk', new Method(
@@ -54,11 +56,12 @@ class Delete extends Base
->param('siteId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Site unique ID.', false, ['dbForProject'])
->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID.', false, ['dbForProject'])
->inject('response')
+ ->inject('queueForEvents')
->inject('dbForProject')
->callback($this->action(...));
}
- public function action(string $siteId, string $variableId, Response $response, Database $dbForProject)
+ public function action(string $siteId, string $variableId, Response $response, QueueEvent $queueForEvents, Database $dbForProject)
{
$site = $dbForProject->getDocument('sites', $siteId);
@@ -77,6 +80,8 @@ class Delete extends Base
'live' => false,
]));
+ $queueForEvents->setParam('variableId', $variable->getId());
+
$response->noContent();
}
}
diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Variables/Update.php b/src/Appwrite/Platform/Modules/Sites/Http/Variables/Update.php
index 08cdd4ac38..0ed7414b9d 100644
--- a/src/Appwrite/Platform/Modules/Sites/Http/Variables/Update.php
+++ b/src/Appwrite/Platform/Modules/Sites/Http/Variables/Update.php
@@ -2,6 +2,7 @@
namespace Appwrite\Platform\Modules\Sites\Http\Variables;
+use Appwrite\Event\Event as QueueEvent;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
@@ -35,6 +36,7 @@ class Update extends Base
->desc('Update variable')
->groups(['api', 'sites'])
->label('scope', 'sites.write')
+ ->label('event', 'variables.[variableId].update')
->label('audits.event', 'variable.update')
->label('audits.resource', 'site/{request.siteId}')
->label('resourceType', RESOURCE_TYPE_SITES)
@@ -55,10 +57,11 @@ class Update extends Base
))
->param('siteId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Site unique ID.', false, ['dbForProject'])
->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID.', false, ['dbForProject'])
- ->param('key', null, new Text(255), 'Variable key. Max length: 255 chars.', false)
+ ->param('key', null, new Nullable(new Text(255, 0)), 'Variable key. Max length: 255 chars.', true)
->param('value', null, new Nullable(new Text(8192, 0)), 'Variable value. Max length: 8192 chars.', true)
->param('secret', null, new Nullable(new Boolean()), 'Secret variables can be updated or deleted, but only sites can read them during build and runtime.', true)
->inject('response')
+ ->inject('queueForEvents')
->inject('dbForProject')
->callback($this->action(...));
}
@@ -66,10 +69,11 @@ class Update extends Base
public function action(
string $siteId,
string $variableId,
- string $key,
+ ?string $key,
?string $value,
?bool $secret,
Response $response,
+ QueueEvent $queueForEvents,
Database $dbForProject
) {
$site = $dbForProject->getDocument('sites', $siteId);
@@ -87,19 +91,27 @@ class Update extends Base
throw new Exception(Exception::VARIABLE_CANNOT_UNSET_SECRET);
}
- $variable
- ->setAttribute('key', $key)
- ->setAttribute('value', $value ?? $variable->getAttribute('value'))
- ->setAttribute('secret', $secret ?? $variable->getAttribute('secret'))
- ->setAttribute('search', implode(' ', [$variableId, $site->getId(), $key, 'site']));
+ if (\is_null($key) && \is_null($value) && \is_null($secret)) {
+ throw new Exception(Exception::GENERAL_ARGUMENT_INVALID);
+ }
+
+ $updates = new Document();
+
+ if (!\is_null($key)) {
+ $updates->setAttribute('key', $key);
+ $updates->setAttribute('search', implode(' ', [$variableId, $site->getId(), $key, 'site']));
+ }
+
+ if (!\is_null($value)) {
+ $updates->setAttribute('value', $value);
+ }
+
+ if (!\is_null($secret)) {
+ $updates->setAttribute('secret', $secret);
+ }
try {
- $dbForProject->updateDocument('variables', $variable->getId(), new Document([
- 'key' => $variable->getAttribute('key'),
- 'value' => $variable->getAttribute('value'),
- 'secret' => $variable->getAttribute('secret'),
- 'search' => $variable->getAttribute('search'),
- ]));
+ $variable = $dbForProject->updateDocument('variables', $variable->getId(), $updates);
} catch (DuplicateException $th) {
throw new Exception(Exception::VARIABLE_ALREADY_EXISTS);
}
@@ -108,6 +120,8 @@ class Update extends Base
'live' => false,
]));
+ $queueForEvents->setParam('variableId', $variable->getId());
+
$response->dynamic($variable, Response::MODEL_VARIABLE);
}
}
diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Variables/XList.php b/src/Appwrite/Platform/Modules/Sites/Http/Variables/XList.php
index 669aa8be98..1270fe4925 100644
--- a/src/Appwrite/Platform/Modules/Sites/Http/Variables/XList.php
+++ b/src/Appwrite/Platform/Modules/Sites/Http/Variables/XList.php
@@ -7,12 +7,18 @@ use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
+use Appwrite\Utopia\Database\Validator\Queries\Variables;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
+use Utopia\Database\Exception\Order as OrderException;
+use Utopia\Database\Exception\Query as QueryException;
+use Utopia\Database\Query;
+use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
+use Utopia\Validator\Boolean;
class XList extends Base
{
@@ -51,13 +57,20 @@ class XList extends Base
)
)
->param('siteId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Site unique ID.', false, ['dbForProject'])
+ ->param('queries', [], new Variables(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Variables::ALLOWED_ATTRIBUTES), true)
+ ->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('response')
->inject('dbForProject')
->callback($this->action(...));
}
+ /**
+ * @param array $queries
+ */
public function action(
string $siteId,
+ array $queries,
+ bool $includeTotal,
Response $response,
Database $dbForProject
) {
@@ -67,9 +80,51 @@ class XList extends Base
throw new Exception(Exception::SITE_NOT_FOUND);
}
+ try {
+ $queries = Query::parseQueries($queries);
+ } catch (QueryException $e) {
+ throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
+ }
+
+ $queries[] = Query::equal('resourceType', ['site']);
+ $queries[] = Query::equal('resourceInternalId', [$site->getSequence()]);
+ $queries[] = Query::orderAsc();
+
+ $cursor = Query::getCursorQueries($queries, false);
+ $cursor = \reset($cursor);
+
+ if ($cursor !== false) {
+ $validator = new Cursor();
+ if (!$validator->isValid($cursor)) {
+ throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
+ }
+
+ $variableId = $cursor->getValue();
+ $cursorDocument = $dbForProject->findOne('variables', [
+ Query::equal('$id', [$variableId]),
+ Query::equal('resourceType', ['site']),
+ Query::equal('resourceInternalId', [$site->getSequence()]),
+ ]);
+
+ if ($cursorDocument->isEmpty()) {
+ throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Variable '{$variableId}' for the 'cursor' value not found.");
+ }
+
+ $cursor->setValue($cursorDocument);
+ }
+
+ $filterQueries = Query::groupByType($queries)['filters'];
+
+ try {
+ $variables = $dbForProject->find('variables', $queries);
+ $total = $includeTotal ? $dbForProject->count('variables', $filterQueries, APP_LIMIT_COUNT) : 0;
+ } catch (OrderException $e) {
+ throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
+ }
+
$response->dynamic(new Document([
- 'variables' => $site->getAttribute('vars', []),
- 'total' => \count($site->getAttribute('vars', [])),
+ 'variables' => $variables,
+ 'total' => $total,
]), Response::MODEL_VARIABLE_LIST);
}
}
diff --git a/src/Appwrite/Platform/Modules/Storage/Config/CacheControl.php b/src/Appwrite/Platform/Modules/Storage/Config/CacheControl.php
new file mode 100644
index 0000000000..ef2ace34ff
--- /dev/null
+++ b/src/Appwrite/Platform/Modules/Storage/Config/CacheControl.php
@@ -0,0 +1,20 @@
+param('bucketId', '', new UID(), 'Bucket unique ID.')
->inject('response')
->inject('dbForProject')
- ->inject('queueForDeletes')
+ ->inject('publisherForDeletes')
->inject('queueForEvents')
->callback($this->action(...));
}
@@ -62,7 +63,7 @@ class Delete extends Action
string $bucketId,
Response $response,
Database $dbForProject,
- DeleteEvent $queueForDeletes,
+ DeletePublisher $publisherForDeletes,
Event $queueForEvents
) {
$bucket = $dbForProject->getDocument('buckets', $bucketId);
@@ -75,9 +76,11 @@ class Delete extends Action
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove bucket from DB');
}
- $queueForDeletes
- ->setType(DELETE_TYPE_DOCUMENT)
- ->setDocument($bucket);
+ $publisherForDeletes->enqueue(new DeleteMessage(
+ project: $queueForEvents->getProject(),
+ type: DELETE_TYPE_DOCUMENT,
+ document: $bucket,
+ ));
$queueForEvents
->setParam('bucketId', $bucket->getId())
diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Delete.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Delete.php
index 5b44c61d18..6d8781d484 100644
--- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Delete.php
+++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Delete.php
@@ -2,8 +2,9 @@
namespace Appwrite\Platform\Modules\Storage\Http\Buckets\Files;
-use Appwrite\Event\Delete as DeleteEvent;
use Appwrite\Event\Event;
+use Appwrite\Event\Message\Delete as DeleteMessage;
+use Appwrite\Event\Publisher\Delete as DeletePublisher;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@@ -64,7 +65,7 @@ class Delete extends Action
->inject('dbForProject')
->inject('queueForEvents')
->inject('deviceForFiles')
- ->inject('queueForDeletes')
+ ->inject('publisherForDeletes')
->inject('authorization')
->inject('user')
->callback($this->action(...));
@@ -77,7 +78,7 @@ class Delete extends Action
Database $dbForProject,
Event $queueForEvents,
Device $deviceForFiles,
- DeleteEvent $queueForDeletes,
+ DeletePublisher $publisherForDeletes,
Authorization $authorization,
User $user,
) {
@@ -126,11 +127,12 @@ class Delete extends Action
}
if ($deviceDeleted) {
- $queueForDeletes
- ->setType(DELETE_TYPE_CACHE_BY_RESOURCE)
- ->setResourceType('bucket/' . $bucket->getId())
- ->setResource('file/' . $fileId)
- ;
+ $publisherForDeletes->enqueue(new DeleteMessage(
+ project: $queueForEvents->getProject(),
+ type: DELETE_TYPE_CACHE_BY_RESOURCE,
+ resource: 'file/' . $fileId,
+ resourceType: 'bucket/' . $bucket->getId(),
+ ));
try {
if ($fileSecurity && !$valid) {
diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Preview/Get.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Preview/Get.php
index ff4844468c..68bc2cabae 100644
--- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Preview/Get.php
+++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Preview/Get.php
@@ -4,6 +4,8 @@ namespace Appwrite\Platform\Modules\Storage\Http\Buckets\Files\Preview;
use Appwrite\Extend\Exception;
use Appwrite\OpenSSL\OpenSSL;
+use Appwrite\Platform\Modules\Storage\Config\CacheControl;
+use Appwrite\Platform\Modules\Storage\Config\StorageCacheControl;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
@@ -94,6 +96,7 @@ class Get extends Action
->inject('project')
->inject('authorization')
->inject('user')
+ ->inject('cacheControlForStorage')
->callback($this->action(...));
}
@@ -120,7 +123,8 @@ class Get extends Action
Device $deviceForLocal,
Document $project,
Authorization $authorization,
- User $user
+ User $user,
+ callable $cacheControlForStorage
) {
if (!\extension_loaded('imagick')) {
@@ -239,28 +243,43 @@ class Get extends Action
throw new Exception(Exception::STORAGE_FILE_TYPE_UNSUPPORTED, $e->getMessage());
}
- $image->crop((int) $width, (int) $height, $gravity);
+ if ($width > 0 || $height > 0 || $gravity !== Image::GRAVITY_CENTER) {
+ Span::add('storage.transform.crop.width', $width);
+ Span::add('storage.transform.crop.height', $height);
+ Span::add('storage.transform.crop.gravity', $gravity);
+ $image->crop($width, $height, $gravity);
+ }
- if (!empty($opacity)) {
+ if ($opacity !== 1.0) {
+ Span::add('storage.transform.opacity', $opacity);
$image->setOpacity($opacity);
}
if (!empty($background)) {
+ Span::add('storage.transform.background', $background);
$image->setBackground('#' . $background);
}
- if (!empty($borderWidth)) {
+ if ($borderWidth > 0) {
+ Span::add('storage.transform.border.width', $borderWidth);
+ Span::add('storage.transform.border.color', $borderColor);
$image->setBorder($borderWidth, '#' . $borderColor);
}
- if (!empty($borderRadius)) {
+ if ($borderRadius > 0) {
+ Span::add('storage.transform.borderRadius', $borderRadius);
$image->setBorderRadius($borderRadius);
}
- if (!empty($rotation)) {
+ if ($rotation !== 0) {
+ Span::add('storage.transform.rotation', $rotation);
$image->setRotation(($rotation + 360) % 360);
}
+ if ($quality !== -1) {
+ Span::add('storage.transform.quality', $quality);
+ }
+
$data = $image->output($output, $quality);
$renderingTime = \microtime(true) - $startTime - $downloadTime - $decryptionTime - $decompressionTime;
@@ -292,8 +311,20 @@ class Get extends Action
}
}
+ $maxAge = 2592000; // 30 days
+ $cacheControl = $cacheControlForStorage(new StorageCacheControl(
+ source: CacheControl::SOURCE_ACTION,
+ user: $user,
+ maxAge: $maxAge,
+ project: $project,
+ bucket: $bucket,
+ file: $file,
+ resourceToken: $resourceToken,
+ fileSecurity: $fileSecurity,
+ ));
+
$response
- ->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days
+ ->addHeader('Cache-Control', $cacheControl)
->setContentType($contentType)
->file($data);
diff --git a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php
index e174029031..5500a56cbc 100644
--- a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php
+++ b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php
@@ -4,8 +4,10 @@ namespace Appwrite\Platform\Modules\Teams\Http\Memberships;
use Appwrite\Auth\Validator\Phone;
use Appwrite\Event\Event;
-use Appwrite\Event\Mail;
-use Appwrite\Event\Messaging;
+use Appwrite\Event\Message\Mail as MailMessage;
+use Appwrite\Event\Message\Messaging as MessagingMessage;
+use Appwrite\Event\Publisher\Mail as MailPublisher;
+use Appwrite\Event\Publisher\Messaging as MessagingPublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action;
use Appwrite\SDK\AuthType;
@@ -87,18 +89,19 @@ class Create extends Action
->inject('dbForProject')
->inject('authorization')
->inject('locale')
- ->inject('queueForMails')
- ->inject('queueForMessaging')
+ ->inject('publisherForMails')
+ ->inject('publisherForMessaging')
->inject('queueForEvents')
->inject('timelimit')
->inject('usage')
->inject('plan')
+ ->inject('platform')
->inject('proofForPassword')
->inject('proofForToken')
->callback($this->action(...));
}
- public function action(string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, User $user, Database $dbForProject, Authorization $authorization, Locale $locale, Mail $queueForMails, Messaging $queueForMessaging, Event $queueForEvents, callable $timelimit, Context $usage, array $plan, Password $proofForPassword, Token $proofForToken)
+ public function action(string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, User $user, Database $dbForProject, Authorization $authorization, Locale $locale, MailPublisher $publisherForMails, MessagingPublisher $publisherForMessaging, Event $queueForEvents, callable $timelimit, Context $usage, array $plan, array $platform, Password $proofForPassword, Token $proofForToken)
{
$isAppUser = $user->isApp($authorization->getRoles());
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
@@ -189,15 +192,15 @@ class Create extends Action
} catch (\Throwable) {
}
- if (($plan['supportsDisposableEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
+ if ((($project->getId() === 'console') || ($plan['supportsDisposableEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['disposableEmails'] ?? false) && ($emailMetadata['emailIsDisposable'] ?? false)) {
throw new Exception(Exception::USER_EMAIL_DISPOSABLE);
}
- if (($plan['supportsCanonicalEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
+ if ((($project->getId() === 'console') || ($plan['supportsCanonicalEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['canonicalEmails'] ?? false) && ($emailMetadata['emailIsCanonical'] ?? true) === false) {
throw new Exception(Exception::USER_EMAIL_NOT_CANONICAL);
}
- if (($plan['supportsFreeEmailValidation'] ?? false) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
+ if ((($project->getId() === 'console') || ($plan['supportsFreeEmailValidation'] ?? false)) && ($project->getAttribute('auths', [])['freeEmails'] ?? false) && ($emailMetadata['emailIsFree'] ?? false)) {
throw new Exception(Exception::USER_EMAIL_FREE);
}
@@ -345,6 +348,7 @@ class Create extends Action
$senderName = System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server');
$replyToEmail = '';
$replyToName = '';
+ $smtpConfig = [];
if ($smtpEnabled) {
if (! empty($smtp['senderEmail'])) {
@@ -362,13 +366,6 @@ class Create extends Action
$replyToName = $smtp['replyToName'];
}
- $queueForMails
- ->setSmtpHost($smtp['host'] ?? '')
- ->setSmtpPort($smtp['port'] ?? '')
- ->setSmtpUsername($smtp['username'] ?? '')
- ->setSmtpPassword($smtp['password'] ?? '')
- ->setSmtpSecure($smtp['secure'] ?? '');
-
if (! empty($customTemplate)) {
if (! empty($customTemplate['senderEmail'])) {
$senderEmail = $customTemplate['senderEmail'];
@@ -389,11 +386,17 @@ class Create extends Action
$subject = $customTemplate['subject'] ?? $subject;
}
- $queueForMails
- ->setSmtpReplyToEmail($replyToEmail)
- ->setSmtpReplyToName($replyToName)
- ->setSmtpSenderEmail($senderEmail)
- ->setSmtpSenderName($senderName);
+ $smtpConfig = [
+ 'host' => $smtp['host'] ?? '',
+ 'port' => $smtp['port'] ?? '',
+ 'username' => $smtp['username'] ?? '',
+ 'password' => $smtp['password'] ?? '',
+ 'secure' => $smtp['secure'] ?? '',
+ 'replyToEmail' => $replyToEmail,
+ 'replyToName' => $replyToName,
+ 'senderEmail' => $senderEmail,
+ 'senderName' => $senderName,
+ ];
}
$emailVariables = [
@@ -406,14 +409,17 @@ class Create extends Action
'project' => $projectName,
];
- $queueForMails
- ->setSubject($subject)
- ->setBody($body)
- ->setPreview($preview)
- ->setRecipient($invitee->getAttribute('email'))
- ->setName($invitee->getAttribute('name', ''))
- ->appendVariables($emailVariables)
- ->trigger();
+ $publisherForMails->enqueue(new MailMessage(
+ project: $project,
+ recipient: $invitee->getAttribute('email'),
+ name: $invitee->getAttribute('name', ''),
+ subject: $subject,
+ body: $body,
+ preview: $preview,
+ smtp: $smtpConfig,
+ variables: $emailVariables,
+ platform: $platform,
+ ));
} elseif (! empty($phone)) {
if (empty(System::getEnv('_APP_SMS_PROVIDER'))) {
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
@@ -431,11 +437,13 @@ class Create extends Action
],
]);
- $queueForMessaging
- ->setType(MESSAGE_SEND_TYPE_INTERNAL)
- ->setMessage($messageDoc)
- ->setRecipients([$phone])
- ->setProviderType('SMS');
+ $publisherForMessaging->enqueue(new MessagingMessage(
+ type: MESSAGE_SEND_TYPE_INTERNAL,
+ project: $project,
+ message: $messageDoc,
+ recipients: [$phone],
+ providerType: 'SMS',
+ ));
$helper = PhoneNumberUtil::getInstance();
try {
diff --git a/src/Appwrite/Platform/Modules/Teams/Http/Teams/Delete.php b/src/Appwrite/Platform/Modules/Teams/Http/Teams/Delete.php
index 0cb7c54a26..3bae031e06 100644
--- a/src/Appwrite/Platform/Modules/Teams/Http/Teams/Delete.php
+++ b/src/Appwrite/Platform/Modules/Teams/Http/Teams/Delete.php
@@ -2,8 +2,9 @@
namespace Appwrite\Platform\Modules\Teams\Http\Teams;
-use Appwrite\Event\Delete as DeleteEvent;
use Appwrite\Event\Event;
+use Appwrite\Event\Message\Delete as DeleteMessage;
+use Appwrite\Event\Publisher\Delete as DeletePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action;
use Appwrite\Platform\Workers\Deletes;
@@ -55,13 +56,13 @@ class Delete extends Action
->inject('response')
->inject('getProjectDB')
->inject('dbForProject')
- ->inject('queueForDeletes')
+ ->inject('publisherForDeletes')
->inject('queueForEvents')
->inject('project')
->callback($this->action(...));
}
- public function action(string $teamId, Response $response, callable $getProjectDB, Database $dbForProject, DeleteEvent $queueForDeletes, Event $queueForEvents, Document $project)
+ public function action(string $teamId, Response $response, callable $getProjectDB, Database $dbForProject, DeletePublisher $publisherForDeletes, Event $queueForEvents, Document $project)
{
$team = $dbForProject->getDocument('teams', $teamId);
@@ -79,15 +80,18 @@ class Delete extends Action
// Async delete
if ($project->getId() === 'console') {
- $queueForDeletes
- ->setType(DELETE_TYPE_TEAM_PROJECTS)
- ->setDocument($team)
- ->trigger();
+ $publisherForDeletes->enqueue(new DeleteMessage(
+ project: $project,
+ type: DELETE_TYPE_TEAM_PROJECTS,
+ document: $team,
+ ));
}
- $queueForDeletes
- ->setType(DELETE_TYPE_DOCUMENT)
- ->setDocument($team);
+ $publisherForDeletes->enqueue(new DeleteMessage(
+ project: $project,
+ type: DELETE_TYPE_DOCUMENT,
+ document: $team,
+ ));
$queueForEvents
->setParam('teamId', $team->getId())
diff --git a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Authorize/External/Update.php b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Authorize/External/Update.php
index 8b320535e9..a40d7fc6b9 100644
--- a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Authorize/External/Update.php
+++ b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Authorize/External/Update.php
@@ -2,7 +2,7 @@
namespace Appwrite\Platform\Modules\VCS\Http\GitHub\Authorize\External;
-use Appwrite\Event\Build;
+use Appwrite\Event\Publisher\Build as BuildPublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action;
use Appwrite\Platform\Modules\VCS\Http\GitHub\Deployment;
@@ -60,7 +60,7 @@ class Update extends Action
->inject('dbForPlatform')
->inject('authorization')
->inject('getProjectDB')
- ->inject('queueForBuilds')
+ ->inject('publisherForBuilds')
->inject('platform')
->callback($this->action(...));
}
@@ -75,7 +75,7 @@ class Update extends Action
Database $dbForPlatform,
Authorization $authorization,
callable $getProjectDB,
- Build $queueForBuilds,
+ BuildPublisher $publisherForBuilds,
array $platform
) {
$installation = $dbForPlatform->getDocument('installations', $installationId);
@@ -130,7 +130,7 @@ class Update extends Action
$providerCommitAuthor = $commitDetails["commitAuthor"] ?? '';
$providerCommitAuthorUrl = $commitDetails["commitAuthorUrl"] ?? '';
- $this->createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, true, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $platform);
+ $this->createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, true, $dbForPlatform, $authorization, $publisherForBuilds, $getProjectDB, $platform);
$response->noContent();
}
diff --git a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php
index 33d7e984fb..a6f0e7fd6d 100644
--- a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php
+++ b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php
@@ -2,8 +2,9 @@
namespace Appwrite\Platform\Modules\VCS\Http\GitHub;
-use Appwrite\Event\Build;
use Appwrite\Event\Event;
+use Appwrite\Event\Message\Build as BuildMessage;
+use Appwrite\Event\Publisher\Build as BuildPublisher;
use Appwrite\Extend\Exception;
use Appwrite\Filter\BranchDomain as BranchDomainFilter;
use Appwrite\Vcs\Comment;
@@ -20,6 +21,7 @@ use Utopia\Database\Validator\Authorization;
use Utopia\DSN\DSN;
use Utopia\Span\Span;
use Utopia\System\System;
+use Utopia\Validator\Contains;
use Utopia\VCS\Adapter\Git\GitHub;
use Utopia\VCS\Exception\RepositoryNotFound;
@@ -43,7 +45,7 @@ trait Deployment
bool $external,
Database $dbForPlatform,
Authorization $authorization,
- Build $queueForBuilds,
+ BuildPublisher $publisherForBuilds,
callable $getProjectDB,
array $platform,
) {
@@ -58,9 +60,9 @@ trait Deployment
$resourceType = $repository->getAttribute('resourceType');
$logBase = "vcs.github.event.repo.{$repositoryId}";
- Span::add("{$logBase}.projectId", $projectId);
- Span::add("{$logBase}.resourceId", $resourceId);
- Span::add("{$logBase}.resourceType", $resourceType);
+ Span::add('project.id', $projectId);
+ Span::add("{$logBase}.resource.id", $resourceId);
+ Span::add("{$logBase}.resource.type", $resourceType);
if ($resourceType !== "function" && $resourceType !== "site") {
continue;
@@ -94,6 +96,13 @@ trait Deployment
$resource = $authorization->skip(fn () => $dbForProject->getDocument($resourceCollection, $resourceId));
$resourceInternalId = $resource->getSequence();
+ $validator = new Contains(VCS_DEPLOYMENT_SKIP_PATTERNS);
+ if ($validator->isValid($providerCommitMessage)) {
+ Span::add("{$logBase}.build.skipped.reason", $validator->getDescription());
+ Span::add("{$logBase}.build.skipped", 'true');
+ continue;
+ }
+
$deploymentId = ID::unique();
$repositoryId = $repository->getId();
$repositoryInternalId = $repository->getSequence();
@@ -528,14 +537,16 @@ trait Deployment
$queueName = $this->getBuildQueueName($project, $dbForPlatform, $authorization);
- $queueForBuilds
- ->setQueue($queueName)
- ->setType(BUILD_TYPE_DEPLOYMENT)
- ->setResource($resource)
- ->setDeployment($deployment)
- ->setProject($project); // set the project because it won't be set for git deployments
-
- $queueForBuilds->trigger(); // must trigger here so that we create a build for each function/site
+ $publisherForBuilds->enqueue(
+ new BuildMessage(
+ project: $project,
+ resource: $resource,
+ deployment: $deployment,
+ type: BUILD_TYPE_DEPLOYMENT,
+ platform: $platform,
+ ),
+ new \Utopia\Queue\Queue($queueName)
+ );
Span::add("{$logBase}.build.triggered", 'true');
//TODO: Add event?
@@ -545,8 +556,6 @@ trait Deployment
}
}
- $queueForBuilds->reset(); // prevent shutdown hook from triggering again
-
if (!empty($errors)) {
throw new Exception(Exception::GENERAL_UNKNOWN, \implode("\n", $errors));
}
@@ -560,4 +569,5 @@ trait Deployment
{
return System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::BUILDS_QUEUE_NAME);
}
+
}
diff --git a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php
index e3dbcfa0e9..0b81504309 100644
--- a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php
+++ b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php
@@ -2,7 +2,7 @@
namespace Appwrite\Platform\Modules\VCS\Http\GitHub\Events;
-use Appwrite\Event\Build;
+use Appwrite\Event\Publisher\Build as BuildPublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action;
use Appwrite\Platform\Modules\VCS\Http\GitHub\Deployment;
@@ -41,7 +41,7 @@ class Create extends Action
->inject('dbForPlatform')
->inject('authorization')
->inject('getProjectDB')
- ->inject('queueForBuilds')
+ ->inject('publisherForBuilds')
->inject('platform')
->callback($this->action(...));
}
@@ -53,7 +53,7 @@ class Create extends Action
Database $dbForPlatform,
Authorization $authorization,
callable $getProjectDB,
- Build $queueForBuilds,
+ BuildPublisher $publisherForBuilds,
array $platform
) {
$this->preprocessEvent($request);
@@ -78,8 +78,8 @@ class Create extends Action
match ($event) {
$github::EVENT_INSTALLATION => $this->handleInstallationEvent($parsedPayload, $dbForPlatform, $authorization),
- $github::EVENT_PUSH => $this->handlePushEvent($parsedPayload, $githubAppId, $privateKey, $github, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $platform),
- $github::EVENT_PULL_REQUEST => $this->handlePullRequestEvent($parsedPayload, $privateKey, $githubAppId, $github, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $platform),
+ $github::EVENT_PUSH => $this->handlePushEvent($parsedPayload, $githubAppId, $privateKey, $github, $dbForPlatform, $authorization, $publisherForBuilds, $getProjectDB, $platform),
+ $github::EVENT_PULL_REQUEST => $this->handlePullRequestEvent($parsedPayload, $privateKey, $githubAppId, $github, $dbForPlatform, $authorization, $publisherForBuilds, $getProjectDB, $platform),
default => null,
};
@@ -129,7 +129,7 @@ class Create extends Action
GitHub $github,
Database $dbForPlatform,
Authorization $authorization,
- Build $queueForBuilds,
+ BuildPublisher $publisherForBuilds,
callable $getProjectDB,
array $platform,
) {
@@ -164,7 +164,7 @@ class Create extends Action
// Create new deployment only on push (not committed by us) and not when branch is deleted
if ($providerCommitAuthorEmail !== APP_VCS_GITHUB_EMAIL && !$providerBranchDeleted) {
- $this->createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthorName, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $platform);
+ $this->createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthorName, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForPlatform, $authorization, $publisherForBuilds, $getProjectDB, $platform);
}
}
@@ -175,7 +175,7 @@ class Create extends Action
GitHub $github,
Database $dbForPlatform,
Authorization $authorization,
- Build $queueForBuilds,
+ BuildPublisher $publisherForBuilds,
callable $getProjectDB,
array $platform,
) {
@@ -216,7 +216,7 @@ class Create extends Action
Query::orderDesc('$createdAt')
]));
- $this->createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $external, $dbForPlatform, $authorization, $queueForBuilds, $getProjectDB, $platform);
+ $this->createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $external, $dbForPlatform, $authorization, $publisherForBuilds, $getProjectDB, $platform);
} elseif ($action == "closed") {
// Allowed external contributions cleanup
diff --git a/src/Appwrite/Platform/Modules/VCS/Http/Installations/Delete.php b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Delete.php
index 26a9476941..5d90d6d231 100644
--- a/src/Appwrite/Platform/Modules/VCS/Http/Installations/Delete.php
+++ b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Delete.php
@@ -2,7 +2,8 @@
namespace Appwrite\Platform\Modules\VCS\Http\Installations;
-use Appwrite\Event\Delete as DeleteEvent;
+use Appwrite\Event\Message\Delete as DeleteMessage;
+use Appwrite\Event\Publisher\Delete as DeletePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action;
use Appwrite\SDK\AuthType;
@@ -11,6 +12,7 @@ use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
+use Utopia\Database\Document;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Text;
@@ -49,7 +51,8 @@ class Delete extends Action
->param('installationId', '', new Text(256), 'Installation Id')
->inject('response')
->inject('dbForPlatform')
- ->inject('queueForDeletes')
+ ->inject('publisherForDeletes')
+ ->inject('project')
->callback($this->action(...));
}
@@ -57,7 +60,8 @@ class Delete extends Action
string $installationId,
Response $response,
Database $dbForPlatform,
- DeleteEvent $queueForDeletes
+ DeletePublisher $publisherForDeletes,
+ Document $project,
) {
$installation = $dbForPlatform->getDocument('installations', $installationId);
@@ -69,9 +73,11 @@ class Delete extends Action
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove installation from DB');
}
- $queueForDeletes
- ->setType(DELETE_TYPE_DOCUMENT)
- ->setDocument($installation);
+ $publisherForDeletes->enqueue(new DeleteMessage(
+ project: $project,
+ type: DELETE_TYPE_DOCUMENT,
+ document: $installation,
+ ));
$response->noContent();
}
diff --git a/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Branches/XList.php b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Branches/XList.php
index 8ead94b7cb..fda462159f 100644
--- a/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Branches/XList.php
+++ b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Branches/XList.php
@@ -7,9 +7,12 @@ use Appwrite\Platform\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
+use Appwrite\Utopia\Database\Validator\Queries\Branches;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
+use Utopia\Database\Exception\Query as QueryException;
+use Utopia\Database\Query;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
use Utopia\Validator\Text;
@@ -49,6 +52,8 @@ class XList extends Action
))
->param('installationId', '', new Text(256), 'Installation Id')
->param('providerRepositoryId', '', new Text(256), 'Repository Id')
+ ->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
+ ->param('queries', [], new Branches(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit, offset, cursorAfter, and cursorBefore', true)
->inject('gitHub')
->inject('response')
->inject('dbForPlatform')
@@ -58,10 +63,18 @@ class XList extends Action
public function action(
string $installationId,
string $providerRepositoryId,
+ string $search,
+ array $queries,
GitHub $github,
Response $response,
Database $dbForPlatform
) {
+ try {
+ $queries = Query::parseQueries($queries);
+ } catch (QueryException $e) {
+ throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
+ }
+
$installation = $dbForPlatform->getDocument('installations', $installationId);
if ($installation->isEmpty()) {
@@ -85,11 +98,48 @@ class XList extends Action
$branches = $github->listBranches($owner, $repositoryName);
+ if (!empty($search)) {
+ $branches = \array_values(\array_filter($branches, fn (string $branch) => \stripos($branch, $search) !== false));
+ }
+
+ $total = \count($branches);
+ [
+ 'limit' => $limit,
+ 'offset' => $offset,
+ ] = Query::groupByType($queries);
+ $cursorQuery = \current(Query::getCursorQueries($queries, false));
+
+ $limit ??= APP_LIMIT_LIST_DEFAULT;
+ $offset ??= 0;
+
+ if ($cursorQuery instanceof Query) {
+ $cursor = $cursorQuery->getValue();
+ $cursorDirection = $cursorQuery->getMethod() === Query::TYPE_CURSOR_AFTER
+ ? Database::CURSOR_AFTER
+ : Database::CURSOR_BEFORE;
+
+ $cursorIndex = \array_search($cursor, $branches, true);
+ if ($cursorIndex === false) {
+ throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Branch '{$cursor}' for the 'cursor' value not found.");
+ }
+
+ $offset += $cursorDirection === Database::CURSOR_AFTER ? $cursorIndex + 1 : 0;
+
+ if ($cursorDirection === Database::CURSOR_BEFORE) {
+ $start = \max(0, $cursorIndex - $limit);
+ $branches = \array_slice($branches, $start, $cursorIndex - $start);
+ } else {
+ $branches = \array_slice($branches, $offset, $limit);
+ }
+ } else {
+ $branches = \array_slice($branches, $offset, $limit);
+ }
+
$response->dynamic(new Document([
'branches' => \array_map(function ($branch) {
return new Document(['name' => $branch]);
}, $branches),
- 'total' => \count($branches),
+ 'total' => $total,
]), Response::MODEL_BRANCH_LIST);
}
}
diff --git a/src/Appwrite/Platform/Tasks/Interval.php b/src/Appwrite/Platform/Tasks/Interval.php
index 7308dc003f..836508c73d 100644
--- a/src/Appwrite/Platform/Tasks/Interval.php
+++ b/src/Appwrite/Platform/Tasks/Interval.php
@@ -101,11 +101,11 @@ class Interval extends Action
]);
$scanned = \count($rules);
- Span::add("interval.domainVerification.scanned", $scanned);
+ Span::add("interval.domain_verification.scanned", $scanned);
if ($scanned === 0) {
- Span::add("interval.domainVerification.processed", 0);
- Span::add("interval.domainVerification.failed", 0);
+ Span::add("interval.domain_verification.processed", 0);
+ Span::add("interval.domain_verification.failed", 0);
return; // No rules to verify
}
@@ -131,7 +131,7 @@ class Interval extends Action
}
}
- Span::add("interval.domainVerification.processed", $processed);
- Span::add("interval.domainVerification.failed", $failed);
+ Span::add("interval.domain_verification.processed", $processed);
+ Span::add("interval.domain_verification.failed", $failed);
}
}
diff --git a/src/Appwrite/Platform/Tasks/Maintenance.php b/src/Appwrite/Platform/Tasks/Maintenance.php
index fe803f1292..e43281545a 100644
--- a/src/Appwrite/Platform/Tasks/Maintenance.php
+++ b/src/Appwrite/Platform/Tasks/Maintenance.php
@@ -2,8 +2,9 @@
namespace Appwrite\Platform\Tasks;
-use Appwrite\Event\Delete;
+use Appwrite\Event\Message\Delete as DeleteMessage;
use Appwrite\Event\Publisher\Certificate;
+use Appwrite\Event\Publisher\Delete as DeletePublisher;
use DateInterval;
use DateTime;
use Utopia\Console;
@@ -30,11 +31,11 @@ class Maintenance extends Action
->inject('dbForPlatform')
->inject('console')
->inject('publisherForCertificates')
- ->inject('queueForDeletes')
+ ->inject('publisherForDeletes')
->callback($this->action(...));
}
- public function action(string $type, Database $dbForPlatform, Document $console, Certificate $publisherForCertificates, Delete $queueForDeletes): void
+ public function action(string $type, Database $dbForPlatform, Document $console, Certificate $publisherForCertificates, DeletePublisher $publisherForDeletes): void
{
Console::title('Maintenance V1');
Console::success(APP_NAME . ' maintenance process v1 has started');
@@ -59,7 +60,7 @@ class Maintenance extends Action
$delay = $next->getTimestamp() - $now->getTimestamp();
}
- $action = function () use ($interval, $cacheRetention, $schedulesDeletionRetention, $usageStatsRetentionHourly, $dbForPlatform, $console, $queueForDeletes, $publisherForCertificates) {
+ $action = function () use ($interval, $cacheRetention, $schedulesDeletionRetention, $usageStatsRetentionHourly, $dbForPlatform, $console, $publisherForDeletes, $publisherForCertificates) {
$time = DatabaseDateTime::now();
Console::info("[{$time}] Notifying workers with maintenance tasks every {$interval} seconds");
@@ -70,12 +71,12 @@ class Maintenance extends Action
$dbForPlatform->foreach(
'projects',
- function (Document $project) use ($queueForDeletes, $usageStatsRetentionHourly) {
- $queueForDeletes
- ->setType(DELETE_TYPE_MAINTENANCE)
- ->setProject($project)
- ->setUsageRetentionHourlyDateTime(DatabaseDateTime::addSeconds(new \DateTime(), -1 * $usageStatsRetentionHourly))
- ->trigger();
+ function (Document $project) use ($publisherForDeletes, $usageStatsRetentionHourly) {
+ $publisherForDeletes->enqueue(new DeleteMessage(
+ project: $project,
+ type: DELETE_TYPE_MAINTENANCE,
+ hourlyUsageRetentionDatetime: DatabaseDateTime::addSeconds(new \DateTime(), -1 * $usageStatsRetentionHourly),
+ ));
},
[
Query::equal('region', [System::getEnv('_APP_REGION', 'default')]),
@@ -85,17 +86,17 @@ class Maintenance extends Action
]
);
- $queueForDeletes
- ->setType(DELETE_TYPE_MAINTENANCE)
- ->setProject($console)
- ->setUsageRetentionHourlyDateTime(DatabaseDateTime::addSeconds(new \DateTime(), -1 * $usageStatsRetentionHourly))
- ->trigger();
+ $publisherForDeletes->enqueue(new DeleteMessage(
+ project: $console,
+ type: DELETE_TYPE_MAINTENANCE,
+ hourlyUsageRetentionDatetime: DatabaseDateTime::addSeconds(new \DateTime(), -1 * $usageStatsRetentionHourly),
+ ));
- $this->notifyDeleteConnections($queueForDeletes);
+ $this->notifyDeleteConnections($publisherForDeletes);
$this->renewCertificates($dbForPlatform, $publisherForCertificates);
- $this->notifyDeleteCache($cacheRetention, $queueForDeletes);
- $this->notifyDeleteSchedules($schedulesDeletionRetention, $queueForDeletes);
- $this->notifyDeleteCSVExports($queueForDeletes);
+ $this->notifyDeleteCache($cacheRetention, $publisherForDeletes);
+ $this->notifyDeleteSchedules($schedulesDeletionRetention, $publisherForDeletes);
+ $this->notifyDeleteCSVExports($publisherForDeletes);
};
if ($type === 'loop') {
@@ -109,19 +110,17 @@ class Maintenance extends Action
}
}
- private function notifyDeleteConnections(Delete $queueForDeletes): void
+ private function notifyDeleteConnections(DeletePublisher $publisherForDeletes): void
{
- $queueForDeletes
- ->setType(DELETE_TYPE_REALTIME)
- ->setDatetime(DatabaseDateTime::addSeconds(new \DateTime(), -60))
- ->trigger();
+ $publisherForDeletes->enqueue(new DeleteMessage(
+ type: DELETE_TYPE_REALTIME,
+ datetime: DatabaseDateTime::addSeconds(new \DateTime(), -60),
+ ));
}
- private function notifyDeleteCSVExports(Delete $queueForDeletes): void
+ private function notifyDeleteCSVExports(DeletePublisher $publisherForDeletes): void
{
- $queueForDeletes
- ->setType(DELETE_TYPE_CSV_EXPORTS)
- ->trigger();
+ $publisherForDeletes->enqueue(new DeleteMessage(type: DELETE_TYPE_CSV_EXPORTS));
}
private function renewCertificates(Database $dbForPlatform, Certificate $publisherForCertificate): void
@@ -172,19 +171,19 @@ class Maintenance extends Action
}
}
- private function notifyDeleteCache($interval, Delete $queueForDeletes): void
+ private function notifyDeleteCache($interval, DeletePublisher $publisherForDeletes): void
{
- $queueForDeletes
- ->setType(DELETE_TYPE_CACHE_BY_TIMESTAMP)
- ->setDatetime(DatabaseDateTime::addSeconds(new \DateTime(), -1 * $interval))
- ->trigger();
+ $publisherForDeletes->enqueue(new DeleteMessage(
+ type: DELETE_TYPE_CACHE_BY_TIMESTAMP,
+ datetime: DatabaseDateTime::addSeconds(new \DateTime(), -1 * $interval),
+ ));
}
- private function notifyDeleteSchedules($interval, Delete $queueForDeletes): void
+ private function notifyDeleteSchedules($interval, DeletePublisher $publisherForDeletes): void
{
- $queueForDeletes
- ->setType(DELETE_TYPE_SCHEDULES)
- ->setDatetime(DatabaseDateTime::addSeconds(new \DateTime(), -1 * $interval))
- ->trigger();
+ $publisherForDeletes->enqueue(new DeleteMessage(
+ type: DELETE_TYPE_SCHEDULES,
+ datetime: DatabaseDateTime::addSeconds(new \DateTime(), -1 * $interval),
+ ));
}
}
diff --git a/src/Appwrite/Platform/Tasks/SDKs.php b/src/Appwrite/Platform/Tasks/SDKs.php
index b1580f0e68..5d52d905f6 100644
--- a/src/Appwrite/Platform/Tasks/SDKs.php
+++ b/src/Appwrite/Platform/Tasks/SDKs.php
@@ -7,6 +7,7 @@ use Appwrite\SDK\Language\Android;
use Appwrite\SDK\Language\Apple;
use Appwrite\SDK\Language\ClaudePlugin;
use Appwrite\SDK\Language\CLI;
+use Appwrite\SDK\Language\CodexPlugin;
use Appwrite\SDK\Language\CursorPlugin;
use Appwrite\SDK\Language\Dart;
use Appwrite\SDK\Language\Deno;
@@ -455,6 +456,9 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
case 'claude-plugin':
$config = new ClaudePlugin();
break;
+ case 'codex-plugin':
+ $config = new CodexPlugin();
+ break;
default:
throw new \Exception('Language "' . $language['key'] . '" not supported');
}
@@ -489,7 +493,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
->setGitRepo($language['gitUrl'])
->setGitRepoName($language['gitRepoName'])
->setGitUserName($language['gitUserName'])
- ->setLogo($cover)
+ ->setCoverImage($cover)
->setURL('https://appwrite.io')
->setShareText('Appwrite is a backend as a service for building web or mobile apps')
->setShareURL('http://appwrite.io')
diff --git a/src/Appwrite/Platform/Tasks/ScheduleExecutions.php b/src/Appwrite/Platform/Tasks/ScheduleExecutions.php
index cd7873bab6..49dd851b6d 100644
--- a/src/Appwrite/Platform/Tasks/ScheduleExecutions.php
+++ b/src/Appwrite/Platform/Tasks/ScheduleExecutions.php
@@ -2,7 +2,8 @@
namespace Appwrite\Platform\Tasks;
-use Appwrite\Event\Func;
+use Appwrite\Event\Message\Func as FunctionMessage;
+use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Swoole\Coroutine as Co;
use Utopia\Database\Database;
@@ -36,7 +37,10 @@ class ScheduleExecutions extends ScheduleBase
{
$intervalEnd = (new \DateTime())->modify('+' . self::ENQUEUE_TIMER . ' seconds');
- $queueForFunctions = new Func($this->publisherFunctions);
+ $publisherForFunctions = new FunctionPublisher(
+ $this->publisherFunctions,
+ new \Utopia\Queue\Queue(\Utopia\System\System::getEnv('_APP_FUNCTIONS_QUEUE_NAME', \Appwrite\Event\Event::FUNCTIONS_QUEUE_NAME), 'utopia-queue', \Appwrite\Event\Event::FUNCTIONS_QUEUE_TTL)
+ );
foreach ($this->schedules as $schedule) {
if (!$schedule['active']) {
@@ -63,23 +67,22 @@ class ScheduleExecutions extends ScheduleBase
$this->updateProjectAccess($schedule['project'], $dbForPlatform);
- \go(function () use ($queueForFunctions, $schedule, $scheduledAt, $delay, $data, $dbForPlatform) {
+ \go(function () use ($publisherForFunctions, $schedule, $scheduledAt, $delay, $data, $dbForPlatform) {
if ($delay > 0) {
Co::sleep($delay);
}
- $queueForFunctions->setType('schedule')
- // Set functionId instead of function as we don't have $dbForProject
- // TODO: Refactor to use function instead of functionId
- ->setFunctionId($schedule['resource']['resourceId'])
- ->setExecution($schedule['resource'])
- ->setMethod($data['method'] ?? 'POST')
- ->setPath($data['path'] ?? '/')
- ->setHeaders($data['headers'] ?? [])
- ->setBody($data['body'] ?? '')
- ->setProject($schedule['project'])
- ->setUserId($data['userId'] ?? '')
- ->trigger();
+ $publisherForFunctions->enqueue(new FunctionMessage(
+ project: $schedule['project'],
+ userId: $data['userId'] ?? '',
+ functionId: $schedule['resource']['resourceId'],
+ execution: $schedule['resource'],
+ type: 'schedule',
+ body: $data['body'] ?? '',
+ path: $data['path'] ?? '/',
+ headers: $data['headers'] ?? [],
+ method: $data['method'] ?? 'POST',
+ ));
$dbForPlatform->deleteDocument(
'schedules',
diff --git a/src/Appwrite/Platform/Tasks/ScheduleFunctions.php b/src/Appwrite/Platform/Tasks/ScheduleFunctions.php
index 75908c99c7..c1a1891386 100644
--- a/src/Appwrite/Platform/Tasks/ScheduleFunctions.php
+++ b/src/Appwrite/Platform/Tasks/ScheduleFunctions.php
@@ -2,13 +2,13 @@
namespace Appwrite\Platform\Tasks;
-use Appwrite\Event\Func;
+use Appwrite\Event\Message\Func as FunctionMessage;
+use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Cron\CronExpression;
use Utopia\Console;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Span\Span;
-use Utopia\System\System;
/**
* ScheduleFunctions
@@ -98,31 +98,29 @@ class ScheduleFunctions extends ScheduleBase
$this->updateProjectAccess($schedule['project'], $dbForPlatform);
- $queueForFunctions = new Func($this->publisherFunctions);
+ $publisherForFunctions = new FunctionPublisher(
+ $this->publisherFunctions,
+ new \Utopia\Queue\Queue(\Utopia\System\System::getEnv('_APP_FUNCTIONS_QUEUE_NAME', \Appwrite\Event\Event::FUNCTIONS_QUEUE_NAME), 'utopia-queue', \Appwrite\Event\Event::FUNCTIONS_QUEUE_TTL)
+ );
- $queueForFunctions
- ->setType('schedule')
- ->setFunction($schedule['resource'])
- ->setMethod('POST')
- ->setPath('/')
- ->setProject($schedule['project']);
+ Span::init('schedule.functions.enqueue');
+ try {
+ Span::add('project.id', $schedule['project']->getId());
+ Span::add('function.id', $schedule['resource']->getId());
+ Span::add('schedule.id', $schedule['$id'] ?? '');
- $projectDoc = $schedule['project'];
- $functionDoc = $schedule['resource'];
- $traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
- $traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
- if ($traceProjectId !== '' && $traceFunctionId !== '' && $projectDoc->getId() === $traceProjectId && $functionDoc->getId() === $traceFunctionId) {
- Span::init('execution.trace.v1_functions_enqueue');
- Span::add('datetime', gmdate('c'));
- Span::add('projectId', $projectDoc->getId());
- Span::add('functionId', $functionDoc->getId());
- Span::add('scheduleId', $schedule['$id'] ?? '');
+ $publisherForFunctions->enqueue(new FunctionMessage(
+ project: $schedule['project'],
+ function: $schedule['resource'],
+ type: 'schedule',
+ method: 'POST',
+ path: '/',
+ ));
+
+ $this->recordEnqueueDelay($delayConfig['nextDate']);
+ } finally {
Span::current()?->finish();
}
-
- $queueForFunctions->trigger();
-
- $this->recordEnqueueDelay($delayConfig['nextDate']);
}
});
}
diff --git a/src/Appwrite/Platform/Tasks/ScheduleMessages.php b/src/Appwrite/Platform/Tasks/ScheduleMessages.php
index 57f6dd8002..634fb26dc2 100644
--- a/src/Appwrite/Platform/Tasks/ScheduleMessages.php
+++ b/src/Appwrite/Platform/Tasks/ScheduleMessages.php
@@ -2,14 +2,20 @@
namespace Appwrite\Platform\Tasks;
-use Appwrite\Event\Messaging;
+use Appwrite\Event\Event;
+use Appwrite\Event\Message\Messaging as MessagingMessage;
+use Appwrite\Event\Publisher\Messaging as MessagingPublisher;
use Utopia\Database\Database;
+use Utopia\Queue\Queue;
+use Utopia\System\System;
class ScheduleMessages extends ScheduleBase
{
public const UPDATE_TIMER = 3; // seconds
public const ENQUEUE_TIMER = 4; // seconds
+ private ?MessagingPublisher $publisherForMessaging = null;
+
public static function getName(): string
{
return 'schedule-messages';
@@ -27,6 +33,11 @@ class ScheduleMessages extends ScheduleBase
protected function enqueueResources(Database $dbForPlatform, callable $getProjectDB): void
{
+ $publisherForMessaging = $this->publisherForMessaging ??= new MessagingPublisher(
+ $this->publisherMessaging,
+ new Queue(System::getEnv('_APP_MESSAGING_QUEUE_NAME', Event::MESSAGING_QUEUE_NAME))
+ );
+
foreach ($this->schedules as $schedule) {
if (!$schedule['active']) {
continue;
@@ -39,16 +50,14 @@ class ScheduleMessages extends ScheduleBase
continue;
}
- \go(function () use ($schedule, $scheduledAt, $dbForPlatform) {
- $queueForMessaging = new Messaging($this->publisherMessaging);
-
+ \go(function () use ($schedule, $scheduledAt, $dbForPlatform, $publisherForMessaging) {
$this->updateProjectAccess($schedule['project'], $dbForPlatform);
- $queueForMessaging
- ->setType(MESSAGE_SEND_TYPE_EXTERNAL)
- ->setMessageId($schedule['resourceId'])
- ->setProject($schedule['project'])
- ->trigger();
+ $publisherForMessaging->enqueue(new MessagingMessage(
+ type: MESSAGE_SEND_TYPE_EXTERNAL,
+ project: $schedule['project'],
+ messageId: $schedule['resourceId'],
+ ));
$dbForPlatform->deleteDocument(
'schedules',
diff --git a/src/Appwrite/Platform/Tasks/Specs.php b/src/Appwrite/Platform/Tasks/Specs.php
index 82020b05b1..c8120bd017 100644
--- a/src/Appwrite/Platform/Tasks/Specs.php
+++ b/src/Appwrite/Platform/Tasks/Specs.php
@@ -163,6 +163,12 @@ class Specs extends Action
'description' => 'Your secret dev API key',
'in' => 'header',
],
+ 'Cookie' => [
+ 'type' => 'apiKey',
+ 'name' => 'Cookie',
+ 'description' => 'The user cookie to authenticate with. Used by SDKs that forward an incoming Cookie header in server-side runtimes.',
+ 'in' => 'header',
+ ],
'ImpersonateUserId' => [
'type' => 'apiKey',
'name' => 'X-Appwrite-Impersonate-User-Id',
@@ -219,6 +225,18 @@ class Specs extends Action
'description' => 'The user agent string of the client that made the request',
'in' => 'header',
],
+ 'DevKey' => [
+ 'type' => 'apiKey',
+ 'name' => 'X-Appwrite-Dev-Key',
+ 'description' => 'Your secret dev API key',
+ 'in' => 'header',
+ ],
+ 'Cookie' => [
+ 'type' => 'apiKey',
+ 'name' => 'Cookie',
+ 'description' => 'The user cookie to authenticate with. Used by SDKs that forward an incoming Cookie header in server-side runtimes.',
+ 'in' => 'header',
+ ],
'ImpersonateUserId' => [
'type' => 'apiKey',
'name' => 'X-Appwrite-Impersonate-User-Id',
@@ -272,7 +290,19 @@ class Specs extends Action
'Cookie' => [
'type' => 'apiKey',
'name' => 'Cookie',
- 'description' => 'The user cookie to authenticate with',
+ 'description' => 'The user cookie to authenticate with. Used by SDKs that forward an incoming Cookie header in server-side runtimes.',
+ 'in' => 'header',
+ ],
+ 'Session' => [
+ 'type' => 'apiKey',
+ 'name' => 'X-Appwrite-Session',
+ 'description' => 'The user session to authenticate with',
+ 'in' => 'header',
+ ],
+ 'DevKey' => [
+ 'type' => 'apiKey',
+ 'name' => 'X-Appwrite-Dev-Key',
+ 'description' => 'Your secret dev API key',
'in' => 'header',
],
'ImpersonateUserId' => [
diff --git a/src/Appwrite/Platform/Workers/Certificates.php b/src/Appwrite/Platform/Workers/Certificates.php
index 4d04a3c92c..4a31216599 100644
--- a/src/Appwrite/Platform/Workers/Certificates.php
+++ b/src/Appwrite/Platform/Workers/Certificates.php
@@ -4,9 +4,11 @@ namespace Appwrite\Platform\Workers;
use Appwrite\Certificates\Adapter as CertificatesAdapter;
use Appwrite\Event\Event;
-use Appwrite\Event\Func;
-use Appwrite\Event\Mail;
+use Appwrite\Event\Message\Func as FunctionMessage;
+use Appwrite\Event\Message\Mail as MailMessage;
use Appwrite\Event\Publisher\Certificate;
+use Appwrite\Event\Publisher\Func as FunctionPublisher;
+use Appwrite\Event\Publisher\Mail as MailPublisher;
use Appwrite\Event\Realtime;
use Appwrite\Event\Webhook;
use Appwrite\Extend\Exception as AppwriteException;
@@ -50,10 +52,10 @@ class Certificates extends Action
->desc('Certificates worker')
->inject('message')
->inject('dbForPlatform')
- ->inject('queueForMails')
+ ->inject('publisherForMails')
->inject('queueForEvents')
->inject('queueForWebhooks')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('queueForRealtime')
->inject('publisherForCertificates')
->inject('log')
@@ -66,10 +68,10 @@ class Certificates extends Action
/**
* @param Message $message
* @param Database $dbForPlatform
- * @param Mail $queueForMails
+ * @param MailPublisher $publisherForMails
* @param Event $queueForEvents
* @param Webhook $queueForWebhooks
- * @param Func $queueForFunctions
+ * @param FunctionPublisher $publisherForFunctions
* @param Realtime $queueForRealtime
* @param Certificate $publisherForCertificates
* @param Log $log
@@ -83,10 +85,10 @@ class Certificates extends Action
public function action(
Message $message,
Database $dbForPlatform,
- Mail $queueForMails,
+ MailPublisher $publisherForMails,
Event $queueForEvents,
Webhook $queueForWebhooks,
- Func $queueForFunctions,
+ FunctionPublisher $publisherForFunctions,
Realtime $queueForRealtime,
Certificate $publisherForCertificates,
Log $log,
@@ -112,11 +114,11 @@ class Certificates extends Action
switch ($action) {
case \Appwrite\Event\Certificate::ACTION_DOMAIN_VERIFICATION:
- $this->handleDomainVerificationAction($domain, $dbForPlatform, $queueForEvents, $queueForWebhooks, $queueForFunctions, $queueForRealtime, $publisherForCertificates, $log, $authorization, $validationDomain);
+ $this->handleDomainVerificationAction($domain, $dbForPlatform, $queueForEvents, $queueForWebhooks, $publisherForFunctions, $queueForRealtime, $publisherForCertificates, $log, $authorization, $validationDomain);
break;
case \Appwrite\Event\Certificate::ACTION_GENERATION:
- $this->handleCertificateGenerationAction($domain, $domainType, $dbForPlatform, $queueForMails, $queueForEvents, $queueForWebhooks, $queueForFunctions, $queueForRealtime, $log, $certificates, $authorization, $skipRenewCheck, $plan, $validationDomain);
+ $this->handleCertificateGenerationAction($domain, $domainType, $dbForPlatform, $publisherForMails, $queueForEvents, $queueForWebhooks, $publisherForFunctions, $queueForRealtime, $log, $certificates, $authorization, $skipRenewCheck, $plan, $validationDomain);
break;
default:
@@ -129,7 +131,7 @@ class Certificates extends Action
* @param Database $dbForPlatform
* @param Event $queueForEvents
* @param Webhook $queueForWebhooks
- * @param Func $queueForFunctions
+ * @param FunctionPublisher $publisherForFunctions
* @param Realtime $queueForRealtime
* @param Certificate $publisherForCertificates
* @param Log $log
@@ -145,7 +147,7 @@ class Certificates extends Action
Database $dbForPlatform,
Event $queueForEvents,
Webhook $queueForWebhooks,
- Func $queueForFunctions,
+ FunctionPublisher $publisherForFunctions,
Realtime $queueForRealtime,
Certificate $publisherForCertificates,
Log $log,
@@ -184,7 +186,7 @@ class Certificates extends Action
$rule->setAttribute('logs', $logs);
} finally {
// Update rule and emit events
- $this->updateRuleAndSendEvents($rule, $dbForPlatform, $queueForEvents, $queueForWebhooks, $queueForFunctions, $queueForRealtime);
+ $this->updateRuleAndSendEvents($rule, $dbForPlatform, $queueForEvents, $queueForWebhooks, $publisherForFunctions, $queueForRealtime);
}
// Issue a TLS certificate when domain is verified
@@ -209,10 +211,10 @@ class Certificates extends Action
* @param Domain $domain
* @param ?string $domainType
* @param Database $dbForPlatform
- * @param Mail $queueForMails
+ * @param MailPublisher $publisherForMails
* @param Event $queueForEvents
* @param Webhook $queueForWebhooks
- * @param Func $queueForFunctions
+ * @param FunctionPublisher $publisherForFunctions
* @param Realtime $queueForRealtime
* @param Log $log
* @param CertificatesAdapter $certificates
@@ -233,10 +235,10 @@ class Certificates extends Action
Domain $domain,
?string $domainType,
Database $dbForPlatform,
- Mail $queueForMails,
+ MailPublisher $publisherForMails,
Event $queueForEvents,
Webhook $queueForWebhooks,
- Func $queueForFunctions,
+ FunctionPublisher $publisherForFunctions,
Realtime $queueForRealtime,
Log $log,
CertificatesAdapter $certificates,
@@ -358,7 +360,7 @@ class Certificates extends Action
$rule->setAttribute('status', RULE_STATUS_CERTIFICATE_GENERATION_FAILED);
// Send email to security email
- $this->notifyError($domain->get(), $e->getMessage(), $attempts, $queueForMails, $plan);
+ $this->notifyError($domain->get(), $e->getMessage(), $attempts, $publisherForMails, $plan);
throw $e;
} finally {
@@ -369,7 +371,7 @@ class Certificates extends Action
// Update rule and emit events
$rule->setAttribute('certificateId', $certificate->getId());
$rule->setAttribute('logs', $logs);
- $this->updateRuleAndSendEvents($rule, $dbForPlatform, $queueForEvents, $queueForWebhooks, $queueForFunctions, $queueForRealtime);
+ $this->updateRuleAndSendEvents($rule, $dbForPlatform, $queueForEvents, $queueForWebhooks, $publisherForFunctions, $queueForRealtime);
}
}
@@ -415,7 +417,7 @@ class Certificates extends Action
* @param Database $dbForPlatform Database connection for console
* @param Event $queueForEvents Event publisher for events
* @param Webhook $queueForWebhooks Webhook publisher for webhooks
- * @param Func $queueForFunctions Function publisher for functions
+ * @param FunctionPublisher $publisherForFunctions Function publisher for functions
* @param Realtime $queueForRealtime Realtime publisher for realtime events
*
* @return void
@@ -425,7 +427,7 @@ class Certificates extends Action
Database $dbForPlatform,
Event $queueForEvents,
Webhook $queueForWebhooks,
- Func $queueForFunctions,
+ FunctionPublisher $publisherForFunctions,
Realtime $queueForRealtime
): void {
$rule = $dbForPlatform->updateDocument('rules', $rule->getId(), new Document([
@@ -458,9 +460,15 @@ class Certificates extends Action
->trigger();
/** Trigger Functions */
- $queueForFunctions
- ->from($queueForEvents)
- ->trigger();
+ $publisherForFunctions->enqueue(FunctionMessage::fromEvent(
+ event: $queueForEvents->getEvent(),
+ params: $queueForEvents->getParams(),
+ project: $queueForEvents->getProject(),
+ user: $queueForEvents->getUser(),
+ userId: $queueForEvents->getUserId(),
+ payload: $queueForEvents->getPayload(),
+ platform: $queueForEvents->getPlatform(),
+ ));
/** Trigger Realtime Events */
$queueForRealtime
@@ -524,12 +532,12 @@ class Certificates extends Action
* @param string $domain Domain that caused the error
* @param string $errorMessage Verbose error message
* @param int $attempt How many times it failed already
- * @param Mail $queueForMails
+ * @param MailPublisher $publisherForMails
* @param array $plan
* @return void
* @throws Exception
*/
- private function notifyError(string $domain, string $errorMessage, int $attempt, Mail $queueForMails, array $plan): void
+ private function notifyError(string $domain, string $errorMessage, int $attempt, MailPublisher $publisherForMails, array $plan): void
{
// Log error into console
Console::warning('Cannot renew domain (' . $domain . ') on attempt no. ' . $attempt . ' certificate: ' . $errorMessage);
@@ -560,14 +568,14 @@ class Certificates extends Action
$subject = $locale->getText("emails.certificate.subject");
$preview = $locale->getText("emails.certificate.preview");
- $queueForMails
- ->setSubject($subject)
- ->setPreview($preview)
- ->setBody($body)
- ->setName('Appwrite Administrator')
- ->setBodyTemplate(__DIR__ . '/../../../../app/config/locale/templates/email-base-styled.tpl')
- ->setVariables($emailVariables)
- ->setRecipient(System::getEnv('_APP_EMAIL_CERTIFICATES', System::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS')))
- ->trigger();
+ $publisherForMails->enqueue(new MailMessage(
+ recipient: System::getEnv('_APP_EMAIL_CERTIFICATES', System::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS')),
+ name: 'Appwrite Administrator',
+ subject: $subject,
+ bodyTemplate: __DIR__ . '/../../../../app/config/locale/templates/email-base-styled.tpl',
+ body: $body,
+ preview: $preview,
+ variables: $emailVariables,
+ ));
}
}
diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php
index a5fe352b07..a58fc48098 100644
--- a/src/Appwrite/Platform/Workers/Deletes.php
+++ b/src/Appwrite/Platform/Workers/Deletes.php
@@ -5,7 +5,8 @@ namespace Appwrite\Platform\Workers;
use Appwrite\Certificates\Adapter as CertificatesAdapter;
use Appwrite\Deletes\Identities;
use Appwrite\Deletes\Targets;
-use Appwrite\Event\Delete as DeleteEvent;
+use Appwrite\Event\Message\Delete as DeleteMessage;
+use Appwrite\Event\Publisher\Delete as DeletePublisher;
use Appwrite\Extend\Exception;
use Executor\Executor;
use Throwable;
@@ -66,7 +67,7 @@ class Deletes extends Action
->inject('executionsRetentionCount')
->inject('auditRetention')
->inject('log')
- ->inject('queueForDeletes')
+ ->inject('publisherForDeletes')
->inject('getAudit')
->callback($this->action(...));
}
@@ -93,7 +94,7 @@ class Deletes extends Action
int $executionsRetentionCount,
string $auditRetention,
Log $log,
- DeleteEvent $queueForDeletes,
+ DeletePublisher $publisherForDeletes,
callable $getAudit,
): void {
$payload = $message->getPayload();
@@ -102,12 +103,13 @@ class Deletes extends Action
throw new Exception('Missing payload');
}
- $type = $payload['type'] ?? '';
- $datetime = $payload['datetime'] ?? null;
- $hourlyUsageRetentionDatetime = $payload['hourlyUsageRetentionDatetime'] ?? null;
- $resource = $payload['resource'] ?? null;
- $resourceType = $payload['resourceType'] ?? null;
- $document = new Document($payload['document'] ?? []);
+ $deleteMessage = DeleteMessage::fromArray($payload);
+ $type = $deleteMessage->type;
+ $datetime = $deleteMessage->datetime;
+ $hourlyUsageRetentionDatetime = $deleteMessage->hourlyUsageRetentionDatetime;
+ $resource = $deleteMessage->resource;
+ $resourceType = $deleteMessage->resourceType;
+ $document = $deleteMessage->document ?? new Document();
$log->addTag('projectId', $project->getId());
$log->addTag('type', $type);
@@ -142,7 +144,7 @@ class Deletes extends Action
case DELETE_TYPE_RULES:
$this->deleteRule($dbForPlatform, $document, $certificates);
break;
- case DELETE_TYPE_TRANSACTION:
+ case DELETE_TYPE_TRANSACTIONS:
$this->deleteTransactionLogs($getProjectDB, $document, $project);
break;
default:
@@ -214,13 +216,27 @@ class Deletes extends Action
$this->deleteUsageStats($project, $getProjectDB, $getLogsDB, $hourlyUsageRetentionDatetime);
$this->deleteExpiredSessions($project, $getProjectDB);
$this->deleteExpiredTransactions($project, $getProjectDB);
- $this->deleteOldDeployments($queueForDeletes, $project, $getProjectDB);
+ $this->deleteOldDeployments($publisherForDeletes, $project, $getProjectDB);
+ break;
+ case DELETE_TYPE_REPORT:
+ $this->deleteReport($dbForPlatform, $project, $document);
break;
default:
throw new \Exception('No delete operation for type: ' . \strval($type));
}
}
+ private function deleteReport(Database $dbForPlatform, Document $project, Document $report): void
+ {
+ $projectInternalId = $project->getSequence();
+ $reportInternalId = $report->getSequence();
+
+ $this->deleteByGroup('insights', [
+ Query::equal('projectInternalId', [$projectInternalId]),
+ Query::equal('reportInternalId', [$reportInternalId]),
+ ], $dbForPlatform);
+ }
+
private function cleanDatabase(
Document $databaseDoc,
callable $executionActionPerDatabase,
@@ -376,12 +392,12 @@ class Deletes extends Action
Targets::delete($getProjectDB($project), Query::equal('sessionInternalId', [$session->getSequence()]));
}
- private function deleteOldDeployments(DeleteEvent $queueForDeletes, Document $project, callable $getProjectDB): void
+ private function deleteOldDeployments(DeletePublisher $publisherForDeletes, Document $project, callable $getProjectDB): void
{
/** @var Database $dbForProject */
$dbForProject = $getProjectDB($project);
- $removalCallback = function (Document $resource) use ($dbForProject, $queueForDeletes, $project) {
+ $removalCallback = function (Document $resource) use ($dbForProject, $publisherForDeletes, $project) {
$retention = $resource->getAttribute('deploymentRetention', 0);
// 0 means unlimited - never delete
@@ -406,12 +422,12 @@ class Deletes extends Action
'deployments',
$queries,
$dbForProject,
- function (Document $deployment) use ($queueForDeletes, $project) {
- $queueForDeletes
- ->setType(DELETE_TYPE_DOCUMENT)
- ->setDocument($deployment)
- ->setProject($project)
- ->trigger();
+ function (Document $deployment) use ($publisherForDeletes, $project) {
+ $publisherForDeletes->enqueue(new DeleteMessage(
+ project: $project,
+ type: DELETE_TYPE_DOCUMENT,
+ document: $deployment,
+ ));
}
);
};
@@ -716,6 +732,26 @@ class Deletes extends Action
Console::error('Failed to delete schedules: ' . $th->getMessage());
}
+ // Delete Advisor insights
+ try {
+ $this->deleteByGroup('insights', [
+ Query::equal('projectInternalId', [$projectInternalId]),
+ Query::orderAsc()
+ ], $dbForPlatform);
+ } catch (Throwable $th) {
+ Console::error('Failed to delete insights: ' . $th->getMessage());
+ }
+
+ // Delete Advisor reports
+ try {
+ $this->deleteByGroup('reports', [
+ Query::equal('projectInternalId', [$projectInternalId]),
+ Query::orderAsc()
+ ], $dbForPlatform);
+ } catch (Throwable $th) {
+ Console::error('Failed to delete reports: ' . $th->getMessage());
+ }
+
/**
* @var Database $dbForProject
*/
diff --git a/src/Appwrite/Platform/Workers/Executions.php b/src/Appwrite/Platform/Workers/Executions.php
index 404b04ce76..8dbf10cae6 100644
--- a/src/Appwrite/Platform/Workers/Executions.php
+++ b/src/Appwrite/Platform/Workers/Executions.php
@@ -8,7 +8,6 @@ use Utopia\Database\Database;
use Utopia\Platform\Action;
use Utopia\Queue\Message;
use Utopia\Span\Span;
-use Utopia\System\System;
class Executions extends Action
{
@@ -41,19 +40,11 @@ class Executions extends Action
throw new Exception('Missing execution');
}
- $traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
- $traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
- $resourceId = $execution->getAttribute('resourceId', '');
- if ($traceProjectId !== '' && $traceFunctionId !== '' && $executionMessage->project->getId() === $traceProjectId && $resourceId === $traceFunctionId) {
- Span::init('execution.trace.executions_worker_upsert');
- Span::add('datetime', gmdate('c'));
- Span::add('projectId', $executionMessage->project->getId());
- Span::add('functionId', $resourceId);
- Span::add('executionId', $execution->getId());
- Span::add('deploymentId', $execution->getAttribute('deploymentId', ''));
- Span::add('resourceType', $execution->getAttribute('resourceType', ''));
- Span::current()?->finish();
- }
+ Span::add('project.id', $executionMessage->project->getId());
+ Span::add('function.id', $execution->getAttribute('resourceId', ''));
+ Span::add('execution.id', $execution->getId());
+ Span::add('deployment.id', $execution->getAttribute('deploymentId', ''));
+ Span::add('resource.type', $execution->getAttribute('resourceType', ''));
$dbForProject->upsertDocument('executions', $execution);
}
diff --git a/src/Appwrite/Platform/Workers/Functions.php b/src/Appwrite/Platform/Workers/Functions.php
index 8167fb975d..73c1db9444 100644
--- a/src/Appwrite/Platform/Workers/Functions.php
+++ b/src/Appwrite/Platform/Workers/Functions.php
@@ -5,11 +5,13 @@ namespace Appwrite\Platform\Workers;
use Ahc\Jwt\JWT;
use Appwrite\Bus\Events\ExecutionCompleted;
use Appwrite\Event\Event;
-use Appwrite\Event\Func;
+use Appwrite\Event\Message\Func as FunctionMessage;
+use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Event\Realtime;
use Appwrite\Event\Webhook;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Utopia\Response\Model\Execution;
+use Executor\Exception\Timeout as ExecutorTimeout;
use Executor\Executor;
use Utopia\Bus\Bus;
use Utopia\Config\Config;
@@ -45,7 +47,7 @@ class Functions extends Action
->inject('message')
->inject('dbForProject')
->inject('queueForWebhooks')
- ->inject('queueForFunctions')
+ ->inject('publisherForFunctions')
->inject('queueForRealtime')
->inject('queueForEvents')
->inject('bus')
@@ -60,7 +62,7 @@ class Functions extends Action
Message $message,
Database $dbForProject,
Webhook $queueForWebhooks,
- Func $queueForFunctions,
+ FunctionPublisher $publisherForFunctions,
Realtime $queueForRealtime,
Event $queueForEvents,
Bus $bus,
@@ -77,20 +79,27 @@ class Functions extends Action
);
}
- $type = $payload['type'] ?? '';
+ $functionMessage = FunctionMessage::fromArray($payload);
+ $type = $functionMessage->type;
- $events = $payload['events'] ?? [];
- $data = $payload['body'] ?? '';
- $eventData = $payload['payload'] ?? '';
- $platform = $payload['platform'] ?? Config::getParam('platform', []);
- $function = new Document($payload['function'] ?? []);
- $functionId = $payload['functionId'] ?? '';
- $user = new Document($payload['user'] ?? []);
- $userId = $payload['userId'] ?? '';
- $method = $payload['method'] ?? 'POST';
- $headers = $payload['headers'] ?? [];
- $path = $payload['path'] ?? '/';
- $jwt = $payload['jwt'] ?? '';
+ Span::add('project.id', $project->getId());
+ Span::add('payload.type', $type);
+ Span::add('queue.pid', $message->getPid());
+ Span::add('queue.name', $message->getQueue());
+ Span::add('message.timestamp', (string) $message->getTimestamp());
+
+ $events = $functionMessage->events;
+ $data = $functionMessage->body;
+ $eventData = $functionMessage->payload;
+ $platform = !empty($functionMessage->platform) ? $functionMessage->platform : Config::getParam('platform', []);
+ $function = $functionMessage->function ?? new Document();
+ $functionId = $functionMessage->functionId ?? '';
+ $user = $functionMessage->user ?? new Document();
+ $userId = $functionMessage->userId ?? '';
+ $method = $functionMessage->method ?: 'POST';
+ $headers = $functionMessage->headers;
+ $path = $functionMessage->path ?: '/';
+ $jwt = $functionMessage->jwt;
if ($user->isEmpty() && !empty($userId)) {
$user = $dbForProject->getDocument('users', $userId);
@@ -117,19 +126,7 @@ class Functions extends Action
$log->addTag('type', $type);
if (empty($events) && !$function->isEmpty()) {
- $traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
- $traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
- if ($traceProjectId !== '' && $traceFunctionId !== '' && $project->getId() === $traceProjectId && $function->getId() === $traceFunctionId) {
- Span::init('execution.trace.functions_worker_dequeue');
- Span::add('datetime', gmdate('c'));
- Span::add('projectId', $project->getId());
- Span::add('functionId', $function->getId());
- Span::add('payloadType', $type);
- Span::add('queuePid', $message->getPid());
- Span::add('queueName', $message->getQueue());
- Span::add('messageTimestamp', (string) $message->getTimestamp());
- Span::current()?->finish();
- }
+ Span::add('function.id', $function->getId());
}
if (!empty($events)) {
@@ -171,7 +168,7 @@ class Functions extends Action
log: $log,
dbForProject: $dbForProject,
queueForWebhooks: $queueForWebhooks,
- queueForFunctions: $queueForFunctions,
+ publisherForFunctions: $publisherForFunctions,
queueForRealtime: $queueForRealtime,
queueForEvents: $queueForEvents,
bus: $bus,
@@ -190,7 +187,7 @@ class Functions extends Action
user: $user,
jwt: null,
event: $events[0],
- eventData: \is_string($eventData) ? $eventData : \json_encode($eventData),
+ eventData: \json_encode($eventData) ?: null,
executionId: null,
);
Console::success('Triggered function: ' . $events[0]);
@@ -215,7 +212,7 @@ class Functions extends Action
log: $log,
dbForProject: $dbForProject,
queueForWebhooks: $queueForWebhooks,
- queueForFunctions: $queueForFunctions,
+ publisherForFunctions: $publisherForFunctions,
queueForRealtime: $queueForRealtime,
queueForEvents: $queueForEvents,
bus: $bus,
@@ -241,7 +238,7 @@ class Functions extends Action
log: $log,
dbForProject: $dbForProject,
queueForWebhooks: $queueForWebhooks,
- queueForFunctions: $queueForFunctions,
+ publisherForFunctions: $publisherForFunctions,
queueForRealtime: $queueForRealtime,
queueForEvents: $queueForEvents,
bus: $bus,
@@ -321,19 +318,11 @@ class Functions extends Action
'duration' => 0.0,
]);
- $traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
- $traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
- if ($traceProjectId !== '' && $traceFunctionId !== '' && $project->getId() === $traceProjectId && $function->getId() === $traceFunctionId) {
- Span::init('execution.trace.functions_worker_before_execution_completed_bus_fail');
- Span::add('datetime', gmdate('c'));
- Span::add('projectId', $project->getId());
- Span::add('functionId', $function->getId());
- Span::add('executionId', $execution->getId());
- Span::add('deploymentId', $execution->getAttribute('deploymentId', ''));
- Span::add('trigger', $trigger);
- Span::add('status', $execution->getAttribute('status', ''));
- Span::current()?->finish();
- }
+ Span::add('function.id', $function->getId());
+ Span::add('execution.id', $execution->getId());
+ Span::add('deployment.id', $execution->getAttribute('deploymentId', ''));
+ Span::add('execution.trigger', $trigger);
+ Span::add('execution.status', $execution->getAttribute('status', ''));
$bus->dispatch(new ExecutionCompleted(
execution: $execution->getArrayCopy(),
@@ -344,7 +333,7 @@ class Functions extends Action
/**
* @param Log $log
* @param Database $dbForProject
- * @param Func $queueForFunctions
+ * @param FunctionPublisher $publisherForFunctions
* @param Realtime $queueForRealtime
* @param Event $queueForEvents
* @param Document $project
@@ -366,7 +355,7 @@ class Functions extends Action
Log $log,
Database $dbForProject,
Webhook $queueForWebhooks,
- Func $queueForFunctions,
+ FunctionPublisher $publisherForFunctions,
Realtime $queueForRealtime,
Event $queueForEvents,
Bus $bus,
@@ -390,6 +379,10 @@ class Functions extends Action
$deploymentId = $function->getAttribute('deploymentId', '');
$spec = Config::getParam('specifications')[$function->getAttribute('runtimeSpecification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
+ Span::add('function.id', $functionId);
+ Span::add('deployment.id', $deploymentId);
+ Span::add('execution.trigger', $trigger);
+
$log->addTag('deploymentId', $deploymentId);
/** Check if deployment exists */
@@ -449,6 +442,8 @@ class Functions extends Action
}
$headers['x-appwrite-execution-id'] = $executionId;
+ Span::add('execution.id', $executionId);
+
$headersFiltered = [];
foreach ($headers as $key => $value) {
if (\in_array(\strtolower($key), FUNCTION_ALLOWLIST_HEADERS_REQUEST)) {
@@ -553,36 +548,28 @@ class Functions extends Action
$source = $deployment->getAttribute('buildPath', '');
$extension = str_ends_with($source, '.tar') ? 'tar' : 'tar.gz';
$command = $version === 'v2' ? '' : "cp /tmp/code.$extension /mnt/code/code.$extension && nohup helpers/start.sh \"$command\"";
- $traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
- $traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
- if ($traceProjectId !== '' && $traceFunctionId !== '' && $project->getId() === $traceProjectId && $functionId === $traceFunctionId) {
- Span::init('execution.trace.functions_worker_before_executor');
- Span::add('datetime', gmdate('c'));
- Span::add('projectId', $project->getId());
- Span::add('functionId', $functionId);
- Span::add('executionId', $executionId);
- Span::add('deploymentId', $deployment->getId());
- Span::add('trigger', $trigger);
- Span::current()?->finish();
+ try {
+ $executionResponse = $executor->createExecution(
+ projectId: $project->getId(),
+ deploymentId: $deploymentId,
+ body: \strlen($body) > 0 ? $body : null,
+ variables: $vars,
+ timeout: $function->getAttribute('timeout', 0),
+ image: $runtime['image'],
+ source: $source,
+ entrypoint: $deployment->getAttribute('entrypoint', ''),
+ version: $version,
+ path: $path,
+ method: $method,
+ headers: $headers,
+ runtimeEntrypoint: $command,
+ cpus: $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT,
+ memory: $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT,
+ logging: $function->getAttribute('logging', true),
+ );
+ } catch (ExecutorTimeout $th) {
+ throw new AppwriteException(AppwriteException::FUNCTION_ASYNCHRONOUS_TIMEOUT, previous: $th);
}
- $executionResponse = $executor->createExecution(
- projectId: $project->getId(),
- deploymentId: $deploymentId,
- body: \strlen($body) > 0 ? $body : null,
- variables: $vars,
- timeout: $function->getAttribute('timeout', 0),
- image: $runtime['image'],
- source: $source,
- entrypoint: $deployment->getAttribute('entrypoint', ''),
- version: $version,
- path: $path,
- method: $method,
- headers: $headers,
- runtimeEntrypoint: $command,
- cpus: $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT,
- memory: $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT,
- logging: $function->getAttribute('logging', true),
- );
$status = $executionResponse['statusCode'] >= 500 ? 'failed' : 'completed';
@@ -637,19 +624,8 @@ class Functions extends Action
$errorCode = $th->getCode();
} finally {
/** Persist final execution status and record usage */
- $traceProjectId = System::getEnv('_APP_TRACE_PROJECT_ID', '');
- $traceFunctionId = System::getEnv('_APP_TRACE_FUNCTION_ID', '');
- if ($traceProjectId !== '' && $traceFunctionId !== '' && $project->getId() === $traceProjectId && $functionId === $traceFunctionId) {
- Span::init('execution.trace.functions_worker_before_execution_completed_bus');
- Span::add('datetime', gmdate('c'));
- Span::add('projectId', $project->getId());
- Span::add('functionId', $functionId);
- Span::add('executionId', $execution->getId());
- Span::add('deploymentId', $execution->getAttribute('deploymentId', ''));
- Span::add('status', $execution->getAttribute('status', ''));
- Span::add('trigger', $trigger);
- Span::current()?->finish();
- }
+ Span::add('execution.status', $execution->getAttribute('status', ''));
+
$bus->dispatch(new ExecutionCompleted(
execution: $execution->getArrayCopy(),
project: $project->getArrayCopy(),
@@ -675,9 +651,15 @@ class Functions extends Action
->trigger();
/** Trigger Functions */
- $queueForFunctions
- ->from($queueForEvents)
- ->trigger();
+ $publisherForFunctions->enqueue(FunctionMessage::fromEvent(
+ event: $queueForEvents->getEvent(),
+ params: $queueForEvents->getParams(),
+ project: $queueForEvents->getProject(),
+ user: $queueForEvents->getUser(),
+ userId: $queueForEvents->getUserId(),
+ payload: $queueForEvents->getPayload(),
+ platform: $queueForEvents->getPlatform(),
+ ));
/** Trigger Realtime Events */
$queueForRealtime
diff --git a/src/Appwrite/Platform/Workers/Messaging.php b/src/Appwrite/Platform/Workers/Messaging.php
index 03adebc4b5..7af079e914 100644
--- a/src/Appwrite/Platform/Workers/Messaging.php
+++ b/src/Appwrite/Platform/Workers/Messaging.php
@@ -106,27 +106,20 @@ class Messaging extends Action
Span::add('message.type', $type);
- try {
- switch ($type) {
- case MESSAGE_SEND_TYPE_INTERNAL:
- $message = new Document($payload['message'] ?? []);
- $recipients = $payload['recipients'] ?? [];
+ switch ($type) {
+ case MESSAGE_SEND_TYPE_INTERNAL:
+ $message = new Document($payload['message'] ?? []);
+ $recipients = $payload['recipients'] ?? [];
- $this->sendInternalSMSMessage($message, $project, $recipients, $log);
- break;
- case MESSAGE_SEND_TYPE_EXTERNAL:
- $message = $dbForProject->getDocument('messages', $payload['messageId']);
+ $this->sendInternalSMSMessage($message, $project, $recipients, $log);
+ break;
+ case MESSAGE_SEND_TYPE_EXTERNAL:
+ $message = $dbForProject->getDocument('messages', $payload['messageId']);
- $this->sendExternalMessage($dbForProject, $message, $deviceForFiles, $project, $publisherForUsage);
- break;
- default:
- throw new \Exception('Unknown message type: ' . $type);
- }
- } catch (\Throwable $e) {
- Span::error($e);
- throw $e;
- } finally {
- Span::current()?->finish();
+ $this->sendExternalMessage($dbForProject, $message, $deviceForFiles, $project, $publisherForUsage);
+ break;
+ default:
+ throw new \Exception('Unknown message type: ' . $type);
}
}
@@ -162,21 +155,26 @@ class Messaging extends Action
}
if (\count($userIds) > 0) {
- $users = $dbForProject->find('users', [
- Query::equal('$id', $userIds),
- Query::limit(\count($userIds)),
- ]);
- foreach ($users as $user) {
- $targets = \array_filter($user->getAttribute('targets'), function (Document $target) use ($providerType) {
- return $target->getAttribute('providerType') === $providerType;
- });
+ $limit = 1000;
+ $offset = 0;
+
+ do {
+ $targets = $dbForProject->find('targets', [
+ Query::equal('userId', $userIds),
+ Query::select(['providerId', 'identifier']),
+ Query::equal('providerType', [$providerType]),
+ Query::limit($limit),
+ Query::offset($offset),
+ ]);
\array_push($allTargets, ...$targets);
- }
+ $offset += \count($targets);
+ } while (\count($targets) === $limit);
}
if (\count($targetIds) > 0) {
$targets = $dbForProject->find('targets', [
+ Query::select(['providerId', 'identifier']),
Query::equal('$id', $targetIds),
Query::equal('providerType', [$providerType]),
Query::limit(\count($targetIds)),
diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php
index 69f72b8e27..2dd59c7b4c 100644
--- a/src/Appwrite/Platform/Workers/Migrations.php
+++ b/src/Appwrite/Platform/Workers/Migrations.php
@@ -3,9 +3,10 @@
namespace Appwrite\Platform\Workers;
use Ahc\Jwt\JWT;
-use Appwrite\Event\Mail;
+use Appwrite\Event\Message\Mail as MailMessage;
use Appwrite\Event\Message\Migration;
use Appwrite\Event\Message\Usage as UsageMessage;
+use Appwrite\Event\Publisher\Mail as MailPublisher;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Event\Realtime;
use Appwrite\Extend\Exception;
@@ -30,6 +31,7 @@ use Utopia\Migration\Destination;
use Utopia\Migration\Destinations\Appwrite as DestinationAppwrite;
use Utopia\Migration\Destinations\CSV as DestinationCSV;
use Utopia\Migration\Destinations\JSON as DestinationJSON;
+use Utopia\Migration\Destinations\OnDuplicate;
use Utopia\Migration\Exception as MigrationException;
use Utopia\Migration\Resource;
use Utopia\Migration\Resources\Database\Database as ResourceDatabase;
@@ -101,7 +103,7 @@ class Migrations extends Action
->inject('queueForRealtime')
->inject('deviceForMigrations')
->inject('deviceForFiles')
- ->inject('queueForMails')
+ ->inject('publisherForMails')
->inject('usage')
->inject('publisherForUsage')
->inject('plan')
@@ -123,7 +125,7 @@ class Migrations extends Action
Realtime $queueForRealtime,
Device $deviceForMigrations,
Device $deviceForFiles,
- Mail $queueForMails,
+ MailPublisher $publisherForMails,
Context $usage,
UsagePublisher $publisherForUsage,
array $plan,
@@ -162,7 +164,7 @@ class Migrations extends Action
$this->processMigration(
$migration,
$queueForRealtime,
- $queueForMails,
+ $publisherForMails,
$usage,
$publisherForUsage,
$platform,
@@ -196,13 +198,13 @@ class Migrations extends Action
$projectDB = null;
$useAppwriteApiSource = false;
if ($source === SourceAppwrite::getName() && empty($credentials['projectId'])) {
- throw new \Exception('Source projectId is required for Appwrite migrations');
+ throw new Exception(Exception::MIGRATION_SOURCE_PROJECT_ID_REQUIRED);
}
if (! empty($credentials['projectId'])) {
$this->sourceProject = $this->dbForPlatform->getDocument('projects', $credentials['projectId']);
if ($this->sourceProject->isEmpty()) {
- throw new \Exception('Source project not found for provided projectId');
+ throw new Exception(Exception::MIGRATION_SOURCE_PROJECT_NOT_FOUND);
}
$sourceRegion = $this->sourceProject->getAttribute('region', 'default');
@@ -265,7 +267,7 @@ class Migrations extends Action
$this->deviceForMigrations,
$this->dbForProject,
),
- default => throw new \Exception('Invalid source type'),
+ default => throw new Exception(Exception::MIGRATION_SOURCE_TYPE_INVALID),
};
$resources = $migration->getAttribute('resources', []);
@@ -291,6 +293,10 @@ class Migrations extends Action
$this->dbForProject,
$this->getDatabasesDB,
Config::getParam('collections', [])['databases']['collections'],
+ $this->dbForPlatform,
+ $this->project->getSequence(),
+ OnDuplicate::tryFrom($options['onDuplicate'] ?? '') ?? OnDuplicate::Fail,
+ $this->resolveDestinationDatabaseDsn(...),
),
DestinationCSV::getName() => new DestinationCSV(
$this->deviceForFiles,
@@ -310,7 +316,20 @@ class Migrations extends Action
$options['filename'],
$options['columns'] ?? [],
),
- default => throw new \Exception('Invalid destination type'),
+ default => throw new Exception(Exception::MIGRATION_DESTINATION_TYPE_INVALID),
+ };
+ }
+
+ /**
+ * Legacy / tablesdb databases route to the destination project's DSN (same as a fresh
+ * Databases create), while documentsdb / vectorsdb keep the source DSN â the dedicated-DB
+ * backfill that would re-point them is not run during migrations.
+ */
+ private function resolveDestinationDatabaseDsn(ResourceDatabase $resource): string
+ {
+ return match ($resource->getType()) {
+ DATABASE_TYPE_DOCUMENTSDB, DATABASE_TYPE_VECTORSDB => (string) $resource->getDatabase(),
+ default => (string) $this->project->getAttribute('database', ''),
};
}
@@ -338,6 +357,55 @@ class Migrations extends Action
);
}
+ /**
+ * @return array
+ */
+ protected function getAPIKeyScopes(): array
+ {
+ return [
+ 'users.read',
+ 'users.write',
+ 'teams.read',
+ 'teams.write',
+ 'buckets.read',
+ 'buckets.write',
+ 'files.read',
+ 'files.write',
+ 'functions.read',
+ 'functions.write',
+ 'sites.read',
+ 'sites.write',
+ 'tokens.read',
+ 'tokens.write',
+ 'providers.read',
+ 'providers.write',
+ 'topics.read',
+ 'topics.write',
+ 'subscribers.read',
+ 'subscribers.write',
+ 'messages.read',
+ 'messages.write',
+ 'targets.read',
+ 'targets.write',
+ 'webhooks.read',
+ 'webhooks.write',
+ 'project.read',
+ 'project.write',
+ 'keys.read',
+ 'keys.write',
+ 'platforms.read',
+ 'platforms.write',
+ 'oauth2.read',
+ 'oauth2.write',
+ 'mocks.read',
+ 'mocks.write',
+ 'project.policies.read',
+ 'project.policies.write',
+ 'templates.read',
+ 'templates.write',
+ ];
+ }
+
/**
* @throws Exception
*/
@@ -358,48 +426,7 @@ class Migrations extends Action
METRIC_NETWORK_INBOUND,
METRIC_NETWORK_OUTBOUND,
],
- 'scopes' => [
- 'users.read',
- 'users.write',
- 'teams.read',
- 'teams.write',
- 'buckets.read',
- 'buckets.write',
- 'files.read',
- 'files.write',
- 'functions.read',
- 'functions.write',
- 'sites.read',
- 'sites.write',
- 'tokens.read',
- 'tokens.write',
- 'providers.read',
- 'providers.write',
- 'topics.read',
- 'topics.write',
- 'subscribers.read',
- 'subscribers.write',
- 'messages.read',
- 'messages.write',
- 'targets.read',
- 'targets.write',
- 'webhooks.read',
- 'webhooks.write',
- 'project.read',
- 'project.write',
- 'keys.read',
- 'keys.write',
- 'platforms.read',
- 'platforms.write',
- 'oauth2.read',
- 'oauth2.write',
- 'mocks.read',
- 'mocks.write',
- 'policies.read',
- 'policies.write',
- 'templates.read',
- 'templates.write',
- ]
+ 'scopes' => $this->getAPIKeyScopes(),
]);
return API_KEY_EPHEMERAL . '_' . $apiKey;
@@ -416,7 +443,7 @@ class Migrations extends Action
protected function processMigration(
Document $migration,
Realtime $queueForRealtime,
- Mail $queueForMails,
+ MailPublisher $publisherForMails,
Context $usage,
UsagePublisher $publisherForUsage,
array $platform,
@@ -428,6 +455,7 @@ class Migrations extends Action
$transfer = $source = $destination = null;
$aggregatedResources = [];
+ $caughtError = null;
$host = System::getEnv('_APP_MIGRATION_HOST');
if (empty($host)) {
@@ -521,7 +549,6 @@ class Migrations extends Action
if (!empty($sourceErrors) || ! empty($destinationErrors)) {
$migration->setAttribute('status', 'failed');
$migration->setAttribute('stage', 'finished');
- $migration->setAttribute('errors', $this->sanitizeErrors($sourceErrors, $destinationErrors));
return;
}
@@ -536,35 +563,69 @@ class Migrations extends Action
$migration->setAttribute('status', 'failed');
$migration->setAttribute('stage', 'finished');
- call_user_func($this->logError, $th, 'appwrite-worker', 'appwrite-queue-' . self::getName(), [
- 'migrationId' => $migration->getId(),
- 'source' => $migration->getAttribute('source') ?? '',
- 'destination' => $migration->getAttribute('destination') ?? '',
- ]);
+ $caughtError = $th;
+ // Mirror general.php's HTTP-error pattern: typed AppwriteException uses its
+ // registry-driven isPublishable() flag; library-thrown Migration\Exception is
+ // always user-facing; anything else is unknown and surfaced to Sentry.
+ if ($th instanceof Exception) {
+ $publish = $th->isPublishable();
+ } elseif ($th instanceof MigrationException) {
+ $publish = false;
+ } else {
+ $publish = true;
+ }
+
+ if ($publish) {
+ call_user_func($this->logError, $th, 'appwrite-worker', 'appwrite-queue-' . self::getName(), [
+ 'migrationId' => $migration->getId(),
+ 'source' => $migration->getAttribute('source') ?? '',
+ 'destination' => $migration->getAttribute('destination') ?? '',
+ ]);
+ }
} finally {
try {
+ $sourceErrors = $source?->getErrors() ?? [];
+ $destinationErrors = $destination?->getErrors() ?? [];
+
+ if ($caughtError !== null) {
+ if ($caughtError instanceof MigrationException) {
+ // library-thrown, message constructed by us
+ $bubbled = $caughtError;
+ } elseif ($caughtError instanceof Exception) {
+ // typed AppwriteException â message comes from the curated registry
+ $bubbled = new MigrationException(
+ resourceName: '',
+ resourceGroup: '',
+ message: $caughtError->getMessage(),
+ code: $caughtError->getCode(),
+ previous: $caughtError,
+ );
+ } else {
+ // unknown throwable â raw message may embed internal hostnames,
+ // DSNs, tokens, etc. Replace with a generic user-facing string;
+ // the original is preserved on `previous:` for Sentry.
+ $bubbled = new MigrationException(
+ resourceName: '',
+ resourceGroup: '',
+ message: 'Migration failed due to an unexpected error.',
+ code: $caughtError->getCode() ?: 500,
+ previous: $caughtError,
+ );
+ }
+ $destinationErrors[] = $bubbled;
+ }
+
+ $migration->setAttribute('errors', $this->sanitizeErrors(
+ $sourceErrors,
+ $destinationErrors,
+ ));
+
$this->updateMigrationDocument($migration, $project, $queueForRealtime);
if ($migration->getAttribute('status', '') === 'failed') {
Console::error('Migration(' . $migration->getSequence() . ':' . $migration->getId() . ') failed, Project(' . $this->project->getSequence() . ':' . $this->project->getId() . ')');
- $sourceErrors = $source?->getErrors() ?? [];
- $destinationErrors = $destination?->getErrors() ?? [];
-
- foreach ([...$sourceErrors, ...$destinationErrors] as $error) {
- /** @var MigrationException $error */
- if ($error->getCode() === 0 || $error->getCode() >= 500) {
- ($this->logError)($error, 'appwrite-worker', 'appwrite-queue-' . self::getName(), [
- 'migrationId' => $migration->getId(),
- 'source' => $migration->getAttribute('source') ?? '',
- 'destination' => $migration->getAttribute('destination') ?? '',
- 'resourceName' => $error->getResourceName(),
- 'resourceGroup' => $error->getResourceGroup(),
- ]);
- }
- }
-
$source?->error();
$destination?->error();
}
@@ -586,7 +647,7 @@ class Migrations extends Action
}
$destination_type = $migration->getAttribute('destination');
if ($destination_type === DestinationCSV::getName() || $destination_type === DestinationJSON::getName()) {
- $this->handleDataExportComplete($project, $migration, $queueForMails, $queueForRealtime, $platform, $authorization);
+ $this->handleDataExportComplete($project, $migration, $publisherForMails, $queueForRealtime, $platform, $authorization);
}
} finally {
$source?->cleanup();
@@ -613,7 +674,7 @@ class Migrations extends Action
*
* @param Document $project
* @param Document $migration
- * @param Mail $queueForMails
+ * @param MailPublisher $publisherForMails
* @param Realtime $queueForRealtime
* @param array $platform
* @param Authorization $authorization
@@ -622,7 +683,7 @@ class Migrations extends Action
protected function handleDataExportComplete(
Document $project,
Document $migration,
- Mail $queueForMails,
+ MailPublisher $publisherForMails,
Realtime $queueForRealtime,
array $platform,
Authorization $authorization,
@@ -674,7 +735,7 @@ class Migrations extends Action
project: $project,
user: $user,
options: $options,
- queueForMails: $queueForMails,
+ publisherForMails: $publisherForMails,
platform: $platform,
exportType: $migration->getAttribute('destination') === DestinationJSON::getName() ? 'JSON' : 'CSV',
sizeMB: $sizeMB
@@ -737,7 +798,7 @@ class Migrations extends Action
project: $project,
user: $user,
options: $options,
- queueForMails: $queueForMails,
+ publisherForMails: $publisherForMails,
platform: $platform,
exportType: $migration->getAttribute('destination') === DestinationJSON::getName() ? 'JSON' : 'CSV',
downloadUrl: $downloadUrl
@@ -751,7 +812,7 @@ class Migrations extends Action
* @param Document $project
* @param Document $user The user who triggered the operation
* @param array $options Migration options
- * @param Mail $queueForMails
+ * @param MailPublisher $publisherForMails
* @param array $platform
* @param string $downloadUrl Download URL for successful exports
* @param float $sizeMB File size in MB for failed exports
@@ -763,7 +824,7 @@ class Migrations extends Action
Document $project,
Document $user,
array $options,
- Mail $queueForMails,
+ MailPublisher $publisherForMails,
array $platform,
string $exportType = 'CSV',
string $downloadUrl = '',
@@ -833,17 +894,18 @@ class Migrations extends Action
'type' => $exportType,
];
- $queueForMails
- ->setProject($project)
- ->setSubject($subject)
- ->setPreview($preview)
- ->setBody($emailBody)
- ->setBodyTemplate(__DIR__ . '/../../../../app/config/locale/templates/email-base-styled.tpl')
- ->setVariables($emailVariables)
- ->setName($user->getAttribute('name', $user->getAttribute('email')))
- ->setRecipient($user->getAttribute('email'))
- ->setSenderName($platform['emailSenderName'])
- ->trigger();
+ $publisherForMails->enqueue(new MailMessage(
+ project: $project,
+ recipient: $user->getAttribute('email'),
+ name: $user->getAttribute('name', $user->getAttribute('email')),
+ subject: $subject,
+ bodyTemplate: __DIR__ . '/../../../../app/config/locale/templates/email-base-styled.tpl',
+ body: $emailBody,
+ preview: $preview,
+ variables: $emailVariables,
+ customMailOptions: ['senderName' => $platform['emailSenderName']],
+ platform: $platform,
+ ));
Console::info("CSV export {$emailType} notification email sent to " . $user->getAttribute('email'));
}
diff --git a/src/Appwrite/Platform/Workers/Webhooks.php b/src/Appwrite/Platform/Workers/Webhooks.php
index a7f4595966..973e487de5 100644
--- a/src/Appwrite/Platform/Workers/Webhooks.php
+++ b/src/Appwrite/Platform/Workers/Webhooks.php
@@ -2,8 +2,9 @@
namespace Appwrite\Platform\Workers;
-use Appwrite\Event\Mail;
+use Appwrite\Event\Message\Mail as MailMessage;
use Appwrite\Event\Message\Usage as UsageMessage;
+use Appwrite\Event\Publisher\Mail as MailPublisher;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Template\Template;
use Appwrite\Usage\Context as UsageContext;
@@ -36,7 +37,7 @@ class Webhooks extends Action
->inject('message')
->inject('project')
->inject('dbForPlatform')
- ->inject('queueForMails')
+ ->inject('publisherForMails')
->inject('publisherForUsage')
->inject('log')
->inject('plan')
@@ -47,14 +48,14 @@ class Webhooks extends Action
* @param Message $message
* @param Document $project
* @param Database $dbForPlatform
- * @param Mail $queueForMails
+ * @param MailPublisher $publisherForMails
* @param UsagePublisher $publisherForUsage
* @param Log $log
* @param array $plan
* @return void
* @throws Exception
*/
- public function action(Message $message, Document $project, Database $dbForPlatform, Mail $queueForMails, UsagePublisher $publisherForUsage, Log $log, array $plan): void
+ public function action(Message $message, Document $project, Database $dbForPlatform, MailPublisher $publisherForMails, UsagePublisher $publisherForUsage, Log $log, array $plan): void
{
$this->errors = [];
$payload = $message->getPayload();
@@ -73,7 +74,7 @@ class Webhooks extends Action
foreach ($project->getAttribute('webhooks', []) as $webhook) {
if (array_intersect($webhook->getAttribute('events', []), $events)) {
- $this->execute($events, $webhookPayload, $webhook, $user, $project, $dbForPlatform, $queueForMails, $publisherForUsage, $plan);
+ $this->execute($events, $webhookPayload, $webhook, $user, $project, $dbForPlatform, $publisherForMails, $publisherForUsage, $plan);
}
}
@@ -89,11 +90,11 @@ class Webhooks extends Action
* @param Document $user
* @param Document $project
* @param Database $dbForPlatform
- * @param Mail $queueForMails
+ * @param MailPublisher $publisherForMails
* @param array $plan
* @return void
*/
- private function execute(array $events, string $payload, Document $webhook, Document $user, Document $project, Database $dbForPlatform, Mail $queueForMails, UsagePublisher $publisherForUsage, array $plan): void
+ private function execute(array $events, string $payload, Document $webhook, Document $user, Document $project, Database $dbForPlatform, MailPublisher $publisherForMails, UsagePublisher $publisherForUsage, array $plan): void
{
if ($webhook->getAttribute('enabled') !== true) {
return;
@@ -171,7 +172,7 @@ class Webhooks extends Action
if ($attempts >= \intval(System::getEnv('_APP_WEBHOOK_MAX_FAILED_ATTEMPTS', '10'))) {
$webhook->setAttribute('enabled', false);
$updatePayload['enabled'] = false;
- $this->sendEmailAlert($attempts, $statusCode, $webhook, $project, $dbForPlatform, $queueForMails, $plan);
+ $this->sendEmailAlert($attempts, $statusCode, $webhook, $project, $dbForPlatform, $publisherForMails, $plan);
}
$dbForPlatform->updateDocument('webhooks', $webhook->getId(), new Document($updatePayload));
@@ -203,11 +204,11 @@ class Webhooks extends Action
* @param Document $webhook
* @param Document $project
* @param Database $dbForPlatform
- * @param Mail $queueForMails
+ * @param MailPublisher $publisherForMails
* @param array $plan
* @return void
*/
- public function sendEmailAlert(int $attempts, mixed $statusCode, Document $webhook, Document $project, Database $dbForPlatform, Mail $queueForMails, array $plan): void
+ public function sendEmailAlert(int $attempts, mixed $statusCode, Document $webhook, Document $project, Database $dbForPlatform, MailPublisher $publisherForMails, array $plan): void
{
$memberships = $dbForPlatform->find('memberships', [
Query::equal('teamInternalId', [$project->getAttribute('teamInternalId')]),
@@ -251,18 +252,16 @@ class Webhooks extends Action
->setParam('{{message}}', $template->render())
->setParam('{{year}}', date("Y"));
- $queueForMails
- ->setProject($project)
- ->setSubject($subject)
- ->setPreview($preview)
- ->setBody($body->render());
-
foreach ($users as $user) {
- $queueForMails
- ->setVariables(['user' => $user->getAttribute('name', '')])
- ->setName($user->getAttribute('name', ''))
- ->setRecipient($user->getAttribute('email'))
- ->trigger();
+ $publisherForMails->enqueue(new MailMessage(
+ project: $project,
+ recipient: $user->getAttribute('email'),
+ name: $user->getAttribute('name', ''),
+ subject: $subject,
+ body: $body->render(),
+ preview: $preview,
+ variables: ['user' => $user->getAttribute('name', '')],
+ ));
}
}
}
diff --git a/src/Appwrite/Promises/Swoole.php b/src/Appwrite/Promises/Swoole.php
index 9c06fbda2f..03c901ead6 100644
--- a/src/Appwrite/Promises/Swoole.php
+++ b/src/Appwrite/Promises/Swoole.php
@@ -8,7 +8,7 @@ use Utopia\DI\Container;
class Swoole extends Promise
{
- private const REQUEST_CONTAINER_CONTEXT_KEY = '__utopia_http_request_container';
+ private const REQUEST_CONTAINER_CONTEXT_KEY = '__utopia__';
public function __construct(?callable $executor = null)
{
diff --git a/src/Appwrite/SDK/Specification/Format.php b/src/Appwrite/SDK/Specification/Format.php
index 30df5acf52..fc67dedb13 100644
--- a/src/Appwrite/SDK/Specification/Format.php
+++ b/src/Appwrite/SDK/Specification/Format.php
@@ -68,6 +68,17 @@ abstract class Format
'mock-unverified'
],
],
+ [
+ 'namespace' => 'project',
+ 'methods' => [
+ 'getOAuth2Provider'
+ ],
+ 'parameter' => 'providerId',
+ 'excludeKeys' => [
+ 'mock',
+ 'mock-unverified'
+ ],
+ ],
];
/**
@@ -743,13 +754,31 @@ abstract class Format
break;
case 'project':
switch ($method) {
+ case 'updateAuthMethod':
+ switch ($param) {
+ case 'methodId':
+ return 'ProjectAuthMethodId';
+ }
+ break;
+ case 'getPolicy':
+ switch ($param) {
+ case 'policyId':
+ return 'ProjectPolicyId';
+ }
+ break;
+ case 'getOAuth2Provider':
+ switch ($param) {
+ case 'providerId':
+ return 'ProjectOAuthProviderId';
+ }
+ break;
case 'getEmailTemplate':
case 'updateEmailTemplate':
switch ($param) {
case 'templateId':
- return 'EmailTemplateType';
+ return 'ProjectEmailTemplateId';
case 'locale':
- return 'EmailTemplateLocale';
+ return 'ProjectEmailTemplateLocale';
}
break;
case 'getUsage':
@@ -758,6 +787,39 @@ abstract class Format
return 'ProjectUsageRange';
}
break;
+ case 'updateProtocol':
+ switch ($param) {
+ case 'protocolId':
+ return 'ProjectProtocolId';
+ }
+ break;
+ case 'updateService':
+ switch ($param) {
+ case 'serviceId':
+ return 'ProjectServiceId';
+ }
+ break;
+ case 'updateSMTP':
+ case 'createSMTPTest':
+ switch ($param) {
+ case 'secure':
+ return 'ProjectSMTPSecure';
+ }
+ break;
+ case 'updateOAuth2Google':
+ switch ($param) {
+ case 'prompt':
+ return 'ProjectOAuth2GooglePrompt';
+ }
+ break;
+ case 'createKey':
+ case 'createEphemeralKey':
+ case 'updateKey':
+ switch ($param) {
+ case 'scopes':
+ return 'ProjectKeyScopes';
+ }
+ break;
}
break;
case 'projects':
@@ -990,8 +1052,12 @@ abstract class Format
return self::REQUEST_PARAMETER_OVERRIDES;
}
- public function getResponseEnumName(string $model, string $param): ?string
+ public function getResponseEnumName(string $model, string $param, ?string $enumSDKName = null): ?string
{
+ if ($enumSDKName) {
+ return $enumSDKName;
+ }
+
if ($param === 'type' && \str_starts_with($model, 'platform') && $model !== 'platformList') {
return 'PlatformType';
}
diff --git a/src/Appwrite/SDK/Specification/Format/OpenAPI3.php b/src/Appwrite/SDK/Specification/Format/OpenAPI3.php
index 66c2cd7c1c..68ab7a0986 100644
--- a/src/Appwrite/SDK/Specification/Format/OpenAPI3.php
+++ b/src/Appwrite/SDK/Specification/Format/OpenAPI3.php
@@ -4,6 +4,7 @@ namespace Appwrite\SDK\Specification\Format;
use Appwrite\Platform\Tasks\Specs;
use Appwrite\SDK\AuthType;
+use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\MethodType;
use Appwrite\SDK\Response;
@@ -291,6 +292,21 @@ class OpenAPI3 extends Format
}
if (!(\is_array($model)) && $model->isNone()) {
+ if ($produces === ContentType::TEXT->value && !\in_array($response->getCode(), [204, 301, 302, 308], true)) {
+ $temp['responses'][(string)$response->getCode()] = [
+ 'description' => 'Text',
+ 'content' => [
+ $produces => [
+ 'schema' => [
+ 'type' => 'string',
+ ],
+ ],
+ ],
+ ];
+
+ continue;
+ }
+
$temp['responses'][(string)$response->getCode()] = [
'description' => in_array($produces, [
'image/*',
@@ -437,6 +453,15 @@ class OpenAPI3 extends Format
$node['schema']['type'] = $validator->getType();
$node['schema']['x-example'] = ($param['example'] ?? '') ?: '<' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . '>';
break;
+ case \Utopia\Database\Validator\BigInt::class:
+ // BigInt validator reports Database::VAR_BIGINT, but OpenAPI expects scalar types.
+ // We expose it as int64 to keep schema consistent with Column/Attribute models.
+ $node['schema']['type'] = 'integer';
+ $node['schema']['format'] = 'int64';
+ if (!empty($param['example'])) {
+ $node['schema']['x-example'] = $param['example'];
+ }
+ break;
case \Utopia\Validator\Boolean::class:
$node['schema']['type'] = $validator->getType();
$node['schema']['x-example'] = ($param['example'] ?? '') ?: false;
@@ -515,6 +540,7 @@ class OpenAPI3 extends Format
case \Appwrite\Utopia\Database\Validator\Queries\Identities::class:
case \Appwrite\Utopia\Database\Validator\Queries\Indexes::class:
case \Appwrite\Utopia\Database\Validator\Queries\Installations::class:
+ case \Appwrite\Utopia\Database\Validator\Queries\Branches::class:
case \Appwrite\Utopia\Database\Validator\Queries\Memberships::class:
case \Appwrite\Utopia\Database\Validator\Queries\Messages::class:
case \Appwrite\Utopia\Database\Validator\Queries\Migrations::class:
@@ -746,7 +772,18 @@ class OpenAPI3 extends Format
$node['schema']['default'] = $param['default'];
}
- if (false !== \strpos($url, ':' . $name)) { // Param is in URL path
+ $pathAliases = [$name, ...($param['aliases'] ?? [])];
+ $pathAliasMap = \array_flip($pathAliases);
+ $isPathParam = false;
+
+ foreach (\explode('/', $url) as $segment) {
+ if ($segment !== '' && $segment[0] === ':' && isset($pathAliasMap[\substr($segment, 1)])) {
+ $isPathParam = true;
+ break;
+ }
+ }
+
+ if ($isPathParam) { // Param is in URL path (directly or through alias)
$node['in'] = 'path';
$temp['parameters'][] = $node;
} elseif ($route->getMethod() == 'GET') { // Param is in query
@@ -787,7 +824,14 @@ class OpenAPI3 extends Format
}
}
- $url = \str_replace(':' . $name, '{' . $name . '}', $url);
+ $segments = \explode('/', $url);
+ foreach ($segments as &$segment) {
+ if ($segment !== '' && $segment[0] === ':' && isset($pathAliasMap[\substr($segment, 1)])) {
+ $segment = '{' . $name . '}';
+ }
+ }
+ unset($segment);
+ $url = \implode('/', $segments);
}
if (!empty($bodyRequired)) {
@@ -960,13 +1004,13 @@ class OpenAPI3 extends Format
if ($rule['type'] === 'enum' && !empty($rule['enum'])) {
if ($rule['array']) {
$output['components']['schemas'][$model->getType()]['properties'][$name]['items']['enum'] = \array_values($rule['enum']);
- $enumName = $this->getResponseEnumName($model->getType(), $name);
+ $enumName = $this->getResponseEnumName($model->getType(), $name, $rule['enumSDKName'] ?? null);
if ($enumName) {
$output['components']['schemas'][$model->getType()]['properties'][$name]['items']['x-enum-name'] = $enumName;
}
} else {
$output['components']['schemas'][$model->getType()]['properties'][$name]['enum'] = \array_values($rule['enum']);
- $enumName = $this->getResponseEnumName($model->getType(), $name);
+ $enumName = $this->getResponseEnumName($model->getType(), $name, $rule['enumSDKName'] ?? null);
if ($enumName) {
$output['components']['schemas'][$model->getType()]['properties'][$name]['x-enum-name'] = $enumName;
}
diff --git a/src/Appwrite/SDK/Specification/Format/Swagger2.php b/src/Appwrite/SDK/Specification/Format/Swagger2.php
index d07d957577..fb1ef66eca 100644
--- a/src/Appwrite/SDK/Specification/Format/Swagger2.php
+++ b/src/Appwrite/SDK/Specification/Format/Swagger2.php
@@ -4,6 +4,7 @@ namespace Appwrite\SDK\Specification\Format;
use Appwrite\Platform\Tasks\Specs;
use Appwrite\SDK\AuthType;
+use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\MethodType;
use Appwrite\SDK\Response;
@@ -298,6 +299,17 @@ class Swagger2 extends Format
}
if (!(\is_array($model)) && $model->isNone()) {
+ if ($produces === ContentType::TEXT->value && !\in_array($response->getCode(), [204, 301, 302, 308], true)) {
+ $temp['responses'][(string)$response->getCode()] = [
+ 'description' => 'Text',
+ 'schema' => [
+ 'type' => 'string',
+ ],
+ ];
+
+ continue;
+ }
+
$temp['responses'][(string)$response->getCode()] = [
'description' => in_array($produces, [
'image/*',
@@ -511,6 +523,7 @@ class Swagger2 extends Format
case \Utopia\Database\Validator\Queries::class:
case \Utopia\Database\Validator\Queries\Document::class:
case \Utopia\Database\Validator\Queries\Documents::class:
+ case \Appwrite\Utopia\Database\Validator\Queries\Branches::class:
case \Appwrite\Utopia\Database\Validator\Queries\Columns::class:
case \Appwrite\Utopia\Database\Validator\Queries\Tables::class:
$node['type'] = 'array';
@@ -722,7 +735,18 @@ class Swagger2 extends Format
$node['default'] = $param['default'];
}
- if (\str_contains($url, ':' . $name)) { // Param is in URL path
+ $pathAliases = [$name, ...($param['aliases'] ?? [])];
+ $pathAliasMap = \array_flip($pathAliases);
+ $isPathParam = false;
+
+ foreach (\explode('/', $url) as $segment) {
+ if ($segment !== '' && $segment[0] === ':' && isset($pathAliasMap[\substr($segment, 1)])) {
+ $isPathParam = true;
+ break;
+ }
+ }
+
+ if ($isPathParam) { // Param is in URL path (directly or through alias)
$node['in'] = 'path';
$temp['parameters'][] = $node;
} elseif ($route->getMethod() == 'GET') { // Param is in query
@@ -767,7 +791,14 @@ class Swagger2 extends Format
}
}
- $url = \str_replace(':' . $name, '{' . $name . '}', $url);
+ $segments = \explode('/', $url);
+ foreach ($segments as &$segment) {
+ if ($segment !== '' && $segment[0] === ':' && isset($pathAliasMap[\substr($segment, 1)])) {
+ $segment = '{' . $name . '}';
+ }
+ }
+ unset($segment);
+ $url = \implode('/', $segments);
}
if (!empty($bodyRequired)) {
@@ -956,13 +987,13 @@ class Swagger2 extends Format
if ($rule['type'] === 'enum' && !empty($rule['enum'])) {
if ($rule['array']) {
$output['definitions'][$model->getType()]['properties'][$name]['items']['enum'] = \array_values($rule['enum']);
- $enumName = $this->getResponseEnumName($model->getType(), $name);
+ $enumName = $this->getResponseEnumName($model->getType(), $name, $rule['enumSDKName'] ?? null);
if ($enumName) {
$output['definitions'][$model->getType()]['properties'][$name]['items']['x-enum-name'] = $enumName;
}
} else {
$output['definitions'][$model->getType()]['properties'][$name]['enum'] = \array_values($rule['enum']);
- $enumName = $this->getResponseEnumName($model->getType(), $name);
+ $enumName = $this->getResponseEnumName($model->getType(), $name, $rule['enumSDKName'] ?? null);
if ($enumName) {
$output['definitions'][$model->getType()]['properties'][$name]['x-enum-name'] = $enumName;
}
diff --git a/src/Appwrite/Utopia/Database/Validator/Attributes.php b/src/Appwrite/Utopia/Database/Validator/Attributes.php
index 16bf0909d2..54aaf135f9 100644
--- a/src/Appwrite/Utopia/Database/Validator/Attributes.php
+++ b/src/Appwrite/Utopia/Database/Validator/Attributes.php
@@ -23,6 +23,7 @@ class Attributes extends Validator
protected array $supportedTypes = [
Database::VAR_STRING,
Database::VAR_INTEGER,
+ Database::VAR_BIGINT,
Database::VAR_FLOAT,
Database::VAR_BOOLEAN,
Database::VAR_DATETIME,
@@ -181,9 +182,9 @@ class Attributes extends Validator
return false;
}
- // Validate signed only for integer/float types
- if (isset($attribute['signed']) && !in_array($attribute['type'], [Database::VAR_INTEGER, Database::VAR_FLOAT])) {
- $this->message = "Attribute '" . $attribute['key'] . "': 'signed' can only be used with integer or float types";
+ // Validate signed only for integer/bigint/float types
+ if (isset($attribute['signed']) && !in_array($attribute['type'], [Database::VAR_INTEGER, Database::VAR_BIGINT, Database::VAR_FLOAT])) {
+ $this->message = "Attribute '" . $attribute['key'] . "': 'signed' can only be used with integer, bigint or float types";
return false;
}
@@ -199,10 +200,10 @@ class Attributes extends Validator
return false;
}
- // Validate min/max range for integer/float
+ // Validate min/max range for integer/bigint/float
if (isset($attribute['min']) || isset($attribute['max'])) {
- if (!in_array($attribute['type'], [Database::VAR_INTEGER, Database::VAR_FLOAT])) {
- $this->message = "Attribute '" . $attribute['key'] . "': min/max can only be used with integer or float types";
+ if (!in_array($attribute['type'], [Database::VAR_INTEGER, Database::VAR_BIGINT, Database::VAR_FLOAT])) {
+ $this->message = "Attribute '" . $attribute['key'] . "': min/max can only be used with integer, bigint or float types";
return false;
}
@@ -264,7 +265,7 @@ class Attributes extends Validator
if (isset($attribute['min']) || isset($attribute['max'])) {
$min = $attribute['min'] ?? \PHP_INT_MIN;
$max = $attribute['max'] ?? \PHP_INT_MAX;
- $rangeValidator = new Range($min, $max, Database::VAR_INTEGER);
+ $rangeValidator = new Range($min, $max, Range::TYPE_INTEGER);
if (!$rangeValidator->isValid($attribute['default'])) {
$this->message = "Default value for integer attribute '" . $attribute['key'] . "' must be between $min and $max";
return false;
@@ -272,6 +273,23 @@ class Attributes extends Validator
}
break;
+ case Database::VAR_BIGINT:
+ if (!is_int($attribute['default'])) {
+ $this->message = "Default value for bigint attribute '" . $attribute['key'] . "' must be an integer";
+ return false;
+ }
+ // Validate within range if min/max specified
+ if (isset($attribute['min']) || isset($attribute['max'])) {
+ $min = $attribute['min'] ?? \PHP_INT_MIN;
+ $max = $attribute['max'] ?? \PHP_INT_MAX;
+ $rangeValidator = new Range($min, $max, Range::TYPE_INTEGER);
+ if (!$rangeValidator->isValid($attribute['default'])) {
+ $this->message = "Default value for bigint attribute '" . $attribute['key'] . "' must be between $min and $max";
+ return false;
+ }
+ }
+ break;
+
case Database::VAR_FLOAT:
if (!is_float($attribute['default']) && !is_int($attribute['default'])) {
$this->message = "Default value for float attribute '" . $attribute['key'] . "' must be a number";
@@ -281,7 +299,7 @@ class Attributes extends Validator
if (isset($attribute['min']) || isset($attribute['max'])) {
$min = $attribute['min'] ?? -\PHP_FLOAT_MAX;
$max = $attribute['max'] ?? \PHP_FLOAT_MAX;
- $rangeValidator = new Range($min, $max, Database::VAR_FLOAT);
+ $rangeValidator = new Range($min, $max, Range::TYPE_FLOAT);
if (!$rangeValidator->isValid((float)$attribute['default'])) {
$this->message = "Default value for float attribute '" . $attribute['key'] . "' must be between $min and $max";
return false;
diff --git a/src/Appwrite/Utopia/Database/Validator/Queries/Branches.php b/src/Appwrite/Utopia/Database/Validator/Queries/Branches.php
new file mode 100644
index 0000000000..82ca911747
--- /dev/null
+++ b/src/Appwrite/Utopia/Database/Validator/Queries/Branches.php
@@ -0,0 +1,20 @@
+getMethod();
+
+ if (!\in_array($method, [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE], true)) {
+ $this->message = 'Invalid query method: ' . $method;
+ return false;
+ }
+
+ $cursor = $value->getValue();
+
+ $validator = new Text(256);
+ if (!$validator->isValid($cursor)) {
+ $this->message = 'Invalid cursor: ' . $validator->getDescription();
+ return false;
+ }
+
+ return true;
+ }
+
+ public function getMethodType(): string
+ {
+ return self::METHOD_TYPE_CURSOR;
+ }
+}
diff --git a/src/Appwrite/Utopia/Request/Filters/V25.php b/src/Appwrite/Utopia/Request/Filters/V25.php
new file mode 100644
index 0000000000..cba70a5f7b
--- /dev/null
+++ b/src/Appwrite/Utopia/Request/Filters/V25.php
@@ -0,0 +1,27 @@
+fillVariableId($content);
+ break;
+ }
+
+ return $content;
+ }
+
+ protected function fillVariableId(array $content): array
+ {
+ $content['variableId'] = $content['variableId'] ?? 'unique()';
+ return $content;
+ }
+}
diff --git a/src/Appwrite/Utopia/Request/Filters/V26.php b/src/Appwrite/Utopia/Request/Filters/V26.php
new file mode 100644
index 0000000000..00310ddff9
--- /dev/null
+++ b/src/Appwrite/Utopia/Request/Filters/V26.php
@@ -0,0 +1,37 @@
+stripProjectMetadata($content);
+ break;
+ }
+
+ return $content;
+ }
+
+ protected function stripProjectMetadata(array $content): array
+ {
+ unset(
+ $content['description'],
+ $content['logo'],
+ $content['url'],
+ $content['legalName'],
+ $content['legalCountry'],
+ $content['legalState'],
+ $content['legalCity'],
+ $content['legalAddress'],
+ $content['legalTaxId'],
+ );
+
+ return $content;
+ }
+}
diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php
index 899cdc086a..c8dcb5f46a 100644
--- a/src/Appwrite/Utopia/Response.php
+++ b/src/Appwrite/Utopia/Response.php
@@ -72,6 +72,7 @@ class Response extends SwooleResponse
public const MODEL_ATTRIBUTE_LIST = 'attributeList';
public const MODEL_ATTRIBUTE_STRING = 'attributeString';
public const MODEL_ATTRIBUTE_INTEGER = 'attributeInteger';
+ public const MODEL_ATTRIBUTE_BIGINT = 'attributeBigint';
public const MODEL_ATTRIBUTE_FLOAT = 'attributeFloat';
public const MODEL_ATTRIBUTE_BOOLEAN = 'attributeBoolean';
public const MODEL_ATTRIBUTE_EMAIL = 'attributeEmail';
@@ -95,6 +96,7 @@ class Response extends SwooleResponse
public const MODEL_COLUMN_LIST = 'columnList';
public const MODEL_COLUMN_STRING = 'columnString';
public const MODEL_COLUMN_INTEGER = 'columnInteger';
+ public const MODEL_COLUMN_BIGINT = 'columnBigint';
public const MODEL_COLUMN_FLOAT = 'columnFloat';
public const MODEL_COLUMN_BOOLEAN = 'columnBoolean';
public const MODEL_COLUMN_EMAIL = 'columnEmail';
@@ -247,6 +249,9 @@ class Response extends SwooleResponse
// Project
public const MODEL_PROJECT = 'project';
public const MODEL_PROJECT_LIST = 'projectList';
+ public const MODEL_PROJECT_AUTH_METHOD = 'projectAuthMethod';
+ public const MODEL_PROJECT_SERVICE = 'projectService';
+ public const MODEL_PROJECT_PROTOCOL = 'projectProtocol';
public const MODEL_WEBHOOK = 'webhook';
public const MODEL_WEBHOOK_LIST = 'webhookList';
public const MODEL_KEY = 'key';
@@ -330,6 +335,13 @@ class Response extends SwooleResponse
public const MODEL_HEALTH_CERTIFICATE = 'healthCertificate';
public const MODEL_HEALTH_STATUS_LIST = 'healthStatusList';
+ // Advisor
+ public const MODEL_INSIGHT = 'insight';
+ public const MODEL_INSIGHT_LIST = 'insightList';
+ public const MODEL_INSIGHT_CTA = 'insightCTA';
+ public const MODEL_REPORT = 'report';
+ public const MODEL_REPORT_LIST = 'reportList';
+
// Console
public const MODEL_CONSOLE_VARIABLES = 'consoleVariables';
public const MODEL_CONSOLE_OAUTH2_PROVIDER_PARAMETER = 'consoleOAuth2ProviderParameter';
@@ -433,9 +445,10 @@ class Response extends SwooleResponse
return isset(self::$models[$key]);
}
- public function applyFilters(array $data, string $model): array
+ public function applyFilters(array $data, string $model, Document $raw): array
{
foreach ($this->filters as $filter) {
+ $filter->setRawContent($raw);
$data = $filter->parse($data, $model);
}
@@ -455,7 +468,7 @@ class Response extends SwooleResponse
public function dynamic(Document $document, string $model): void
{
$output = $this->output(clone $document, $model);
- $output = $this->applyFilters($output, $model);
+ $output = $this->applyFilters($output, $model, raw: clone $document);
switch ($this->getContentType()) {
case self::CONTENT_TYPE_JSON:
diff --git a/src/Appwrite/Utopia/Response/Filter.php b/src/Appwrite/Utopia/Response/Filter.php
index bd82467f81..13833be328 100644
--- a/src/Appwrite/Utopia/Response/Filter.php
+++ b/src/Appwrite/Utopia/Response/Filter.php
@@ -2,8 +2,15 @@
namespace Appwrite\Utopia\Response;
+use Utopia\Database\Document;
+
abstract class Filter
{
+ /**
+ * @var ?Document $rawContent
+ */
+ protected ?Document $rawContent = null;
+
/**
* Parse the content to another format.
*
@@ -14,6 +21,10 @@ abstract class Filter
*/
abstract public function parse(array $content, string $model): array;
+ public function setRawContent(Document $rawContent): void
+ {
+ $this->rawContent = $rawContent;
+ }
/**
* Handle list
diff --git a/src/Appwrite/Utopia/Response/Filters/V25.php b/src/Appwrite/Utopia/Response/Filters/V25.php
new file mode 100644
index 0000000000..bda98ed0d8
--- /dev/null
+++ b/src/Appwrite/Utopia/Response/Filters/V25.php
@@ -0,0 +1,34 @@
+ $this->parseOAuth2Oidc($content),
+ Response::MODEL_OAUTH2_PROVIDER_LIST => $this->handleList($content, 'providers', fn ($item) => ($item['$id'] ?? null) === 'oidc' ? $this->parseOAuth2Oidc($item) : $item),
+ default => $content,
+ };
+ }
+
+ private function parseOAuth2Oidc(array $content): array
+ {
+ if (isset($content['tokenURL'])) {
+ $content['tokenUrl'] = $content['tokenURL'];
+ unset($content['tokenURL']);
+ }
+
+ if (isset($content['userInfoURL'])) {
+ $content['userInfoUrl'] = $content['userInfoURL'];
+ unset($content['userInfoURL']);
+ }
+
+ return $content;
+ }
+}
diff --git a/src/Appwrite/Utopia/Response/Filters/V26.php b/src/Appwrite/Utopia/Response/Filters/V26.php
new file mode 100644
index 0000000000..3867ba907f
--- /dev/null
+++ b/src/Appwrite/Utopia/Response/Filters/V26.php
@@ -0,0 +1,227 @@
+ $this->parseProject($content, $this->rawContent),
+ Response::MODEL_PROJECT_LIST => $this->handleList($content, 'projects', function ($item) {
+ $projectId = $item['$id'] ?? '';
+
+ $rawProjects = $this->rawContent->getAttribute('projects', []);
+ $rawProject = new Document();
+ foreach ($rawProjects as $rawItem) {
+ if ($rawItem->getId() === $projectId) {
+ $rawProject = $rawItem;
+ break;
+ }
+ }
+
+ return $this->parseProject($item, $rawProject);
+ }),
+ default => $content,
+ };
+ }
+
+ private function parseProject(array $content, Document $raw): array
+ {
+ $this->expandAuthMethods($content);
+ $this->expandServices($content);
+ $this->expandProtocols($content);
+
+ unset($content['authMethods'], $content['services'], $content['protocols']);
+
+ $auths = new Document($raw->getAttribute('auths', []));
+ $content['authLimit'] = $auths->getAttribute('limit', 0);
+ $content['authDuration'] = $auths->getAttribute('duration', TOKEN_EXPIRATION_LOGIN_LONG);
+ $content['authSessionsLimit'] = $auths->getAttribute('maxSessions', 0);
+ $content['authPasswordHistory'] = $auths->getAttribute('passwordHistory', 0);
+ $content['authPasswordDictionary'] = $auths->getAttribute('passwordDictionary', false);
+ $content['authPersonalDataCheck'] = $auths->getAttribute('personalDataCheck', false);
+ $content['authDisposableEmails'] = $auths->getAttribute('disposableEmails', false);
+ $content['authCanonicalEmails'] = $auths->getAttribute('canonicalEmails', false);
+ $content['authFreeEmails'] = $auths->getAttribute('freeEmails', false);
+ $content['authMockNumbers'] = $auths->getAttribute('mockNumbers', []);
+ $content['authSessionAlerts'] = $auths->getAttribute('sessionAlerts', false);
+ $content['authMembershipsUserName'] = $auths->getAttribute('membershipsUserName', false);
+ $content['authMembershipsUserEmail'] = $auths->getAttribute('membershipsUserEmail', false);
+ $content['authMembershipsMfa'] = $auths->getAttribute('membershipsMfa', false);
+ $content['authMembershipsUserId'] = $auths->getAttribute('membershipsUserId', false);
+ $content['authMembershipsUserPhone'] = $auths->getAttribute('membershipsUserPhone', false);
+ $content['authInvalidateSessions'] = $auths->getAttribute('invalidateSessions', false);
+
+ $content['description'] = $raw->getAttribute('description', '');
+ $content['logo'] = $raw->getAttribute('logo', '');
+ $content['url'] = $raw->getAttribute('url', '');
+ $content['legalName'] = $raw->getAttribute('legalName', '');
+ $content['legalCountry'] = $raw->getAttribute('legalCountry', '');
+ $content['legalState'] = $raw->getAttribute('legalState', '');
+ $content['legalCity'] = $raw->getAttribute('legalCity', '');
+ $content['legalAddress'] = $raw->getAttribute('legalAddress', '');
+ $content['legalTaxId'] = $raw->getAttribute('legalTaxId', '');
+
+ $content['oAuthProviders'] = $this->expandOAuthProviders($raw);
+
+ $content['platforms'] = [];
+ foreach ($raw->getAttribute('platforms', []) as $platform) {
+ $content['platforms'][] = $this->parsePlatform($platform);
+ }
+
+ $content['webhooks'] = [];
+ foreach ($raw->getAttribute('webhooks', []) as $webhook) {
+ $content['webhooks'][] = $this->parseWebhook($webhook);
+ }
+
+ $content['keys'] = [];
+ foreach ($raw->getAttribute('keys', []) as $key) {
+ $content['keys'][] = $this->parseKey($key);
+ }
+
+ return $content;
+ }
+
+ private function parsePlatform(Document $platform): array
+ {
+ $type = $platform->getAttribute('type', '');
+ $key = $platform->getAttribute('key', '');
+
+ $result = [
+ '$id' => $platform->getAttribute('$id', ''),
+ '$createdAt' => $platform->getAttribute('$createdAt', ''),
+ '$updatedAt' => $platform->getAttribute('$updatedAt', ''),
+ 'name' => $platform->getAttribute('name', ''),
+ 'type' => $type,
+ ];
+
+ switch ($type) {
+ case Platform::TYPE_ANDROID:
+ $result['applicationId'] = $key;
+ break;
+ case Platform::TYPE_APPLE:
+ $result['bundleIdentifier'] = $key;
+ break;
+ case Platform::TYPE_LINUX:
+ $result['packageName'] = $key;
+ break;
+ case Platform::TYPE_WINDOWS:
+ $result['packageIdentifierName'] = $key;
+ break;
+ default:
+ // Web and backwards-compatibility types are mapped to web
+ $result['hostname'] = $platform->getAttribute('hostname', '');
+ $result['key'] = $key;
+ break;
+ }
+
+ return $result;
+ }
+
+ private function parseWebhook(Document $webhook): array
+ {
+ return [
+ '$id' => $webhook->getAttribute('$id', ''),
+ '$createdAt' => $webhook->getAttribute('$createdAt', ''),
+ '$updatedAt' => $webhook->getAttribute('$updatedAt', ''),
+ 'name' => $webhook->getAttribute('name', ''),
+ 'url' => $webhook->getAttribute('url', ''),
+ 'events' => $webhook->getAttribute('events', []),
+ 'tls' => $webhook->getAttribute('security', true),
+ 'authUsername' => $webhook->getAttribute('httpUser', ''),
+ 'authPassword' => $webhook->getAttribute('httpPass', ''),
+ 'secret' => $webhook->getAttribute('signatureKey', ''),
+ 'enabled' => $webhook->getAttribute('enabled', true),
+ 'logs' => $webhook->getAttribute('logs', ''),
+ 'attempts' => $webhook->getAttribute('attempts', 0),
+ ];
+ }
+
+ private function parseKey(Document $key): array
+ {
+ return [
+ '$id' => $key->getAttribute('$id', ''),
+ '$createdAt' => $key->getAttribute('$createdAt', ''),
+ '$updatedAt' => $key->getAttribute('$updatedAt', ''),
+ 'name' => $key->getAttribute('name', ''),
+ 'expire' => $key->getAttribute('expire', ''),
+ 'scopes' => $key->getAttribute('scopes', []),
+ 'secret' => $key->getAttribute('secret', ''),
+ 'accessedAt' => $key->getAttribute('accessedAt', ''),
+ 'sdks' => $key->getAttribute('sdks', []),
+ ];
+ }
+
+ private function expandAuthMethods(array &$content): void
+ {
+ $authMethods = [];
+ foreach ($content['authMethods'] ?? [] as $method) {
+ $authMethods[$method['$id'] ?? ''] = $method['enabled'] ?? true;
+ }
+
+ foreach (Config::getParam('auth', []) as $id => $method) {
+ $key = $method['key'] ?? '';
+ $content['auth' . ucfirst($key)] = $authMethods[$id] ?? true;
+ }
+ }
+
+ private function expandServices(array &$content): void
+ {
+ $services = [];
+ foreach ($content['services'] ?? [] as $service) {
+ $services[$service['$id'] ?? ''] = $service['enabled'] ?? true;
+ }
+
+ foreach (Config::getParam('services', []) as $id => $service) {
+ if (!($service['optional'] ?? false)) {
+ continue;
+ }
+ $key = $service['key'] ?? '';
+ $content['serviceStatusFor' . ucfirst($key)] = $services[$id] ?? true;
+ }
+ }
+
+ private function expandProtocols(array &$content): void
+ {
+ $protocols = [];
+ foreach ($content['protocols'] ?? [] as $protocol) {
+ $protocols[$protocol['$id'] ?? ''] = $protocol['enabled'] ?? true;
+ }
+
+ foreach (Config::getParam('protocols', []) as $id => $api) {
+ $key = $api['key'] ?? '';
+ $content['protocolStatusFor' . ucfirst($key)] = $protocols[$id] ?? true;
+ }
+ }
+
+ private function expandOAuthProviders(Document $raw): array
+ {
+ $providers = Config::getParam('oAuthProviders', []);
+ $providerValues = $raw->getAttribute('oAuthProviders', []);
+ $projectProviders = [];
+
+ foreach ($providers as $key => $provider) {
+ if (!($provider['enabled'] ?? false)) {
+ continue;
+ }
+
+ $projectProviders[] = [
+ 'key' => $key,
+ 'name' => $provider['name'] ?? '',
+ 'appId' => $providerValues[$key . 'Appid'] ?? '',
+ 'secret' => '',
+ 'enabled' => $providerValues[$key . 'Enabled'] ?? false,
+ ];
+ }
+
+ return $projectProviders;
+ }
+}
diff --git a/src/Appwrite/Utopia/Response/Model/AttributeBigInt.php b/src/Appwrite/Utopia/Response/Model/AttributeBigInt.php
new file mode 100644
index 0000000000..baa93ff103
--- /dev/null
+++ b/src/Appwrite/Utopia/Response/Model/AttributeBigInt.php
@@ -0,0 +1,66 @@
+addRule('key', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Attribute Key.',
+ 'default' => '',
+ 'example' => 'count',
+ ])
+ ->addRule('type', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Attribute type.',
+ 'default' => '',
+ 'example' => 'bigint',
+ ])
+ ->addRule('min', [
+ 'type' => self::TYPE_INTEGER,
+ 'format' => 'int64',
+ 'description' => 'Minimum value to enforce for new documents.',
+ 'default' => null,
+ 'required' => false,
+ 'example' => 1,
+ ])
+ ->addRule('max', [
+ 'type' => self::TYPE_INTEGER,
+ 'format' => 'int64',
+ 'description' => 'Maximum value to enforce for new documents.',
+ 'default' => null,
+ 'required' => false,
+ 'example' => 10,
+ ])
+ ->addRule('default', [
+ 'type' => self::TYPE_INTEGER,
+ 'format' => 'int64',
+ 'description' => 'Default value for attribute when not provided. Cannot be set when attribute is required.',
+ 'default' => null,
+ 'required' => false,
+ 'example' => 10,
+ ])
+ ;
+ }
+
+ public array $conditions = [
+ 'type' => 'bigint'
+ ];
+
+ public function getName(): string
+ {
+ return 'AttributeBigInt';
+ }
+
+ public function getType(): string
+ {
+ return Response::MODEL_ATTRIBUTE_BIGINT;
+ }
+}
diff --git a/src/Appwrite/Utopia/Response/Model/AttributeList.php b/src/Appwrite/Utopia/Response/Model/AttributeList.php
index 50189a80c3..87d1dc8b9f 100644
--- a/src/Appwrite/Utopia/Response/Model/AttributeList.php
+++ b/src/Appwrite/Utopia/Response/Model/AttributeList.php
@@ -19,6 +19,9 @@ class AttributeList extends Model
->addRule('attributes', [
'type' => [
Response::MODEL_ATTRIBUTE_BOOLEAN,
+ // BigInt must come before Integer: response model dispatch is "first match wins",
+ // and Integer matches all int types (including bigint), while BigInt is more specific (size=8).
+ Response::MODEL_ATTRIBUTE_BIGINT,
Response::MODEL_ATTRIBUTE_INTEGER,
Response::MODEL_ATTRIBUTE_FLOAT,
Response::MODEL_ATTRIBUTE_EMAIL,
diff --git a/src/Appwrite/Utopia/Response/Model/Collection.php b/src/Appwrite/Utopia/Response/Model/Collection.php
index 4ab7de8e4d..bc4de22858 100644
--- a/src/Appwrite/Utopia/Response/Model/Collection.php
+++ b/src/Appwrite/Utopia/Response/Model/Collection.php
@@ -62,6 +62,9 @@ class Collection extends Model
->addRule('attributes', [
'type' => [
Response::MODEL_ATTRIBUTE_BOOLEAN,
+ // BigInt must come before Integer: response model dispatch is "first match wins",
+ // and Integer matches all int types (including bigint), while BigInt is more specific (size=8).
+ Response::MODEL_ATTRIBUTE_BIGINT,
Response::MODEL_ATTRIBUTE_INTEGER,
Response::MODEL_ATTRIBUTE_FLOAT,
Response::MODEL_ATTRIBUTE_EMAIL,
diff --git a/src/Appwrite/Utopia/Response/Model/ColumnBigInt.php b/src/Appwrite/Utopia/Response/Model/ColumnBigInt.php
new file mode 100644
index 0000000000..895356dbf2
--- /dev/null
+++ b/src/Appwrite/Utopia/Response/Model/ColumnBigInt.php
@@ -0,0 +1,66 @@
+addRule('key', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Column Key.',
+ 'default' => '',
+ 'example' => 'count',
+ ])
+ ->addRule('type', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Column type.',
+ 'default' => '',
+ 'example' => 'bigint',
+ ])
+ ->addRule('min', [
+ 'type' => self::TYPE_INTEGER,
+ 'format' => 'int64',
+ 'description' => 'Minimum value to enforce for new documents.',
+ 'default' => null,
+ 'required' => false,
+ 'example' => 1,
+ ])
+ ->addRule('max', [
+ 'type' => self::TYPE_INTEGER,
+ 'format' => 'int64',
+ 'description' => 'Maximum value to enforce for new documents.',
+ 'default' => null,
+ 'required' => false,
+ 'example' => 10,
+ ])
+ ->addRule('default', [
+ 'type' => self::TYPE_INTEGER,
+ 'format' => 'int64',
+ 'description' => 'Default value for column when not provided. Cannot be set when column is required.',
+ 'default' => null,
+ 'required' => false,
+ 'example' => 10,
+ ])
+ ;
+ }
+
+ public array $conditions = [
+ 'type' => 'bigint'
+ ];
+
+ public function getName(): string
+ {
+ return 'ColumnBigInt';
+ }
+
+ public function getType(): string
+ {
+ return Response::MODEL_COLUMN_BIGINT;
+ }
+}
diff --git a/src/Appwrite/Utopia/Response/Model/ColumnList.php b/src/Appwrite/Utopia/Response/Model/ColumnList.php
index e99223cd17..0586015e4d 100644
--- a/src/Appwrite/Utopia/Response/Model/ColumnList.php
+++ b/src/Appwrite/Utopia/Response/Model/ColumnList.php
@@ -19,6 +19,9 @@ class ColumnList extends Model
->addRule('columns', [
'type' => [
Response::MODEL_COLUMN_BOOLEAN,
+ // BigInt must come before Integer: response model dispatch is "first match wins",
+ // and Integer matches all int types (including bigint), while BigInt is more specific (size=8).
+ Response::MODEL_COLUMN_BIGINT,
Response::MODEL_COLUMN_INTEGER,
Response::MODEL_COLUMN_FLOAT,
Response::MODEL_COLUMN_EMAIL,
diff --git a/src/Appwrite/Utopia/Response/Model/Insight.php b/src/Appwrite/Utopia/Response/Model/Insight.php
new file mode 100644
index 0000000000..39a12a9832
--- /dev/null
+++ b/src/Appwrite/Utopia/Response/Model/Insight.php
@@ -0,0 +1,130 @@
+addRule('$id', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Insight ID.',
+ 'default' => '',
+ 'example' => '5e5ea5c16897e',
+ ])
+ ->addRule('$createdAt', [
+ 'type' => self::TYPE_DATETIME,
+ 'description' => 'Insight creation date in ISO 8601 format.',
+ 'default' => '',
+ 'example' => self::TYPE_DATETIME_EXAMPLE,
+ ])
+ ->addRule('$updatedAt', [
+ 'type' => self::TYPE_DATETIME,
+ 'description' => 'Insight update date in ISO 8601 format.',
+ 'default' => '',
+ 'example' => self::TYPE_DATETIME_EXAMPLE,
+ ])
+ ->addRule('reportId', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Parent report ID. Insights always belong to a report.',
+ 'default' => '',
+ 'example' => '5e5ea5c16897e',
+ ])
+ ->addRule('type', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Insight type. One of databaseIndex (legacy), tablesDBIndex, documentsDBIndex, vectorsDBIndex, databasePerformance, sitePerformance, siteAccessibility, siteSeo, functionPerformance. The index types are engine-specific so each CTA can pair the right service+method (databases.createIndex, tablesDB.createIndex, documentsDB.createIndex, or vectorsDB.createIndex).',
+ 'default' => '',
+ 'example' => 'tablesDBIndex',
+ ])
+ ->addRule('severity', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Insight severity. One of info, warning, critical.',
+ 'default' => 'info',
+ 'example' => 'warning',
+ ])
+ ->addRule('status', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Insight status. One of active, dismissed.',
+ 'default' => 'active',
+ 'example' => 'active',
+ ])
+ ->addRule('resourceType', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Type of the resource the insight is about. Plural noun, e.g. databases, sites, functions.',
+ 'default' => '',
+ 'example' => 'databases',
+ ])
+ ->addRule('resourceId', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'ID of the resource the insight is about.',
+ 'default' => '',
+ 'example' => 'main',
+ ])
+ ->addRule('parentResourceType', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Plural noun for the parent resource that contains the insight\'s resource, e.g. an insight about a column index on a table â resourceType=indexes, parentResourceType=tables. Empty when the resource has no parent.',
+ 'default' => '',
+ 'example' => 'tables',
+ ])
+ ->addRule('parentResourceId', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'ID of the parent resource. Empty when the resource has no parent.',
+ 'default' => '',
+ 'example' => 'orders',
+ ])
+ ->addRule('title', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Insight title.',
+ 'default' => '',
+ 'example' => 'Missing index on collection orders',
+ ])
+ ->addRule('summary', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Short markdown summary describing the insight.',
+ 'default' => '',
+ 'example' => 'Queries against `orders.status` are scanning the full collection.',
+ ])
+ ->addRule('ctas', [
+ 'type' => Response::MODEL_INSIGHT_CTA,
+ 'description' => 'List of call-to-action buttons attached to this insight.',
+ 'default' => [],
+ 'example' => [],
+ 'array' => true,
+ ])
+ ->addRule('analyzedAt', [
+ 'type' => self::TYPE_DATETIME,
+ 'description' => 'Time the insight was analyzed in ISO 8601 format.',
+ 'default' => null,
+ 'example' => self::TYPE_DATETIME_EXAMPLE,
+ 'required' => false,
+ ])
+ ->addRule('dismissedAt', [
+ 'type' => self::TYPE_DATETIME,
+ 'description' => 'Time the insight was dismissed in ISO 8601 format. Empty when not dismissed.',
+ 'default' => null,
+ 'example' => self::TYPE_DATETIME_EXAMPLE,
+ 'required' => false,
+ ])
+ ->addRule('dismissedBy', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'User ID that dismissed the insight. Empty when not dismissed.',
+ 'default' => '',
+ 'example' => '5e5ea5c16897e',
+ 'required' => false,
+ ]);
+ }
+
+ public function getName(): string
+ {
+ return 'Insight';
+ }
+
+ public function getType(): string
+ {
+ return Response::MODEL_INSIGHT;
+ }
+}
diff --git a/src/Appwrite/Utopia/Response/Model/InsightCTA.php b/src/Appwrite/Utopia/Response/Model/InsightCTA.php
new file mode 100644
index 0000000000..3ebd8b5796
--- /dev/null
+++ b/src/Appwrite/Utopia/Response/Model/InsightCTA.php
@@ -0,0 +1,48 @@
+addRule('label', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Human-readable label for the CTA, used in UI.',
+ 'default' => '',
+ 'example' => 'Create missing index',
+ ])
+ ->addRule('service', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Public API service (SDK namespace) the client should invoke. Must match the engine that owns the resource â for index suggestions: databases (legacy), tablesDB, documentsDB, or vectorsDB.',
+ 'default' => '',
+ 'example' => 'tablesDB',
+ ])
+ ->addRule('method', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Public API method on the chosen service the client should invoke when this CTA is triggered.',
+ 'default' => '',
+ 'example' => 'createIndex',
+ ])
+ ->addRule('params', [
+ 'type' => self::TYPE_JSON,
+ 'description' => 'Parameter map the client should pass to the service method when this CTA is triggered. Keys match the target API\'s parameter names (e.g. databaseId/tableId/columns for tablesDB, databaseId/collectionId/attributes for the legacy Databases API).',
+ 'default' => new \stdClass(),
+ 'example' => ['databaseId' => 'main', 'tableId' => 'orders', 'key' => '_idx_status', 'type' => 'key', 'columns' => ['status']],
+ ]);
+ }
+
+ public function getName(): string
+ {
+ return 'InsightCTA';
+ }
+
+ public function getType(): string
+ {
+ return Response::MODEL_INSIGHT_CTA;
+ }
+}
diff --git a/src/Appwrite/Utopia/Response/Model/MigrationReport.php b/src/Appwrite/Utopia/Response/Model/MigrationReport.php
index 850e4b5ae9..388630af3f 100644
--- a/src/Appwrite/Utopia/Response/Model/MigrationReport.php
+++ b/src/Appwrite/Utopia/Response/Model/MigrationReport.php
@@ -53,6 +53,12 @@ class MigrationReport extends Model
'default' => 0,
'example' => 20,
])
+ ->addRule(Resource::TYPE_PLATFORM, [
+ 'type' => self::TYPE_INTEGER,
+ 'description' => 'Number of platforms to be migrated.',
+ 'default' => 0,
+ 'example' => 5,
+ ])
->addRule(Resource::TYPE_SITE, [
'type' => self::TYPE_INTEGER,
'description' => 'Number of sites to be migrated.',
diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Google.php b/src/Appwrite/Utopia/Response/Model/OAuth2Google.php
index 3dbc892631..ebef9aecf7 100644
--- a/src/Appwrite/Utopia/Response/Model/OAuth2Google.php
+++ b/src/Appwrite/Utopia/Response/Model/OAuth2Google.php
@@ -25,6 +25,20 @@ class OAuth2Google extends OAuth2Base
return 'GOCSPX-2k8gsR0000000000000000VNahJj';
}
+ public function __construct()
+ {
+ parent::__construct();
+
+ $this->addRule('prompt', [
+ 'type' => self::TYPE_ENUM,
+ 'description' => 'Google OAuth2 prompt values.',
+ 'default' => ['consent'],
+ 'example' => ['consent'],
+ 'array' => true,
+ 'enum' => ['none', 'consent', 'select_account'],
+ ]);
+ }
+
/**
* Get Name
*
diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Oidc.php b/src/Appwrite/Utopia/Response/Model/OAuth2Oidc.php
index e4f0919666..0b18539423 100644
--- a/src/Appwrite/Utopia/Response/Model/OAuth2Oidc.php
+++ b/src/Appwrite/Utopia/Response/Model/OAuth2Oidc.php
@@ -42,13 +42,13 @@ class OAuth2Oidc extends OAuth2Base
'default' => '',
'example' => 'https://myoauth.com/oauth2/authorize',
])
- ->addRule('tokenUrl', [
+ ->addRule('tokenURL', [
'type' => self::TYPE_STRING,
'description' => 'OpenID Connect token endpoint URL.',
'default' => '',
'example' => 'https://myoauth.com/oauth2/token',
])
- ->addRule('userInfoUrl', [
+ ->addRule('userInfoURL', [
'type' => self::TYPE_STRING,
'description' => 'OpenID Connect user info endpoint URL.',
'default' => '',
diff --git a/src/Appwrite/Utopia/Response/Model/Project.php b/src/Appwrite/Utopia/Response/Model/Project.php
index 36be3b751f..af2a21d551 100644
--- a/src/Appwrite/Utopia/Response/Model/Project.php
+++ b/src/Appwrite/Utopia/Response/Model/Project.php
@@ -12,6 +12,7 @@ class Project extends Model
public function __construct()
{
$this
+ // Basic project information
->addRule('$id', [
'type' => self::TYPE_STRING,
'description' => 'Project ID.',
@@ -36,210 +37,23 @@ class Project extends Model
'default' => '',
'example' => 'New Project',
])
- ->addRule('description', [
- 'type' => self::TYPE_STRING,
- 'description' => 'Project description.',
- 'default' => '',
- 'example' => 'This is a new project.',
- ])
->addRule('teamId', [
'type' => self::TYPE_STRING,
'description' => 'Project team ID.',
'default' => '',
'example' => '1592981250',
])
- ->addRule('logo', [
- 'type' => self::TYPE_STRING,
- 'description' => 'Project logo file ID.',
- 'default' => '',
- 'example' => '5f5c451b403cb',
- ])
- ->addRule('url', [
- 'type' => self::TYPE_STRING,
- 'description' => 'Project website URL.',
- 'default' => '',
- 'example' => '5f5c451b403cb',
- ])
- ->addRule('legalName', [
- 'type' => self::TYPE_STRING,
- 'description' => 'Company legal name.',
- 'default' => '',
- 'example' => 'Company LTD.',
- ])
- ->addRule('legalCountry', [
- 'type' => self::TYPE_STRING,
- 'description' => 'Country code in [ISO 3166-1](http://en.wikipedia.org/wiki/ISO_3166-1) two-character format.',
- 'default' => '',
- 'example' => 'US',
- ])
- ->addRule('legalState', [
- 'type' => self::TYPE_STRING,
- 'description' => 'State name.',
- 'default' => '',
- 'example' => 'New York',
- ])
- ->addRule('legalCity', [
- 'type' => self::TYPE_STRING,
- 'description' => 'City name.',
- 'default' => '',
- 'example' => 'New York City.',
- ])
- ->addRule('legalAddress', [
- 'type' => self::TYPE_STRING,
- 'description' => 'Company Address.',
- 'default' => '',
- 'example' => '620 Eighth Avenue, New York, NY 10018',
- ])
- ->addRule('legalTaxId', [
- 'type' => self::TYPE_STRING,
- 'description' => 'Company Tax ID.',
- 'default' => '',
- 'example' => '131102020',
- ])
- ->addRule('authDuration', [
- 'type' => self::TYPE_INTEGER,
- 'description' => 'Session duration in seconds.',
- 'default' => TOKEN_EXPIRATION_LOGIN_LONG,
- 'example' => 60,
- ])
- ->addRule('authLimit', [
- 'type' => self::TYPE_INTEGER,
- 'description' => 'Max users allowed. 0 is unlimited.',
- 'default' => 0,
- 'example' => 100,
- ])
- ->addRule('authSessionsLimit', [
- 'type' => self::TYPE_INTEGER,
- 'description' => 'Max sessions allowed per user. 100 maximum.',
- 'default' => 10,
- 'example' => 10,
- ])
- ->addRule('authPasswordHistory', [
- 'type' => self::TYPE_INTEGER,
- 'description' => 'Max allowed passwords in the history list per user. Max passwords limit allowed in history is 20. Use 0 for disabling password history.',
- 'default' => 0,
- 'example' => 5,
- ])
- ->addRule('authPasswordDictionary', [
- 'type' => self::TYPE_BOOLEAN,
- 'description' => 'Whether or not to check user\'s password against most commonly used passwords.',
- 'default' => false,
- 'example' => true,
- ])
- ->addRule('authPersonalDataCheck', [
- 'type' => self::TYPE_BOOLEAN,
- 'description' => 'Whether or not to check the user password for similarity with their personal data.',
- 'default' => false,
- 'example' => true,
- ])
- ->addRule('authDisposableEmails', [
- 'type' => self::TYPE_BOOLEAN,
- 'description' => 'Whether or not to disallow disposable email addresses during signup and email updates.',
- 'default' => false,
- 'example' => true,
- ])
- ->addRule('authCanonicalEmails', [
- 'type' => self::TYPE_BOOLEAN,
- 'description' => 'Whether or not to require canonical email addresses during signup and email updates.',
- 'default' => false,
- 'example' => true,
- ])
- ->addRule('authFreeEmails', [
- 'type' => self::TYPE_BOOLEAN,
- 'description' => 'Whether or not to disallow free email addresses during signup and email updates.',
- 'default' => false,
- 'example' => true,
- ])
- ->addRule('authMockNumbers', [
- 'type' => Response::MODEL_MOCK_NUMBER,
- 'description' => 'An array of mock numbers and their corresponding verification codes (OTPs).',
- 'default' => [],
- 'array' => true,
- 'example' => [new \stdClass()],
- ])
- ->addRule('authSessionAlerts', [
- 'type' => self::TYPE_BOOLEAN,
- 'description' => 'Whether or not to send session alert emails to users.',
- 'default' => false,
- 'example' => true,
- ])
- ->addRule('authMembershipsUserName', [
- 'type' => self::TYPE_BOOLEAN,
- 'description' => 'Whether or not to show user names in the teams membership response.',
- 'default' => false,
- 'example' => true,
- ])
- ->addRule('authMembershipsUserEmail', [
- 'type' => self::TYPE_BOOLEAN,
- 'description' => 'Whether or not to show user emails in the teams membership response.',
- 'default' => false,
- 'example' => true,
- ])
- ->addRule('authMembershipsMfa', [
- 'type' => self::TYPE_BOOLEAN,
- 'description' => 'Whether or not to show user MFA status in the teams membership response.',
- 'default' => false,
- 'example' => true,
- ])
- ->addRule('authMembershipsUserId', [
- 'type' => self::TYPE_BOOLEAN,
- 'description' => 'Whether or not to show user IDs in the teams membership response.',
- 'default' => false,
- 'example' => true,
- ])
- ->addRule('authMembershipsUserPhone', [
- 'type' => self::TYPE_BOOLEAN,
- 'description' => 'Whether or not to show user phone numbers in the teams membership response.',
- 'default' => false,
- 'example' => true,
- ])
- ->addRule('authInvalidateSessions', [
- 'type' => self::TYPE_BOOLEAN,
- 'description' => 'Whether or not all existing sessions should be invalidated on password change',
- 'default' => false,
- 'example' => true,
- ])
- ->addRule('oAuthProviders', [
- 'type' => Response::MODEL_AUTH_PROVIDER,
- 'description' => 'List of Auth Providers.',
- 'default' => [],
- 'example' => [new \stdClass()],
- 'array' => true,
- ])
- ->addRule('platforms', [
- 'type' => [
- Response::MODEL_PLATFORM_WEB,
- Response::MODEL_PLATFORM_APPLE,
- Response::MODEL_PLATFORM_ANDROID,
- Response::MODEL_PLATFORM_WINDOWS,
- Response::MODEL_PLATFORM_LINUX,
- ],
- 'description' => 'List of Platforms.',
- 'default' => [],
- 'example' => new \stdClass(),
- 'array' => true,
- ])
- ->addRule('webhooks', [
- 'type' => Response::MODEL_WEBHOOK,
- 'description' => 'List of Webhooks.',
- 'default' => [],
- 'example' => new \stdClass(),
- 'array' => true,
- ])
- ->addRule('keys', [
- 'type' => Response::MODEL_KEY,
- 'description' => 'List of API Keys.',
- 'default' => [],
- 'example' => new \stdClass(),
- 'array' => true,
- ])
+
+ // Resource: Dev Keys
->addRule('devKeys', [
'type' => Response::MODEL_DEV_KEY,
- 'description' => 'List of dev keys.',
+ 'description' => 'Deprecated since 1.9.5: List of dev keys.',
'default' => [],
'example' => new \stdClass(),
'array' => true,
])
+
+ // Resource: SMTP
->addRule('smtpEnabled', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Status for custom SMTP',
@@ -301,6 +115,8 @@ class Project extends Model
'default' => '',
'example' => 'tls',
])
+
+ // Resource: Ping
->addRule('pingCount', [
'type' => self::TYPE_INTEGER,
'description' => 'Number of times the ping was received for this project.',
@@ -313,6 +129,8 @@ class Project extends Model
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
+
+ // Resource: Labels
->addRule('labels', [
'type' => self::TYPE_STRING,
'description' => 'Labels for the project.',
@@ -320,64 +138,42 @@ class Project extends Model
'example' => ['vip'],
'array' => true,
])
+
+ // Resource: Billing
->addRule('status', [
'type' => self::TYPE_STRING,
'description' => 'Project status.',
'default' => 'active',
'example' => 'active',
])
+
+ // Resource: Auth methods
+ ->addRule('authMethods', [
+ 'type' => Response::MODEL_PROJECT_AUTH_METHOD,
+ 'description' => 'List of auth methods.',
+ 'default' => [],
+ 'example' => new \stdClass(),
+ 'array' => true,
+ ])
+
+ // Resource: Services
+ ->addRule('services', [
+ 'type' => Response::MODEL_PROJECT_SERVICE,
+ 'description' => 'List of services.',
+ 'default' => [],
+ 'example' => new \stdClass(),
+ 'array' => true,
+ ])
+
+ // Resource: Protocols
+ ->addRule('protocols', [
+ 'type' => Response::MODEL_PROJECT_PROTOCOL,
+ 'description' => 'List of protocols.',
+ 'default' => [],
+ 'example' => new \stdClass(),
+ 'array' => true,
+ ])
;
-
- $services = Config::getParam('services', []);
- $auth = Config::getParam('auth', []);
-
- foreach ($auth as $index => $method) {
- $name = $method['name'] ?? '';
- $key = $method['key'] ?? '';
-
- $this
- ->addRule('auth' . ucfirst($key), [
- 'type' => self::TYPE_BOOLEAN,
- 'description' => $name . ' auth method status',
- 'example' => true,
- 'default' => true,
- ])
- ;
- }
-
- foreach ($services as $service) {
- if (!$service['optional']) {
- continue;
- }
-
- $name = $service['name'] ?? '';
- $key = $service['key'] ?? '';
-
- $this
- ->addRule('serviceStatusFor' . ucfirst($key), [
- 'type' => self::TYPE_BOOLEAN,
- 'description' => $name . ' service status',
- 'example' => true,
- 'default' => true,
- ])
- ;
- }
-
- $apis = Config::getParam('protocols', []);
-
- foreach ($apis as $api) {
- $name = $api['name'] ?? '';
- $key = $api['key'] ?? '';
-
- $this
- ->addRule('protocolStatusFor' . ucfirst($key), [
- 'type' => self::TYPE_BOOLEAN,
- 'description' => $name . ' protocol status',
- 'example' => true,
- 'default' => true,
- ])
- ;
- }
}
/**
@@ -408,10 +204,9 @@ class Project extends Model
public function filter(Document $document): Document
{
$this->expandSmtpFields($document);
- $this->expandServiceFields($document);
- $this->expandApiFields($document);
- $this->expandAuthFields($document);
- $this->expandOAuthProviders($document);
+ $this->expandServices($document);
+ $this->expandProtocols($document);
+ $this->expandAuthMethods($document);
return $document;
}
@@ -422,8 +217,8 @@ class Project extends Model
return;
}
- // SMTP
$smtp = $document->getAttribute('smtp', []);
+
$document->setAttribute('smtpEnabled', $smtp['enabled'] ?? false);
$document->setAttribute('smtpSenderEmail', $smtp['senderEmail'] ?? '');
$document->setAttribute('smtpSenderName', $smtp['senderName'] ?? '');
@@ -436,100 +231,52 @@ class Project extends Model
$document->setAttribute('smtpSecure', $smtp['secure'] ?? '');
}
- private function expandServiceFields(Document $document): void
+ private function expandServices(Document $document): void
{
- if (!$document->isSet('services')) {
- return;
- }
-
$values = $document->getAttribute('services', []);
- $services = Config::getParam('services', []);
+ $services = [];
- foreach ($services as $service) {
+ foreach (Config::getParam('services', []) as $id => $service) {
if (!$service['optional']) {
continue;
}
- $key = $service['key'] ?? '';
- $value = $values[$key] ?? true;
- $document->setAttribute('serviceStatusFor' . ucfirst($key), $value);
- }
- }
- private function expandApiFields(Document $document): void
- {
- if (!$document->isSet('apis')) {
- return;
- }
-
- $values = $document->getAttribute('apis', []);
- $apis = Config::getParam('protocols', []);
-
- foreach ($apis as $api) {
- $key = $api['key'] ?? '';
- $value = $values[$key] ?? true;
- $document->setAttribute('protocolStatusFor' . ucfirst($key), $value);
- }
- }
-
- private function expandAuthFields(Document $document): void
- {
- if (!$document->isSet('auths')) {
- return;
- }
-
- $authValues = $document->getAttribute('auths', []);
- $auth = Config::getParam('auth', []);
-
- $document->setAttribute('authLimit', $authValues['limit'] ?? 0);
- $document->setAttribute('authDuration', $authValues['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG);
- $document->setAttribute('authSessionsLimit', $authValues['maxSessions'] ?? 0);
- $document->setAttribute('authPasswordHistory', $authValues['passwordHistory'] ?? 0);
- $document->setAttribute('authPasswordDictionary', $authValues['passwordDictionary'] ?? false);
- $document->setAttribute('authPersonalDataCheck', $authValues['personalDataCheck'] ?? false);
- $document->setAttribute('authDisposableEmails', $authValues['disposableEmails'] ?? false);
- $document->setAttribute('authCanonicalEmails', $authValues['canonicalEmails'] ?? false);
- $document->setAttribute('authFreeEmails', $authValues['freeEmails'] ?? false);
- $document->setAttribute('authMockNumbers', $authValues['mockNumbers'] ?? []);
- $document->setAttribute('authSessionAlerts', $authValues['sessionAlerts'] ?? false);
- $document->setAttribute('authMembershipsUserName', $authValues['membershipsUserName'] ?? false);
- $document->setAttribute('authMembershipsUserEmail', $authValues['membershipsUserEmail'] ?? false);
- $document->setAttribute('authMembershipsMfa', $authValues['membershipsMfa'] ?? false);
- $document->setAttribute('authMembershipsUserId', $authValues['membershipsUserId'] ?? false);
- $document->setAttribute('authMembershipsUserPhone', $authValues['membershipsUserPhone'] ?? false);
- $document->setAttribute('authInvalidateSessions', $authValues['invalidateSessions'] ?? false);
-
- foreach ($auth as $method) {
- $key = $method['key'];
- $value = $authValues[$key] ?? true;
- $document->setAttribute('auth' . ucfirst($key), $value);
- }
- }
-
- private function expandOAuthProviders(Document $document): void
- {
- if (!$document->isSet('oAuthProviders')) {
- return;
- }
-
- $providers = Config::getParam('oAuthProviders', []);
- $providerValues = $document->getAttribute('oAuthProviders', []);
- $projectProviders = [];
-
- foreach ($providers as $key => $provider) {
- if (!$provider['enabled']) {
- // Disabled by Appwrite configuration, exclude from response
- continue;
- }
-
- $projectProviders[] = new Document([
- 'key' => $key,
- 'name' => $provider['name'] ?? '',
- 'appId' => $providerValues[$key . 'Appid'] ?? '',
- 'secret' => '', // Write-only: never expose the stored value
- 'enabled' => $providerValues[$key . 'Enabled'] ?? false,
+ $services[] = new Document([
+ '$id' => $id,
+ 'enabled' => $values[$service['key']] ?? true,
]);
}
- $document->setAttribute('oAuthProviders', $projectProviders);
+ $document->setAttribute('services', $services);
+ }
+
+ private function expandProtocols(Document $document): void
+ {
+ $values = $document->getAttribute('apis', []);
+ $protocols = [];
+
+ foreach (Config::getParam('protocols', []) as $id => $api) {
+ $protocols[] = new Document([
+ '$id' => $id,
+ 'enabled' => $values[$api['key']] ?? true,
+ ]);
+ }
+
+ $document->setAttribute('protocols', $protocols);
+ }
+
+ private function expandAuthMethods(Document $document): void
+ {
+ $values = $document->getAttribute('auths', []);
+ $authMethods = [];
+
+ foreach (Config::getParam('auth', []) as $id => $method) {
+ $authMethods[] = new Document([
+ '$id' => $id,
+ 'enabled' => $values[$method['key']] ?? true
+ ]);
+ }
+
+ $document->setAttribute('authMethods', $authMethods);
}
}
diff --git a/src/Appwrite/Utopia/Response/Model/ProjectAuthMethod.php b/src/Appwrite/Utopia/Response/Model/ProjectAuthMethod.php
new file mode 100644
index 0000000000..cb4a7dc93f
--- /dev/null
+++ b/src/Appwrite/Utopia/Response/Model/ProjectAuthMethod.php
@@ -0,0 +1,50 @@
+addRule('$id', [
+ 'type' => self::TYPE_ENUM,
+ 'description' => 'Auth method ID.',
+ 'default' => '',
+ 'example' => 'email-password',
+ 'enum' => \array_keys(Config::getParam('auth', [])),
+ 'enumSDKName' => 'ProjectAuthMethodId',
+ ])
+ ->addRule('enabled', [
+ 'type' => self::TYPE_BOOLEAN,
+ 'description' => 'Auth method status.',
+ 'example' => false,
+ 'default' => true,
+ ])
+ ;
+ }
+
+ /**
+ * Get Name
+ *
+ * @return string
+ */
+ public function getName(): string
+ {
+ return 'ProjectAuthMethod';
+ }
+
+ /**
+ * Get Type
+ *
+ * @return string
+ */
+ public function getType(): string
+ {
+ return Response::MODEL_PROJECT_AUTH_METHOD;
+ }
+}
diff --git a/src/Appwrite/Utopia/Response/Model/ProjectProtocol.php b/src/Appwrite/Utopia/Response/Model/ProjectProtocol.php
new file mode 100644
index 0000000000..918b1bc630
--- /dev/null
+++ b/src/Appwrite/Utopia/Response/Model/ProjectProtocol.php
@@ -0,0 +1,50 @@
+addRule('$id', [
+ 'type' => self::TYPE_ENUM,
+ 'description' => 'Protocol ID.',
+ 'default' => '',
+ 'example' => 'graphql',
+ 'enum' => \array_keys(Config::getParam('protocols', [])),
+ 'enumSDKName' => 'ProjectProtocolId',
+ ])
+ ->addRule('enabled', [
+ 'type' => self::TYPE_BOOLEAN,
+ 'description' => 'Protocol status.',
+ 'example' => false,
+ 'default' => true,
+ ])
+ ;
+ }
+
+ /**
+ * Get Name
+ *
+ * @return string
+ */
+ public function getName(): string
+ {
+ return 'ProjectProtocol';
+ }
+
+ /**
+ * Get Type
+ *
+ * @return string
+ */
+ public function getType(): string
+ {
+ return Response::MODEL_PROJECT_PROTOCOL;
+ }
+}
diff --git a/src/Appwrite/Utopia/Response/Model/ProjectService.php b/src/Appwrite/Utopia/Response/Model/ProjectService.php
new file mode 100644
index 0000000000..2e76dcfbe7
--- /dev/null
+++ b/src/Appwrite/Utopia/Response/Model/ProjectService.php
@@ -0,0 +1,50 @@
+addRule('$id', [
+ 'type' => self::TYPE_ENUM,
+ 'description' => 'Service ID.',
+ 'default' => '',
+ 'example' => 'sites',
+ 'enum' => \array_keys(\array_filter(Config::getParam('services', []), fn ($element) => $element['optional'])),
+ 'enumSDKName' => 'ProjectServiceId',
+ ])
+ ->addRule('enabled', [
+ 'type' => self::TYPE_BOOLEAN,
+ 'description' => 'Service status.',
+ 'example' => false,
+ 'default' => true,
+ ])
+ ;
+ }
+
+ /**
+ * Get Name
+ *
+ * @return string
+ */
+ public function getName(): string
+ {
+ return 'ProjectService';
+ }
+
+ /**
+ * Get Type
+ *
+ * @return string
+ */
+ public function getType(): string
+ {
+ return Response::MODEL_PROJECT_SERVICE;
+ }
+}
diff --git a/src/Appwrite/Utopia/Response/Model/Report.php b/src/Appwrite/Utopia/Response/Model/Report.php
new file mode 100644
index 0000000000..0c5baf9cdd
--- /dev/null
+++ b/src/Appwrite/Utopia/Response/Model/Report.php
@@ -0,0 +1,99 @@
+addRule('$id', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Report ID.',
+ 'default' => '',
+ 'example' => '5e5ea5c16897e',
+ ])
+ ->addRule('$createdAt', [
+ 'type' => self::TYPE_DATETIME,
+ 'description' => 'Report creation date in ISO 8601 format.',
+ 'default' => '',
+ 'example' => self::TYPE_DATETIME_EXAMPLE,
+ ])
+ ->addRule('$updatedAt', [
+ 'type' => self::TYPE_DATETIME,
+ 'description' => 'Report update date in ISO 8601 format.',
+ 'default' => '',
+ 'example' => self::TYPE_DATETIME_EXAMPLE,
+ ])
+ ->addRule('appId', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'ID of the third-party app that submitted the report.',
+ 'default' => '',
+ 'example' => '5e5ea5c16897e',
+ ])
+ ->addRule('type', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Analyzer that produced this report. e.g. lighthouse, audit, databaseAnalyzer.',
+ 'default' => '',
+ 'example' => 'lighthouse',
+ ])
+ ->addRule('title', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Short, human-readable title for the report.',
+ 'default' => '',
+ 'example' => 'Lighthouse audit for https://appwrite.io/',
+ ])
+ ->addRule('summary', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Markdown summary describing the report.',
+ 'default' => '',
+ 'example' => 'Performance score 78. 4 opportunities found.',
+ ])
+ ->addRule('targetType', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Plural noun describing what the report analyzes, e.g. databases, sites, urls.',
+ 'default' => '',
+ 'example' => 'urls',
+ ])
+ ->addRule('target', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Free-form target identifier (URL for lighthouse, resource ID for db).',
+ 'default' => '',
+ 'example' => 'https://appwrite.io/',
+ ])
+ ->addRule('categories', [
+ 'type' => self::TYPE_STRING,
+ 'description' => 'Categories covered by the report, e.g. performance, accessibility.',
+ 'default' => [],
+ 'example' => ['performance', 'accessibility'],
+ 'array' => true,
+ ])
+ ->addRule('insights', [
+ 'type' => Response::MODEL_INSIGHT,
+ 'description' => 'Insights nested under this report.',
+ 'default' => [],
+ 'example' => [],
+ 'array' => true,
+ ])
+ ->addRule('analyzedAt', [
+ 'type' => self::TYPE_DATETIME,
+ 'description' => 'Time the report was analyzed in ISO 8601 format.',
+ 'default' => null,
+ 'example' => self::TYPE_DATETIME_EXAMPLE,
+ 'required' => false,
+ ]);
+ }
+
+ public function getName(): string
+ {
+ return 'Report';
+ }
+
+ public function getType(): string
+ {
+ return Response::MODEL_REPORT;
+ }
+}
diff --git a/src/Appwrite/Utopia/Response/Model/Rule.php b/src/Appwrite/Utopia/Response/Model/Rule.php
index 1ff854e7ce..d5ea9ee0b7 100644
--- a/src/Appwrite/Utopia/Response/Model/Rule.php
+++ b/src/Appwrite/Utopia/Response/Model/Rule.php
@@ -74,7 +74,7 @@ class Rule extends Model
])
->addRule('deploymentResourceId', [
'type' => self::TYPE_STRING,
- 'description' => 'ID deployment\'s resource. Used if type is "deployment"',
+ 'description' => 'ID of deployment\'s resource (site or function ID). Used if type is "deployment"',
'default' => '',
'example' => 'n3u9feiwmf',
])
@@ -86,10 +86,10 @@ class Rule extends Model
])
->addRule('status', [
'type' => self::TYPE_ENUM,
- 'description' => 'Domain verification status. Possible values are "created", "verifying", "verified" and "unverified"',
- 'default' => 'created',
+ 'description' => 'Domain verification status. Possible values are "unverified", "verifying", "verified"',
+ 'default' => 'unverified',
'example' => 'verified',
- 'enum' => ['created', 'verifying', 'verified', 'unverified'],
+ 'enum' => ['unverified', 'verifying', 'verified'],
])
->addRule('logs', [
'type' => self::TYPE_STRING,
diff --git a/src/Appwrite/Utopia/Response/Model/Table.php b/src/Appwrite/Utopia/Response/Model/Table.php
index 20cd3ccca2..f9f2804fe5 100644
--- a/src/Appwrite/Utopia/Response/Model/Table.php
+++ b/src/Appwrite/Utopia/Response/Model/Table.php
@@ -63,6 +63,9 @@ class Table extends Model
->addRule('columns', [
'type' => [
Response::MODEL_COLUMN_BOOLEAN,
+ // BigInt must come before Integer: response model dispatch is "first match wins",
+ // and Integer matches all int types (including bigint), while BigInt is more specific (size=8).
+ Response::MODEL_COLUMN_BIGINT,
Response::MODEL_COLUMN_INTEGER,
Response::MODEL_COLUMN_FLOAT,
Response::MODEL_COLUMN_EMAIL,
diff --git a/src/Appwrite/Vcs/Comment.php b/src/Appwrite/Vcs/Comment.php
index 4dc0174e50..8741ecff6c 100644
--- a/src/Appwrite/Vcs/Comment.php
+++ b/src/Appwrite/Vcs/Comment.php
@@ -50,6 +50,8 @@ class Comment
protected string $statePrefix = '[appwrite]: #';
+ protected ?string $tip = null;
+
/**
* @var mixed[] $builds
*/
@@ -81,7 +83,14 @@ class Comment
public function generateComment(): string
{
- $json = \json_encode($this->builds);
+ if ($this->tip === null) {
+ $this->tip = $this->tips[\array_rand($this->tips)];
+ }
+
+ $json = \json_encode([
+ 'builds' => $this->builds,
+ 'tip' => $this->tip,
+ ]);
$text = $this->statePrefix . \base64_encode($json) . "\n\n";
@@ -226,8 +235,7 @@ class Comment
$i++;
}
- $tip = $this->tips[array_rand($this->tips)];
- $text .= "\n
\n\n> [!TIP]\n> $tip\n\n";
+ $text .= "\n
\n\n> [!TIP]\n> {$this->tip}\n\n";
return $text;
}
@@ -252,8 +260,15 @@ class Comment
$json = \base64_decode($state);
- $builds = \json_decode($json, true);
- $this->builds = \is_array($builds) ? $builds : [];
+ $data = \json_decode($json, true);
+
+ if (\is_array($data) && \array_key_exists('builds', $data)) {
+ $this->builds = \is_array($data['builds']) ? $data['builds'] : [];
+ $this->tip = $data['tip'] ?? null;
+ } else {
+ // Backward compatibility with old state format (builds array only)
+ $this->builds = \is_array($data) ? $data : [];
+ }
return $this;
}
diff --git a/src/Executor/Exception.php b/src/Executor/Exception.php
new file mode 100644
index 0000000000..b799d22567
--- /dev/null
+++ b/src/Executor/Exception.php
@@ -0,0 +1,7 @@
+timeoutSeconds;
+ }
+}
diff --git a/src/Executor/Executor.php b/src/Executor/Executor.php
index eb74867c9c..c570970732 100644
--- a/src/Executor/Executor.php
+++ b/src/Executor/Executor.php
@@ -2,9 +2,9 @@
namespace Executor;
-use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Utopia\Fetch\BodyMultipart;
-use Exception;
+use Executor\Exception as ExecutorException;
+use Executor\Exception\Timeout as ExecutorTimeout;
use Utopia\System\System;
class Executor
@@ -104,7 +104,7 @@ class Executor
$status = $response['headers']['status-code'];
if ($status >= 400) {
$message = \is_string($response['body']) ? $response['body'] : $response['body']['message'];
- throw new \Exception($message, $status);
+ throw new ExecutorException($message, $status);
}
return $response['body'];
@@ -163,7 +163,7 @@ class Executor
}
if ($status >= 400) {
- throw new \Exception($message, $status);
+ throw new ExecutorException($message, $status);
}
return $response['body'];
@@ -247,7 +247,7 @@ class Executor
$status = $response['headers']['status-code'];
if ($status >= 400) {
$message = \is_string($response['body']) ? $response['body'] : $response['body']['message'];
- throw new \Exception($message, $status);
+ throw new ExecutorException($message, $status);
}
$headers = $response['body']['headers'] ?? [];
@@ -281,7 +281,7 @@ class Executor
$status = $response['headers']['status-code'];
if ($status >= 400) {
$message = \is_string($response['body']) ? $response['body'] : $response['body']['message'];
- throw new \Exception($message, $status);
+ throw new ExecutorException($message, $status);
}
return $response['body'];
@@ -401,7 +401,7 @@ class Executor
$json = json_decode($responseBody, true);
if ($json === null) {
- throw new Exception('Failed to parse response: ' . $responseBody);
+ throw new ExecutorException('Failed to parse response: ' . $responseBody);
}
$responseBody = $json;
@@ -412,9 +412,9 @@ class Executor
if ($curlError) {
if ($curlError == CURLE_OPERATION_TIMEDOUT) {
- throw new AppwriteException(AppwriteException::FUNCTION_SYNCHRONOUS_TIMEOUT);
+ throw new ExecutorTimeout('Executor request timed out', $timeout);
}
- throw new Exception($curlErrorMessage . ' with status code ' . $responseStatus, $responseStatus);
+ throw new ExecutorException($curlErrorMessage . ' with status code ' . $responseStatus, $responseStatus);
}
$responseHeaders['status-code'] = $responseStatus;
diff --git a/tests/benchmarks/http.js b/tests/benchmarks/http.js
index f7bb54024d..ef42e99663 100644
--- a/tests/benchmarks/http.js
+++ b/tests/benchmarks/http.js
@@ -380,6 +380,7 @@ function computeFlow(ctx) {
api('GET', '/functions/runtimes', null, ctx.sessionHeaders, [200], 'functions.runtimes.list');
api('GET', '/functions/specifications', null, ctx.apiHeaders, [200], 'functions.specifications.list');
const functionVariable = api('POST', `/functions/${functionId}/variables`, {
+ variableId: 'unique()',
key: 'BENCHMARK',
value: 'true',
secret: false,
diff --git a/tests/e2e/Scopes/ProjectCustom.php b/tests/e2e/Scopes/ProjectCustom.php
index 3071ddfa2a..c34a6527f9 100644
--- a/tests/e2e/Scopes/ProjectCustom.php
+++ b/tests/e2e/Scopes/ProjectCustom.php
@@ -173,10 +173,14 @@ trait ProjectCustom
'oauth2.write',
'mocks.read',
'mocks.write',
- 'policies.read',
- 'policies.write',
+ 'project.policies.read',
+ 'project.policies.write',
'templates.read',
'templates.write',
+ 'insights.read',
+ 'insights.write',
+ 'reports.read',
+ 'reports.write',
],
]);
diff --git a/tests/e2e/Services/Account/AccountCustomClientTest.php b/tests/e2e/Services/Account/AccountCustomClientTest.php
index da788c3caa..160ee39e21 100644
--- a/tests/e2e/Services/Account/AccountCustomClientTest.php
+++ b/tests/e2e/Services/Account/AccountCustomClientTest.php
@@ -4163,4 +4163,72 @@ class AccountCustomClientTest extends Scope
$this->assertEquals(401, $verification3['headers']['status-code']);
}
+
+ public function testRefreshEmailPasswordSession(): void
+ {
+ $email = uniqid() . 'user@localhost.test';
+
+ $account = $this->client->call(Client::METHOD_POST, '/account', array_merge([
+ 'origin' => 'http://localhost',
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ]), [
+ 'userId' => ID::unique(),
+ 'email' => $email,
+ 'password' => 'password',
+ ]);
+
+ $this->assertEquals(201, $account['headers']['status-code']);
+
+ $session = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
+ 'origin' => 'http://localhost',
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ]), [
+ 'email' => $email,
+ 'password' => 'password',
+ ]);
+
+ $this->assertEquals(201, $session['headers']['status-code']);
+ $this->assertNotEmpty($session['body']['$id']);
+
+ $sessionId = $session['body']['$id'];
+ $cookie = 'a_session_' . $this->getProject()['$id'] . '=' .$session['cookies']['a_session_' . $this->getProject()['$id']];
+
+ $session = $this->client->call(Client::METHOD_GET, '/account/sessions/current', array_merge([
+ 'origin' => 'http://localhost',
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'cookie' => $cookie,
+ ]));
+
+ $this->assertEquals(200, $session['headers']['status-code']);
+ $this->assertNotEmpty($session['body']['expire']);
+ $expiryBefore = $session['body']['expire'];
+
+ \sleep(3); // Small delay to ensure expiry an expand
+
+ $session = $this->client->call(Client::METHOD_PATCH, '/account/sessions/' . $sessionId, array_merge([
+ 'origin' => 'http://localhost',
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'cookie' => $cookie,
+ ]));
+
+ $this->assertEquals(200, $session['headers']['status-code']);
+ $this->assertNotEmpty($session['body']['expire']);
+ $expiryAfter = $session['body']['expire'];
+
+ $this->assertGreaterThan(\strtotime($expiryBefore), \strtotime($expiryAfter));
+
+ $session = $this->client->call(Client::METHOD_GET, '/account/sessions/current', array_merge([
+ 'origin' => 'http://localhost',
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'cookie' => $cookie,
+ ]));
+
+ $this->assertEquals(200, $session['headers']['status-code']);
+ $this->assertEquals(\strtotime($expiryAfter), \strtotime($session['body']['expire']));
+ }
}
diff --git a/tests/e2e/Services/Advisor/AdvisorBase.php b/tests/e2e/Services/Advisor/AdvisorBase.php
new file mode 100644
index 0000000000..f228cf5591
--- /dev/null
+++ b/tests/e2e/Services/Advisor/AdvisorBase.php
@@ -0,0 +1,122 @@
+ 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ];
+ }
+
+ protected function getReport(string $reportId, ?array $headers = null): array
+ {
+ return $this->client->call(Client::METHOD_GET, '/reports/' . $reportId, $headers ?? $this->serverHeaders());
+ }
+
+ protected function listReports(array $params = [], ?array $headers = null): array
+ {
+ return $this->client->call(Client::METHOD_GET, '/reports', $headers ?? $this->serverHeaders(), $params);
+ }
+
+ protected function getInsight(string $reportId, string $insightId, ?array $headers = null): array
+ {
+ return $this->client->call(Client::METHOD_GET, '/reports/' . $reportId . '/insights/' . $insightId, $headers ?? $this->serverHeaders());
+ }
+
+ protected function listInsights(string $reportId, array $params = [], ?array $headers = null): array
+ {
+ return $this->client->call(Client::METHOD_GET, '/reports/' . $reportId . '/insights', $headers ?? $this->serverHeaders(), $params);
+ }
+
+ public function testListReports(): void
+ {
+ $list = $this->listReports();
+
+ $this->assertSame(200, $list['headers']['status-code']);
+ $this->assertArrayHasKey('reports', $list['body']);
+ $this->assertArrayHasKey('total', $list['body']);
+ $this->assertIsArray($list['body']['reports']);
+ }
+
+ public function testGetReportMissing(): void
+ {
+ $missing = $this->getReport(ID::unique());
+
+ $this->assertSame(404, $missing['headers']['status-code']);
+ $this->assertSame('report_not_found', $missing['body']['type']);
+ }
+
+ public function testListInsightsMissingReport(): void
+ {
+ $missing = $this->listInsights(ID::unique());
+
+ $this->assertSame(404, $missing['headers']['status-code']);
+ $this->assertSame('report_not_found', $missing['body']['type']);
+ }
+
+ public function testGetInsightMissingReport(): void
+ {
+ $missing = $this->getInsight(ID::unique(), ID::unique());
+
+ $this->assertSame(404, $missing['headers']['status-code']);
+ $this->assertSame('report_not_found', $missing['body']['type']);
+ }
+
+ public function testReportsCreateAndUpdateNotExposed(): void
+ {
+ $create = $this->client->call(Client::METHOD_POST, '/reports', $this->serverHeaders(), [
+ 'reportId' => ID::unique(),
+ 'type' => 'audit',
+ 'title' => 'Read-only check',
+ 'targetType' => 'sites',
+ 'target' => 'home',
+ ]);
+ $this->assertSame(404, $create['headers']['status-code']);
+
+ $update = $this->client->call(Client::METHOD_PATCH, '/reports/' . ID::unique(), $this->serverHeaders(), [
+ 'title' => 'Read-only check',
+ ]);
+ $this->assertSame(404, $update['headers']['status-code']);
+ }
+
+ public function testDeleteReportMissing(): void
+ {
+ $delete = $this->client->call(Client::METHOD_DELETE, '/reports/' . ID::unique(), $this->serverHeaders());
+ $this->assertSame(404, $delete['headers']['status-code']);
+ $this->assertSame('report_not_found', $delete['body']['type']);
+ }
+
+ public function testInsightsCreateUpdateDeleteNotExposed(): void
+ {
+ $create = $this->client->call(
+ Client::METHOD_POST,
+ '/reports/' . ID::unique() . '/insights',
+ $this->serverHeaders(),
+ []
+ );
+ $this->assertSame(404, $create['headers']['status-code']);
+
+ $update = $this->client->call(
+ Client::METHOD_PATCH,
+ '/reports/' . ID::unique() . '/insights/' . ID::unique(),
+ $this->serverHeaders(),
+ ['status' => 'dismissed']
+ );
+ $this->assertSame(404, $update['headers']['status-code']);
+
+ $delete = $this->client->call(
+ Client::METHOD_DELETE,
+ '/reports/' . ID::unique() . '/insights/' . ID::unique(),
+ $this->serverHeaders()
+ );
+ $this->assertSame(404, $delete['headers']['status-code']);
+ }
+}
diff --git a/tests/e2e/Services/Advisor/AdvisorCustomServerTest.php b/tests/e2e/Services/Advisor/AdvisorCustomServerTest.php
new file mode 100644
index 0000000000..d91f95035e
--- /dev/null
+++ b/tests/e2e/Services/Advisor/AdvisorCustomServerTest.php
@@ -0,0 +1,58 @@
+getProject()['$id'];
+
+ $userKey = $this->getNewKey([
+ // Advisor read APIs are protected by the underlying report/insight resource scopes.
+ 'insights.read',
+ 'reports.read',
+ ]);
+
+ $listed = $this->client->call(
+ Client::METHOD_GET,
+ '/reports',
+ [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $projectId,
+ 'x-appwrite-key' => $userKey,
+ ]
+ );
+
+ $this->assertSame(200, $listed['headers']['status-code']);
+
+ $create = $this->client->call(
+ Client::METHOD_POST,
+ '/reports',
+ [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $projectId,
+ 'x-appwrite-key' => $userKey,
+ ],
+ [
+ 'reportId' => ID::unique(),
+ 'type' => 'audit',
+ 'title' => 'Read-only check',
+ 'targetType' => 'sites',
+ 'target' => 'home',
+ ]
+ );
+
+ $this->assertSame(404, $create['headers']['status-code']);
+ }
+}
diff --git a/tests/e2e/Services/Avatars/AvatarsBase.php b/tests/e2e/Services/Avatars/AvatarsBase.php
index 14442abb60..614349ae74 100644
--- a/tests/e2e/Services/Avatars/AvatarsBase.php
+++ b/tests/e2e/Services/Avatars/AvatarsBase.php
@@ -204,7 +204,7 @@ trait AvatarsBase
$response = $this->client->call(Client::METHOD_GET, '/avatars/image', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
- 'url' => 'https://appwrite.io/images/open-graph/website.png',
+ 'url' => 'https://appwrite.io/images/open-graph/website.avif',
]);
$this->assertEquals(200, $response['headers']['status-code']);
@@ -216,7 +216,7 @@ trait AvatarsBase
$response = $this->client->call(Client::METHOD_GET, '/avatars/image', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
- 'url' => 'https://appwrite.io/images/open-graph/website.png',
+ 'url' => 'https://appwrite.io/images/open-graph/website.avif',
'width' => 200,
'height' => 200,
]);
@@ -230,7 +230,7 @@ trait AvatarsBase
$response = $this->client->call(Client::METHOD_GET, '/avatars/image', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
- 'url' => 'https://appwrite.io/images/open-graph/website.png',
+ 'url' => 'https://appwrite.io/images/open-graph/website.avif',
'width' => 300,
'height' => 300,
'quality' => 30,
@@ -258,7 +258,7 @@ trait AvatarsBase
$response = $this->client->call(Client::METHOD_GET, '/avatars/image', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
- 'url' => 'https://appwrite.io/images/open-graph/website.png',
+ 'url' => 'https://appwrite.io/images/open-graph/website.avif',
'width' => 2001,
'height' => 300,
'quality' => 30,
diff --git a/tests/e2e/Services/Console/ConsoleConsoleClientTest.php b/tests/e2e/Services/Console/ConsoleConsoleClientTest.php
index c8f921f2ec..43daba470b 100644
--- a/tests/e2e/Services/Console/ConsoleConsoleClientTest.php
+++ b/tests/e2e/Services/Console/ConsoleConsoleClientTest.php
@@ -175,4 +175,49 @@ class ConsoleConsoleClientTest extends Scope
$this->assertNotNull($usersRead);
$this->assertEquals('Access to read users', $usersRead['description']);
}
+
+ public function testListOrganizationScopes(): void
+ {
+ $response = $this->client->call(Client::METHOD_GET, '/console/scopes/organization', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertIsInt($response['body']['total']);
+ $this->assertIsArray($response['body']['scopes']);
+ $this->assertGreaterThan(0, $response['body']['total']);
+ $this->assertEquals($response['body']['total'], \count($response['body']['scopes']));
+
+ $scopeIds = \array_column($response['body']['scopes'], '$id');
+
+ // Well-known scopes must be present
+ $this->assertContains('projects.read', $scopeIds);
+ $this->assertContains('projects.write', $scopeIds);
+
+ // Every scope has the expected shape
+ foreach ($response['body']['scopes'] as $scope) {
+ $this->assertArrayHasKey('$id', $scope);
+ $this->assertIsString($scope['$id']);
+ $this->assertNotEmpty($scope['$id']);
+ $this->assertArrayHasKey('description', $scope);
+ $this->assertIsString($scope['description']);
+ $this->assertNotEmpty($scope['description']);
+ $this->assertArrayHasKey('deprecated', $scope);
+ $this->assertIsBool($scope['deprecated']);
+ $this->assertArrayHasKey('category', $scope);
+ $this->assertIsString($scope['category']);
+ }
+
+ // A specific scope has the expected description
+ $projectsRead = null;
+ foreach ($response['body']['scopes'] as $scope) {
+ if ($scope['$id'] === 'projects.read') {
+ $projectsRead = $scope;
+ break;
+ }
+ }
+ $this->assertNotNull($projectsRead);
+ $this->assertEquals('Access to read organization projects', $projectsRead['description']);
+ }
}
diff --git a/tests/e2e/Services/Console/ConsoleCustomServerTest.php b/tests/e2e/Services/Console/ConsoleCustomServerTest.php
index f06011843f..e7a95fd357 100644
--- a/tests/e2e/Services/Console/ConsoleCustomServerTest.php
+++ b/tests/e2e/Services/Console/ConsoleCustomServerTest.php
@@ -74,4 +74,35 @@ class ConsoleCustomServerTest extends Scope
$this->assertArrayHasKey('deprecated', $usersRead);
$this->assertIsBool($usersRead['deprecated']);
}
+
+ public function testListOrganizationScopes(): void
+ {
+ // Public endpoint: must succeed without admin authentication. Drop the
+ // headers from getHeaders() and only pass project + content-type.
+ $response = $this->client->call(Client::METHOD_GET, '/console/scopes/organization', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertIsInt($response['body']['total']);
+ $this->assertIsArray($response['body']['scopes']);
+ $this->assertGreaterThan(0, $response['body']['total']);
+
+ $scopeIds = \array_column($response['body']['scopes'], '$id');
+ $this->assertContains('projects.read', $scopeIds);
+
+ $projectsRead = null;
+ foreach ($response['body']['scopes'] as $scope) {
+ if ($scope['$id'] === 'projects.read') {
+ $projectsRead = $scope;
+ break;
+ }
+ }
+ $this->assertNotNull($projectsRead);
+ $this->assertIsString($projectsRead['description']);
+ $this->assertNotEmpty($projectsRead['description']);
+ $this->assertArrayHasKey('deprecated', $projectsRead);
+ $this->assertIsBool($projectsRead['deprecated']);
+ }
}
diff --git a/tests/e2e/Services/Databases/VectorsDB/DatabasesConsoleClientTest.php b/tests/e2e/Services/Databases/VectorsDB/DatabasesConsoleClientTest.php
index abe4d4968b..a7cad1c0f7 100644
--- a/tests/e2e/Services/Databases/VectorsDB/DatabasesConsoleClientTest.php
+++ b/tests/e2e/Services/Databases/VectorsDB/DatabasesConsoleClientTest.php
@@ -10,7 +10,6 @@ use Tests\E2E\Scopes\SideConsole;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
-use Utopia\Database\Query;
class DatabasesConsoleClientTest extends Scope
{
@@ -258,55 +257,4 @@ class DatabasesConsoleClientTest extends Scope
$this->assertIsArray($response['body']['documents']);
}
- #[Depends('testCreateCollection')]
- public function testGetCollectionLogs(array $data)
- {
- $databaseId = $data['databaseId'];
- /**
- * Test for SUCCESS
- */
- $logs = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $data['moviesId'] . '/logs', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- ], $this->getHeaders()));
-
- $this->assertEquals(200, $logs['headers']['status-code']);
- $this->assertIsArray($logs['body']['logs']);
- $this->assertIsNumeric($logs['body']['total']);
-
- $logs = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $data['moviesId'] . '/logs', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- ], $this->getHeaders()), [
- 'queries' => [Query::limit(1)->toString()]
- ]);
-
- $this->assertEquals(200, $logs['headers']['status-code']);
- $this->assertIsArray($logs['body']['logs']);
- $this->assertLessThanOrEqual(1, count($logs['body']['logs']));
- $this->assertIsNumeric($logs['body']['total']);
-
- $logs = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $data['moviesId'] . '/logs', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- ], $this->getHeaders()), [
- 'queries' => [Query::offset(1)->toString()]
- ]);
-
- $this->assertEquals(200, $logs['headers']['status-code']);
- $this->assertIsArray($logs['body']['logs']);
- $this->assertIsNumeric($logs['body']['total']);
-
- $logs = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $data['moviesId'] . '/logs', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- ], $this->getHeaders()), [
- 'queries' => [Query::offset(1)->toString(), Query::limit(1)->toString()]
- ]);
-
- $this->assertEquals(200, $logs['headers']['status-code']);
- $this->assertIsArray($logs['body']['logs']);
- $this->assertLessThanOrEqual(1, count($logs['body']['logs']));
- $this->assertIsNumeric($logs['body']['total']);
- }
}
diff --git a/tests/e2e/Services/Databases/VectorsDBConsoleClientTest.php b/tests/e2e/Services/Databases/VectorsDBConsoleClientTest.php
index 80c2bc9d5c..238d197158 100644
--- a/tests/e2e/Services/Databases/VectorsDBConsoleClientTest.php
+++ b/tests/e2e/Services/Databases/VectorsDBConsoleClientTest.php
@@ -10,7 +10,6 @@ use Tests\E2E\Scopes\SideConsole;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
-use Utopia\Database\Query;
class VectorsDBConsoleClientTest extends Scope
{
@@ -258,55 +257,4 @@ class VectorsDBConsoleClientTest extends Scope
$this->assertIsArray($response['body']['documents']);
}
- #[Depends('testCreateCollection')]
- public function testGetCollectionLogs(array $data)
- {
- $databaseId = $data['databaseId'];
- /**
- * Test for SUCCESS
- */
- $logs = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $data['moviesId'] . '/logs', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- ], $this->getHeaders()));
-
- $this->assertEquals(200, $logs['headers']['status-code']);
- $this->assertIsArray($logs['body']['logs']);
- $this->assertIsNumeric($logs['body']['total']);
-
- $logs = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $data['moviesId'] . '/logs', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- ], $this->getHeaders()), [
- 'queries' => [Query::limit(1)->toString()]
- ]);
-
- $this->assertEquals(200, $logs['headers']['status-code']);
- $this->assertIsArray($logs['body']['logs']);
- $this->assertLessThanOrEqual(1, count($logs['body']['logs']));
- $this->assertIsNumeric($logs['body']['total']);
-
- $logs = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $data['moviesId'] . '/logs', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- ], $this->getHeaders()), [
- 'queries' => [Query::offset(1)->toString()]
- ]);
-
- $this->assertEquals(200, $logs['headers']['status-code']);
- $this->assertIsArray($logs['body']['logs']);
- $this->assertIsNumeric($logs['body']['total']);
-
- $logs = $this->client->call(Client::METHOD_GET, '/vectorsdb/' . $databaseId . '/collections/' . $data['moviesId'] . '/logs', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- ], $this->getHeaders()), [
- 'queries' => [Query::offset(1)->toString(), Query::limit(1)->toString()]
- ]);
-
- $this->assertEquals(200, $logs['headers']['status-code']);
- $this->assertIsArray($logs['body']['logs']);
- $this->assertLessThanOrEqual(1, count($logs['body']['logs']));
- $this->assertIsNumeric($logs['body']['total']);
- }
}
diff --git a/tests/e2e/Services/Functions/FunctionsConsoleClientTest.php b/tests/e2e/Services/Functions/FunctionsConsoleClientTest.php
index 06044d9984..5d501486fd 100644
--- a/tests/e2e/Services/Functions/FunctionsConsoleClientTest.php
+++ b/tests/e2e/Services/Functions/FunctionsConsoleClientTest.php
@@ -7,8 +7,10 @@ use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideConsole;
use Utopia\Console;
+use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Role;
+use Utopia\Database\Query;
class FunctionsConsoleClientTest extends Scope
{
@@ -70,6 +72,7 @@ class FunctionsConsoleClientTest extends Scope
$variable = $this->createVariable(
$functionId,
[
+ 'variableId' => ID::unique(),
'key' => 'APP_TEST',
'value' => 'TESTINGVALUE',
'secret' => false
@@ -82,6 +85,7 @@ class FunctionsConsoleClientTest extends Scope
$secretVariable = $this->createVariable(
$functionId,
[
+ 'variableId' => ID::unique(),
'key' => 'APP_TEST_1',
'value' => 'TESTINGVALUE_1',
'secret' => true
@@ -196,6 +200,7 @@ class FunctionsConsoleClientTest extends Scope
$variable = $this->createVariable(
$functionId,
[
+ 'variableId' => ID::unique(),
'key' => 'APP_TEST',
'value' => 'TESTINGVALUE',
'secret' => false
@@ -208,6 +213,7 @@ class FunctionsConsoleClientTest extends Scope
$variable = $this->createVariable(
$functionId,
[
+ 'variableId' => ID::unique(),
'key' => 'APP_TEST_1',
'value' => 'TESTINGVALUE_1',
'secret' => true
@@ -226,6 +232,7 @@ class FunctionsConsoleClientTest extends Scope
$variable = $this->createVariable(
$functionId,
[
+ 'variableId' => ID::unique(),
'key' => 'APP_TEST',
'value' => 'ANOTHERTESTINGVALUE',
'secret' => false
@@ -234,10 +241,47 @@ class FunctionsConsoleClientTest extends Scope
$this->assertEquals(409, $variable['headers']['status-code']);
+ // Test for invalid variableId
+ $variable = $this->createVariable(
+ $functionId,
+ [
+ 'variableId' => '!invalid-id!',
+ 'key' => 'INVALID_ID_KEY',
+ 'value' => 'value',
+ ]
+ );
+
+ $this->assertEquals(400, $variable['headers']['status-code']);
+
+ // Test for duplicate variableId
+ $duplicateVariableId = ID::unique();
+ $variable = $this->createVariable(
+ $functionId,
+ [
+ 'variableId' => $duplicateVariableId,
+ 'key' => 'DUP_ID_KEY_1',
+ 'value' => 'value1',
+ ]
+ );
+
+ $this->assertEquals(201, $variable['headers']['status-code']);
+
+ $duplicate = $this->createVariable(
+ $functionId,
+ [
+ 'variableId' => $duplicateVariableId,
+ 'key' => 'DUP_ID_KEY_2',
+ 'value' => 'value2',
+ ]
+ );
+
+ $this->assertEquals(409, $duplicate['headers']['status-code']);
+
// Test for invalid key
$variable = $this->createVariable(
$functionId,
[
+ 'variableId' => ID::unique(),
'key' => str_repeat("A", 256),
'value' => 'TESTINGVALUE'
]
@@ -249,6 +293,7 @@ class FunctionsConsoleClientTest extends Scope
$variable = $this->createVariable(
$functionId,
[
+ 'variableId' => ID::unique(),
'key' => 'LONGKEY',
'value' => str_repeat("#", 8193),
]
@@ -283,6 +328,150 @@ class FunctionsConsoleClientTest extends Scope
*/
}
+ public function testListVariablesWithLimit(): void
+ {
+ // Create a fresh function for this test
+ $function = $this->createFunction([
+ 'functionId' => ID::unique(),
+ 'name' => 'Test List Variables With Limit',
+ 'execute' => [Role::user($this->getUser()['$id'])->toString()],
+ 'runtime' => 'node-22',
+ 'entrypoint' => 'index.js',
+ 'timeout' => 10,
+ ]);
+ $this->assertEquals(201, $function['headers']['status-code']);
+ $functionId = $function['body']['$id'];
+
+ $variable1 = $this->createVariable($functionId, [
+ 'variableId' => ID::unique(),
+ 'key' => 'LIMIT_KEY_1',
+ 'value' => 'limit-value-1',
+ ]);
+ $this->assertEquals(201, $variable1['headers']['status-code']);
+
+ $variable2 = $this->createVariable($functionId, [
+ 'variableId' => ID::unique(),
+ 'key' => 'LIMIT_KEY_2',
+ 'value' => 'limit-value-2',
+ ]);
+ $this->assertEquals(201, $variable2['headers']['status-code']);
+
+ // List with limit of 1
+ $response = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/variables', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'queries' => [
+ Query::limit(1)->toString(),
+ ],
+ 'total' => true,
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertCount(1, $response['body']['variables']);
+ $this->assertGreaterThanOrEqual(2, $response['body']['total']);
+
+ $this->cleanupFunction($functionId);
+ }
+
+ public function testListVariablesWithoutTotal(): void
+ {
+ // Create a fresh function for this test
+ $function = $this->createFunction([
+ 'functionId' => ID::unique(),
+ 'name' => 'Test List Variables Without Total',
+ 'execute' => [Role::user($this->getUser()['$id'])->toString()],
+ 'runtime' => 'node-22',
+ 'entrypoint' => 'index.js',
+ 'timeout' => 10,
+ ]);
+ $this->assertEquals(201, $function['headers']['status-code']);
+ $functionId = $function['body']['$id'];
+
+ $variable = $this->createVariable($functionId, [
+ 'variableId' => ID::unique(),
+ 'key' => 'NO_TOTAL_KEY',
+ 'value' => 'no-total-value',
+ ]);
+ $this->assertEquals(201, $variable['headers']['status-code']);
+
+ // List with total=false
+ $response = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/variables', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'total' => false,
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals(0, $response['body']['total']);
+ $this->assertGreaterThanOrEqual(1, \count($response['body']['variables']));
+
+ $this->cleanupFunction($functionId);
+ }
+
+ public function testListVariablesCursorPagination(): void
+ {
+ // Create a fresh function for this test
+ $function = $this->createFunction([
+ 'functionId' => ID::unique(),
+ 'name' => 'Test List Variables Cursor Pagination',
+ 'execute' => [Role::user($this->getUser()['$id'])->toString()],
+ 'runtime' => 'node-22',
+ 'entrypoint' => 'index.js',
+ 'timeout' => 10,
+ ]);
+ $this->assertEquals(201, $function['headers']['status-code']);
+ $functionId = $function['body']['$id'];
+
+ $variable1 = $this->createVariable($functionId, [
+ 'variableId' => ID::unique(),
+ 'key' => 'CURSOR_KEY_1',
+ 'value' => 'cursor-value-1',
+ ]);
+ $this->assertEquals(201, $variable1['headers']['status-code']);
+
+ $variable2 = $this->createVariable($functionId, [
+ 'variableId' => ID::unique(),
+ 'key' => 'CURSOR_KEY_2',
+ 'value' => 'cursor-value-2',
+ ]);
+ $this->assertEquals(201, $variable2['headers']['status-code']);
+
+ // Get first page with limit 1
+ $page1 = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/variables', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'queries' => [
+ Query::limit(1)->toString(),
+ ],
+ 'total' => true,
+ ]);
+
+ $this->assertEquals(200, $page1['headers']['status-code']);
+ $this->assertCount(1, $page1['body']['variables']);
+ $cursorId = $page1['body']['variables'][0]['$id'];
+
+ // Get next page using cursor
+ $page2 = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/variables', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'queries' => [
+ Query::limit(1)->toString(),
+ Query::cursorAfter(new Document(['$id' => $cursorId]))->toString(),
+ ],
+ 'total' => true,
+ ]);
+
+ $this->assertEquals(200, $page2['headers']['status-code']);
+ $this->assertCount(1, $page2['body']['variables']);
+ $this->assertNotEquals($cursorId, $page2['body']['variables'][0]['$id']);
+
+ $this->cleanupFunction($functionId);
+ }
+
public function testGetVariable(): void
{
$data = $this->setupTestVariables();
@@ -337,6 +526,7 @@ class FunctionsConsoleClientTest extends Scope
$functionId = $function['body']['$id'];
$variable = $this->createVariable($functionId, [
+ 'variableId' => ID::unique(),
'key' => 'APP_TEST',
'value' => 'TESTINGVALUE',
'secret' => false
@@ -345,6 +535,7 @@ class FunctionsConsoleClientTest extends Scope
$variableId = $variable['body']['$id'];
$secretVariable = $this->createVariable($functionId, [
+ 'variableId' => ID::unique(),
'key' => 'APP_TEST_1',
'value' => 'TESTINGVALUE_1',
'secret' => true
@@ -457,6 +648,7 @@ class FunctionsConsoleClientTest extends Scope
* Test for FAILURE
*/
+ // Update with no parameters should fail with 400
$response = $this->client->call(Client::METHOD_PUT, '/functions/' . $functionId . '/variables/' . $variableId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
@@ -464,6 +656,7 @@ class FunctionsConsoleClientTest extends Scope
$this->assertEquals(400, $response['headers']['status-code']);
+ // Update with only value should succeed
$response = $this->client->call(Client::METHOD_PUT, '/functions/' . $functionId . '/variables/' . $variableId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
@@ -471,7 +664,7 @@ class FunctionsConsoleClientTest extends Scope
'value' => 'TESTINGVALUEUPDATED_2'
]);
- $this->assertEquals(400, $response['headers']['status-code']);
+ $this->assertEquals(200, $response['headers']['status-code']);
$longKey = str_repeat("A", 256);
$response = $this->client->call(Client::METHOD_PUT, '/functions/' . $functionId . '/variables/' . $variableId, array_merge([
@@ -496,6 +689,110 @@ class FunctionsConsoleClientTest extends Scope
$this->assertEquals(400, $response['headers']['status-code']);
}
+ public function testUpdateVariableKey(): void
+ {
+ // Create a fresh function and variable for this test
+ $function = $this->createFunction([
+ 'functionId' => ID::unique(),
+ 'name' => 'Test Update Variable Key',
+ 'execute' => [Role::user($this->getUser()['$id'])->toString()],
+ 'runtime' => 'node-22',
+ 'entrypoint' => 'index.js',
+ 'timeout' => 10,
+ ]);
+ $this->assertEquals(201, $function['headers']['status-code']);
+ $functionId = $function['body']['$id'];
+
+ $variable = $this->createVariable($functionId, [
+ 'variableId' => ID::unique(),
+ 'key' => 'KEY_BEFORE',
+ 'value' => 'unchanged-value',
+ 'secret' => false
+ ]);
+ $this->assertEquals(201, $variable['headers']['status-code']);
+ $variableId = $variable['body']['$id'];
+
+ // Update only key (key is nullable, but we provide a new key)
+ $response = $this->client->call(Client::METHOD_PUT, '/functions/' . $functionId . '/variables/' . $variableId, array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'key' => 'KEY_AFTER',
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals('KEY_AFTER', $response['body']['key']);
+ $this->assertEquals('unchanged-value', $response['body']['value']);
+
+ $this->cleanupFunction($functionId);
+ }
+
+ public function testUpdateVariableValueOnly(): void
+ {
+ // Create a fresh function and variable for this test
+ $function = $this->createFunction([
+ 'functionId' => ID::unique(),
+ 'name' => 'Test Update Variable Value',
+ 'execute' => [Role::user($this->getUser()['$id'])->toString()],
+ 'runtime' => 'node-22',
+ 'entrypoint' => 'index.js',
+ 'timeout' => 10,
+ ]);
+ $this->assertEquals(201, $function['headers']['status-code']);
+ $functionId = $function['body']['$id'];
+
+ $variable = $this->createVariable($functionId, [
+ 'variableId' => ID::unique(),
+ 'key' => 'UNCHANGED_KEY',
+ 'value' => 'value-before',
+ 'secret' => false
+ ]);
+ $this->assertEquals(201, $variable['headers']['status-code']);
+ $variableId = $variable['body']['$id'];
+
+ // Update only value
+ $response = $this->client->call(Client::METHOD_PUT, '/functions/' . $functionId . '/variables/' . $variableId, array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'value' => 'value-after',
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals('UNCHANGED_KEY', $response['body']['key']);
+ $this->assertEquals('value-after', $response['body']['value']);
+
+ $this->cleanupFunction($functionId);
+ }
+
+ public function testUpdateVariableNotFound(): void
+ {
+ // Create a fresh function for this test
+ $function = $this->createFunction([
+ 'functionId' => ID::unique(),
+ 'name' => 'Test Update Variable Not Found',
+ 'execute' => [Role::user($this->getUser()['$id'])->toString()],
+ 'runtime' => 'node-22',
+ 'entrypoint' => 'index.js',
+ 'timeout' => 10,
+ ]);
+ $this->assertEquals(201, $function['headers']['status-code']);
+ $functionId = $function['body']['$id'];
+
+ $response = $this->client->call(Client::METHOD_PUT, '/functions/' . $functionId . '/variables/non-existent-id', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'key' => 'NEW_KEY',
+ 'value' => 'new-value',
+ ]);
+
+ $this->assertEquals(404, $response['headers']['status-code']);
+ $this->assertEquals('variable_not_found', $response['body']['type']);
+
+ $this->cleanupFunction($functionId);
+ }
+
public function testDeleteVariable(): void
{
// Create a fresh function and variables for this test since it deletes them
@@ -512,6 +809,7 @@ class FunctionsConsoleClientTest extends Scope
$functionId = $function['body']['$id'];
$variable = $this->createVariable($functionId, [
+ 'variableId' => ID::unique(),
'key' => 'APP_TEST',
'value' => 'TESTINGVALUE',
'secret' => false
@@ -520,6 +818,7 @@ class FunctionsConsoleClientTest extends Scope
$variableId = $variable['body']['$id'];
$secretVariable = $this->createVariable($functionId, [
+ 'variableId' => ID::unique(),
'key' => 'APP_TEST_1',
'value' => 'TESTINGVALUE_1',
'secret' => true
@@ -585,6 +884,7 @@ class FunctionsConsoleClientTest extends Scope
// create variable
$variable = $this->createVariable($functionId, [
+ 'variableId' => ID::unique(),
'key' => 'CUSTOM_VARIABLE',
'value' => 'a_secret_value',
'secret' => true,
diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php
index 8e784b3d5b..b1f07c3f9d 100644
--- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php
+++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php
@@ -53,14 +53,17 @@ class FunctionsCustomServerTest extends Scope
$functionId = $function['body']['$id'] ?? '';
$variable = $this->createVariable($functionId, [
+ 'variableId' => 'unique()',
'key' => 'funcKey1',
'value' => 'funcValue1',
]);
$variable2 = $this->createVariable($functionId, [
+ 'variableId' => 'unique()',
'key' => 'funcKey2',
'value' => 'funcValue2',
]);
$variable3 = $this->createVariable($functionId, [
+ 'variableId' => 'unique()',
'key' => 'funcKey3',
'value' => 'funcValue3',
]);
@@ -109,6 +112,7 @@ class FunctionsCustomServerTest extends Scope
// Create a variable for later tests
$variable = $this->createVariable($functionId, [
+ 'variableId' => 'unique()',
'key' => 'GLOBAL_VARIABLE',
'value' => 'Global Variable Value',
]);
@@ -278,14 +282,17 @@ class FunctionsCustomServerTest extends Scope
$this->assertEquals(10, $function['body']['timeout']);
$variable = $this->createVariable($functionId, [
+ 'variableId' => 'unique()',
'key' => 'funcKey1',
'value' => 'funcValue1',
]);
$variable2 = $this->createVariable($functionId, [
+ 'variableId' => 'unique()',
'key' => 'funcKey2',
'value' => 'funcValue2',
]);
$variable3 = $this->createVariable($functionId, [
+ 'variableId' => 'unique()',
'key' => 'funcKey3',
'value' => 'funcValue3',
]);
@@ -521,6 +528,7 @@ class FunctionsCustomServerTest extends Scope
// Create a variable for later tests
$variable = $this->createVariable($functionId, [
+ 'variableId' => 'unique()',
'key' => 'GLOBAL_VARIABLE',
'value' => 'Global Variable Value',
]);
@@ -2149,6 +2157,7 @@ class FunctionsCustomServerTest extends Scope
]);
$variable = $this->createVariable($functionId, [
+ 'variableId' => 'unique()',
'key' => 'CUSTOM_VARIABLE',
'value' => 'variable'
]);
diff --git a/tests/e2e/Services/GraphQL/Base.php b/tests/e2e/Services/GraphQL/Base.php
index c42679018e..9010490cc9 100644
--- a/tests/e2e/Services/GraphQL/Base.php
+++ b/tests/e2e/Services/GraphQL/Base.php
@@ -4,6 +4,7 @@ namespace Tests\E2E\Services\GraphQL;
use CURLFile;
use Utopia\Console;
+use Utopia\Image\Image;
trait Base
{
@@ -516,6 +517,21 @@ trait Base
}
';
+ protected function assertFilePreviewResponse(array $file): void
+ {
+ $this->assertEquals(200, $file['headers']['status-code']);
+ $this->assertEquals('image/png', $file['headers']['content-type']);
+ $this->assertNotEmpty($file['body']);
+
+ $image = new Image($file['body']);
+ $dimensions = \getimagesizefromstring($file['body']);
+
+ $this->assertNotEmpty($image->output('png'));
+ $this->assertIsArray($dimensions);
+ $this->assertEquals(100, $dimensions[0]);
+ $this->assertEquals(100, $dimensions[1]);
+ }
+
public function getQuery(string $name): string
{
switch ($name) {
@@ -2388,8 +2404,8 @@ trait Base
}
}';
case self::GET_FILE_PREVIEW:
- return 'query getFilePreview($bucketId: String!, $fileId: String!) {
- storageGetFilePreview(bucketId: $bucketId, fileId: $fileId) {
+ return 'query getFilePreview($bucketId: String!, $fileId: String!, $width: Int, $height: Int) {
+ storageGetFilePreview(bucketId: $bucketId, fileId: $fileId, width: $width, height: $height) {
status
}
}';
diff --git a/tests/e2e/Services/GraphQL/FunctionsClientTest.php b/tests/e2e/Services/GraphQL/FunctionsClientTest.php
index ed436ad075..e8e033f353 100644
--- a/tests/e2e/Services/GraphQL/FunctionsClientTest.php
+++ b/tests/e2e/Services/GraphQL/FunctionsClientTest.php
@@ -55,10 +55,10 @@ class FunctionsClientTest extends Scope
$query = '
mutation createVariables($functionId: String!) {
- var1: functionsCreateVariable(functionId: $functionId, key: "name", value: "John Doe") {
+ var1: functionsCreateVariable(functionId: $functionId, variableId: "unique()", key: "name", value: "John Doe") {
_id
}
- var2: functionsCreateVariable(functionId: $functionId, key: "age", value: "42") {
+ var2: functionsCreateVariable(functionId: $functionId, variableId: "unique()", key: "age", value: "42") {
_id
}
}
diff --git a/tests/e2e/Services/GraphQL/FunctionsServerTest.php b/tests/e2e/Services/GraphQL/FunctionsServerTest.php
index 572fde49bf..95b52bcbe3 100644
--- a/tests/e2e/Services/GraphQL/FunctionsServerTest.php
+++ b/tests/e2e/Services/GraphQL/FunctionsServerTest.php
@@ -55,10 +55,10 @@ class FunctionsServerTest extends Scope
$query = '
mutation createVariables($functionId: String!) {
- var1: functionsCreateVariable(functionId: $functionId, key: "name", value: "John Doe") {
+ var1: functionsCreateVariable(functionId: $functionId, variableId: "unique()", key: "name", value: "John Doe") {
_id
}
- var2: functionsCreateVariable(functionId: $functionId, key: "age", value: "42") {
+ var2: functionsCreateVariable(functionId: $functionId, variableId: "unique()", key: "age", value: "42") {
_id
}
}
diff --git a/tests/e2e/Services/GraphQL/StorageClientTest.php b/tests/e2e/Services/GraphQL/StorageClientTest.php
index dd89819c34..9cdf523a0a 100644
--- a/tests/e2e/Services/GraphQL/StorageClientTest.php
+++ b/tests/e2e/Services/GraphQL/StorageClientTest.php
@@ -200,7 +200,7 @@ class StorageClientTest extends Scope
'x-appwrite-project' => $projectId,
], $this->getHeaders()), $gqlPayload);
- $this->assertEquals(46719, \strlen($file['body']));
+ $this->assertFilePreviewResponse($file);
return $file;
}
diff --git a/tests/e2e/Services/GraphQL/StorageServerTest.php b/tests/e2e/Services/GraphQL/StorageServerTest.php
index 1377ef9207..7808c50be6 100644
--- a/tests/e2e/Services/GraphQL/StorageServerTest.php
+++ b/tests/e2e/Services/GraphQL/StorageServerTest.php
@@ -262,7 +262,7 @@ class StorageServerTest extends Scope
'x-appwrite-project' => $projectId,
], $this->getHeaders()), $gqlPayload);
- $this->assertEquals(46719, \strlen($file['body']));
+ $this->assertFilePreviewResponse($file);
return $file;
}
diff --git a/tests/e2e/Services/Messaging/MessagingBase.php b/tests/e2e/Services/Messaging/MessagingBase.php
index d83b450739..43dc58d44c 100644
--- a/tests/e2e/Services/Messaging/MessagingBase.php
+++ b/tests/e2e/Services/Messaging/MessagingBase.php
@@ -2525,6 +2525,119 @@ trait MessagingBase
$this->assertEquals(0, \count($message['body']['deliveryErrors']));
}
+ public function testCreatePushNotificationWithUsersRecipients(): void
+ {
+ if (empty(System::getEnv('_APP_MESSAGE_PUSH_TEST_DSN'))) {
+ $this->markTestSkipped('Push DSN empty');
+ }
+
+ $dsn = new DSN(System::getEnv('_APP_MESSAGE_PUSH_TEST_DSN'));
+ $to = $dsn->getParam('to');
+ $serviceAccountJSON = $dsn->getParam('serviceAccountJSON');
+
+ if (empty($to) || empty($serviceAccountJSON)) {
+ $this->markTestSkipped('Push provider not configured');
+ }
+
+ $provider1 = $this->client->call(Client::METHOD_POST, '/messaging/providers/fcm', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), [
+ 'providerId' => ID::unique(),
+ 'name' => 'FCM-Users-1',
+ 'serviceAccountJSON' => $serviceAccountJSON,
+ 'enabled' => true,
+ ]);
+
+ $this->assertEquals(201, $provider1['headers']['status-code']);
+
+ $provider2 = $this->client->call(Client::METHOD_POST, '/messaging/providers/fcm', \array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]), [
+ 'providerId' => ID::unique(),
+ 'name' => 'FCM-Users-2',
+ 'serviceAccountJSON' => $serviceAccountJSON,
+ 'enabled' => true,
+ ]);
+
+ $this->assertEquals(201, $provider2['headers']['status-code']);
+
+ $user = $this->client->call(Client::METHOD_POST, '/users', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'userId' => ID::unique(),
+ 'email' => uniqid() . "@mail.org",
+ 'password' => 'password',
+ 'name' => 'Messaging User Recipients',
+ ]);
+
+ $this->assertEquals(201, $user['headers']['status-code']);
+
+ $target1 = $this->client->call(Client::METHOD_POST, '/users/' . $user['body']['$id'] . '/targets', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'targetId' => ID::unique(),
+ 'providerType' => 'push',
+ 'providerId' => $provider1['body']['$id'],
+ 'identifier' => $to,
+ ]);
+
+ $this->assertEquals(201, $target1['headers']['status-code']);
+
+ $target2 = $this->client->call(Client::METHOD_POST, '/users/' . $user['body']['$id'] . '/targets', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'targetId' => ID::unique(),
+ 'providerType' => 'push',
+ 'providerId' => $provider2['body']['$id'],
+ 'identifier' => $to,
+ ]);
+
+ $this->assertEquals(201, $target2['headers']['status-code']);
+
+ $push = $this->client->call(Client::METHOD_POST, '/messaging/messages/push', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'messageId' => ID::unique(),
+ 'users' => [$user['body']['$id']],
+ 'title' => 'Test-Notification-Users',
+ 'body' => 'Test-Notification-Body-Users',
+ ]);
+
+ $this->assertEquals(201, $push['headers']['status-code']);
+
+ $pushMessageId = $push['body']['$id'];
+ $this->assertEventually(function () use ($pushMessageId) {
+ $response = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $pushMessageId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+ $this->assertContains($response['body']['status'], ['sent', 'failed']);
+ }, 30000, 500);
+
+ $message = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $pushMessageId, [
+ 'origin' => 'http://localhost',
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $message['headers']['status-code']);
+ $this->assertEquals(2, $message['body']['deliveredTotal'] + \count($message['body']['deliveryErrors']));
+ }
+
public function testUpdatePushNotification(): void
{
$push = $this->setupSentPushData();
diff --git a/tests/e2e/Services/Migrations/MigrationsBase.php b/tests/e2e/Services/Migrations/MigrationsBase.php
index 4346e5a5fa..82fcee4838 100644
--- a/tests/e2e/Services/Migrations/MigrationsBase.php
+++ b/tests/e2e/Services/Migrations/MigrationsBase.php
@@ -761,6 +761,1275 @@ trait MigrationsBase
self::$cachedTableData = [];
}
+ /** Rows under all three modes; schema tolerance lets every run hit 'completed'. */
+ public function testAppwriteMigrationRowsOnDuplicate(): void
+ {
+ $sourceHeaders = [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ];
+ $destHeaders = [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDestinationProject()['$id'],
+ 'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
+ ];
+
+ $data = $this->setupMigrationTable();
+ $databaseId = $data['databaseId'];
+ $tableId = $data['tableId'];
+
+ $row = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', $sourceHeaders, [
+ 'rowId' => ID::unique(),
+ 'data' => ['name' => 'Original'],
+ ]);
+ $this->assertEquals(201, $row['headers']['status-code']);
+ $rowId = $row['body']['$id'];
+
+ $resources = [
+ Resource::TYPE_DATABASE,
+ Resource::TYPE_TABLE,
+ Resource::TYPE_COLUMN,
+ Resource::TYPE_ROW,
+ ];
+
+ // First migration: destination is empty, strict completion expected.
+ $first = $this->performMigrationSync([
+ 'resources' => $resources,
+ 'endpoint' => $this->webEndpoint,
+ 'projectId' => $this->getProject()['$id'],
+ 'apiKey' => $this->getProject()['apiKey'],
+ ]);
+ $this->assertEquals('completed', $first['status']);
+
+ // Mutate destination row to prove onDuplicate=skip preserves it.
+ $mutate = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, $destHeaders, [
+ 'data' => ['name' => 'Mutated'],
+ ]);
+ $this->assertEquals(200, $mutate['headers']['status-code']);
+ $this->assertEquals('Mutated', $mutate['body']['name']);
+
+ // Re-migration with onDuplicate=skip â completion is strict because
+ // DestinationAppwrite tolerates existing schema resources.
+ $skipResult = $this->performMigrationSync([
+ 'resources' => $resources,
+ 'endpoint' => $this->webEndpoint,
+ 'projectId' => $this->getProject()['$id'],
+ 'apiKey' => $this->getProject()['apiKey'],
+ 'onDuplicate' => 'skip',
+ ]);
+ $this->assertEquals('completed', $skipResult['status']);
+
+ $rowAfterSkip = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, $destHeaders);
+ $this->assertEquals(200, $rowAfterSkip['headers']['status-code']);
+ $this->assertEquals('Mutated', $rowAfterSkip['body']['name'], 'onDuplicate=skip must not overwrite destination row');
+
+ // Re-migration with onDuplicate=overwrite â strict completion; destination
+ // row restored to source value.
+ $overwriteResult = $this->performMigrationSync([
+ 'resources' => $resources,
+ 'endpoint' => $this->webEndpoint,
+ 'projectId' => $this->getProject()['$id'],
+ 'apiKey' => $this->getProject()['apiKey'],
+ 'onDuplicate' => 'overwrite',
+ ]);
+ $this->assertEquals('completed', $overwriteResult['status']);
+
+ $rowAfterOverwrite = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, $destHeaders);
+ $this->assertEquals(200, $rowAfterOverwrite['headers']['status-code']);
+ $this->assertEquals('Original', $rowAfterOverwrite['body']['name'], 'onDuplicate=overwrite must restore source value');
+
+ $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, $destHeaders);
+ $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, $sourceHeaders);
+
+ self::$cachedDatabaseData = [];
+ self::$cachedTableData = [];
+ }
+
+ /** Unchanged source under Skip/Overwrite is a no-op â every resource Tolerated. */
+ public function testAppwriteMigrationReRunIsIdempotent(): void
+ {
+ $sourceHeaders = [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ];
+ $destHeaders = [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDestinationProject()['$id'],
+ 'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
+ ];
+
+ $data = $this->setupMigrationTable();
+ $databaseId = $data['databaseId'];
+ $tableId = $data['tableId'];
+
+ // Seed two rows on source so the row-level tolerance is exercised too.
+ foreach (['row-a', 'row-b'] as $rowId) {
+ $row = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', $sourceHeaders, [
+ 'rowId' => $rowId,
+ 'data' => ['name' => 'Seeded ' . $rowId],
+ ]);
+ $this->assertEquals(201, $row['headers']['status-code']);
+ }
+
+ $resources = [
+ Resource::TYPE_DATABASE,
+ Resource::TYPE_TABLE,
+ Resource::TYPE_COLUMN,
+ Resource::TYPE_ROW,
+ ];
+
+ // First migration: fresh destination.
+ $first = $this->performMigrationSync([
+ 'resources' => $resources,
+ 'endpoint' => $this->webEndpoint,
+ 'projectId' => $this->getProject()['$id'],
+ 'apiKey' => $this->getProject()['apiKey'],
+ ]);
+ $this->assertEquals('completed', $first['status']);
+
+ // Re-run under Skip: nothing on source has changed. Destination
+ // schema + rows are already correct â expect clean completion.
+ $reRunSkip = $this->performMigrationSync([
+ 'resources' => $resources,
+ 'endpoint' => $this->webEndpoint,
+ 'projectId' => $this->getProject()['$id'],
+ 'apiKey' => $this->getProject()['apiKey'],
+ 'onDuplicate' => 'skip',
+ ]);
+ $this->assertEquals('completed', $reRunSkip['status']);
+
+ // Re-run under Overwrite: same unchanged source. Schema tolerance path
+ // fires for each resource; rows go through DB-native upsert.
+ $reRunOverwrite = $this->performMigrationSync([
+ 'resources' => $resources,
+ 'endpoint' => $this->webEndpoint,
+ 'projectId' => $this->getProject()['$id'],
+ 'apiKey' => $this->getProject()['apiKey'],
+ 'onDuplicate' => 'overwrite',
+ ]);
+ $this->assertEquals('completed', $reRunOverwrite['status']);
+
+ foreach (['row-a', 'row-b'] as $rowId) {
+ $check = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, $destHeaders);
+ $this->assertEquals(200, $check['headers']['status-code']);
+ $this->assertEquals('Seeded ' . $rowId, $check['body']['name']);
+ }
+
+ $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, $destHeaders);
+ $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, $sourceHeaders);
+
+ self::$cachedDatabaseData = [];
+ self::$cachedTableData = [];
+ }
+
+ /** Overwrite reconciles container drift via UpdateInPlace; children (rows) preserved. */
+ public function testAppwriteMigrationOverwriteUpdatesContainerMetadata(): void
+ {
+ $sourceHeaders = [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ];
+ $destHeaders = [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDestinationProject()['$id'],
+ 'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
+ ];
+
+ $data = $this->setupMigrationTable();
+ $databaseId = $data['databaseId'];
+ $tableId = $data['tableId'];
+ $rowId = 'persist-me';
+
+ $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', $sourceHeaders, [
+ 'rowId' => $rowId,
+ 'data' => ['name' => 'SeedRow'],
+ ]);
+
+ $resources = [
+ Resource::TYPE_DATABASE,
+ Resource::TYPE_TABLE,
+ Resource::TYPE_COLUMN,
+ Resource::TYPE_ROW,
+ ];
+
+ // First migration â dest empty, strict completion.
+ $first = $this->performMigrationSync([
+ 'resources' => $resources,
+ 'endpoint' => $this->webEndpoint,
+ 'projectId' => $this->getProject()['$id'],
+ 'apiKey' => $this->getProject()['apiKey'],
+ ]);
+ $this->assertEquals('completed', $first['status']);
+
+ // `_updatedAt` is stored at second granularity (strtotime) â ensure
+ // the source edits below produce a strictly-newer timestamp than
+ // dest's first-migration timestamp.
+ sleep(1);
+
+ // Mutate source: rename database + toggle table enabled.
+ $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId, $sourceHeaders, [
+ 'name' => 'Renamed Source DB',
+ ]);
+ $this->client->call(Client::METHOD_PUT, '/tablesdb/' . $databaseId . '/tables/' . $tableId, $sourceHeaders, [
+ 'name' => 'Renamed Source Table',
+ 'permissions' => [Permission::read(Role::any())],
+ 'rowSecurity' => true,
+ 'enabled' => false,
+ ]);
+
+ // Overwrite re-migration: UpdateInPlace path fires for database + table.
+ $overwriteResult = $this->performMigrationSync([
+ 'resources' => $resources,
+ 'endpoint' => $this->webEndpoint,
+ 'projectId' => $this->getProject()['$id'],
+ 'apiKey' => $this->getProject()['apiKey'],
+ 'onDuplicate' => 'overwrite',
+ ]);
+ $this->assertEquals('completed', $overwriteResult['status']);
+
+ // Assert dest database metadata reflects source's new values.
+ $destDb = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId, $destHeaders);
+ $this->assertEquals(200, $destDb['headers']['status-code']);
+ $this->assertEquals('Renamed Source DB', $destDb['body']['name']);
+
+ // Assert dest table metadata reflects source's new values.
+ $destTable = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId, $destHeaders);
+ $this->assertEquals(200, $destTable['headers']['status-code']);
+ $this->assertEquals('Renamed Source Table', $destTable['body']['name']);
+ $this->assertFalse($destTable['body']['enabled'], 'Overwrite must propagate source enabled=false');
+ $this->assertTrue($destTable['body']['documentSecurity'] ?? $destTable['body']['rowSecurity'], 'Overwrite must propagate source rowSecurity=true');
+
+ // Child row untouched â UpdateInPlace only rewrites container metadata.
+ $row = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, $destHeaders);
+ $this->assertEquals(200, $row['headers']['status-code']);
+ $this->assertEquals('SeedRow', $row['body']['name'], 'Overwrite must not touch child rows when updating container metadata');
+
+ $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, $destHeaders);
+ $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, $sourceHeaders);
+
+ self::$cachedDatabaseData = [];
+ self::$cachedTableData = [];
+ }
+
+ /** Skip preserves dest container drift even when source has diverged. */
+ public function testAppwriteMigrationSkipPreservesContainerDrift(): void
+ {
+ $sourceHeaders = [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ];
+ $destHeaders = [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDestinationProject()['$id'],
+ 'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
+ ];
+
+ $data = $this->setupMigrationTable();
+ $databaseId = $data['databaseId'];
+ $tableId = $data['tableId'];
+
+ $resources = [
+ Resource::TYPE_DATABASE,
+ Resource::TYPE_TABLE,
+ Resource::TYPE_COLUMN,
+ ];
+
+ // First migration: dest gets whatever source had.
+ $first = $this->performMigrationSync([
+ 'resources' => $resources,
+ 'endpoint' => $this->webEndpoint,
+ 'projectId' => $this->getProject()['$id'],
+ 'apiKey' => $this->getProject()['apiKey'],
+ ]);
+ $this->assertEquals('completed', $first['status']);
+
+ sleep(1);
+
+ // Mutate dest: ops tightens permissions and renames the table for
+ // its production-specific branding.
+ $this->client->call(Client::METHOD_PUT, '/tablesdb/' . $databaseId . '/tables/' . $tableId, $destHeaders, [
+ 'name' => 'Dest-Managed Table',
+ 'permissions' => [Permission::read(Role::users())],
+ 'rowSecurity' => false,
+ 'enabled' => true,
+ ]);
+
+ // Also mutate source so the second run has a real divergence.
+ $this->client->call(Client::METHOD_PUT, '/tablesdb/' . $databaseId . '/tables/' . $tableId, $sourceHeaders, [
+ 'name' => 'Source Renamed',
+ 'permissions' => [Permission::read(Role::any())],
+ 'rowSecurity' => true,
+ 'enabled' => false,
+ ]);
+
+ // Skip re-migration: must tolerate existing destination â no update.
+ $skipResult = $this->performMigrationSync([
+ 'resources' => $resources,
+ 'endpoint' => $this->webEndpoint,
+ 'projectId' => $this->getProject()['$id'],
+ 'apiKey' => $this->getProject()['apiKey'],
+ 'onDuplicate' => 'skip',
+ ]);
+ $this->assertEquals('completed', $skipResult['status']);
+
+ // Dest kept its tightened values.
+ $destTable = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId, $destHeaders);
+ $this->assertEquals(200, $destTable['headers']['status-code']);
+ $this->assertEquals('Dest-Managed Table', $destTable['body']['name'], 'Skip must not propagate source name over dest drift');
+ $this->assertTrue($destTable['body']['enabled'], 'Skip must preserve dest enabled flag');
+
+ $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, $destHeaders);
+ $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, $sourceHeaders);
+
+ self::$cachedDatabaseData = [];
+ self::$cachedTableData = [];
+ }
+
+ /** Overwrite drops dest columns source no longer declares; cleanup runs before rows land. */
+ public function testAppwriteMigrationOverwriteDropsOrphanColumn(): void
+ {
+ $sourceHeaders = [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ];
+ $destHeaders = [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDestinationProject()['$id'],
+ 'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
+ ];
+
+ $data = $this->setupMigrationTable();
+ $databaseId = $data['databaseId'];
+ $tableId = $data['tableId'];
+
+ $resources = [
+ Resource::TYPE_DATABASE,
+ Resource::TYPE_TABLE,
+ Resource::TYPE_COLUMN,
+ Resource::TYPE_ROW,
+ ];
+
+ // First migration: dest mirrors source (one column 'name').
+ $first = $this->performMigrationSync([
+ 'resources' => $resources,
+ 'endpoint' => $this->webEndpoint,
+ 'projectId' => $this->getProject()['$id'],
+ 'apiKey' => $this->getProject()['apiKey'],
+ ]);
+ $this->assertEquals('completed', $first['status']);
+
+ // Add an orphan column directly on destination (not on source).
+ // Simulates the post-rename state: source dropped a column, dest
+ // still has it â or a dest-only column added by a separate app.
+ $orphanResp = $this->client->call(
+ Client::METHOD_POST,
+ '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/string',
+ $destHeaders,
+ [
+ 'key' => 'orphan_col',
+ 'size' => 50,
+ 'required' => false,
+ ]
+ );
+ $this->assertEquals(202, $orphanResp['headers']['status-code']);
+
+ $this->assertEventually(function () use ($databaseId, $tableId, $destHeaders) {
+ $r = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/orphan_col', $destHeaders);
+ $this->assertEquals(200, $r['headers']['status-code']);
+ $this->assertEquals('available', $r['body']['status']);
+ }, 5000, 500);
+
+ // Seed a row on source so per-table orphan cleanup fires inside
+ // createRecord (before rows land), not just at end of run.
+ $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', $sourceHeaders, [
+ 'rowId' => ID::unique(),
+ 'data' => ['name' => 'seed'],
+ ]);
+
+ // Overwrite re-migration: orphan_col must be dropped from dest.
+ $overwriteResult = $this->performMigrationSync([
+ 'resources' => $resources,
+ 'endpoint' => $this->webEndpoint,
+ 'projectId' => $this->getProject()['$id'],
+ 'apiKey' => $this->getProject()['apiKey'],
+ 'onDuplicate' => 'overwrite',
+ ]);
+ $this->assertEquals('completed', $overwriteResult['status']);
+
+ // Orphan column dropped.
+ $orphanCheck = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/orphan_col', $destHeaders);
+ $this->assertEquals(404, $orphanCheck['headers']['status-code'], 'Overwrite must drop destination column source no longer declares');
+
+ // Source's column preserved.
+ $nameCheck = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/name', $destHeaders);
+ $this->assertEquals(200, $nameCheck['headers']['status-code'], 'Overwrite must preserve columns source declared');
+
+ $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, $destHeaders);
+ $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, $sourceHeaders);
+
+ self::$cachedDatabaseData = [];
+ self::$cachedTableData = [];
+ }
+
+ /** Skip preserves orphan columns; cleanup is Overwrite-only. */
+ public function testAppwriteMigrationSkipKeepsOrphanColumn(): void
+ {
+ $sourceHeaders = [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ];
+ $destHeaders = [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDestinationProject()['$id'],
+ 'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
+ ];
+
+ $data = $this->setupMigrationTable();
+ $databaseId = $data['databaseId'];
+ $tableId = $data['tableId'];
+
+ $resources = [
+ Resource::TYPE_DATABASE,
+ Resource::TYPE_TABLE,
+ Resource::TYPE_COLUMN,
+ Resource::TYPE_ROW,
+ ];
+
+ $first = $this->performMigrationSync([
+ 'resources' => $resources,
+ 'endpoint' => $this->webEndpoint,
+ 'projectId' => $this->getProject()['$id'],
+ 'apiKey' => $this->getProject()['apiKey'],
+ ]);
+ $this->assertEquals('completed', $first['status']);
+
+ $orphanResp = $this->client->call(
+ Client::METHOD_POST,
+ '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/string',
+ $destHeaders,
+ [
+ 'key' => 'dest_only_col',
+ 'size' => 50,
+ 'required' => false,
+ ]
+ );
+ $this->assertEquals(202, $orphanResp['headers']['status-code']);
+
+ $this->assertEventually(function () use ($databaseId, $tableId, $destHeaders) {
+ $r = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/dest_only_col', $destHeaders);
+ $this->assertEquals(200, $r['headers']['status-code']);
+ $this->assertEquals('available', $r['body']['status']);
+ }, 5000, 500);
+
+ $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', $sourceHeaders, [
+ 'rowId' => ID::unique(),
+ 'data' => ['name' => 'seed'],
+ ]);
+
+ // Skip re-migration: orphan column must NOT be dropped.
+ $skipResult = $this->performMigrationSync([
+ 'resources' => $resources,
+ 'endpoint' => $this->webEndpoint,
+ 'projectId' => $this->getProject()['$id'],
+ 'apiKey' => $this->getProject()['apiKey'],
+ 'onDuplicate' => 'skip',
+ ]);
+ $this->assertEquals('completed', $skipResult['status']);
+
+ $orphanCheck = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/dest_only_col', $destHeaders);
+ $this->assertEquals(200, $orphanCheck['headers']['status-code'], 'Skip must preserve destination columns, including orphans');
+
+ $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, $destHeaders);
+ $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, $sourceHeaders);
+
+ self::$cachedDatabaseData = [];
+ self::$cachedTableData = [];
+ }
+
+ /** SDK-reachable attribute change propagates via updateAttributeInPlace; row data preserved. */
+ public function testAppwriteMigrationOverwriteUpdatesAttributeInPlace(): void
+ {
+ $sourceHeaders = [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ];
+ $destHeaders = [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDestinationProject()['$id'],
+ 'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
+ ];
+
+ $data = $this->setupMigrationTable();
+ $databaseId = $data['databaseId'];
+ $tableId = $data['tableId'];
+ $rowId = 'persist-on-inplace';
+
+ // Seed a row that proves drop+recreate didn't happen â recreate would
+ // have wiped this column's data on the destination.
+ $row = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', $sourceHeaders, [
+ 'rowId' => $rowId,
+ 'data' => ['name' => 'SeedRow'],
+ ]);
+ $this->assertEquals(201, $row['headers']['status-code']);
+
+ $resources = [
+ Resource::TYPE_DATABASE,
+ Resource::TYPE_TABLE,
+ Resource::TYPE_COLUMN,
+ Resource::TYPE_ROW,
+ ];
+
+ // First migration â dest gets the column as required:true.
+ $first = $this->performMigrationSync([
+ 'resources' => $resources,
+ 'endpoint' => $this->webEndpoint,
+ 'projectId' => $this->getProject()['$id'],
+ 'apiKey' => $this->getProject()['apiKey'],
+ ]);
+ $this->assertEquals('completed', $first['status']);
+
+ $beforeUpdate = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/name', $destHeaders);
+ $this->assertEquals(200, $beforeUpdate['headers']['status-code']);
+ $this->assertTrue($beforeUpdate['body']['required']);
+
+ // _updatedAt has second granularity; ensure source's PATCH produces a
+ // strictly-newer timestamp than the dest's first-migration value.
+ sleep(1);
+
+ // SDK-reachable change set: required trueâfalse, default nullâ'unknown'.
+ // Both fields are supported by PATCH /columns/string/:key â must route
+ // through updateAttributeInPlace, not DropAndRecreate.
+ $patch = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/string/name', $sourceHeaders, [
+ 'required' => false,
+ 'default' => 'unknown',
+ ]);
+ $this->assertEquals(200, $patch['headers']['status-code']);
+
+ $this->assertEventually(function () use ($databaseId, $tableId, $sourceHeaders) {
+ $r = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/name', $sourceHeaders);
+ $this->assertEquals(200, $r['headers']['status-code']);
+ $this->assertEquals('available', $r['body']['status']);
+ $this->assertFalse($r['body']['required']);
+ $this->assertEquals('unknown', $r['body']['default']);
+ }, 5000, 500);
+
+ $overwriteResult = $this->performMigrationSync([
+ 'resources' => $resources,
+ 'endpoint' => $this->webEndpoint,
+ 'projectId' => $this->getProject()['$id'],
+ 'apiKey' => $this->getProject()['apiKey'],
+ 'onDuplicate' => 'overwrite',
+ ]);
+ $this->assertEquals('completed', $overwriteResult['status']);
+
+ $this->assertEventually(function () use ($databaseId, $tableId, $destHeaders) {
+ $r = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/name', $destHeaders);
+ $this->assertEquals(200, $r['headers']['status-code']);
+ $this->assertEquals('available', $r['body']['status']);
+ $this->assertFalse($r['body']['required'], 'updateAttributeInPlace must propagate source required=false');
+ $this->assertEquals('unknown', $r['body']['default'], 'updateAttributeInPlace must propagate source default');
+ }, 10000, 500);
+
+ // Pre-existing row preserved â proof that the path was UpdateInPlace
+ // and not DropAndRecreate (which would have nulled this column).
+ $rowAfter = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, $destHeaders);
+ $this->assertEquals(200, $rowAfter['headers']['status-code']);
+ $this->assertEquals('SeedRow', $rowAfter['body']['name'], 'updateAttributeInPlace must not touch row data');
+
+ $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, $destHeaders);
+ $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, $sourceHeaders);
+
+ self::$cachedDatabaseData = [];
+ self::$cachedTableData = [];
+ }
+
+ /** Skip preserves dest attribute drift; leaf-level analog of the container drift test. */
+ public function testAppwriteMigrationSkipPreservesAttributeDrift(): void
+ {
+ $sourceHeaders = [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ];
+ $destHeaders = [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDestinationProject()['$id'],
+ 'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
+ ];
+
+ $data = $this->setupMigrationTable();
+ $databaseId = $data['databaseId'];
+ $tableId = $data['tableId'];
+
+ $resources = [
+ Resource::TYPE_DATABASE,
+ Resource::TYPE_TABLE,
+ Resource::TYPE_COLUMN,
+ ];
+
+ $first = $this->performMigrationSync([
+ 'resources' => $resources,
+ 'endpoint' => $this->webEndpoint,
+ 'projectId' => $this->getProject()['$id'],
+ 'apiKey' => $this->getProject()['apiKey'],
+ ]);
+ $this->assertEquals('completed', $first['status']);
+
+ sleep(1);
+
+ // Dest divergence: ops loosens the column for a production-only need.
+ $destPatch = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/string/name', $destHeaders, [
+ 'required' => false,
+ 'default' => 'dest-default',
+ ]);
+ $this->assertEquals(200, $destPatch['headers']['status-code']);
+
+ $this->assertEventually(function () use ($databaseId, $tableId, $destHeaders) {
+ $r = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/name', $destHeaders);
+ $this->assertEquals(200, $r['headers']['status-code']);
+ $this->assertEquals('available', $r['body']['status']);
+ $this->assertFalse($r['body']['required']);
+ }, 5000, 500);
+
+ sleep(1);
+
+ // Source advances strictly later (and to a different value). Under
+ // Overwrite this would propagate to dest; under Skip it must not.
+ $sourcePatch = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/string/name', $sourceHeaders, [
+ 'required' => true,
+ 'default' => null,
+ ]);
+ $this->assertEquals(200, $sourcePatch['headers']['status-code']);
+
+ $this->assertEventually(function () use ($databaseId, $tableId, $sourceHeaders) {
+ $r = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/name', $sourceHeaders);
+ $this->assertEquals(200, $r['headers']['status-code']);
+ $this->assertEquals('available', $r['body']['status']);
+ $this->assertTrue($r['body']['required']);
+ }, 5000, 500);
+
+ $skipResult = $this->performMigrationSync([
+ 'resources' => $resources,
+ 'endpoint' => $this->webEndpoint,
+ 'projectId' => $this->getProject()['$id'],
+ 'apiKey' => $this->getProject()['apiKey'],
+ 'onDuplicate' => 'skip',
+ ]);
+ $this->assertEquals('completed', $skipResult['status']);
+
+ $destAttr = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/name', $destHeaders);
+ $this->assertEquals(200, $destAttr['headers']['status-code']);
+ $this->assertFalse($destAttr['body']['required'], 'Skip must not propagate source required over dest drift');
+ $this->assertEquals('dest-default', $destAttr['body']['default'], 'Skip must preserve dest default');
+
+ $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, $destHeaders);
+ $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, $sourceHeaders);
+
+ self::$cachedDatabaseData = [];
+ self::$cachedTableData = [];
+ }
+
+ /** Two-way onDelete change updates in place on both sides; partner meta refreshed by hand. */
+ public function testAppwriteMigrationOverwriteUpdatesRelationshipOnDeleteInPlace(): void
+ {
+ $sourceHeaders = [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ];
+ $destHeaders = [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDestinationProject()['$id'],
+ 'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
+ ];
+
+ $databaseId = ID::unique();
+ $createDb = $this->client->call(Client::METHOD_POST, '/databases', $sourceHeaders, [
+ 'databaseId' => $databaseId,
+ 'name' => 'Rel In-Place DB',
+ ]);
+ $this->assertEquals(201, $createDb['headers']['status-code']);
+
+ foreach (['parents', 'children'] as $tbl) {
+ $createTable = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', $sourceHeaders, [
+ 'tableId' => $tbl,
+ 'name' => $tbl,
+ ]);
+ $this->assertEquals(201, $createTable['headers']['status-code']);
+ }
+
+ // Two-way: parents.kids â children.parent. Required to hit the in-place path.
+ $createRel = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/parents/columns/relationship', $sourceHeaders, [
+ 'relatedTableId' => 'children',
+ 'type' => Database::RELATION_ONE_TO_MANY,
+ 'twoWay' => true,
+ 'key' => 'kids',
+ 'twoWayKey' => 'parent',
+ 'onDelete' => Database::RELATION_MUTATE_CASCADE,
+ ]);
+ $this->assertEquals(202, $createRel['headers']['status-code']);
+
+ $this->assertEventually(function () use ($databaseId, $sourceHeaders) {
+ $r = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/parents/columns/kids', $sourceHeaders);
+ $this->assertEquals(200, $r['headers']['status-code']);
+ $this->assertEquals('available', $r['body']['status']);
+ $this->assertEquals(Database::RELATION_MUTATE_CASCADE, $r['body']['onDelete']);
+ }, 10000, 500);
+
+ $resources = [
+ Resource::TYPE_DATABASE,
+ Resource::TYPE_TABLE,
+ Resource::TYPE_COLUMN,
+ ];
+
+ $first = $this->performMigrationSync([
+ 'resources' => $resources,
+ 'endpoint' => $this->webEndpoint,
+ 'projectId' => $this->getProject()['$id'],
+ 'apiKey' => $this->getProject()['apiKey'],
+ ]);
+ $this->assertEquals('completed', $first['status']);
+
+ // Both sides land on dest with onDelete=cascade.
+ $this->assertEventually(function () use ($databaseId, $destHeaders) {
+ $parent = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/parents/columns/kids', $destHeaders);
+ $this->assertEquals(200, $parent['headers']['status-code']);
+ $this->assertEquals('available', $parent['body']['status']);
+ $this->assertEquals(Database::RELATION_MUTATE_CASCADE, $parent['body']['onDelete']);
+
+ $child = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/children/columns/parent', $destHeaders);
+ $this->assertEquals(200, $child['headers']['status-code']);
+ $this->assertEquals('available', $child['body']['status']);
+ $this->assertEquals(Database::RELATION_MUTATE_CASCADE, $child['body']['onDelete']);
+ }, 10000, 500);
+
+ sleep(1);
+
+ // SDK-reachable: PATCH /columns/:key/relationship accepts onDelete.
+ $patch = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/parents/columns/kids/relationship', $sourceHeaders, [
+ 'onDelete' => Database::RELATION_MUTATE_RESTRICT,
+ ]);
+ $this->assertEquals(200, $patch['headers']['status-code']);
+
+ $this->assertEventually(function () use ($databaseId, $sourceHeaders) {
+ $r = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/parents/columns/kids', $sourceHeaders);
+ $this->assertEquals(200, $r['headers']['status-code']);
+ $this->assertEquals('available', $r['body']['status']);
+ $this->assertEquals(Database::RELATION_MUTATE_RESTRICT, $r['body']['onDelete']);
+ }, 5000, 500);
+
+ $overwriteResult = $this->performMigrationSync([
+ 'resources' => $resources,
+ 'endpoint' => $this->webEndpoint,
+ 'projectId' => $this->getProject()['$id'],
+ 'apiKey' => $this->getProject()['apiKey'],
+ 'onDuplicate' => 'overwrite',
+ ]);
+ $this->assertEquals('completed', $overwriteResult['status']);
+
+ // Both sides on dest must reflect onDelete=restrict. Asserting the
+ // partner side is the regression guard for the previously-missed
+ // partner meta refresh in updateRelationshipInPlace.
+ $this->assertEventually(function () use ($databaseId, $destHeaders) {
+ $parent = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/parents/columns/kids', $destHeaders);
+ $this->assertEquals(200, $parent['headers']['status-code']);
+ $this->assertEquals('available', $parent['body']['status']);
+ $this->assertEquals(Database::RELATION_MUTATE_RESTRICT, $parent['body']['onDelete'], 'parent-side onDelete must reflect source');
+ $this->assertEquals(Database::RELATION_ONE_TO_MANY, $parent['body']['relationType'], 'In-place update must not change relationType');
+ $this->assertTrue($parent['body']['twoWay'], 'In-place update must not change twoWay');
+
+ $child = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/children/columns/parent', $destHeaders);
+ $this->assertEquals(200, $child['headers']['status-code']);
+ $this->assertEquals('available', $child['body']['status']);
+ $this->assertEquals(Database::RELATION_MUTATE_RESTRICT, $child['body']['onDelete'], 'partner-side onDelete must reflect source after in-place update');
+ }, 10000, 500);
+
+ $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, $destHeaders);
+ $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, $sourceHeaders);
+
+ self::$cachedDatabaseData = [];
+ self::$cachedTableData = [];
+ }
+
+ /** Two-way recreate with same spec: spec-match guard tolerates parent; pair-key dedup tolerates partner. Both sides + child rows preserved. */
+ public function testAppwriteMigrationOverwriteTwoWayRecreateSkipsPartnerSide(): void
+ {
+ $sourceHeaders = [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ];
+ $destHeaders = [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDestinationProject()['$id'],
+ 'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
+ ];
+
+ $databaseId = ID::unique();
+ $createDb = $this->client->call(Client::METHOD_POST, '/databases', $sourceHeaders, [
+ 'databaseId' => $databaseId,
+ 'name' => 'Two-Way Recreate DB',
+ ]);
+ $this->assertEquals(201, $createDb['headers']['status-code']);
+
+ foreach (['parents', 'children'] as $tbl) {
+ $createTable = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', $sourceHeaders, [
+ 'tableId' => $tbl,
+ 'name' => $tbl,
+ 'permissions' => [
+ Permission::create(Role::any()),
+ Permission::read(Role::any()),
+ Permission::update(Role::any()),
+ Permission::delete(Role::any()),
+ ],
+ ]);
+ $this->assertEquals(201, $createTable['headers']['status-code']);
+ }
+
+ // Add a non-relationship column on parents so we can POST a row with
+ // non-empty data. tablesdb POST /rows rejects empty data arrays in
+ // 1.9.x (Create.php:161 â getSupportForEmptyDocument() defaults false).
+ $createLabel = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/parents/columns/string', $sourceHeaders, [
+ 'key' => 'label',
+ 'size' => 32,
+ 'required' => false,
+ ]);
+ $this->assertEquals(202, $createLabel['headers']['status-code']);
+
+ $this->assertEventually(function () use ($databaseId, $sourceHeaders) {
+ $r = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/parents/columns/label', $sourceHeaders);
+ $this->assertEquals(200, $r['headers']['status-code']);
+ $this->assertEquals('available', $r['body']['status']);
+ }, 10000, 500);
+
+ $createRel = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/parents/columns/relationship', $sourceHeaders, [
+ 'relatedTableId' => 'children',
+ 'type' => Database::RELATION_ONE_TO_MANY,
+ 'twoWay' => true,
+ 'key' => 'kids',
+ 'twoWayKey' => 'parent',
+ 'onDelete' => Database::RELATION_MUTATE_CASCADE,
+ ]);
+ $this->assertEquals(202, $createRel['headers']['status-code']);
+
+ $this->assertEventually(function () use ($databaseId, $sourceHeaders) {
+ $r = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/parents/columns/kids', $sourceHeaders);
+ $this->assertEquals(200, $r['headers']['status-code']);
+ $this->assertEquals('available', $r['body']['status']);
+ }, 10000, 500);
+
+ $parentRow = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/parents/rows', $sourceHeaders, [
+ 'rowId' => 'parent-1',
+ 'data' => ['label' => 'p1'],
+ ]);
+ $this->assertEquals(201, $parentRow['headers']['status-code']);
+ $childRow = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/children/rows', $sourceHeaders, [
+ 'rowId' => 'child-1',
+ 'data' => ['parent' => 'parent-1'],
+ ]);
+ $this->assertEquals(201, $childRow['headers']['status-code']);
+
+ $resources = [
+ Resource::TYPE_DATABASE,
+ Resource::TYPE_TABLE,
+ Resource::TYPE_COLUMN,
+ Resource::TYPE_ROW,
+ ];
+
+ $first = $this->performMigrationSync([
+ 'resources' => $resources,
+ 'endpoint' => $this->webEndpoint,
+ 'projectId' => $this->getProject()['$id'],
+ 'apiKey' => $this->getProject()['apiKey'],
+ ]);
+ $this->assertEquals('completed', $first['status']);
+
+ // Recreate the relationship on source so its createdAt advances past
+ // dest's stored value â forces SchemaAction::DropAndRecreate on the
+ // parent side, which is the path the partner-side dedup guards.
+ sleep(1);
+ $deleteRel = $this->client->call(Client::METHOD_DELETE, '/tablesdb/' . $databaseId . '/tables/parents/columns/kids', $sourceHeaders);
+ $this->assertEquals(204, $deleteRel['headers']['status-code']);
+
+ $this->assertEventually(function () use ($databaseId, $sourceHeaders) {
+ $r = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/parents/columns/kids', $sourceHeaders);
+ $this->assertEquals(404, $r['headers']['status-code']);
+ }, 10000, 500);
+
+ sleep(1);
+ $recreate = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/parents/columns/relationship', $sourceHeaders, [
+ 'relatedTableId' => 'children',
+ 'type' => Database::RELATION_ONE_TO_MANY,
+ 'twoWay' => true,
+ 'key' => 'kids',
+ 'twoWayKey' => 'parent',
+ 'onDelete' => Database::RELATION_MUTATE_CASCADE,
+ ]);
+ $this->assertEquals(202, $recreate['headers']['status-code']);
+
+ $this->assertEventually(function () use ($databaseId, $sourceHeaders) {
+ $r = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/parents/columns/kids', $sourceHeaders);
+ $this->assertEquals(200, $r['headers']['status-code']);
+ $this->assertEquals('available', $r['body']['status']);
+ }, 10000, 500);
+
+ // Child-row's relationship was wiped by the source-side delete. Re-link.
+ $relink = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/children/rows/child-1', $sourceHeaders, [
+ 'data' => ['parent' => 'parent-1'],
+ ]);
+ $this->assertEquals(200, $relink['headers']['status-code']);
+
+ $overwriteResult = $this->performMigrationSync([
+ 'resources' => $resources,
+ 'endpoint' => $this->webEndpoint,
+ 'projectId' => $this->getProject()['$id'],
+ 'apiKey' => $this->getProject()['apiKey'],
+ 'onDuplicate' => 'overwrite',
+ ]);
+ $this->assertEquals('completed', $overwriteResult['status']);
+
+ $this->assertEventually(function () use ($databaseId, $destHeaders) {
+ $parent = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/parents/columns/kids', $destHeaders);
+ $this->assertEquals(200, $parent['headers']['status-code']);
+ $this->assertEquals('available', $parent['body']['status']);
+
+ $child = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/children/columns/parent', $destHeaders);
+ $this->assertEquals(200, $child['headers']['status-code']);
+ $this->assertEquals('available', $child['body']['status']);
+ }, 10000, 500);
+
+ // Both rows survive the re-migration. If the partner-side dedup were
+ // missing and the partner pass re-fired DropAndRecreate, the partner
+ // (children) table's row would have been wiped before the row pass.
+ $destChild = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/children/rows/child-1', $destHeaders);
+ $this->assertEquals(200, $destChild['headers']['status-code'], 'partner-table row must survive two-way recreate re-migration');
+ $this->assertEquals('parent-1', $destChild['body']['parent']['$id'] ?? $destChild['body']['parent'], 'partner-table row relationship must point to the migrated parent');
+
+ $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, $destHeaders);
+ $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, $sourceHeaders);
+
+ self::$cachedDatabaseData = [];
+ self::$cachedTableData = [];
+ }
+
+ /** One-way + onDelete change falls through to DropAndRecreate (in-place gated off for one-way). */
+ public function testAppwriteMigrationOverwriteOneWayRelationshipDropAndRecreate(): void
+ {
+ $sourceHeaders = [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ];
+ $destHeaders = [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDestinationProject()['$id'],
+ 'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
+ ];
+
+ $databaseId = ID::unique();
+ $createDb = $this->client->call(Client::METHOD_POST, '/databases', $sourceHeaders, [
+ 'databaseId' => $databaseId,
+ 'name' => 'One-Way DropAndRecreate DB',
+ ]);
+ $this->assertEquals(201, $createDb['headers']['status-code']);
+
+ foreach (['parents', 'children'] as $tbl) {
+ $createTable = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', $sourceHeaders, [
+ 'tableId' => $tbl,
+ 'name' => $tbl,
+ ]);
+ $this->assertEquals(201, $createTable['headers']['status-code']);
+ }
+
+ $createRel = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/parents/columns/relationship', $sourceHeaders, [
+ 'relatedTableId' => 'children',
+ 'type' => Database::RELATION_ONE_TO_MANY,
+ 'twoWay' => false,
+ 'key' => 'kids',
+ 'onDelete' => Database::RELATION_MUTATE_CASCADE,
+ ]);
+ $this->assertEquals(202, $createRel['headers']['status-code']);
+
+ $this->assertEventually(function () use ($databaseId, $sourceHeaders) {
+ $r = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/parents/columns/kids', $sourceHeaders);
+ $this->assertEquals(200, $r['headers']['status-code']);
+ $this->assertEquals('available', $r['body']['status']);
+ }, 10000, 500);
+
+ $resources = [
+ Resource::TYPE_DATABASE,
+ Resource::TYPE_TABLE,
+ Resource::TYPE_COLUMN,
+ ];
+
+ $first = $this->performMigrationSync([
+ 'resources' => $resources,
+ 'endpoint' => $this->webEndpoint,
+ 'projectId' => $this->getProject()['$id'],
+ 'apiKey' => $this->getProject()['apiKey'],
+ ]);
+ $this->assertEquals('completed', $first['status']);
+
+ $this->assertEventually(function () use ($databaseId, $destHeaders) {
+ $r = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/parents/columns/kids', $destHeaders);
+ $this->assertEquals(200, $r['headers']['status-code']);
+ $this->assertEquals('available', $r['body']['status']);
+ $this->assertEquals(Database::RELATION_MUTATE_CASCADE, $r['body']['onDelete']);
+ }, 10000, 500);
+
+ sleep(1);
+
+ $patch = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/parents/columns/kids/relationship', $sourceHeaders, [
+ 'onDelete' => Database::RELATION_MUTATE_RESTRICT,
+ ]);
+ $this->assertEquals(200, $patch['headers']['status-code']);
+
+ $this->assertEventually(function () use ($databaseId, $sourceHeaders) {
+ $r = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/parents/columns/kids', $sourceHeaders);
+ $this->assertEquals('available', $r['body']['status']);
+ $this->assertEquals(Database::RELATION_MUTATE_RESTRICT, $r['body']['onDelete']);
+ }, 5000, 500);
+
+ $overwriteResult = $this->performMigrationSync([
+ 'resources' => $resources,
+ 'endpoint' => $this->webEndpoint,
+ 'projectId' => $this->getProject()['$id'],
+ 'apiKey' => $this->getProject()['apiKey'],
+ 'onDuplicate' => 'overwrite',
+ ]);
+ $this->assertEquals('completed', $overwriteResult['status']);
+
+ $this->assertEventually(function () use ($databaseId, $destHeaders) {
+ $r = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/parents/columns/kids', $destHeaders);
+ $this->assertEquals(200, $r['headers']['status-code']);
+ $this->assertEquals('available', $r['body']['status']);
+ $this->assertEquals(Database::RELATION_MUTATE_RESTRICT, $r['body']['onDelete'], 'one-way DropAndRecreate must propagate source onDelete');
+ $this->assertEquals(Database::RELATION_ONE_TO_MANY, $r['body']['relationType'], 'DropAndRecreate must preserve relationType');
+ $this->assertFalse($r['body']['twoWay'], 'DropAndRecreate must preserve twoWay=false');
+ }, 10000, 500);
+
+ $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, $destHeaders);
+ $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, $sourceHeaders);
+
+ self::$cachedDatabaseData = [];
+ self::$cachedTableData = [];
+ }
+
+ /** Recreate with non-SDK spec change (array toggle): updateAttributeInPlace bails â drop+recreate; row pass refills. */
+ public function testAppwriteMigrationOverwriteAttributeRecreateDropsAndRecreates(): void
+ {
+ $sourceHeaders = [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ];
+ $destHeaders = [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDestinationProject()['$id'],
+ 'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
+ ];
+
+ $data = $this->setupMigrationTable();
+ $databaseId = $data['databaseId'];
+ $tableId = $data['tableId'];
+ $rowId = 'row-after-recreate';
+
+ $row = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', $sourceHeaders, [
+ 'rowId' => $rowId,
+ 'data' => ['name' => 'before-recreate'],
+ ]);
+ $this->assertEquals(201, $row['headers']['status-code']);
+
+ $resources = [
+ Resource::TYPE_DATABASE,
+ Resource::TYPE_TABLE,
+ Resource::TYPE_COLUMN,
+ Resource::TYPE_ROW,
+ ];
+
+ $first = $this->performMigrationSync([
+ 'resources' => $resources,
+ 'endpoint' => $this->webEndpoint,
+ 'projectId' => $this->getProject()['$id'],
+ 'apiKey' => $this->getProject()['apiKey'],
+ ]);
+ $this->assertEquals('completed', $first['status']);
+
+ sleep(1);
+
+ // Drop + recreate the column on source. createdAt advances â re-migration
+ // must take the createdAt-diff DropAndRecreate path on dest.
+ $delete = $this->client->call(Client::METHOD_DELETE, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/name', $sourceHeaders);
+ $this->assertEquals(204, $delete['headers']['status-code']);
+
+ $this->assertEventually(function () use ($databaseId, $tableId, $sourceHeaders) {
+ $r = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/name', $sourceHeaders);
+ $this->assertEquals(404, $r['headers']['status-code']);
+ }, 10000, 500);
+
+ // Recreate with `array: true` â a non-SDK change (`array` is in
+ // ATTRIBUTE_NON_SDK_FIELDS). Forces updateAttributeInPlace to bail
+ // and the caller to fall through to drop+recreate, which is what
+ // this test pins.
+ $recreate = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/string', $sourceHeaders, [
+ 'key' => 'name',
+ 'size' => 100,
+ 'required' => false,
+ 'array' => true,
+ ]);
+ $this->assertEquals(202, $recreate['headers']['status-code']);
+
+ $this->assertEventually(function () use ($databaseId, $tableId, $sourceHeaders) {
+ $r = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/name', $sourceHeaders);
+ $this->assertEquals(200, $r['headers']['status-code']);
+ $this->assertEquals('available', $r['body']['status']);
+ }, 10000, 500);
+
+ // Source row's data was nulled by the source-side delete. Set a list value (column is array=true now).
+ $relink = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, $sourceHeaders, [
+ 'data' => ['name' => ['after-recreate']],
+ ]);
+ $this->assertEquals(200, $relink['headers']['status-code']);
+
+ $overwriteResult = $this->performMigrationSync([
+ 'resources' => $resources,
+ 'endpoint' => $this->webEndpoint,
+ 'projectId' => $this->getProject()['$id'],
+ 'apiKey' => $this->getProject()['apiKey'],
+ 'onDuplicate' => 'overwrite',
+ ]);
+ $this->assertEquals('completed', $overwriteResult['status']);
+
+ $this->assertEventually(function () use ($databaseId, $tableId, $destHeaders) {
+ $col = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/name', $destHeaders);
+ $this->assertEquals(200, $col['headers']['status-code']);
+ $this->assertEquals('available', $col['body']['status']);
+ $this->assertTrue($col['body']['array'], 'recreated column must reflect the new spec (array=true)');
+ $this->assertFalse($col['body']['required']);
+ }, 10000, 500);
+
+ $rowAfter = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, $destHeaders);
+ $this->assertEquals(200, $rowAfter['headers']['status-code']);
+ $this->assertEquals(['after-recreate'], $rowAfter['body']['name'], 'row pass must repopulate the recreated column with source value');
+
+ $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, $destHeaders);
+ $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, $sourceHeaders);
+
+ self::$cachedDatabaseData = [];
+ self::$cachedTableData = [];
+ }
+
+ /** Source drops+recreates with SAME spec: spec-match guard forces Tolerate; dest meta untouched. */
+ public function testAppwriteMigrationOverwriteSameSpecRecreateTolerates(): void
+ {
+ $sourceHeaders = [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ];
+ $destHeaders = [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDestinationProject()['$id'],
+ 'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
+ ];
+
+ $data = $this->setupMigrationTable();
+ $databaseId = $data['databaseId'];
+ $tableId = $data['tableId'];
+ $rowId = 'row-spec-match';
+
+ $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', $sourceHeaders, [
+ 'rowId' => $rowId,
+ 'data' => ['name' => 'before-recreate'],
+ ]);
+
+ $resources = [
+ Resource::TYPE_DATABASE,
+ Resource::TYPE_TABLE,
+ Resource::TYPE_COLUMN,
+ Resource::TYPE_ROW,
+ ];
+
+ $first = $this->performMigrationSync([
+ 'resources' => $resources,
+ 'endpoint' => $this->webEndpoint,
+ 'projectId' => $this->getProject()['$id'],
+ 'apiKey' => $this->getProject()['apiKey'],
+ ]);
+ $this->assertEquals('completed', $first['status']);
+
+ $destBefore = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/name', $destHeaders);
+ $this->assertEquals(200, $destBefore['headers']['status-code']);
+ $destCreatedAtBefore = $destBefore['body']['$createdAt'];
+
+ sleep(1);
+
+ // Drop + recreate with the EXACT same spec as setupMigrationTable
+ // (size=100, required=true). Source's $createdAt advances but the
+ // spec is identical â spec-match guard must force Tolerate.
+ $delete = $this->client->call(Client::METHOD_DELETE, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/name', $sourceHeaders);
+ $this->assertEquals(204, $delete['headers']['status-code']);
+
+ $this->assertEventually(function () use ($databaseId, $tableId, $sourceHeaders) {
+ $r = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/name', $sourceHeaders);
+ $this->assertEquals(404, $r['headers']['status-code']);
+ }, 10000, 500);
+
+ $recreate = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/string', $sourceHeaders, [
+ 'key' => 'name',
+ 'size' => 100,
+ 'required' => true,
+ ]);
+ $this->assertEquals(202, $recreate['headers']['status-code']);
+
+ $this->assertEventually(function () use ($databaseId, $tableId, $sourceHeaders) {
+ $r = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/name', $sourceHeaders);
+ $this->assertEquals(200, $r['headers']['status-code']);
+ $this->assertEquals('available', $r['body']['status']);
+ }, 10000, 500);
+
+ $relink = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, $sourceHeaders, [
+ 'data' => ['name' => 'after-recreate'],
+ ]);
+ $this->assertEquals(200, $relink['headers']['status-code']);
+
+ $overwriteResult = $this->performMigrationSync([
+ 'resources' => $resources,
+ 'endpoint' => $this->webEndpoint,
+ 'projectId' => $this->getProject()['$id'],
+ 'apiKey' => $this->getProject()['apiKey'],
+ 'onDuplicate' => 'overwrite',
+ ]);
+ $this->assertEquals('completed', $overwriteResult['status']);
+
+ // Spec-match guard fired â dest column's $createdAt stayed at the
+ // first-migration value. If DropAndRecreate had run, $createdAt
+ // would have been bumped to source's NEW createdAt.
+ $destAfter = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/name', $destHeaders);
+ $this->assertEquals(200, $destAfter['headers']['status-code']);
+ $this->assertEquals($destCreatedAtBefore, $destAfter['body']['$createdAt'], 'spec-match guard must keep dest column meta untouched');
+ $this->assertEquals(100, $destAfter['body']['size']);
+ $this->assertTrue($destAfter['body']['required']);
+
+ // Row pass under Overwrite still propagated source's new row value.
+ $rowAfter = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, $destHeaders);
+ $this->assertEquals(200, $rowAfter['headers']['status-code']);
+ $this->assertEquals('after-recreate', $rowAfter['body']['name']);
+
+ $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, $destHeaders);
+ $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, $sourceHeaders);
+
+ self::$cachedDatabaseData = [];
+ self::$cachedTableData = [];
+ }
+
/**
* Storage
*/
@@ -1096,6 +2365,7 @@ trait MigrationsBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
+ 'x-appwrite-response-format' => '1.9.3'
], [
'key' => 'TEST_VAR',
'value' => 'test_value',
@@ -1193,6 +2463,83 @@ trait MigrationsBase
return new CURLFile($tarPath, 'application/x-gzip', \basename($tarPath));
}
+ /**
+ * Integrations
+ */
+ public function testAppwriteMigrationPlatform(): void
+ {
+ $sourceHeaders = [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ];
+
+ $destinationHeaders = [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDestinationProject()['$id'],
+ 'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
+ ];
+
+ // Create platform on source project
+ $response = $this->client->call(Client::METHOD_POST, '/project/platforms/web', $sourceHeaders, [
+ 'platformId' => ID::unique(),
+ 'name' => 'Test Platform',
+ 'hostname' => 'localhost',
+ ]);
+
+ $this->assertEquals(201, $response['headers']['status-code']);
+ $this->assertNotEmpty($response['body']);
+ $this->assertNotEmpty($response['body']['$id']);
+
+ $platform = $response['body'];
+
+ $result = $this->performMigrationSync([
+ 'resources' => [
+ Resource::TYPE_PLATFORM,
+ ],
+ 'endpoint' => $this->webEndpoint,
+ 'projectId' => $this->getProject()['$id'],
+ 'apiKey' => $this->getProject()['apiKey'],
+ ]);
+
+ $this->assertEquals('completed', $result['status']);
+ $this->assertEquals([Resource::TYPE_PLATFORM], $result['resources']);
+ $this->assertArrayHasKey(Resource::TYPE_PLATFORM, $result['statusCounters']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_PLATFORM]['error']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_PLATFORM]['pending']);
+ $this->assertEquals(1, $result['statusCounters'][Resource::TYPE_PLATFORM]['success']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_PLATFORM]['processing']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_PLATFORM]['warning']);
+
+ // Verify platform on destination project using the project's API key
+ $response = $this->client->call(Client::METHOD_GET, '/project/platforms', $destinationHeaders);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertNotEmpty($response['body']);
+ $this->assertGreaterThan(0, $response['body']['total']);
+
+ $foundPlatform = null;
+
+ foreach ($response['body']['platforms'] as $p) {
+ if ($p['name'] === 'Test Platform' && $p['type'] === 'web') {
+ $foundPlatform = $p;
+
+ break;
+ }
+ }
+
+ $this->assertNotNull($foundPlatform);
+ $this->assertEquals('web', $foundPlatform['type']);
+ $this->assertEquals('Test Platform', $foundPlatform['name']);
+ $this->assertEquals('localhost', $foundPlatform['hostname']);
+
+ // Cleanup on destination
+ $this->client->call(Client::METHOD_DELETE, '/project/platforms/' . $foundPlatform['$id'], $destinationHeaders);
+
+ // Cleanup on source
+ $this->client->call(Client::METHOD_DELETE, '/project/platforms/' . $platform['$id'], $sourceHeaders);
+ }
+
/**
* Import documents from a CSV file.
*/
@@ -1256,7 +2603,6 @@ trait MigrationsBase
'max' => 65,
'required' => true,
]);
-
$this->assertEquals(202, $response['headers']['status-code']);
$this->assertEquals($response['body']['key'], 'age');
$this->assertEquals($response['body']['type'], 'integer');
@@ -1483,6 +2829,260 @@ trait MigrationsBase
}, 10_000, 500);
}
+ /**
+ * Set up a database + table + bucket + uploaded CSV for the skip/overwrite tests.
+ * Returns [$databaseId, $tableId, $bucketId, $fileId, $firstRowId, $firstRowName, $firstRowAge].
+ *
+ * @return array{string,string,string,string,string,string,int}
+ */
+ private function prepareCsvImportFixture(string $testLabel): array
+ {
+ $headers = [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ];
+
+ // database
+ $response = $this->client->call(Client::METHOD_POST, '/databases', $headers, [
+ 'databaseId' => ID::unique(),
+ 'name' => 'Test DB ' . $testLabel,
+ ]);
+ $this->assertEquals(201, $response['headers']['status-code']);
+ $databaseId = $response['body']['$id'];
+
+ // table
+ $response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', $headers, [
+ 'name' => 'Test table ' . $testLabel,
+ 'tableId' => ID::unique(),
+ ]);
+ $this->assertEquals(201, $response['headers']['status-code']);
+ $tableId = $response['body']['$id'];
+
+ // columns: name, age (match documents.csv fixture)
+ $response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/string', $headers, [
+ 'key' => 'name',
+ 'size' => 256,
+ 'required' => true,
+ ]);
+ $this->assertEquals(202, $response['headers']['status-code']);
+
+ $response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/integer', $headers, [
+ 'key' => 'age',
+ 'min' => 18,
+ 'max' => 65,
+ 'required' => true,
+ ]);
+ $this->assertEquals(202, $response['headers']['status-code']);
+
+ // Columns are created async (202). Wait for both to be `available`
+ // before proceeding so the migration worker doesn't race the schema.
+ foreach (['name', 'age'] as $column) {
+ $this->assertEventually(function () use ($databaseId, $tableId, $column, $headers) {
+ $response = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/' . $column, $headers);
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals('available', $response['body']['status']);
+ }, 5000, 500);
+ }
+
+ // bucket
+ $response = $this->client->call(Client::METHOD_POST, '/storage/buckets', $headers, [
+ 'bucketId' => ID::unique(),
+ 'name' => 'Bucket ' . $testLabel,
+ 'maximumFileSize' => 2000000,
+ 'allowedFileExtensions' => ['csv'],
+ ]);
+ $this->assertEquals(201, $response['headers']['status-code']);
+ $bucketId = $response['body']['$id'];
+
+ // upload documents.csv (100 rows with $id, name, age columns)
+ $response = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucketId . '/files', array_merge([
+ 'content-type' => 'multipart/form-data',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'fileId' => ID::unique(),
+ 'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/csv/documents.csv'), 'text/csv', 'documents.csv'),
+ ]);
+ $this->assertEquals(201, $response['headers']['status-code']);
+ $fileId = $response['body']['$id'];
+
+ // first row in documents.csv: hxfcwpcas5xokpwe,Diamond Mendez,56
+ return [$databaseId, $tableId, $bucketId, $fileId, 'hxfcwpcas5xokpwe', 'Diamond Mendez', 56];
+ }
+
+ /**
+ * onDuplicate=skip on re-import: duplicates are silently no-op'd, existing rows preserved unchanged.
+ */
+ public function testCreateCSVImportSkipDuplicates(): void
+ {
+ [$databaseId, $tableId, $bucketId, $fileId, $rowId, $originalName, $originalAge] = $this->prepareCsvImportFixture('skip');
+
+ // First import: 100 rows created
+ $first = $this->performCsvMigration([
+ 'fileId' => $fileId,
+ 'bucketId' => $bucketId,
+ 'resourceId' => $databaseId . ':' . $tableId,
+ ]);
+ $this->assertEventually(function () use ($first) {
+ $migration = $this->client->call(Client::METHOD_GET, '/migrations/' . $first['body']['$id'], array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+ $this->assertEquals('completed', $migration['body']['status']);
+ $this->assertEquals(100, $migration['body']['statusCounters'][Resource::TYPE_ROW]['success']);
+ }, 10_000, 500);
+
+ // Mutate one row so we can prove skip does NOT overwrite it
+ $mutate = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'data' => ['age' => 22],
+ ]);
+ $this->assertEquals(200, $mutate['headers']['status-code']);
+ $this->assertEquals(22, $mutate['body']['age']);
+
+ // Second import with onDuplicate=skip: no errors, mutated row preserved
+ $second = $this->performCsvMigration([
+ 'fileId' => $fileId,
+ 'bucketId' => $bucketId,
+ 'resourceId' => $databaseId . ':' . $tableId,
+ 'onDuplicate' => 'skip',
+ ]);
+ $this->assertEventually(function () use ($second) {
+ $migration = $this->client->call(Client::METHOD_GET, '/migrations/' . $second['body']['$id'], array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+ $this->assertEquals('completed', $migration['body']['status']);
+ }, 10_000, 500);
+
+ // Mutated row kept its mutated value (not overwritten by CSV's original age)
+ $row = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+ $this->assertEquals(200, $row['headers']['status-code']);
+ $this->assertEquals($originalName, $row['body']['name']);
+ $this->assertEquals(22, $row['body']['age'], 'onDuplicate=skip must not overwrite mutated row');
+
+ // Row count still 100 (no duplicates created)
+ $rows = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'queries' => [Query::limit(150)->toString()],
+ ]);
+ $this->assertEquals(100, $rows['body']['total']);
+ }
+
+ /**
+ * onDuplicate=overwrite on re-import: existing rows are replaced with imported values.
+ */
+ public function testCreateCSVImportOverwrite(): void
+ {
+ [$databaseId, $tableId, $bucketId, $fileId, $rowId, $originalName, $originalAge] = $this->prepareCsvImportFixture('overwrite');
+
+ // First import: 100 rows created
+ $first = $this->performCsvMigration([
+ 'fileId' => $fileId,
+ 'bucketId' => $bucketId,
+ 'resourceId' => $databaseId . ':' . $tableId,
+ ]);
+ $this->assertEventually(function () use ($first) {
+ $migration = $this->client->call(Client::METHOD_GET, '/migrations/' . $first['body']['$id'], array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+ $this->assertEquals('completed', $migration['body']['status']);
+ $this->assertEquals(100, $migration['body']['statusCounters'][Resource::TYPE_ROW]['success']);
+ }, 10_000, 500);
+
+ // Mutate one row so we can prove overwrite restores it to the CSV's original value
+ $mutate = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'data' => ['age' => 22],
+ ]);
+ $this->assertEquals(200, $mutate['headers']['status-code']);
+ $this->assertEquals(22, $mutate['body']['age']);
+
+ // Second import with onDuplicate=overwrite: mutated row restored to CSV value
+ $second = $this->performCsvMigration([
+ 'fileId' => $fileId,
+ 'bucketId' => $bucketId,
+ 'resourceId' => $databaseId . ':' . $tableId,
+ 'onDuplicate' => 'overwrite',
+ ]);
+ $this->assertEventually(function () use ($second) {
+ $migration = $this->client->call(Client::METHOD_GET, '/migrations/' . $second['body']['$id'], array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+ $this->assertEquals('completed', $migration['body']['status']);
+ }, 10_000, 500);
+
+ // Mutated row is back to CSV's original age (proving overwrite actually replaced the row)
+ $row = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+ $this->assertEquals(200, $row['headers']['status-code']);
+ $this->assertEquals($originalName, $row['body']['name']);
+ $this->assertEquals($originalAge, $row['body']['age'], 'onDuplicate=overwrite must restore row to imported value');
+
+ // Row count still 100 (no duplicates created)
+ $rows = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'queries' => [Query::limit(150)->toString()],
+ ]);
+ $this->assertEquals(100, $rows['body']['total']);
+ }
+
+ /**
+ * Default behavior (neither flag): re-import of duplicate ids fails with DuplicateException.
+ * Regression guard so the skip/overwrite additions don't silently change the default.
+ */
+ public function testCreateCSVImportDefaultFailsOnDuplicate(): void
+ {
+ [$databaseId, $tableId, $bucketId, $fileId] = $this->prepareCsvImportFixture('default');
+
+ // First import: succeeds
+ $first = $this->performCsvMigration([
+ 'fileId' => $fileId,
+ 'bucketId' => $bucketId,
+ 'resourceId' => $databaseId . ':' . $tableId,
+ ]);
+ $this->assertEventually(function () use ($first) {
+ $migration = $this->client->call(Client::METHOD_GET, '/migrations/' . $first['body']['$id'], array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+ $this->assertEquals('completed', $migration['body']['status']);
+ }, 10_000, 500);
+
+ // Second import with no flags: should fail on duplicate ids
+ $second = $this->performCsvMigration([
+ 'fileId' => $fileId,
+ 'bucketId' => $bucketId,
+ 'resourceId' => $databaseId . ':' . $tableId,
+ ]);
+ $this->assertEventually(function () use ($second) {
+ $migration = $this->client->call(Client::METHOD_GET, '/migrations/' . $second['body']['$id'], array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+ $this->assertEquals('finished', $migration['body']['stage']);
+ $this->assertEquals('failed', $migration['body']['status']);
+ $this->assertNotEmpty($migration['body']['errors']);
+ }, 60_000, 500);
+ }
+
private function performCsvMigration(array $body): array
{
return $this->client->call(Client::METHOD_POST, '/migrations/csv', [
@@ -1492,6 +3092,246 @@ trait MigrationsBase
], $body);
}
+ /**
+ * Set up a database + table + bucket + uploaded JSON for the skip/overwrite tests.
+ * Mirrors prepareCsvImportFixture but uploads documents.json instead.
+ *
+ * @return array{string,string,string,string,string,string,int}
+ */
+ private function prepareJsonImportFixture(string $testLabel): array
+ {
+ $headers = [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ];
+
+ // database
+ $response = $this->client->call(Client::METHOD_POST, '/databases', $headers, [
+ 'databaseId' => ID::unique(),
+ 'name' => 'Test JSON DB ' . $testLabel,
+ ]);
+ $this->assertEquals(201, $response['headers']['status-code']);
+ $databaseId = $response['body']['$id'];
+
+ // table
+ $response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', $headers, [
+ 'name' => 'Test JSON table ' . $testLabel,
+ 'tableId' => ID::unique(),
+ ]);
+ $this->assertEquals(201, $response['headers']['status-code']);
+ $tableId = $response['body']['$id'];
+
+ // columns: name, age (match documents.json fixture)
+ $response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/string', $headers, [
+ 'key' => 'name',
+ 'size' => 256,
+ 'required' => true,
+ ]);
+ $this->assertEquals(202, $response['headers']['status-code']);
+
+ $response = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/integer', $headers, [
+ 'key' => 'age',
+ 'min' => 18,
+ 'max' => 65,
+ 'required' => true,
+ ]);
+ $this->assertEquals(202, $response['headers']['status-code']);
+
+ foreach (['name', 'age'] as $column) {
+ $this->assertEventually(function () use ($databaseId, $tableId, $column, $headers) {
+ $response = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/' . $column, $headers);
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals('available', $response['body']['status']);
+ }, 5000, 500);
+ }
+
+ // bucket
+ $response = $this->client->call(Client::METHOD_POST, '/storage/buckets', $headers, [
+ 'bucketId' => ID::unique(),
+ 'name' => 'JSON Bucket ' . $testLabel,
+ 'maximumFileSize' => 2000000,
+ 'allowedFileExtensions' => ['json'],
+ ]);
+ $this->assertEquals(201, $response['headers']['status-code']);
+ $bucketId = $response['body']['$id'];
+
+ // upload documents.json (same row shape as documents.csv)
+ $response = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucketId . '/files', array_merge([
+ 'content-type' => 'multipart/form-data',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'fileId' => ID::unique(),
+ 'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/json/documents.json'), 'application/json', 'documents.json'),
+ ]);
+ $this->assertEquals(201, $response['headers']['status-code']);
+ $fileId = $response['body']['$id'];
+
+ // first row in documents.json: hxfcwpcas5xokpwe, Diamond Mendez, 56
+ return [$databaseId, $tableId, $bucketId, $fileId, 'hxfcwpcas5xokpwe', 'Diamond Mendez', 56];
+ }
+
+ /**
+ * onDuplicate=skip on JSON re-import: duplicates silently no-op, existing rows preserved unchanged.
+ */
+ public function testCreateJSONImportSkipDuplicates(): void
+ {
+ [$databaseId, $tableId, $bucketId, $fileId, $rowId, $originalName, $originalAge] = $this->prepareJsonImportFixture('skip');
+
+ $first = $this->performJsonMigration([
+ 'fileId' => $fileId,
+ 'bucketId' => $bucketId,
+ 'resourceId' => $databaseId . ':' . $tableId,
+ ]);
+ $this->assertEventually(function () use ($first) {
+ $migration = $this->client->call(Client::METHOD_GET, '/migrations/' . $first['body']['$id'], array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+ $this->assertEquals('completed', $migration['body']['status']);
+ $this->assertEquals(100, $migration['body']['statusCounters'][Resource::TYPE_ROW]['success']);
+ }, 10_000, 500);
+
+ // Mutate one row so we can prove skip does NOT overwrite it
+ $mutate = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'data' => ['age' => 22],
+ ]);
+ $this->assertEquals(200, $mutate['headers']['status-code']);
+ $this->assertEquals(22, $mutate['body']['age']);
+
+ $second = $this->performJsonMigration([
+ 'fileId' => $fileId,
+ 'bucketId' => $bucketId,
+ 'resourceId' => $databaseId . ':' . $tableId,
+ 'onDuplicate' => 'skip',
+ ]);
+ $this->assertEventually(function () use ($second) {
+ $migration = $this->client->call(Client::METHOD_GET, '/migrations/' . $second['body']['$id'], array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+ $this->assertEquals('completed', $migration['body']['status']);
+ }, 10_000, 500);
+
+ $row = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+ $this->assertEquals(200, $row['headers']['status-code']);
+ $this->assertEquals($originalName, $row['body']['name']);
+ $this->assertEquals(22, $row['body']['age'], 'onDuplicate=skip must not overwrite mutated row');
+
+ $rows = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'queries' => [Query::limit(150)->toString()],
+ ]);
+ $this->assertEquals(100, $rows['body']['total']);
+ }
+
+ /**
+ * onDuplicate=overwrite on JSON re-import: existing rows replaced with imported values.
+ */
+ public function testCreateJSONImportOverwrite(): void
+ {
+ [$databaseId, $tableId, $bucketId, $fileId, $rowId, $originalName, $originalAge] = $this->prepareJsonImportFixture('overwrite');
+
+ $first = $this->performJsonMigration([
+ 'fileId' => $fileId,
+ 'bucketId' => $bucketId,
+ 'resourceId' => $databaseId . ':' . $tableId,
+ ]);
+ $this->assertEventually(function () use ($first) {
+ $migration = $this->client->call(Client::METHOD_GET, '/migrations/' . $first['body']['$id'], array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+ $this->assertEquals('completed', $migration['body']['status']);
+ $this->assertEquals(100, $migration['body']['statusCounters'][Resource::TYPE_ROW]['success']);
+ }, 10_000, 500);
+
+ $mutate = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'data' => ['age' => 22],
+ ]);
+ $this->assertEquals(200, $mutate['headers']['status-code']);
+ $this->assertEquals(22, $mutate['body']['age']);
+
+ $second = $this->performJsonMigration([
+ 'fileId' => $fileId,
+ 'bucketId' => $bucketId,
+ 'resourceId' => $databaseId . ':' . $tableId,
+ 'onDuplicate' => 'overwrite',
+ ]);
+ $this->assertEventually(function () use ($second) {
+ $migration = $this->client->call(Client::METHOD_GET, '/migrations/' . $second['body']['$id'], array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+ $this->assertEquals('completed', $migration['body']['status']);
+ }, 10_000, 500);
+
+ $row = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+ $this->assertEquals(200, $row['headers']['status-code']);
+ $this->assertEquals($originalName, $row['body']['name']);
+ $this->assertEquals($originalAge, $row['body']['age'], 'onDuplicate=overwrite must restore row to imported value');
+
+ $rows = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'queries' => [Query::limit(150)->toString()],
+ ]);
+ $this->assertEquals(100, $rows['body']['total']);
+ }
+
+ /**
+ * Default (no onDuplicate) on JSON re-import: regression guard, must fail on duplicate ids.
+ */
+ public function testCreateJSONImportDefaultFailsOnDuplicate(): void
+ {
+ [$databaseId, $tableId, $bucketId, $fileId] = $this->prepareJsonImportFixture('default');
+
+ $first = $this->performJsonMigration([
+ 'fileId' => $fileId,
+ 'bucketId' => $bucketId,
+ 'resourceId' => $databaseId . ':' . $tableId,
+ ]);
+ $this->assertEventually(function () use ($first) {
+ $migration = $this->client->call(Client::METHOD_GET, '/migrations/' . $first['body']['$id'], array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+ $this->assertEquals('completed', $migration['body']['status']);
+ }, 10_000, 500);
+
+ $second = $this->performJsonMigration([
+ 'fileId' => $fileId,
+ 'bucketId' => $bucketId,
+ 'resourceId' => $databaseId . ':' . $tableId,
+ ]);
+ $this->assertEventually(function () use ($second) {
+ $migration = $this->client->call(Client::METHOD_GET, '/migrations/' . $second['body']['$id'], array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+ $this->assertEquals('finished', $migration['body']['stage']);
+ $this->assertEquals('failed', $migration['body']['status']);
+ $this->assertNotEmpty($migration['body']['errors']);
+ }, 60_000, 500);
+ }
+
/**
* Test CSV export with email notification
*/
@@ -1573,6 +3413,19 @@ trait MigrationsBase
$this->assertEquals(202, $varchar['headers']['status-code']);
+ $bigint = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/bigint', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey']
+ ], [
+ 'key' => 'bigint',
+ 'min' => 2147483648,
+ 'max' => 9223372036854775807,
+ 'required' => false,
+ ]);
+
+ $this->assertEquals(202, $bigint['headers']['status-code']);
+
$mediumtext = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/mediumtext', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
@@ -1623,6 +3476,7 @@ trait MigrationsBase
'mediumtext' => 'mediumText',
'longtext' => 'longText',
'varchar' => 'varchar',
+ 'bigint' => 2147483648 + $i,
]
]);
@@ -1711,6 +3565,8 @@ trait MigrationsBase
$this->assertStringContainsString('mediumText', $csvData, 'CSV should contain the medium column header');
$this->assertStringContainsString('longText', $csvData, 'CSV should contain the long text column header');
$this->assertStringContainsString('varchar', $csvData, 'CSV should contain the varchar column header');
+ $this->assertStringContainsString('bigint', $csvData, 'CSV should contain the bigint column header');
+ $this->assertStringContainsString('2147483649', $csvData, 'CSV should contain bigint test data');
// Cleanup
$this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, [
diff --git a/tests/e2e/Services/Project/AuthMethodsBase.php b/tests/e2e/Services/Project/AuthMethodsBase.php
index afa58a3640..cccdca9ea1 100644
--- a/tests/e2e/Services/Project/AuthMethodsBase.php
+++ b/tests/e2e/Services/Project/AuthMethodsBase.php
@@ -212,6 +212,7 @@ trait AuthMethodsBase
$headers = \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders());
$projectId = $this->getProject()['$id'];
@@ -322,7 +323,9 @@ trait AuthMethodsBase
];
if ($authenticated) {
- $headers = \array_merge($headers, $this->getHeaders());
+ $headers = \array_merge($headers, $this->getHeaders(), [
+ 'x-appwrite-response-format' => '1.9.4',
+ ]);
}
return $this->client->call(
diff --git a/tests/e2e/Services/Project/OAuth2Base.php b/tests/e2e/Services/Project/OAuth2Base.php
index 5451435c3c..5959a584ea 100644
--- a/tests/e2e/Services/Project/OAuth2Base.php
+++ b/tests/e2e/Services/Project/OAuth2Base.php
@@ -5,6 +5,7 @@ namespace Tests\E2E\Services\Project;
use PHPUnit\Framework\Attributes\Before;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\E2E\Client;
+use Utopia\Database\Query;
trait OAuth2Base
{
@@ -172,6 +173,40 @@ trait OAuth2Base
$this->assertNotContains('mock-unverified', $ids);
}
+ public function testListOAuth2ProvidersTotalFalse(): void
+ {
+ $response = $this->listOAuth2Providers(total: false);
+
+ $this->assertSame(200, $response['headers']['status-code']);
+ $this->assertSame(0, $response['body']['total']);
+ $this->assertGreaterThan(0, \count($response['body']['providers']));
+ }
+
+ public function testListOAuth2ProvidersWithLimit(): void
+ {
+ $response = $this->listOAuth2Providers([
+ Query::limit(1)->toString(),
+ ]);
+
+ $this->assertSame(200, $response['headers']['status-code']);
+ $this->assertCount(1, $response['body']['providers']);
+ $this->assertGreaterThan(1, $response['body']['total']);
+ }
+
+ public function testListOAuth2ProvidersWithOffset(): void
+ {
+ $listAll = $this->listOAuth2Providers();
+ $this->assertSame(200, $listAll['headers']['status-code']);
+
+ $listOffset = $this->listOAuth2Providers([
+ Query::offset(1)->toString(),
+ ]);
+
+ $this->assertSame(200, $listOffset['headers']['status-code']);
+ $this->assertCount(\count($listAll['body']['providers']) - 1, $listOffset['body']['providers']);
+ $this->assertSame($listAll['body']['total'], $listOffset['body']['total']);
+ }
+
// =========================================================================
// Get OAuth2 provider
// =========================================================================
@@ -188,6 +223,28 @@ trait OAuth2Base
$this->assertSame('', $response['body']['clientSecret']);
}
+ public function testGetOAuth2ProviderWithAlias(): void
+ {
+ // The action declares the canonical param name as `providerId` and
+ // registers `provider` as an alias so that older SDK versions that
+ // send the provider in the query string continue to work.
+ $headers = [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ];
+ $headers = \array_merge($headers, $this->getHeaders());
+
+ // Call with `provider` in query string (legacy behaviour)
+ $response = $this->client->call(
+ Client::METHOD_GET,
+ '/project/oauth2/github?provider=github',
+ $headers,
+ );
+
+ $this->assertSame(200, $response['headers']['status-code']);
+ $this->assertSame('github', $response['body']['$id']);
+ }
+
public function testGetOAuth2ProviderClientSecretWriteOnly(): void
{
$this->updateOAuth2('amazon', [
@@ -221,19 +278,23 @@ trait OAuth2Base
public function testGetOAuth2ProviderUnsupported(): void
{
+ // The `providerId` param is validated by a WhiteList of registered
+ // OAuth2 provider keys, so an unknown value is rejected at validation
+ // time â before the action runs â and surfaces as a generic argument
+ // error rather than `project_provider_unsupported`.
$response = $this->getOAuth2Provider('not-a-real-provider');
$this->assertSame(400, $response['headers']['status-code']);
- $this->assertSame('project_provider_unsupported', $response['body']['type']);
+ $this->assertSame('general_argument_invalid', $response['body']['type']);
}
public function testGetOAuth2ProviderRegisteredInConfigButNoUpdateClass(): void
{
- // `mock` is present in oAuthProviders config (enabled: true) but is NOT
- // registered in Base::getProviderActions(). Get::action has two
- // separate `unsupported` throw branches â testGetOAuth2ProviderUnsupported
- // covers the first (provider missing from config); this covers the
- // second (provider in config but missing from the action registry).
+ // `mock` is present in oAuthProviders config (enabled: true) but is
+ // NOT registered in Base::getProviderActions(). It passes the
+ // WhiteList validator (which only checks config membership) and
+ // reaches the action body, where the action-registry check throws
+ // `project_provider_unsupported`.
$response = $this->getOAuth2Provider('mock');
$this->assertSame(400, $response['headers']['status-code']);
@@ -1590,8 +1651,8 @@ trait OAuth2Base
$this->assertSame(200, $response['headers']['status-code']);
$this->assertSame('https://idp.example.com/.well-known/openid-configuration', $response['body']['wellKnownURL']);
$this->assertArrayHasKey('authorizationURL', $response['body']);
- $this->assertArrayHasKey('tokenUrl', $response['body']);
- $this->assertArrayHasKey('userInfoUrl', $response['body']);
+ $this->assertArrayHasKey('tokenURL', $response['body']);
+ $this->assertArrayHasKey('userInfoURL', $response['body']);
// Cleanup
$this->updateOAuth2('oidc', [
@@ -1599,8 +1660,8 @@ trait OAuth2Base
'clientSecret' => '',
'wellKnownURL' => '',
'authorizationURL' => '',
- 'tokenUrl' => '',
- 'userInfoUrl' => '',
+ 'tokenURL' => '',
+ 'userInfoURL' => '',
'enabled' => false,
]);
}
@@ -1611,15 +1672,15 @@ trait OAuth2Base
'clientId' => 'oidc-discovery',
'clientSecret' => 'oidc-discovery-secret',
'authorizationURL' => 'https://idp.example.com/oauth2/authorize',
- 'tokenUrl' => 'https://idp.example.com/oauth2/token',
- 'userInfoUrl' => 'https://idp.example.com/oauth2/userinfo',
+ 'tokenURL' => 'https://idp.example.com/oauth2/token',
+ 'userInfoURL' => 'https://idp.example.com/oauth2/userinfo',
'enabled' => false,
]);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertSame('https://idp.example.com/oauth2/authorize', $response['body']['authorizationURL']);
- $this->assertSame('https://idp.example.com/oauth2/token', $response['body']['tokenUrl']);
- $this->assertSame('https://idp.example.com/oauth2/userinfo', $response['body']['userInfoUrl']);
+ $this->assertSame('https://idp.example.com/oauth2/token', $response['body']['tokenURL']);
+ $this->assertSame('https://idp.example.com/oauth2/userinfo', $response['body']['userInfoURL']);
// Cleanup
$this->updateOAuth2('oidc', [
@@ -1627,8 +1688,8 @@ trait OAuth2Base
'clientSecret' => '',
'wellKnownURL' => '',
'authorizationURL' => '',
- 'tokenUrl' => '',
- 'userInfoUrl' => '',
+ 'tokenURL' => '',
+ 'userInfoURL' => '',
'enabled' => false,
]);
}
@@ -1640,8 +1701,8 @@ trait OAuth2Base
'clientSecret' => '',
'wellKnownURL' => '',
'authorizationURL' => '',
- 'tokenUrl' => '',
- 'userInfoUrl' => '',
+ 'tokenURL' => '',
+ 'userInfoURL' => '',
'enabled' => false,
]);
@@ -1670,8 +1731,8 @@ trait OAuth2Base
'clientSecret' => '',
'wellKnownURL' => '',
'authorizationURL' => '',
- 'tokenUrl' => '',
- 'userInfoUrl' => '',
+ 'tokenURL' => '',
+ 'userInfoURL' => '',
'enabled' => false,
]);
@@ -1679,7 +1740,7 @@ trait OAuth2Base
'clientId' => 'oidc-partial',
'clientSecret' => 'oidc-partial-secret',
'authorizationURL' => 'https://idp.example.com/oauth2/authorize',
- 'tokenUrl' => 'https://idp.example.com/oauth2/token',
+ 'tokenURL' => 'https://idp.example.com/oauth2/token',
'enabled' => true,
]);
@@ -1692,8 +1753,8 @@ trait OAuth2Base
'clientSecret' => '',
'wellKnownURL' => '',
'authorizationURL' => '',
- 'tokenUrl' => '',
- 'userInfoUrl' => '',
+ 'tokenURL' => '',
+ 'userInfoURL' => '',
'enabled' => false,
]);
}
@@ -1724,8 +1785,8 @@ trait OAuth2Base
'clientSecret' => '',
'wellKnownURL' => '',
'authorizationURL' => '',
- 'tokenUrl' => '',
- 'userInfoUrl' => '',
+ 'tokenURL' => '',
+ 'userInfoURL' => '',
'enabled' => false,
]);
}
@@ -1755,8 +1816,8 @@ trait OAuth2Base
'clientSecret' => '',
'wellKnownURL' => '',
'authorizationURL' => '',
- 'tokenUrl' => '',
- 'userInfoUrl' => '',
+ 'tokenURL' => '',
+ 'userInfoURL' => '',
'enabled' => false,
]);
}
@@ -1770,8 +1831,8 @@ trait OAuth2Base
'clientSecret' => '',
'wellKnownURL' => '',
'authorizationURL' => '',
- 'tokenUrl' => '',
- 'userInfoUrl' => '',
+ 'tokenURL' => '',
+ 'userInfoURL' => '',
'enabled' => false,
]);
@@ -1780,7 +1841,7 @@ trait OAuth2Base
'clientId' => 'oidc-split-discovery',
'clientSecret' => 'oidc-split-discovery-secret',
'authorizationURL' => 'https://idp.example.com/oauth2/authorize',
- 'tokenUrl' => 'https://idp.example.com/oauth2/token',
+ 'tokenURL' => 'https://idp.example.com/oauth2/token',
'enabled' => false,
]);
@@ -1788,19 +1849,19 @@ trait OAuth2Base
// state must include the two stored URLs + the new one to satisfy
// the all-three-discovery-URLs branch of the enable check.
$enable = $this->updateOAuth2('oidc', [
- 'userInfoUrl' => 'https://idp.example.com/oauth2/userinfo',
+ 'userInfoURL' => 'https://idp.example.com/oauth2/userinfo',
'enabled' => true,
]);
$this->assertSame(200, $enable['headers']['status-code']);
$this->assertTrue($enable['body']['enabled']);
// Confirm all three URLs ended up persisted (merge wrote the new
- // userInfoUrl while preserving the previously stored two).
+ // userInfoURL while preserving the previously stored two).
$get = $this->getOAuth2Provider('oidc');
$this->assertSame(200, $get['headers']['status-code']);
$this->assertSame('https://idp.example.com/oauth2/authorize', $get['body']['authorizationURL']);
- $this->assertSame('https://idp.example.com/oauth2/token', $get['body']['tokenUrl']);
- $this->assertSame('https://idp.example.com/oauth2/userinfo', $get['body']['userInfoUrl']);
+ $this->assertSame('https://idp.example.com/oauth2/token', $get['body']['tokenURL']);
+ $this->assertSame('https://idp.example.com/oauth2/userinfo', $get['body']['userInfoURL']);
// Cleanup
$this->updateOAuth2('oidc', [
@@ -1808,8 +1869,8 @@ trait OAuth2Base
'clientSecret' => '',
'wellKnownURL' => '',
'authorizationURL' => '',
- 'tokenUrl' => '',
- 'userInfoUrl' => '',
+ 'tokenURL' => '',
+ 'userInfoURL' => '',
'enabled' => false,
]);
}
@@ -1822,8 +1883,8 @@ trait OAuth2Base
'clientSecret' => 'oidc-clear-then-enable-secret',
'wellKnownURL' => 'https://idp.example.com/.well-known/openid-configuration',
'authorizationURL' => '',
- 'tokenUrl' => '',
- 'userInfoUrl' => '',
+ 'tokenURL' => '',
+ 'userInfoURL' => '',
'enabled' => false,
]);
@@ -1846,8 +1907,8 @@ trait OAuth2Base
'clientSecret' => '',
'wellKnownURL' => '',
'authorizationURL' => '',
- 'tokenUrl' => '',
- 'userInfoUrl' => '',
+ 'tokenURL' => '',
+ 'userInfoURL' => '',
'enabled' => false,
]);
}
@@ -1868,16 +1929,16 @@ trait OAuth2Base
$switch = $this->updateOAuth2('oidc', [
'wellKnownURL' => '',
'authorizationURL' => 'https://idp.example.com/oauth2/authorize',
- 'tokenUrl' => 'https://idp.example.com/oauth2/token',
- 'userInfoUrl' => 'https://idp.example.com/oauth2/userinfo',
+ 'tokenURL' => 'https://idp.example.com/oauth2/token',
+ 'userInfoURL' => 'https://idp.example.com/oauth2/userinfo',
'enabled' => true,
]);
$this->assertSame(200, $switch['headers']['status-code']);
$this->assertTrue($switch['body']['enabled']);
$this->assertSame('', $switch['body']['wellKnownURL']);
$this->assertSame('https://idp.example.com/oauth2/authorize', $switch['body']['authorizationURL']);
- $this->assertSame('https://idp.example.com/oauth2/token', $switch['body']['tokenUrl']);
- $this->assertSame('https://idp.example.com/oauth2/userinfo', $switch['body']['userInfoUrl']);
+ $this->assertSame('https://idp.example.com/oauth2/token', $switch['body']['tokenURL']);
+ $this->assertSame('https://idp.example.com/oauth2/userinfo', $switch['body']['userInfoURL']);
// Cleanup
$this->updateOAuth2('oidc', [
@@ -1885,8 +1946,8 @@ trait OAuth2Base
'clientSecret' => '',
'wellKnownURL' => '',
'authorizationURL' => '',
- 'tokenUrl' => '',
- 'userInfoUrl' => '',
+ 'tokenURL' => '',
+ 'userInfoURL' => '',
'enabled' => false,
]);
}
@@ -1900,23 +1961,23 @@ trait OAuth2Base
'clientSecret' => 'oidc-clear-secret',
'wellKnownURL' => 'https://idp.example.com/.well-known/openid-configuration',
'authorizationURL' => 'https://idp.example.com/oauth2/authorize',
- 'tokenUrl' => 'https://idp.example.com/oauth2/token',
- 'userInfoUrl' => 'https://idp.example.com/oauth2/userinfo',
+ 'tokenURL' => 'https://idp.example.com/oauth2/token',
+ 'userInfoURL' => 'https://idp.example.com/oauth2/userinfo',
'enabled' => false,
]);
$response = $this->updateOAuth2('oidc', [
'wellKnownURL' => '',
'authorizationURL' => '',
- 'tokenUrl' => '',
- 'userInfoUrl' => '',
+ 'tokenURL' => '',
+ 'userInfoURL' => '',
]);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertSame('', $response['body']['wellKnownURL']);
$this->assertSame('', $response['body']['authorizationURL']);
- $this->assertSame('', $response['body']['tokenUrl']);
- $this->assertSame('', $response['body']['userInfoUrl']);
+ $this->assertSame('', $response['body']['tokenURL']);
+ $this->assertSame('', $response['body']['userInfoURL']);
// Cleanup
$this->updateOAuth2('oidc', [
@@ -1926,6 +1987,96 @@ trait OAuth2Base
]);
}
+ public function testUpdateOAuth2OidcBackwardCompatibleResponseFormat(): void
+ {
+ // Reset to clean state
+ $this->updateOAuth2('oidc', [
+ 'clientId' => '',
+ 'clientSecret' => '',
+ 'wellKnownURL' => '',
+ 'authorizationURL' => '',
+ 'tokenURL' => '',
+ 'userInfoURL' => '',
+ 'enabled' => false,
+ ]);
+
+ $headers = [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.3',
+ ];
+ $headers = \array_merge($headers, $this->getHeaders());
+
+ // Update using OLD param names (aliases must still work)
+ $response = $this->client->call(
+ Client::METHOD_PATCH,
+ '/project/oauth2/oidc',
+ $headers,
+ [
+ 'clientId' => 'oidc-compat-client',
+ 'clientSecret' => 'oidc-compat-secret',
+ 'tokenUrl' => 'https://idp.example.com/oauth2/token',
+ 'userInfoUrl' => 'https://idp.example.com/oauth2/userinfo',
+ 'enabled' => false,
+ ],
+ );
+
+ $this->assertSame(200, $response['headers']['status-code']);
+ $this->assertArrayHasKey('tokenUrl', $response['body']);
+ $this->assertArrayHasKey('userInfoUrl', $response['body']);
+ $this->assertArrayNotHasKey('tokenURL', $response['body']);
+ $this->assertArrayNotHasKey('userInfoURL', $response['body']);
+ $this->assertSame('https://idp.example.com/oauth2/token', $response['body']['tokenUrl']);
+ $this->assertSame('https://idp.example.com/oauth2/userinfo', $response['body']['userInfoUrl']);
+
+ // GET with 1.9.3 format must also return old param names
+ $get = $this->client->call(
+ Client::METHOD_GET,
+ '/project/oauth2/oidc',
+ $headers,
+ );
+
+ $this->assertSame(200, $get['headers']['status-code']);
+ $this->assertArrayHasKey('tokenUrl', $get['body']);
+ $this->assertArrayHasKey('userInfoUrl', $get['body']);
+ $this->assertArrayNotHasKey('tokenURL', $get['body']);
+ $this->assertArrayNotHasKey('userInfoURL', $get['body']);
+ $this->assertSame('https://idp.example.com/oauth2/token', $get['body']['tokenUrl']);
+ $this->assertSame('https://idp.example.com/oauth2/userinfo', $get['body']['userInfoUrl']);
+
+ // LIST with 1.9.3 format must also return old param names for OIDC
+ $list = $this->client->call(
+ Client::METHOD_GET,
+ '/project/oauth2',
+ $headers,
+ );
+
+ $this->assertSame(200, $list['headers']['status-code']);
+ $oidcEntry = null;
+ foreach ($list['body']['providers'] as $provider) {
+ if ($provider['$id'] === 'oidc') {
+ $oidcEntry = $provider;
+ break;
+ }
+ }
+ $this->assertNotNull($oidcEntry, 'OIDC provider missing from listOAuth2Providers response');
+ $this->assertArrayHasKey('tokenUrl', $oidcEntry);
+ $this->assertArrayHasKey('userInfoUrl', $oidcEntry);
+ $this->assertArrayNotHasKey('tokenURL', $oidcEntry);
+ $this->assertArrayNotHasKey('userInfoURL', $oidcEntry);
+ $this->assertSame('https://idp.example.com/oauth2/token', $oidcEntry['tokenUrl']);
+ $this->assertSame('https://idp.example.com/oauth2/userinfo', $oidcEntry['userInfoUrl']);
+
+ // Cleanup
+ $this->updateOAuth2('oidc', [
+ 'clientId' => '',
+ 'clientSecret' => '',
+ 'tokenURL' => '',
+ 'userInfoURL' => '',
+ 'enabled' => false,
+ ]);
+ }
+
// =========================================================================
// Update Okta (clientId + clientSecret + optional domain/authServer)
// =========================================================================
@@ -2413,6 +2564,159 @@ trait OAuth2Base
]);
}
+ // =========================================================================
+ // Update Google (clientId + clientSecret + optional prompt)
+ // =========================================================================
+
+ /**
+ * Default prompt MUST run before any other Google test that sets a custom
+ * prompt value. The global resetProjectOAuth2() only clears Amazon state,
+ * so Google state leaks across tests in the same class. Running this first
+ * guarantees the stored JSON blob has no pre-existing "prompt" key.
+ */
+ public function testUpdateOAuth2GoogleDefaultPrompt(): void
+ {
+ // When prompt is omitted and nothing is stored, the default is ['consent'].
+ $response = $this->updateOAuth2('google', [
+ 'clientId' => 'google-default-client',
+ 'clientSecret' => 'google-default-secret',
+ 'enabled' => false,
+ ]);
+
+ $this->assertSame(200, $response['headers']['status-code']);
+ $this->assertSame(['consent'], $response['body']['prompt']);
+
+ // Cleanup
+ $this->updateOAuth2('google', [
+ 'clientId' => '',
+ 'clientSecret' => '',
+ 'enabled' => false,
+ ]);
+ }
+
+ public function testUpdateOAuth2Google(): void
+ {
+ $response = $this->updateOAuth2('google', [
+ 'clientId' => '120000000095-92ifjb00000000000000000000g7ijfb.apps.googleusercontent.com',
+ 'clientSecret' => 'GOCSPX-2k8gsR0000000000000000VNahJj',
+ 'prompt' => ['select_account'],
+ 'enabled' => false,
+ ]);
+
+ $this->assertSame(200, $response['headers']['status-code']);
+ $this->assertSame('google', $response['body']['$id']);
+ $this->assertSame('120000000095-92ifjb00000000000000000000g7ijfb.apps.googleusercontent.com', $response['body']['clientId']);
+ $this->assertSame(['select_account'], $response['body']['prompt']);
+
+ // Cleanup
+ $this->updateOAuth2('google', [
+ 'clientId' => '',
+ 'clientSecret' => '',
+ 'enabled' => false,
+ ]);
+ }
+
+ public function testUpdateOAuth2GooglePartialPreservesPrompt(): void
+ {
+ // Seed clientSecret + prompt.
+ $this->updateOAuth2('google', [
+ 'clientId' => 'google-seed-client',
+ 'clientSecret' => 'google-seed-secret',
+ 'prompt' => ['consent', 'select_account'],
+ 'enabled' => false,
+ ]);
+
+ // Update only clientId.
+ $response = $this->updateOAuth2('google', [
+ 'clientId' => 'google-rotated-client',
+ ]);
+
+ $this->assertSame(200, $response['headers']['status-code']);
+ $this->assertSame('google-rotated-client', $response['body']['clientId']);
+ $this->assertSame(['consent', 'select_account'], $response['body']['prompt']);
+
+ // Cleanup
+ $this->updateOAuth2('google', [
+ 'clientId' => '',
+ 'clientSecret' => '',
+ 'enabled' => false,
+ ]);
+ }
+
+ public function testUpdateOAuth2GooglePromptNoneAloneRejected(): void
+ {
+ $response = $this->updateOAuth2('google', [
+ 'clientId' => 'whatever',
+ 'clientSecret' => 'whatever',
+ 'prompt' => ['none', 'consent'],
+ 'enabled' => false,
+ ]);
+
+ $this->assertSame(400, $response['headers']['status-code']);
+ $this->assertSame('general_argument_invalid', $response['body']['type']);
+ }
+
+ public function testUpdateOAuth2GooglePromptEmptyArrayRejected(): void
+ {
+ $response = $this->updateOAuth2('google', [
+ 'clientId' => 'whatever',
+ 'clientSecret' => 'whatever',
+ 'prompt' => [],
+ 'enabled' => false,
+ ]);
+
+ $this->assertSame(400, $response['headers']['status-code']);
+ $this->assertSame('general_argument_invalid', $response['body']['type']);
+ }
+
+ public function testUpdateOAuth2GooglePromptNoneAloneAccepted(): void
+ {
+ $response = $this->updateOAuth2('google', [
+ 'clientId' => '120000000095-92ifjb00000000000000000000g7ijfb.apps.googleusercontent.com',
+ 'clientSecret' => 'GOCSPX-2k8gsR0000000000000000VNahJj',
+ 'prompt' => ['none'],
+ 'enabled' => false,
+ ]);
+
+ $this->assertSame(200, $response['headers']['status-code']);
+ $this->assertSame(['none'], $response['body']['prompt']);
+
+ // Cleanup
+ $this->updateOAuth2('google', [
+ 'clientId' => '',
+ 'clientSecret' => '',
+ 'enabled' => false,
+ ]);
+ }
+
+ public function testUpdateOAuth2GoogleEnableAndReadBack(): void
+ {
+ $update = $this->updateOAuth2('google', [
+ 'clientId' => 'google-enable-client',
+ 'clientSecret' => 'google-enable-secret',
+ 'prompt' => ['select_account'],
+ 'enabled' => true,
+ ]);
+
+ $this->assertSame(200, $update['headers']['status-code']);
+ $this->assertTrue($update['body']['enabled']);
+
+ // GET must hide clientSecret while keeping clientId and prompt.
+ $get = $this->getOAuth2Provider('google');
+ $this->assertSame(200, $get['headers']['status-code']);
+ $this->assertTrue($get['body']['enabled']);
+ $this->assertSame('google-enable-client', $get['body']['clientId']);
+ $this->assertSame(['select_account'], $get['body']['prompt']);
+ $this->assertSame('', $get['body']['clientSecret']);
+
+ // Cleanup
+ $this->updateOAuth2('google', [
+ 'clientId' => '',
+ 'clientSecret' => '',
+ 'enabled' => false,
+ ]);
+ }
+
// =========================================================================
// Smoke test: every plain (clientId + clientSecret) provider
//
@@ -2573,7 +2877,7 @@ trait OAuth2Base
);
}
- protected function getOAuth2Provider(string $provider, bool $authenticated = true): mixed
+ protected function getOAuth2Provider(string $providerId, bool $authenticated = true): mixed
{
$headers = [
'content-type' => 'application/json',
@@ -2586,13 +2890,23 @@ trait OAuth2Base
return $this->client->call(
Client::METHOD_GET,
- '/project/oauth2/' . $provider,
+ '/project/oauth2/' . $providerId,
$headers,
);
}
- protected function listOAuth2Providers(bool $authenticated = true): mixed
+ protected function listOAuth2Providers(?array $queries = null, ?bool $total = null, bool $authenticated = true): mixed
{
+ $params = [];
+
+ if ($queries !== null) {
+ $params['queries'] = $queries;
+ }
+
+ if ($total !== null) {
+ $params['total'] = $total;
+ }
+
$headers = [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
@@ -2606,6 +2920,7 @@ trait OAuth2Base
Client::METHOD_GET,
'/project/oauth2',
$headers,
+ $params,
);
}
}
diff --git a/tests/e2e/Services/Project/OAuthGitHubIntegrationTest.php b/tests/e2e/Services/Project/OAuthGitHubIntegrationTest.php
index f86557a432..7b0479ea09 100644
--- a/tests/e2e/Services/Project/OAuthGitHubIntegrationTest.php
+++ b/tests/e2e/Services/Project/OAuthGitHubIntegrationTest.php
@@ -28,6 +28,7 @@ class OAuthGitHubIntegrationTest extends Scope
'content-type' => 'application/json',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
'x-appwrite-project' => 'console',
+ 'x-appwrite-response-format' => '1.9.4',
];
// Step 1: Create new organization (team)
diff --git a/tests/e2e/Services/Project/PoliciesBase.php b/tests/e2e/Services/Project/PoliciesBase.php
index 04906c6c2b..43c09b55c3 100644
--- a/tests/e2e/Services/Project/PoliciesBase.php
+++ b/tests/e2e/Services/Project/PoliciesBase.php
@@ -1082,7 +1082,9 @@ trait PoliciesBase
];
if ($authenticated) {
- $headers = array_merge($headers, $this->getHeaders());
+ $headers = array_merge($headers, $this->getHeaders(), [
+ 'x-appwrite-response-format' => '1.9.4',
+ ]);
}
return $headers;
@@ -1094,6 +1096,7 @@ trait PoliciesBase
'content-type' => 'application/json',
'x-appwrite-project' => 'console',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
+ 'x-appwrite-response-format' => '1.9.4',
]);
}
diff --git a/tests/e2e/Services/Project/PoliciesMembershipPrivacyIntegrationTest.php b/tests/e2e/Services/Project/PoliciesMembershipPrivacyIntegrationTest.php
index 378cd1800d..95d96c49e2 100644
--- a/tests/e2e/Services/Project/PoliciesMembershipPrivacyIntegrationTest.php
+++ b/tests/e2e/Services/Project/PoliciesMembershipPrivacyIntegrationTest.php
@@ -22,6 +22,7 @@ class PoliciesMembershipPrivacyIntegrationTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $apiKey,
+ 'x-appwrite-response-format' => '1.9.4',
];
// Step 1: Configure privacy to false
diff --git a/tests/e2e/Services/Project/PoliciesPasswordDictionaryIntegrationTest.php b/tests/e2e/Services/Project/PoliciesPasswordDictionaryIntegrationTest.php
index 2d0e15a70f..0da64eb50b 100644
--- a/tests/e2e/Services/Project/PoliciesPasswordDictionaryIntegrationTest.php
+++ b/tests/e2e/Services/Project/PoliciesPasswordDictionaryIntegrationTest.php
@@ -22,6 +22,7 @@ class PoliciesPasswordDictionaryIntegrationTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $apiKey,
+ 'x-appwrite-response-format' => '1.9.4',
];
// "password" is the top entry in the common-passwords dictionary and is 8 chars (min length).
diff --git a/tests/e2e/Services/Project/PoliciesPasswordHistoryIntegrationTest.php b/tests/e2e/Services/Project/PoliciesPasswordHistoryIntegrationTest.php
index c2dfd7be5e..1027d88222 100644
--- a/tests/e2e/Services/Project/PoliciesPasswordHistoryIntegrationTest.php
+++ b/tests/e2e/Services/Project/PoliciesPasswordHistoryIntegrationTest.php
@@ -22,6 +22,7 @@ class PoliciesPasswordHistoryIntegrationTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $apiKey,
+ 'x-appwrite-response-format' => '1.9.4',
];
// Step 1: Enable password history policy with limit 3
diff --git a/tests/e2e/Services/Project/PoliciesPasswordPersonalDataIntegrationTest.php b/tests/e2e/Services/Project/PoliciesPasswordPersonalDataIntegrationTest.php
index 3284fed16f..ebb94a2631 100644
--- a/tests/e2e/Services/Project/PoliciesPasswordPersonalDataIntegrationTest.php
+++ b/tests/e2e/Services/Project/PoliciesPasswordPersonalDataIntegrationTest.php
@@ -21,6 +21,7 @@ class PoliciesPasswordPersonalDataIntegrationTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $apiKey,
+ 'x-appwrite-response-format' => '1.9.4',
];
$setPersonalData = function (bool $enabled) use ($serverHeaders): void {
diff --git a/tests/e2e/Services/Project/PoliciesSessionAlertIntegrationTest.php b/tests/e2e/Services/Project/PoliciesSessionAlertIntegrationTest.php
index 1500a1dcfa..3e847b9ddb 100644
--- a/tests/e2e/Services/Project/PoliciesSessionAlertIntegrationTest.php
+++ b/tests/e2e/Services/Project/PoliciesSessionAlertIntegrationTest.php
@@ -23,6 +23,7 @@ class PoliciesSessionAlertIntegrationTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $apiKey,
+ 'x-appwrite-response-format' => '1.9.4',
];
$publicHeaders = [
diff --git a/tests/e2e/Services/Project/PoliciesSessionDurationIntegrationTest.php b/tests/e2e/Services/Project/PoliciesSessionDurationIntegrationTest.php
index 71562f52a5..582da65373 100644
--- a/tests/e2e/Services/Project/PoliciesSessionDurationIntegrationTest.php
+++ b/tests/e2e/Services/Project/PoliciesSessionDurationIntegrationTest.php
@@ -22,6 +22,7 @@ class PoliciesSessionDurationIntegrationTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $apiKey,
+ 'x-appwrite-response-format' => '1.9.4',
];
$publicHeaders = [
diff --git a/tests/e2e/Services/Project/PoliciesSessionInvalidationIntegrationTest.php b/tests/e2e/Services/Project/PoliciesSessionInvalidationIntegrationTest.php
index c9de2be9a5..b6f972ca3e 100644
--- a/tests/e2e/Services/Project/PoliciesSessionInvalidationIntegrationTest.php
+++ b/tests/e2e/Services/Project/PoliciesSessionInvalidationIntegrationTest.php
@@ -22,6 +22,7 @@ class PoliciesSessionInvalidationIntegrationTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $apiKey,
+ 'x-appwrite-response-format' => '1.9.4',
];
$publicHeaders = [
diff --git a/tests/e2e/Services/Project/PoliciesUserLimitIntegrationTest.php b/tests/e2e/Services/Project/PoliciesUserLimitIntegrationTest.php
index 5ddcd8aaa1..6025d80536 100644
--- a/tests/e2e/Services/Project/PoliciesUserLimitIntegrationTest.php
+++ b/tests/e2e/Services/Project/PoliciesUserLimitIntegrationTest.php
@@ -22,6 +22,7 @@ class PoliciesUserLimitIntegrationTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $apiKey,
+ 'x-appwrite-response-format' => '1.9.4',
];
$signupHeaders = [
diff --git a/tests/e2e/Services/Project/ProjectConsoleClientTest.php b/tests/e2e/Services/Project/ProjectConsoleClientTest.php
index 0ba69c21b6..a4c8b73efc 100644
--- a/tests/e2e/Services/Project/ProjectConsoleClientTest.php
+++ b/tests/e2e/Services/Project/ProjectConsoleClientTest.php
@@ -51,6 +51,88 @@ class ProjectConsoleClientTest extends Scope
$this->assertSame(404, $getProject['headers']['status-code']);
}
+ public function testGetProject(): void
+ {
+ $team = $this->createTeam('Get Project Team');
+ $project = $this->createProject($team['body']['$id'], 'Get Project');
+
+ $response = $this->client->call(Client::METHOD_GET, '/project', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $project['body']['$id'],
+ ], $this->getHeaders()));
+
+ $this->assertSame(200, $response['headers']['status-code']);
+ $this->assertSame($project['body']['$id'], $response['body']['$id']);
+ $this->assertNotEmpty($response['body']['$createdAt']);
+ $this->assertNotEmpty($response['body']['$updatedAt']);
+ $this->assertSame('Get Project', $response['body']['name']);
+ $this->assertSame($team['body']['$id'], $response['body']['teamId']);
+ $this->assertSame('active', $response['body']['status']);
+
+ // Auth methods
+ $this->assertIsArray($response['body']['authMethods']);
+ $this->assertNotEmpty($response['body']['authMethods']);
+ foreach ($response['body']['authMethods'] as $authMethod) {
+ $this->assertArrayHasKey('$id', $authMethod);
+ $this->assertArrayHasKey('enabled', $authMethod);
+ $this->assertIsBool($authMethod['enabled']);
+ }
+
+ // Services
+ $this->assertIsArray($response['body']['services']);
+ $this->assertNotEmpty($response['body']['services']);
+ foreach ($response['body']['services'] as $service) {
+ $this->assertArrayHasKey('$id', $service);
+ $this->assertArrayHasKey('enabled', $service);
+ $this->assertIsBool($service['enabled']);
+ }
+
+ // Protocols
+ $this->assertIsArray($response['body']['protocols']);
+ $this->assertNotEmpty($response['body']['protocols']);
+ foreach ($response['body']['protocols'] as $protocol) {
+ $this->assertArrayHasKey('$id', $protocol);
+ $this->assertArrayHasKey('enabled', $protocol);
+ $this->assertIsBool($protocol['enabled']);
+ }
+
+ // SMTP defaults
+ $this->assertFalse($response['body']['smtpEnabled']);
+ $this->assertSame('', $response['body']['smtpSenderEmail']);
+ $this->assertSame('', $response['body']['smtpSenderName']);
+ $this->assertSame('', $response['body']['smtpReplyToEmail']);
+ $this->assertSame('', $response['body']['smtpReplyToName']);
+ $this->assertSame('', $response['body']['smtpHost']);
+ $this->assertSame('', $response['body']['smtpPort']);
+ $this->assertSame('', $response['body']['smtpUsername']);
+ $this->assertSame('', $response['body']['smtpPassword']);
+ $this->assertSame('', $response['body']['smtpSecure']);
+
+ // Other fields
+ $this->assertIsArray($response['body']['labels']);
+ $this->assertIsArray($response['body']['devKeys']);
+ $this->assertSame(0, $response['body']['pingCount']);
+ $this->assertSame('', $response['body']['pingedAt']);
+
+ // Ensure old flattened fields are not present
+ $this->assertArrayNotHasKey('description', $response['body']);
+ $this->assertArrayNotHasKey('logo', $response['body']);
+ $this->assertArrayNotHasKey('url', $response['body']);
+ $this->assertArrayNotHasKey('authDuration', $response['body']);
+ $this->assertArrayNotHasKey('authLimit', $response['body']);
+ $this->assertArrayNotHasKey('authSessionsLimit', $response['body']);
+ $this->assertArrayNotHasKey('authPasswordHistory', $response['body']);
+ $this->assertArrayNotHasKey('authPasswordDictionary', $response['body']);
+ $this->assertArrayNotHasKey('authPersonalDataCheck', $response['body']);
+ $this->assertArrayNotHasKey('authDisposableEmails', $response['body']);
+ $this->assertArrayNotHasKey('authCanonicalEmails', $response['body']);
+ $this->assertArrayNotHasKey('authFreeEmails', $response['body']);
+ $this->assertArrayNotHasKey('oAuthProviders', $response['body']);
+ $this->assertArrayNotHasKey('platforms', $response['body']);
+ $this->assertArrayNotHasKey('webhooks', $response['body']);
+ $this->assertArrayNotHasKey('keys', $response['body']);
+ }
+
protected function createTeam(string $name): array
{
$response = $this->client->call(Client::METHOD_POST, '/teams', $this->getConsoleSessionHeaders(), [
diff --git a/tests/e2e/Services/Project/ProtocolsBase.php b/tests/e2e/Services/Project/ProtocolsBase.php
index f828994ea3..092ed97b6a 100644
--- a/tests/e2e/Services/Project/ProtocolsBase.php
+++ b/tests/e2e/Services/Project/ProtocolsBase.php
@@ -248,6 +248,7 @@ trait ProtocolsBase
$headers = array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders());
// Disable via the legacy `/status` alias
@@ -278,7 +279,9 @@ trait ProtocolsBase
];
if ($authenticated) {
- $headers = array_merge($headers, $this->getHeaders());
+ $headers = array_merge($headers, $this->getHeaders(), [
+ 'x-appwrite-response-format' => '1.9.4',
+ ]);
}
return $this->client->call(Client::METHOD_PATCH, '/project/protocols/' . $protocolId, $headers, [
diff --git a/tests/e2e/Services/Project/ServicesBase.php b/tests/e2e/Services/Project/ServicesBase.php
index b5f94f8181..0c8b337315 100644
--- a/tests/e2e/Services/Project/ServicesBase.php
+++ b/tests/e2e/Services/Project/ServicesBase.php
@@ -246,6 +246,7 @@ trait ServicesBase
$headers = array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders());
// Disable via the legacy `/status` alias
@@ -276,7 +277,9 @@ trait ServicesBase
];
if ($authenticated) {
- $headers = array_merge($headers, $this->getHeaders());
+ $headers = array_merge($headers, $this->getHeaders(), [
+ 'x-appwrite-response-format' => '1.9.4',
+ ]);
}
return $this->client->call(Client::METHOD_PATCH, '/project/services/' . $serviceId, $headers, [
diff --git a/tests/e2e/Services/ProjectWebhooks/WebhooksCustomServerTest.php b/tests/e2e/Services/ProjectWebhooks/WebhooksCustomServerTest.php
index 9085733b70..eb08da56f2 100644
--- a/tests/e2e/Services/ProjectWebhooks/WebhooksCustomServerTest.php
+++ b/tests/e2e/Services/ProjectWebhooks/WebhooksCustomServerTest.php
@@ -89,6 +89,7 @@ class WebhooksCustomServerTest extends Scope
$this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/variables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.3',
], $this->getHeaders()), [
'key' => 'key1',
'value' => 'value1',
@@ -699,6 +700,7 @@ class WebhooksCustomServerTest extends Scope
$variable = $this->client->call(Client::METHOD_POST, '/functions/' . $id . '/variables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.3',
], $this->getHeaders()), [
'key' => 'key1',
'value' => 'value1',
diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php
index 6936de9aff..aa5e6911f1 100644
--- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php
+++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php
@@ -46,6 +46,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4'
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Project Test',
@@ -68,6 +69,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Project Test',
@@ -89,6 +91,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => '',
@@ -101,6 +104,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Project Test',
@@ -127,6 +131,7 @@ class ProjectsConsoleClientTest extends Scope
$project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'MultiDB Project',
@@ -210,6 +215,7 @@ class ProjectsConsoleClientTest extends Scope
$getProject = $this->client->call(Client::METHOD_GET, '/projects/' . $projectId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()));
$this->assertEquals(404, $getProject['headers']['status-code']);
@@ -234,6 +240,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'projectId' => $projectId,
'name' => 'Original Project',
@@ -249,6 +256,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'projectId' => $projectId,
'name' => 'Project Duplicate',
@@ -298,6 +306,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Team 1 Project',
@@ -318,6 +327,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $projectId . '/team', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'teamId' => $team2,
]);
@@ -341,6 +351,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
@@ -353,6 +364,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders(), [
'search' => $id
]));
@@ -364,6 +376,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders(), [
'search' => 'Project Test'
]));
@@ -390,6 +403,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Project Test 2',
@@ -408,6 +422,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'queries' => [
Query::equal('teamId', [$team['body']['$id']])->toString(),
@@ -422,6 +437,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'queries' => [
Query::limit(1)->toString(),
@@ -435,6 +451,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'queries' => [
Query::offset(1)->toString(),
@@ -447,6 +464,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'queries' => [
Query::equal('name', ['Project Test 2'])->toString(),
@@ -461,6 +479,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'queries' => [
Query::orderDesc()->toString(),
@@ -474,6 +493,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
@@ -483,6 +503,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'queries' => [
Query::cursorAfter(new Document(['$id' => $response['body']['projects'][0]['$id']]))->toString(),
@@ -498,6 +519,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'queries' => [
Query::cursorAfter(new Document(['$id' => 'unknown']))->toString(),
@@ -524,6 +546,7 @@ class ProjectsConsoleClientTest extends Scope
$project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Query Select Test Project',
@@ -540,6 +563,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'queries' => [
Query::select(['$id', 'name'])->toString(),
@@ -569,6 +593,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4'
], $this->getHeaders()), [
'queries' => [
Query::select(['$id', 'name', 'teamId', 'description', '$createdAt', '$updatedAt'])->toString(),
@@ -600,6 +625,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'queries' => [
Query::select(['$id', 'name', 'teamId'])->toString(),
@@ -631,6 +657,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'queries' => [
Query::select(['$id', 'name'])->toString(),
@@ -661,6 +688,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'queries' => [
Query::select(['$id', 'name', 'platforms'])->toString(),
@@ -690,6 +718,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'queries' => [
Query::select(['$id', 'name', 'webhooks', 'keys'])->toString(),
@@ -719,6 +748,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'queries' => [
Query::select(['*'])->toString(),
@@ -749,6 +779,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'queries' => [
Query::select(['$id', 'invalidAttribute'])->toString(),
@@ -783,35 +814,709 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Project Test',
'teamId' => $team['body']['$id'],
- 'region' => System::getEnv('_APP_REGION', 'default')
+ 'region' => System::getEnv('_APP_REGION', 'default'),
+ 'description' => 'My description',
+ 'logo' => 'https://google.com/logo.png',
+ 'url' => 'https://myapp.com/',
+ 'legalName' => 'Legal company',
+ 'legalCountry' => 'Slovakia',
+ 'legalState' => 'Custom state',
+ 'legalCity' => 'KoÅĄice',
+ 'legalAddress' => 'Main street 32',
+ 'legalTaxId' => 'TAXID_123456'
]);
$this->assertEquals(201, $response['headers']['status-code']);
$id = $response['body']['$id'];
+ // Increase ping 3x
+ for ($i = 0; $i < 3; $i++) {
+ $response = $this->client->call(
+ Client::METHOD_GET,
+ '/ping',
+ array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $id,
+ ], $this->getHeaders()),
+ );
+ $this->assertEquals(200, $response['headers']['status-code']);
+ }
+
+ // Configure SMTP
+ $response = $this->client->call(
+ Client::METHOD_PATCH,
+ '/project/smtp',
+ array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $id,
+ 'x-appwrite-mode' => 'admin',
+ ], $this->getHeaders()),
+ [
+ 'enabled' => true,
+ 'senderName' => 'Custom sender',
+ 'senderEmail' => 'email@custom.com',
+ 'host' => 'maildev',
+ 'port' => 1025,
+ 'replyToEmail' => 'replyto@custom.com',
+ 'replyToName' => 'Reply sender',
+ ],
+ );
+ $this->assertEquals(200, $response['headers']['status-code']);
+
+ // Add mock numbers
+ $response = $this->client->call(Client::METHOD_POST, '/project/mock-phones', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $id,
+ 'x-appwrite-mode' => 'admin',
+ ], $this->getHeaders()), [
+ 'number' => '+421123456789',
+ 'otp' => '123456'
+ ]);
+ $this->assertEquals(201, $response['headers']['status-code']);
+
+ // Add labels
+ $response = $this->client->call(Client::METHOD_PUT, '/project/labels', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $id,
+ 'x-appwrite-mode' => 'admin',
+ ], $this->getHeaders()), [
+ 'labels' => ['custom1', 'custom2']
+ ]);
+ $this->assertEquals(200, $response['headers']['status-code']);
+
+ // Create dev keys
+ $response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/dev-keys', array_merge([
+ 'content-type' => 'application/json',
+ ], $this->getHeaders()), [
+ 'name' => 'Custom key 1',
+ 'expire' => '2099-05-07 09:23:30.713',
+ ]);
+ $this->assertEquals(201, $response['headers']['status-code']);
+
+ $response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/dev-keys', array_merge([
+ 'content-type' => 'application/json',
+ ], $this->getHeaders()), [
+ 'name' => 'Custom key 2',
+ 'expire' => '2099-05-07 11:23:30.713'
+ ]);
+ $this->assertEquals(201, $response['headers']['status-code']);
+
+ $response = $this->client->call(Client::METHOD_POST, '/project/mock-phones', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $id,
+ 'x-appwrite-mode' => 'admin',
+ ], $this->getHeaders()), [
+ 'number' => '+420987654321',
+ 'otp' => '654321'
+ ]);
+ $this->assertEquals(201, $response['headers']['status-code']);
+
+ // Setup custom values for project policies
+ $response = $this->client->call(Client::METHOD_PATCH, '/project/policies/session-duration', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $id,
+ 'x-appwrite-mode' => 'admin',
+ ], $this->getHeaders()), [
+ 'duration' => 135
+ ]);
+ $this->assertEquals(200, $response['headers']['status-code']);
+
+ $response = $this->client->call(Client::METHOD_PATCH, '/project/policies/user-limit', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $id,
+ 'x-appwrite-mode' => 'admin',
+ ], $this->getHeaders()), [
+ 'total' => 54
+ ]);
+ $this->assertEquals(200, $response['headers']['status-code']);
+
+ $response = $this->client->call(Client::METHOD_PATCH, '/project/policies/session-limit', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $id,
+ 'x-appwrite-mode' => 'admin',
+ ], $this->getHeaders()), [
+ 'total' => 7
+ ]);
+ $this->assertEquals(200, $response['headers']['status-code']);
+
+ $response = $this->client->call(Client::METHOD_PATCH, '/project/policies/password-history', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $id,
+ 'x-appwrite-mode' => 'admin',
+ ], $this->getHeaders()), [
+ 'total' => 9
+ ]);
+ $this->assertEquals(200, $response['headers']['status-code']);
+
+ $response = $this->client->call(Client::METHOD_PATCH, '/project/policies/password-dictionary', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $id,
+ 'x-appwrite-mode' => 'admin',
+ ], $this->getHeaders()), [
+ 'enabled' => true
+ ]);
+ $this->assertEquals(200, $response['headers']['status-code']);
+
+ $response = $this->client->call(Client::METHOD_PATCH, '/project/policies/password-personal-data', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $id,
+ 'x-appwrite-mode' => 'admin',
+ ], $this->getHeaders()), [
+ 'enabled' => true
+ ]);
+ $this->assertEquals(200, $response['headers']['status-code']);
+
+ $response = $this->client->call(Client::METHOD_PATCH, '/project/policies/session-alert', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $id,
+ 'x-appwrite-mode' => 'admin',
+ ], $this->getHeaders()), [
+ 'enabled' => true
+ ]);
+ $this->assertEquals(200, $response['headers']['status-code']);
+
+ $response = $this->client->call(Client::METHOD_PATCH, '/project/policies/membership-privacy', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $id,
+ 'x-appwrite-mode' => 'admin',
+ ], $this->getHeaders()), [
+ 'userId' => true,
+ 'userEmail' => true,
+ 'userPhone' => true,
+ 'userName' => true,
+ 'userMFA' => true,
+ ]);
+ $this->assertEquals(200, $response['headers']['status-code']);
+
+ $response = $this->client->call(Client::METHOD_PATCH, '/project/policies/session-invalidation', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $id,
+ 'x-appwrite-mode' => 'admin',
+ ], $this->getHeaders()), [
+ 'enabled' => true
+ ]);
+ $this->assertEquals(200, $response['headers']['status-code']);
+
+ // Create webhook
+ $webhook = $this->client->call(Client::METHOD_POST, '/webhooks', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $id,
+ 'x-appwrite-mode' => 'admin',
+ ], $this->getHeaders()), [
+ 'webhookId' => 'unique()',
+ 'name' => 'Webhook Test',
+ 'events' => ['users.*.create', 'users.*.update.email'],
+ 'url' => 'https://appwrite.io',
+ 'tls' => true,
+ 'authUsername' => 'username',
+ 'authPassword' => 'password',
+ ]);
+ $this->assertEquals(201, $webhook['headers']['status-code']);
+
+ // Create API key
+ $key = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/keys', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'keyId' => ID::unique(),
+ 'name' => 'Key Test',
+ 'scopes' => ['teams.read', 'teams.write'],
+ ]);
+ $this->assertEquals(201, $key['headers']['status-code']);
+
+ // Create platform
+ $platform = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/platforms', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'platformId' => ID::unique(),
+ 'type' => 'web',
+ 'name' => 'Web App',
+ 'hostname' => 'localhost',
+ ]);
+ $this->assertEquals(201, $platform['headers']['status-code']);
+
+ // Configure OAuth provider
+ $oauth = $this->client->call(Client::METHOD_PATCH, '/project/oauth2/github', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $id,
+ 'x-appwrite-mode' => 'admin',
+ ], $this->getHeaders()), [
+ 'clientId' => 'github-client-id',
+ 'clientSecret' => 'github-client-secret',
+ 'enabled' => false,
+ ]);
+ $this->assertEquals(200, $oauth['headers']['status-code']);
+
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
- $this->assertNotEmpty($response['body']);
$this->assertEquals($id, $response['body']['$id']);
$this->assertEquals('Project Test', $response['body']['name']);
+ $this->assertIsString($response['body']['$createdAt']);
+ $this->assertNotEmpty($response['body']['$createdAt']);
+ $this->assertNotFalse(\strtotime($response['body']['$createdAt']));
+
+ $this->assertIsString($response['body']['$updatedAt']);
+ $this->assertNotEmpty($response['body']['$updatedAt']);
+ $this->assertNotFalse(\strtotime($response['body']['$updatedAt']));
+
+ // $this->assertEquals('My description', $response['body']['description']); // No longer supported
+ $this->assertEquals($team['body']['$id'], $response['body']['teamId']);
+ $this->assertEquals('active', $response['body']['status']);
+ // $this->assertEquals('https://google.com/logo.png', $response['body']['logo']); // No longer supported
+ // $this->assertEquals('https://myapp.com/', $response['body']['url']); // No longer supported
+ // $this->assertEquals('Legal company', $response['body']['legalName']); // No longer supported
+ // $this->assertEquals('Slovakia', $response['body']['legalCountry']); // No longer supported
+ // $this->assertEquals('Custom state', $response['body']['legalState']); // No longer supported
+ // $this->assertEquals('KoÅĄice', $response['body']['legalCity']); // No longer supported
+ // $this->assertEquals('Main street 32', $response['body']['legalAddress']); // No longer supported
+ // $this->assertEquals('TAXID_123456', $response['body']['legalTaxId']); // No longer supported
+ $this->assertEquals(135, $response['body']['authDuration']);
+ $this->assertEquals(54, $response['body']['authLimit']);
+ $this->assertEquals(7, $response['body']['authSessionsLimit']);
+ $this->assertEquals(9, $response['body']['authPasswordHistory']);
+ $this->assertTrue($response['body']['authPasswordDictionary']);
+ $this->assertTrue($response['body']['authPersonalDataCheck']);
+ $this->assertFalse($response['body']['authDisposableEmails']);
+ $this->assertFalse($response['body']['authCanonicalEmails']);
+ $this->assertFalse($response['body']['authFreeEmails']);
+ $this->assertTrue($response['body']['authSessionAlerts']);
+ $this->assertTrue($response['body']['authMembershipsUserName']);
+ $this->assertTrue($response['body']['authMembershipsUserEmail']);
+ $this->assertTrue($response['body']['authMembershipsMfa']);
+ $this->assertTrue($response['body']['authMembershipsUserId']);
+ $this->assertTrue($response['body']['authMembershipsUserPhone']);
+ $this->assertTrue($response['body']['authInvalidateSessions']);
+ $this->assertTrue($response['body']['smtpEnabled']);
+ $this->assertSame('Custom sender', $response['body']['smtpSenderName']);
+ $this->assertSame('email@custom.com', $response['body']['smtpSenderEmail']);
+ $this->assertSame('Reply sender', $response['body']['smtpReplyToName']);
+ $this->assertSame('replyto@custom.com', $response['body']['smtpReplyToEmail']);
+ $this->assertSame('maildev', $response['body']['smtpHost']);
+ $this->assertSame(1025, $response['body']['smtpPort']);
+ $this->assertSame('', $response['body']['smtpUsername']);
+ $this->assertSame('', $response['body']['smtpPassword']); // Write only
+ $this->assertSame('', $response['body']['smtpSecure']);
+ $this->assertSame(3, $response['body']['pingCount']);
+
+ $this->assertIsString($response['body']['pingedAt']);
+ $this->assertNotEmpty($response['body']['pingedAt']);
+ $this->assertNotFalse(\strtotime($response['body']['pingedAt']));
+
+ $this->assertCount(2, $response['body']['labels']);
+ $this->assertEquals('custom1', $response['body']['labels'][0]);
+ $this->assertEquals('custom2', $response['body']['labels'][1]);
+
+ $this->assertCount(2, $response['body']['devKeys']);
+ $this->assertEquals('Custom key 1', $response['body']['devKeys'][0]['name']);
+ $this->assertEquals('Custom key 2', $response['body']['devKeys'][1]['name']);
+ $this->assertEquals('2099-05-07T09:23:30.713+00:00', $response['body']['devKeys'][0]['expire']);
+ $this->assertEquals('2099-05-07T11:23:30.713+00:00', $response['body']['devKeys'][1]['expire']);
+
+ foreach ($response['body']['devKeys'] as $devKey) {
+ $this->assertIsString($devKey['$id']);
+ $this->assertNotEmpty($devKey['$id']);
+
+ $this->assertIsString($devKey['secret']);
+ $this->assertNotEmpty($devKey['secret']);
+
+ $this->assertIsString($devKey['accessedAt']);
+ $this->assertEmpty($devKey['accessedAt']);
+
+ $this->assertIsString($devKey['$createdAt']);
+ $this->assertNotEmpty($devKey['$createdAt']);
+ $this->assertNotFalse(\strtotime($devKey['$createdAt']));
+
+ $this->assertIsString($devKey['$updatedAt']);
+ $this->assertNotEmpty($devKey['$updatedAt']);
+ $this->assertNotFalse(\strtotime($devKey['$updatedAt']));
+
+ $this->assertIsArray($devKey['sdks']);
+ $this->assertCount(0, $devKey['sdks']);
+ }
+
+ $this->assertCount(2, $response['body']['authMockNumbers']);
+ $this->assertEquals('+421123456789', $response['body']['authMockNumbers'][0]['phone']);
+ $this->assertEquals('+420987654321', $response['body']['authMockNumbers'][1]['phone']);
+ $this->assertEquals('123456', $response['body']['authMockNumbers'][0]['otp']);
+ $this->assertEquals('654321', $response['body']['authMockNumbers'][1]['otp']);
+
+ foreach ($response['body']['authMockNumbers'] as $mockNumber) {
+ $this->assertIsString($mockNumber['$createdAt']);
+ $this->assertNotEmpty($mockNumber['$createdAt']);
+ $this->assertNotFalse(\strtotime($mockNumber['$createdAt']));
+
+ $this->assertIsString($mockNumber['$updatedAt']);
+ $this->assertNotEmpty($mockNumber['$updatedAt']);
+ $this->assertNotFalse(\strtotime($mockNumber['$updatedAt']));
+
+ $this->assertIsString($mockNumber['phone']);
+ $this->assertNotEmpty($mockNumber['phone']);
+
+ $this->assertIsString($mockNumber['otp']);
+ $this->assertNotEmpty($mockNumber['otp']);
+ }
+
+ $this->assertIsArray($response['body']['oAuthProviders']);
+ $this->assertGreaterThan(0, count($response['body']['oAuthProviders']));
+
+ $githubProvider = null;
+ foreach ($response['body']['oAuthProviders'] as $provider) {
+ $this->assertIsString($provider['key']);
+ $this->assertNotEmpty($provider['key']);
+
+ $this->assertIsString($provider['name']);
+ $this->assertIsString($provider['appId']);
+ $this->assertIsString($provider['secret']);
+ $this->assertIsBool($provider['enabled']);
+
+ if ($provider['key'] === 'github') {
+ $githubProvider = $provider;
+ }
+ }
+
+ $this->assertNotNull($githubProvider, 'GitHub provider not found');
+ $this->assertEquals('github-client-id', $githubProvider['appId']);
+ $this->assertEquals('', $githubProvider['secret']); // Write only
+ $this->assertEquals(false, $githubProvider['enabled']);
+
+ $this->assertIsArray($response['body']['platforms']);
+ $this->assertCount(1, $response['body']['platforms']);
+ $this->assertIsString($response['body']['platforms'][0]['$id']);
+ $this->assertNotEmpty($response['body']['platforms'][0]['$id']);
+ $this->assertEquals('Web App', $response['body']['platforms'][0]['name']);
+ $this->assertEquals('web', $response['body']['platforms'][0]['type']);
+ $this->assertEquals('localhost', $response['body']['platforms'][0]['hostname']);
+
+ $this->assertIsString($response['body']['platforms'][0]['$createdAt']);
+ $this->assertNotEmpty($response['body']['platforms'][0]['$createdAt']);
+ $this->assertNotFalse(\strtotime($response['body']['platforms'][0]['$createdAt']));
+
+ $this->assertIsString($response['body']['platforms'][0]['$updatedAt']);
+ $this->assertNotEmpty($response['body']['platforms'][0]['$updatedAt']);
+ $this->assertNotFalse(\strtotime($response['body']['platforms'][0]['$updatedAt']));
+
+ $this->assertArrayHasKey('webhooks', $response['body']);
+ $this->assertIsArray($response['body']['webhooks']);
+ $this->assertCount(1, $response['body']['webhooks']);
+ $this->assertIsString($response['body']['webhooks'][0]['$id']);
+ $this->assertNotEmpty($response['body']['webhooks'][0]['$id']);
+ $this->assertEquals('Webhook Test', $response['body']['webhooks'][0]['name']);
+ $this->assertEquals('https://appwrite.io', $response['body']['webhooks'][0]['url']);
+ $this->assertContains('users.*.create', $response['body']['webhooks'][0]['events']);
+ $this->assertContains('users.*.update.email', $response['body']['webhooks'][0]['events']);
+ $this->assertCount(2, $response['body']['webhooks'][0]['events']);
+ $this->assertTrue($response['body']['webhooks'][0]['tls']);
+ $this->assertEquals('username', $response['body']['webhooks'][0]['authUsername']);
+ $this->assertEquals('password', $response['body']['webhooks'][0]['authPassword']);
+ $this->assertTrue($response['body']['webhooks'][0]['enabled']);
+ $this->assertIsString($response['body']['webhooks'][0]['secret']);
+ $this->assertNotEmpty($response['body']['webhooks'][0]['secret']);
+ $this->assertIsString($response['body']['webhooks'][0]['$createdAt']);
+ $this->assertNotEmpty($response['body']['webhooks'][0]['$createdAt']);
+ $this->assertNotFalse(\strtotime($response['body']['webhooks'][0]['$createdAt']));
+ $this->assertIsString($response['body']['webhooks'][0]['$updatedAt']);
+ $this->assertNotEmpty($response['body']['webhooks'][0]['$updatedAt']);
+ $this->assertNotFalse(\strtotime($response['body']['webhooks'][0]['$updatedAt']));
+
+ $this->assertArrayHasKey('keys', $response['body']);
+ $this->assertIsArray($response['body']['keys']);
+ $this->assertCount(1, $response['body']['keys']);
+ $this->assertIsString($response['body']['keys'][0]['$id']);
+ $this->assertNotEmpty($response['body']['keys'][0]['$id']);
+ $this->assertEquals('Key Test', $response['body']['keys'][0]['name']);
+ $this->assertContains('teams.read', $response['body']['keys'][0]['scopes']);
+ $this->assertContains('teams.write', $response['body']['keys'][0]['scopes']);
+ $this->assertCount(2, $response['body']['keys'][0]['scopes']);
+ $this->assertNotEmpty($response['body']['keys'][0]['secret']);
+ $this->assertEmpty($response['body']['keys'][0]['accessedAt']);
+ $this->assertIsArray($response['body']['keys'][0]['sdks']);
+ $this->assertCount(0, $response['body']['keys'][0]['sdks']);
+ $this->assertIsString($response['body']['keys'][0]['$createdAt']);
+ $this->assertNotEmpty($response['body']['keys'][0]['$createdAt']);
+ $this->assertNotFalse(\strtotime($response['body']['keys'][0]['$createdAt']));
+ $this->assertIsString($response['body']['keys'][0]['$updatedAt']);
+ $this->assertNotEmpty($response['body']['keys'][0]['$updatedAt']);
+ $this->assertNotFalse(\strtotime($response['body']['keys'][0]['$updatedAt']));
+
+ $authsKeys = [
+ 'authEmailPassword',
+ 'authUsersAuthMagicURL',
+ 'authEmailOtp',
+ 'authAnonymous',
+ 'authInvites',
+ 'authJWT',
+ 'authPhone',
+ ];
+ foreach ($authsKeys as $authsKey) {
+ $this->assertTrue($response['body'][$authsKey], 'Auth method should be enabled: ' . $authsKey);
+ }
+
+ $serviceKeys = [
+ 'serviceStatusForAccount',
+ 'serviceStatusForAvatars',
+ 'serviceStatusForDatabases',
+ 'serviceStatusForTablesdb',
+ 'serviceStatusForLocale',
+ 'serviceStatusForHealth',
+ 'serviceStatusForProject',
+ 'serviceStatusForStorage',
+ 'serviceStatusForTeams',
+ 'serviceStatusForUsers',
+ 'serviceStatusForVcs',
+ 'serviceStatusForSites',
+ 'serviceStatusForFunctions',
+ 'serviceStatusForProxy',
+ 'serviceStatusForGraphql',
+ 'serviceStatusForMigrations',
+ 'serviceStatusForMessaging',
+ ];
+ foreach ($serviceKeys as $serviceKey) {
+ $this->assertTrue($response['body'][$serviceKey], 'Service should be enabled: ' . $serviceKey);
+ }
+
+ $protocolKeys = [
+ 'protocolStatusForRest',
+ 'protocolStatusForGraphql',
+ 'protocolStatusForWebsocket',
+ ];
+ foreach ($protocolKeys as $protocolKey) {
+ $this->assertTrue($response['body'][$protocolKey], 'Protocol should be enabled: ' . $protocolKey);
+ }
+
+ // Ensure booleans can be falsy
+
+ $response = $this->client->call(Client::METHOD_PATCH, '/project/policies/password-dictionary', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $id,
+ 'x-appwrite-mode' => 'admin',
+ ], $this->getHeaders()), [
+ 'enabled' => false
+ ]);
+ $this->assertEquals(200, $response['headers']['status-code']);
+
+ $response = $this->client->call(Client::METHOD_PATCH, '/project/policies/password-personal-data', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $id,
+ 'x-appwrite-mode' => 'admin',
+ ], $this->getHeaders()), [
+ 'enabled' => false
+ ]);
+ $this->assertEquals(200, $response['headers']['status-code']);
+
+ $response = $this->client->call(Client::METHOD_PATCH, '/project/policies/session-alert', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $id,
+ 'x-appwrite-mode' => 'admin',
+ ], $this->getHeaders()), [
+ 'enabled' => false
+ ]);
+ $this->assertEquals(200, $response['headers']['status-code']);
+
+ $response = $this->client->call(Client::METHOD_PATCH, '/project/policies/membership-privacy', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $id,
+ 'x-appwrite-mode' => 'admin',
+ ], $this->getHeaders()), [
+ 'userId' => false,
+ 'userEmail' => false,
+ 'userPhone' => false,
+ 'userName' => false,
+ 'userMFA' => false,
+ ]);
+ $this->assertEquals(200, $response['headers']['status-code']);
+
+ $response = $this->client->call(Client::METHOD_PATCH, '/project/policies/session-invalidation', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $id,
+ 'x-appwrite-mode' => 'admin',
+ ], $this->getHeaders()), [
+ 'enabled' => false
+ ]);
+ $this->assertEquals(200, $response['headers']['status-code']);
+
+ // Toggle auth methods, services, protocols
+
+ $authMethods = ['email-password', 'magic-url', 'email-otp', 'anonymous', 'invites', 'jwt', 'phone'];
+ foreach ($authMethods as $authMethod) {
+ $response = $this->client->call(
+ Client::METHOD_PATCH,
+ '/project/auth-methods/' . $authMethod,
+ array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $id,
+ 'x-appwrite-mode' => 'admin',
+ ], $this->getHeaders()),
+ [
+ 'enabled' => false,
+ ],
+ );
+ $this->assertEquals(200, $response['headers']['status-code']);
+ }
+
+ $protocols = ['rest', 'graphql', 'websocket'];
+ foreach ($protocols as $protocol) {
+ $response = $this->client->call(
+ Client::METHOD_PATCH,
+ '/project/protocols/' . $protocol,
+ array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $id,
+ 'x-appwrite-mode' => 'admin',
+ ], $this->getHeaders()),
+ [
+ 'enabled' => false,
+ ],
+ );
+ $this->assertEquals(200, $response['headers']['status-code']);
+ }
+
+ $services = [
+ 'account',
+ 'avatars',
+ 'databases',
+ 'tablesdb',
+ 'locale',
+ 'health',
+ 'project',
+ 'storage',
+ 'teams',
+ 'users',
+ 'vcs',
+ 'sites',
+ 'functions',
+ 'proxy',
+ 'graphql',
+ 'migrations',
+ 'messaging',
+ ];
+
+ foreach ($services as $service) {
+ $response = $this->client->call(
+ Client::METHOD_PATCH,
+ '/project/services/' . $service,
+ array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $id,
+ 'x-appwrite-mode' => 'admin',
+ ], $this->getHeaders()),
+ [
+ 'enabled' => false,
+ ],
+ );
+ $this->assertEquals(200, $response['headers']['status-code']);
+ }
+
+ // Configure SMTP
+ $response = $this->client->call(
+ Client::METHOD_PATCH,
+ '/project/smtp',
+ array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $id,
+ 'x-appwrite-mode' => 'admin',
+ ], $this->getHeaders()),
+ [
+ 'enabled' => false,
+ 'host' => 'customhost.com',
+ 'port' => 4444,
+ 'username' => 'myuser',
+ 'password' => 'mypassword',
+ 'secure' => 'ssl',
+ ],
+ );
+ $this->assertEquals(200, $response['headers']['status-code']);
+
+ $response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
+ ], $this->getHeaders()));
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+
+ $this->assertFalse($response['body']['authPasswordDictionary']);
+ $this->assertFalse($response['body']['authPersonalDataCheck']);
+ $this->assertFalse($response['body']['authSessionAlerts']);
+ $this->assertFalse($response['body']['authMembershipsUserName']);
+ $this->assertFalse($response['body']['authMembershipsUserEmail']);
+ $this->assertFalse($response['body']['authMembershipsMfa']);
+ $this->assertFalse($response['body']['authMembershipsUserId']);
+ $this->assertFalse($response['body']['authMembershipsUserPhone']);
+ $this->assertFalse($response['body']['authInvalidateSessions']);
+ $this->assertFalse($response['body']['smtpEnabled']);
+ $this->assertSame('customhost.com', $response['body']['smtpHost']);
+ $this->assertSame(4444, $response['body']['smtpPort']);
+ $this->assertSame('myuser', $response['body']['smtpUsername']);
+ $this->assertSame('', $response['body']['smtpPassword']); // Write only
+ $this->assertSame('ssl', $response['body']['smtpSecure']);
+
+ $authsKeys = [
+ 'authEmailPassword',
+ 'authUsersAuthMagicURL',
+ 'authEmailOtp',
+ 'authAnonymous',
+ 'authInvites',
+ 'authJWT',
+ 'authPhone',
+ ];
+ foreach ($authsKeys as $authsKey) {
+ $this->assertFalse($response['body'][$authsKey], 'Auth method should be disabled: ' . $authsKey);
+ }
+
+ $serviceKeys = [
+ 'serviceStatusForAccount',
+ 'serviceStatusForAvatars',
+ 'serviceStatusForDatabases',
+ 'serviceStatusForTablesdb',
+ 'serviceStatusForLocale',
+ 'serviceStatusForHealth',
+ 'serviceStatusForProject',
+ 'serviceStatusForStorage',
+ 'serviceStatusForTeams',
+ 'serviceStatusForUsers',
+ 'serviceStatusForVcs',
+ 'serviceStatusForSites',
+ 'serviceStatusForFunctions',
+ 'serviceStatusForProxy',
+ 'serviceStatusForGraphql',
+ 'serviceStatusForMigrations',
+ 'serviceStatusForMessaging',
+ ];
+ foreach ($serviceKeys as $serviceKey) {
+ $this->assertFalse($response['body'][$serviceKey], 'Service should be disabled: ' . $serviceKey);
+ }
+
+ $protocolKeys = [
+ 'protocolStatusForRest',
+ 'protocolStatusForGraphql',
+ 'protocolStatusForWebsocket',
+ ];
+ foreach ($protocolKeys as $protocolKey) {
+ $this->assertFalse($response['body'][$protocolKey], 'Protocol should be disabled: ' . $protocolKey);
+ }
+
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/projects/empty', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()));
$this->assertEquals(404, $response['headers']['status-code']);
@@ -820,9 +1525,10 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/'.$projectId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()));
- $this->assertEquals(400, $response['headers']['status-code']);
+ $this->assertEquals(404, $response['headers']['status-code']);
}
public function testGetProjectUsage(): void
@@ -850,6 +1556,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Project Test',
@@ -866,6 +1573,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Project Test 2',
@@ -886,6 +1594,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => '',
@@ -910,6 +1619,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/smtp', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'enabled' => true,
'senderEmail' => 'mailer@appwrite.io',
@@ -935,6 +1645,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
@@ -954,6 +1665,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/smtp', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'enabled' => true,
'senderEmail' => 'fail@appwrite.io',
@@ -993,6 +1705,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Project Test',
@@ -1181,6 +1894,7 @@ class ProjectsConsoleClientTest extends Scope
$project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Session Alert Locale Fallback Test',
@@ -1194,6 +1908,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $projectId . '/smtp', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'enabled' => true,
'senderEmail' => 'mailer@appwrite.io',
@@ -1262,6 +1977,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $projectId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertTrue($response['body']['authSessionAlerts']);
@@ -1384,6 +2100,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => 'console',
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
@@ -1496,6 +2213,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $projectId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => 'console',
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
@@ -1519,6 +2237,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Session Invalidation Test Project',
@@ -1533,6 +2252,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => 'console',
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
@@ -1550,6 +2270,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => 'console',
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
@@ -1567,6 +2288,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => 'console',
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
@@ -1591,6 +2313,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Project Test',
@@ -1610,6 +2333,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/oauth2', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'provider' => $key,
'appId' => 'AppId-' . ucfirst($key),
@@ -1623,6 +2347,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
@@ -1650,6 +2375,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/oauth2', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'provider' => $key,
'enabled' => $i === 0 ? false : true // On first provider, test enabled=false
@@ -1664,6 +2390,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
@@ -1693,6 +2420,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/oauth2', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'provider' => 'unknown',
'appId' => 'AppId',
@@ -1720,6 +2448,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Project Test',
@@ -1775,6 +2504,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
@@ -2185,6 +2915,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/mock-numbers', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), []);
$this->assertEquals(400, $response['headers']['status-code']);
@@ -2194,6 +2925,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/mock-numbers', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'numbers' => [
'phone' => '+1655513432',
@@ -2207,6 +2939,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/mock-numbers', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'numbers' => [
[
@@ -2222,6 +2955,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/mock-numbers', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'numbers' => [
[
@@ -2237,6 +2971,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/mock-numbers', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'numbers' => [
[
@@ -2252,6 +2987,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/mock-numbers', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'numbers' => [
[
@@ -2267,6 +3003,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/mock-numbers', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'numbers' => [
[
@@ -2282,6 +3019,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/mock-numbers', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'numbers' => [
[
@@ -2309,6 +3047,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/mock-numbers', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'numbers' => $numbers
]);
@@ -2322,6 +3061,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/mock-numbers', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'numbers' => []
]);
@@ -2330,6 +3070,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/mock-numbers', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'numbers' => [
[
@@ -2653,6 +3394,7 @@ class ProjectsConsoleClientTest extends Scope
$project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'projectId' => ID::unique(),
@@ -2688,6 +3430,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]));
@@ -2717,6 +3460,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]));
@@ -2742,6 +3486,7 @@ class ProjectsConsoleClientTest extends Scope
$project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'projectId' => ID::unique(),
@@ -2772,6 +3517,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]));
@@ -2794,6 +3540,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]));
@@ -2817,6 +3564,7 @@ class ProjectsConsoleClientTest extends Scope
$project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]), [
'projectId' => ID::unique(),
@@ -2857,6 +3605,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]));
@@ -2933,6 +3682,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]));
@@ -3009,6 +3759,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
]));
@@ -3767,6 +4518,7 @@ class ProjectsConsoleClientTest extends Scope
$response = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Project Test 2',
@@ -4815,6 +5567,7 @@ class ProjectsConsoleClientTest extends Scope
$project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Amazing Project',
@@ -4840,6 +5593,7 @@ class ProjectsConsoleClientTest extends Scope
$project = $this->client->call(Client::METHOD_GET, '/projects/' . $projectId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()));
$this->assertEquals(200, $project['headers']['status-code']);
@@ -4863,6 +5617,7 @@ class ProjectsConsoleClientTest extends Scope
$project = $this->client->call(Client::METHOD_GET, '/projects/' . $projectId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()));
$this->assertEquals(404, $project['headers']['status-code']);
@@ -4886,6 +5641,7 @@ class ProjectsConsoleClientTest extends Scope
$project1 = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Amazing Project 1',
@@ -4896,6 +5652,7 @@ class ProjectsConsoleClientTest extends Scope
$project2 = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Amazing Project 2',
@@ -6238,6 +6995,7 @@ class ProjectsConsoleClientTest extends Scope
$project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Test project - Labels 1',
@@ -6282,6 +7040,7 @@ class ProjectsConsoleClientTest extends Scope
$projects = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'queries' => [
Query::contains('labels', ['nonvip'])->toString(),
@@ -6294,6 +7053,7 @@ class ProjectsConsoleClientTest extends Scope
$projects = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'queries' => [
Query::contains('labels', ['vip'])->toString(),
@@ -6305,6 +7065,7 @@ class ProjectsConsoleClientTest extends Scope
$projects = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'queries' => [
Query::contains('labels', ['imagine'])->toString(),
@@ -6317,6 +7078,7 @@ class ProjectsConsoleClientTest extends Scope
$projects = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'queries' => [
Query::contains('labels', ['nonvip', 'imagine'])->toString(),
@@ -6330,6 +7092,7 @@ class ProjectsConsoleClientTest extends Scope
$project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Test project - Labels 2',
@@ -6358,6 +7121,7 @@ class ProjectsConsoleClientTest extends Scope
$projects = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'queries' => [
Query::contains('labels', ['imagine'])->toString(),
@@ -6372,6 +7136,7 @@ class ProjectsConsoleClientTest extends Scope
$projects = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'queries' => [
Query::contains('labels', ['vip'])->toString(),
@@ -6385,6 +7150,7 @@ class ProjectsConsoleClientTest extends Scope
$projects = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'queries' => [
Query::contains('labels', ['vip'])->toString(),
@@ -6399,6 +7165,7 @@ class ProjectsConsoleClientTest extends Scope
$projects = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
], $this->getHeaders()), [
'queries' => [
Query::contains('labels', ['vip', 'imagine'])->toString(),
@@ -6504,6 +7271,7 @@ class ProjectsConsoleClientTest extends Scope
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $token,
]);
@@ -6600,6 +7368,7 @@ class ProjectsConsoleClientTest extends Scope
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $token,
], [
'name' => $newProjectName,
@@ -6616,6 +7385,7 @@ class ProjectsConsoleClientTest extends Scope
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-response-format' => '1.9.4',
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $token,
], [
'name' => $newProjectName,
@@ -6813,6 +7583,7 @@ class ProjectsConsoleClientTest extends Scope
'x-appwrite-mode' => 'admin',
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $token,
], [
+ 'variableId' => $variableId,
'key' => 'APP_TEST_' . $variableId,
'value' => 'TESTINGVALUE',
'secret' => false
@@ -6832,6 +7603,7 @@ class ProjectsConsoleClientTest extends Scope
'x-appwrite-mode' => 'admin',
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $token,
], [
+ 'variableId' => $variableId,
'key' => 'APP_TEST_' . $variableId,
'value' => 'TESTINGVALUE',
'secret' => false
diff --git a/tests/e2e/Services/Proxy/ProxyBase.php b/tests/e2e/Services/Proxy/ProxyBase.php
index 59a853bfc8..c83958afe1 100644
--- a/tests/e2e/Services/Proxy/ProxyBase.php
+++ b/tests/e2e/Services/Proxy/ProxyBase.php
@@ -2,298 +2,801 @@
namespace Tests\E2E\Services\Proxy;
-use Appwrite\ID;
-use Appwrite\Tests\Async;
-use CURLFile;
use Tests\E2E\Client;
-use Utopia\Console;
+use Utopia\Database\Query;
+use Utopia\System\System;
trait ProxyBase
{
- use Async;
+ use ProxyHelpers;
- protected function listRules(array $params = []): mixed
+ protected function tearDown(): void
{
- $rule = $this->client->call(Client::METHOD_GET, '/proxy/rules', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- ], $this->getHeaders()), $params);
-
- return $rule;
- }
-
- protected function createAPIRule(string $domain): mixed
- {
- $rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/api', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- ], $this->getHeaders()), [
- 'domain' => $domain,
+ // Cleanup for testRuleVerification test
+ // Required as it uses static domain name
+ $rules = $this->listRules([
+ 'queries' => [
+ Query::endsWith('domain', 'webapp.com')->toString(),
+ Query::limit(1000)->toString(),
+ ]
]);
+ $this->assertEquals(200, $rules['headers']['status-code']);
+ foreach ($rules['body']['rules'] as $rule) {
+ $ruleId = $rule['$id'];
+ $response = $this->deleteRule($ruleId);
+ $this->assertEquals(204, $response['headers']['status-code']);
+ }
- return $rule;
+ if ($rules['body']['total'] > 0) {
+ $rules = $this->listRules([
+ 'queries' => [
+ Query::endsWith('domain', 'webapp.com')->toString(),
+ Query::limit(1)->toString()
+ ]
+ ]);
+ $this->assertEquals(200, $rules['headers']['status-code']);
+ $this->assertEquals(0, count($rules['body']['rules']));
+ $this->assertEquals(0, $rules['body']['total']);
+ }
}
- protected function updateRuleVerification(string $ruleId): mixed
- {
- $rule = $this->client->call(Client::METHOD_PATCH, '/proxy/rules/' . $ruleId . '/verification', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- ], $this->getHeaders()), []);
-
- return $rule;
- }
-
- protected function createSiteRule(string $domain, string $siteId, string $branch = ''): mixed
- {
- $rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/site', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- ], $this->getHeaders()), [
- 'domain' => $domain,
- 'siteId' => $siteId,
- 'branch' => $branch,
- ]);
-
- return $rule;
- }
-
- protected function getRule(string $ruleId): mixed
- {
- $rule = $this->client->call(Client::METHOD_GET, '/proxy/rules/' . $ruleId, array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- ], $this->getHeaders()), []);
-
- return $rule;
- }
-
- protected function createRedirectRule(string $domain, string $url, int $statusCode, string $resourceType, string $resourceId): mixed
- {
- $rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/redirect', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- ], $this->getHeaders()), [
- 'domain' => $domain,
- 'url' => $url,
- 'statusCode' => $statusCode,
- 'resourceType' => $resourceType,
- 'resourceId' => $resourceId,
- ]);
-
- return $rule;
- }
-
- protected function createFunctionRule(string $domain, string $functionId, string $branch = ''): mixed
- {
- $rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/function', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- ], $this->getHeaders()), [
- 'domain' => $domain,
- 'functionId' => $functionId,
- 'branch' => $branch,
- ]);
-
- return $rule;
- }
-
- protected function deleteRule(string $ruleId): mixed
- {
- $rule = $this->client->call(Client::METHOD_DELETE, '/proxy/rules/' . $ruleId, array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- ], $this->getHeaders()), []);
-
- return $rule;
- }
-
- protected function setupAPIRule(string $domain): string
+ public function testCreateRule(): void
{
+ $domain = \uniqid() . '-api.myapp.com';
$rule = $this->createAPIRule($domain);
- $this->assertEquals(201, $rule['headers']['status-code'], 'Failed to setup rule: ' . \json_encode($rule));
+ $this->assertEquals(201, $rule['headers']['status-code']);
+ $this->assertEquals($domain, $rule['body']['domain']);
+ $this->assertEquals('manual', $rule['body']['trigger']);
+ $this->assertArrayHasKey('$id', $rule['body']);
+ $this->assertArrayHasKey('domain', $rule['body']);
+ $this->assertArrayHasKey('type', $rule['body']);
+ $this->assertArrayHasKey('redirectUrl', $rule['body']);
+ $this->assertArrayHasKey('redirectStatusCode', $rule['body']);
+ $this->assertArrayHasKey('deploymentResourceType', $rule['body']);
+ $this->assertArrayHasKey('deploymentId', $rule['body']);
+ $this->assertArrayHasKey('deploymentResourceId', $rule['body']);
+ $this->assertArrayHasKey('deploymentVcsProviderBranch', $rule['body']);
+ $this->assertArrayHasKey('logs', $rule['body']);
+ $this->assertArrayHasKey('renewAt', $rule['body']);
- return $rule['body']['$id'];
- }
+ $ruleId = $rule['body']['$id'];
- protected function setupRedirectRule(string $domain, string $url, int $statusCode, string $resourceType, string $resourceId): string
- {
- $rule = $this->createRedirectRule($domain, $url, $statusCode, $resourceType, $resourceId);
+ $rule = $this->createAPIRule($domain);
+ $this->assertEquals(409, $rule['headers']['status-code']);
- $this->assertEquals(201, $rule['headers']['status-code'], 'Failed to setup rule: ' . \json_encode($rule));
-
- return $rule['body']['$id'];
- }
-
- protected function setupFunctionRule(string $domain, string $functionId, string $branch = ''): string
- {
- $rule = $this->createFunctionRule($domain, $functionId, $branch);
-
- $this->assertEquals(201, $rule['headers']['status-code'], 'Failed to setup rule: ' . \json_encode($rule));
-
- return $rule['body']['$id'];
- }
-
- protected function setupSiteRule(string $domain, string $siteId, string $branch = ''): string
- {
- $rule = $this->createSiteRule($domain, $siteId, $branch);
-
- $this->assertEquals(201, $rule['headers']['status-code'], 'Failed to setup rule: ' . \json_encode($rule));
-
- return $rule['body']['$id'];
- }
-
- protected function cleanupRule(string $ruleId): void
- {
$rule = $this->deleteRule($ruleId);
- $this->assertEquals(204, $rule['headers']['status-code'], 'Failed to cleanup rule: ' . \json_encode($rule));
+
+ $this->assertEquals(204, $rule['headers']['status-code']);
}
- protected function cleanupSite(string $siteId): void
+ public function testCreateRuleDeletesOrphanedRule(): void
{
- $site = $this->client->call(Client::METHOD_DELETE, '/sites/' . $siteId, array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- ], $this->getHeaders()), []);
+ $domain = \uniqid() . '-orphan-api.custom.localhost';
+ $orphanProject = $this->getProject(true);
- $this->assertEquals(204, $site['headers']['status-code'], 'Failed to cleanup site: ' . \json_encode($site));
+ $orphanRule = $this->client->call(Client::METHOD_POST, '/proxy/rules/api', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $orphanProject['$id'],
+ 'x-appwrite-key' => $orphanProject['apiKey'],
+ ], [
+ 'domain' => $domain,
+ ]);
+
+ $this->assertEquals(201, $orphanRule['headers']['status-code']);
+ $this->assertEquals($domain, $orphanRule['body']['domain']);
+
+ $duplicateRule = $this->createAPIRule($domain);
+ $this->assertEquals(409, $duplicateRule['headers']['status-code']);
+
+ $deleteProject = $this->client->call(Client::METHOD_DELETE, '/projects/' . $orphanProject['$id'], [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $orphanProject['$id'],
+ 'x-appwrite-key' => $orphanProject['apiKey'],
+ ]);
+
+ $this->assertEquals(204, $deleteProject['headers']['status-code']);
+
+ // Project deletion removes the project document synchronously, while rule cleanup is queued.
+ // Creating the same domain now should clean up that orphaned rule before retrying.
+ $rule = $this->createAPIRule($domain);
+
+ $this->assertEquals(201, $rule['headers']['status-code']);
+ $this->assertEquals($domain, $rule['body']['domain']);
+
+ $rules = $this->listRules([
+ 'queries' => [
+ Query::equal('domain', [$domain])->toString(),
+ ],
+ ]);
+
+ $this->assertEquals(200, $rules['headers']['status-code']);
+ $this->assertEquals(1, $rules['body']['total']);
+ $this->assertEquals($rule['body']['$id'], $rules['body']['rules'][0]['$id']);
+
+ $this->cleanupRule($rule['body']['$id']);
}
- protected function cleanupFunction(string $functionId): void
+ public function testCreateRuleSetup(): void
{
- $function = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- ], $this->getHeaders()), []);
-
- $this->assertEquals(204, $function['headers']['status-code'], 'Failed to cleanup function: ' . \json_encode($function));
+ $ruleId = $this->setupAPIRule(\uniqid() . '-api2.myapp.com');
+ $this->cleanupRule($ruleId);
}
- protected function setupSite(): mixed
+ public function testCreateRuleApex(): void
{
- // Site
- $site = $this->client->call(Client::METHOD_POST, '/sites', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey'],
- ]), [
- 'siteId' => ID::unique(),
- 'name' => 'Proxy site',
- 'framework' => 'other',
- 'adapter' => 'static',
- 'buildRuntime' => 'static-1',
- 'outputDirectory' => './',
- 'buildCommand' => '',
- 'installCommand' => '',
- 'fallbackFile' => '',
- ]);
-
- $this->assertEquals($site['headers']['status-code'], 201, 'Setup site failed with status code: ' . $site['headers']['status-code'] . ' and response: ' . json_encode($site['body'], JSON_PRETTY_PRINT));
-
- $siteId = $site['body']['$id'];
-
- // Deployment
- $deployment = $this->client->call(Client::METHOD_POST, '/sites/' . $siteId . '/deployments', array_merge([
- 'content-type' => 'multipart/form-data',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey'],
- ]), [
- 'code' => $this->packageSite('static'),
- 'activate' => 'true'
- ]);
-
- $this->assertEquals($deployment['headers']['status-code'], 202, 'Setup deployment failed with status code: ' . $deployment['headers']['status-code'] . ' and response: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT));
- $deploymentId = $deployment['body']['$id'] ?? '';
-
- $this->assertEventually(function () use ($siteId, $deploymentId) {
- $site = $this->client->call(Client::METHOD_GET, '/sites/' . $siteId, array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey'],
- ]));
- $this->assertEquals($deploymentId, $site['body']['deploymentId'], 'Deployment is not activated, deployment: ' . json_encode($site['body'], JSON_PRETTY_PRINT));
- }, 120000, 500);
-
- return ['siteId' => $siteId, 'deploymentId' => $deploymentId];
+ $domain = \uniqid() . '.com';
+ $rule = $this->createAPIRule($domain);
+ $this->assertEquals(201, $rule['headers']['status-code']);
+ $this->assertEquals('unverified', $rule['body']['status']);
}
- protected function setupFunction(): mixed
+ public function testCreateRuleVcs(): void
{
- // Function
- $function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey'],
- ]), [
- 'functionId' => ID::unique(),
- 'runtime' => 'node-22',
- 'name' => 'Proxy Function',
- 'entrypoint' => 'index.js',
- 'commands' => '',
- 'execute' => ['any']
+ $domain = \uniqid() . '-vcs.myapp.com';
+
+ $setup = $this->setupSite();
+ $siteId = $setup['siteId'];
+ $deploymentId = $setup['deploymentId'];
+
+ $this->assertNotEmpty($siteId);
+ $this->assertNotEmpty($deploymentId);
+
+ $rule = $this->createSiteRule('commit-' . $domain, $siteId);
+ $this->assertEquals(201, $rule['headers']['status-code']);
+ $this->cleanupRule($rule['body']['$id']);
+
+ $rule = $this->createSiteRule('branch-' . $domain, $siteId);
+ $this->assertEquals(201, $rule['headers']['status-code']);
+ $this->cleanupRule($rule['body']['$id']);
+
+ $rule = $this->createSiteRule('anything-' . $domain, $siteId);
+ $this->assertEquals(201, $rule['headers']['status-code']);
+ $this->cleanupRule($rule['body']['$id']);
+
+ $sitesDomain = \explode(',', System::getEnv('_APP_DOMAIN_SITES', ''))[0];
+ $domain = \uniqid() . '-vcs.' . $sitesDomain;
+
+ $rule = $this->createSiteRule('commit-' . $domain, $siteId);
+ $this->assertEquals(400, $rule['headers']['status-code']);
+
+ $rule = $this->createSiteRule('branch-' . $domain, $siteId);
+ $this->assertEquals(400, $rule['headers']['status-code']);
+
+ $rule = $this->createSiteRule('subdomain.anything-' . $domain, $siteId);
+ $this->assertEquals(400, $rule['headers']['status-code']);
+
+ $rule = $this->createSiteRule('anything-' . $domain, $siteId);
+ $this->assertEquals(201, $rule['headers']['status-code']);
+ $this->cleanupRule($rule['body']['$id']);
+ }
+
+ public function testCreateAPIRule(): void
+ {
+ $domain = \uniqid() . '-api.custom.localhost';
+
+ $proxyClient = new Client();
+ $proxyClient->setEndpoint('http://appwrite.test');
+ $proxyClient->addHeader('x-appwrite-hostname', $domain);
+
+ $response = $proxyClient->call(Client::METHOD_GET, '/versions');
+ $this->assertEquals(401, $response['headers']['status-code']);
+
+ $ruleId = $this->setupAPIRule($domain);
+
+ $this->assertNotEmpty($ruleId);
+
+ $response = $proxyClient->call(Client::METHOD_GET, '/versions');
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals(APP_VERSION_STABLE, $response['body']['server']);
+
+ $this->cleanupRule($ruleId);
+
+ $rule = $this->createAPIRule('http://' . $domain);
+ $this->assertEquals(400, $rule['headers']['status-code']);
+
+ $rule = $this->createAPIRule('https://' . $domain);
+ $this->assertEquals(400, $rule['headers']['status-code']);
+
+ $rule = $this->createAPIRule('wss://' . $domain);
+ $this->assertEquals(400, $rule['headers']['status-code']);
+
+ $rule = $this->createAPIRule($domain . '/some-path');
+ $this->assertEquals(400, $rule['headers']['status-code']);
+ }
+
+ public function testCreateRedirectRule(): void
+ {
+ $domain = \uniqid() . '-redirect.custom.localhost';
+
+ $proxyClient = new Client();
+ $proxyClient->setEndpoint('http://appwrite.test');
+ $proxyClient->addHeader('x-appwrite-hostname', $domain);
+
+ $response = $proxyClient->call(Client::METHOD_GET, '/todos/1');
+ $this->assertEquals(401, $response['headers']['status-code']);
+
+ $siteId = $this->setupSite()['siteId'];
+
+ $ruleId301 = $this->setupRedirectRule($domain, 'https://jsonplaceholder.typicode.com/todos/1', 301, 'site', $siteId);
+ $this->assertNotEmpty($ruleId301);
+
+ $response = $proxyClient->call(Client::METHOD_GET, '/todos/1');
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals(1, $response['body']['id']);
+
+ $response = $proxyClient->call(Client::METHOD_GET, '/');
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals(1, $response['body']['id']);
+
+ $response = $proxyClient->call(Client::METHOD_GET, '/', followRedirects: false);
+ $this->assertEquals(301, $response['headers']['status-code']);
+ $this->assertEquals('https://jsonplaceholder.typicode.com/todos/1', $response['headers']['location']);
+
+ $domain = \uniqid() . '-redirect-307.custom.localhost';
+ $ruleId307 = $this->setupRedirectRule($domain, 'https://jsonplaceholder.typicode.com/todos/1', 307, 'site', $siteId);
+ $this->assertNotEmpty($ruleId307);
+
+ $proxyClient = new Client();
+ $proxyClient->setEndpoint('http://appwrite.test');
+ $proxyClient->addHeader('x-appwrite-hostname', $domain);
+
+ $response = $proxyClient->call(Client::METHOD_GET, '/', followRedirects: false);
+ $this->assertEquals(307, $response['headers']['status-code']);
+ $this->assertEquals('https://jsonplaceholder.typicode.com/todos/1', $response['headers']['location']);
+
+ $rules = $this->listRules([
+ 'queries' => [
+ Query::equal('type', ['redirect'])->toString(),
+ Query::equal('trigger', ['manual'])->toString(),
+ Query::equal('deploymentResourceType', ['site'])->toString(),
+ Query::equal('deploymentResourceId', [$siteId])->toString(),
+ ],
]);
+ $this->assertEquals(200, $rules['headers']['status-code']);
+ $this->assertEquals(2, $rules['body']['total']);
- $this->assertEquals($function['headers']['status-code'], 201, 'Setup function failed with status code: ' . $function['headers']['status-code'] . ' and response: ' . json_encode($function['body'], JSON_PRETTY_PRINT));
+ $this->cleanupRule($ruleId301);
+ $this->cleanupRule($ruleId307);
+ $this->cleanupSite($siteId);
+ }
- $functionId = $function['body']['$id'];
+ public function testCreateFunctionRule(): void
+ {
+ $domain = \uniqid() . '-function.custom.localhost';
- // Deployment
- $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([
- 'content-type' => 'multipart/form-data',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey'],
- ]), [
- 'code' => $this->packageFunction('basic'),
- 'activate' => 'true'
- ]);
+ $proxyClient = new Client();
+ $proxyClient->setEndpoint('http://appwrite.test');
+ $proxyClient->addHeader('x-appwrite-hostname', $domain);
- $this->assertEquals($deployment['headers']['status-code'], 202, 'Setup deployment failed with status code: ' . $deployment['headers']['status-code'] . ' and response: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT));
- $deploymentId = $deployment['body']['$id'] ?? '';
+ $response = $proxyClient->call(Client::METHOD_GET, '/ping');
+ $this->assertEquals(401, $response['headers']['status-code']);
+
+ $setup = $this->setupFunction();
+ $functionId = $setup['functionId'];
+ $deploymentId = $setup['deploymentId'];
+
+ $this->assertNotEmpty($functionId);
+ $this->assertNotEmpty($deploymentId);
+
+ $ruleId = $this->setupFunctionRule($domain, $functionId);
+ $this->assertNotEmpty($ruleId);
+
+ $response = $proxyClient->call(Client::METHOD_GET, '/ping');
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals($functionId, $response['body']['APPWRITE_FUNCTION_ID']);
+
+ $this->cleanupRule($ruleId);
+
+ $this->cleanupFunction($functionId);
$this->assertEventually(function () use ($functionId, $deploymentId) {
- $function = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId, array_merge([
- 'content-type' => 'application/json',
- 'x-appwrite-project' => $this->getProject()['$id'],
- 'x-appwrite-key' => $this->getProject()['apiKey'],
- ]));
- $this->assertEquals($deploymentId, $function['body']['deploymentId'], 'Deployment is not activated, deployment: ' . json_encode($function['body'], JSON_PRETTY_PRINT));
- }, 100000, 500);
+ $rules = $this->listRules([
+ 'queries' => [
+ Query::limit(1)->toString(),
+ Query::equal('type', ['deployment'])->toString(),
+ Query::equal('deploymentResourceType', ['function'])->toString(),
+ Query::equal('deploymentResourceId', [$functionId])->toString(),
+ ]
+ ]);
+ $this->assertEquals(200, $rules['headers']['status-code']);
+ $this->assertEquals(0, $rules['body']['total']);
+ $this->assertCount(0, $rules['body']['rules']);
- return ['functionId' => $functionId, 'deploymentId' => $deploymentId];
+ $rules = $this->listRules([
+ 'queries' => [
+ Query::limit(1)->toString(),
+ Query::equal('type', ['deployment'])->toString(),
+ Query::equal('deploymentId', [$deploymentId])->toString()
+ ]
+ ]);
+ $this->assertEquals(200, $rules['headers']['status-code']);
+ $this->assertEquals(0, $rules['body']['total']);
+ $this->assertCount(0, $rules['body']['rules']);
+ });
}
- private function packageSite(string $site): CURLFile
+ public function testCreateSiteRule(): void
{
- $stdout = '';
- $stderr = '';
+ $domain = \uniqid() . '-site.custom.localhost';
- $folderPath = realpath(__DIR__ . '/../../../resources/sites') . "/$site";
- $tarPath = "$folderPath/code.tar.gz";
+ $proxyClient = new Client();
+ $proxyClient->setEndpoint('http://appwrite.test');
+ $proxyClient->addHeader('x-appwrite-hostname', $domain);
- Console::execute("cd $folderPath && tar --exclude code.tar.gz --exclude node_modules -czf code.tar.gz .", '', $stdout, $stderr);
+ $response = $proxyClient->call(Client::METHOD_GET, '/contact');
+ $this->assertEquals(401, $response['headers']['status-code']);
- if (filesize($tarPath) > 1024 * 1024 * 5) {
- throw new \Exception('Code package is too large. Use the chunked upload method instead.');
+ $setup = $this->setupSite();
+ $siteId = $setup['siteId'];
+ $deploymentId = $setup['deploymentId'];
+
+ $this->assertNotEmpty($siteId);
+ $this->assertNotEmpty($deploymentId);
+
+ $ruleId = $this->setupSiteRule($domain, $siteId);
+ $this->assertNotEmpty($ruleId);
+ $rule = $this->getRule($ruleId);
+ $this->assertSame(200, $rule['headers']['status-code']);
+ $this->assertSame('unverified', $rule['body']['status']);
+
+ $response = $proxyClient->call(Client::METHOD_GET, '/contact');
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertStringContainsString('Contact page', $response['body']);
+
+ // Wildcard domains automatically get verified status
+ $domains = [
+ \uniqid() . '.sites.localhost',
+ \uniqid() . '.rebranded.localhost',
+ ];
+ foreach ($domains as $domain) {
+ $wildcardRuleId = $this->setupSiteRule($domain, $siteId);
+ $this->assertNotEmpty($wildcardRuleId);
+ $rule = $this->getRule($wildcardRuleId);
+ $this->assertSame(200, $rule['headers']['status-code']);
+ $this->assertSame('verified', $rule['body']['status']);
+ $this->cleanupRule($wildcardRuleId);
}
- return new CURLFile($tarPath, 'application/x-gzip', \basename($tarPath));
+ $rules = $this->listRules([
+ 'queries' => [
+ Query::limit(1)->toString(),
+ Query::equal('trigger', ['deployment'])->toString(),
+ Query::equal('type', ['deployment'])->toString(),
+ Query::equal('deploymentResourceType', ['site'])->toString(),
+ Query::equal('deploymentResourceId', [$siteId])->toString(),
+ ]
+ ]);
+ $this->assertEquals(200, $rules['headers']['status-code']);
+ $this->assertGreaterThan(0, $rules['body']['total']);
+
+ $this->cleanupRule($ruleId);
+
+ $this->cleanupSite($siteId);
+
+ $this->assertEventually(function () use ($siteId, $deploymentId) {
+ $rules = $this->listRules([
+ 'queries' => [
+ Query::limit(1)->toString(),
+ Query::equal('type', ['deployment'])->toString(),
+ Query::equal('deploymentResourceType', ['site'])->toString(),
+ Query::equal('deploymentResourceId', [$siteId])->toString(),
+ ]
+ ]);
+ $this->assertEquals(200, $rules['headers']['status-code']);
+ $this->assertEquals(0, $rules['body']['total']);
+ $this->assertCount(0, $rules['body']['rules']);
+
+ $rules = $this->listRules([
+ 'queries' => [
+ Query::limit(1)->toString(),
+ Query::equal('type', ['deployment'])->toString(),
+ Query::equal('deploymentId', [$deploymentId])->toString()
+ ]
+ ]);
+ $this->assertEquals(200, $rules['headers']['status-code']);
+ $this->assertEquals(0, $rules['body']['total']);
+ $this->assertCount(0, $rules['body']['rules']);
+ });
}
- private function packageFunction(string $function): CURLFile
+ public function testCreateSiteBranchRule(): void
{
- $stdout = '';
- $stderr = '';
+ $domain = \uniqid() . '-site-branch.custom.localhost';
- $folderPath = realpath(__DIR__ . '/../../../resources/functions') . "/$function";
- $tarPath = "$folderPath/code.tar.gz";
+ $setup = $this->setupSite();
+ $siteId = $setup['siteId'];
+ $deploymentId = $setup['deploymentId'];
- Console::execute("cd $folderPath && tar --exclude code.tar.gz --exclude node_modules -czf code.tar.gz .", '', $stdout, $stderr);
+ $this->assertNotEmpty($siteId);
+ $this->assertNotEmpty($deploymentId);
- if (filesize($tarPath) > 1024 * 1024 * 5) {
- throw new \Exception('Code package is too large. Use the chunked upload method instead.');
+ $ruleId = $this->setupSiteRule($domain, $siteId, 'dev');
+ $this->assertNotEmpty($ruleId);
+
+ $rule = $this->getRule($ruleId);
+ $this->assertEquals(200, $rule['headers']['status-code']);
+
+ $this->cleanupRule($ruleId);
+ }
+
+ public function testCreateFunctionBranchRule(): void
+ {
+ $domain = \uniqid() . '-function-branch.custom.localhost';
+
+ $setup = $this->setupFunction();
+ $functionId = $setup['functionId'];
+ $deploymentId = $setup['deploymentId'];
+
+ $this->assertNotEmpty($functionId);
+ $this->assertNotEmpty($deploymentId);
+
+ $ruleId = $this->setupFunctionRule($domain, $functionId, 'dev');
+ $this->assertNotEmpty($ruleId);
+
+ $rule = $this->getRule($ruleId);
+ $this->assertEquals(200, $rule['headers']['status-code']);
+
+ $this->cleanupRule($ruleId);
+
+ $this->cleanupFunction($functionId);
+ }
+
+ public function testUpdateRule(): void
+ {
+ // Create function appwrite-network domain
+ $functionsDomain = \explode(',', System::getEnv('_APP_DOMAIN_FUNCTIONS', ''))[0];
+ $domain = \uniqid() . '-cname-api.' . $functionsDomain;
+
+ $rule = $this->createAPIRule($domain);
+ $this->assertEquals(201, $rule['headers']['status-code']);
+ $this->assertEquals('verified', $rule['body']['status']);
+
+ $this->cleanupRule($rule['body']['$id']);
+
+ // Create site appwrite-network domain
+ $sitesDomain = \explode(',', System::getEnv('_APP_DOMAIN_SITES', ''))[0];
+ $domain = \uniqid() . '-cname-api.' . $sitesDomain;
+
+ $rule = $this->createAPIRule($domain);
+ $this->assertEquals(201, $rule['headers']['status-code']);
+ $this->assertEquals('verified', $rule['body']['status']);
+
+ $this->cleanupRule($rule['body']['$id']);
+
+ // Create + update
+ $domain = \uniqid() . '-cname-api.custom.com';
+
+ $rule = $this->createAPIRule($domain);
+ $this->assertEquals(201, $rule['headers']['status-code']);
+ $this->assertEquals('unverified', $rule['body']['status']);
+
+ $ruleId = $rule['body']['$id'];
+
+ $rule = $this->updateRuleStatus($ruleId);
+ $this->assertEquals(400, $rule['headers']['status-code']);
+
+ $this->cleanupRule($ruleId);
+ }
+
+ public function testGetRule()
+ {
+ $domain = \uniqid() . '-get.custom.localhost';
+ $ruleId = $this->setupAPIRule($domain);
+
+ $this->assertNotEmpty($ruleId);
+
+ $rule = $this->getRule($ruleId);
+ $this->assertEquals(200, $rule['headers']['status-code']);
+ $this->assertEquals($domain, $rule['body']['domain']);
+ $this->assertEquals('manual', $rule['body']['trigger']);
+ $this->assertArrayHasKey('$id', $rule['body']);
+ $this->assertArrayHasKey('domain', $rule['body']);
+ $this->assertArrayHasKey('type', $rule['body']);
+ $this->assertArrayHasKey('redirectUrl', $rule['body']);
+ $this->assertArrayHasKey('redirectStatusCode', $rule['body']);
+ $this->assertArrayHasKey('deploymentResourceType', $rule['body']);
+ $this->assertArrayHasKey('deploymentId', $rule['body']);
+ $this->assertArrayHasKey('deploymentResourceId', $rule['body']);
+ $this->assertArrayHasKey('deploymentVcsProviderBranch', $rule['body']);
+ $this->assertArrayHasKey('logs', $rule['body']);
+ $this->assertArrayHasKey('renewAt', $rule['body']);
+
+ $this->cleanupRule($ruleId);
+ }
+
+ public function testListRules()
+ {
+ $rules = $this->listRules();
+ $this->assertEquals(200, $rules['headers']['status-code']);
+ foreach ($rules['body']['rules'] as $rule) {
+ $rule = $this->deleteRule($rule['$id']);
+ $this->assertEquals(204, $rule['headers']['status-code']);
}
- return new CURLFile($tarPath, 'application/x-gzip', \basename($tarPath));
+ $rules = $this->listRules();
+ $this->assertEquals(200, $rules['headers']['status-code']);
+ $this->assertEquals(0, $rules['body']['total']);
+ $this->assertCount(0, $rules['body']['rules']);
+
+ $rule1Domain = \uniqid() . '-list1.custom.localhost';
+ $rule1Id = $this->setupAPIRule($rule1Domain);
+ $this->assertNotEmpty($rule1Id);
+
+ $rules = $this->listRules();
+ $this->assertEquals(200, $rules['headers']['status-code']);
+ $this->assertEquals(1, $rules['body']['total']);
+ $this->assertCount(1, $rules['body']['rules']);
+ $this->assertEquals($rule1Domain, $rules['body']['rules'][0]['domain']);
+
+ $this->assertEquals('manual', $rules['body']['rules'][0]['trigger']);
+ $this->assertArrayHasKey('$id', $rules['body']['rules'][0]);
+ $this->assertArrayHasKey('domain', $rules['body']['rules'][0]);
+ $this->assertArrayHasKey('type', $rules['body']['rules'][0]);
+ $this->assertArrayHasKey('redirectUrl', $rules['body']['rules'][0]);
+ $this->assertArrayHasKey('redirectStatusCode', $rules['body']['rules'][0]);
+ $this->assertArrayHasKey('deploymentResourceType', $rules['body']['rules'][0]);
+ $this->assertArrayHasKey('deploymentId', $rules['body']['rules'][0]);
+ $this->assertArrayHasKey('deploymentResourceId', $rules['body']['rules'][0]);
+ $this->assertArrayHasKey('deploymentVcsProviderBranch', $rules['body']['rules'][0]);
+ $this->assertArrayHasKey('logs', $rules['body']['rules'][0]);
+ $this->assertArrayHasKey('renewAt', $rules['body']['rules'][0]);
+
+ $rule2Domain = \uniqid() . '-list1.custom.localhost';
+ $rule2Id = $this->setupAPIRule($rule2Domain);
+ $this->assertNotEmpty($rule2Id);
+
+ $rules = $this->listRules();
+ $this->assertEquals(200, $rules['headers']['status-code']);
+ $this->assertEquals(2, $rules['body']['total']);
+ $this->assertCount(2, $rules['body']['rules']);
+
+ $rules = $this->listRules([
+ 'queries' => [
+ Query::limit(1)->toString()
+ ]
+ ]);
+ $this->assertEquals(200, $rules['headers']['status-code']);
+ $this->assertEquals(2, $rules['body']['total']);
+ $this->assertCount(1, $rules['body']['rules']);
+
+ $rules = $this->listRules([
+ 'queries' => [
+ Query::equal('$id', [$rule1Id])->toString()
+ ]
+ ]);
+ $this->assertEquals(200, $rules['headers']['status-code']);
+ $this->assertCount(1, $rules['body']['rules']);
+ $this->assertEquals($rule1Domain, $rules['body']['rules'][0]['domain']);
+
+ $rules = $this->listRules([
+ 'queries' => [
+ Query::orderDesc('$id')->toString()
+ ]
+ ]);
+ $this->assertEquals(200, $rules['headers']['status-code']);
+ $this->assertCount(2, $rules['body']['rules']);
+ $this->assertEquals($rule2Id, $rules['body']['rules'][0]['$id']);
+
+ $rules = $this->listRules([
+ 'queries' => [
+ Query::equal('domain', [$rule2Domain])->toString()
+ ]
+ ]);
+ $this->assertEquals(200, $rules['headers']['status-code']);
+ $this->assertCount(1, $rules['body']['rules']);
+ $this->assertEquals($rule2Id, $rules['body']['rules'][0]['$id']);
+
+ $rules = $this->listRules([
+ 'search' => $rule1Domain,
+ 'queries' => [ Query::orderDesc('$createdAt')->toString() ]
+ ]);
+
+ $this->assertEquals(200, $rules['headers']['status-code']);
+ $ruleIds = \array_column($rules['body']['rules'], '$id');
+ $this->assertContains($rule1Id, $ruleIds);
+
+ $rules = $this->listRules([
+ 'search' => $rule2Domain,
+ 'queries' => [ Query::orderDesc('$createdAt')->toString() ]
+ ]);
+ $this->assertEquals(200, $rules['headers']['status-code']);
+ $ruleIds = \array_column($rules['body']['rules'], '$id');
+ $this->assertContains($rule2Id, $ruleIds);
+
+ $rules = $this->listRules([
+ 'search' => $rule1Id,
+ 'queries' => [ Query::orderDesc('$createdAt')->toString() ]
+ ]);
+ $this->assertEquals(200, $rules['headers']['status-code']);
+ $ruleDomains = \array_column($rules['body']['rules'], 'domain');
+ $this->assertContains($rule1Domain, $ruleDomains);
+
+ $rules = $this->listRules([
+ 'search' => $rule2Id,
+ 'queries' => [ Query::orderDesc('$createdAt')->toString() ]
+ ]);
+ $this->assertEquals(200, $rules['headers']['status-code']);
+ $ruleDomains = \array_column($rules['body']['rules'], 'domain');
+ $this->assertContains($rule2Domain, $ruleDomains);
+
+ $rules = $this->listRules();
+ $this->assertEquals(200, $rules['headers']['status-code']);
+ foreach ($rules['body']['rules'] as $rule) {
+ $rule = $this->deleteRule($rule['$id']);
+ $this->assertEquals(204, $rule['headers']['status-code']);
+ }
+
+ $rules = $this->listRules();
+ $this->assertEquals(200, $rules['headers']['status-code']);
+ $this->assertEquals(0, $rules['body']['total']);
+ $this->assertCount(0, $rules['body']['rules']);
+ }
+
+ public function testRuleVerification(): void
+ {
+
+ // 1. Site rule can verify
+ $site = $this->setupSite();
+ $siteId = $site['siteId'];
+
+ $rule = $this->createSiteRule('stage-site.webapp.com', $siteId);
+ $this->assertEquals(201, $rule['headers']['status-code']);
+ $this->assertEquals('verifying', $rule['body']['status']);
+ $this->assertEmpty($rule['body']['logs']);
+ $this->assertNotEmpty($rule['body']['$id']);
+ $ruleId = $rule['body']['$id'];
+
+ $rule = $this->updateRuleStatus($ruleId);
+ $this->assertEquals(200, $rule['headers']['status-code']);
+ $this->assertEquals($ruleId, $rule['body']['$id']);
+ $this->assertEquals('verifying', $rule['body']['status']);
+ $this->assertEmpty($rule['body']['logs']);
+
+ $this->cleanupRule($rule['body']['$id']);
+ $this->cleanupSite($siteId);
+
+ // 2. Function rule can verify
+ $function = $this->setupFunction();
+ $functionId = $function['functionId'];
+
+ $rule = $this->createFunctionRule('stage-function.webapp.com', $functionId);
+ $this->assertEquals(201, $rule['headers']['status-code']);
+ $this->assertEquals('verifying', $rule['body']['status']);
+ $this->assertEmpty($rule['body']['logs']);
+ $this->cleanupRule($rule['body']['$id']);
+
+ $rule = $this->createAPIRule('stage-site.webapp.com');
+ $this->assertEquals(201, $rule['headers']['status-code']);
+ $this->assertEquals('unverified', $rule['body']['status']);
+ $this->assertStringContainsString('has incorrect CNAME value', $rule['body']['logs']);
+ $this->cleanupRule($rule['body']['$id']);
+
+ $this->cleanupFunction($functionId);
+
+ // 3. Wrong A record fails to verify
+ $rule = $this->createAPIRule('wrong-a-webapp.com');
+ $this->assertEquals(201, $rule['headers']['status-code']);
+ $this->assertEquals('unverified', $rule['body']['status']);
+ $this->assertStringContainsString('is missing CNAME record', $rule['body']['logs']);
+
+ $ruleId = $rule['body']['$id'];
+ $rule = $this->updateRuleStatus($ruleId);
+ $this->assertEquals(400, $rule['headers']['status-code']);
+ $this->assertStringContainsString('is missing CNAME record', $rule['body']['message']);
+
+ $rule = $this->getRule($ruleId);
+ $this->assertEquals(200, $rule['headers']['status-code']);
+ $this->assertEquals('unverified', $rule['body']['status']);
+
+ $this->cleanupRule($ruleId);
+
+ // 4. Correct A record can verify
+ $rule = $this->createAPIRule('webapp.com');
+ $this->assertEquals(201, $rule['headers']['status-code']);
+ $this->assertEquals('verifying', $rule['body']['status']);
+ $this->assertEmpty($rule['body']['logs']);
+
+ $this->cleanupRule($rule['body']['$id']);
+
+ // 5. Correct CNAME record can verify (no CAA record)
+ $rule = $this->createAPIRule('stage.webapp.com');
+ $this->assertEquals(201, $rule['headers']['status-code']);
+ $this->assertEquals('verifying', $rule['body']['status']);
+ $this->assertEmpty($rule['body']['logs']);
+
+ $this->cleanupRule($rule['body']['$id']);
+
+ // 6. Missing CNAME record fails to verify
+ $rule = $this->createAPIRule('stage-missing-cname.webapp.com');
+ $this->assertEquals(201, $rule['headers']['status-code']);
+ $this->assertEquals('unverified', $rule['body']['status']);
+ $this->assertStringContainsString('is missing CNAME record', $rule['body']['logs']);
+
+ $ruleId = $rule['body']['$id'];
+ $rule = $this->updateRuleStatus($ruleId);
+ $this->assertEquals(400, $rule['headers']['status-code']);
+ $this->assertStringContainsString('is missing CNAME record', $rule['body']['message']);
+
+ $rule = $this->getRule($ruleId);
+ $this->assertEquals(200, $rule['headers']['status-code']);
+ $this->assertEquals('unverified', $rule['body']['status']);
+
+ $this->cleanupRule($ruleId);
+
+ // 7. Wrong CNAME record fails to verify
+ $rule = $this->createAPIRule('stage-wrong-cname.webapp.com');
+ $this->assertEquals(201, $rule['headers']['status-code']);
+ $this->assertEquals('unverified', $rule['body']['status']);
+ $this->assertStringContainsString('has incorrect CNAME value', $rule['body']['logs']);
+
+ $ruleId = $rule['body']['$id'];
+ $rule = $this->updateRuleStatus($ruleId);
+ $this->assertEquals(400, $rule['headers']['status-code']);
+ $this->assertStringContainsString('has incorrect CNAME value', $rule['body']['message']);
+
+ $rule = $this->getRule($ruleId);
+ $this->assertEquals(200, $rule['headers']['status-code']);
+ $this->assertEquals('unverified', $rule['body']['status']);
+
+ $this->cleanupRule($ruleId);
+
+ // 8. Wrong CAA record fails to verify
+ $rule = $this->createAPIRule('stage-wrong-caa.webapp.com');
+ $this->assertEquals(201, $rule['headers']['status-code']);
+ $this->assertEquals('unverified', $rule['body']['status']);
+ $this->assertStringContainsString('has incorrect CAA value', $rule['body']['logs']);
+
+ $ruleId = $rule['body']['$id'];
+ $rule = $this->updateRuleStatus($ruleId);
+ $this->assertEquals(400, $rule['headers']['status-code']);
+ $this->assertStringContainsString('has incorrect CAA value', $rule['body']['message']);
+
+ $rule = $this->getRule($ruleId);
+ $this->assertEquals(200, $rule['headers']['status-code']);
+ $this->assertEquals('unverified', $rule['body']['status']);
+
+ $this->cleanupRule($ruleId);
+
+ // 9. Correct CAA record can verify
+ $rule = $this->createAPIRule('stage-correct-caa.webapp.com');
+ $this->assertEquals(201, $rule['headers']['status-code']);
+ $this->assertEquals('verifying', $rule['body']['status']);
+ $this->assertEmpty($rule['body']['logs']);
+
+ $this->cleanupRule($rule['body']['$id']);
+ }
+
+ public function testUpdateRuleVerificationWithSameDataUpdatesTimestamp(): void
+ {
+ $domain = \uniqid() . '-timestamp-test.webapp.com';
+ $rule = $this->createAPIRule($domain);
+
+ $this->assertEquals(201, $rule['headers']['status-code']);
+ $this->assertEquals('unverified', $rule['body']['status']);
+ $this->assertNotEmpty($rule['body']['logs']);
+
+ $ruleId = $rule['body']['$id'];
+ $initialUpdatedAt = $rule['body']['$updatedAt'];
+ $initiallogs = $rule['body']['logs'];
+
+ sleep(1);
+
+ $updatedRule = $this->updateRuleStatus($ruleId);
+
+ $this->assertEquals(400, $updatedRule['headers']['status-code']);
+ $this->assertStringContainsString($initiallogs, $updatedRule['body']['message']);
+
+ $ruleAfterUpdate = $this->getRule($ruleId);
+ $this->assertEquals(200, $ruleAfterUpdate['headers']['status-code']);
+ $this->assertEquals('unverified', $ruleAfterUpdate['body']['status']);
+ $this->assertEquals($initiallogs, $ruleAfterUpdate['body']['logs']);
+ $this->assertNotEquals($initialUpdatedAt, $ruleAfterUpdate['body']['$updatedAt']);
+
+ $initialTime = new \DateTime($initialUpdatedAt);
+ $updatedTime = new \DateTime($ruleAfterUpdate['body']['$updatedAt']);
+ $this->assertGreaterThan($initialTime, $updatedTime);
+
+ $this->cleanupRule($ruleId);
}
}
diff --git a/tests/e2e/Services/Proxy/ProxyConsoleClientTest.php b/tests/e2e/Services/Proxy/ProxyConsoleClientTest.php
new file mode 100644
index 0000000000..68761f34a9
--- /dev/null
+++ b/tests/e2e/Services/Proxy/ProxyConsoleClientTest.php
@@ -0,0 +1,14 @@
+listRules([
- 'queries' => [
- Query::endsWith('domain', 'webapp.com')->toString(),
- Query::limit(1000)->toString(),
- ]
- ]);
- $this->assertEquals(200, $rules['headers']['status-code']);
- foreach ($rules['body']['rules'] as $rule) {
- $ruleId = $rule['$id'];
- $response = $this->deleteRule($ruleId);
- $this->assertEquals(204, $response['headers']['status-code']);
- }
-
- if ($rules['body']['total'] > 0) {
- $rules = $this->listRules([
- 'queries' => [
- Query::endsWith('domain', 'webapp.com')->toString(),
- Query::limit(1)->toString()
- ]
- ]);
- $this->assertEquals(200, $rules['headers']['status-code']);
- $this->assertEquals(0, count($rules['body']['rules']));
- $this->assertEquals(0, $rules['body']['total']);
- }
- }
-
- public function testCreateRule(): void
- {
- $domain = \uniqid() . '-api.myapp.com';
- $rule = $this->createAPIRule($domain);
-
- $this->assertEquals(201, $rule['headers']['status-code']);
- $this->assertEquals($domain, $rule['body']['domain']);
- $this->assertEquals('manual', $rule['body']['trigger']);
- $this->assertArrayHasKey('$id', $rule['body']);
- $this->assertArrayHasKey('domain', $rule['body']);
- $this->assertArrayHasKey('type', $rule['body']);
- $this->assertArrayHasKey('redirectUrl', $rule['body']);
- $this->assertArrayHasKey('redirectStatusCode', $rule['body']);
- $this->assertArrayHasKey('deploymentResourceType', $rule['body']);
- $this->assertArrayHasKey('deploymentId', $rule['body']);
- $this->assertArrayHasKey('deploymentResourceId', $rule['body']);
- $this->assertArrayHasKey('deploymentVcsProviderBranch', $rule['body']);
- $this->assertArrayHasKey('logs', $rule['body']);
- $this->assertArrayHasKey('renewAt', $rule['body']);
-
- $ruleId = $rule['body']['$id'];
-
- $rule = $this->createAPIRule($domain);
- $this->assertEquals(409, $rule['headers']['status-code']);
-
- $rule = $this->deleteRule($ruleId);
-
- $this->assertEquals(204, $rule['headers']['status-code']);
- }
-
- public function testCreateRuleSetup(): void
- {
- $ruleId = $this->setupAPIRule(\uniqid() . '-api2.myapp.com');
- $this->cleanupRule($ruleId);
- }
-
- public function testCreateRuleApex(): void
- {
- $domain = \uniqid() . '.com';
- $rule = $this->createAPIRule($domain);
- $this->assertEquals(201, $rule['headers']['status-code']);
- $this->assertEquals('created', $rule['body']['status']);
- }
-
- public function testCreateRuleVcs(): void
- {
- $domain = \uniqid() . '-vcs.myapp.com';
-
- $setup = $this->setupSite();
- $siteId = $setup['siteId'];
- $deploymentId = $setup['deploymentId'];
-
- $this->assertNotEmpty($siteId);
- $this->assertNotEmpty($deploymentId);
-
- $rule = $this->createSiteRule('commit-' . $domain, $siteId);
- $this->assertEquals(201, $rule['headers']['status-code']);
- $this->cleanupRule($rule['body']['$id']);
-
- $rule = $this->createSiteRule('branch-' . $domain, $siteId);
- $this->assertEquals(201, $rule['headers']['status-code']);
- $this->cleanupRule($rule['body']['$id']);
-
- $rule = $this->createSiteRule('anything-' . $domain, $siteId);
- $this->assertEquals(201, $rule['headers']['status-code']);
- $this->cleanupRule($rule['body']['$id']);
-
- $sitesDomain = \explode(',', System::getEnv('_APP_DOMAIN_SITES', ''))[0];
- $domain = \uniqid() . '-vcs.' . $sitesDomain;
-
- $rule = $this->createSiteRule('commit-' . $domain, $siteId);
- $this->assertEquals(400, $rule['headers']['status-code']);
-
- $rule = $this->createSiteRule('branch-' . $domain, $siteId);
- $this->assertEquals(400, $rule['headers']['status-code']);
-
- $rule = $this->createSiteRule('subdomain.anything-' . $domain, $siteId);
- $this->assertEquals(400, $rule['headers']['status-code']);
-
- $rule = $this->createSiteRule('anything-' . $domain, $siteId);
- $this->assertEquals(201, $rule['headers']['status-code']);
- $this->cleanupRule($rule['body']['$id']);
- }
-
- public function testCreateAPIRule(): void
- {
- $domain = \uniqid() . '-api.custom.localhost';
-
- $proxyClient = new Client();
- $proxyClient->setEndpoint('http://appwrite.test');
- $proxyClient->addHeader('x-appwrite-hostname', $domain);
-
- $response = $proxyClient->call(Client::METHOD_GET, '/versions');
- $this->assertEquals(401, $response['headers']['status-code']);
-
- $ruleId = $this->setupAPIRule($domain);
-
- $this->assertNotEmpty($ruleId);
-
- $response = $proxyClient->call(Client::METHOD_GET, '/versions');
- $this->assertEquals(200, $response['headers']['status-code']);
- $this->assertEquals(APP_VERSION_STABLE, $response['body']['server']);
-
- $this->cleanupRule($ruleId);
-
- $rule = $this->createAPIRule('http://' . $domain);
- $this->assertEquals(400, $rule['headers']['status-code']);
-
- $rule = $this->createAPIRule('https://' . $domain);
- $this->assertEquals(400, $rule['headers']['status-code']);
-
- $rule = $this->createAPIRule('wss://' . $domain);
- $this->assertEquals(400, $rule['headers']['status-code']);
-
- $rule = $this->createAPIRule($domain . '/some-path');
- $this->assertEquals(400, $rule['headers']['status-code']);
- }
-
- public function testCreateRedirectRule(): void
- {
- $domain = \uniqid() . '-redirect.custom.localhost';
-
- $proxyClient = new Client();
- $proxyClient->setEndpoint('http://appwrite.test');
- $proxyClient->addHeader('x-appwrite-hostname', $domain);
-
- $response = $proxyClient->call(Client::METHOD_GET, '/todos/1');
- $this->assertEquals(401, $response['headers']['status-code']);
-
- $siteId = $this->setupSite()['siteId'];
-
- $ruleId = $this->setupRedirectRule($domain, 'https://jsonplaceholder.typicode.com/todos/1', 301, 'site', $siteId);
- $this->assertNotEmpty($ruleId);
-
- $response = $proxyClient->call(Client::METHOD_GET, '/todos/1');
- $this->assertEquals(200, $response['headers']['status-code']);
- $this->assertEquals(1, $response['body']['id']);
-
- $response = $proxyClient->call(Client::METHOD_GET, '/');
- $this->assertEquals(200, $response['headers']['status-code']);
- $this->assertEquals(1, $response['body']['id']);
-
- $response = $proxyClient->call(Client::METHOD_GET, '/', followRedirects: false);
- $this->assertEquals(301, $response['headers']['status-code']);
- $this->assertEquals('https://jsonplaceholder.typicode.com/todos/1', $response['headers']['location']);
-
- $domain = \uniqid() . '-redirect-307.custom.localhost';
- $ruleId = $this->setupRedirectRule($domain, 'https://jsonplaceholder.typicode.com/todos/1', 307, 'site', $siteId);
- $this->assertNotEmpty($ruleId);
-
- $proxyClient = new Client();
- $proxyClient->setEndpoint('http://appwrite.test');
- $proxyClient->addHeader('x-appwrite-hostname', $domain);
-
- $response = $proxyClient->call(Client::METHOD_GET, '/', followRedirects: false);
- $this->assertEquals(307, $response['headers']['status-code']);
- $this->assertEquals('https://jsonplaceholder.typicode.com/todos/1', $response['headers']['location']);
-
- $rules = $this->listRules([
- 'queries' => [
- Query::equal('type', ['redirect'])->toString(),
- Query::equal('trigger', ['manual'])->toString(),
- Query::equal('deploymentResourceType', ['site'])->toString(),
- Query::equal('deploymentResourceId', [$siteId])->toString(),
- ],
- ]);
- $this->assertEquals(200, $rules['headers']['status-code']);
- $this->assertEquals(2, $rules['body']['total']);
-
- $this->cleanupSite($siteId);
- $this->cleanupRule($ruleId);
- }
-
- public function testCreateFunctionRule(): void
- {
- $domain = \uniqid() . '-function.custom.localhost';
-
- $proxyClient = new Client();
- $proxyClient->setEndpoint('http://appwrite.test');
- $proxyClient->addHeader('x-appwrite-hostname', $domain);
-
- $response = $proxyClient->call(Client::METHOD_GET, '/ping');
- $this->assertEquals(401, $response['headers']['status-code']);
-
- $setup = $this->setupFunction();
- $functionId = $setup['functionId'];
- $deploymentId = $setup['deploymentId'];
-
- $this->assertNotEmpty($functionId);
- $this->assertNotEmpty($deploymentId);
-
- $ruleId = $this->setupFunctionRule($domain, $functionId);
- $this->assertNotEmpty($ruleId);
-
- $response = $proxyClient->call(Client::METHOD_GET, '/ping');
- $this->assertEquals(200, $response['headers']['status-code']);
- $this->assertEquals($functionId, $response['body']['APPWRITE_FUNCTION_ID']);
-
- $this->cleanupRule($ruleId);
-
- $this->cleanupFunction($functionId);
-
- $this->assertEventually(function () use ($functionId, $deploymentId) {
- $rules = $this->listRules([
- 'queries' => [
- Query::limit(1)->toString(),
- Query::equal('type', ['deployment'])->toString(),
- Query::equal('deploymentResourceType', ['function'])->toString(),
- Query::equal('deploymentResourceId', [$functionId])->toString(),
- ]
- ]);
- $this->assertEquals(200, $rules['headers']['status-code']);
- $this->assertEquals(0, $rules['body']['total']);
- $this->assertCount(0, $rules['body']['rules']);
-
- $rules = $this->listRules([
- 'queries' => [
- Query::limit(1)->toString(),
- Query::equal('type', ['deployment'])->toString(),
- Query::equal('deploymentId', [$deploymentId])->toString()
- ]
- ]);
- $this->assertEquals(200, $rules['headers']['status-code']);
- $this->assertEquals(0, $rules['body']['total']);
- $this->assertCount(0, $rules['body']['rules']);
- });
- }
-
- public function testCreateSiteRule(): void
- {
- $domain = \uniqid() . '-site.custom.localhost';
-
- $proxyClient = new Client();
- $proxyClient->setEndpoint('http://appwrite.test');
- $proxyClient->addHeader('x-appwrite-hostname', $domain);
-
- $response = $proxyClient->call(Client::METHOD_GET, '/contact');
- $this->assertEquals(401, $response['headers']['status-code']);
-
- $setup = $this->setupSite();
- $siteId = $setup['siteId'];
- $deploymentId = $setup['deploymentId'];
-
- $this->assertNotEmpty($siteId);
- $this->assertNotEmpty($deploymentId);
-
- $ruleId = $this->setupSiteRule($domain, $siteId);
- $this->assertNotEmpty($ruleId);
- $rule = $this->getRule($ruleId);
- $this->assertSame(200, $rule['headers']['status-code']);
- $this->assertSame('created', $rule['body']['status']);
-
- $response = $proxyClient->call(Client::METHOD_GET, '/contact');
- $this->assertEquals(200, $response['headers']['status-code']);
- $this->assertStringContainsString('Contact page', $response['body']);
-
- // Wildcard domains automatically get verified status
- $domains = [
- \uniqid() . '.sites.localhost',
- \uniqid() . '.rebranded.localhost',
- ];
- foreach ($domains as $domain) {
- $wildcardRuleId = $this->setupSiteRule($domain, $siteId);
- $this->assertNotEmpty($wildcardRuleId);
- $rule = $this->getRule($wildcardRuleId);
- $this->assertSame(200, $rule['headers']['status-code']);
- $this->assertSame('verified', $rule['body']['status']);
- $this->cleanupRule($wildcardRuleId);
- }
-
- $rules = $this->listRules([
- 'queries' => [
- Query::limit(1)->toString(),
- Query::equal('trigger', ['deployment'])->toString(),
- Query::equal('type', ['deployment'])->toString(),
- Query::equal('deploymentResourceType', ['site'])->toString(),
- Query::equal('deploymentResourceId', [$siteId])->toString(),
- ]
- ]);
- $this->assertEquals(200, $rules['headers']['status-code']);
- $this->assertGreaterThan(0, $rules['body']['total']);
-
- $this->cleanupRule($ruleId);
-
- $this->cleanupSite($siteId);
-
- $this->assertEventually(function () use ($siteId, $deploymentId) {
- $rules = $this->listRules([
- 'queries' => [
- Query::limit(1)->toString(),
- Query::equal('type', ['deployment'])->toString(),
- Query::equal('deploymentResourceType', ['site'])->toString(),
- Query::equal('deploymentResourceId', [$siteId])->toString(),
- ]
- ]);
- $this->assertEquals(200, $rules['headers']['status-code']);
- $this->assertEquals(0, $rules['body']['total']);
- $this->assertCount(0, $rules['body']['rules']);
-
- $rules = $this->listRules([
- 'queries' => [
- Query::limit(1)->toString(),
- Query::equal('type', ['deployment'])->toString(),
- Query::equal('deploymentId', [$deploymentId])->toString()
- ]
- ]);
- $this->assertEquals(200, $rules['headers']['status-code']);
- $this->assertEquals(0, $rules['body']['total']);
- $this->assertCount(0, $rules['body']['rules']);
- });
- }
-
- public function testCreateSiteBranchRule(): void
- {
- $domain = \uniqid() . '-site-branch.custom.localhost';
-
- $setup = $this->setupSite();
- $siteId = $setup['siteId'];
- $deploymentId = $setup['deploymentId'];
-
- $this->assertNotEmpty($siteId);
- $this->assertNotEmpty($deploymentId);
-
- $ruleId = $this->setupSiteRule($domain, $siteId, 'dev');
- $this->assertNotEmpty($ruleId);
-
- $rule = $this->getRule($ruleId);
- $this->assertEquals(200, $rule['headers']['status-code']);
-
- $this->cleanupRule($ruleId);
- }
-
- public function testCreateFunctionBranchRule(): void
- {
- $domain = \uniqid() . '-function-branch.custom.localhost';
-
- $setup = $this->setupFunction();
- $functionId = $setup['functionId'];
- $deploymentId = $setup['deploymentId'];
-
- $this->assertNotEmpty($functionId);
- $this->assertNotEmpty($deploymentId);
-
- $ruleId = $this->setupFunctionRule($domain, $functionId, 'dev');
- $this->assertNotEmpty($ruleId);
-
- $rule = $this->getRule($ruleId);
- $this->assertEquals(200, $rule['headers']['status-code']);
-
- $this->cleanupRule($ruleId);
-
- $this->cleanupFunction($functionId);
- }
-
- public function testUpdateRule(): void
- {
- // Create function appwrite-network domain
- $functionsDomain = \explode(',', System::getEnv('_APP_DOMAIN_FUNCTIONS', ''))[0];
- $domain = \uniqid() . '-cname-api.' . $functionsDomain;
-
- $rule = $this->createAPIRule($domain);
- $this->assertEquals(201, $rule['headers']['status-code']);
- $this->assertEquals('verified', $rule['body']['status']);
-
- $this->cleanupRule($rule['body']['$id']);
-
- // Create site appwrite-network domain
- $sitesDomain = \explode(',', System::getEnv('_APP_DOMAIN_SITES', ''))[0];
- $domain = \uniqid() . '-cname-api.' . $sitesDomain;
-
- $rule = $this->createAPIRule($domain);
- $this->assertEquals(201, $rule['headers']['status-code']);
- $this->assertEquals('verified', $rule['body']['status']);
-
- $this->cleanupRule($rule['body']['$id']);
-
- // Create + update
- $domain = \uniqid() . '-cname-api.custom.com';
-
- $rule = $this->createAPIRule($domain);
- $this->assertEquals(201, $rule['headers']['status-code']);
- $this->assertEquals('created', $rule['body']['status']);
-
- $ruleId = $rule['body']['$id'];
-
- $rule = $this->updateRuleVerification($ruleId);
- $this->assertEquals(400, $rule['headers']['status-code']);
-
- $this->cleanupRule($ruleId);
- }
-
- public function testGetRule()
- {
- $domain = \uniqid() . '-get.custom.localhost';
- $ruleId = $this->setupAPIRule($domain);
-
- $this->assertNotEmpty($ruleId);
-
- $rule = $this->getRule($ruleId);
- $this->assertEquals(200, $rule['headers']['status-code']);
- $this->assertEquals($domain, $rule['body']['domain']);
- $this->assertEquals('manual', $rule['body']['trigger']);
- $this->assertArrayHasKey('$id', $rule['body']);
- $this->assertArrayHasKey('domain', $rule['body']);
- $this->assertArrayHasKey('type', $rule['body']);
- $this->assertArrayHasKey('redirectUrl', $rule['body']);
- $this->assertArrayHasKey('redirectStatusCode', $rule['body']);
- $this->assertArrayHasKey('deploymentResourceType', $rule['body']);
- $this->assertArrayHasKey('deploymentId', $rule['body']);
- $this->assertArrayHasKey('deploymentResourceId', $rule['body']);
- $this->assertArrayHasKey('deploymentVcsProviderBranch', $rule['body']);
- $this->assertArrayHasKey('logs', $rule['body']);
- $this->assertArrayHasKey('renewAt', $rule['body']);
-
- $this->cleanupRule($ruleId);
- }
-
- public function testListRules()
- {
- $rules = $this->listRules();
- $this->assertEquals(200, $rules['headers']['status-code']);
- foreach ($rules['body']['rules'] as $rule) {
- $rule = $this->deleteRule($rule['$id']);
- $this->assertEquals(204, $rule['headers']['status-code']);
- }
-
- $rules = $this->listRules();
- $this->assertEquals(200, $rules['headers']['status-code']);
- $this->assertEquals(0, $rules['body']['total']);
- $this->assertCount(0, $rules['body']['rules']);
-
- $rule1Domain = \uniqid() . '-list1.custom.localhost';
- $rule1Id = $this->setupAPIRule($rule1Domain);
- $this->assertNotEmpty($rule1Id);
-
- $rules = $this->listRules();
- $this->assertEquals(200, $rules['headers']['status-code']);
- $this->assertEquals(1, $rules['body']['total']);
- $this->assertCount(1, $rules['body']['rules']);
- $this->assertEquals($rule1Domain, $rules['body']['rules'][0]['domain']);
-
- $this->assertEquals('manual', $rules['body']['rules'][0]['trigger']);
- $this->assertArrayHasKey('$id', $rules['body']['rules'][0]);
- $this->assertArrayHasKey('domain', $rules['body']['rules'][0]);
- $this->assertArrayHasKey('type', $rules['body']['rules'][0]);
- $this->assertArrayHasKey('redirectUrl', $rules['body']['rules'][0]);
- $this->assertArrayHasKey('redirectStatusCode', $rules['body']['rules'][0]);
- $this->assertArrayHasKey('deploymentResourceType', $rules['body']['rules'][0]);
- $this->assertArrayHasKey('deploymentId', $rules['body']['rules'][0]);
- $this->assertArrayHasKey('deploymentResourceId', $rules['body']['rules'][0]);
- $this->assertArrayHasKey('deploymentVcsProviderBranch', $rules['body']['rules'][0]);
- $this->assertArrayHasKey('logs', $rules['body']['rules'][0]);
- $this->assertArrayHasKey('renewAt', $rules['body']['rules'][0]);
-
- $rule2Domain = \uniqid() . '-list1.custom.localhost';
- $rule2Id = $this->setupAPIRule($rule2Domain);
- $this->assertNotEmpty($rule2Id);
-
- $rules = $this->listRules();
- $this->assertEquals(200, $rules['headers']['status-code']);
- $this->assertEquals(2, $rules['body']['total']);
- $this->assertCount(2, $rules['body']['rules']);
-
- $rules = $this->listRules([
- 'queries' => [
- Query::limit(1)->toString()
- ]
- ]);
- $this->assertEquals(200, $rules['headers']['status-code']);
- $this->assertEquals(2, $rules['body']['total']);
- $this->assertCount(1, $rules['body']['rules']);
-
- $rules = $this->listRules([
- 'queries' => [
- Query::equal('$id', [$rule1Id])->toString()
- ]
- ]);
- $this->assertEquals(200, $rules['headers']['status-code']);
- $this->assertCount(1, $rules['body']['rules']);
- $this->assertEquals($rule1Domain, $rules['body']['rules'][0]['domain']);
-
- $rules = $this->listRules([
- 'queries' => [
- Query::orderDesc('$id')->toString()
- ]
- ]);
- $this->assertEquals(200, $rules['headers']['status-code']);
- $this->assertCount(2, $rules['body']['rules']);
- $this->assertEquals($rule2Id, $rules['body']['rules'][0]['$id']);
-
- $rules = $this->listRules([
- 'queries' => [
- Query::equal('domain', [$rule2Domain])->toString()
- ]
- ]);
- $this->assertEquals(200, $rules['headers']['status-code']);
- $this->assertCount(1, $rules['body']['rules']);
- $this->assertEquals($rule2Id, $rules['body']['rules'][0]['$id']);
-
- $rules = $this->listRules([
- 'search' => $rule1Domain,
- 'queries' => [ Query::orderDesc('$createdAt')->toString() ]
- ]);
-
- $this->assertEquals(200, $rules['headers']['status-code']);
- $ruleIds = \array_column($rules['body']['rules'], '$id');
- $this->assertContains($rule1Id, $ruleIds);
-
- $rules = $this->listRules([
- 'search' => $rule2Domain,
- 'queries' => [ Query::orderDesc('$createdAt')->toString() ]
- ]);
- $this->assertEquals(200, $rules['headers']['status-code']);
- $ruleIds = \array_column($rules['body']['rules'], '$id');
- $this->assertContains($rule2Id, $ruleIds);
-
- $rules = $this->listRules([
- 'search' => $rule1Id,
- 'queries' => [ Query::orderDesc('$createdAt')->toString() ]
- ]);
- $this->assertEquals(200, $rules['headers']['status-code']);
- $ruleDomains = \array_column($rules['body']['rules'], 'domain');
- $this->assertContains($rule1Domain, $ruleDomains);
-
- $rules = $this->listRules([
- 'search' => $rule2Id,
- 'queries' => [ Query::orderDesc('$createdAt')->toString() ]
- ]);
- $this->assertEquals(200, $rules['headers']['status-code']);
- $ruleDomains = \array_column($rules['body']['rules'], 'domain');
- $this->assertContains($rule2Domain, $ruleDomains);
-
- $rules = $this->listRules();
- $this->assertEquals(200, $rules['headers']['status-code']);
- foreach ($rules['body']['rules'] as $rule) {
- $rule = $this->deleteRule($rule['$id']);
- $this->assertEquals(204, $rule['headers']['status-code']);
- }
-
- $rules = $this->listRules();
- $this->assertEquals(200, $rules['headers']['status-code']);
- $this->assertEquals(0, $rules['body']['total']);
- $this->assertCount(0, $rules['body']['rules']);
- }
-
- public function testRuleVerification(): void
- {
-
- // 1. Site rule can verify
- $site = $this->setupSite();
- $siteId = $site['siteId'];
-
- $rule = $this->createSiteRule('stage-site.webapp.com', $siteId);
- $this->assertEquals(201, $rule['headers']['status-code']);
- $this->assertEquals('verifying', $rule['body']['status']);
- $this->assertEmpty($rule['body']['logs']);
- $this->assertNotEmpty($rule['body']['$id']);
- $ruleId = $rule['body']['$id'];
-
- $rule = $this->updateRuleVerification($ruleId);
- $this->assertEquals(200, $rule['headers']['status-code']);
- $this->assertEquals($ruleId, $rule['body']['$id']);
- $this->assertEquals('verifying', $rule['body']['status']);
- $this->assertEmpty($rule['body']['logs']);
-
- $this->cleanupRule($rule['body']['$id']);
- $this->cleanupSite($siteId);
-
- // 2. Function rule can verify
- $function = $this->setupFunction();
- $functionId = $function['functionId'];
-
- $rule = $this->createFunctionRule('stage-function.webapp.com', $functionId);
- $this->assertEquals(201, $rule['headers']['status-code']);
- $this->assertEquals('verifying', $rule['body']['status']);
- $this->assertEmpty($rule['body']['logs']);
- $this->cleanupRule($rule['body']['$id']);
-
- $rule = $this->createAPIRule('stage-site.webapp.com');
- $this->assertEquals(201, $rule['headers']['status-code']);
- $this->assertEquals('created', $rule['body']['status']);
- $this->assertStringContainsString('has incorrect CNAME value', $rule['body']['logs']);
- $this->cleanupRule($rule['body']['$id']);
-
- $this->cleanupFunction($functionId);
-
- // 3. Wrong A record fails to verify
- $rule = $this->createAPIRule('wrong-a-webapp.com');
- $this->assertEquals(201, $rule['headers']['status-code']);
- $this->assertEquals('created', $rule['body']['status']);
- $this->assertStringContainsString('is missing CNAME record', $rule['body']['logs']);
-
- $ruleId = $rule['body']['$id'];
- $rule = $this->updateRuleVerification($ruleId);
- $this->assertEquals(400, $rule['headers']['status-code']);
- $this->assertStringContainsString('is missing CNAME record', $rule['body']['message']);
-
- $rule = $this->getRule($ruleId);
- $this->assertEquals(200, $rule['headers']['status-code']);
- $this->assertEquals('created', $rule['body']['status']);
-
- $this->cleanupRule($ruleId);
-
- // 4. Correct A record can verify
- $rule = $this->createAPIRule('webapp.com');
- $this->assertEquals(201, $rule['headers']['status-code']);
- $this->assertEquals('verifying', $rule['body']['status']);
- $this->assertEmpty($rule['body']['logs']);
-
- $this->cleanupRule($rule['body']['$id']);
-
- // 5. Correct CNAME record can verify (no CAA record)
- $rule = $this->createAPIRule('stage.webapp.com');
- $this->assertEquals(201, $rule['headers']['status-code']);
- $this->assertEquals('verifying', $rule['body']['status']);
- $this->assertEmpty($rule['body']['logs']);
-
- $this->cleanupRule($rule['body']['$id']);
-
- // 6. Missing CNAME record fails to verify
- $rule = $this->createAPIRule('stage-missing-cname.webapp.com');
- $this->assertEquals(201, $rule['headers']['status-code']);
- $this->assertEquals('created', $rule['body']['status']);
- $this->assertStringContainsString('is missing CNAME record', $rule['body']['logs']);
-
- $ruleId = $rule['body']['$id'];
- $rule = $this->updateRuleVerification($ruleId);
- $this->assertEquals(400, $rule['headers']['status-code']);
- $this->assertStringContainsString('is missing CNAME record', $rule['body']['message']);
-
- $rule = $this->getRule($ruleId);
- $this->assertEquals(200, $rule['headers']['status-code']);
- $this->assertEquals('created', $rule['body']['status']);
-
- $this->cleanupRule($ruleId);
-
- // 7. Wrong CNAME record fails to verify
- $rule = $this->createAPIRule('stage-wrong-cname.webapp.com');
- $this->assertEquals(201, $rule['headers']['status-code']);
- $this->assertEquals('created', $rule['body']['status']);
- $this->assertStringContainsString('has incorrect CNAME value', $rule['body']['logs']);
-
- $ruleId = $rule['body']['$id'];
- $rule = $this->updateRuleVerification($ruleId);
- $this->assertEquals(400, $rule['headers']['status-code']);
- $this->assertStringContainsString('has incorrect CNAME value', $rule['body']['message']);
-
- $rule = $this->getRule($ruleId);
- $this->assertEquals(200, $rule['headers']['status-code']);
- $this->assertEquals('created', $rule['body']['status']);
-
- $this->cleanupRule($ruleId);
-
- // 8. Wrong CAA record fails to verify
- $rule = $this->createAPIRule('stage-wrong-caa.webapp.com');
- $this->assertEquals(201, $rule['headers']['status-code']);
- $this->assertEquals('created', $rule['body']['status']);
- $this->assertStringContainsString('has incorrect CAA value', $rule['body']['logs']);
-
- $ruleId = $rule['body']['$id'];
- $rule = $this->updateRuleVerification($ruleId);
- $this->assertEquals(400, $rule['headers']['status-code']);
- $this->assertStringContainsString('has incorrect CAA value', $rule['body']['message']);
-
- $rule = $this->getRule($ruleId);
- $this->assertEquals(200, $rule['headers']['status-code']);
- $this->assertEquals('created', $rule['body']['status']);
-
- $this->cleanupRule($ruleId);
-
- // 9. Correct CAA record can verify
- $rule = $this->createAPIRule('stage-correct-caa.webapp.com');
- $this->assertEquals(201, $rule['headers']['status-code']);
- $this->assertEquals('verifying', $rule['body']['status']);
- $this->assertEmpty($rule['body']['logs']);
-
- $this->cleanupRule($rule['body']['$id']);
- }
-
- public function testUpdateRuleVerificationWithSameDataUpdatesTimestamp(): void
- {
- $domain = \uniqid() . '-timestamp-test.webapp.com';
- $rule = $this->createAPIRule($domain);
-
- $this->assertEquals(201, $rule['headers']['status-code']);
- $this->assertEquals('created', $rule['body']['status']);
- $this->assertNotEmpty($rule['body']['logs']);
-
- $ruleId = $rule['body']['$id'];
- $initialUpdatedAt = $rule['body']['$updatedAt'];
- $initiallogs = $rule['body']['logs'];
-
- sleep(1);
-
- $updatedRule = $this->updateRuleVerification($ruleId);
-
- $this->assertEquals(400, $updatedRule['headers']['status-code']);
- $this->assertStringContainsString($initiallogs, $updatedRule['body']['message']);
-
- $ruleAfterUpdate = $this->getRule($ruleId);
- $this->assertEquals(200, $ruleAfterUpdate['headers']['status-code']);
- $this->assertEquals('created', $ruleAfterUpdate['body']['status']);
- $this->assertEquals($initiallogs, $ruleAfterUpdate['body']['logs']);
- $this->assertNotEquals($initialUpdatedAt, $ruleAfterUpdate['body']['$updatedAt']);
-
- $initialTime = new \DateTime($initialUpdatedAt);
- $updatedTime = new \DateTime($ruleAfterUpdate['body']['$updatedAt']);
- $this->assertGreaterThan($initialTime, $updatedTime);
-
- $this->cleanupRule($ruleId);
- }
}
diff --git a/tests/e2e/Services/Proxy/ProxyHelpers.php b/tests/e2e/Services/Proxy/ProxyHelpers.php
new file mode 100644
index 0000000000..6f15abdad8
--- /dev/null
+++ b/tests/e2e/Services/Proxy/ProxyHelpers.php
@@ -0,0 +1,293 @@
+client->call(Client::METHOD_GET, '/proxy/rules', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), $params);
+
+ return $rule;
+ }
+
+ protected function createAPIRule(string $domain): mixed
+ {
+ $rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/api', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'domain' => $domain,
+ ]);
+
+ return $rule;
+ }
+
+ protected function updateRuleStatus(string $ruleId): mixed
+ {
+ $rule = $this->client->call(Client::METHOD_PATCH, '/proxy/rules/' . $ruleId . '/status', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), []);
+
+ return $rule;
+ }
+
+ protected function createSiteRule(string $domain, string $siteId, string $branch = ''): mixed
+ {
+ $rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/site', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'domain' => $domain,
+ 'siteId' => $siteId,
+ 'branch' => $branch,
+ ]);
+
+ return $rule;
+ }
+
+ protected function getRule(string $ruleId): mixed
+ {
+ $rule = $this->client->call(Client::METHOD_GET, '/proxy/rules/' . $ruleId, array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), []);
+
+ return $rule;
+ }
+
+ protected function createRedirectRule(string $domain, string $url, int $statusCode, string $resourceType, string $resourceId): mixed
+ {
+ $rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/redirect', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'domain' => $domain,
+ 'url' => $url,
+ 'statusCode' => $statusCode,
+ 'resourceType' => $resourceType,
+ 'resourceId' => $resourceId,
+ ]);
+
+ return $rule;
+ }
+
+ protected function createFunctionRule(string $domain, string $functionId, string $branch = ''): mixed
+ {
+ $rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/function', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'domain' => $domain,
+ 'functionId' => $functionId,
+ 'branch' => $branch,
+ ]);
+
+ return $rule;
+ }
+
+ protected function deleteRule(string $ruleId): mixed
+ {
+ $rule = $this->client->call(Client::METHOD_DELETE, '/proxy/rules/' . $ruleId, array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), []);
+
+ return $rule;
+ }
+
+ protected function setupAPIRule(string $domain): string
+ {
+ $rule = $this->createAPIRule($domain);
+
+ $this->assertEquals(201, $rule['headers']['status-code'], 'Failed to setup rule: ' . \json_encode($rule));
+
+ return $rule['body']['$id'];
+ }
+
+ protected function setupRedirectRule(string $domain, string $url, int $statusCode, string $resourceType, string $resourceId): string
+ {
+ $rule = $this->createRedirectRule($domain, $url, $statusCode, $resourceType, $resourceId);
+
+ $this->assertEquals(201, $rule['headers']['status-code'], 'Failed to setup rule: ' . \json_encode($rule));
+
+ return $rule['body']['$id'];
+ }
+
+ protected function setupFunctionRule(string $domain, string $functionId, string $branch = ''): string
+ {
+ $rule = $this->createFunctionRule($domain, $functionId, $branch);
+
+ $this->assertEquals(201, $rule['headers']['status-code'], 'Failed to setup rule: ' . \json_encode($rule));
+
+ return $rule['body']['$id'];
+ }
+
+ protected function setupSiteRule(string $domain, string $siteId, string $branch = ''): string
+ {
+ $rule = $this->createSiteRule($domain, $siteId, $branch);
+
+ $this->assertEquals(201, $rule['headers']['status-code'], 'Failed to setup rule: ' . \json_encode($rule));
+
+ return $rule['body']['$id'];
+ }
+
+ protected function cleanupRule(string $ruleId): void
+ {
+ $rule = $this->deleteRule($ruleId);
+ $this->assertEquals(204, $rule['headers']['status-code'], 'Failed to cleanup rule: ' . \json_encode($rule));
+ }
+
+ protected function cleanupSite(string $siteId): void
+ {
+ $site = $this->client->call(Client::METHOD_DELETE, '/sites/' . $siteId, array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), []);
+
+ $this->assertEquals(204, $site['headers']['status-code'], 'Failed to cleanup site: ' . \json_encode($site));
+ }
+
+ protected function cleanupFunction(string $functionId): void
+ {
+ $function = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), []);
+
+ $this->assertEquals(204, $function['headers']['status-code'], 'Failed to cleanup function: ' . \json_encode($function));
+ }
+
+ protected function setupSite(): mixed
+ {
+ // Site
+ $site = $this->client->call(Client::METHOD_POST, '/sites', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'siteId' => ID::unique(),
+ 'name' => 'Proxy site',
+ 'framework' => 'other',
+ 'adapter' => 'static',
+ 'buildRuntime' => 'static-1',
+ 'outputDirectory' => './',
+ 'buildCommand' => '',
+ 'installCommand' => '',
+ 'fallbackFile' => '',
+ ]);
+
+ $this->assertEquals($site['headers']['status-code'], 201, 'Setup site failed with status code: ' . $site['headers']['status-code'] . ' and response: ' . json_encode($site['body'], JSON_PRETTY_PRINT));
+
+ $siteId = $site['body']['$id'];
+
+ // Deployment
+ $deployment = $this->client->call(Client::METHOD_POST, '/sites/' . $siteId . '/deployments', array_merge([
+ 'content-type' => 'multipart/form-data',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'code' => $this->packageSite('static'),
+ 'activate' => 'true'
+ ]);
+
+ $this->assertEquals($deployment['headers']['status-code'], 202, 'Setup deployment failed with status code: ' . $deployment['headers']['status-code'] . ' and response: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT));
+ $deploymentId = $deployment['body']['$id'] ?? '';
+
+ $this->assertEventually(function () use ($siteId, $deploymentId) {
+ $site = $this->client->call(Client::METHOD_GET, '/sites/' . $siteId, array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+ $this->assertEquals($deploymentId, $site['body']['deploymentId'], 'Deployment is not activated, deployment: ' . json_encode($site['body'], JSON_PRETTY_PRINT));
+ }, 120000, 500);
+
+ return ['siteId' => $siteId, 'deploymentId' => $deploymentId];
+ }
+
+ protected function setupFunction(): mixed
+ {
+ // Function
+ $function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'functionId' => ID::unique(),
+ 'runtime' => 'node-22',
+ 'name' => 'Proxy Function',
+ 'entrypoint' => 'index.js',
+ 'commands' => '',
+ 'execute' => ['any']
+ ]);
+
+ $this->assertEquals($function['headers']['status-code'], 201, 'Setup function failed with status code: ' . $function['headers']['status-code'] . ' and response: ' . json_encode($function['body'], JSON_PRETTY_PRINT));
+
+ $functionId = $function['body']['$id'];
+
+ // Deployment
+ $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([
+ 'content-type' => 'multipart/form-data',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'code' => $this->packageFunction('basic'),
+ 'activate' => 'true'
+ ]);
+
+ $this->assertEquals($deployment['headers']['status-code'], 202, 'Setup deployment failed with status code: ' . $deployment['headers']['status-code'] . ' and response: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT));
+ $deploymentId = $deployment['body']['$id'] ?? '';
+
+ $this->assertEventually(function () use ($functionId, $deploymentId) {
+ $function = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId, array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()));
+ $this->assertEquals($deploymentId, $function['body']['deploymentId'], 'Deployment is not activated, deployment: ' . json_encode($function['body'], JSON_PRETTY_PRINT));
+ }, 100000, 500);
+
+ return ['functionId' => $functionId, 'deploymentId' => $deploymentId];
+ }
+
+ private function packageSite(string $site): CURLFile
+ {
+ $stdout = '';
+ $stderr = '';
+
+ $folderPath = realpath(__DIR__ . '/../../../resources/sites') . "/$site";
+ $tarPath = "$folderPath/code.tar.gz";
+
+ Console::execute("cd $folderPath && tar --exclude code.tar.gz --exclude node_modules -czf code.tar.gz .", '', $stdout, $stderr);
+
+ if (filesize($tarPath) > 1024 * 1024 * 5) {
+ throw new \Exception('Code package is too large. Use the chunked upload method instead.');
+ }
+
+ return new CURLFile($tarPath, 'application/x-gzip', \basename($tarPath));
+ }
+
+ private function packageFunction(string $function): CURLFile
+ {
+ $stdout = '';
+ $stderr = '';
+
+ $folderPath = realpath(__DIR__ . '/../../../resources/functions') . "/$function";
+ $tarPath = "$folderPath/code.tar.gz";
+
+ Console::execute("cd $folderPath && tar --exclude code.tar.gz --exclude node_modules -czf code.tar.gz .", '', $stdout, $stderr);
+
+ if (filesize($tarPath) > 1024 * 1024 * 5) {
+ throw new \Exception('Code package is too large. Use the chunked upload method instead.');
+ }
+
+ return new CURLFile($tarPath, 'application/x-gzip', \basename($tarPath));
+ }
+}
diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php
index 653b5919eb..9cca689780 100644
--- a/tests/e2e/Services/Sites/SitesCustomServerTest.php
+++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php
@@ -104,14 +104,17 @@ class SitesCustomServerTest extends Scope
$this->assertEquals('./', $site['body']['outputDirectory']);
$variable = $this->createVariable($siteId, [
+ 'variableId' => ID::unique(),
'key' => 'siteKey1',
'value' => 'siteValue1',
]);
$variable2 = $this->createVariable($siteId, [
+ 'variableId' => ID::unique(),
'key' => 'siteKey2',
'value' => 'siteValue2',
]);
$variable3 = $this->createVariable($siteId, [
+ 'variableId' => ID::unique(),
'key' => 'siteKey3',
'value' => 'siteValue3',
]);
@@ -211,6 +214,7 @@ class SitesCustomServerTest extends Scope
$this->assertEquals('Test Site', $site['body']['name']);
$variable = $this->createVariable($siteId, [
+ 'variableId' => ID::unique(),
'key' => 'siteKey1',
'value' => 'siteValue1',
'secret' => false,
@@ -223,6 +227,7 @@ class SitesCustomServerTest extends Scope
$this->assertEquals(false, $variable['body']['secret']);
$variable2 = $this->createVariable($siteId, [
+ 'variableId' => ID::unique(),
'key' => 'siteKey2',
'value' => 'siteValue2',
'secret' => false,
@@ -235,6 +240,7 @@ class SitesCustomServerTest extends Scope
$this->assertEquals(false, $variable2['body']['secret']);
$secretVariable = $this->createVariable($siteId, [
+ 'variableId' => ID::unique(),
'key' => 'siteKey3',
'value' => 'siteValue3',
'secret' => true,
@@ -330,6 +336,316 @@ class SitesCustomServerTest extends Scope
$this->cleanupSite($siteId);
}
+ public function testListVariablesWithLimit(): void
+ {
+ $site = $this->createSite([
+ 'buildRuntime' => 'node-22',
+ 'fallbackFile' => '',
+ 'framework' => 'other',
+ 'name' => 'Test List Variables Limit',
+ 'outputDirectory' => './',
+ 'siteId' => ID::unique()
+ ]);
+ $siteId = $site['body']['$id'] ?? '';
+ $this->assertEquals(201, $site['headers']['status-code']);
+
+ $variable1 = $this->createVariable($siteId, [
+ 'variableId' => ID::unique(),
+ 'key' => 'LIMIT_KEY_1',
+ 'value' => 'limit-value-1',
+ ]);
+ $this->assertEquals(201, $variable1['headers']['status-code']);
+
+ $variable2 = $this->createVariable($siteId, [
+ 'variableId' => ID::unique(),
+ 'key' => 'LIMIT_KEY_2',
+ 'value' => 'limit-value-2',
+ ]);
+ $this->assertEquals(201, $variable2['headers']['status-code']);
+
+ // List with limit of 1
+ $response = $this->listVariables($siteId, [
+ 'queries' => [
+ Query::limit(1)->toString(),
+ ],
+ 'total' => true,
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertCount(1, $response['body']['variables']);
+ $this->assertGreaterThanOrEqual(2, $response['body']['total']);
+
+ $this->cleanupSite($siteId);
+ }
+
+ public function testListVariablesWithoutTotal(): void
+ {
+ $site = $this->createSite([
+ 'buildRuntime' => 'node-22',
+ 'fallbackFile' => '',
+ 'framework' => 'other',
+ 'name' => 'Test List Variables No Total',
+ 'outputDirectory' => './',
+ 'siteId' => ID::unique()
+ ]);
+ $siteId = $site['body']['$id'] ?? '';
+ $this->assertEquals(201, $site['headers']['status-code']);
+
+ $variable = $this->createVariable($siteId, [
+ 'variableId' => ID::unique(),
+ 'key' => 'NO_TOTAL_KEY',
+ 'value' => 'no-total-value',
+ ]);
+ $this->assertEquals(201, $variable['headers']['status-code']);
+
+ // List with total=false
+ $response = $this->listVariables($siteId, [
+ 'total' => false,
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals(0, $response['body']['total']);
+ $this->assertGreaterThanOrEqual(1, \count($response['body']['variables']));
+
+ $this->cleanupSite($siteId);
+ }
+
+ public function testListVariablesCursorPagination(): void
+ {
+ $site = $this->createSite([
+ 'buildRuntime' => 'node-22',
+ 'fallbackFile' => '',
+ 'framework' => 'other',
+ 'name' => 'Test List Variables Cursor',
+ 'outputDirectory' => './',
+ 'siteId' => ID::unique()
+ ]);
+ $siteId = $site['body']['$id'] ?? '';
+ $this->assertEquals(201, $site['headers']['status-code']);
+
+ $variable1 = $this->createVariable($siteId, [
+ 'variableId' => ID::unique(),
+ 'key' => 'CURSOR_KEY_1',
+ 'value' => 'cursor-value-1',
+ ]);
+ $this->assertEquals(201, $variable1['headers']['status-code']);
+
+ $variable2 = $this->createVariable($siteId, [
+ 'variableId' => ID::unique(),
+ 'key' => 'CURSOR_KEY_2',
+ 'value' => 'cursor-value-2',
+ ]);
+ $this->assertEquals(201, $variable2['headers']['status-code']);
+
+ // Get first page with limit 1
+ $page1 = $this->listVariables($siteId, [
+ 'queries' => [
+ Query::limit(1)->toString(),
+ ],
+ 'total' => true,
+ ]);
+
+ $this->assertEquals(200, $page1['headers']['status-code']);
+ $this->assertCount(1, $page1['body']['variables']);
+ $cursorId = $page1['body']['variables'][0]['$id'];
+
+ // Get next page using cursor
+ $page2 = $this->listVariables($siteId, [
+ 'queries' => [
+ Query::limit(1)->toString(),
+ Query::cursorAfter(new Document(['$id' => $cursorId]))->toString(),
+ ],
+ 'total' => true,
+ ]);
+
+ $this->assertEquals(200, $page2['headers']['status-code']);
+ $this->assertCount(1, $page2['body']['variables']);
+ $this->assertNotEquals($cursorId, $page2['body']['variables'][0]['$id']);
+
+ $this->cleanupSite($siteId);
+ }
+
+ public function testUpdateVariableKey(): void
+ {
+ $site = $this->createSite([
+ 'buildRuntime' => 'node-22',
+ 'fallbackFile' => '',
+ 'framework' => 'other',
+ 'name' => 'Test Update Variable Key',
+ 'outputDirectory' => './',
+ 'siteId' => ID::unique()
+ ]);
+ $siteId = $site['body']['$id'] ?? '';
+ $this->assertEquals(201, $site['headers']['status-code']);
+
+ $variable = $this->createVariable($siteId, [
+ 'variableId' => ID::unique(),
+ 'key' => 'KEY_BEFORE',
+ 'value' => 'unchanged-value',
+ 'secret' => false
+ ]);
+ $this->assertEquals(201, $variable['headers']['status-code']);
+ $variableId = $variable['body']['$id'];
+
+ // Update only key
+ $response = $this->updateVariable($siteId, $variableId, [
+ 'key' => 'KEY_AFTER',
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals('KEY_AFTER', $response['body']['key']);
+ $this->assertEquals('unchanged-value', $response['body']['value']);
+
+ $this->cleanupSite($siteId);
+ }
+
+ public function testUpdateVariableValueOnly(): void
+ {
+ $site = $this->createSite([
+ 'buildRuntime' => 'node-22',
+ 'fallbackFile' => '',
+ 'framework' => 'other',
+ 'name' => 'Test Update Variable Value',
+ 'outputDirectory' => './',
+ 'siteId' => ID::unique()
+ ]);
+ $siteId = $site['body']['$id'] ?? '';
+ $this->assertEquals(201, $site['headers']['status-code']);
+
+ $variable = $this->createVariable($siteId, [
+ 'variableId' => ID::unique(),
+ 'key' => 'UNCHANGED_KEY',
+ 'value' => 'value-before',
+ 'secret' => false
+ ]);
+ $this->assertEquals(201, $variable['headers']['status-code']);
+ $variableId = $variable['body']['$id'];
+
+ // Update only value
+ $response = $this->updateVariable($siteId, $variableId, [
+ 'value' => 'value-after',
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals('UNCHANGED_KEY', $response['body']['key']);
+ $this->assertEquals('value-after', $response['body']['value']);
+
+ $this->cleanupSite($siteId);
+ }
+
+ public function testUpdateVariableNoOp(): void
+ {
+ $site = $this->createSite([
+ 'buildRuntime' => 'node-22',
+ 'fallbackFile' => '',
+ 'framework' => 'other',
+ 'name' => 'Test Update Variable NoOp',
+ 'outputDirectory' => './',
+ 'siteId' => ID::unique()
+ ]);
+ $siteId = $site['body']['$id'] ?? '';
+ $this->assertEquals(201, $site['headers']['status-code']);
+
+ $variable = $this->createVariable($siteId, [
+ 'variableId' => ID::unique(),
+ 'key' => 'NOOP_KEY',
+ 'value' => 'noop-value',
+ 'secret' => false
+ ]);
+ $this->assertEquals(201, $variable['headers']['status-code']);
+ $variableId = $variable['body']['$id'];
+
+ // Update with no parameters should fail with 400
+ $response = $this->updateVariable($siteId, $variableId, []);
+
+ $this->assertEquals(400, $response['headers']['status-code']);
+
+ $this->cleanupSite($siteId);
+ }
+
+ public function testUpdateVariableNotFound(): void
+ {
+ $site = $this->createSite([
+ 'buildRuntime' => 'node-22',
+ 'fallbackFile' => '',
+ 'framework' => 'other',
+ 'name' => 'Test Update Variable Not Found',
+ 'outputDirectory' => './',
+ 'siteId' => ID::unique()
+ ]);
+ $siteId = $site['body']['$id'] ?? '';
+ $this->assertEquals(201, $site['headers']['status-code']);
+
+ $response = $this->updateVariable($siteId, 'non-existent-id', [
+ 'key' => 'NEW_KEY',
+ 'value' => 'new-value',
+ ]);
+
+ $this->assertEquals(404, $response['headers']['status-code']);
+ $this->assertEquals('variable_not_found', $response['body']['type']);
+
+ $this->cleanupSite($siteId);
+ }
+
+ public function testCreateVariableInvalidId(): void
+ {
+ $site = $this->createSite([
+ 'buildRuntime' => 'node-22',
+ 'fallbackFile' => '',
+ 'framework' => 'other',
+ 'name' => 'Test Invalid Variable ID',
+ 'outputDirectory' => './',
+ 'siteId' => ID::unique()
+ ]);
+ $siteId = $site['body']['$id'] ?? '';
+ $this->assertEquals(201, $site['headers']['status-code']);
+
+ $variable = $this->createVariable($siteId, [
+ 'variableId' => '!invalid-id!',
+ 'key' => 'INVALID_ID_KEY',
+ 'value' => 'value',
+ ]);
+
+ $this->assertEquals(400, $variable['headers']['status-code']);
+
+ $this->cleanupSite($siteId);
+ }
+
+ public function testCreateVariableDuplicateId(): void
+ {
+ $site = $this->createSite([
+ 'buildRuntime' => 'node-22',
+ 'fallbackFile' => '',
+ 'framework' => 'other',
+ 'name' => 'Test Duplicate Variable ID',
+ 'outputDirectory' => './',
+ 'siteId' => ID::unique()
+ ]);
+ $siteId = $site['body']['$id'] ?? '';
+ $this->assertEquals(201, $site['headers']['status-code']);
+
+ $variableId = ID::unique();
+
+ $variable = $this->createVariable($siteId, [
+ 'variableId' => $variableId,
+ 'key' => 'DUP_ID_KEY_1',
+ 'value' => 'value1',
+ ]);
+ $this->assertEquals(201, $variable['headers']['status-code']);
+
+ // Attempt to create with same ID
+ $duplicate = $this->createVariable($siteId, [
+ 'variableId' => $variableId,
+ 'key' => 'DUP_ID_KEY_2',
+ 'value' => 'value2',
+ ]);
+
+ $this->assertEquals(409, $duplicate['headers']['status-code']);
+ $this->assertEquals('variable_already_exists', $duplicate['body']['type']);
+
+ $this->cleanupSite($siteId);
+ }
+
// This is first Sites test with Proxy
// If this fails, it may not be related to variables; but Router flow failing
public function testVariablesE2E(): void
@@ -351,6 +667,7 @@ class SitesCustomServerTest extends Scope
$domain = $this->setupSiteDomain($siteId);
$secretVariable = $this->createVariable($siteId, [
+ 'variableId' => ID::unique(),
'key' => 'name',
'value' => 'Appwrite',
]);
@@ -2455,6 +2772,7 @@ class SitesCustomServerTest extends Scope
// Poll for execution logs to be written (async)
// Filter by requestPath to avoid picking up screenshot worker executions
+ // Wait for both the execution entry AND its logs field to be populated
$logs = null;
$timeout = 120;
$start = \time();
@@ -2464,12 +2782,13 @@ class SitesCustomServerTest extends Scope
Query::equal('requestPath', ['/logs-inline'])->toString(),
Query::limit(1)->toString(),
]);
- if (!empty($logs['body']['executions'])) {
+ if (!empty($logs['body']['executions']) && !empty($logs['body']['executions'][0]['logs'])) {
break;
}
\usleep(500000);
}
$this->assertNotEmpty($logs['body']['executions'], 'Execution logs were not available within timeout');
+ $this->assertNotNull($logs['body']['executions'][0]['logs'], 'Execution logs content was not populated within timeout');
$this->assertEquals(200, $logs['headers']['status-code']);
$this->assertStringContainsString($deploymentId, $logs['body']['executions'][0]['deploymentId']);
$this->assertStringContainsString("GET", $logs['body']['executions'][0]['requestMethod']);
@@ -2503,11 +2822,21 @@ class SitesCustomServerTest extends Scope
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertStringContainsString("Action logs printed.", $response['body']);
- $logs = $this->listLogs($siteId, [
- Query::orderDesc('$createdAt')->toString(),
- Query::equal('requestPath', ['/logs-action'])->toString(),
- Query::limit(1)->toString(),
- ]);
+ $logs = null;
+ $start = \time();
+ while (\time() - $start < $timeout) {
+ $logs = $this->listLogs($siteId, [
+ Query::orderDesc('$createdAt')->toString(),
+ Query::equal('requestPath', ['/logs-action'])->toString(),
+ Query::limit(1)->toString(),
+ ]);
+ if (!empty($logs['body']['executions']) && !empty($logs['body']['executions'][0]['logs'])) {
+ break;
+ }
+ \usleep(500000);
+ }
+ $this->assertNotEmpty($logs['body']['executions'], 'Action execution logs were not available within timeout');
+ $this->assertNotNull($logs['body']['executions'][0]['logs'], 'Action execution logs content was not populated within timeout');
$this->assertEquals(200, $logs['headers']['status-code']);
$this->assertStringContainsString($deploymentId, $logs['body']['executions'][0]['deploymentId']);
$this->assertStringContainsString("GET", $logs['body']['executions'][0]['requestMethod']);
diff --git a/tests/e2e/Services/Storage/StorageBase.php b/tests/e2e/Services/Storage/StorageBase.php
index a3aa88db31..b2d7155208 100644
--- a/tests/e2e/Services/Storage/StorageBase.php
+++ b/tests/e2e/Services/Storage/StorageBase.php
@@ -963,6 +963,68 @@ trait StorageBase
$this->assertNotEquals($imageBefore->getImageBlob(), $imageAfter->getImageBlob());
}
+ public function testFilePreviewCacheControlOnCacheHit(): void
+ {
+ $data = $this->setupBucketFile();
+ $bucketId = $data['bucketId'];
+ $file = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucketId . '/files', array_merge([
+ 'content-type' => 'multipart/form-data',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'fileId' => ID::unique(),
+ 'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/logo.png'), 'image/png', 'logo.png'),
+ 'permissions' => [
+ Permission::read(Role::any()),
+ Permission::update(Role::any()),
+ Permission::delete(Role::any()),
+ ],
+ ]);
+ $this->assertEquals(201, $file['headers']['status-code']);
+ $this->assertNotEmpty($file['body']['$id']);
+
+ $fileId = $file['body']['$id'];
+ $params = [
+ 'width' => 123,
+ 'height' => 45,
+ 'output' => 'png',
+ 'quality' => 80,
+ ];
+ $headers = array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders());
+
+ $preview = $this->client->call(
+ Client::METHOD_GET,
+ '/storage/buckets/' . $bucketId . '/files/' . $fileId . '/preview',
+ $headers,
+ $params
+ );
+
+ $this->assertEquals(200, $preview['headers']['status-code']);
+ $this->assertEquals('image/png', $preview['headers']['content-type']);
+ $this->assertEquals('private, max-age=2592000', $preview['headers']['cache-control']);
+ $this->assertEquals('miss', $preview['headers']['x-appwrite-cache']);
+ $this->assertNotEmpty($preview['body']);
+
+ $cachedPreview = [];
+ $this->assertEventually(function () use (&$cachedPreview, $bucketId, $fileId, $headers, $params) {
+ $cachedPreview = $this->client->call(
+ Client::METHOD_GET,
+ '/storage/buckets/' . $bucketId . '/files/' . $fileId . '/preview',
+ $headers,
+ $params
+ );
+
+ $this->assertEquals('hit', $cachedPreview['headers']['x-appwrite-cache']);
+ });
+
+ $this->assertEquals(200, $cachedPreview['headers']['status-code']);
+ $this->assertEquals('image/png', $cachedPreview['headers']['content-type']);
+ $this->assertStringStartsWith('private, max-age=', $cachedPreview['headers']['cache-control']);
+ $this->assertEquals($preview['body'], $cachedPreview['body']);
+ }
+
public function testFilePreviewZstdCompression(): void
{
$data = $this->setupZstdCompressionBucket();
diff --git a/tests/e2e/Services/TablesDB/DatabasesNumericTypesTest.php b/tests/e2e/Services/TablesDB/DatabasesNumericTypesTest.php
new file mode 100644
index 0000000000..4280bfece9
--- /dev/null
+++ b/tests/e2e/Services/TablesDB/DatabasesNumericTypesTest.php
@@ -0,0 +1,482 @@
+getProject()['$id'] ?? 'default';
+ if (!empty(self::$setupCache[$cacheKey])) {
+ return self::$setupCache[$cacheKey];
+ }
+
+ $projectId = $this->getProject()['$id'];
+ $apiKey = $this->getProject()['apiKey'];
+ $headers = [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $projectId,
+ 'x-appwrite-key' => $apiKey,
+ ];
+
+ $database = $this->client->call(Client::METHOD_POST, '/tablesdb', $headers, [
+ 'databaseId' => ID::unique(),
+ 'name' => 'Numeric Types Test Database',
+ ]);
+
+ $this->assertEquals(201, $database['headers']['status-code']);
+ $databaseId = $database['body']['$id'];
+
+ $table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', $headers, [
+ 'tableId' => ID::unique(),
+ 'name' => 'Numeric Types Table',
+ 'rowSecurity' => true,
+ 'permissions' => [
+ Permission::create(Role::any()),
+ Permission::read(Role::any()),
+ ],
+ ]);
+
+ $this->assertEquals(201, $table['headers']['status-code']);
+ $tableId = $table['body']['$id'];
+
+ // Create integer column
+ $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/integer', $headers, [
+ 'key' => 'integer_field',
+ 'required' => false,
+ 'min' => -10,
+ 'max' => 10,
+ 'default' => 0,
+ ]);
+
+ // Create bigint column
+ $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/bigint', $headers, [
+ 'key' => 'bigint_field',
+ 'required' => false,
+ 'min' => -9007199254740991,
+ 'max' => 9007199254740991,
+ 'default' => 9007199254740000,
+ ]);
+
+ // Create unsigned integer column
+ $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/integer', $headers, [
+ 'key' => 'unsigned_int_field',
+ 'required' => false,
+ 'min' => 0,
+ 'max' => 100,
+ 'default' => 0,
+ 'signed' => false,
+ ]);
+
+ // Create unsigned bigint column
+ $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/bigint', $headers, [
+ 'key' => 'unsigned_bigint_field',
+ 'required' => false,
+ 'min' => 0,
+ 'max' => 9223372036854775807,
+ 'default' => 0,
+ 'signed' => false,
+ ]);
+
+ // Cache before waiting so that if waitForAllAttributes times out,
+ // subsequent calls don't try to re-create the same columns (causing 409)
+ self::$setupCache[$cacheKey] = [
+ 'databaseId' => $databaseId,
+ 'tableId' => $tableId,
+ ];
+
+ // Wait for all columns to be available
+ $this->waitForAllAttributes($databaseId, $tableId);
+
+ return self::$setupCache[$cacheKey];
+ }
+
+ /**
+ * Setup database/table without caching so mutations (update/delete) don't
+ * affect other tests that might be executed in a different order.
+ */
+ protected function setupFreshDatabaseAndTable(): array
+ {
+ $projectId = $this->getProject()['$id'];
+ $apiKey = $this->getProject()['apiKey'];
+ $headers = [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $projectId,
+ 'x-appwrite-key' => $apiKey,
+ ];
+
+ $database = $this->client->call(Client::METHOD_POST, '/tablesdb', $headers, [
+ 'databaseId' => ID::unique(),
+ 'name' => 'Numeric Types Test Database',
+ ]);
+
+ $this->assertEquals(201, $database['headers']['status-code']);
+ $databaseId = $database['body']['$id'];
+
+ $table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', $headers, [
+ 'tableId' => ID::unique(),
+ 'name' => 'Numeric Types Table',
+ 'rowSecurity' => true,
+ 'permissions' => [
+ Permission::create(Role::any()),
+ Permission::read(Role::any()),
+ ],
+ ]);
+
+ $this->assertEquals(201, $table['headers']['status-code']);
+ $tableId = $table['body']['$id'];
+
+ $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/integer', $headers, [
+ 'key' => 'integer_field',
+ 'required' => false,
+ 'min' => -10,
+ 'max' => 10,
+ 'default' => 0,
+ ]);
+
+ $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/bigint', $headers, [
+ 'key' => 'bigint_field',
+ 'required' => false,
+ 'min' => -9007199254740991,
+ 'max' => 9007199254740991,
+ 'default' => 9007199254740000,
+ ]);
+
+ $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/integer', $headers, [
+ 'key' => 'unsigned_int_field',
+ 'required' => false,
+ 'max' => 100,
+ 'default' => 0,
+ 'signed' => false,
+ ]);
+
+ $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/bigint', $headers, [
+ 'key' => 'unsigned_bigint_field',
+ 'required' => false,
+ 'max' => 9223372036854775807,
+ 'default' => 0,
+ 'signed' => false,
+ ]);
+
+ $this->waitForAllAttributes($databaseId, $tableId);
+
+ return [
+ 'databaseId' => $databaseId,
+ 'tableId' => $tableId,
+ ];
+ }
+
+ public function testCreateDatabase(): void
+ {
+ $database = $this->client->call(Client::METHOD_POST, '/tablesdb', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'databaseId' => ID::unique(),
+ 'name' => 'Numeric Types Test Database',
+ ]);
+
+ $this->assertEquals(201, $database['headers']['status-code']);
+ }
+
+ public function testCreateTable(): void
+ {
+ $data = $this->setupDatabaseAndTable();
+
+ $table = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $data['databaseId'] . '/tables/' . $data['tableId'], [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $table['headers']['status-code']);
+ $this->assertEquals($data['tableId'], $table['body']['$id']);
+ }
+
+ public function testGetIntegerAndBigIntColumns(): void
+ {
+ $data = $this->setupDatabaseAndTable();
+ $databaseId = $data['databaseId'];
+ $tableId = $data['tableId'];
+
+ $integerColumn = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/integer_field', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $integerColumn['headers']['status-code']);
+ $this->assertEquals('integer_field', $integerColumn['body']['key']);
+ $this->assertEquals('integer', $integerColumn['body']['type']);
+ $this->assertEquals(false, $integerColumn['body']['required']);
+ $this->assertEquals(false, $integerColumn['body']['array']);
+ $this->assertEquals(-10, $integerColumn['body']['min']);
+ $this->assertEquals(10, $integerColumn['body']['max']);
+ $this->assertEquals(0, $integerColumn['body']['default']);
+
+ $bigintColumn = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/bigint_field', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $bigintColumn['headers']['status-code']);
+ $this->assertEquals('bigint_field', $bigintColumn['body']['key']);
+
+ $this->assertEquals('bigint', $bigintColumn['body']['type']);
+ $this->assertEquals(false, $bigintColumn['body']['required']);
+ $this->assertEquals(false, $bigintColumn['body']['array']);
+ $this->assertEquals(-9007199254740991, $bigintColumn['body']['min']);
+ $this->assertEquals(9007199254740991, $bigintColumn['body']['max']);
+ $this->assertEquals(9007199254740000, $bigintColumn['body']['default']);
+ }
+
+ public function testGetUnsignedIntegerAndBigIntColumns(): void
+ {
+ $data = $this->setupDatabaseAndTable();
+ $databaseId = $data['databaseId'];
+ $tableId = $data['tableId'];
+
+ $unsignedIntColumn = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/unsigned_int_field', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $unsignedIntColumn['headers']['status-code']);
+ $this->assertEquals('unsigned_int_field', $unsignedIntColumn['body']['key']);
+ $this->assertEquals('integer', $unsignedIntColumn['body']['type']);
+ $this->assertEquals(false, $unsignedIntColumn['body']['required']);
+ $this->assertEquals(false, $unsignedIntColumn['body']['array']);
+ $this->assertEquals(false, $unsignedIntColumn['body']['signed']);
+ $this->assertEquals(0, $unsignedIntColumn['body']['min']);
+ $this->assertEquals(100, $unsignedIntColumn['body']['max']);
+ $this->assertEquals(0, $unsignedIntColumn['body']['default']);
+
+ $unsignedBigintColumn = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/unsigned_bigint_field', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $unsignedBigintColumn['headers']['status-code']);
+ $this->assertEquals('unsigned_bigint_field', $unsignedBigintColumn['body']['key']);
+ $this->assertEquals('bigint', $unsignedBigintColumn['body']['type']);
+ $this->assertEquals(false, $unsignedBigintColumn['body']['required']);
+ $this->assertEquals(false, $unsignedBigintColumn['body']['array']);
+ $this->assertEquals(false, $unsignedBigintColumn['body']['signed']);
+ $this->assertEquals(0, $unsignedBigintColumn['body']['min']);
+ $this->assertEquals(9223372036854775807, $unsignedBigintColumn['body']['max']);
+ $this->assertEquals(0, $unsignedBigintColumn['body']['default']);
+ }
+
+ public function testListColumnsWithNumericTypes(): void
+ {
+ $data = $this->setupDatabaseAndTable();
+ $databaseId = $data['databaseId'];
+ $tableId = $data['tableId'];
+
+ $columns = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $columns['headers']['status-code']);
+ $this->assertIsArray($columns['body']['columns']);
+ $this->assertGreaterThan(0, $columns['body']['total']);
+
+ $columnKeys = array_map(fn ($col) => $col['key'], $columns['body']['columns']);
+ $this->assertContains('integer_field', $columnKeys);
+ $this->assertContains('bigint_field', $columnKeys);
+ $this->assertContains('unsigned_int_field', $columnKeys);
+ $this->assertContains('unsigned_bigint_field', $columnKeys);
+
+ $columnTypeByKey = [];
+ foreach ($columns['body']['columns'] as $col) {
+ $columnTypeByKey[$col['key']] = $col['type'];
+ }
+
+ $this->assertEquals('integer', $columnTypeByKey['integer_field']);
+ $this->assertEquals('bigint', $columnTypeByKey['bigint_field']);
+ $this->assertEquals('integer', $columnTypeByKey['unsigned_int_field']);
+ $this->assertEquals('bigint', $columnTypeByKey['unsigned_bigint_field']);
+ }
+
+ public function testCreateRowWithIntegerAndBigIntTypes(): void
+ {
+ $data = $this->setupDatabaseAndTable();
+ $databaseId = $data['databaseId'];
+ $tableId = $data['tableId'];
+
+ $row = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'rowId' => ID::unique(),
+ 'data' => [
+ 'integer_field' => 5,
+ 'bigint_field' => 456,
+ 'unsigned_int_field' => 50,
+ 'unsigned_bigint_field' => 9007199254740000,
+ ],
+ 'permissions' => [
+ Permission::read(Role::any()),
+ ],
+ ]);
+
+ $this->assertEquals(201, $row['headers']['status-code']);
+ $this->assertEquals(5, $row['body']['integer_field']);
+ $this->assertEquals(456, $row['body']['bigint_field']);
+ $this->assertEquals(50, $row['body']['unsigned_int_field']);
+ $this->assertEquals(9007199254740000, $row['body']['unsigned_bigint_field']);
+ }
+
+ public function testUpdateIntegerAndBigIntColumns(): void
+ {
+ $data = $this->setupFreshDatabaseAndTable();
+ $databaseId = $data['databaseId'];
+ $tableId = $data['tableId'];
+
+ // Update integer column
+ $updateInteger = $this->client->call(
+ Client::METHOD_PATCH,
+ '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/integer/integer_field',
+ [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ],
+ [
+ 'required' => false,
+ 'min' => -20,
+ 'max' => 20,
+ 'default' => 3,
+ ]
+ );
+
+ $this->assertEquals(200, $updateInteger['headers']['status-code']);
+
+ $this->assertEventually(function () use ($databaseId, $tableId) {
+ $column = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/integer_field', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $column['headers']['status-code']);
+ $this->assertEquals(-20, $column['body']['min']);
+ $this->assertEquals(20, $column['body']['max']);
+ $this->assertEquals(3, $column['body']['default']);
+ }, 30000, 250);
+
+ // Update bigint column
+ $updateBigint = $this->client->call(
+ Client::METHOD_PATCH,
+ '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/bigint/bigint_field',
+ [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ],
+ [
+ 'required' => false,
+ 'min' => -999,
+ 'max' => 999,
+ 'default' => 10,
+ ]
+ );
+
+ $this->assertEquals(200, $updateBigint['headers']['status-code']);
+
+ $this->assertEventually(function () use ($databaseId, $tableId) {
+ $column = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/bigint_field', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $column['headers']['status-code']);
+ $this->assertEquals(-999, $column['body']['min']);
+ $this->assertEquals(999, $column['body']['max']);
+ $this->assertEquals(10, $column['body']['default']);
+ }, 30000, 250);
+ }
+
+ public function testDeleteIntegerAndBigIntColumns(): void
+ {
+ $data = $this->setupFreshDatabaseAndTable();
+ $databaseId = $data['databaseId'];
+ $tableId = $data['tableId'];
+
+ // Delete integer column
+ $deleteInteger = $this->client->call(
+ Client::METHOD_DELETE,
+ '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/integer_field',
+ [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]
+ );
+
+ $this->assertEquals(204, $deleteInteger['headers']['status-code']);
+
+ $this->assertEventually(function () use ($databaseId, $tableId) {
+ $column = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/integer_field', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(404, $column['headers']['status-code']);
+ }, 30000, 250);
+
+ // Delete bigint column
+ $deleteBigint = $this->client->call(
+ Client::METHOD_DELETE,
+ '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/bigint_field',
+ [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]
+ );
+
+ $this->assertEquals(204, $deleteBigint['headers']['status-code']);
+
+ $this->assertEventually(function () use ($databaseId, $tableId) {
+ $column = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/bigint_field', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(404, $column['headers']['status-code']);
+ }, 30000, 250);
+ }
+}
diff --git a/tests/e2e/Services/VCS/VCSConsoleClientTest.php b/tests/e2e/Services/VCS/VCSConsoleClientTest.php
index 854e7110f1..23007339de 100644
--- a/tests/e2e/Services/VCS/VCSConsoleClientTest.php
+++ b/tests/e2e/Services/VCS/VCSConsoleClientTest.php
@@ -513,6 +513,59 @@ class VCSConsoleClientTest extends Scope
$this->assertEquals($repositoryBranches['body']['branches'][0]['name'], 'main');
$this->assertEquals($repositoryBranches['body']['branches'][1]['name'], 'test');
+ $repositoryBranches = $this->client->call(Client::METHOD_GET, '/vcs/github/installations/' . $installationId . '/providerRepositories/' . $this->providerRepositoryId . '/branches', array_merge([
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'search' => 'tes',
+ ]);
+
+ $this->assertEquals(200, $repositoryBranches['headers']['status-code']);
+ $this->assertEquals($repositoryBranches['body']['total'], 1);
+ $this->assertCount(1, $repositoryBranches['body']['branches']);
+ $this->assertEquals($repositoryBranches['body']['branches'][0]['name'], 'test');
+
+ $repositoryBranches = $this->client->call(Client::METHOD_GET, '/vcs/github/installations/' . $installationId . '/providerRepositories/' . $this->providerRepositoryId . '/branches', array_merge([
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'queries' => [
+ Query::limit(1)->toString(),
+ Query::offset(1)->toString(),
+ ],
+ ]);
+
+ $this->assertEquals(200, $repositoryBranches['headers']['status-code']);
+ $this->assertEquals($repositoryBranches['body']['total'], 2);
+ $this->assertCount(1, $repositoryBranches['body']['branches']);
+ $this->assertEquals($repositoryBranches['body']['branches'][0]['name'], 'test');
+
+ $repositoryBranches = $this->client->call(Client::METHOD_GET, '/vcs/github/installations/' . $installationId . '/providerRepositories/' . $this->providerRepositoryId . '/branches', array_merge([
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'queries' => [
+ Query::limit(1)->toString(),
+ Query::cursorAfter(new \Utopia\Database\Document(['$id' => 'main']))->toString(),
+ ],
+ ]);
+
+ $this->assertEquals(200, $repositoryBranches['headers']['status-code']);
+ $this->assertEquals($repositoryBranches['body']['total'], 2);
+ $this->assertCount(1, $repositoryBranches['body']['branches']);
+ $this->assertEquals($repositoryBranches['body']['branches'][0]['name'], 'test');
+
+ $repositoryBranches = $this->client->call(Client::METHOD_GET, '/vcs/github/installations/' . $installationId . '/providerRepositories/' . $this->providerRepositoryId . '/branches', array_merge([
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'queries' => [
+ Query::limit(1)->toString(),
+ Query::cursorBefore(new \Utopia\Database\Document(['$id' => 'test']))->toString(),
+ ],
+ ]);
+
+ $this->assertEquals(200, $repositoryBranches['headers']['status-code']);
+ $this->assertEquals($repositoryBranches['body']['total'], 2);
+ $this->assertCount(1, $repositoryBranches['body']['branches']);
+ $this->assertEquals($repositoryBranches['body']['branches'][0]['name'], 'main');
+
/**
* Test for FAILURE
*/
@@ -522,6 +575,16 @@ class VCSConsoleClientTest extends Scope
], $this->getHeaders()));
$this->assertEquals(404, $repositoryBranches['headers']['status-code']);
+
+ $repositoryBranches = $this->client->call(Client::METHOD_GET, '/vcs/github/installations/' . $installationId . '/providerRepositories/' . $this->providerRepositoryId . '/branches', array_merge([
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ ], $this->getHeaders()), [
+ 'queries' => [
+ Query::cursorAfter(new \Utopia\Database\Document(['$id' => 'missing-branch']))->toString(),
+ ],
+ ]);
+
+ $this->assertEquals(400, $repositoryBranches['headers']['status-code']);
}
public function testCreateFunctionUsingVCS(): void
diff --git a/tests/unit/Advisor/Validator/CTAsTest.php b/tests/unit/Advisor/Validator/CTAsTest.php
new file mode 100644
index 0000000000..5511910072
--- /dev/null
+++ b/tests/unit/Advisor/Validator/CTAsTest.php
@@ -0,0 +1,241 @@
+assertFalse($validator->isValid('not-an-array'));
+ $this->assertFalse($validator->isValid(42));
+ $this->assertFalse($validator->isValid(null));
+ }
+
+ public function testAcceptsEmptyArray(): void
+ {
+ $validator = new CTAs();
+
+ $this->assertTrue($validator->isValid([]));
+ }
+
+ public function testAcceptsCompleteEntry(): void
+ {
+ $validator = new CTAs();
+
+ $this->assertTrue($validator->isValid([[
+ 'label' => 'Create missing index',
+ 'service' => 'tablesDB',
+ 'method' => 'createIndex',
+ 'params' => [
+ 'databaseId' => 'main',
+ 'tableId' => 'orders',
+ ],
+ ]]));
+ }
+
+ public function testAcceptsEntryWithoutParams(): void
+ {
+ $validator = new CTAs();
+
+ $this->assertTrue($validator->isValid([[
+ 'label' => 'Create missing index',
+ 'service' => 'tablesDB',
+ 'method' => 'createIndex',
+ ]]));
+ }
+
+ public function testRejectsEntryMissingRequiredKeys(): void
+ {
+ $validator = new CTAs();
+
+ $this->assertFalse($validator->isValid([['label' => 'x']]));
+ $this->assertFalse($validator->isValid([['label' => 'x', 'service' => 'tablesDB']]));
+ $this->assertFalse($validator->isValid([['label' => 'x', 'method' => 'createIndex']]));
+ }
+
+ public function testRejectsEntryWithEmptyStrings(): void
+ {
+ $validator = new CTAs();
+
+ $this->assertFalse($validator->isValid([[
+ 'label' => '',
+ 'service' => 'tablesDB',
+ 'method' => 'createIndex',
+ ]]));
+ }
+
+ public function testRejectsEntryWithNonStringFields(): void
+ {
+ $validator = new CTAs();
+
+ $this->assertFalse($validator->isValid([[
+ 'label' => 123,
+ 'service' => 'tablesDB',
+ 'method' => 'createIndex',
+ ]]));
+ }
+
+ public function testRejectsEntryWithScalarParams(): void
+ {
+ $validator = new CTAs();
+
+ $this->assertFalse($validator->isValid([[
+ 'label' => 'Create missing index',
+ 'service' => 'tablesDB',
+ 'method' => 'createIndex',
+ 'params' => 'not-a-map',
+ ]]));
+ }
+
+ public function testReportsArrayType(): void
+ {
+ $validator = new CTAs();
+
+ $this->assertTrue($validator->isArray());
+ $this->assertSame($validator::TYPE_ARRAY, $validator->getType());
+ }
+
+ public function testRejectsMoreThanMaxCount(): void
+ {
+ $validator = new CTAs(maxCount: 3);
+
+ $entries = [];
+ for ($i = 0; $i < 4; $i++) {
+ $entries[] = [
+ 'label' => 'Label ' . $i,
+ 'service' => 'tablesDB',
+ 'method' => 'createIndex',
+ ];
+ }
+
+ $this->assertFalse($validator->isValid($entries));
+ $this->assertStringContainsString('maximum of 3', $validator->getDescription());
+ }
+
+ public function testAcceptsExactlyMaxCount(): void
+ {
+ $validator = new CTAs(maxCount: 3);
+
+ $entries = [];
+ for ($i = 0; $i < 3; $i++) {
+ $entries[] = [
+ 'label' => 'Label ' . $i,
+ 'service' => 'tablesDB',
+ 'method' => 'createIndex',
+ ];
+ }
+
+ $this->assertTrue($validator->isValid($entries));
+ }
+
+ public function testAcceptsObjectParams(): void
+ {
+ $validator = new CTAs();
+
+ $entry = [
+ 'label' => 'Create missing index',
+ 'service' => 'tablesDB',
+ 'method' => 'createIndex',
+ 'params' => new \stdClass(),
+ ];
+
+ $this->assertTrue($validator->isValid([$entry]));
+ }
+
+ public function testRejectsEntryWithEmptyService(): void
+ {
+ $validator = new CTAs();
+
+ $this->assertFalse($validator->isValid([[
+ 'label' => 'Create missing index',
+ 'service' => '',
+ 'method' => 'createIndex',
+ ]]));
+ }
+
+ public function testRejectsEntryWithEmptyMethod(): void
+ {
+ $validator = new CTAs();
+
+ $this->assertFalse($validator->isValid([[
+ 'label' => 'Create missing index',
+ 'service' => 'tablesDB',
+ 'method' => '',
+ ]]));
+ }
+
+ public function testRejectsUnknownService(): void
+ {
+ $validator = new CTAs();
+
+ $this->assertFalse($validator->isValid([[
+ 'label' => 'Create missing index',
+ 'service' => 'nonExistentService',
+ 'method' => 'createIndex',
+ ]]));
+ $this->assertStringContainsString('service', $validator->getDescription());
+ }
+
+ public function testRejectsUnknownMethod(): void
+ {
+ $validator = new CTAs();
+
+ $this->assertFalse($validator->isValid([[
+ 'label' => 'Create missing index',
+ 'service' => 'tablesDB',
+ 'method' => 'nonExistentMethod',
+ ]]));
+ $this->assertStringContainsString('method', $validator->getDescription());
+ }
+
+ public function testAcceptsCustomAllowedLists(): void
+ {
+ $validator = new CTAs(
+ allowedServices: ['custom'],
+ allowedMethods: ['doThing'],
+ );
+
+ $this->assertTrue($validator->isValid([[
+ 'label' => 'Custom action',
+ 'service' => 'custom',
+ 'method' => 'doThing',
+ ]]));
+
+ $this->assertFalse($validator->isValid([[
+ 'label' => 'Custom action',
+ 'service' => 'tablesDB',
+ 'method' => 'doThing',
+ ]]));
+ }
+
+ public function testDefaultMaxCountIsSixteen(): void
+ {
+ $validator = new CTAs();
+
+ $this->assertSame(CTAs::MAX_COUNT_DEFAULT, 16);
+
+ $entries = [];
+ for ($i = 0; $i < 16; $i++) {
+ $entries[] = [
+ 'label' => 'Label ' . $i,
+ 'service' => 'tablesDB',
+ 'method' => 'createIndex',
+ ];
+ }
+
+ $this->assertTrue($validator->isValid($entries));
+
+ $entries[] = [
+ 'label' => 'Label 16',
+ 'service' => 'tablesDB',
+ 'method' => 'createIndex',
+ ];
+
+ $this->assertFalse($validator->isValid($entries));
+ }
+}
diff --git a/tests/unit/Utopia/ResponseTest.php b/tests/unit/Utopia/ResponseTest.php
index f5a30a5500..74c68303f4 100644
--- a/tests/unit/Utopia/ResponseTest.php
+++ b/tests/unit/Utopia/ResponseTest.php
@@ -34,10 +34,11 @@ class ResponseTest extends TestCase
$this->assertTrue($this->response->hasFilters());
$this->assertCount(2, $this->response->getFilters());
- $output = $this->response->applyFilters([
+ $content = [
'initial' => true,
'first' => false
- ], 'test');
+ ];
+ $output = $this->response->applyFilters($content, 'test', raw: new Document($content));
$this->assertArrayHasKey('initial', $output);
$this->assertTrue($output['initial']);
diff --git a/tests/unit/Vcs/CommentTest.php b/tests/unit/Vcs/CommentTest.php
new file mode 100644
index 0000000000..29973089c6
--- /dev/null
+++ b/tests/unit/Vcs/CommentTest.php
@@ -0,0 +1,154 @@
+ 'localhost']);
+ $comment->addBuild(
+ new Document(['$id' => 'project1', 'name' => 'Test Project', 'region' => 'default']),
+ new Document(['$id' => 'func1', 'name' => 'Test Function']),
+ 'function',
+ 'ready',
+ 'dep1',
+ ['type' => 'logs'],
+ ''
+ );
+
+ $first = $comment->generateComment();
+ $firstTip = $this->extractTip($first);
+
+ $this->assertNotNull($firstTip);
+ $this->assertNotEmpty($firstTip);
+
+ $second = $comment->generateComment();
+ $secondTip = $this->extractTip($second);
+
+ $this->assertEquals($firstTip, $secondTip);
+ }
+
+ public function testTipIsRestoredFromParsedComment(): void
+ {
+ $comment = new Comment(['consoleHostname' => 'localhost']);
+ $comment->addBuild(
+ new Document(['$id' => 'project1', 'name' => 'Test Project', 'region' => 'default']),
+ new Document(['$id' => 'func1', 'name' => 'Test Function']),
+ 'function',
+ 'ready',
+ 'dep1',
+ ['type' => 'logs'],
+ ''
+ );
+
+ $original = $comment->generateComment();
+ $originalTip = $this->extractTip($original);
+
+ $parsed = new Comment(['consoleHostname' => 'localhost']);
+ $parsed->parseComment($original);
+ $parsed->addBuild(
+ new Document(['$id' => 'project1', 'name' => 'Test Project', 'region' => 'default']),
+ new Document(['$id' => 'func2', 'name' => 'Another Function']),
+ 'function',
+ 'building',
+ 'dep2',
+ ['type' => 'logs'],
+ ''
+ );
+
+ $regenerated = $parsed->generateComment();
+ $regeneratedTip = $this->extractTip($regenerated);
+
+ $this->assertEquals($originalTip, $regeneratedTip);
+ }
+
+ public function testBackwardCompatibilityWithOldStateFormat(): void
+ {
+ $oldBuilds = [
+ 'project1_func1' => [
+ 'projectName' => 'Test Project',
+ 'projectId' => 'project1',
+ 'region' => 'default',
+ 'resourceName' => 'Test Function',
+ 'resourceId' => 'func1',
+ 'resourceType' => 'function',
+ 'buildStatus' => 'ready',
+ 'deploymentId' => 'dep1',
+ 'action' => ['type' => 'logs'],
+ 'previewUrl' => '',
+ ],
+ ];
+
+ $oldState = '[appwrite]: #' . \base64_encode(\json_encode($oldBuilds)) . "\n\n";
+ $oldState .= "> [!TIP]\n> Old tip that should be ignored\n\n";
+
+ $comment = new Comment(['consoleHostname' => 'localhost']);
+ $comment->parseComment($oldState);
+
+ $new = $comment->generateComment();
+ $newTip = $this->extractTip($new);
+
+ $this->assertNotNull($newTip);
+ $this->assertNotEquals('Old tip that should be ignored', $newTip);
+ $this->assertContains($newTip, $this->getTips());
+ }
+
+ public function testParseOldStateFormatWithOnlyBuilds(): void
+ {
+ $oldBuilds = [
+ 'project1_func1' => [
+ 'projectName' => 'Test Project',
+ 'projectId' => 'project1',
+ 'region' => 'default',
+ 'resourceName' => 'Test Function',
+ 'resourceId' => 'func1',
+ 'resourceType' => 'function',
+ 'buildStatus' => 'ready',
+ 'deploymentId' => 'dep1',
+ 'action' => ['type' => 'logs'],
+ 'previewUrl' => '',
+ ],
+ ];
+
+ $state = '[appwrite]: #' . \base64_encode(\json_encode($oldBuilds)) . "\n\n";
+
+ $comment = new Comment(['consoleHostname' => 'localhost']);
+ $comment->parseComment($state);
+
+ $this->assertEquals(false, $comment->isEmpty());
+
+ $first = $comment->generateComment();
+ $firstTip = $this->extractTip($first);
+
+ $this->assertNotNull($firstTip);
+ $this->assertNotEmpty($firstTip);
+ $this->assertContains($firstTip, $this->getTips());
+
+ $second = $comment->generateComment();
+ $secondTip = $this->extractTip($second);
+
+ $this->assertEquals($firstTip, $secondTip);
+ }
+
+ private function extractTip(string $comment): ?string
+ {
+ if (\preg_match('/> \[!TIP\]\n> (.+)/', $comment, $matches)) {
+ return $matches[1];
+ }
+
+ return null;
+ }
+
+ private function getTips(): array
+ {
+ $reflection = new \ReflectionClass(Comment::class);
+ $property = $reflection->getProperty('tips');
+
+ return $property->getValue(new Comment(['consoleHostname' => 'localhost']));
+ }
+}