mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Compare commits
88
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
995b1c76ca | ||
|
|
a071c715bc | ||
|
|
52981e0164 | ||
|
|
387668bb47 | ||
|
|
b6e95f4502 | ||
|
|
1900492aad | ||
|
|
cb9d3869f0 | ||
|
|
96f4ef7d22 | ||
|
|
8e02a5a227 | ||
|
|
d17ae517d3 | ||
|
|
c2dd8aceda | ||
|
|
24fcf4247c | ||
|
|
2dc20ef0eb | ||
|
|
04943b6313 | ||
|
|
eb366cf94b | ||
|
|
77b4f8b7a0 | ||
|
|
24bd4c3d7b | ||
|
|
25d9891f2b | ||
|
|
8c9f9c6f58 | ||
|
|
33f8e35b62 | ||
|
|
2c1060f57a | ||
|
|
30e0ca81bd | ||
|
|
92cc382a6b | ||
|
|
c322cc3ffe | ||
|
|
7d428ffe83 | ||
|
|
fb96aecbef | ||
|
|
d531c29fc8 | ||
|
|
7044601603 | ||
|
|
53bc126015 | ||
|
|
f1dc468e50 | ||
|
|
ce63b00cf4 | ||
|
|
b9aaecba25 | ||
|
|
d9eb69aa47 | ||
|
|
a5b0378138 | ||
|
|
320068a576 | ||
|
|
3cd90ae629 | ||
|
|
44f33473bd | ||
|
|
358f1b78a8 | ||
|
|
1f6b9d94bf | ||
|
|
a734c8cd46 | ||
|
|
1788e1bd6c | ||
|
|
7f9ce1ca85 | ||
|
|
983adf3ffd | ||
|
|
f2ea0b9b48 | ||
|
|
44610462b3 | ||
|
|
28ece7de02 | ||
|
|
8c6d4d8b36 | ||
|
|
2ebc6f70ef | ||
|
|
2b7690d6db | ||
|
|
d9af799cc7 | ||
|
|
3ed1ca736d | ||
|
|
9fa35db838 | ||
|
|
b6e020389b | ||
|
|
ccc0cfbfdc | ||
|
|
b91506fc2d | ||
|
|
9ffc23946c | ||
|
|
8bb6b5cd2a | ||
|
|
afea4ca57b | ||
|
|
b30b89ade9 | ||
|
|
829cf887dc | ||
|
|
d862a64874 | ||
|
|
168166b9c1 | ||
|
|
7dcfd3ff3d | ||
|
|
4dfdfb5e59 | ||
|
|
5d1009b324 | ||
|
|
a80ecd0cb6 | ||
|
|
8be7c6182e | ||
|
|
3bb6a8bcc8 | ||
|
|
0085d93aeb | ||
|
|
3b9b0c96c4 | ||
|
|
2611bf4af1 | ||
|
|
de219de31d | ||
|
|
d8bbd82556 | ||
|
|
aaebeec61e | ||
|
|
53d0e14f97 | ||
|
|
741267c6c5 | ||
|
|
e85080499a | ||
|
|
ee1ca5ace6 | ||
|
|
b36472f0da | ||
|
|
52ae8b3880 | ||
|
|
8f09e74462 | ||
|
|
30907d716f | ||
|
|
eb46855e72 | ||
|
|
45557e5929 | ||
|
|
6ad6a5dea3 | ||
|
|
098f7aa3e3 | ||
|
|
f8c8c17757 | ||
|
|
5b1ee93927 |
@@ -39,6 +39,7 @@ _APP_REDIS_HOST=redis
|
||||
_APP_REDIS_PORT=6379
|
||||
_APP_REDIS_PASS=
|
||||
_APP_REDIS_USER=
|
||||
COMPOSE_PROFILES=mariadb,mongodb,postgresql
|
||||
_APP_DB_ADAPTER=mongodb
|
||||
_APP_DB_HOST=mongodb
|
||||
_APP_DB_PORT=27017
|
||||
|
||||
+160
-79
@@ -150,8 +150,16 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: composer install --prefer-dist --no-progress --ignore-platform-reqs
|
||||
|
||||
- name: Cache PHPStan result cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .phpstan-cache
|
||||
key: phpstan-${{ github.sha }}
|
||||
restore-keys: |
|
||||
phpstan-
|
||||
|
||||
- name: Run PHPStan
|
||||
run: composer analyze
|
||||
run: composer analyze -- --no-progress
|
||||
|
||||
locale:
|
||||
name: Checks / Locale
|
||||
@@ -174,46 +182,22 @@ jobs:
|
||||
outputs:
|
||||
databases: ${{ steps.generate.outputs.databases }}
|
||||
modes: ${{ steps.generate.outputs.modes }}
|
||||
services: ${{ steps.generate.outputs.services }}
|
||||
steps:
|
||||
- name: Generate matrix
|
||||
id: generate
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const databases = [
|
||||
{ name: 'MariaDB', env: '.env,compose/mariadb.env', compose: 'docker-compose.yml:compose/mariadb.yml' },
|
||||
{ name: 'PostgreSQL', env: '.env,compose/postgresql.env', compose: 'docker-compose.yml:compose/postgresql.yml' },
|
||||
{ name: 'MongoDB', env: '.env', compose: 'docker-compose.yml' },
|
||||
];
|
||||
const modes = ['dedicated', 'shared_v1', 'shared_v2'];
|
||||
const allDatabases = ['MariaDB', 'PostgreSQL', 'MongoDB'];
|
||||
const allModes = ['dedicated', 'shared_v1', 'shared_v2'];
|
||||
|
||||
const services = [
|
||||
{ name: 'Account', parallel: true },
|
||||
{ name: 'Avatars', parallel: true, runner: 'blacksmith-4vcpu-ubuntu-2404' },
|
||||
{ name: 'Databases', runner: 'blacksmith-4vcpu-ubuntu-2404' },
|
||||
{ name: 'TablesDB', runner: 'blacksmith-4vcpu-ubuntu-2404' },
|
||||
{ name: 'Functions', runner: 'blacksmith-4vcpu-ubuntu-2404' },
|
||||
{ name: 'FunctionsSchedule', parallel: true },
|
||||
{ name: 'GraphQL', parallel: true },
|
||||
{ name: 'Projects', parallel: true },
|
||||
{ name: 'Realtime', runner: 'blacksmith-4vcpu-ubuntu-2404' },
|
||||
{ name: 'Sites', parallel: true, runner: 'blacksmith-4vcpu-ubuntu-2404' },
|
||||
{ name: 'Teams', parallel: true },
|
||||
{ name: 'Users', parallel: true },
|
||||
{ name: 'ProjectWebhooks', parallel: true },
|
||||
{ name: 'Migrations', parallel: true },
|
||||
{ name: 'Project', parallel: true },
|
||||
];
|
||||
core.setOutput('services', JSON.stringify(services));
|
||||
|
||||
const defaultDatabases = [databases.find(d => d.name === 'MongoDB')];
|
||||
const defaultDatabases = ['MongoDB'];
|
||||
const defaultModes = ['dedicated'];
|
||||
|
||||
const pr = context.payload.pull_request;
|
||||
if (!pr) {
|
||||
core.setOutput('databases', JSON.stringify(databases));
|
||||
core.setOutput('modes', JSON.stringify(modes));
|
||||
core.setOutput('databases', JSON.stringify(allDatabases));
|
||||
core.setOutput('modes', JSON.stringify(allModes));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -234,8 +218,8 @@ jobs:
|
||||
const decode = (content) => JSON.parse(Buffer.from(content, 'base64').toString());
|
||||
const databaseChanged = getDbVersion(decode(base.content)) !== getDbVersion(decode(head.content));
|
||||
|
||||
core.setOutput('databases', JSON.stringify(databaseChanged ? databases : defaultDatabases));
|
||||
core.setOutput('modes', JSON.stringify(databaseChanged ? modes : defaultModes));
|
||||
core.setOutput('databases', JSON.stringify(databaseChanged ? allDatabases : defaultDatabases));
|
||||
core.setOutput('modes', JSON.stringify(databaseChanged ? allModes : defaultModes));
|
||||
|
||||
build:
|
||||
name: Build
|
||||
@@ -312,29 +296,28 @@ jobs:
|
||||
run: docker compose exec -T appwrite vars
|
||||
|
||||
- name: Run Unit Tests
|
||||
timeout-minutes: 15
|
||||
run: >-
|
||||
docker compose exec
|
||||
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}"
|
||||
appwrite test /usr/src/code/tests/unit
|
||||
uses: itznotabug/php-retry@v3
|
||||
with:
|
||||
max_attempts: 2
|
||||
retry_wait_seconds: 60
|
||||
timeout_minutes: 15
|
||||
job_id: ${{ job.check_run_id }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
test_dir: tests/unit
|
||||
command: >-
|
||||
docker compose exec
|
||||
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}"
|
||||
appwrite test /usr/src/code/tests/unit
|
||||
|
||||
e2e_general:
|
||||
name: Tests / E2E / ${{ matrix.database.name }} (${{ matrix.mode }}) / General
|
||||
name: Tests / E2E / General
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build, matrix]
|
||||
env:
|
||||
COMPOSE_FILE: ${{ matrix.database.compose }}
|
||||
COMPOSE_ENV_FILES: ${{ matrix.database.env }}
|
||||
needs: build
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
database: ${{ fromJSON(needs.matrix.outputs.databases) }}
|
||||
mode: ${{ fromJSON(needs.matrix.outputs.modes) }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
- name: checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Download Docker Image
|
||||
@@ -351,10 +334,6 @@ jobs:
|
||||
|
||||
- name: Load and Start Appwrite
|
||||
timeout-minutes: 5
|
||||
env:
|
||||
_APP_BROWSER_HOST: http://invalid-browser/v1
|
||||
_APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }}
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker compose pull --quiet --ignore-buildable
|
||||
@@ -368,12 +347,19 @@ jobs:
|
||||
sleep 1
|
||||
done
|
||||
|
||||
- name: Run tests
|
||||
timeout-minutes: 20
|
||||
run: >-
|
||||
docker compose exec -T
|
||||
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}"
|
||||
appwrite vendor/bin/paratest --processes $(nproc) --functional --testsuite GeneralGroup --exclude-group abuseEnabled --exclude-group screenshots
|
||||
- name: Run General Tests
|
||||
uses: itznotabug/php-retry@v3
|
||||
with:
|
||||
max_attempts: 2
|
||||
retry_wait_seconds: 60
|
||||
timeout_minutes: 15
|
||||
job_id: ${{ job.check_run_id }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
test_dir: tests/e2e/General
|
||||
command: >-
|
||||
docker compose exec -T
|
||||
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}"
|
||||
appwrite test /usr/src/code/tests/e2e/General
|
||||
|
||||
- name: Failure Logs
|
||||
if: failure()
|
||||
@@ -382,12 +368,9 @@ jobs:
|
||||
docker compose logs
|
||||
|
||||
e2e_service:
|
||||
name: Tests / E2E / ${{ matrix.database.name }} (${{ matrix.mode }}) / ${{ matrix.service.name }}
|
||||
runs-on: ${{ matrix.service.runner || 'ubuntu-latest' }}
|
||||
name: Tests / E2E / ${{ matrix.database }} (${{ matrix.mode }}) / ${{ matrix.service }}
|
||||
runs-on: ${{ matrix.runner || 'ubuntu-latest' }}
|
||||
needs: [build, matrix]
|
||||
env:
|
||||
COMPOSE_FILE: ${{ matrix.database.compose }}
|
||||
COMPOSE_ENV_FILES: ${{ matrix.database.env }}
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
@@ -396,7 +379,45 @@ jobs:
|
||||
matrix:
|
||||
database: ${{ fromJSON(needs.matrix.outputs.databases) }}
|
||||
mode: ${{ fromJSON(needs.matrix.outputs.modes) }}
|
||||
service: ${{ fromJSON(needs.matrix.outputs.services) }}
|
||||
service: [
|
||||
Account,
|
||||
Avatars,
|
||||
Console,
|
||||
Databases,
|
||||
TablesDB,
|
||||
Functions,
|
||||
FunctionsSchedule,
|
||||
GraphQL,
|
||||
Health,
|
||||
Locale,
|
||||
Projects,
|
||||
Realtime,
|
||||
Sites,
|
||||
Proxy,
|
||||
Storage,
|
||||
Tokens,
|
||||
Teams,
|
||||
Users,
|
||||
ProjectWebhooks,
|
||||
Webhooks,
|
||||
VCS,
|
||||
Messaging,
|
||||
Migrations,
|
||||
Project
|
||||
]
|
||||
include:
|
||||
- service: Databases
|
||||
runner: blacksmith-4vcpu-ubuntu-2404
|
||||
- service: Sites
|
||||
runner: blacksmith-4vcpu-ubuntu-2404
|
||||
- service: Functions
|
||||
runner: blacksmith-4vcpu-ubuntu-2404
|
||||
- service: Avatars
|
||||
runner: blacksmith-4vcpu-ubuntu-2404
|
||||
- service: Realtime
|
||||
runner: blacksmith-4vcpu-ubuntu-2404
|
||||
- service: TablesDB
|
||||
runner: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
@@ -407,6 +428,25 @@ jobs:
|
||||
name: ${{ env.IMAGE }}
|
||||
path: /tmp
|
||||
|
||||
- name: Set database environment
|
||||
run: |
|
||||
if [ "${{ matrix.database }}" = "MariaDB" ]; then
|
||||
echo "COMPOSE_PROFILES=mariadb" >> $GITHUB_ENV
|
||||
echo "_APP_DB_ADAPTER=mariadb" >> $GITHUB_ENV
|
||||
echo "_APP_DB_HOST=mariadb" >> $GITHUB_ENV
|
||||
echo "_APP_DB_PORT=3306" >> $GITHUB_ENV
|
||||
elif [ "${{ matrix.database }}" = "MongoDB" ]; then
|
||||
echo "COMPOSE_PROFILES=mongodb" >> $GITHUB_ENV
|
||||
echo "_APP_DB_ADAPTER=mongodb" >> $GITHUB_ENV
|
||||
echo "_APP_DB_HOST=mongodb" >> $GITHUB_ENV
|
||||
echo "_APP_DB_PORT=27017" >> $GITHUB_ENV
|
||||
elif [ "${{ matrix.database }}" = "PostgreSQL" ]; then
|
||||
echo "COMPOSE_PROFILES=postgresql" >> $GITHUB_ENV
|
||||
echo "_APP_DB_ADAPTER=postgresql" >> $GITHUB_ENV
|
||||
echo "_APP_DB_HOST=postgresql" >> $GITHUB_ENV
|
||||
echo "_APP_DB_PORT=5432" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
@@ -419,6 +459,10 @@ jobs:
|
||||
_APP_BROWSER_HOST: http://invalid-browser/v1
|
||||
_APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'vectorsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'vectorsdb_db_main' || '' }}
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker compose pull --quiet --ignore-buildable
|
||||
@@ -433,11 +477,26 @@ jobs:
|
||||
done
|
||||
|
||||
- name: Run tests
|
||||
timeout-minutes: 20
|
||||
run: |
|
||||
docker compose exec -T \
|
||||
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \
|
||||
appwrite vendor/bin/paratest --processes $(nproc) ${{ matrix.service.parallel && '--functional' || '' }} --testsuite ${{ matrix.service.name }} --exclude-group abuseEnabled --exclude-group screenshots
|
||||
uses: itznotabug/php-retry@v3
|
||||
with:
|
||||
max_attempts: 2
|
||||
retry_wait_seconds: 60
|
||||
timeout_minutes: 20
|
||||
job_id: ${{ job.check_run_id }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
test_dir: tests/e2e/Services/${{ matrix.service }}
|
||||
command: |
|
||||
SERVICE_PATH="/usr/src/code/tests/e2e/Services/${{ matrix.service }}"
|
||||
|
||||
# Services that rely on sequential test method execution (shared static state)
|
||||
FUNCTIONAL_FLAG="--functional"
|
||||
case "${{ matrix.service }}" in
|
||||
Databases|TablesDB|Functions|Realtime) FUNCTIONAL_FLAG="" ;;
|
||||
esac
|
||||
|
||||
docker compose exec -T \
|
||||
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \
|
||||
appwrite vendor/bin/paratest --processes $(nproc) $FUNCTIONAL_FLAG "$SERVICE_PATH" --exclude-group abuseEnabled --exclude-group screenshots --log-junit tests/e2e/Services/${{ matrix.service }}/junit.xml
|
||||
|
||||
- name: Failure Logs
|
||||
if: failure()
|
||||
@@ -478,17 +537,28 @@ jobs:
|
||||
_APP_OPTIONS_ABUSE: enabled
|
||||
_APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'vectorsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'vectorsdb_db_main' || '' }}
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker compose pull --quiet --ignore-buildable
|
||||
docker compose up -d --quiet-pull --wait
|
||||
|
||||
- name: Run tests
|
||||
timeout-minutes: 15
|
||||
run: >-
|
||||
docker compose exec -T
|
||||
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}"
|
||||
appwrite test /usr/src/code/tests/e2e --group=abuseEnabled
|
||||
uses: itznotabug/php-retry@v3
|
||||
with:
|
||||
max_attempts: 2
|
||||
retry_wait_seconds: 60
|
||||
timeout_minutes: 15
|
||||
job_id: ${{ job.check_run_id }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
test_dir: tests/e2e
|
||||
command: >-
|
||||
docker compose exec -T
|
||||
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}"
|
||||
appwrite test /usr/src/code/tests/e2e --group=abuseEnabled
|
||||
|
||||
- name: Failure Logs
|
||||
if: failure()
|
||||
@@ -528,6 +598,10 @@ jobs:
|
||||
env:
|
||||
_APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }}
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'documentsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'vectorsdb_db_main' || '' }}
|
||||
_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'vectorsdb_db_main' || '' }}
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE }}.tar
|
||||
docker compose pull --quiet --ignore-buildable
|
||||
@@ -542,11 +616,18 @@ jobs:
|
||||
done
|
||||
|
||||
- name: Run tests
|
||||
timeout-minutes: 15
|
||||
run: >-
|
||||
docker compose exec -T
|
||||
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}"
|
||||
appwrite test /usr/src/code/tests/e2e/Services/Sites --group=screenshots
|
||||
uses: itznotabug/php-retry@v3
|
||||
with:
|
||||
max_attempts: 2
|
||||
retry_wait_seconds: 60
|
||||
timeout_minutes: 15
|
||||
job_id: ${{ job.check_run_id }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
test_dir: tests/e2e/Services/Sites
|
||||
command: >-
|
||||
docker compose exec -T
|
||||
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}"
|
||||
appwrite test /usr/src/code/tests/e2e/Services/Sites --group=screenshots
|
||||
|
||||
- name: Failure Logs
|
||||
if: failure()
|
||||
|
||||
@@ -21,6 +21,7 @@ appwrite.config.json
|
||||
/app/config/specs/
|
||||
/docs/examples/
|
||||
.phpunit.cache
|
||||
.phpstan-cache
|
||||
playwright-report
|
||||
test-results
|
||||
docker-compose.web-installer.yml
|
||||
|
||||
@@ -20,7 +20,7 @@ Self-hosted Backend-as-a-Service platform. Hybrid monolithic-microservice archit
|
||||
|
||||
- PHP 8.3+, Swoole 6.x (async runtime, replaces PHP-FPM)
|
||||
- Utopia PHP framework (HTTP routing, CLI, DI, queue)
|
||||
- MongoDB (default), MariaDB, PostgreSQL (adapters via utopia-php/database; use `compose/mariadb.yml` or `compose/postgresql.yml` overlays to switch)
|
||||
- MongoDB (default), MariaDB, MySQL, PostgreSQL (adapters via utopia-php/database)
|
||||
- Redis (cache, queue, pub/sub)
|
||||
- Docker + Traefik (reverse proxy)
|
||||
- PHPUnit 12, Pint (PSR-12), PHPStan level 3
|
||||
|
||||
+1
-1
@@ -296,7 +296,7 @@ Appwrite stack is a combination of a variety of open-source technologies and too
|
||||
### Other Technologies
|
||||
|
||||
- Redis - for managing cache and in-memory data (currently, we do not use Redis for persistent data).
|
||||
- MongoDB - default database for storage and queries (MariaDB and PostgreSQL also supported via compose overlays).
|
||||
- MariaDB - for database storage and queries.
|
||||
- InfluxDB - for managing stats and time-series based data
|
||||
- Statsd - for sending data over UDP protocol (using Telegraf)
|
||||
- ClamAV - for validating and scanning storage files.
|
||||
|
||||
@@ -72,6 +72,7 @@ Before running the installation command, make sure you have [Docker](https://www
|
||||
|
||||
```bash
|
||||
docker run -it --rm \
|
||||
--publish 20080:20080 \
|
||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
|
||||
--entrypoint="install" \
|
||||
@@ -84,6 +85,7 @@ docker run -it --rm \
|
||||
|
||||
```cmd
|
||||
docker run -it --rm ^
|
||||
--publish 20080:20080 ^
|
||||
--volume //var/run/docker.sock:/var/run/docker.sock ^
|
||||
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
|
||||
--entrypoint="install" ^
|
||||
@@ -94,6 +96,7 @@ docker run -it --rm ^
|
||||
|
||||
```powershell
|
||||
docker run -it --rm `
|
||||
--publish 20080:20080 `
|
||||
--volume /var/run/docker.sock:/var/run/docker.sock `
|
||||
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw `
|
||||
--entrypoint="install" `
|
||||
|
||||
@@ -57,21 +57,21 @@
|
||||
"emails.recovery.thanks": "Thanks,",
|
||||
"emails.recovery.buttonText": "Reset password",
|
||||
"emails.recovery.signature": "{{project}} team",
|
||||
"emails.csvExport.success.subject": "Your CSV export is ready",
|
||||
"emails.csvExport.success.preview": "Your data export has been completed successfully.",
|
||||
"emails.csvExport.success.hello": "Hello {{user}},",
|
||||
"emails.csvExport.success.body": "Your CSV export is ready to download. Click the button below to download your data export.",
|
||||
"emails.csvExport.success.footer": "This download link will expire in 1 hour.",
|
||||
"emails.csvExport.success.thanks": "Thanks,",
|
||||
"emails.csvExport.success.buttonText": "Download CSV",
|
||||
"emails.csvExport.success.signature": "Appwrite team",
|
||||
"emails.csvExport.failure.subject": "Your CSV export failed - file too large",
|
||||
"emails.csvExport.failure.preview": "Your data export failed because the file size exceeds your plan limit.",
|
||||
"emails.csvExport.failure.hello": "Hello {{user}},",
|
||||
"emails.csvExport.failure.body": "Your CSV export could not be completed because the export file size ({{size}}MB) exceeds your plan limit. Please consider upgrading your plan or exporting a smaller dataset.",
|
||||
"emails.csvExport.failure.footer": "If you have any questions, please contact our support team.",
|
||||
"emails.csvExport.failure.thanks": "Thanks,",
|
||||
"emails.csvExport.failure.signature": "{{project}} team",
|
||||
"emails.dataExport.success.subject": "Your {{type}} export is ready",
|
||||
"emails.dataExport.success.preview": "Your data export has been completed successfully.",
|
||||
"emails.dataExport.success.hello": "Hello {{user}},",
|
||||
"emails.dataExport.success.body": "Your {{type}} export is ready to download. Click the button below to download your data export.",
|
||||
"emails.dataExport.success.footer": "This download link will expire in 1 hour.",
|
||||
"emails.dataExport.success.thanks": "Thanks,",
|
||||
"emails.dataExport.success.buttonText": "Download {{type}}",
|
||||
"emails.dataExport.success.signature": "Appwrite team",
|
||||
"emails.dataExport.failure.subject": "Your {{type}} export failed - file too large",
|
||||
"emails.dataExport.failure.preview": "Your data export failed because the file size exceeds your plan limit.",
|
||||
"emails.dataExport.failure.hello": "Hello {{user}},",
|
||||
"emails.dataExport.failure.body": "Your {{type}} export could not be completed because the export file size ({{size}}MB) exceeds your plan limit. Please consider upgrading your plan or exporting a smaller dataset.",
|
||||
"emails.dataExport.failure.footer": "If you have any questions, please contact our support team.",
|
||||
"emails.dataExport.failure.thanks": "Thanks,",
|
||||
"emails.dataExport.failure.signature": "{{project}} team",
|
||||
"emails.invitation.subject": "Invitation to {{team}} Team at {{project}}",
|
||||
"emails.invitation.preview": "{{owner}} invited you to join {{team}} at {{project}}",
|
||||
"emails.invitation.hello": "Hello {{user}},",
|
||||
|
||||
@@ -31,9 +31,6 @@ class FunctionUseCases
|
||||
public const DEV_TOOLS = 'dev-tools';
|
||||
public const AUTH = 'auth';
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
public static function getAll(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -25,9 +25,6 @@ class SiteUseCases
|
||||
public const FORMS = 'forms';
|
||||
public const DASHBOARD = 'dashboard';
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
public static function getAll(): array
|
||||
{
|
||||
return [
|
||||
@@ -252,7 +249,7 @@ return [
|
||||
'frameworks' => [
|
||||
getFramework('VITE', [
|
||||
'providerRootDirectory' => './vite/vitepress',
|
||||
'outputDirectory' => '404.html',
|
||||
'fallbackFile' => '404.html',
|
||||
'installCommand' => 'npm i vitepress && npm install',
|
||||
'buildCommand' => 'npm run docs:build',
|
||||
'outputDirectory' => './.vitepress/dist',
|
||||
@@ -275,7 +272,7 @@ return [
|
||||
'frameworks' => [
|
||||
getFramework('VUE', [
|
||||
'providerRootDirectory' => './vue/vuepress',
|
||||
'outputDirectory' => '404.html',
|
||||
'fallbackFile' => '404.html',
|
||||
'installCommand' => 'npm install',
|
||||
'buildCommand' => 'npm run build',
|
||||
'outputDirectory' => './src/.vuepress/dist',
|
||||
@@ -298,7 +295,7 @@ return [
|
||||
'frameworks' => [
|
||||
getFramework('REACT', [
|
||||
'providerRootDirectory' => './react/docusaurus',
|
||||
'outputDirectory' => '404.html',
|
||||
'fallbackFile' => '404.html',
|
||||
'installCommand' => 'npm install',
|
||||
'buildCommand' => 'npm run build',
|
||||
'outputDirectory' => './build',
|
||||
|
||||
@@ -695,6 +695,7 @@ Http::delete('/v1/account/sessions')
|
||||
|
||||
$protocol = $request->getProtocol();
|
||||
$sessions = $user->getAttribute('sessions', []);
|
||||
$currentSession = null;
|
||||
|
||||
foreach ($sessions as $session) {/** @var Document $session */
|
||||
$dbForProject->deleteDocument('sessions', $session->getId());
|
||||
@@ -716,6 +717,7 @@ Http::delete('/v1/account/sessions')
|
||||
->addCookie($store->getKey(), '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'));
|
||||
|
||||
// Use current session for events.
|
||||
$currentSession = $session;
|
||||
$queueForEvents
|
||||
->setPayload($response->output($session, Response::MODEL_SESSION));
|
||||
|
||||
@@ -728,9 +730,11 @@ Http::delete('/v1/account/sessions')
|
||||
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
|
||||
$queueForEvents
|
||||
->setParam('userId', $user->getId())
|
||||
->setParam('sessionId', $session->getId());
|
||||
if ($currentSession instanceof Document) {
|
||||
$queueForEvents
|
||||
->setParam('userId', $user->getId())
|
||||
->setParam('sessionId', $currentSession->getId());
|
||||
}
|
||||
|
||||
$response->noContent();
|
||||
});
|
||||
@@ -776,7 +780,8 @@ Http::get('/v1/account/sessions/:sessionId')
|
||||
->setAttribute('secret', $session->getAttribute('secret', ''))
|
||||
;
|
||||
|
||||
return $response->dynamic($session, Response::MODEL_SESSION);
|
||||
$response->dynamic($session, Response::MODEL_SESSION);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -956,7 +961,7 @@ Http::patch('/v1/account/sessions/:sessionId')
|
||||
->setPayload($response->output($session, Response::MODEL_SESSION))
|
||||
;
|
||||
|
||||
return $response->dynamic($session, Response::MODEL_SESSION);
|
||||
$response->dynamic($session, Response::MODEL_SESSION);
|
||||
});
|
||||
|
||||
Http::post('/v1/account/sessions/email')
|
||||
@@ -1988,7 +1993,7 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
->addCookie($store->getKey(), $encoded, (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'));
|
||||
}
|
||||
|
||||
if (isset($sessionUpgrade) && $sessionUpgrade) {
|
||||
if (isset($sessionUpgrade) && $sessionUpgrade && isset($session)) {
|
||||
foreach ($user->getAttribute('targets', []) as $target) {
|
||||
if ($target->getAttribute('providerType') !== MESSAGE_TYPE_PUSH) {
|
||||
continue;
|
||||
@@ -4715,5 +4720,5 @@ Http::delete('/v1/account/identities/:identityId')
|
||||
->setParam('identityId', $identity->getId())
|
||||
->setPayload($response->output($identity, Response::MODEL_IDENTITY));
|
||||
|
||||
return $response->noContent();
|
||||
$response->noContent();
|
||||
});
|
||||
|
||||
@@ -231,6 +231,7 @@ Http::get('/v1/locale/continents')
|
||||
->inject('locale')
|
||||
->action(function (Response $response, Locale $locale) {
|
||||
$list = array_keys(Config::getParam('locale-continents'));
|
||||
$output = [];
|
||||
|
||||
foreach ($list as $value) {
|
||||
$output[] = new Document([
|
||||
|
||||
@@ -3566,7 +3566,7 @@ Http::post('/v1/messaging/messages/push')
|
||||
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
|
||||
$endpoint = "$protocol://{$platform['apiHostname']}/v1";
|
||||
|
||||
$scheduleTime = $currentScheduledAt ?? $scheduledAt;
|
||||
$scheduleTime = $scheduledAt;
|
||||
if (!\is_null($scheduleTime)) {
|
||||
$expiry = (new \DateTime($scheduleTime))->add(new \DateInterval('P15D'))->format('U');
|
||||
} else {
|
||||
|
||||
@@ -29,6 +29,7 @@ use Utopia\Migration\Resource;
|
||||
use Utopia\Migration\Sources\Appwrite;
|
||||
use Utopia\Migration\Sources\CSV;
|
||||
use Utopia\Migration\Sources\Firebase;
|
||||
use Utopia\Migration\Sources\JSON;
|
||||
use Utopia\Migration\Sources\NHost;
|
||||
use Utopia\Migration\Sources\Supabase;
|
||||
use Utopia\Migration\Transfer;
|
||||
@@ -53,6 +54,15 @@ function getDatabaseTransferResourceServices(string $databaseType)
|
||||
};
|
||||
}
|
||||
|
||||
function getDatabaseResourceType(string $databaseType): string
|
||||
{
|
||||
return match($databaseType) {
|
||||
DATABASE_TYPE_VECTORSDB => Resource::TYPE_DATABASE_VECTORSDB,
|
||||
DATABASE_TYPE_DOCUMENTSDB => Resource::TYPE_DATABASE_DOCUMENTSDB,
|
||||
default => Resource::TYPE_DATABASE,
|
||||
};
|
||||
}
|
||||
|
||||
Http::post('/v1/migrations/appwrite')
|
||||
->groups(['api', 'migrations'])
|
||||
->desc('Create Appwrite migration')
|
||||
@@ -447,6 +457,7 @@ Http::post('/v1/migrations/csv/imports')
|
||||
}
|
||||
$fileSize = $deviceForMigrations->getFileSize($newPath);
|
||||
$resources = Transfer::extractServices([getDatabaseTransferResourceServices($databaseType)]);
|
||||
$resourceType = getDatabaseResourceType($databaseType);
|
||||
|
||||
$migration = $dbForProject->createDocument('migrations', new Document([
|
||||
'$id' => $migrationId,
|
||||
@@ -456,7 +467,7 @@ Http::post('/v1/migrations/csv/imports')
|
||||
'destination' => Appwrite::getName(),
|
||||
'resources' => $resources,
|
||||
'resourceId' => $resourceId,
|
||||
'resourceType' => Resource::TYPE_DATABASE,
|
||||
'resourceType' => $resourceType,
|
||||
'statusCounters' => '{}',
|
||||
'resourceData' => '{}',
|
||||
'errors' => [],
|
||||
@@ -565,16 +576,6 @@ Http::post('/v1/migrations/csv/exports')
|
||||
throw new Exception(Exception::COLLECTION_NOT_FOUND);
|
||||
}
|
||||
|
||||
$validator = new Documents(
|
||||
attributes: $collection->getAttribute('attributes', []),
|
||||
indexes: $collection->getAttribute('indexes', []),
|
||||
idAttributeType: $dbForProject->getAdapter()->getIdAttributeType(),
|
||||
);
|
||||
|
||||
if (!$validator->isValid($parsedQueries)) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
|
||||
}
|
||||
|
||||
// getting databasetype
|
||||
$resources = explode(':', $resourceId);
|
||||
$databaseId = $resources[0];
|
||||
@@ -583,7 +584,23 @@ Http::post('/v1/migrations/csv/exports')
|
||||
if (!in_array($databaseType, CSV_ALLOWED_DATABASE_TYPES)) {
|
||||
throw new Exception(Exception::MIGRATION_DATABASE_TYPE_UNSUPPORTED, 'Database type not supported for csv');
|
||||
}
|
||||
|
||||
// Schemaless databases (DocumentsDB, VectorsDB) allow queries on dynamic fields
|
||||
$isSchemaless = in_array($databaseType, [DATABASE_TYPE_DOCUMENTSDB, DATABASE_TYPE_VECTORSDB]);
|
||||
|
||||
$validator = new Documents(
|
||||
attributes: $collection->getAttribute('attributes', []),
|
||||
indexes: $collection->getAttribute('indexes', []),
|
||||
idAttributeType: $dbForProject->getAdapter()->getIdAttributeType(),
|
||||
supportForAttributes: !$isSchemaless,
|
||||
);
|
||||
|
||||
if (!$validator->isValid($parsedQueries)) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
|
||||
}
|
||||
|
||||
$resources = Transfer::extractServices([getDatabaseTransferResourceServices($databaseType)]);
|
||||
$resourceType = getDatabaseResourceType($databaseType);
|
||||
|
||||
$migration = $dbForProject->createDocument('migrations', new Document([
|
||||
'$id' => ID::unique(),
|
||||
@@ -593,7 +610,7 @@ Http::post('/v1/migrations/csv/exports')
|
||||
'destination' => CSV::getName(),
|
||||
'resources' => $resources,
|
||||
'resourceId' => $resourceId,
|
||||
'resourceType' => Resource::TYPE_DATABASE,
|
||||
'resourceType' => $resourceType,
|
||||
'statusCounters' => '{}',
|
||||
'resourceData' => '{}',
|
||||
'errors' => [],
|
||||
@@ -624,6 +641,291 @@ Http::post('/v1/migrations/csv/exports')
|
||||
->dynamic($migration, Response::MODEL_MIGRATION);
|
||||
});
|
||||
|
||||
Http::post('/v1/migrations/json/imports')
|
||||
->groups(['api', 'migrations'])
|
||||
->desc('Import documents from a JSON')
|
||||
->label('scope', 'migrations.write')
|
||||
->label('event', 'migrations.[migrationId].create')
|
||||
->label('audits.event', 'migration.create')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'migrations',
|
||||
group: null,
|
||||
name: 'createJSONImport',
|
||||
description: '/docs/references/migrations/migration-json-import.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_ACCEPTED,
|
||||
model: Response::MODEL_MIGRATION,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('bucketId', '', new UID(), 'Storage bucket unique ID. You can create a new storage bucket using the Storage service [server integration](https://appwrite.io/docs/server/storage#createBucket).')
|
||||
->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)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('dbForPlatform')
|
||||
->inject('authorization')
|
||||
->inject('project')
|
||||
->inject('platform')
|
||||
->inject('deviceForFiles')
|
||||
->inject('deviceForMigrations')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForMigrations')
|
||||
->action(function (
|
||||
string $bucketId,
|
||||
string $fileId,
|
||||
string $resourceId,
|
||||
bool $internalFile,
|
||||
Response $response,
|
||||
Database $dbForProject,
|
||||
Database $dbForPlatform,
|
||||
Authorization $authorization,
|
||||
Document $project,
|
||||
array $platform,
|
||||
Device $deviceForFiles,
|
||||
Device $deviceForMigrations,
|
||||
Event $queueForEvents,
|
||||
Migration $queueForMigrations
|
||||
) {
|
||||
$bucket = $authorization->skip(function () use ($internalFile, $dbForPlatform, $dbForProject, $bucketId) {
|
||||
if ($internalFile) {
|
||||
return $dbForPlatform->getDocument('buckets', 'default');
|
||||
}
|
||||
return $dbForProject->getDocument('buckets', $bucketId);
|
||||
});
|
||||
|
||||
if ($bucket->isEmpty()) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
}
|
||||
|
||||
$file = $authorization->skip(fn () => $internalFile ? $dbForPlatform->getDocument('bucket_' . $bucket->getSequence(), $fileId) : $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId));
|
||||
if ($file->isEmpty()) {
|
||||
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$path = $file->getAttribute('path', '');
|
||||
if (!$deviceForFiles->exists($path)) {
|
||||
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND, 'File not found in ' . $path);
|
||||
}
|
||||
|
||||
// No encryption or compression on files above 20MB.
|
||||
$hasEncryption = !empty($file->getAttribute('openSSLCipher'));
|
||||
$compression = $file->getAttribute('algorithm', Compression::NONE);
|
||||
$hasCompression = $compression !== Compression::NONE;
|
||||
|
||||
$migrationId = ID::unique();
|
||||
$newPath = $deviceForMigrations->getPath($migrationId . '_' . $fileId . '.json');
|
||||
|
||||
if ($hasEncryption || $hasCompression) {
|
||||
$source = $deviceForFiles->read($path);
|
||||
|
||||
if ($hasEncryption) {
|
||||
$source = OpenSSL::decrypt(
|
||||
$source,
|
||||
$file->getAttribute('openSSLCipher'),
|
||||
System::getEnv('_APP_OPENSSL_KEY_V' . $file->getAttribute('openSSLVersion')),
|
||||
0,
|
||||
hex2bin($file->getAttribute('openSSLIV')),
|
||||
hex2bin($file->getAttribute('openSSLTag'))
|
||||
);
|
||||
}
|
||||
|
||||
if ($hasCompression) {
|
||||
switch ($compression) {
|
||||
case Compression::ZSTD:
|
||||
$source = (new Zstd())->decompress($source);
|
||||
break;
|
||||
case Compression::GZIP:
|
||||
$source = (new GZIP())->decompress($source);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Manual write after decryption and/or decompression
|
||||
if (!$deviceForMigrations->write($newPath, $source, 'application/json')) {
|
||||
throw new \Exception('Unable to copy file');
|
||||
}
|
||||
} elseif (!$deviceForFiles->transfer($path, $newPath, $deviceForMigrations)) {
|
||||
throw new \Exception('Unable to copy file');
|
||||
}
|
||||
|
||||
$fileSize = $deviceForMigrations->getFileSize($newPath);
|
||||
|
||||
[$databaseId] = \explode(':', $resourceId, 2);
|
||||
$database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
if ($database->isEmpty()) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
}
|
||||
$databaseType = $database->getAttribute('type');
|
||||
$resources = Transfer::extractServices([getDatabaseTransferResourceServices($databaseType)]);
|
||||
$resourceType = getDatabaseResourceType($databaseType);
|
||||
|
||||
$migration = $dbForProject->createDocument('migrations', new Document([
|
||||
'$id' => $migrationId,
|
||||
'status' => 'pending',
|
||||
'stage' => 'init',
|
||||
'source' => JSON::getName(),
|
||||
'destination' => Appwrite::getName(),
|
||||
'resources' => $resources,
|
||||
'resourceId' => $resourceId,
|
||||
'resourceType' => $resourceType,
|
||||
'statusCounters' => '{}',
|
||||
'resourceData' => '{}',
|
||||
'errors' => [],
|
||||
'options' => [
|
||||
'path' => $newPath,
|
||||
'size' => $fileSize,
|
||||
],
|
||||
]));
|
||||
|
||||
$queueForEvents->setParam('migrationId', $migration->getId());
|
||||
|
||||
$queueForMigrations
|
||||
->setMigration($migration)
|
||||
->setProject($project)
|
||||
->setPlatform($platform)
|
||||
->trigger();
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
|
||||
->dynamic($migration, Response::MODEL_MIGRATION);
|
||||
});
|
||||
|
||||
Http::post('/v1/migrations/json/exports')
|
||||
->groups(['api', 'migrations'])
|
||||
->desc('Export documents to JSON')
|
||||
->label('scope', 'migrations.write')
|
||||
->label('event', 'migrations.[migrationId].create')
|
||||
->label('audits.event', 'migration.create')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'migrations',
|
||||
group: null,
|
||||
name: 'createJSONExport',
|
||||
description: '/docs/references/migrations/migration-json-export.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_ACCEPTED,
|
||||
model: Response::MODEL_MIGRATION,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('resourceId', null, new CompoundUID(), 'Composite ID in the format {databaseId:collectionId}, identifying a collection within a database to export.')
|
||||
->param('filename', '', new Text(255), 'The name of the file to be created for the export, excluding the .json extension.')
|
||||
->param('columns', [], new ArrayList(new Text(Database::LENGTH_KEY)), 'List of attributes to export. If empty, all attributes will be exported. You can use the `*` wildcard to export all attributes from the collection.', true)
|
||||
->param('queries', [], new ArrayList(new Text(0)), 'Array of query strings generated using the Query class provided by the SDK to filter documents to export. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true)
|
||||
->param('notify', true, new Boolean(), 'Set to true to receive an email when the export is complete. Default is true.', true)
|
||||
->inject('user')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('dbForPlatform')
|
||||
->inject('authorization')
|
||||
->inject('project')
|
||||
->inject('platform')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForMigrations')
|
||||
->action(function (
|
||||
string $resourceId,
|
||||
string $filename,
|
||||
array $columns,
|
||||
array $queries,
|
||||
bool $notify,
|
||||
Document $user,
|
||||
Response $response,
|
||||
Database $dbForProject,
|
||||
Database $dbForPlatform,
|
||||
Authorization $authorization,
|
||||
Document $project,
|
||||
array $platform,
|
||||
Event $queueForEvents,
|
||||
Migration $queueForMigrations
|
||||
) {
|
||||
try {
|
||||
$parsedQueries = Query::parseQueries($queries);
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
$bucket = $authorization->skip(fn () => $dbForPlatform->getDocument('buckets', 'default'));
|
||||
if ($bucket->isEmpty()) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
}
|
||||
|
||||
[$databaseId, $collectionId] = \explode(':', $resourceId, 2);
|
||||
if (empty($databaseId)) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
}
|
||||
if (empty($collectionId)) {
|
||||
throw new Exception(Exception::COLLECTION_NOT_FOUND);
|
||||
}
|
||||
|
||||
$database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
|
||||
if ($database->isEmpty()) {
|
||||
throw new Exception(Exception::DATABASE_NOT_FOUND);
|
||||
}
|
||||
|
||||
$collection = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId));
|
||||
if ($collection->isEmpty()) {
|
||||
throw new Exception(Exception::COLLECTION_NOT_FOUND);
|
||||
}
|
||||
|
||||
$databaseType = $database->getAttribute('type');
|
||||
|
||||
// Schemaless databases (DocumentsDB, VectorsDB) allow queries on dynamic fields
|
||||
$isSchemaless = in_array($databaseType, [DATABASE_TYPE_DOCUMENTSDB, DATABASE_TYPE_VECTORSDB]);
|
||||
|
||||
$validator = new Documents(
|
||||
attributes: $collection->getAttribute('attributes', []),
|
||||
indexes: $collection->getAttribute('indexes', []),
|
||||
idAttributeType: $dbForProject->getAdapter()->getIdAttributeType(),
|
||||
supportForAttributes: !$isSchemaless,
|
||||
);
|
||||
|
||||
if (!$validator->isValid($parsedQueries)) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
|
||||
}
|
||||
|
||||
$resources = Transfer::extractServices([getDatabaseTransferResourceServices($databaseType)]);
|
||||
$resourceType = getDatabaseResourceType($databaseType);
|
||||
|
||||
$migration = $dbForProject->createDocument('migrations', new Document([
|
||||
'$id' => ID::unique(),
|
||||
'status' => 'pending',
|
||||
'stage' => 'init',
|
||||
'source' => Appwrite::getName(),
|
||||
'destination' => JSON::getName(),
|
||||
'resources' => $resources,
|
||||
'resourceId' => $resourceId,
|
||||
'resourceType' => $resourceType,
|
||||
'statusCounters' => '{}',
|
||||
'resourceData' => '{}',
|
||||
'errors' => [],
|
||||
'options' => [
|
||||
'bucketId' => 'default', // Always use internal bucket
|
||||
'filename' => $filename,
|
||||
'columns' => $columns,
|
||||
'queries' => $queries,
|
||||
'notify' => $notify,
|
||||
'userInternalId' => $user->getSequence(),
|
||||
],
|
||||
]));
|
||||
|
||||
$queueForEvents->setParam('migrationId', $migration->getId());
|
||||
|
||||
$queueForMigrations
|
||||
->setMigration($migration)
|
||||
->setProject($project)
|
||||
->setPlatform($platform)
|
||||
->trigger();
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
|
||||
->dynamic($migration, Response::MODEL_MIGRATION);
|
||||
});
|
||||
|
||||
Http::get('/v1/migrations')
|
||||
->groups(['api', 'migrations'])
|
||||
->desc('List migrations')
|
||||
|
||||
@@ -1570,7 +1570,7 @@ Http::post('/v1/projects/:projectId/smtp/tests')
|
||||
->trigger();
|
||||
}
|
||||
|
||||
return $response->noContent();
|
||||
$response->noContent();
|
||||
});
|
||||
|
||||
Http::get('/v1/projects/:projectId/templates/sms/:type/:locale')
|
||||
|
||||
@@ -2337,9 +2337,8 @@ Http::post('/v1/users/:userId/sessions')
|
||||
->setParam('sessionId', $session->getId())
|
||||
->setPayload($response->output($session, Response::MODEL_SESSION));
|
||||
|
||||
return $response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($session, Response::MODEL_SESSION);
|
||||
$response->setStatusCode(Response::STATUS_CODE_CREATED);
|
||||
$response->dynamic($session, Response::MODEL_SESSION);
|
||||
});
|
||||
|
||||
Http::post('/v1/users/:userId/tokens')
|
||||
@@ -2402,9 +2401,8 @@ Http::post('/v1/users/:userId/tokens')
|
||||
->setParam('tokenId', $token->getId())
|
||||
->setPayload($response->output($token, Response::MODEL_TOKEN));
|
||||
|
||||
return $response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($token, Response::MODEL_TOKEN);
|
||||
$response->setStatusCode(Response::STATUS_CODE_CREATED);
|
||||
$response->dynamic($token, Response::MODEL_TOKEN);
|
||||
});
|
||||
|
||||
Http::delete('/v1/users/:userId/sessions/:sessionId')
|
||||
@@ -2658,7 +2656,7 @@ Http::delete('/v1/users/identities/:identityId')
|
||||
->setParam('identityId', $identity->getId())
|
||||
->setPayload($response->output($identity, Response::MODEL_IDENTITY));
|
||||
|
||||
return $response->noContent();
|
||||
$response->noContent();
|
||||
});
|
||||
|
||||
Http::post('/v1/users/:userId/jwts')
|
||||
|
||||
+17
-10
@@ -166,14 +166,14 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
if ($request->getMethod() !== Request::METHOD_GET) {
|
||||
throw new AppwriteException(AppwriteException::GENERAL_PROTOCOL_UNSUPPORTED, 'Method unsupported over HTTP. Please use HTTPS instead.', view: $errorView);
|
||||
}
|
||||
return $response->redirect('https://' . $request->getHostname() . $request->getURI());
|
||||
$response->redirect('https://' . $request->getHostname() . $request->getURI());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** @var Database $dbForProject */
|
||||
$dbForProject = $getProjectDB($project);
|
||||
|
||||
/** @var Document $deployment */
|
||||
if (!empty($rule->getAttribute('deploymentId', ''))) {
|
||||
$deployment = $authorization->skip(fn () => $dbForProject->getDocument('deployments', $rule->getAttribute('deploymentId')));
|
||||
} else {
|
||||
@@ -244,6 +244,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
if ($isPreview && $requirePreview) {
|
||||
$cookie = $request->getCookie(COOKIE_NAME_PREVIEW, '');
|
||||
$authorized = false;
|
||||
$user = new Document();
|
||||
|
||||
// Security checks to mark authorized true
|
||||
if (!empty($cookie)) {
|
||||
@@ -273,7 +274,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
|
||||
$membershipExists = false;
|
||||
$project = $authorization->skip(fn () => $dbForPlatform->getDocument('projects', $projectId));
|
||||
if (!$project->isEmpty() && isset($user)) {
|
||||
if (!$project->isEmpty() && !$user->isEmpty()) {
|
||||
$teamId = $project->getAttribute('teamId', '');
|
||||
$membership = $user->find('teamId', $teamId, 'memberships');
|
||||
if (!empty($membership)) {
|
||||
@@ -379,7 +380,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
$executionId = ID::unique();
|
||||
|
||||
$headers = \array_merge([], $requestHeaders);
|
||||
$headers['x-appwrite-execution-id'] = $executionId ?? '';
|
||||
$headers['x-appwrite-execution-id'] = $executionId;
|
||||
$headers['x-appwrite-user-id'] = '';
|
||||
$headers['x-appwrite-country-code'] = '';
|
||||
$headers['x-appwrite-continent-code'] = '';
|
||||
@@ -459,7 +460,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
if ($version === 'v2') {
|
||||
$vars = \array_merge($vars, [
|
||||
'APPWRITE_FUNCTION_TRIGGER' => $headers['x-appwrite-trigger'] ?? '',
|
||||
'APPWRITE_FUNCTION_DATA' => $body ?? '',
|
||||
'APPWRITE_FUNCTION_DATA' => $body,
|
||||
'APPWRITE_FUNCTION_USER_ID' => $headers['x-appwrite-user-id'] ?? '',
|
||||
'APPWRITE_FUNCTION_JWT' => $headers['x-appwrite-user-jwt'] ?? ''
|
||||
]);
|
||||
@@ -529,6 +530,11 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
}
|
||||
|
||||
/** Execute function */
|
||||
$executionResponse = [
|
||||
'headers' => [],
|
||||
'body' => '',
|
||||
];
|
||||
|
||||
try {
|
||||
$version = match ($type) {
|
||||
'function' => $resource->getAttribute('version', 'v2'),
|
||||
@@ -734,7 +740,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
|
||||
$execution->setAttribute('responseBody', $executionResponse['body'] ?? '');
|
||||
$execution->setAttribute('responseHeaders', $headers);
|
||||
|
||||
$body = $execution['responseBody'] ?? '';
|
||||
$body = $execution['responseBody'];
|
||||
|
||||
$contentType = 'text/plain';
|
||||
foreach ($executionResponse['headers'] as $name => $values) {
|
||||
@@ -865,9 +871,9 @@ Http::init()
|
||||
Request::setRoute($route);
|
||||
|
||||
if ($route === null) {
|
||||
return $response
|
||||
->setStatusCode(404)
|
||||
->send('Not Found');
|
||||
$response->setStatusCode(404);
|
||||
$response->send('Not Found');
|
||||
return;
|
||||
}
|
||||
|
||||
$requestFormat = $request->getHeader('x-appwrite-response-format', System::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
|
||||
@@ -973,7 +979,8 @@ Http::init()
|
||||
throw new AppwriteException(AppwriteException::GENERAL_PROTOCOL_UNSUPPORTED, 'Method unsupported over HTTP. Please use HTTPS instead.');
|
||||
}
|
||||
|
||||
return $response->redirect('https://' . $request->getHostname() . $request->getURI());
|
||||
$response->redirect('https://' . $request->getHostname() . $request->getURI());
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
+30
-28
@@ -244,36 +244,38 @@ Http::get('/v1/mock/github/callback')
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND, $error);
|
||||
}
|
||||
|
||||
if (!empty($providerInstallationId)) {
|
||||
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
|
||||
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
|
||||
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
|
||||
$owner = $github->getOwnerName($providerInstallationId) ?? '';
|
||||
|
||||
$projectInternalId = $project->getSequence();
|
||||
|
||||
$teamId = $project->getAttribute('teamId', '');
|
||||
|
||||
$installation = new Document([
|
||||
'$id' => ID::unique(),
|
||||
'$permissions' => [
|
||||
Permission::read(Role::team(ID::custom($teamId))),
|
||||
Permission::update(Role::team(ID::custom($teamId), 'owner')),
|
||||
Permission::update(Role::team(ID::custom($teamId), 'developer')),
|
||||
Permission::delete(Role::team(ID::custom($teamId), 'owner')),
|
||||
Permission::delete(Role::team(ID::custom($teamId), 'developer')),
|
||||
],
|
||||
'providerInstallationId' => $providerInstallationId,
|
||||
'projectId' => $projectId,
|
||||
'projectInternalId' => $projectInternalId,
|
||||
'provider' => 'github',
|
||||
'organization' => $owner,
|
||||
'personal' => false
|
||||
]);
|
||||
|
||||
$installation = $dbForPlatform->createDocument('installations', $installation);
|
||||
if (empty($providerInstallationId)) {
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Missing provider installation ID');
|
||||
}
|
||||
|
||||
$privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY');
|
||||
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
|
||||
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
|
||||
$owner = $github->getOwnerName($providerInstallationId) ?? '';
|
||||
|
||||
$projectInternalId = $project->getSequence();
|
||||
|
||||
$teamId = $project->getAttribute('teamId', '');
|
||||
|
||||
$installation = new Document([
|
||||
'$id' => ID::unique(),
|
||||
'$permissions' => [
|
||||
Permission::read(Role::team(ID::custom($teamId))),
|
||||
Permission::update(Role::team(ID::custom($teamId), 'owner')),
|
||||
Permission::update(Role::team(ID::custom($teamId), 'developer')),
|
||||
Permission::delete(Role::team(ID::custom($teamId), 'owner')),
|
||||
Permission::delete(Role::team(ID::custom($teamId), 'developer')),
|
||||
],
|
||||
'providerInstallationId' => $providerInstallationId,
|
||||
'projectId' => $projectId,
|
||||
'projectInternalId' => $projectInternalId,
|
||||
'provider' => 'github',
|
||||
'organization' => $owner,
|
||||
'personal' => false
|
||||
]);
|
||||
|
||||
$installation = $dbForPlatform->createDocument('installations', $installation);
|
||||
|
||||
$response->json([
|
||||
'installationId' => $installation->getId(),
|
||||
]);
|
||||
|
||||
+4
-3
@@ -3,6 +3,8 @@
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
require_once __DIR__ . '/init/span.php';
|
||||
|
||||
global $register;
|
||||
|
||||
use Appwrite\Utopia\Request;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Swoole\Constant;
|
||||
@@ -31,7 +33,6 @@ use Utopia\Http\Files;
|
||||
use Utopia\Http\Http;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Logger\Log\User;
|
||||
use Utopia\Pools\Group;
|
||||
use Utopia\Span\Span;
|
||||
use Utopia\System\System;
|
||||
|
||||
@@ -293,7 +294,7 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $tot
|
||||
|
||||
go(function () use ($register, $app) {
|
||||
$pools = $register->get('pools');
|
||||
/** @var Group $pools */
|
||||
/** @var \Utopia\Pools\Group $pools */
|
||||
Http::setResource('pools', fn () => $pools);
|
||||
|
||||
/** @var array $collections */
|
||||
@@ -655,7 +656,7 @@ $http->on(Constant::EVENT_TASK, function () use ($register) {
|
||||
/** @var Utopia\Database\Database $dbForPlatform */
|
||||
$dbForPlatform = $app->getResource('dbForPlatform');
|
||||
|
||||
/** @var Table $riskyDomains */
|
||||
/** @var \Swoole\Table $riskyDomains */
|
||||
$riskyDomains = $app->getResource('riskyDomains');
|
||||
|
||||
Timer::tick(DOMAIN_SYNC_TIMER * 1000, function () use ($dbForPlatform, $riskyDomains, &$lastSyncUpdate, $app) {
|
||||
|
||||
+21
-36
@@ -6,7 +6,6 @@ use Appwrite\Hooks\Hooks;
|
||||
use Appwrite\PubSub\Adapter\Redis as PubSub;
|
||||
use Appwrite\URL\URL as AppwriteURL;
|
||||
use MaxMind\Db\Reader;
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use Swoole\Database\PDOProxy;
|
||||
use Utopia\Cache\Adapter\Redis as RedisCache;
|
||||
use Utopia\Config\Config;
|
||||
@@ -25,6 +24,7 @@ use Utopia\Logger\Adapter\LogOwl;
|
||||
use Utopia\Logger\Adapter\Raygun;
|
||||
use Utopia\Logger\Adapter\Sentry;
|
||||
use Utopia\Logger\Logger;
|
||||
use Utopia\Messaging\Adapter\Email\SMTP;
|
||||
use Utopia\Mongo\Client as MongoClient;
|
||||
use Utopia\Pools\Adapter\Stack as StackPool;
|
||||
use Utopia\Pools\Adapter\Swoole as SwoolePool;
|
||||
@@ -56,7 +56,7 @@ $register->set('logger', function () {
|
||||
}
|
||||
|
||||
try {
|
||||
$loggingProvider = new DSN($providerConfig ?? '');
|
||||
$loggingProvider = new DSN($providerConfig);
|
||||
|
||||
$providerName = $loggingProvider->getScheme();
|
||||
$providerConfig = match ($providerName) {
|
||||
@@ -76,7 +76,7 @@ $register->set('logger', function () {
|
||||
};
|
||||
}
|
||||
|
||||
if (empty($providerName) || empty($providerConfig)) {
|
||||
if (empty($providerName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ $register->set('realtimeLogger', function () {
|
||||
default => ['key' => $loggingProvider->getHost()],
|
||||
};
|
||||
|
||||
if (empty($providerName) || empty($providerConfig)) {
|
||||
if (empty($providerName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -242,8 +242,8 @@ $register->set('pools', function () {
|
||||
],
|
||||
];
|
||||
|
||||
$maxConnections = System::getEnv('_APP_CONNECTIONS_MAX', 151);
|
||||
$instanceConnections = $maxConnections / System::getEnv('_APP_POOL_CLIENTS', 14);
|
||||
$maxConnections = (int) System::getEnv('_APP_CONNECTIONS_MAX', 151);
|
||||
$instanceConnections = $maxConnections / (int) System::getEnv('_APP_POOL_CLIENTS', 14);
|
||||
|
||||
$multiprocessing = System::getEnv('_APP_SERVER_MULTIPROCESS', 'disabled') === 'enabled';
|
||||
|
||||
@@ -308,7 +308,7 @@ $register->set('pools', function () {
|
||||
]);
|
||||
});
|
||||
},
|
||||
'mongodb' => function () use ($dsnHost, $dsnPort, $dsnUser, $dsnPass, $dsnDatabase, $dsn) {
|
||||
'mongodb' => function () use ($dsnHost, $dsnPort, $dsnUser, $dsnPass, $dsnDatabase) {
|
||||
try {
|
||||
$mongo = new MongoClient($dsnDatabase, $dsnHost, (int)$dsnPort, $dsnUser, $dsnPass, false);
|
||||
@$mongo->connect();
|
||||
@@ -433,35 +433,20 @@ $register->set('db', function () {
|
||||
});
|
||||
|
||||
$register->set('smtp', function () {
|
||||
$mail = new PHPMailer(true);
|
||||
|
||||
$mail->isSMTP();
|
||||
|
||||
$username = System::getEnv('_APP_SMTP_USERNAME');
|
||||
$password = System::getEnv('_APP_SMTP_PASSWORD');
|
||||
|
||||
$mail->XMailer = 'Appwrite Mailer';
|
||||
$mail->Host = System::getEnv('_APP_SMTP_HOST', 'smtp');
|
||||
$mail->Port = System::getEnv('_APP_SMTP_PORT', 25);
|
||||
$mail->SMTPAuth = !empty($username) && !empty($password);
|
||||
$mail->Username = $username;
|
||||
$mail->Password = $password;
|
||||
$mail->SMTPSecure = System::getEnv('_APP_SMTP_SECURE', '');
|
||||
$mail->SMTPAutoTLS = false;
|
||||
$mail->SMTPKeepAlive = true;
|
||||
$mail->CharSet = 'UTF-8';
|
||||
$mail->Timeout = 10; /* Connection timeout */
|
||||
$mail->getSMTPInstance()->Timelimit = 30; /* Timeout for each individual SMTP command (e.g. HELO, EHLO, etc.) */
|
||||
|
||||
$from = \urldecode(System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'));
|
||||
$email = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM);
|
||||
|
||||
$mail->setFrom($email, $from);
|
||||
$mail->addReplyTo($email, $from);
|
||||
|
||||
$mail->isHTML(true);
|
||||
|
||||
return $mail;
|
||||
$username = System::getEnv('_APP_SMTP_USERNAME', '');
|
||||
$password = System::getEnv('_APP_SMTP_PASSWORD', '');
|
||||
return new SMTP(
|
||||
host: System::getEnv('_APP_SMTP_HOST', 'smtp'),
|
||||
port: (int) System::getEnv('_APP_SMTP_PORT', 25),
|
||||
username: $username,
|
||||
password: $password,
|
||||
smtpSecure: System::getEnv('_APP_SMTP_SECURE', ''),
|
||||
smtpAutoTLS: false,
|
||||
xMailer: 'Appwrite Mailer',
|
||||
timeout: 10,
|
||||
keepAlive: true,
|
||||
timelimit: 30,
|
||||
);
|
||||
});
|
||||
$register->set('geodb', function () {
|
||||
return new Reader(__DIR__ . '/../assets/dbip/dbip-country-lite-2025-12.mmdb');
|
||||
|
||||
+26
-5
@@ -696,7 +696,7 @@ Http::setResource('dbForProject', function (Group $pools, Database $dbForPlatfor
|
||||
$cacheKey = \sprintf(
|
||||
'%s-cache-%s:%s:%s:project:%s:functions:events',
|
||||
$dbForProject->getCacheName(),
|
||||
$hostname ?? '',
|
||||
$hostname,
|
||||
$dbForProject->getNamespace(),
|
||||
$dbForProject->getTenant(),
|
||||
$project->getId()
|
||||
@@ -888,7 +888,6 @@ Http::setResource('dbForPlatform', function (Group $pools, Cache $cache, Authori
|
||||
}, ['pools', 'cache', 'authorization']);
|
||||
|
||||
Http::setResource('getDatabasesDB', function (Group $pools, Cache $cache, Document $project, Request $request, UsageContext $usage, Authorization $authorization) {
|
||||
|
||||
return function (Document $database) use ($pools, $cache, $project, $request, $usage, $authorization): Database {
|
||||
$databaseDSN = $database->getAttribute('database', $project->getAttribute('database', ''));
|
||||
$databaseType = $database->getAttribute('type', '');
|
||||
@@ -907,11 +906,12 @@ Http::setResource('getDatabasesDB', function (Group $pools, Cache $cache, Docume
|
||||
$dsn = new DSN('mysql://' . $project->getAttribute('database'));
|
||||
}
|
||||
|
||||
$pool = $pools->get($databaseDSN->getHost());
|
||||
$databaseHost = $databaseDSN->getHost();
|
||||
$pool = $pools->get($databaseHost);
|
||||
|
||||
$adapter = new DatabasePool($pool);
|
||||
$database = new Database($adapter, $cache);
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
$sharedTables = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')));
|
||||
|
||||
$database
|
||||
->setDatabase(APP_DATABASE)
|
||||
@@ -922,7 +922,28 @@ Http::setResource('getDatabasesDB', function (Group $pools, Cache $cache, Docume
|
||||
->setMaxQueryValues(APP_DATABASE_QUERY_MAX_VALUES);
|
||||
// inside pools authorization needs to be set first
|
||||
$database->getAdapter()->setSupportForAttributes($databaseType !== DOCUMENTSDB);
|
||||
if (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
|
||||
// For separate pools (documentsdb/vectorsdb), check their own shared tables config.
|
||||
// If not configured, use dedicated mode to avoid cross-engine tenant type mismatches.
|
||||
if ($databaseHost !== $dsn->getHost()) {
|
||||
$dbTypeSharedTables = match ($databaseType) {
|
||||
DOCUMENTSDB => \array_filter(\explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES', ''))),
|
||||
VECTORSDB => \array_filter(\explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES', ''))),
|
||||
default => [],
|
||||
};
|
||||
|
||||
if (\in_array($databaseHost, $dbTypeSharedTables)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setTenant((int)$project->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
$database
|
||||
->setSharedTables(false)
|
||||
->setTenant(null)
|
||||
->setNamespace('_' . $project->getSequence());
|
||||
}
|
||||
} elseif (\in_array($dsn->getHost(), $sharedTables)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setTenant((int)$project->getSequence())
|
||||
|
||||
+6
-7
@@ -237,7 +237,6 @@ if (!function_exists('getTelemetry')) {
|
||||
if (!function_exists('triggerStats')) {
|
||||
function triggerStats(array $event, string $projectId): void
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,14 +319,14 @@ if (!function_exists('logError')) {
|
||||
|
||||
$server->error(logError(...));
|
||||
|
||||
$server->onStart(function () use ($stats, $register, $containerId, &$statsDocument) {
|
||||
$server->onStart(function () use ($stats, $containerId, &$statsDocument) {
|
||||
sleep(5); // wait for the initial database schema to be ready
|
||||
Console::success('Server started successfully');
|
||||
|
||||
/**
|
||||
* Create document for this worker to share stats across Containers.
|
||||
*/
|
||||
go(function () use ($register, $containerId, &$statsDocument) {
|
||||
go(function () use ($containerId, &$statsDocument) {
|
||||
$attempts = 0;
|
||||
$database = getConsoleDB();
|
||||
|
||||
@@ -357,7 +356,7 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume
|
||||
*/
|
||||
// TODO: Remove this if check once it doesn't cause issues for cloud
|
||||
if (System::getEnv('_APP_EDITION', 'self-hosted') === 'self-hosted') {
|
||||
Timer::tick(5000, function () use ($register, $stats, &$statsDocument) {
|
||||
Timer::tick(5000, function () use ($stats, &$statsDocument) {
|
||||
$payload = [];
|
||||
foreach ($stats as $projectId => $value) {
|
||||
$payload[$projectId] = $stats->get($projectId, 'connectionsTotal');
|
||||
@@ -396,7 +395,7 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
|
||||
$attempts = 0;
|
||||
$start = time();
|
||||
|
||||
Timer::tick(5000, function () use ($server, $register, $realtime, $stats) {
|
||||
Timer::tick(5000, function () use ($server, $realtime, $stats) {
|
||||
/**
|
||||
* Sending current connections to project channels on the console project every 5 seconds.
|
||||
*/
|
||||
@@ -798,7 +797,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
}
|
||||
});
|
||||
|
||||
$server->onMessage(function (int $connection, string $message) use ($server, $register, $realtime, $containerId) {
|
||||
$server->onMessage(function (int $connection, string $message) use ($server, $realtime, $containerId) {
|
||||
$project = null;
|
||||
$authorization = null;
|
||||
|
||||
@@ -810,7 +809,7 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
||||
// Get authorization from connection (stored during onOpen)
|
||||
$authorization = $realtime->connections[$connection]['authorization'] ?? null;
|
||||
if ($authorization === null) {
|
||||
$authorization = new Authorization('');
|
||||
$authorization = new Authorization();
|
||||
}
|
||||
|
||||
$database = getConsoleDB();
|
||||
|
||||
+27
-6
@@ -246,7 +246,8 @@ Server::setResource('getDatabasesDB', function (Cache $cache, Registry $register
|
||||
}
|
||||
|
||||
$pools = $register->get('pools');
|
||||
$pool = $pools->get($databaseDSN->getHost());
|
||||
$databaseHost = $databaseDSN->getHost();
|
||||
$pool = $pools->get($databaseHost);
|
||||
|
||||
$adapter = new DatabasePool($pool);
|
||||
$database = new Database($adapter, $cache);
|
||||
@@ -255,9 +256,29 @@ Server::setResource('getDatabasesDB', function (Cache $cache, Registry $register
|
||||
->setAuthorization($authorization);
|
||||
$database->getAdapter()->setSupportForAttributes($databaseType !== DOCUMENTSDB);
|
||||
|
||||
$sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', ''));
|
||||
$sharedTables = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')));
|
||||
|
||||
if (\in_array($dsn->getHost(), $sharedTables, true)) {
|
||||
// For separate pools (documentsdb/vectorsdb), check their own shared tables config.
|
||||
// If not configured, use dedicated mode to avoid cross-engine tenant type mismatches.
|
||||
if ($databaseHost !== $dsn->getHost()) {
|
||||
$dbTypeSharedTables = match ($databaseType) {
|
||||
DOCUMENTSDB => \array_filter(\explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES', ''))),
|
||||
VECTORSDB => \array_filter(\explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES', ''))),
|
||||
default => [],
|
||||
};
|
||||
|
||||
if (\in_array($databaseHost, $dbTypeSharedTables)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setTenant((int) $projectDocument->getSequence())
|
||||
->setNamespace($dsn->getParam('namespace'));
|
||||
} else {
|
||||
$database
|
||||
->setSharedTables(false)
|
||||
->setTenant(null)
|
||||
->setNamespace('_' . $projectDocument->getSequence());
|
||||
}
|
||||
} elseif (\in_array($dsn->getHost(), $sharedTables, true)) {
|
||||
$database
|
||||
->setSharedTables(true)
|
||||
->setTenant((int) $projectDocument->getSequence())
|
||||
@@ -280,14 +301,14 @@ Server::setResource('abuseRetention', function () {
|
||||
|
||||
Server::setResource('auditRetention', function (Document $project) {
|
||||
if ($project->getId() === 'console') {
|
||||
return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE', 15778800)); // 6 months
|
||||
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE', 15778800)); // 6 months
|
||||
}
|
||||
|
||||
return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 1209600)); // 14 days
|
||||
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_AUDIT', 1209600)); // 14 days
|
||||
}, ['project']);
|
||||
|
||||
Server::setResource('executionRetention', function () {
|
||||
return DateTime::addSeconds(new \DateTime(), -1 * System::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', 1209600)); // 14 days
|
||||
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', 1209600)); // 14 days
|
||||
});
|
||||
|
||||
Server::setResource('cache', function (Registry $register) {
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
_APP_DB_ADAPTER=mariadb
|
||||
_APP_DB_HOST=mariadb
|
||||
_APP_DB_PORT=3306
|
||||
@@ -1,36 +0,0 @@
|
||||
x-logging: &x-logging
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-file: "5"
|
||||
max-size: "10m"
|
||||
|
||||
services:
|
||||
mongodb:
|
||||
scale: 0
|
||||
|
||||
mariadb:
|
||||
image: mariadb:10.11 # fix issues when upgrading using: mysql_upgrade -u root -p
|
||||
container_name: appwrite-mariadb
|
||||
<<: *x-logging
|
||||
networks:
|
||||
- appwrite
|
||||
volumes:
|
||||
- appwrite-mariadb:/var/lib/mysql:rw
|
||||
ports:
|
||||
- "3306:3306"
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=${_APP_DB_ROOT_PASS}
|
||||
- MYSQL_DATABASE=${_APP_DB_SCHEMA}
|
||||
- MYSQL_USER=${_APP_DB_USER}
|
||||
- MYSQL_PASSWORD=${_APP_DB_PASS}
|
||||
- MARIADB_AUTO_UPGRADE=1
|
||||
command: "mysqld --innodb-flush-method=fsync"
|
||||
healthcheck:
|
||||
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
|
||||
volumes:
|
||||
appwrite-mariadb:
|
||||
@@ -1,3 +0,0 @@
|
||||
_APP_DB_ADAPTER=postgresql
|
||||
_APP_DB_HOST=postgresql
|
||||
_APP_DB_PORT=5432
|
||||
@@ -1,3 +0,0 @@
|
||||
services:
|
||||
mongodb:
|
||||
scale: 0
|
||||
+2
-2
@@ -72,8 +72,8 @@
|
||||
"utopia-php/image": "0.8.*",
|
||||
"utopia-php/locale": "0.8.*",
|
||||
"utopia-php/logger": "0.6.*",
|
||||
"utopia-php/messaging": "0.20.*",
|
||||
"utopia-php/migration": "1.8.*",
|
||||
"utopia-php/messaging": "0.22.*",
|
||||
"utopia-php/migration": "1.9.*",
|
||||
"utopia-php/platform": "0.7.*",
|
||||
"utopia-php/pools": "1.*",
|
||||
"utopia-php/span": "1.1.*",
|
||||
|
||||
Generated
+26
-26
@@ -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": "f9225f2b580de0ccb796b2fb8c881384",
|
||||
"content-hash": "4fe91e67f343fbe6deac1fdc7eda949f",
|
||||
"packages": [
|
||||
{
|
||||
"name": "adhocore/jwt",
|
||||
@@ -161,16 +161,16 @@
|
||||
},
|
||||
{
|
||||
"name": "appwrite/php-runtimes",
|
||||
"version": "0.19.4",
|
||||
"version": "0.19.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/appwrite/runtimes.git",
|
||||
"reference": "eea9d1b3ca2540eab623b419c8afde09ef406c0b"
|
||||
"reference": "aa2f7760cd0493c0880209b92df812c9386b3546"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/appwrite/runtimes/zipball/eea9d1b3ca2540eab623b419c8afde09ef406c0b",
|
||||
"reference": "eea9d1b3ca2540eab623b419c8afde09ef406c0b",
|
||||
"url": "https://api.github.com/repos/appwrite/runtimes/zipball/aa2f7760cd0493c0880209b92df812c9386b3546",
|
||||
"reference": "aa2f7760cd0493c0880209b92df812c9386b3546",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -210,9 +210,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/appwrite/runtimes/issues",
|
||||
"source": "https://github.com/appwrite/runtimes/tree/0.19.4"
|
||||
"source": "https://github.com/appwrite/runtimes/tree/0.19.5"
|
||||
},
|
||||
"time": "2026-02-17T10:04:39+00:00"
|
||||
"time": "2026-04-01T01:39:23+00:00"
|
||||
},
|
||||
{
|
||||
"name": "brick/math",
|
||||
@@ -3850,16 +3850,16 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/database",
|
||||
"version": "5.3.17",
|
||||
"version": "5.3.19",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/database.git",
|
||||
"reference": "cff2b6ed63d3291b74110d086e16ff089fe05993"
|
||||
"reference": "72ee1614c37e37c7fdd9d4dc87f1f7cdfa1ca691"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/database/zipball/cff2b6ed63d3291b74110d086e16ff089fe05993",
|
||||
"reference": "cff2b6ed63d3291b74110d086e16ff089fe05993",
|
||||
"url": "https://api.github.com/repos/utopia-php/database/zipball/72ee1614c37e37c7fdd9d4dc87f1f7cdfa1ca691",
|
||||
"reference": "72ee1614c37e37c7fdd9d4dc87f1f7cdfa1ca691",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3903,9 +3903,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/database/issues",
|
||||
"source": "https://github.com/utopia-php/database/tree/5.3.17"
|
||||
"source": "https://github.com/utopia-php/database/tree/5.3.19"
|
||||
},
|
||||
"time": "2026-03-20T01:18:52+00:00"
|
||||
"time": "2026-03-31T15:52:08+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/detector",
|
||||
@@ -4467,23 +4467,23 @@
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/messaging",
|
||||
"version": "0.20.1",
|
||||
"version": "0.22.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/messaging.git",
|
||||
"reference": "fcb4c3c46a48008a677957690bd45ec934dd33b0"
|
||||
"reference": "a6ac04fd204fb6a16bf8c75a84d0b9fc10aa5030"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/messaging/zipball/fcb4c3c46a48008a677957690bd45ec934dd33b0",
|
||||
"reference": "fcb4c3c46a48008a677957690bd45ec934dd33b0",
|
||||
"url": "https://api.github.com/repos/utopia-php/messaging/zipball/a6ac04fd204fb6a16bf8c75a84d0b9fc10aa5030",
|
||||
"reference": "a6ac04fd204fb6a16bf8c75a84d0b9fc10aa5030",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-curl": "*",
|
||||
"ext-openssl": "*",
|
||||
"giggsey/libphonenumber-for-php-lite": "9.0.23",
|
||||
"php": ">=8.0.0",
|
||||
"php": ">=8.1.0",
|
||||
"phpmailer/phpmailer": "6.9.1"
|
||||
},
|
||||
"require-dev": {
|
||||
@@ -4512,22 +4512,22 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/messaging/issues",
|
||||
"source": "https://github.com/utopia-php/messaging/tree/0.20.1"
|
||||
"source": "https://github.com/utopia-php/messaging/tree/0.22.0"
|
||||
},
|
||||
"time": "2026-02-06T09:56:06+00:00"
|
||||
"time": "2026-04-02T04:09:19+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/migration",
|
||||
"version": "1.8.3",
|
||||
"version": "1.9.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/utopia-php/migration.git",
|
||||
"reference": "8633523b3343d492427331b6eec53f020f6ab7a7"
|
||||
"reference": "7a86aeadf182b63a9f4ceba7e137588b31c5d2e2"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/utopia-php/migration/zipball/8633523b3343d492427331b6eec53f020f6ab7a7",
|
||||
"reference": "8633523b3343d492427331b6eec53f020f6ab7a7",
|
||||
"url": "https://api.github.com/repos/utopia-php/migration/zipball/7a86aeadf182b63a9f4ceba7e137588b31c5d2e2",
|
||||
"reference": "7a86aeadf182b63a9f4ceba7e137588b31c5d2e2",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4567,9 +4567,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/utopia-php/migration/issues",
|
||||
"source": "https://github.com/utopia-php/migration/tree/1.8.3"
|
||||
"source": "https://github.com/utopia-php/migration/tree/1.9.1"
|
||||
},
|
||||
"time": "2026-03-19T09:18:47+00:00"
|
||||
"time": "2026-03-25T07:05:27+00:00"
|
||||
},
|
||||
{
|
||||
"name": "utopia-php/mongo",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
services:
|
||||
appwrite-mongo-express:
|
||||
profiles: ["mongodb"]
|
||||
image: mongo-express
|
||||
container_name: appwrite-mongo-express
|
||||
networks:
|
||||
|
||||
+37
-13
@@ -1261,27 +1261,28 @@ services:
|
||||
retries: 20
|
||||
start_period: 5s
|
||||
|
||||
postgresql:
|
||||
image: appwrite/postgres:0.1.0
|
||||
container_name: appwrite-postgresql
|
||||
mariadb:
|
||||
image: mariadb:10.11 # fix issues when upgrading using: mysql_upgrade -u root -p
|
||||
container_name: appwrite-mariadb
|
||||
<<: *x-logging
|
||||
networks:
|
||||
- appwrite
|
||||
volumes:
|
||||
- appwrite-postgresql:/var/lib/postgresql/18/data:rw
|
||||
- appwrite-mariadb:/var/lib/mysql:rw
|
||||
ports:
|
||||
- "5432:5432"
|
||||
- "3306:3306"
|
||||
environment:
|
||||
- POSTGRES_DB=${_APP_DB_SCHEMA}
|
||||
- POSTGRES_USER=${_APP_DB_USER}
|
||||
- POSTGRES_PASSWORD=${_APP_DB_PASS}
|
||||
- MYSQL_ROOT_PASSWORD=${_APP_DB_ROOT_PASS}
|
||||
- MYSQL_DATABASE=${_APP_DB_SCHEMA}
|
||||
- MYSQL_USER=${_APP_DB_USER}
|
||||
- MYSQL_PASSWORD=${_APP_DB_PASS}
|
||||
- MARIADB_AUTO_UPGRADE=1
|
||||
command: "mysqld --innodb-flush-method=fsync"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${_APP_DB_USER} -d ${_APP_DB_SCHEMA}"]
|
||||
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 10s
|
||||
command: "postgres"
|
||||
retries: 12
|
||||
|
||||
mongodb:
|
||||
image: mongo:8.2.5
|
||||
@@ -1319,6 +1320,28 @@ services:
|
||||
retries: 10
|
||||
start_period: 30s
|
||||
|
||||
postgresql:
|
||||
image: appwrite/postgres:0.1.0
|
||||
container_name: appwrite-postgresql
|
||||
<<: *x-logging
|
||||
networks:
|
||||
- appwrite
|
||||
volumes:
|
||||
- appwrite-postgresql:/var/lib/postgresql/18/data:rw
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
- POSTGRES_DB=${_APP_DB_SCHEMA}
|
||||
- POSTGRES_USER=${_APP_DB_USER}
|
||||
- POSTGRES_PASSWORD=${_APP_DB_PASS}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${_APP_DB_USER} -d ${_APP_DB_SCHEMA}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 10s
|
||||
command: "postgres"
|
||||
|
||||
ollama:
|
||||
image: appwrite/ollama:0.1.1
|
||||
container_name: ollama
|
||||
@@ -1441,9 +1464,10 @@ networks:
|
||||
|
||||
|
||||
volumes:
|
||||
appwrite-postgresql:
|
||||
appwrite-mariadb:
|
||||
appwrite-mongodb:
|
||||
appwrite-mongodb-keyfile:
|
||||
appwrite-postgresql:
|
||||
appwrite-redis:
|
||||
appwrite-cache:
|
||||
appwrite-uploads:
|
||||
|
||||
@@ -1 +1 @@
|
||||
Get the number of audit logs that are waiting to be processed in the Appwrite internal queue server.
|
||||
Get the number of audit logs that are waiting to be processed in the Appwrite internal queue server.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+1
-4
@@ -1,8 +1,6 @@
|
||||
includes:
|
||||
- phpstan-baseline.neon
|
||||
|
||||
parameters:
|
||||
level: 3
|
||||
tmpDir: .phpstan-cache
|
||||
paths:
|
||||
- src
|
||||
- app
|
||||
@@ -14,4 +12,3 @@ parameters:
|
||||
- vendor/swoole/ide-helper
|
||||
excludePaths:
|
||||
- tests/resources
|
||||
|
||||
|
||||
+10
-105
@@ -16,123 +16,28 @@
|
||||
<testsuite name="unit">
|
||||
<directory>./tests/unit</directory>
|
||||
</testsuite>
|
||||
<testsuite name="General">
|
||||
<directory>./tests/e2e/General</directory>
|
||||
<directory>./tests/e2e/Scopes</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Console">
|
||||
<directory>./tests/e2e/Services/Console</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Health">
|
||||
<directory>./tests/e2e/Services/Health</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Locale">
|
||||
<directory>./tests/e2e/Services/Locale</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Tokens">
|
||||
<directory>./tests/e2e/Services/Tokens</directory>
|
||||
</testsuite>
|
||||
<testsuite name="VCS">
|
||||
<directory>./tests/e2e/Services/VCS</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Webhooks">
|
||||
<directory>./tests/e2e/Services/Webhooks</directory>
|
||||
</testsuite>
|
||||
<testsuite name="GeneralGroup">
|
||||
<directory>./tests/e2e/General</directory>
|
||||
<directory>./tests/e2e/Scopes</directory>
|
||||
<directory>./tests/e2e/Services/Console</directory>
|
||||
<directory>./tests/e2e/Services/Health</directory>
|
||||
<directory>./tests/e2e/Services/Locale</directory>
|
||||
<directory>./tests/e2e/Services/Messaging</directory>
|
||||
<directory>./tests/e2e/Services/Proxy</directory>
|
||||
<directory>./tests/e2e/Services/Storage</directory>
|
||||
<directory>./tests/e2e/Services/Tokens</directory>
|
||||
<directory>./tests/e2e/Services/VCS</directory>
|
||||
<directory>./tests/e2e/Services/Webhooks</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Account">
|
||||
<directory>./tests/e2e/Services/Account</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Avatars">
|
||||
<directory>./tests/e2e/Services/Avatars</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Databases">
|
||||
<directory>./tests/e2e/Services/Databases</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Functions">
|
||||
<file>./tests/e2e/Services/Functions/FunctionsBase.php</file>
|
||||
<file>./tests/e2e/Services/Functions/FunctionsCustomServerTest.php</file>
|
||||
<file>./tests/e2e/Services/Functions/FunctionsCustomClientTest.php</file>
|
||||
</testsuite>
|
||||
<testsuite name="FunctionsSchedule">
|
||||
<directory>./tests/e2e/Services/FunctionsSchedule</directory>
|
||||
</testsuite>
|
||||
<testsuite name="GraphQL">
|
||||
<directory>./tests/e2e/Services/GraphQL</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Messaging">
|
||||
<directory>./tests/e2e/Services/Messaging</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Migrations">
|
||||
<directory>./tests/e2e/Services/Migrations</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Project">
|
||||
<directory>./tests/e2e/Services/Project</directory>
|
||||
</testsuite>
|
||||
<testsuite name="ProjectWebhooks">
|
||||
<directory>./tests/e2e/Services/ProjectWebhooks</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Projects">
|
||||
<directory>./tests/e2e/Services/Projects</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Proxy">
|
||||
<directory>./tests/e2e/Services/Proxy</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Realtime">
|
||||
<directory>./tests/e2e/Services/Realtime</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Sites">
|
||||
<directory>./tests/e2e/Services/Sites</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Storage">
|
||||
<directory>./tests/e2e/Services/Storage</directory>
|
||||
</testsuite>
|
||||
<testsuite name="TablesDB">
|
||||
<directory>./tests/e2e/Services/TablesDB</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Teams">
|
||||
<directory>./tests/e2e/Services/Teams</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Users">
|
||||
<directory>./tests/e2e/Services/Users</directory>
|
||||
</testsuite>
|
||||
<testsuite name="e2e">
|
||||
<file>./tests/e2e/Client.php</file>
|
||||
<directory>./tests/e2e/General</directory>
|
||||
<directory>./tests/e2e/Scopes</directory>
|
||||
<directory>./tests/e2e/Services/Teams</directory>
|
||||
<directory>./tests/e2e/Services/Realtime</directory>
|
||||
<directory>./tests/e2e/Services/Account</directory>
|
||||
<directory>./tests/e2e/Services/Avatars</directory>
|
||||
<directory>./tests/e2e/Services/Users</directory>
|
||||
<directory>./tests/e2e/Services/Console</directory>
|
||||
<directory>./tests/e2e/Services/Avatars</directory>
|
||||
<directory>./tests/e2e/Services/Databases</directory>
|
||||
<directory>./tests/e2e/Services/FunctionsSchedule</directory>
|
||||
<directory>./tests/e2e/Services/GraphQL</directory>
|
||||
<directory>./tests/e2e/Services/Health</directory>
|
||||
<directory>./tests/e2e/Services/Locale</directory>
|
||||
<directory>./tests/e2e/Services/Projects</directory>
|
||||
<directory>./tests/e2e/Services/Storage</directory>
|
||||
<directory>./tests/e2e/Services/Tokens</directory>
|
||||
<directory>./tests/e2e/Services/Webhooks</directory>
|
||||
<directory>./tests/e2e/Services/ProjectWebhooks</directory>
|
||||
<directory>./tests/e2e/Services/Messaging</directory>
|
||||
<directory>./tests/e2e/Services/Migrations</directory>
|
||||
<directory>./tests/e2e/Services/Project</directory>
|
||||
<directory>./tests/e2e/Services/ProjectWebhooks</directory>
|
||||
<directory>./tests/e2e/Services/Projects</directory>
|
||||
<directory>./tests/e2e/Services/Proxy</directory>
|
||||
<directory>./tests/e2e/Services/Realtime</directory>
|
||||
<directory>./tests/e2e/Services/Sites</directory>
|
||||
<directory>./tests/e2e/Services/Storage</directory>
|
||||
<directory>./tests/e2e/Services/TablesDB</directory>
|
||||
<directory>./tests/e2e/Services/Teams</directory>
|
||||
<directory>./tests/e2e/Services/Tokens</directory>
|
||||
<directory>./tests/e2e/Services/Users</directory>
|
||||
<directory>./tests/e2e/Services/VCS</directory>
|
||||
<directory>./tests/e2e/Services/Webhooks</directory>
|
||||
<file>./tests/e2e/Services/Functions/FunctionsBase.php</file>
|
||||
<file>./tests/e2e/Services/Functions/FunctionsCustomServerTest.php</file>
|
||||
<file>./tests/e2e/Services/Functions/FunctionsCustomClientTest.php</file>
|
||||
|
||||
@@ -285,7 +285,7 @@ class Event
|
||||
*
|
||||
* @param string $key
|
||||
* @param Document $context
|
||||
* @return self
|
||||
* @return static
|
||||
*/
|
||||
public function setContext(string $key, Document $context): self
|
||||
{
|
||||
@@ -309,7 +309,7 @@ class Event
|
||||
/**
|
||||
* Set class used for this event.
|
||||
* @param string $class
|
||||
* @return self
|
||||
* @return static
|
||||
*/
|
||||
public function setClass(string $class): self
|
||||
{
|
||||
@@ -648,10 +648,8 @@ class Event
|
||||
*
|
||||
* @param Event $event
|
||||
*
|
||||
* @return self
|
||||
*
|
||||
*/
|
||||
public function from(Event $event): self
|
||||
public function from(Event $event): static
|
||||
{
|
||||
$this->project = $event->getProject();
|
||||
$this->user = $event->getUser();
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace Appwrite\Event\Message;
|
||||
|
||||
use Utopia\Database\Document;
|
||||
|
||||
final class Usage extends Base
|
||||
class Usage extends Base
|
||||
{
|
||||
/**
|
||||
* @param Document $project
|
||||
@@ -40,6 +40,7 @@ final class Usage extends Base
|
||||
*/
|
||||
public static function fromArray(array $data): static
|
||||
{
|
||||
/** @phpstan-ignore new.static (subclass constructors are backwards-compatible via optional params) */
|
||||
return new static(
|
||||
project: new Document($data['project'] ?? []),
|
||||
metrics: $data['metrics'] ?? [],
|
||||
|
||||
@@ -25,11 +25,7 @@ class Resolvers
|
||||
?Route $route,
|
||||
): callable {
|
||||
return static fn ($type, $args, $context, $info) => new Swoole(
|
||||
function (callable $resolve, callable $reject) use ($utopia, $route, $args, $context, $info) {
|
||||
/** @var Http $utopia */
|
||||
/** @var Response $response */
|
||||
/** @var Request $request */
|
||||
|
||||
function (callable $resolve, callable $reject) use ($utopia, $route, $args) {
|
||||
$utopia = $utopia->getResource('utopia:graphql', true);
|
||||
$request = $utopia->getResource('request', true);
|
||||
$response = $utopia->getResource('response', true);
|
||||
@@ -96,7 +92,7 @@ class Resolvers
|
||||
callable $url,
|
||||
): callable {
|
||||
return static fn ($type, $args, $context, $info) => new Swoole(
|
||||
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $type, $args) {
|
||||
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $args) {
|
||||
$utopia = $utopia->getResource('utopia:graphql', true);
|
||||
$request = $utopia->getResource('request', true);
|
||||
$response = $utopia->getResource('response', true);
|
||||
@@ -127,7 +123,7 @@ 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, $type, $args) {
|
||||
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
|
||||
$utopia = $utopia->getResource('utopia:graphql', true);
|
||||
$request = $utopia->getResource('request', true);
|
||||
$response = $utopia->getResource('response', true);
|
||||
@@ -163,7 +159,7 @@ 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, $type, $args) {
|
||||
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
|
||||
$utopia = $utopia->getResource('utopia:graphql', true);
|
||||
$request = $utopia->getResource('request', true);
|
||||
$response = $utopia->getResource('response', true);
|
||||
@@ -195,7 +191,7 @@ 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, $type, $args) {
|
||||
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $params, $args) {
|
||||
$utopia = $utopia->getResource('utopia:graphql', true);
|
||||
$request = $utopia->getResource('request', true);
|
||||
$response = $utopia->getResource('response', true);
|
||||
@@ -225,7 +221,7 @@ class Resolvers
|
||||
callable $url,
|
||||
): callable {
|
||||
return static fn ($type, $args, $context, $info) => new Swoole(
|
||||
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $type, $args) {
|
||||
function (callable $resolve, callable $reject) use ($utopia, $databaseId, $collectionId, $url, $args) {
|
||||
$utopia = $utopia->getResource('utopia:graphql', true);
|
||||
$request = $utopia->getResource('request', true);
|
||||
$response = $utopia->getResource('response', true);
|
||||
|
||||
@@ -98,10 +98,9 @@ class Schema
|
||||
foreach ($routes as $route) {
|
||||
/** @var Route $route */
|
||||
|
||||
/** @var \Appwrite\SDK\Method $sdk */
|
||||
$sdk = $route->getLabel('sdk', false);
|
||||
|
||||
if (empty($sdk)) {
|
||||
if ($sdk === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -177,7 +176,7 @@ class Schema
|
||||
$required = $attr['required'];
|
||||
$default = $attr['default'];
|
||||
$escapedKey = str_replace('$', '', $key);
|
||||
$collections[$collectionId][$escapedKey] = [
|
||||
$collections[$databaseId][$collectionId][$escapedKey] = [
|
||||
'type' => Mapper::attribute(
|
||||
$type,
|
||||
$array,
|
||||
@@ -187,80 +186,82 @@ class Schema
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($collections as $collectionId => $attributes) {
|
||||
$objectType = new ObjectType([
|
||||
'name' => $collectionId,
|
||||
'fields' => \array_merge(
|
||||
["_id" => ['type' => Type::string()]],
|
||||
$attributes
|
||||
),
|
||||
]);
|
||||
$attributes = \array_merge(
|
||||
$attributes,
|
||||
Mapper::args('mutate')
|
||||
);
|
||||
|
||||
$queryFields[$collectionId . 'Get'] = [
|
||||
'type' => $objectType,
|
||||
'args' => Mapper::args('id'),
|
||||
'resolve' => Resolvers::documentGet(
|
||||
$utopia,
|
||||
$databaseId,
|
||||
$collectionId,
|
||||
$urls['get'],
|
||||
)
|
||||
];
|
||||
$queryFields[$collectionId . 'List'] = [
|
||||
'type' => Type::listOf($objectType),
|
||||
'args' => Mapper::args('list'),
|
||||
'resolve' => Resolvers::documentList(
|
||||
$utopia,
|
||||
$databaseId,
|
||||
$collectionId,
|
||||
$urls['list'],
|
||||
$params['list'],
|
||||
),
|
||||
'complexity' => $complexity,
|
||||
];
|
||||
|
||||
$mutationFields[$collectionId . 'Create'] = [
|
||||
'type' => $objectType,
|
||||
'args' => $attributes,
|
||||
'resolve' => Resolvers::documentCreate(
|
||||
$utopia,
|
||||
$databaseId,
|
||||
$collectionId,
|
||||
$urls['create'],
|
||||
$params['create'],
|
||||
)
|
||||
];
|
||||
$mutationFields[$collectionId . 'Update'] = [
|
||||
'type' => $objectType,
|
||||
'args' => \array_merge(
|
||||
Mapper::args('id'),
|
||||
\array_map(
|
||||
fn ($attr) => $attr['type'] = Type::getNullableType($attr['type']),
|
||||
foreach ($collections as $databaseId => $databaseCollections) {
|
||||
foreach ($databaseCollections as $collectionId => $attributes) {
|
||||
$objectType = new ObjectType([
|
||||
'name' => $collectionId,
|
||||
'fields' => \array_merge(
|
||||
["_id" => ['type' => Type::string()]],
|
||||
$attributes
|
||||
),
|
||||
]);
|
||||
$attributes = \array_merge(
|
||||
$attributes,
|
||||
Mapper::args('mutate')
|
||||
);
|
||||
|
||||
$queryFields[$collectionId . 'Get'] = [
|
||||
'type' => $objectType,
|
||||
'args' => Mapper::args('id'),
|
||||
'resolve' => Resolvers::documentGet(
|
||||
$utopia,
|
||||
$databaseId,
|
||||
$collectionId,
|
||||
$urls['get'],
|
||||
)
|
||||
),
|
||||
'resolve' => Resolvers::documentUpdate(
|
||||
$utopia,
|
||||
$databaseId,
|
||||
$collectionId,
|
||||
$urls['update'],
|
||||
$params['update'],
|
||||
)
|
||||
];
|
||||
$mutationFields[$collectionId . 'Delete'] = [
|
||||
'type' => Mapper::model('none'),
|
||||
'args' => Mapper::args('id'),
|
||||
'resolve' => Resolvers::documentDelete(
|
||||
$utopia,
|
||||
$databaseId,
|
||||
$collectionId,
|
||||
$urls['delete'],
|
||||
)
|
||||
];
|
||||
];
|
||||
$queryFields[$collectionId . 'List'] = [
|
||||
'type' => Type::listOf($objectType),
|
||||
'args' => Mapper::args('list'),
|
||||
'resolve' => Resolvers::documentList(
|
||||
$utopia,
|
||||
$databaseId,
|
||||
$collectionId,
|
||||
$urls['list'],
|
||||
$params['list'],
|
||||
),
|
||||
'complexity' => $complexity,
|
||||
];
|
||||
|
||||
$mutationFields[$collectionId . 'Create'] = [
|
||||
'type' => $objectType,
|
||||
'args' => $attributes,
|
||||
'resolve' => Resolvers::documentCreate(
|
||||
$utopia,
|
||||
$databaseId,
|
||||
$collectionId,
|
||||
$urls['create'],
|
||||
$params['create'],
|
||||
)
|
||||
];
|
||||
$mutationFields[$collectionId . 'Update'] = [
|
||||
'type' => $objectType,
|
||||
'args' => \array_merge(
|
||||
Mapper::args('id'),
|
||||
\array_map(
|
||||
fn ($attr) => $attr['type'] = Type::getNullableType($attr['type']),
|
||||
$attributes
|
||||
)
|
||||
),
|
||||
'resolve' => Resolvers::documentUpdate(
|
||||
$utopia,
|
||||
$databaseId,
|
||||
$collectionId,
|
||||
$urls['update'],
|
||||
$params['update'],
|
||||
)
|
||||
];
|
||||
$mutationFields[$collectionId . 'Delete'] = [
|
||||
'type' => Mapper::model('none'),
|
||||
'args' => Mapper::args('id'),
|
||||
'resolve' => Resolvers::documentDelete(
|
||||
$utopia,
|
||||
$databaseId,
|
||||
$collectionId,
|
||||
$urls['delete'],
|
||||
)
|
||||
];
|
||||
}
|
||||
}
|
||||
$offset += $limit;
|
||||
}
|
||||
|
||||
@@ -15,10 +15,13 @@ class Types
|
||||
*
|
||||
* @return Json
|
||||
*/
|
||||
public static function json(): Type
|
||||
public static function json(): Json
|
||||
{
|
||||
if (Registry::has(Json::class)) {
|
||||
return Registry::get(Json::class);
|
||||
$type = Registry::get(Json::class);
|
||||
if ($type instanceof Json) {
|
||||
return $type;
|
||||
}
|
||||
}
|
||||
$type = new Json();
|
||||
Registry::set(Json::class, $type);
|
||||
@@ -28,12 +31,15 @@ class Types
|
||||
/**
|
||||
* Get the JSON type.
|
||||
*
|
||||
* @return Json
|
||||
* @return Assoc
|
||||
*/
|
||||
public static function assoc(): Type
|
||||
public static function assoc(): Assoc
|
||||
{
|
||||
if (Registry::has(Assoc::class)) {
|
||||
return Registry::get(Assoc::class);
|
||||
$type = Registry::get(Assoc::class);
|
||||
if ($type instanceof Assoc) {
|
||||
return $type;
|
||||
}
|
||||
}
|
||||
$type = new Assoc();
|
||||
Registry::set(Assoc::class, $type);
|
||||
@@ -45,10 +51,13 @@ class Types
|
||||
*
|
||||
* @return InputFile
|
||||
*/
|
||||
public static function inputFile(): Type
|
||||
public static function inputFile(): InputFile
|
||||
{
|
||||
if (Registry::has(InputFile::class)) {
|
||||
return Registry::get(InputFile::class);
|
||||
$type = Registry::get(InputFile::class);
|
||||
if ($type instanceof InputFile) {
|
||||
return $type;
|
||||
}
|
||||
}
|
||||
$type = new InputFile();
|
||||
Registry::set(InputFile::class, $type);
|
||||
|
||||
@@ -273,11 +273,9 @@ class Mapper
|
||||
case \Appwrite\Auth\Validator\Password::class:
|
||||
case \Appwrite\Event\Validator\Event::class:
|
||||
case \Appwrite\Event\Validator\FunctionEvent::class:
|
||||
case \Appwrite\Network\Validator\CNAME::class:
|
||||
case \Utopia\Emails\Validator\Email::class:
|
||||
case \Appwrite\Network\Validator\Redirect::class:
|
||||
case \Appwrite\Network\Validator\DNS::class:
|
||||
case \Appwrite\Network\Validator\Origin::class:
|
||||
case \Appwrite\Task\Validator\Cron::class:
|
||||
case \Appwrite\Utopia\Database\Validator\CustomId::class:
|
||||
case \Utopia\Database\Validator\Key::class:
|
||||
@@ -286,7 +284,7 @@ class Mapper
|
||||
case \Utopia\Validator\HexColor::class:
|
||||
case \Utopia\Validator\Host::class:
|
||||
case \Utopia\Validator\IP::class:
|
||||
case \Utopia\Validator\Origin::class:
|
||||
case \Appwrite\Network\Validator\Origin::class:
|
||||
case \Utopia\Validator\Text::class:
|
||||
case \Utopia\Validator\URL::class:
|
||||
case \Utopia\Validator\WhiteList::class:
|
||||
|
||||
@@ -13,6 +13,7 @@ use Utopia\Database\Exception\Limit;
|
||||
use Utopia\Database\Exception\Structure;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\PDO;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
|
||||
abstract class Migration
|
||||
@@ -204,6 +205,30 @@ abstract class Migration
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<Query> $queries
|
||||
* @return \Generator<int, Document>
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function documentsIterator(string $collection, array $queries = []): \Generator
|
||||
{
|
||||
$offset = 0;
|
||||
|
||||
do {
|
||||
$documents = $this->dbForProject->find($collection, [
|
||||
...$queries,
|
||||
Query::limit($this->limit),
|
||||
Query::offset($offset),
|
||||
]);
|
||||
|
||||
foreach ($documents as $document) {
|
||||
yield $document;
|
||||
}
|
||||
|
||||
$offset += \count($documents);
|
||||
} while (\count($documents) === $this->limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates collection from the config collection.
|
||||
*
|
||||
|
||||
@@ -1224,7 +1224,7 @@ class V15 extends Migration
|
||||
* @param \Utopia\Database\Document $document
|
||||
* @return \Utopia\Database\Document
|
||||
*/
|
||||
protected function fixDocument(Document $document)
|
||||
protected function fixDocument(Document $document): Document
|
||||
{
|
||||
switch ($document->getCollection()) {
|
||||
case 'cache':
|
||||
@@ -1234,7 +1234,7 @@ class V15 extends Migration
|
||||
* skipping migration for 'cache' and 'variables'.
|
||||
* 'users' already migrated.
|
||||
*/
|
||||
return;
|
||||
return $document;
|
||||
|
||||
case '_metadata':
|
||||
/**
|
||||
@@ -1480,7 +1480,6 @@ class V15 extends Migration
|
||||
* Filter from the 'encrypt' filter.
|
||||
*
|
||||
* @param string $value
|
||||
* @return string|false
|
||||
*/
|
||||
protected function encryptFilter(string $value): string
|
||||
{
|
||||
@@ -1492,8 +1491,8 @@ class V15 extends Migration
|
||||
'data' => OpenSSL::encrypt($value, OpenSSL::CIPHER_AES_128_GCM, $key, 0, $iv, $tag),
|
||||
'method' => OpenSSL::CIPHER_AES_128_GCM,
|
||||
'iv' => \bin2hex($iv),
|
||||
'tag' => \bin2hex($tag ?? ''),
|
||||
'tag' => \bin2hex($tag),
|
||||
'version' => '1',
|
||||
]);
|
||||
]) ?: '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -452,7 +452,7 @@ class V20 extends Migration
|
||||
Query::equal('period', ['1d']),
|
||||
]);
|
||||
|
||||
$value = $query ?? 0;
|
||||
$value = $query;
|
||||
$this->createInfMetric($to, $value);
|
||||
}
|
||||
|
||||
|
||||
@@ -187,6 +187,20 @@ class V24 extends Migration
|
||||
$this->dbForProject->purgeCachedCollection($id);
|
||||
break;
|
||||
|
||||
case 'users':
|
||||
try {
|
||||
$this->createAttributeFromCollection($this->dbForProject, $id, 'impersonator');
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("Failed to create attribute \"impersonator\" in collection {$id}: {$th->getMessage()}");
|
||||
}
|
||||
try {
|
||||
$this->createIndexFromCollection($this->dbForProject, $id, 'impersonator');
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("Failed to create index \"impersonator\" from {$id}: {$th->getMessage()}");
|
||||
}
|
||||
$this->dbForProject->purgeCachedCollection($id);
|
||||
break;
|
||||
|
||||
case 'teams':
|
||||
try {
|
||||
$this->createAttributeFromCollection($this->dbForProject, $id, 'labels');
|
||||
|
||||
@@ -48,7 +48,7 @@ final class Cors
|
||||
/**
|
||||
* Build CORS headers for a given request origin.
|
||||
*
|
||||
* @return array<string,string>
|
||||
* @return array<string, int|string>
|
||||
*/
|
||||
public function headers(string $origin): array
|
||||
{
|
||||
|
||||
@@ -18,7 +18,7 @@ class OpenSSL
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function encrypt($data, $method, $key, $options = 0, $iv = '', &$tag = null, $aad = '', $tag_length = 16)
|
||||
public static function encrypt($data, $method, $key, $options = 0, $iv = '', ?string &$tag = null, $aad = '', $tag_length = 16)
|
||||
{
|
||||
return \openssl_encrypt($data, $method, $key, $options, $iv, $tag, $aad, $tag_length);
|
||||
}
|
||||
|
||||
@@ -49,7 +49,6 @@ class Action extends PlatformAction
|
||||
|
||||
$image = new Image(\file_get_contents($path));
|
||||
$image->crop((int) $width, (int) $height);
|
||||
$output = (empty($output)) ? $type : $output;
|
||||
$data = $image->output($output, $quality);
|
||||
$response
|
||||
->addHeader('Cache-Control', 'private, max-age=2592000') // 30 days
|
||||
|
||||
@@ -204,7 +204,6 @@ class Get extends Action
|
||||
|
||||
$image = new Image($data);
|
||||
$image->crop((int) $width, (int) $height);
|
||||
$output = (empty($output)) ? $type : $output;
|
||||
$data = $image->output($output, $quality);
|
||||
|
||||
$response
|
||||
|
||||
@@ -95,7 +95,6 @@ class Get extends Action
|
||||
}
|
||||
|
||||
$image->crop((int) $width, (int) $height);
|
||||
$output = (empty($output)) ? $type : $output;
|
||||
$data = $image->output($output, $quality);
|
||||
|
||||
$response
|
||||
|
||||
@@ -90,7 +90,7 @@ class Get extends Action
|
||||
}
|
||||
}
|
||||
|
||||
$rand = \substr($code, -1);
|
||||
$rand = (int) \substr((string) $code, -1);
|
||||
|
||||
$rand = ($rand > \count($themes) - 1) ? $rand % \count($themes) : $rand;
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ class Action extends AppwriteAction
|
||||
return $this->context;
|
||||
}
|
||||
|
||||
public function setHttpPath(string $path): AppwriteAction
|
||||
public function setHttpPath(string $path): self
|
||||
{
|
||||
if (\str_contains($path, '/tablesdb')) {
|
||||
$this->context = DATABASE_TYPE_TABLESDB;
|
||||
@@ -28,7 +28,8 @@ class Action extends AppwriteAction
|
||||
if (\str_contains($path, '/vectorsdb')) {
|
||||
$this->context = DATABASE_TYPE_VECTORSDB;
|
||||
}
|
||||
return parent::setHttpPath($path);
|
||||
parent::setHttpPath($path);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+3
-4
@@ -24,7 +24,7 @@ abstract class Action extends DatabasesAction
|
||||
*/
|
||||
abstract protected function getResponseModel(): string;
|
||||
|
||||
public function setHttpPath(string $path): DatabasesAction
|
||||
public function setHttpPath(string $path): self
|
||||
{
|
||||
if (str_contains($path, '/tablesdb/')) {
|
||||
$this->context = ROWS;
|
||||
@@ -47,7 +47,8 @@ abstract class Action extends DatabasesAction
|
||||
],
|
||||
];
|
||||
|
||||
return parent::setHttpPath($path);
|
||||
parent::setHttpPath($path);
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function getDatabasesOperationReadMetric(): string
|
||||
@@ -406,8 +407,6 @@ abstract class Action extends DatabasesAction
|
||||
|
||||
if (\is_array($related)) {
|
||||
$document->setAttribute($relationship->getAttribute('key'), \array_values($relations));
|
||||
} elseif (empty($relations)) {
|
||||
$document->setAttribute($relationship->getAttribute('key'), null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -209,7 +209,7 @@ class Create extends Action
|
||||
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Bulk create is not supported for ' . $this->getSDKNamespace() . ' with relationship ' . $this->getStructureContext());
|
||||
}
|
||||
|
||||
$setPermissions = function (Document $document, ?array $permissions) use ($user, $isAPIKey, $isPrivilegedUser, $isBulk, $dbForProject, $authorization) {
|
||||
$setPermissions = function (Document $document, ?array $permissions) use ($user, $isAPIKey, $isPrivilegedUser, $isBulk, $authorization) {
|
||||
$allowedPermissions = [
|
||||
Database::PERMISSION_READ,
|
||||
Database::PERMISSION_UPDATE,
|
||||
|
||||
-1
@@ -122,7 +122,6 @@ class Update extends Action
|
||||
|
||||
$dbForDatabases = $getDatabasesDB($database);
|
||||
// Read permission should not be required for update
|
||||
/** @var Document $document */
|
||||
$collectionTableId = 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence();
|
||||
|
||||
if ($transactionId !== null) {
|
||||
|
||||
+1
-1
@@ -147,7 +147,7 @@ class XList extends Action
|
||||
$cacheKeyBase = \sprintf(
|
||||
'%s-cache-%s:%s:%s:collection:%s:%s:user:%s:%s',
|
||||
$dbForProject->getCacheName(),
|
||||
$hostname ?? '',
|
||||
$hostname,
|
||||
$dbForProject->getNamespace(),
|
||||
$dbForProject->getTenant(),
|
||||
$collectionId,
|
||||
|
||||
@@ -99,8 +99,6 @@ class Update extends Action
|
||||
// Map aggregate permissions into the multiple permissions they represent.
|
||||
$permissions = Permission::aggregate($permissions);
|
||||
|
||||
$enabled ??= $collection->getAttribute('enabled', true);
|
||||
|
||||
$collection = $dbForProject->updateDocument(
|
||||
'database_' . $database->getSequence(),
|
||||
$collectionId,
|
||||
|
||||
@@ -61,16 +61,16 @@ class Create extends Action
|
||||
$databaseKeys = System::getEnv('_APP_DATABASE_DOCUMENTSDB_KEYS', '');
|
||||
$databaseOverride = System::getEnv('_APP_DATABASE_DOCUMENTSDB_OVERRIDE');
|
||||
$dbScheme = System::getEnv('_APP_DB_HOST_DOCUMENTSDB', 'mongodb');
|
||||
$databaseSharedTables = \explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES', ''));
|
||||
$databaseSharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1', ''));
|
||||
$databaseSharedTables = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES', '')));
|
||||
$databaseSharedTablesV1 = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1', '')));
|
||||
break;
|
||||
case VECTORSDB:
|
||||
$databases = Config::getParam('pools-vectorsdb', []);
|
||||
$databaseKeys = System::getEnv('_APP_DATABASE_VECTORSDB_KEYS', '');
|
||||
$databaseOverride = System::getEnv('_APP_DATABASE_VECTORSDB_OVERRIDE');
|
||||
$dbScheme = System::getEnv('_APP_DB_HOST_VECTORSDB', 'postgresql');
|
||||
$databaseSharedTables = \explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES', ''));
|
||||
$databaseSharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1', ''));
|
||||
$databaseSharedTables = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES', '')));
|
||||
$databaseSharedTablesV1 = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1', '')));
|
||||
break;
|
||||
default:
|
||||
// legacy/tablesdb
|
||||
@@ -108,7 +108,7 @@ class Create extends Action
|
||||
if ($index !== false) {
|
||||
$selectedDsn = $databases[$index];
|
||||
} else {
|
||||
if (!empty($dsn)) {
|
||||
if (!empty($dsn) && !empty($databaseSharedTables)) {
|
||||
$beforeFilter = \array_values($databases);
|
||||
if ($isSharedTablesV1) {
|
||||
$databases = array_filter($databases, fn ($value) => \in_array($value, $databaseSharedTablesV1));
|
||||
@@ -118,7 +118,10 @@ class Create extends Action
|
||||
$databases = array_filter($databases, fn ($value) => !\in_array($value, $databaseSharedTables));
|
||||
}
|
||||
}
|
||||
$selectedDsn = !empty($databases) ? $databases[array_rand($databases)] : '';
|
||||
if (empty($databases)) {
|
||||
throw new Exception(Exception::GENERAL_SERVER_ERROR, "No {$databasetype} database pool available for the current shared-tables mode");
|
||||
}
|
||||
$selectedDsn = $databases[array_rand($databases)];
|
||||
}
|
||||
|
||||
if (\in_array($selectedDsn, $databaseSharedTables)) {
|
||||
|
||||
@@ -103,6 +103,9 @@ class XList extends Action
|
||||
$os = $detector->getOS();
|
||||
$client = $detector->getClient();
|
||||
$device = $detector->getDevice();
|
||||
$deviceName = \is_array($device) ? ($device['deviceName'] ?? '') : '';
|
||||
$deviceBrand = \is_array($device) ? ($device['deviceBrand'] ?? '') : '';
|
||||
$deviceModel = \is_array($device) ? ($device['deviceModel'] ?? '') : '';
|
||||
|
||||
$output[$i] = new Document([
|
||||
'event' => $log['event'],
|
||||
@@ -121,9 +124,9 @@ class XList extends Action
|
||||
'clientVersion' => $client['clientVersion'],
|
||||
'clientEngine' => $client['clientEngine'],
|
||||
'clientEngineVersion' => $client['clientEngineVersion'],
|
||||
'deviceName' => $device['deviceName'],
|
||||
'deviceBrand' => $device['deviceBrand'],
|
||||
'deviceModel' => $device['deviceModel'],
|
||||
'deviceName' => $deviceName,
|
||||
'deviceBrand' => $deviceBrand,
|
||||
'deviceModel' => $deviceModel,
|
||||
]);
|
||||
|
||||
$record = $geodb->get($log['ip']);
|
||||
|
||||
@@ -33,7 +33,7 @@ abstract class Action extends DatabasesAction
|
||||
return $this->databaseType.'.'.METRIC_DATABASE_ID_OPERATIONS_WRITES;
|
||||
}
|
||||
|
||||
public function setHttpPath(string $path): DatabasesAction
|
||||
public function setHttpPath(string $path): self
|
||||
{
|
||||
switch (true) {
|
||||
case str_contains($path, '/tablesdb'):
|
||||
@@ -50,7 +50,8 @@ abstract class Action extends DatabasesAction
|
||||
$this->databaseType = VECTORSDB;
|
||||
break;
|
||||
}
|
||||
return parent::setHttpPath($path);
|
||||
parent::setHttpPath($path);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -239,7 +239,7 @@ class Create extends Action
|
||||
}
|
||||
}
|
||||
|
||||
$transaction = $authorization->skip(fn () => $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged, $existing, $operations) {
|
||||
$transaction = $authorization->skip(fn () => $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged, $operations) {
|
||||
$dbForProject->createDocuments('transactionLogs', $staged);
|
||||
return $dbForProject->increaseDocumentAttribute(
|
||||
'transactions',
|
||||
|
||||
@@ -105,8 +105,7 @@ class Update extends Action
|
||||
* @throws Exception
|
||||
* @throws \Throwable
|
||||
* @throws \Utopia\Database\Exception
|
||||
* @throws Authorization
|
||||
* @throws Structure
|
||||
* @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
|
||||
@@ -183,7 +182,7 @@ class Update extends Action
|
||||
$dbForDatabases = $getDatabasesDB($databaseDoc);
|
||||
|
||||
try {
|
||||
$dbForDatabases->withTransaction(function () use ($dbForDatabases, $dbForProject, $transactionState, $queueForDeletes, $transactionId, &$transaction, &$operations, &$totalOperations, &$databaseOperations, &$currentDocumentId, $queueForEvents, $usage, $queueForRealtime, $queueForFunctions, $queueForWebhooks, $authorization) {
|
||||
$dbForDatabases->withTransaction(function () use ($dbForDatabases, $dbForProject, $transactionState, $queueForDeletes, $transactionId, &$transaction, &$operations, &$totalOperations, &$databaseOperations, &$currentDocumentId, $authorization) {
|
||||
$authorization->skip(fn () => $dbForProject->updateDocument('transactions', $transactionId, new Document([
|
||||
'status' => 'committing',
|
||||
])));
|
||||
|
||||
@@ -97,6 +97,9 @@ class XList extends Action
|
||||
$os = $detector->getOS();
|
||||
$client = $detector->getClient();
|
||||
$device = $detector->getDevice();
|
||||
$deviceName = \is_array($device) ? ($device['deviceName'] ?? '') : '';
|
||||
$deviceBrand = \is_array($device) ? ($device['deviceBrand'] ?? '') : '';
|
||||
$deviceModel = \is_array($device) ? ($device['deviceModel'] ?? '') : '';
|
||||
|
||||
$output[$i] = new Document([
|
||||
'event' => $log['event'],
|
||||
@@ -115,9 +118,9 @@ class XList extends Action
|
||||
'clientVersion' => $client['clientVersion'],
|
||||
'clientEngine' => $client['clientEngine'],
|
||||
'clientEngineVersion' => $client['clientEngineVersion'],
|
||||
'deviceName' => $device['deviceName'],
|
||||
'deviceBrand' => $device['deviceBrand'],
|
||||
'deviceModel' => $device['deviceModel'],
|
||||
'deviceName' => $deviceName,
|
||||
'deviceBrand' => $deviceBrand,
|
||||
'deviceModel' => $deviceModel,
|
||||
]);
|
||||
|
||||
$record = $geodb->get($log['ip']);
|
||||
|
||||
@@ -130,9 +130,11 @@ class Create extends CollectionAction
|
||||
$indexes[] = new Document($index);
|
||||
}
|
||||
try {
|
||||
// passing null in creates only creates the metadata collection
|
||||
if (!$dbForDatabases->exists(null, Database::METADATA)) {
|
||||
$dbForDatabases->create();
|
||||
try {
|
||||
$dbForDatabases->create();
|
||||
} catch (DuplicateException) {
|
||||
}
|
||||
}
|
||||
$dbForDatabases->createCollection(
|
||||
id: 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
|
||||
|
||||
@@ -88,16 +88,11 @@ class Get extends Action
|
||||
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
|
||||
}
|
||||
|
||||
switch ($type) {
|
||||
case 'output':
|
||||
$path = $deployment->getAttribute('buildPath', '');
|
||||
$device = $deviceForBuilds;
|
||||
break;
|
||||
case 'source':
|
||||
$path = $deployment->getAttribute('sourcePath', '');
|
||||
$device = $deviceForFunctions;
|
||||
break;
|
||||
}
|
||||
[$path, $device] = match ($type) {
|
||||
'output' => [$deployment->getAttribute('buildPath', ''), $deviceForBuilds],
|
||||
'source' => [$deployment->getAttribute('sourcePath', ''), $deviceForFunctions],
|
||||
default => throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Invalid deployment download type.'),
|
||||
};
|
||||
|
||||
if (!$device->exists($path)) {
|
||||
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
|
||||
|
||||
@@ -213,7 +213,10 @@ class Create extends Base
|
||||
$current = new Document();
|
||||
|
||||
foreach ($sessions as $session) {
|
||||
/** @var Utopia\Database\Document $session */
|
||||
if (!$session instanceof Document) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($proofForToken->verify($store->getProperty('secret', ''), $session->getAttribute('secret'))) { // Find most recent active session for user ID and JWT headers
|
||||
$current = $session;
|
||||
}
|
||||
@@ -237,11 +240,11 @@ class Create extends Base
|
||||
]);
|
||||
|
||||
$executionId = ID::unique();
|
||||
$headers['x-appwrite-execution-id'] = $executionId ?? '';
|
||||
$headers['x-appwrite-execution-id'] = $executionId;
|
||||
$headers['x-appwrite-key'] = API_KEY_DYNAMIC . '_' . $apiKey;
|
||||
$headers['x-appwrite-trigger'] = 'http';
|
||||
$headers['x-appwrite-user-id'] = $user->getId() ?? '';
|
||||
$headers['x-appwrite-user-jwt'] = $jwt ?? '';
|
||||
$headers['x-appwrite-user-id'] = $user->getId();
|
||||
$headers['x-appwrite-user-jwt'] = $jwt;
|
||||
$headers['x-appwrite-country-code'] = '';
|
||||
$headers['x-appwrite-continent-code'] = '';
|
||||
$headers['x-appwrite-continent-eu'] = 'false';
|
||||
@@ -350,16 +353,18 @@ class Create extends Base
|
||||
}
|
||||
}
|
||||
|
||||
$this->enqueueDeletes(
|
||||
$project,
|
||||
$function->getSequence(),
|
||||
$executionsRetentionCount,
|
||||
if ($executionsRetentionCount > 0 && ENABLE_EXECUTIONS_LIMIT_ON_ROUTE) {
|
||||
$queueForDeletes
|
||||
);
|
||||
->setProject($project)
|
||||
->setResource($function->getSequence())
|
||||
->setResourceType(RESOURCE_TYPE_FUNCTIONS)
|
||||
->setType(DELETE_TYPE_EXECUTIONS_LIMIT)
|
||||
->trigger();
|
||||
}
|
||||
|
||||
return $response
|
||||
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
|
||||
->dynamic($execution, Response::MODEL_EXECUTION);
|
||||
$response->setStatusCode(Response::STATUS_CODE_ACCEPTED);
|
||||
$response->dynamic($execution, Response::MODEL_EXECUTION);
|
||||
return;
|
||||
}
|
||||
|
||||
$durationStart = \microtime(true);
|
||||
@@ -370,7 +375,7 @@ class Create extends Base
|
||||
if ($version === 'v2') {
|
||||
$vars = \array_merge($vars, [
|
||||
'APPWRITE_FUNCTION_TRIGGER' => $headers['x-appwrite-trigger'] ?? '',
|
||||
'APPWRITE_FUNCTION_DATA' => $body ?? '',
|
||||
'APPWRITE_FUNCTION_DATA' => $body,
|
||||
'APPWRITE_FUNCTION_USER_ID' => $headers['x-appwrite-user-id'] ?? '',
|
||||
'APPWRITE_FUNCTION_JWT' => $headers['x-appwrite-user-jwt'] ?? ''
|
||||
]);
|
||||
@@ -537,32 +542,18 @@ class Create extends Base
|
||||
}
|
||||
}
|
||||
|
||||
$this->enqueueDeletes(
|
||||
$project,
|
||||
$function->getSequence(),
|
||||
$executionsRetentionCount,
|
||||
if ($executionsRetentionCount > 0 && ENABLE_EXECUTIONS_LIMIT_ON_ROUTE) {
|
||||
$queueForDeletes
|
||||
);
|
||||
->setProject($project)
|
||||
->setResource($function->getSequence())
|
||||
->setResourceType(RESOURCE_TYPE_FUNCTIONS)
|
||||
->setType(DELETE_TYPE_EXECUTIONS_LIMIT)
|
||||
->trigger();
|
||||
}
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($execution, Response::MODEL_EXECUTION);
|
||||
}
|
||||
|
||||
private function enqueueDeletes(
|
||||
Document $project,
|
||||
string $resourceId,
|
||||
int $executionsRetentionCount,
|
||||
DeleteEvent $queueForDeletes
|
||||
): void {
|
||||
/* cleanup */
|
||||
if ($executionsRetentionCount > 0 && ENABLE_EXECUTIONS_LIMIT_ON_ROUTE) {
|
||||
$queueForDeletes
|
||||
->setProject($project)
|
||||
->setResource($resourceId)
|
||||
->setResourceType(RESOURCE_TYPE_FUNCTIONS)
|
||||
->setType(DELETE_TYPE_EXECUTIONS_LIMIT)
|
||||
->trigger();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,8 +424,8 @@ class Create extends Base
|
||||
|
||||
/** Trigger Realtime Events */
|
||||
$queueForRealtime
|
||||
->from($ruleCreate)
|
||||
->setSubscribers(['console', $project->getId()])
|
||||
->from($ruleCreate)
|
||||
->trigger();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,8 +170,6 @@ class Update extends Base
|
||||
$runtime = $function->getAttribute('runtime');
|
||||
}
|
||||
|
||||
$enabled ??= $function->getAttribute('enabled', true);
|
||||
|
||||
$repositoryId = $function->getAttribute('repositoryId', '');
|
||||
$repositoryInternalId = $function->getAttribute('repositoryInternalId', '');
|
||||
|
||||
|
||||
@@ -450,7 +450,7 @@ class Builds extends Action
|
||||
|
||||
$providerCommitHash = \trim($stdout);
|
||||
|
||||
$deployment->setAttribute('providerCommitHash', $providerCommitHash ?? '');
|
||||
$deployment->setAttribute('providerCommitHash', $providerCommitHash);
|
||||
$deployment->setAttribute('providerCommitAuthorUrl', APP_VCS_GITHUB_URL);
|
||||
$deployment->setAttribute('providerCommitAuthor', APP_VCS_GITHUB_USERNAME);
|
||||
$deployment->setAttribute('providerCommitMessage', "Create '" . $resource->getAttribute('name', '') . "' function");
|
||||
@@ -862,7 +862,7 @@ class Builds extends Action
|
||||
if (\str_contains($logs, '{APPWRITE_DETECTION_SEPARATOR_START}')) {
|
||||
[$logsBefore, $detectionLogsStart] = \explode('{APPWRITE_DETECTION_SEPARATOR_START}', $logs, 2);
|
||||
[$detectionLogs, $logsAfter] = \explode('{APPWRITE_DETECTION_SEPARATOR_END}', $detectionLogsStart, 2);
|
||||
$logs = ($logsBefore ?? '') . ($logsAfter ?? '');
|
||||
$logs = $logsBefore . $logsAfter;
|
||||
}
|
||||
|
||||
$deployment->setAttribute('buildLogs', $logs);
|
||||
@@ -1203,6 +1203,8 @@ class Builds extends Action
|
||||
protected function sendUsage(Document $resource, Document $deployment, Document $project, Context $usage, UsagePublisher $publisherForUsage): void
|
||||
{
|
||||
$spec = Config::getParam('specifications')[$resource->getAttribute('buildSpecification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
|
||||
$cpus = (int) ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT);
|
||||
$memory = (int) ($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT);
|
||||
|
||||
switch ($deployment->getAttribute('status')) {
|
||||
case 'ready':
|
||||
@@ -1364,6 +1366,8 @@ class Builds extends Action
|
||||
Realtime $queueForRealtime,
|
||||
array $platform
|
||||
): void {
|
||||
$deployment = new Document();
|
||||
|
||||
try {
|
||||
if ($resource->getAttribute('providerSilentMode', false) === true) {
|
||||
return;
|
||||
@@ -1444,7 +1448,7 @@ class Builds extends Action
|
||||
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
|
||||
$previewUrl = match ($resource->getCollection()) {
|
||||
'functions' => '',
|
||||
'sites' => ! empty($rule) ? ("{$protocol}://" . $rule->getAttribute('domain', '')) : '',
|
||||
'sites' => !$rule->isEmpty() ? ("{$protocol}://" . $rule->getAttribute('domain', '')) : '',
|
||||
default => throw new \Exception('Invalid resource type')
|
||||
};
|
||||
|
||||
|
||||
@@ -83,7 +83,8 @@ class Update extends Action
|
||||
|
||||
// If rule is already verified or in certificate generation state, don't queue for verification again
|
||||
if ($rule->getAttribute('status') === RULE_STATUS_VERIFIED || $rule->getAttribute('status') === RULE_STATUS_CERTIFICATE_GENERATING) {
|
||||
return $response->dynamic($rule, Response::MODEL_PROXY_RULE);
|
||||
$response->dynamic($rule, Response::MODEL_PROXY_RULE);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -87,16 +87,11 @@ class Get extends Action
|
||||
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
|
||||
}
|
||||
|
||||
switch ($type) {
|
||||
case 'output':
|
||||
$path = $deployment->getAttribute('buildPath', '');
|
||||
$device = $deviceForBuilds;
|
||||
break;
|
||||
case 'source':
|
||||
$path = $deployment->getAttribute('sourcePath', '');
|
||||
$device = $deviceForSites;
|
||||
break;
|
||||
}
|
||||
[$path, $device] = match ($type) {
|
||||
'output' => [$deployment->getAttribute('buildPath', ''), $deviceForBuilds],
|
||||
'source' => [$deployment->getAttribute('sourcePath', ''), $deviceForSites],
|
||||
default => throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Invalid deployment download type.'),
|
||||
};
|
||||
|
||||
if (!$device->exists($path)) {
|
||||
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
|
||||
|
||||
@@ -172,8 +172,6 @@ class Update extends Base
|
||||
$framework = $site->getAttribute('framework');
|
||||
}
|
||||
|
||||
$enabled ??= $site->getAttribute('enabled', true);
|
||||
|
||||
$repositoryId = $site->getAttribute('repositoryId', '');
|
||||
$repositoryInternalId = $site->getAttribute('repositoryInternalId', '');
|
||||
|
||||
|
||||
@@ -286,6 +286,8 @@ class Create extends Action
|
||||
$mimeType = $deviceForFiles->getFileMimeType($path); // Get mime-type before compression and encryption
|
||||
$fileHash = $deviceForFiles->getFileHash($path); // Get file hash before compression and encryption
|
||||
$data = '';
|
||||
$iv = '';
|
||||
$tag = null;
|
||||
// Compression
|
||||
$algorithm = $bucket->getAttribute('compression', Compression::NONE);
|
||||
if ($fileSize <= APP_STORAGE_READ_BUFFER && $algorithm != Compression::NONE) {
|
||||
|
||||
@@ -99,12 +99,8 @@ class Update extends Action
|
||||
|
||||
$permissions ??= $bucket->getPermissions();
|
||||
$maximumFileSize ??= $bucket->getAttribute('maximumFileSize', (int) System::getEnv('_APP_STORAGE_LIMIT', 0));
|
||||
$allowedFileExtensions ??= $bucket->getAttribute('allowedFileExtensions', []);
|
||||
$enabled ??= $bucket->getAttribute('enabled', true);
|
||||
$encryption ??= $bucket->getAttribute('encryption', true);
|
||||
$antivirus ??= $bucket->getAttribute('antivirus', true);
|
||||
$compression ??= $bucket->getAttribute('compression', Compression::NONE);
|
||||
$transformations ??= $bucket->getAttribute('transformations', true);
|
||||
|
||||
// Map aggregate permissions into the multiple permissions they represent.
|
||||
$permissions = Permission::aggregate($permissions);
|
||||
|
||||
@@ -17,6 +17,7 @@ use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Response;
|
||||
use libphonenumber\NumberParseException;
|
||||
use libphonenumber\PhoneNumberUtil;
|
||||
use Throwable;
|
||||
use Utopia\Auth\Proofs\Password;
|
||||
use Utopia\Auth\Proofs\Token;
|
||||
use Utopia\Database\Database;
|
||||
@@ -102,6 +103,8 @@ class Create extends Action
|
||||
{
|
||||
$isAppUser = $user->isApp($authorization->getRoles());
|
||||
$isPrivilegedUser = $user->isPrivileged($authorization->getRoles());
|
||||
$invitee = new Document();
|
||||
$hash = '';
|
||||
|
||||
if (empty($url)) {
|
||||
if (! $isAppUser && ! $isPrivilegedUser) {
|
||||
@@ -145,9 +148,6 @@ class Create extends Action
|
||||
}
|
||||
} elseif (! empty($phone)) {
|
||||
$invitee = $dbForProject->findOne('users', [Query::equal('phone', [$phone])]);
|
||||
if (! $invitee->isEmpty() && ! empty($email) && $invitee->getAttribute('email', '') !== $email) {
|
||||
throw new Exception(Exception::USER_ALREADY_EXISTS, 'Given phone and email doesn\'t match', 409);
|
||||
}
|
||||
}
|
||||
|
||||
if ($invitee->isEmpty()) { // Create new user if no user with same email found
|
||||
|
||||
+1
-4
@@ -110,10 +110,7 @@ class Update extends Action
|
||||
$providerRepositoryId = $repository->getAttribute('providerRepositoryId');
|
||||
|
||||
try {
|
||||
$providerRepositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
|
||||
if (empty($providerRepositoryName)) {
|
||||
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
|
||||
}
|
||||
$providerRepositoryName = $github->getRepositoryName($providerRepositoryId);
|
||||
} catch (RepositoryNotFound $e) {
|
||||
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ class Get extends Action
|
||||
}
|
||||
|
||||
$state = \json_decode($state, true);
|
||||
$redirectFailure = $state['failure'] ?? '';
|
||||
$projectId = $state['projectId'] ?? '';
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
@@ -74,10 +75,11 @@ class Get extends Action
|
||||
|
||||
if (!empty($redirectFailure)) {
|
||||
$separator = \str_contains($redirectFailure, '?') ? '&' : ':';
|
||||
return $response
|
||||
$response
|
||||
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
|
||||
->addHeader('Pragma', 'no-cache')
|
||||
->redirect($redirectFailure . $separator . \http_build_query(['error' => $error]));
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND, $error);
|
||||
@@ -165,10 +167,11 @@ class Get extends Action
|
||||
|
||||
if (!empty($redirectFailure)) {
|
||||
$separator = \str_contains($redirectFailure, '?') ? '&' : ':';
|
||||
return $response
|
||||
$response
|
||||
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
|
||||
->addHeader('Pragma', 'no-cache')
|
||||
->redirect($redirectFailure . $separator . \http_build_query(['error' => $error]));
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, $error);
|
||||
|
||||
@@ -49,6 +49,8 @@ trait Deployment
|
||||
) {
|
||||
$errors = [];
|
||||
foreach ($repositories as $repository) {
|
||||
$logBase = 'vcs.github.event.repo.unknown';
|
||||
|
||||
try {
|
||||
$repositoryId = $repository->getId();
|
||||
$projectId = $repository->getAttribute('projectId');
|
||||
@@ -70,6 +72,8 @@ trait Deployment
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND, 'Repository references non-existent project');
|
||||
}
|
||||
|
||||
$this->beforeCreateGitDeployment($project, $repository, $dbForPlatform, $authorization);
|
||||
|
||||
try {
|
||||
$dsn = new DSN($project->getAttribute('database'));
|
||||
$databaseName = $dsn->getHost();
|
||||
@@ -105,18 +109,11 @@ trait Deployment
|
||||
|
||||
$owner = $github->getOwnerName($providerInstallationId) ?? '';
|
||||
try {
|
||||
$repositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
|
||||
if (empty($repositoryName)) {
|
||||
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
|
||||
}
|
||||
$repositoryName = $github->getRepositoryName($providerRepositoryId);
|
||||
} catch (RepositoryNotFound $e) {
|
||||
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (empty($repositoryName)) {
|
||||
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
|
||||
}
|
||||
|
||||
$isAuthorized = !$external;
|
||||
|
||||
if (!$isAuthorized && !empty($providerPullRequestId)) {
|
||||
@@ -289,10 +286,7 @@ trait Deployment
|
||||
|
||||
$providerRepositoryId = $repository->getAttribute('providerRepositoryId');
|
||||
try {
|
||||
$repositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
|
||||
if (empty($repositoryName)) {
|
||||
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
|
||||
}
|
||||
$repositoryName = $github->getRepositoryName($providerRepositoryId);
|
||||
} catch (RepositoryNotFound $e) {
|
||||
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
|
||||
}
|
||||
@@ -499,7 +493,7 @@ trait Deployment
|
||||
$rule = $authorization->skip(fn () => $dbForPlatform->getDocument('rules', $previewRuleId));
|
||||
|
||||
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
|
||||
$previewUrl = !empty($rule) ? ("{$protocol}://" . $rule->getAttribute('domain', '')) : '';
|
||||
$previewUrl = !$rule->isEmpty() ? ("{$protocol}://" . $rule->getAttribute('domain', '')) : '';
|
||||
|
||||
if (!empty($previewUrl)) {
|
||||
$comment = new Comment($platform);
|
||||
@@ -522,10 +516,7 @@ trait Deployment
|
||||
|
||||
$providerRepositoryId = $repository->getAttribute('providerRepositoryId');
|
||||
try {
|
||||
$repositoryName = $github->getRepositoryName($providerRepositoryId) ?? '';
|
||||
if (empty($repositoryName)) {
|
||||
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
|
||||
}
|
||||
$repositoryName = $github->getRepositoryName($providerRepositoryId);
|
||||
} catch (RepositoryNotFound $e) {
|
||||
throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND);
|
||||
}
|
||||
@@ -561,6 +552,10 @@ trait Deployment
|
||||
}
|
||||
}
|
||||
|
||||
protected function beforeCreateGitDeployment(Document $project, Document $repository, Database $dbForPlatform, Authorization $authorization): void
|
||||
{
|
||||
}
|
||||
|
||||
protected function getBuildQueueName(Document $project, Database $dbForPlatform, Authorization $authorization): string
|
||||
{
|
||||
return System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::BUILDS_QUEUE_NAME);
|
||||
|
||||
@@ -83,7 +83,7 @@ class Create extends Action
|
||||
default => null,
|
||||
};
|
||||
|
||||
return $response->json($parsedPayload);
|
||||
$response->json($parsedPayload);
|
||||
}
|
||||
|
||||
protected function preprocessEvent(Request $request)
|
||||
|
||||
@@ -4,7 +4,6 @@ namespace Appwrite\Platform\Tasks;
|
||||
|
||||
use Appwrite\ClamAV\Network;
|
||||
use Appwrite\PubSub\Adapter\Pool as PubSubPool;
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use Utopia\Cache\Adapter\Pool as CachePool;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Console;
|
||||
@@ -13,6 +12,8 @@ use Utopia\Domains\Domain;
|
||||
use Utopia\DSN\DSN;
|
||||
use Utopia\Http\Http;
|
||||
use Utopia\Logger\Logger;
|
||||
use Utopia\Messaging\Adapter\Email as EmailAdapter;
|
||||
use Utopia\Messaging\Messages\Email as EmailMessage;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Pools\Group;
|
||||
use Utopia\Queue\Broker\Pool as BrokerPool;
|
||||
@@ -124,7 +125,7 @@ class Doctor extends Action
|
||||
$providerConfig = System::getEnv('_APP_LOGGING_CONFIG', '');
|
||||
|
||||
try {
|
||||
$loggingProvider = new DSN($providerConfig ?? '');
|
||||
$loggingProvider = new DSN($providerConfig);
|
||||
|
||||
$providerName = $loggingProvider->getScheme();
|
||||
|
||||
@@ -212,15 +213,18 @@ class Doctor extends Action
|
||||
}
|
||||
|
||||
try {
|
||||
/* @var PHPMailer $mail */
|
||||
$mail = $register->get('smtp');
|
||||
/** @var EmailAdapter $smtp */
|
||||
$smtp = $register->get('smtp');
|
||||
|
||||
$mail->addAddress('demo@example.com', 'Example.com');
|
||||
$mail->Subject = 'Test SMTP Connection';
|
||||
$mail->Body = 'Hello World';
|
||||
$mail->AltBody = 'Hello World';
|
||||
$emailMessage = new EmailMessage(
|
||||
to: ['demo@example.com'],
|
||||
subject: 'Test SMTP Connection',
|
||||
content: 'Hello World',
|
||||
fromName: \urldecode(System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server')),
|
||||
fromEmail: System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM),
|
||||
);
|
||||
|
||||
$mail->send();
|
||||
$smtp->send($emailMessage);
|
||||
Console::success('🟢 ' . str_pad("SMTP", 50, '.') . 'connected');
|
||||
} catch (\Throwable) {
|
||||
Console::error('🔴 ' . str_pad("SMTP", 47, '.') . 'disconnected');
|
||||
|
||||
@@ -227,7 +227,7 @@ class Install extends Action
|
||||
// Fall back to CLI mode
|
||||
$enableAssistant = false;
|
||||
$assistantExistsInOldCompose = false;
|
||||
if ($existingInstallation && isset($compose)) {
|
||||
if ($existingInstallation) {
|
||||
try {
|
||||
$assistantService = $compose->getService('appwrite-assistant');
|
||||
$assistantExistsInOldCompose = $assistantService !== null;
|
||||
@@ -1343,7 +1343,7 @@ class Install extends Action
|
||||
{
|
||||
$argv = $_SERVER['argv'] ?? [];
|
||||
foreach ($argv as $arg) {
|
||||
if (\str_starts_with($arg, '--') && !\str_starts_with($arg, '--interactive')) {
|
||||
if (\str_starts_with($arg, '--')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -566,6 +566,8 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
|
||||
$repoBranch = $language['repoBranch'] ?? 'main';
|
||||
if ($git && !empty($gitUrl)) {
|
||||
$prUrls = [];
|
||||
|
||||
// Generate commit message: use provided message, AI changelog, or fallback
|
||||
if (! empty($message)) {
|
||||
$commitMessage = $message;
|
||||
|
||||
@@ -60,7 +60,7 @@ class StatsResources extends Action
|
||||
|
||||
$interval = (int) System::getEnv('_APP_STATS_RESOURCES_INTERVAL', '3600');
|
||||
|
||||
Console::loop(function () use ($queueForStatsResources, $dbForPlatform) {
|
||||
Console::loop(function () use ($queueForStatsResources) {
|
||||
|
||||
$last24Hours = (new \DateTime())->sub(\DateInterval::createFromDateString('24 hours'));
|
||||
/**
|
||||
|
||||
@@ -30,7 +30,7 @@ class Upgrade extends Install
|
||||
->param('interactive', 'Y', new Text(1), 'Run an interactive session', true)
|
||||
->param('no-start', false, new Boolean(true), 'Run an interactive session', true)
|
||||
->param('database', 'mongodb', new Text(length: 0), 'Database to use (mongodb|mariadb|postgresql)', true)
|
||||
->param('migrate', true, new Boolean(true), 'Run database migration after upgrade', true)
|
||||
->param('migrate', false, new Boolean(true), 'Run database migration after upgrade', true)
|
||||
->callback($this->action(...));
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ class Upgrade extends Install
|
||||
string $interactive,
|
||||
bool $noStart,
|
||||
string $database,
|
||||
bool $migrate = true,
|
||||
bool $migrate = false,
|
||||
): void {
|
||||
$this->isUpgrade = true;
|
||||
$this->migrate = $migrate;
|
||||
|
||||
@@ -6,7 +6,6 @@ use Exception;
|
||||
use Throwable;
|
||||
use Utopia\Console;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Authorization;
|
||||
use Utopia\Database\Exception\Structure;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Queue\Message;
|
||||
@@ -51,13 +50,11 @@ class Audits extends Action
|
||||
|
||||
/**
|
||||
* @param Message $message
|
||||
* @param callable $getProjectDB
|
||||
* @param Document $project
|
||||
* @param callable $getAudit
|
||||
* @param callable(Document): \Utopia\Audit\Audit $getAudit
|
||||
* @return Commit|NoCommit
|
||||
* @throws Throwable
|
||||
* @throws \Utopia\Database\Exception
|
||||
* @throws Authorization
|
||||
* @throws Structure
|
||||
*/
|
||||
public function action(Message $message, Document $project, callable $getAudit): Commit|NoCommit
|
||||
|
||||
@@ -24,7 +24,6 @@ use Utopia\Database\Exception\Conflict;
|
||||
use Utopia\Database\Exception\Restricted;
|
||||
use Utopia\Database\Exception\Structure;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\DSN\DSN;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Platform\Action;
|
||||
@@ -363,7 +362,6 @@ class Deletes extends Action
|
||||
/**
|
||||
* @param Document $project
|
||||
* @param callable $getProjectDB
|
||||
* @param Document $target
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
@@ -438,7 +436,6 @@ class Deletes extends Action
|
||||
* @param string $resource
|
||||
* @param string|null $resourceType
|
||||
* @return void
|
||||
* @throws Authorization
|
||||
* @throws Exception
|
||||
*/
|
||||
private function deleteCacheByResource(Document $project, callable $getProjectDB, string $resource, ?string $resourceType = null): void
|
||||
@@ -518,7 +515,6 @@ class Deletes extends Action
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Database $dbForPlatform
|
||||
* @param callable $getProjectDB
|
||||
* @param string $hourlyUsageRetentionDatetime
|
||||
* @return void
|
||||
@@ -586,7 +582,6 @@ class Deletes extends Action
|
||||
* @param Database $dbForPlatform
|
||||
* @param Document $document
|
||||
* @return void
|
||||
* @throws Authorization
|
||||
* @throws DatabaseException
|
||||
* @throws Conflict
|
||||
* @throws Restricted
|
||||
@@ -623,7 +618,6 @@ class Deletes extends Action
|
||||
* @param Document $document
|
||||
* @return void
|
||||
* @throws Exception
|
||||
* @throws Authorization
|
||||
* @throws DatabaseException
|
||||
*/
|
||||
protected function deleteProject(Database $dbForPlatform, callable $getProjectDB, callable $getDatabasesDB, Device $deviceForFiles, Device $deviceForSites, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, CertificatesAdapter $certificates, Document $document): void
|
||||
@@ -952,7 +946,7 @@ class Deletes extends Action
|
||||
// fast path, no need to list anything!
|
||||
$delete($dbForProject, $resourceInternalId, $resourceType);
|
||||
} else {
|
||||
$processResource = function (string $type) use ($dbForProject, $delete, $resourceType) {
|
||||
$processResource = function (string $type) use ($dbForProject, $delete) {
|
||||
$this->listByGroup(
|
||||
collection: $type,
|
||||
queries: [Query::select(['$id', '$sequence'])],
|
||||
@@ -1109,7 +1103,7 @@ class Deletes extends Action
|
||||
Query::equal('resourceInternalId', [$siteInternalId]),
|
||||
Query::equal('resourceType', ['sites']),
|
||||
Query::orderAsc()
|
||||
], $dbForProject, function (Document $document) use ($project, $certificates, $deviceForSites, $deviceForBuilds, $deviceForFiles, $dbForPlatform, &$deploymentInternalIds) {
|
||||
], $dbForProject, function (Document $document) use ($deviceForSites, $deviceForBuilds, $deviceForFiles, $dbForPlatform, &$deploymentInternalIds, &$deploymentIds) {
|
||||
$deploymentInternalIds[] = $document->getSequence();
|
||||
$deploymentIds[] = $document->getId();
|
||||
$this->deleteBuildFiles($deviceForBuilds, $document);
|
||||
@@ -1172,7 +1166,7 @@ class Deletes extends Action
|
||||
Query::equal('deploymentResourceInternalId', [$functionInternalId]),
|
||||
Query::equal('projectInternalId', [$project->getSequence()]),
|
||||
Query::orderAsc()
|
||||
], $dbForPlatform, function (Document $document) use ($project, $dbForPlatform, $certificates) {
|
||||
], $dbForPlatform, function (Document $document) use ($dbForPlatform, $certificates) {
|
||||
$this->deleteRule($dbForPlatform, $document, $certificates);
|
||||
});
|
||||
|
||||
@@ -1196,7 +1190,7 @@ class Deletes extends Action
|
||||
Query::equal('resourceInternalId', [$functionInternalId]),
|
||||
Query::equal('resourceType', ['functions']),
|
||||
Query::orderAsc()
|
||||
], $dbForProject, function (Document $document) use ($dbForPlatform, $project, $certificates, $deviceForFunctions, $deviceForBuilds, &$deploymentInternalIds) {
|
||||
], $dbForProject, function (Document $document) use ($deviceForFunctions, $deviceForBuilds, &$deploymentInternalIds) {
|
||||
$deploymentInternalIds[] = $document->getSequence();
|
||||
$this->deleteDeploymentFiles($deviceForFunctions, $document);
|
||||
$this->deleteBuildFiles($deviceForBuilds, $document);
|
||||
@@ -1321,7 +1315,7 @@ class Deletes extends Action
|
||||
|
||||
/**
|
||||
* @param Device $device
|
||||
* @param Document $build
|
||||
* @param Document $deployment
|
||||
* @return void
|
||||
*/
|
||||
private function deleteBuildFiles(Device $device, Document $deployment): void
|
||||
@@ -1631,9 +1625,9 @@ class Deletes extends Action
|
||||
try {
|
||||
$dbForProject->deleteDocuments('transactions', [
|
||||
Query::lessThan('expiresAt', DateTime::format(new \DateTime())),
|
||||
], onNext: function (Document $transaction) use ($dbForProject, $project, &$transactionInternalIds) {
|
||||
], onNext: function (Document $transaction) use (&$transactionInternalIds) {
|
||||
$transactionInternalIds[] = $transaction->getSequence();
|
||||
}, onError: function (Throwable $th) use ($project) {
|
||||
}, onError: function (Throwable $th) {
|
||||
// Swallow errors to avoid breaking the cleanup process
|
||||
});
|
||||
} catch (Throwable $th) {
|
||||
@@ -1646,7 +1640,7 @@ class Deletes extends Action
|
||||
|
||||
$dbForProject->deleteDocuments('transactionLogs', [
|
||||
Query::equal('transactionInternalId', $transactionInternalIds),
|
||||
], onError: function (Throwable $th) use ($project) {
|
||||
], onError: function (Throwable $th) {
|
||||
// Swallow errors to avoid breaking the cleanup process
|
||||
});
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ class Functions extends Action
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
@@ -256,7 +256,7 @@ class Functions extends Action
|
||||
* @param Document $user
|
||||
* @param string|null $jwt
|
||||
* @param string|null $event
|
||||
* @throws Exception
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function fail(
|
||||
string $message,
|
||||
@@ -271,10 +271,10 @@ class Functions extends Action
|
||||
?string $event = null,
|
||||
): void {
|
||||
$executionId = ID::unique();
|
||||
$headers['x-appwrite-execution-id'] = $executionId ?? '';
|
||||
$headers['x-appwrite-execution-id'] = $executionId;
|
||||
$headers['x-appwrite-trigger'] = $trigger;
|
||||
$headers['x-appwrite-event'] = $event ?? '';
|
||||
$headers['x-appwrite-user-id'] = $user->getId() ?? '';
|
||||
$headers['x-appwrite-user-id'] = $user->getId();
|
||||
$headers['x-appwrite-user-jwt'] = $jwt ?? '';
|
||||
|
||||
$headersFiltered = [];
|
||||
@@ -458,8 +458,8 @@ class Functions extends Action
|
||||
if ($version === 'v2') {
|
||||
$vars = \array_merge($vars, [
|
||||
'APPWRITE_FUNCTION_TRIGGER' => $headers['x-appwrite-trigger'] ?? '',
|
||||
'APPWRITE_FUNCTION_DATA' => $body ?? '',
|
||||
'APPWRITE_FUNCTION_EVENT_DATA' => $body ?? '',
|
||||
'APPWRITE_FUNCTION_DATA' => $body,
|
||||
'APPWRITE_FUNCTION_EVENT_DATA' => $body,
|
||||
'APPWRITE_FUNCTION_EVENT' => $headers['x-appwrite-event'] ?? '',
|
||||
'APPWRITE_FUNCTION_USER_ID' => $headers['x-appwrite-user-id'] ?? '',
|
||||
'APPWRITE_FUNCTION_JWT' => $headers['x-appwrite-user-jwt'] ?? ''
|
||||
@@ -508,6 +508,9 @@ class Functions extends Action
|
||||
]);
|
||||
|
||||
/** Execute function */
|
||||
$error = null;
|
||||
$errorCode = 0;
|
||||
|
||||
try {
|
||||
$version = $function->getAttribute('version', 'v2');
|
||||
$command = $runtime['startCommand'];
|
||||
|
||||
@@ -4,10 +4,13 @@ namespace Appwrite\Platform\Workers;
|
||||
|
||||
use Appwrite\Template\Template;
|
||||
use Exception;
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use Swoole\Runtime;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\Messaging\Adapter\Email as EmailAdapter;
|
||||
use Utopia\Messaging\Adapter\Email\SMTP;
|
||||
use Utopia\Messaging\Messages\Email as EmailMessage;
|
||||
use Utopia\Messaging\Messages\Email\Attachment;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Queue\Message;
|
||||
use Utopia\Registry\Registry;
|
||||
@@ -49,9 +52,9 @@ class Mails extends Action
|
||||
|
||||
/**
|
||||
* @param Message $message
|
||||
* @param Document $project
|
||||
* @param Registry $register
|
||||
* @param Log $log
|
||||
* @throws \PHPMailer\PHPMailer\Exception
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
@@ -132,36 +135,38 @@ class Mails extends Action
|
||||
// render() will return the subject in <p> tags, so use strip_tags() to remove them
|
||||
$subject = \strip_tags($subjectTemplate->render());
|
||||
|
||||
/** @var PHPMailer $mail */
|
||||
$mail = empty($smtp)
|
||||
/** @var EmailAdapter $adapter */
|
||||
$adapter = empty($smtp)
|
||||
? $register->get('smtp')
|
||||
: $this->getMailer($smtp);
|
||||
: new SMTP(
|
||||
host: $smtp['host'],
|
||||
port: (int) $smtp['port'],
|
||||
username: $smtp['username'] ?? '',
|
||||
password: $smtp['password'] ?? '',
|
||||
smtpSecure: $smtp['secure'] ?? '',
|
||||
smtpAutoTLS: false,
|
||||
xMailer: 'Appwrite Mailer',
|
||||
timeout: 10,
|
||||
keepAlive: true,
|
||||
timelimit: 30,
|
||||
);
|
||||
|
||||
$mail->clearAddresses();
|
||||
$mail->clearAllRecipients();
|
||||
$mail->clearReplyTos();
|
||||
$mail->clearAttachments();
|
||||
$mail->clearBCCs();
|
||||
$mail->clearCCs();
|
||||
$mail->addAddress($recipient, $name);
|
||||
$mail->Subject = $subject;
|
||||
$mail->Body = $body;
|
||||
// Resolve from/replyTo using fallback hierarchy: Custom options > SMTP config > Defaults
|
||||
$defaultFromEmail = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM);
|
||||
$defaultFromName = \urldecode(System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'));
|
||||
|
||||
$mail->AltBody = $body;
|
||||
$mail->AltBody = preg_replace('/<style\b[^>]*>(.*?)<\/style>/is', '', $mail->AltBody);
|
||||
$mail->AltBody = \strip_tags($mail->AltBody);
|
||||
$mail->AltBody = \trim($mail->AltBody);
|
||||
|
||||
$replyTo = System::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM);
|
||||
$replyToName = \urldecode(System::getEnv('_APP_SYSTEM_EMAIL_NAME', APP_NAME . ' Server'));
|
||||
$fromEmail = !empty($smtp) ? ($smtp['senderEmail'] ?? $defaultFromEmail) : $defaultFromEmail;
|
||||
$fromName = !empty($smtp) ? ($smtp['senderName'] ?? $defaultFromName) : $defaultFromName;
|
||||
$replyTo = $defaultFromEmail;
|
||||
$replyToName = $defaultFromName;
|
||||
|
||||
$customMailOptions = $payload['customMailOptions'] ?? [];
|
||||
|
||||
// fallback hierarchy: Custom options > SMTP config > Defaults.
|
||||
if (!empty($customMailOptions['senderEmail']) || !empty($customMailOptions['senderName'])) {
|
||||
$fromEmail = $customMailOptions['senderEmail'] ?? $mail->From;
|
||||
$fromName = $customMailOptions['senderName'] ?? $mail->FromName;
|
||||
$mail->setFrom($fromEmail, $fromName);
|
||||
if (!empty($customMailOptions['senderEmail'])) {
|
||||
$fromEmail = $customMailOptions['senderEmail'];
|
||||
}
|
||||
if (!empty($customMailOptions['senderName'])) {
|
||||
$fromName = $customMailOptions['senderName'];
|
||||
}
|
||||
|
||||
if (!empty($customMailOptions['replyToEmail']) || !empty($customMailOptions['replyToName'])) {
|
||||
@@ -172,18 +177,32 @@ class Mails extends Action
|
||||
$replyToName = $smtp['senderName'] ?? $replyToName;
|
||||
}
|
||||
|
||||
$mail->addReplyTo($replyTo, $replyToName);
|
||||
$attachments = null;
|
||||
if (!empty($attachment['content'] ?? '')) {
|
||||
$mail->AddStringAttachment(
|
||||
base64_decode($attachment['content']),
|
||||
$attachment['filename'] ?? 'unknown.file',
|
||||
$attachment['encoding'] ?? PHPMailer::ENCODING_BASE64,
|
||||
$attachment['type'] ?? 'plain/text'
|
||||
);
|
||||
$attachments = [
|
||||
new Attachment(
|
||||
name: $attachment['filename'] ?? 'unknown.file',
|
||||
path: '',
|
||||
type: $attachment['type'] ?? 'plain/text',
|
||||
content: \base64_decode($attachment['content']),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
$emailMessage = new EmailMessage(
|
||||
to: [['email' => $recipient, 'name' => $name]],
|
||||
subject: $subject,
|
||||
content: $body,
|
||||
fromName: $fromName,
|
||||
fromEmail: $fromEmail,
|
||||
replyToName: $replyToName,
|
||||
replyToEmail: $replyTo,
|
||||
attachments: $attachments,
|
||||
html: true,
|
||||
);
|
||||
|
||||
try {
|
||||
$mail->send();
|
||||
$adapter->send($emailMessage);
|
||||
} catch (\Throwable $error) {
|
||||
if ($type === 'smtp') {
|
||||
throw new Exception('Error sending mail: ' . $error->getMessage(), 401);
|
||||
@@ -191,38 +210,4 @@ class Mails extends Action
|
||||
throw new Exception('Error sending mail: ' . $error->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $smtp
|
||||
* @return PHPMailer
|
||||
* @throws \PHPMailer\PHPMailer\Exception
|
||||
*/
|
||||
protected function getMailer(array $smtp): PHPMailer
|
||||
{
|
||||
$mail = new PHPMailer(true);
|
||||
|
||||
$mail->isSMTP();
|
||||
|
||||
$username = $smtp['username'];
|
||||
$password = $smtp['password'];
|
||||
|
||||
$mail->XMailer = 'Appwrite Mailer';
|
||||
$mail->Host = $smtp['host'];
|
||||
$mail->Port = $smtp['port'];
|
||||
$mail->SMTPAuth = (!empty($username) && !empty($password));
|
||||
$mail->Username = $username;
|
||||
$mail->Password = $password;
|
||||
$mail->SMTPSecure = $smtp['secure'];
|
||||
$mail->SMTPAutoTLS = false;
|
||||
$mail->SMTPKeepAlive = true;
|
||||
$mail->CharSet = 'UTF-8';
|
||||
$mail->Timeout = 10; /* Connection timeout */
|
||||
$mail->getSMTPInstance()->Timelimit = 30; /* Timeout for each individual SMTP command (e.g. HELO, EHLO, etc.) */
|
||||
|
||||
$mail->setFrom($smtp['senderEmail'], $smtp['senderName']);
|
||||
|
||||
$mail->isHTML();
|
||||
|
||||
return $mail;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,7 +285,7 @@ class Messaging extends Action
|
||||
|
||||
try {
|
||||
$response = $adapter->send($data);
|
||||
$deliveredTotal += $response['deliveredTo'];
|
||||
$deliveredTotal += (int) $response['deliveredTo'];
|
||||
foreach ($response['results'] as $result) {
|
||||
if ($result['status'] === 'failure') {
|
||||
$deliveryErrors[] = "Failed sending to target {$result['recipient']} with error: {$result['error']}";
|
||||
@@ -380,7 +380,7 @@ class Messaging extends Action
|
||||
]));
|
||||
|
||||
// Delete any attachments that were downloaded to local storage
|
||||
if ($provider->getAttribute('type') === MESSAGE_TYPE_EMAIL) {
|
||||
if ($providerType === MESSAGE_TYPE_EMAIL) {
|
||||
if ($deviceForFiles->getType() === Storage::DEVICE_LOCAL) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ use Utopia\Locale\Locale;
|
||||
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\Exception as MigrationException;
|
||||
use Utopia\Migration\Resource;
|
||||
use Utopia\Migration\Resources\Database\Database as ResourceDatabase;
|
||||
@@ -37,6 +38,7 @@ use Utopia\Migration\Source;
|
||||
use Utopia\Migration\Sources\Appwrite as SourceAppwrite;
|
||||
use Utopia\Migration\Sources\CSV;
|
||||
use Utopia\Migration\Sources\Firebase;
|
||||
use Utopia\Migration\Sources\JSON;
|
||||
use Utopia\Migration\Sources\NHost;
|
||||
use Utopia\Migration\Sources\Supabase;
|
||||
use Utopia\Migration\Transfer;
|
||||
@@ -208,8 +210,8 @@ class Migrations extends Action
|
||||
$getDatabasesDB = fn (Document $database): Database =>
|
||||
$this->getDatabasesDBForProject($database);
|
||||
$queries = [];
|
||||
if ($source === SourceAppwrite::getName() && $destination === DestinationCSV::getName()) {
|
||||
$queries = Query::parseQueries($migrationOptions['queries']);
|
||||
if ($source === SourceAppwrite::getName() && in_array($destination, [DestinationCSV::getName(), DestinationJSON::getName()])) {
|
||||
$queries = Query::parseQueries($migrationOptions['queries'] ?? []);
|
||||
}
|
||||
|
||||
$migrationSource = match ($source) {
|
||||
@@ -250,6 +252,12 @@ class Migrations extends Action
|
||||
$this->dbForProject,
|
||||
$getDatabasesDB
|
||||
),
|
||||
JSON::getName() => new JSON(
|
||||
$resourceId,
|
||||
$migrationOptions['path'],
|
||||
$this->deviceForMigrations,
|
||||
$this->dbForProject,
|
||||
),
|
||||
default => throw new \Exception('Invalid source type'),
|
||||
};
|
||||
|
||||
@@ -288,6 +296,13 @@ class Migrations extends Action
|
||||
$options['escape'],
|
||||
$options['header'],
|
||||
),
|
||||
DestinationJSON::getName() => new DestinationJSON(
|
||||
$this->deviceForFiles,
|
||||
$migration->getAttribute('resourceId'),
|
||||
$options['bucketId'] ?? 'default',
|
||||
$options['filename'],
|
||||
$options['columns'] ?? [],
|
||||
),
|
||||
default => throw new \Exception('Invalid destination type'),
|
||||
};
|
||||
}
|
||||
@@ -393,6 +408,7 @@ class Migrations extends Action
|
||||
$tempAPIKey = $this->generateAPIKey($project);
|
||||
|
||||
$transfer = $source = $destination = null;
|
||||
$aggregatedResources = [];
|
||||
|
||||
$host = System::getEnv('_APP_MIGRATION_HOST');
|
||||
if (empty($host)) {
|
||||
@@ -429,7 +445,6 @@ class Migrations extends Action
|
||||
$destination
|
||||
);
|
||||
|
||||
$aggregatedResources = [];
|
||||
/** Start Transfer */
|
||||
if (empty($source->getErrors())) {
|
||||
$migration->setAttribute('stage', 'migrating');
|
||||
@@ -550,8 +565,9 @@ class Migrations extends Action
|
||||
$destination?->success();
|
||||
$source?->success();
|
||||
}
|
||||
if ($migration->getAttribute('destination') === DestinationCSV::getName()) {
|
||||
$this->handleCSVExportComplete($project, $migration, $queueForMails, $queueForRealtime, $platform, $authorization);
|
||||
$destination_type = $migration->getAttribute('destination');
|
||||
if ($destination_type === DestinationCSV::getName() || $destination_type === DestinationJSON::getName()) {
|
||||
$this->handleDataExportComplete($project, $migration, $queueForMails, $queueForRealtime, $platform, $authorization);
|
||||
}
|
||||
} finally {
|
||||
$source?->cleanup();
|
||||
@@ -583,7 +599,7 @@ class Migrations extends Action
|
||||
* @param Authorization $authorization
|
||||
* @return void
|
||||
*/
|
||||
protected function handleCSVExportComplete(
|
||||
protected function handleDataExportComplete(
|
||||
Document $project,
|
||||
Document $migration,
|
||||
Mail $queueForMails,
|
||||
@@ -608,7 +624,8 @@ class Migrations extends Action
|
||||
throw new \Exception('Bucket not found');
|
||||
}
|
||||
|
||||
$path = $this->deviceForFiles->getPath($bucketId . '/' . $this->sanitizeFilename($filename) . '.csv');
|
||||
$extension = $migration->getAttribute('destination') === DestinationJSON::getName() ? '.json' : '.csv';
|
||||
$path = $this->deviceForFiles->getPath($bucketId . '/' . $this->sanitizeFilename($filename) . $extension);
|
||||
$size = $this->deviceForFiles->getFileSize($path);
|
||||
$mime = $this->deviceForFiles->getFileMimeType($path);
|
||||
$hash = $this->deviceForFiles->getFileHash($path);
|
||||
@@ -632,13 +649,14 @@ class Migrations extends Action
|
||||
$migration->setAttribute('errors', $errors);
|
||||
$migration = $this->updateMigrationDocument($migration, $project, $queueForRealtime);
|
||||
|
||||
$this->sendCSVEmail(
|
||||
$this->sendExportEmail(
|
||||
success: false,
|
||||
project: $project,
|
||||
user: $user,
|
||||
options: $options,
|
||||
queueForMails: $queueForMails,
|
||||
platform: $platform,
|
||||
exportType: $migration->getAttribute('destination') === DestinationJSON::getName() ? 'JSON' : 'CSV',
|
||||
sizeMB: $sizeMB
|
||||
);
|
||||
|
||||
@@ -694,13 +712,14 @@ class Migrations extends Action
|
||||
$migration->setAttribute('options', $options);
|
||||
$this->updateMigrationDocument($migration, $project, $queueForRealtime);
|
||||
|
||||
$this->sendCSVEmail(
|
||||
$this->sendExportEmail(
|
||||
success: true,
|
||||
project: $project,
|
||||
user: $user,
|
||||
options: $options,
|
||||
queueForMails: $queueForMails,
|
||||
platform: $platform,
|
||||
exportType: $migration->getAttribute('destination') === DestinationJSON::getName() ? 'JSON' : 'CSV',
|
||||
downloadUrl: $downloadUrl
|
||||
);
|
||||
}
|
||||
@@ -719,13 +738,14 @@ class Migrations extends Action
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function sendCSVEmail(
|
||||
protected function sendExportEmail(
|
||||
bool $success,
|
||||
Document $project,
|
||||
Document $user,
|
||||
array $options,
|
||||
Mail $queueForMails,
|
||||
array $platform,
|
||||
string $exportType = 'CSV',
|
||||
string $downloadUrl = '',
|
||||
float $sizeMB = 0.0,
|
||||
): void {
|
||||
@@ -745,15 +765,15 @@ class Migrations extends Action
|
||||
? 'success'
|
||||
: 'failure';
|
||||
|
||||
// Get localized email content
|
||||
$subject = $locale->getText("emails.csvExport.{$emailType}.subject");
|
||||
$preview = $locale->getText("emails.csvExport.{$emailType}.preview");
|
||||
$hello = $locale->getText("emails.csvExport.{$emailType}.hello");
|
||||
$body = $locale->getText("emails.csvExport.{$emailType}.body");
|
||||
$footer = $locale->getText("emails.csvExport.{$emailType}.footer");
|
||||
$thanks = $locale->getText("emails.csvExport.{$emailType}.thanks");
|
||||
$signature = $locale->getText("emails.csvExport.{$emailType}.signature");
|
||||
$buttonText = $success ? $locale->getText("emails.csvExport.{$emailType}.buttonText") : '';
|
||||
// Get localized email content — replace {{type}} with export format (CSV/JSON)
|
||||
$subject = \str_replace('{{type}}', $exportType, $locale->getText("emails.dataExport.{$emailType}.subject"));
|
||||
$preview = \str_replace('{{type}}', $exportType, $locale->getText("emails.dataExport.{$emailType}.preview"));
|
||||
$hello = $locale->getText("emails.dataExport.{$emailType}.hello");
|
||||
$body = $locale->getText("emails.dataExport.{$emailType}.body");
|
||||
$footer = $locale->getText("emails.dataExport.{$emailType}.footer");
|
||||
$thanks = $locale->getText("emails.dataExport.{$emailType}.thanks");
|
||||
$signature = $locale->getText("emails.dataExport.{$emailType}.signature");
|
||||
$buttonText = $success ? $locale->getText("emails.dataExport.{$emailType}.buttonText") : '';
|
||||
|
||||
// Build email body using appropriate template
|
||||
$templatePath = $success
|
||||
@@ -769,6 +789,7 @@ class Migrations extends Action
|
||||
->setParam('{{direction}}', $locale->getText('settings.direction'))
|
||||
->setParam('{{project}}', $project->getAttribute('name'))
|
||||
->setParam('{{user}}', $user->getAttribute('name', $user->getAttribute('email')))
|
||||
->setParam('{{type}}', $exportType)
|
||||
->setParam('{{size}}', $success ? '' : (string)$sizeMB);
|
||||
|
||||
if ($success) {
|
||||
@@ -789,6 +810,7 @@ class Migrations extends Action
|
||||
'terms' => $platform['termsUrl'],
|
||||
'privacy' => $platform['privacyUrl'],
|
||||
'platform' => $platform['platformName'],
|
||||
'type' => $exportType,
|
||||
];
|
||||
|
||||
$queueForMails
|
||||
|
||||
@@ -208,7 +208,7 @@ class StatsResources extends Action
|
||||
{
|
||||
$totalFiles = 0;
|
||||
$totalStorage = 0;
|
||||
$this->foreachDocument($dbForProject, 'buckets', [], function ($bucket) use ($dbForProject, $dbForLogs, $region, &$totalFiles, &$totalStorage) {
|
||||
$this->foreachDocument($dbForProject, 'buckets', [], function ($bucket) use ($dbForProject, $region, &$totalFiles, &$totalStorage) {
|
||||
try {
|
||||
$files = $dbForProject->count('bucket_' . $bucket->getSequence());
|
||||
} catch (Throwable $th) {
|
||||
|
||||
@@ -140,7 +140,7 @@ class StatsUsage extends Action
|
||||
|
||||
/**
|
||||
* @param Message $message
|
||||
* @param callable(): Database $getProjectDB
|
||||
* @param callable(Document): Database $getProjectDB
|
||||
* @param callable(): Database $getLogsDB
|
||||
* @param Registry $register
|
||||
* @return void
|
||||
@@ -212,7 +212,7 @@ class StatsUsage extends Action
|
||||
* @param Document $project
|
||||
* @param Document $document
|
||||
* @param array $metrics
|
||||
* @param callable(): Database $getProjectDB
|
||||
* @param callable(Document): Database $getProjectDB
|
||||
* @param string $databaseType Database type from context
|
||||
* @return void
|
||||
*/
|
||||
@@ -394,7 +394,7 @@ class StatsUsage extends Action
|
||||
|
||||
/**
|
||||
* Commit stats to DB
|
||||
* @param callable(): Database $getProjectDB
|
||||
* @param callable(Document): Database $getProjectDB
|
||||
* @return void
|
||||
*/
|
||||
public function commitToDb(callable $getProjectDB): void
|
||||
@@ -459,7 +459,7 @@ class StatsUsage extends Action
|
||||
/**
|
||||
* Sort by unique index key reduce locks/deadlocks
|
||||
*/
|
||||
usort($projectStats['stats'], function ($a, $b) use ($sequence) {
|
||||
usort($projectStats['stats'], function ($a, $b) {
|
||||
// Metric DESC
|
||||
$cmp = strcmp($b['metric'], $a['metric']);
|
||||
if ($cmp !== 0) {
|
||||
|
||||
@@ -233,7 +233,7 @@ class Webhooks extends Action
|
||||
$template->setParam('{{webhook}}', $webhook->getAttribute('name'));
|
||||
$template->setParam('{{project}}', $project->getAttribute('name'));
|
||||
$template->setParam('{{url}}', $webhook->getAttribute('url'));
|
||||
$template->setParam('{{error}}', $curlError ?? 'The server returned ' . $statusCode . ' status code');
|
||||
$template->setParam('{{error}}', 'The server returned ' . $statusCode . ' status code');
|
||||
$template->setParam('{{path}}', "/console/project-$region-$projectId/settings/webhooks/$webhookId");
|
||||
$template->setParam('{{attempts}}', $attempts);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace Appwrite\Promises;
|
||||
|
||||
/** @phpstan-consistent-constructor */
|
||||
abstract class Promise
|
||||
{
|
||||
protected const STATE_PENDING = 1;
|
||||
|
||||
@@ -197,7 +197,7 @@ class Method
|
||||
|
||||
public function isHidden(): bool|array
|
||||
{
|
||||
return $this->hide ?? false;
|
||||
return $this->hide;
|
||||
}
|
||||
|
||||
public function isPackaging(): bool
|
||||
|
||||
@@ -204,9 +204,6 @@ abstract class Format
|
||||
*
|
||||
* Get services value
|
||||
*
|
||||
* @param array $services
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function getServices(): array
|
||||
{
|
||||
|
||||
@@ -79,8 +79,8 @@ class OpenAPI3 extends Format
|
||||
$output['components']['securitySchemes']['Key']['x-appwrite'] = ['demo' => '<YOUR_API_KEY>'];
|
||||
}
|
||||
|
||||
if (isset($output['securityDefinitions']['JWT'])) {
|
||||
$output['securityDefinitions']['JWT']['x-appwrite'] = ['demo' => '<YOUR_JWT>'];
|
||||
if (isset($output['components']['securitySchemes']['JWT'])) {
|
||||
$output['components']['securitySchemes']['JWT']['x-appwrite'] = ['demo' => '<YOUR_JWT>'];
|
||||
}
|
||||
|
||||
if (isset($output['components']['securitySchemes']['Locale'])) {
|
||||
@@ -99,7 +99,7 @@ class OpenAPI3 extends Format
|
||||
|
||||
$sdk = $route->getLabel('sdk', false);
|
||||
|
||||
if (empty($sdk)) {
|
||||
if ($sdk === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -125,7 +125,9 @@ class OpenAPI3 extends Format
|
||||
|
||||
$namespace = $sdk->getNamespace() ?? 'default';
|
||||
|
||||
$desc ??= '';
|
||||
if ($desc === null) {
|
||||
$desc = '';
|
||||
}
|
||||
$descContents = \str_ends_with($desc, '.md') ? \file_get_contents($desc) : $desc;
|
||||
|
||||
$temp = [
|
||||
@@ -163,7 +165,7 @@ class OpenAPI3 extends Format
|
||||
];
|
||||
}
|
||||
|
||||
if (!empty($additionalMethods)) {
|
||||
if (\is_array($additionalMethods) && \count($additionalMethods) > 0) {
|
||||
$temp['x-appwrite']['methods'] = [];
|
||||
foreach ($additionalMethods as $methodObj) {
|
||||
/** @var Method $methodObj */
|
||||
@@ -329,7 +331,7 @@ class OpenAPI3 extends Format
|
||||
|
||||
if (($response->getCode() ?? 500) === 204) {
|
||||
$temp['responses'][(string)$response->getCode() ?? '500']['description'] = 'No content';
|
||||
unset($temp['responses'][(string)$response->getCode() ?? '500']['schema']);
|
||||
unset($temp['responses'][(string)$response->getCode() ?? '500']['content']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,7 +385,7 @@ class OpenAPI3 extends Format
|
||||
$validator = $validator->getValidator();
|
||||
}
|
||||
|
||||
$class = !empty($validator)
|
||||
$class = $validator instanceof Validator
|
||||
? \get_class($validator)
|
||||
: '';
|
||||
|
||||
@@ -431,7 +433,7 @@ 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\DatetimeValidator::class:
|
||||
case \Utopia\Database\Validator\Datetime::class:
|
||||
$node['schema']['type'] = $validator->getType();
|
||||
$node['schema']['format'] = 'datetime';
|
||||
$node['schema']['x-example'] = ($param['example'] ?? '') ?: Model::TYPE_DATETIME_EXAMPLE;
|
||||
@@ -463,7 +465,6 @@ class OpenAPI3 extends Format
|
||||
$node['schema']['x-example'] = ($param['example'] ?? '') ?: 'https://example.com';
|
||||
break;
|
||||
case \Utopia\Validator\JSON::class:
|
||||
case \Utopia\Validator\Mock::class:
|
||||
case \Utopia\Validator\Assoc::class:
|
||||
$param['default'] = (empty($param['default'])) ? new \stdClass() : $param['default'];
|
||||
$node['schema']['type'] = 'object';
|
||||
@@ -563,12 +564,6 @@ class OpenAPI3 extends Format
|
||||
$node['schema']['x-example'] = $param['example'];
|
||||
}
|
||||
break;
|
||||
case \Utopia\Validator\Length::class:
|
||||
$node['schema']['type'] = $validator->getType();
|
||||
if (!empty($param['example'])) {
|
||||
$node['schema']['x-example'] = $param['example'];
|
||||
}
|
||||
break;
|
||||
case \Utopia\Validator\WhiteList::class:
|
||||
if ($array) {
|
||||
$validator = $validator->getValidator();
|
||||
|
||||
@@ -96,10 +96,9 @@ class Swagger2 extends Format
|
||||
|
||||
$scope = $route->getLabel('scope', '');
|
||||
|
||||
/** @var Method $sdk */
|
||||
$sdk = $route->getLabel('sdk', false);
|
||||
|
||||
if (empty($sdk)) {
|
||||
if ($sdk === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -127,7 +126,9 @@ class Swagger2 extends Format
|
||||
$sdkPlatforms = array_values(array_unique($sdkPlatforms));
|
||||
$namespace = $sdk->getNamespace() ?? 'default';
|
||||
|
||||
$desc ??= '';
|
||||
if ($desc === null) {
|
||||
$desc = '';
|
||||
}
|
||||
$descContents = \str_ends_with($desc, '.md') ? \file_get_contents($desc) : $desc;
|
||||
|
||||
$temp = [
|
||||
@@ -171,7 +172,7 @@ class Swagger2 extends Format
|
||||
$temp['produces'][] = $produces;
|
||||
}
|
||||
|
||||
if (!empty($additionalMethods)) {
|
||||
if (\is_array($additionalMethods) && \count($additionalMethods) > 0) {
|
||||
$temp['x-appwrite']['methods'] = [];
|
||||
foreach ($additionalMethods as $methodObj) {
|
||||
/** @var Method $methodObj */
|
||||
@@ -388,7 +389,7 @@ class Swagger2 extends Format
|
||||
$validator = $validator->getValidator();
|
||||
}
|
||||
|
||||
$class = !empty($validator)
|
||||
$class = $validator instanceof Validator
|
||||
? \get_class($validator)
|
||||
: '';
|
||||
|
||||
@@ -436,7 +437,7 @@ class Swagger2 extends Format
|
||||
$node['type'] = $validator->getType();
|
||||
$node['x-example'] = ($param['example'] ?? '') ?: '<' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . '>';
|
||||
break;
|
||||
case \Utopia\Database\Validator\DatetimeValidator::class:
|
||||
case \Utopia\Database\Validator\Datetime::class:
|
||||
$node['type'] = $validator->getType();
|
||||
$node['format'] = 'datetime';
|
||||
$node['x-example'] = ($param['example'] ?? '') ?: Model::TYPE_DATETIME_EXAMPLE;
|
||||
@@ -479,7 +480,6 @@ class Swagger2 extends Format
|
||||
}
|
||||
break;
|
||||
case \Utopia\Validator\JSON::class:
|
||||
case \Utopia\Validator\Mock::class:
|
||||
case \Utopia\Validator\Assoc::class:
|
||||
$node['type'] = 'object';
|
||||
$node['default'] = (empty($param['default'])) ? new \stdClass() : $param['default'];
|
||||
@@ -552,12 +552,6 @@ class Swagger2 extends Format
|
||||
$node['x-example'] = $param['example'];
|
||||
}
|
||||
break;
|
||||
case \Utopia\Validator\Length::class:
|
||||
$node['type'] = $validator->getType();
|
||||
if (!empty($param['example'])) {
|
||||
$node['x-example'] = $param['example'];
|
||||
}
|
||||
break;
|
||||
case \Utopia\Validator\WhiteList::class:
|
||||
if ($array) {
|
||||
$validator = $validator->getValidator();
|
||||
|
||||
@@ -86,7 +86,6 @@ class User extends Document
|
||||
/**
|
||||
* Check if user is anonymous.
|
||||
*
|
||||
* @param Document $this
|
||||
* @return bool
|
||||
*/
|
||||
public function isAnonymous(): bool
|
||||
@@ -153,7 +152,6 @@ class User extends Document
|
||||
/**
|
||||
* Verify session and check that its not expired.
|
||||
*
|
||||
* @param array<Document> $sessions
|
||||
* @param string $secret
|
||||
*
|
||||
* @return bool|string
|
||||
|
||||
@@ -11,34 +11,48 @@ class V21 extends Filter
|
||||
public function parse(array $content, string $model): array
|
||||
{
|
||||
return match ($model) {
|
||||
Response::MODEL_USER => $this->parseUser($content),
|
||||
Response::MODEL_USER_LIST => $this->handleList(
|
||||
$content,
|
||||
'users',
|
||||
fn ($item) => $this->parseUser($item),
|
||||
),
|
||||
Response::MODEL_ACCOUNT => $this->parseUser($content),
|
||||
Response::MODEL_SITE => $this->parseSite($content),
|
||||
Response::MODEL_SITE_LIST => $this->handleList(
|
||||
$content,
|
||||
"sites",
|
||||
'sites',
|
||||
fn ($item) => $this->parseSite($item),
|
||||
),
|
||||
Response::MODEL_FUNCTION => $this->parseFunction($content),
|
||||
Response::MODEL_FUNCTION_LIST => $this->handleList(
|
||||
$content,
|
||||
"functions",
|
||||
'functions',
|
||||
fn ($item) => $this->parseFunction($item),
|
||||
),
|
||||
Response::MODEL_DOCUMENT => $this->parseDocument($content),
|
||||
Response::MODEL_DOCUMENT_LIST => $this->handleList(
|
||||
$content,
|
||||
"documents",
|
||||
'documents',
|
||||
fn ($item) => $this->parseDocument($item),
|
||||
),
|
||||
Response::MODEL_ROW => $this->parseRow($content),
|
||||
Response::MODEL_ROW_LIST => $this->handleList(
|
||||
$content,
|
||||
"rows",
|
||||
'rows',
|
||||
fn ($item) => $this->parseRow($item),
|
||||
),
|
||||
default => $content,
|
||||
};
|
||||
}
|
||||
|
||||
protected function parseUser(array $content): array
|
||||
{
|
||||
unset($content['impersonator']);
|
||||
unset($content['impersonatorUserId']);
|
||||
return $content;
|
||||
}
|
||||
|
||||
protected function parseSite(array $content): array
|
||||
{
|
||||
$content = $this->parseSpecs($content);
|
||||
|
||||
@@ -29,8 +29,8 @@ class DatabasesStringTypesTest extends Scope
|
||||
protected function setupDatabaseAndCollection(): array
|
||||
{
|
||||
$cacheKey = $this->getProject()['$id'] ?? 'default';
|
||||
if (!empty(static::$setupCache[$cacheKey])) {
|
||||
return static::$setupCache[$cacheKey];
|
||||
if (!empty(self::$setupCache[$cacheKey])) {
|
||||
return self::$setupCache[$cacheKey];
|
||||
}
|
||||
|
||||
$projectId = $this->getProject()['$id'];
|
||||
@@ -135,12 +135,12 @@ class DatabasesStringTypesTest extends Scope
|
||||
// Wait for all attributes to be available
|
||||
$this->waitForAllAttributes($databaseId, $collectionId);
|
||||
|
||||
static::$setupCache[$cacheKey] = [
|
||||
self::$setupCache[$cacheKey] = [
|
||||
'databaseId' => $databaseId,
|
||||
'collectionId' => $collectionId,
|
||||
];
|
||||
|
||||
return static::$setupCache[$cacheKey];
|
||||
return self::$setupCache[$cacheKey];
|
||||
}
|
||||
|
||||
public function testCreateDatabase(): void
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user