Merge branch '1.8.x' into ser-1127

This commit is contained in:
Hemachandar
2026-03-12 12:20:45 +05:30
23 changed files with 1470 additions and 427 deletions
+139 -152
View File
@@ -53,8 +53,14 @@ jobs:
with:
submodules: recursive
- name: Login to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- name: Build Appwrite
uses: docker/build-push-action@v6
@@ -73,7 +79,7 @@ jobs:
VERSION=dev
- name: Cache Docker Image
uses: actions/cache@v4
uses: actions/cache@v5
with:
key: ${{ env.CACHE_KEY }}
path: /tmp/${{ env.IMAGE }}.tar
@@ -91,21 +97,24 @@ jobs:
uses: actions/checkout@v6
- name: Load Cache
uses: actions/cache@v4
uses: actions/cache@v5
with:
key: ${{ env.CACHE_KEY }}
path: /tmp/${{ env.IMAGE }}.tar
fail-on-cache-miss: true
- name: Login to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Load and Start Appwrite
timeout-minutes: 3
timeout-minutes: 5
run: |
docker load --input /tmp/${{ env.IMAGE }}.tar
docker compose up -d
until docker compose exec -T appwrite doctor > /dev/null 2>&1; do
echo "Waiting for Appwrite to be ready..."
sleep 2
done
docker compose pull --quiet --ignore-buildable
docker compose up -d --quiet-pull --wait
- name: Environment Variables
run: docker compose exec -T appwrite vars
@@ -114,7 +123,7 @@ jobs:
uses: itznotabug/php-retry@v3
with:
max_attempts: 2
retry_wait_seconds: 300
retry_wait_seconds: 60
timeout_minutes: 15
job_id: ${{ job.check_run_id }}
github_token: ${{ secrets.GITHUB_TOKEN }}
@@ -136,21 +145,24 @@ jobs:
uses: actions/checkout@v6
- name: Load Cache
uses: actions/cache@v4
uses: actions/cache@v5
with:
key: ${{ env.CACHE_KEY }}
path: /tmp/${{ env.IMAGE }}.tar
fail-on-cache-miss: true
- name: Login to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Load and Start Appwrite
timeout-minutes: 3
timeout-minutes: 5
run: |
docker load --input /tmp/${{ env.IMAGE }}.tar
docker compose up -d
until docker compose exec -T appwrite doctor > /dev/null 2>&1; do
echo "Waiting for Appwrite to be ready..."
sleep 2
done
docker compose pull --quiet --ignore-buildable
docker compose up -d --quiet-pull --wait
- name: Wait for Open Runtimes
timeout-minutes: 3
@@ -164,7 +176,7 @@ jobs:
uses: itznotabug/php-retry@v3
with:
max_attempts: 2
retry_wait_seconds: 300
retry_wait_seconds: 60
timeout_minutes: 15
job_id: ${{ job.check_run_id }}
github_token: ${{ secrets.GITHUB_TOKEN }}
@@ -172,7 +184,7 @@ jobs:
command: >-
docker compose exec -T
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}"
appwrite test /usr/src/code/tests/e2e/General --debug
appwrite test /usr/src/code/tests/e2e/General
- name: Failure Logs
if: failure()
@@ -223,7 +235,7 @@ jobs:
uses: actions/checkout@v6
- name: Load Cache
uses: actions/cache@v4
uses: actions/cache@v5
with:
key: ${{ env.CACHE_KEY }}
path: /tmp/${{ env.IMAGE }}.tar
@@ -234,7 +246,7 @@ jobs:
run: |
DB_ADAPTER_LOWER=$(echo "${{ matrix.db_adapter }}" | tr 'A-Z' 'a-z')
echo "COMPOSE_PROFILES=${DB_ADAPTER_LOWER}" >> $GITHUB_ENV
if [ "${{ matrix.db_adapter }}" = "MARIADB" ]; then
echo "_APP_DB_ADAPTER=mariadb" >> $GITHUB_ENV
echo "_APP_DB_HOST=mariadb" >> $GITHUB_ENV
@@ -249,17 +261,22 @@ jobs:
echo "_APP_DB_PORT=5432" >> $GITHUB_ENV
fi
- name: Login to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Load and Start Appwrite
timeout-minutes: 3
timeout-minutes: 5
env:
_APP_BROWSER_HOST: http://invalid-browser/v1
_APP_DATABASE_SHARED_TABLES: ""
_APP_DATABASE_SHARED_TABLES_V1: ""
run: |
docker load --input /tmp/${{ env.IMAGE }}.tar
docker compose up -d
until docker compose exec -T appwrite doctor > /dev/null 2>&1; do
echo "Waiting for Appwrite to be ready..."
sleep 2
done
docker compose pull --quiet --ignore-buildable
docker compose up -d --quiet-pull --wait
- name: Wait for Open Runtimes
timeout-minutes: 3
@@ -273,13 +290,12 @@ jobs:
uses: itznotabug/php-retry@v3
with:
max_attempts: 2
retry_wait_seconds: 300
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: |
echo "Using project tables"
SERVICE_PATH="/usr/src/code/tests/e2e/Services/${{ matrix.service }}"
# Services that rely on sequential test method execution (shared static state)
@@ -288,14 +304,7 @@ jobs:
Databases|Functions|Realtime) FUNCTIONAL_FLAG="" ;;
esac
echo "Running with paratest (parallel) for: ${{ matrix.service }} ${FUNCTIONAL_FLAG:+(functional)}"
docker compose exec -T \
-e _APP_DATABASE_SHARED_TABLES="" \
-e _APP_DATABASE_SHARED_TABLES_V1="" \
-e _APP_DB_ADAPTER="${{ env._APP_DB_ADAPTER }}" \
-e _APP_DB_HOST="${{ env._APP_DB_HOST }}" \
-e _APP_DB_PORT="${{ env._APP_DB_PORT }}" \
-e _APP_DB_SCHEMA=appwrite \
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \
appwrite vendor/bin/paratest --processes $(nproc) $FUNCTIONAL_FLAG "$SERVICE_PATH" --exclude-group abuseEnabled --exclude-group screenshots --exclude-group ciIgnore --log-junit tests/e2e/Services/${{ matrix.service }}/junit.xml
@@ -350,21 +359,27 @@ jobs:
uses: actions/checkout@v6
- name: Load Cache
uses: actions/cache@v4
uses: actions/cache@v5
with:
key: ${{ env.CACHE_KEY }}
path: /tmp/${{ env.IMAGE }}.tar
fail-on-cache-miss: true
- name: Login to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Load and Start Appwrite
timeout-minutes: 3
timeout-minutes: 5
env:
_APP_DATABASE_SHARED_TABLES: database_db_main
_APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.tables-mode == 'Shared V1' && 'database_db_main' || '' }}
run: |
docker load --input /tmp/${{ env.IMAGE }}.tar
docker compose up -d
until docker compose exec -T appwrite doctor > /dev/null 2>&1; do
echo "Waiting for Appwrite to be ready..."
sleep 2
done
docker compose pull --quiet --ignore-buildable
docker compose up -d --quiet-pull --wait
- name: Wait for Open Runtimes
timeout-minutes: 3
@@ -378,22 +393,12 @@ jobs:
uses: itznotabug/php-retry@v3
with:
max_attempts: 2
retry_wait_seconds: 300
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: |
if [ "${{ matrix.tables-mode }}" == "Shared V1" ]; then
echo "Using shared tables V1"
export _APP_DATABASE_SHARED_TABLES=database_db_main
export _APP_DATABASE_SHARED_TABLES_V1=database_db_main
elif [ "${{ matrix.tables-mode }}" == "Shared V2" ]; then
echo "Using shared tables V2"
export _APP_DATABASE_SHARED_TABLES=database_db_main
export _APP_DATABASE_SHARED_TABLES_V1=
fi
SERVICE_PATH="/usr/src/code/tests/e2e/Services/${{ matrix.service }}"
# Services that rely on sequential test method execution (shared static state)
@@ -403,8 +408,6 @@ jobs:
esac
docker compose exec -T \
-e _APP_DATABASE_SHARED_TABLES \
-e _APP_DATABASE_SHARED_TABLES_V1 \
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \
appwrite vendor/bin/paratest --processes $(nproc) $FUNCTIONAL_FLAG "$SERVICE_PATH" --exclude-group abuseEnabled --exclude-group screenshots --exclude-group ciIgnore --log-junit tests/e2e/Services/${{ matrix.service }}/junit.xml
@@ -417,7 +420,7 @@ jobs:
docker compose logs openruntimes-executor
e2e_abuse_enabled:
name: E2E Service Test (Abuse enabled)
name: E2E Service Test (Abuse)
runs-on: ubuntu-latest
needs: setup
permissions:
@@ -428,42 +431,42 @@ jobs:
uses: actions/checkout@v6
- name: Load Cache
uses: actions/cache@v4
uses: actions/cache@v5
with:
key: ${{ env.CACHE_KEY }}
path: /tmp/${{ env.IMAGE }}.tar
fail-on-cache-miss: true
- name: Login to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Load and Start Appwrite
timeout-minutes: 3
timeout-minutes: 5
env:
_APP_OPTIONS_ABUSE: enabled
_APP_DATABASE_SHARED_TABLES: ""
_APP_DATABASE_SHARED_TABLES_V1: ""
run: |
docker load --input /tmp/${{ env.IMAGE }}.tar
sed -i 's/_APP_OPTIONS_ABUSE=disabled/_APP_OPTIONS_ABUSE=enabled/' .env
docker compose up -d
until docker compose exec -T appwrite doctor > /dev/null 2>&1; do
echo "Waiting for Appwrite to be ready..."
sleep 2
done
docker compose pull --quiet --ignore-buildable
docker compose up -d --quiet-pull --wait
- name: Run Projects tests in dedicated table mode
- name: Run abuse-enabled tests in dedicated table mode
uses: itznotabug/php-retry@v3
with:
max_attempts: 2
retry_wait_seconds: 300
retry_wait_seconds: 60
timeout_minutes: 15
job_id: ${{ job.check_run_id }}
github_token: ${{ secrets.GITHUB_TOKEN }}
test_dir: tests/e2e/Services/Projects
command: |
echo "Using project tables"
export _APP_DATABASE_SHARED_TABLES=
export _APP_DATABASE_SHARED_TABLES_V1=
docker compose exec -T \
-e _APP_DATABASE_SHARED_TABLES \
-e _APP_DATABASE_SHARED_TABLES_V1 \
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \
appwrite test /usr/src/code/tests/e2e/Services/Projects --debug --group=abuseEnabled
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()
@@ -474,7 +477,7 @@ jobs:
docker compose logs openruntimes-executor
e2e_abuse_enabled_shared_mode:
name: E2E Shared Mode Service Test (Abuse enabled)
name: E2E Shared Mode Service Test (Abuse)
runs-on: ubuntu-latest
needs: [ setup, check_database_changes ]
if: needs.check_database_changes.outputs.database_changed == 'true'
@@ -493,48 +496,42 @@ jobs:
uses: actions/checkout@v6
- name: Load Cache
uses: actions/cache@v4
uses: actions/cache@v5
with:
key: ${{ env.CACHE_KEY }}
path: /tmp/${{ env.IMAGE }}.tar
fail-on-cache-miss: true
- name: Login to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Load and Start Appwrite
timeout-minutes: 3
timeout-minutes: 5
env:
_APP_OPTIONS_ABUSE: enabled
_APP_DATABASE_SHARED_TABLES: database_db_main
_APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.tables-mode == 'Shared V1' && 'database_db_main' || '' }}
run: |
docker load --input /tmp/${{ env.IMAGE }}.tar
sed -i 's/_APP_OPTIONS_ABUSE=disabled/_APP_OPTIONS_ABUSE=enabled/' .env
docker compose up -d
until docker compose exec -T appwrite doctor > /dev/null 2>&1; do
echo "Waiting for Appwrite to be ready..."
sleep 2
done
docker compose pull --quiet --ignore-buildable
docker compose up -d --quiet-pull --wait
- name: Run Projects tests in ${{ matrix.tables-mode }} table mode
- name: Run abuse-enabled tests in ${{ matrix.tables-mode }} table mode
uses: itznotabug/php-retry@v3
with:
max_attempts: 2
retry_wait_seconds: 300
retry_wait_seconds: 60
timeout_minutes: 15
job_id: ${{ job.check_run_id }}
github_token: ${{ secrets.GITHUB_TOKEN }}
test_dir: tests/e2e/Services/Projects
command: |
if [ "${{ matrix.tables-mode }}" == "Shared V1" ]; then
echo "Using shared tables V1"
export _APP_DATABASE_SHARED_TABLES=database_db_main
export _APP_DATABASE_SHARED_TABLES_V1=database_db_main
elif [ "${{ matrix.tables-mode }}" == "Shared V2" ]; then
echo "Using shared tables V2"
export _APP_DATABASE_SHARED_TABLES=database_db_main
export _APP_DATABASE_SHARED_TABLES_V1=
fi
docker compose exec -T \
-e _APP_DATABASE_SHARED_TABLES \
-e _APP_DATABASE_SHARED_TABLES_V1 \
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \
appwrite test /usr/src/code/tests/e2e/Services/Projects --debug --group=abuseEnabled
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()
@@ -556,22 +553,27 @@ jobs:
uses: actions/checkout@v6
- name: Load Cache
uses: actions/cache@v4
uses: actions/cache@v5
with:
key: ${{ env.CACHE_KEY }}
path: /tmp/${{ env.IMAGE }}.tar
fail-on-cache-miss: true
- name: Login to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Load and Start Appwrite
timeout-minutes: 3
timeout-minutes: 5
env:
_APP_DATABASE_SHARED_TABLES: ""
_APP_DATABASE_SHARED_TABLES_V1: ""
run: |
docker load --input /tmp/${{ env.IMAGE }}.tar
sed -i 's/_APP_OPTIONS_ABUSE=disabled/_APP_OPTIONS_ABUSE=enabled/' .env
docker compose up -d
until docker compose exec -T appwrite doctor > /dev/null 2>&1; do
echo "Waiting for Appwrite to be ready..."
sleep 2
done
docker compose pull --quiet --ignore-buildable
docker compose up -d --quiet-pull --wait
- name: Wait for Open Runtimes
timeout-minutes: 3
@@ -585,22 +587,15 @@ jobs:
uses: itznotabug/php-retry@v3
with:
max_attempts: 2
retry_wait_seconds: 300
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: |
echo "Keeping original value of _APP_BROWSER_HOST"
echo "Using project tables"
export _APP_DATABASE_SHARED_TABLES=
export _APP_DATABASE_SHARED_TABLES_V1=
docker compose exec -T \
-e _APP_DATABASE_SHARED_TABLES \
-e _APP_DATABASE_SHARED_TABLES_V1 \
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \
appwrite test /usr/src/code/tests/e2e/Services/Sites --debug --group=screenshots
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()
@@ -630,22 +625,27 @@ jobs:
uses: actions/checkout@v6
- name: Load Cache
uses: actions/cache@v4
uses: actions/cache@v5
with:
key: ${{ env.CACHE_KEY }}
path: /tmp/${{ env.IMAGE }}.tar
fail-on-cache-miss: true
- name: Login to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Load and Start Appwrite
timeout-minutes: 3
timeout-minutes: 5
env:
_APP_DATABASE_SHARED_TABLES: database_db_main
_APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.tables-mode == 'Shared V1' && 'database_db_main' || '' }}
run: |
docker load --input /tmp/${{ env.IMAGE }}.tar
sed -i 's/_APP_OPTIONS_ABUSE=disabled/_APP_OPTIONS_ABUSE=enabled/' .env
docker compose up -d
until docker compose exec -T appwrite doctor > /dev/null 2>&1; do
echo "Waiting for Appwrite to be ready..."
sleep 2
done
docker compose pull --quiet --ignore-buildable
docker compose up -d --quiet-pull --wait
- name: Wait for Open Runtimes
timeout-minutes: 3
@@ -659,28 +659,15 @@ jobs:
uses: itznotabug/php-retry@v3
with:
max_attempts: 2
retry_wait_seconds: 300
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: |
echo "Keeping original value of _APP_BROWSER_HOST"
if [ "${{ matrix.tables-mode }}" == "Shared V1" ]; then
echo "Using shared tables V1"
export _APP_DATABASE_SHARED_TABLES=database_db_main
export _APP_DATABASE_SHARED_TABLES_V1=database_db_main
elif [ "${{ matrix.tables-mode }}" == "Shared V2" ]; then
echo "Using shared tables V2"
export _APP_DATABASE_SHARED_TABLES=database_db_main
export _APP_DATABASE_SHARED_TABLES_V1=
fi
docker compose exec -T \
-e _APP_DATABASE_SHARED_TABLES \
-e _APP_DATABASE_SHARED_TABLES_V1 \
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \
appwrite test /usr/src/code/tests/e2e/Services/Sites --debug --group=screenshots
command: >-
docker compose exec -T
-e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}"
appwrite test /usr/src/code/tests/e2e/Services/Sites --group=screenshots
- name: Failure Logs
if: failure()
+1 -1
View File
@@ -70,7 +70,7 @@ RUN chmod +x /usr/local/bin/doctor && \
chmod +x /usr/local/bin/sdks && \
chmod +x /usr/local/bin/specs && \
chmod +x /usr/local/bin/ssl && \
chmod +x /usr/local/bin/time-travel && \
chmod +x /usr/local/bin/task-time-travel && \
chmod +x /usr/local/bin/screenshot && \
chmod +x /usr/local/bin/test && \
chmod +x /usr/local/bin/upgrade && \
+8 -5
View File
@@ -1560,12 +1560,15 @@ Http::patch('/v1/users/:userId/phone')
$oldPhone = $user->getAttribute('phone');
// Store null instead of empty string so unique constraint allows multiple users without phone
$phoneValue = $number !== '' ? $number : null;
$user
->setAttribute('phone', $number)
->setAttribute('phone', $phoneValue)
->setAttribute('phoneVerification', false)
;
if (\strlen($number) !== 0) {
if ($number !== '') {
$target = $dbForProject->findOne('targets', [
Query::equal('identifier', [$number]),
]);
@@ -1577,7 +1580,7 @@ Http::patch('/v1/users/:userId/phone')
try {
$user = $dbForProject->updateDocument('users', $user->getId(), new Document([
'phone' => $user->getAttribute('phone'),
'phone' => $phoneValue,
'phoneVerification' => $user->getAttribute('phoneVerification'),
]));
/**
@@ -1586,14 +1589,14 @@ Http::patch('/v1/users/:userId/phone')
$oldTarget = $user->find('identifier', $oldPhone, 'targets');
if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) {
if (\strlen($number) !== 0) {
if ($number !== '') {
$dbForProject->updateDocument('targets', $oldTarget->getId(), new Document(['identifier' => $number]));
$oldTarget->setAttribute('identifier', $number);
} else {
$dbForProject->deleteDocument('targets', $oldTarget->getId());
}
} else {
if (\strlen($number) !== 0) {
if ($number !== '') {
$target = $dbForProject->createDocument('targets', new Document([
'$permissions' => [
Permission::read(Role::user($user->getId())),
+3
View File
@@ -1414,6 +1414,9 @@ Http::error()
$sdk = $route?->getLabel("sdk", false);
$action = 'UNKNOWN_NAMESPACE.UNKNOWN.METHOD';
if (!empty($sdk)) {
if (\is_array($sdk)) {
$sdk = $sdk[0];
}
/** @var \Appwrite\SDK\Method $sdk */
$action = $sdk->getNamespace() . '.' . $sdk->getMethodName();
} elseif ($route === null) {
+3
View File
@@ -581,6 +581,9 @@ $http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, Swool
$action = 'UNKNOWN_NAMESPACE.UNKNOWN.METHOD';
if (!empty($sdk)) {
if (\is_array($sdk)) {
$sdk = $sdk[0];
}
/** @var Appwrite\SDK\Method $sdk */
$action = $sdk->getNamespace() . '.' . $sdk->getMethodName();
} elseif ($route === null) {
+6
View File
@@ -362,6 +362,12 @@ const METRIC_AVATARS_SCREENSHOTS_GENERATED = 'avatars.screenshotsGenerated';
const METRIC_FUNCTIONS_RUNTIME = 'functions.runtimes.{runtime}';
const METRIC_SITES_FRAMEWORK = 'sites.frameworks.{framework}';
// Realtime metrics
const METRIC_REALTIME_CONNECTIONS = 'realtime.connections';
const METRIC_REALTIME_CONNECTIONS_MESSAGES_SENT = 'realtime.messages.sent';
const METRIC_REALTIME_INBOUND = 'realtime.inbound';
const METRIC_REALTIME_OUTBOUND = 'realtime.outbound';
// Resource types
const RESOURCE_TYPE_PROJECTS = 'projects';
const RESOURCE_TYPE_FUNCTIONS = 'functions';
+1
View File
@@ -420,6 +420,7 @@ $register->set('smtp', function () {
$mail->Password = $password;
$mail->SMTPSecure = System::getEnv('_APP_SMTP_SECURE', '');
$mail->SMTPAutoTLS = false;
$mail->SMTPKeepAlive = true;
$mail->CharSet = 'UTF-8';
$mail->Timeout = 10; /* Connection timeout */
$mail->getSMTPInstance()->Timelimit = 30; /* Timeout for each individual SMTP command (e.g. HELO, EHLO, etc.) */
+6 -1
View File
@@ -5,4 +5,9 @@ use Utopia\Span\Span;
use Utopia\Span\Storage;
Span::setStorage(new Storage\Coroutine());
Span::addExporter(new Exporter\Pretty());
Span::addExporter(new Exporter\Pretty(), function (Span $span): bool {
if (\str_starts_with($span->getAction(), 'listener.')) {
return $span->getError() !== null;
}
return true;
});
+90 -9
View File
@@ -224,6 +224,13 @@ if (!function_exists('getTelemetry')) {
}
}
if (!function_exists('triggerStats')) {
function triggerStats(array $event, string $projectId): void
{
return;
}
}
$realtime = getRealtime();
/**
@@ -548,20 +555,41 @@ $server->onWorkerStart(function (int $workerId) use ($server, $register, $stats,
}
$total = 0;
$outboundBytes = 0;
foreach ($groups as $group) {
$data = $event['data'];
$data['subscriptions'] = $group['subscriptions'];
$server->send($group['ids'], json_encode([
$payloadJson = json_encode([
'type' => 'event',
'data' => $data
]));
$total += count($group['ids']);
]);
$server->send($group['ids'], $payloadJson);
$count = count($group['ids']);
$total += $count;
$outboundBytes += strlen($payloadJson) * $count;
}
if ($total > 0) {
$register->get('telemetry.messageSentCounter')->add($total);
$stats->incr($event['project'], 'messages', $total);
$projectId = $event['project'] ?? null;
if (!empty($projectId)) {
$metrics = [
METRIC_REALTIME_CONNECTIONS_MESSAGES_SENT => $total,
];
if ($outboundBytes > 0) {
$metrics[METRIC_REALTIME_OUTBOUND] = $outboundBytes;
}
triggerStats($metrics, $projectId);
}
}
});
} catch (Throwable $th) {
@@ -638,6 +666,12 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
throw new Exception(Exception::REALTIME_TOO_MANY_MESSAGES, 'Too many requests');
}
$rawSize = $request->getSize();
triggerStats([
METRIC_REALTIME_INBOUND => $rawSize,
], $project->getId());
/*
* Validate Client Domain - Check to avoid CSRF attack.
* Adding Appwrite API domains to allow XDOMAIN communication.
@@ -692,14 +726,16 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
$user = empty($user->getId()) ? null : $response->output($user, Response::MODEL_ACCOUNT);
$server->send([$connection], json_encode([
$connectedPayloadJson = json_encode([
'type' => 'connected',
'data' => [
'channels' => $names,
'subscriptions' => $mapping,
'user' => $user
]
]));
]);
$server->send([$connection], $connectedPayloadJson);
$register->get('telemetry.connectionCounter')->add(1);
$register->get('telemetry.connectionCreatedCounter')->add(1);
@@ -710,6 +746,12 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
]);
$stats->incr($project->getId(), 'connections');
$stats->incr($project->getId(), 'connectionsTotal');
$connectedOutboundBytes = \strlen($connectedPayloadJson);
triggerStats([METRIC_REALTIME_CONNECTIONS => 1, METRIC_REALTIME_OUTBOUND => $connectedOutboundBytes], $project->getId());
} catch (Throwable $th) {
logError($th, 'realtime', project: $project, user: $logUser, authorization: $authorization);
@@ -751,6 +793,7 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
$authorization = null;
try {
$rawSize = \strlen($message);
$response = new Response(new SwooleResponse());
$projectId = $realtime->connections[$connection]['projectId'] ?? null;
@@ -789,6 +832,13 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
throw new Exception(Exception::REALTIME_TOO_MANY_MESSAGES, 'Too many messages.');
}
// Record realtime inbound bytes for this project
if ($project !== null && !$project->isEmpty()) {
triggerStats([
METRIC_REALTIME_INBOUND => $rawSize,
], $project->getId());
}
$message = json_decode($message, true);
if (is_null($message) || (!array_key_exists('type', $message) && !array_key_exists('data', $message))) {
@@ -802,9 +852,21 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
switch ($message['type']) {
case 'ping':
$server->send([$connection], json_encode([
$pongPayloadJson = json_encode([
'type' => 'pong'
]));
]);
$server->send([$connection], $pongPayloadJson);
if ($project !== null && !$project->isEmpty()) {
$pongOutboundBytes = \strlen($pongPayloadJson);
if ($pongOutboundBytes > 0) {
triggerStats([
METRIC_REALTIME_OUTBOUND => $pongOutboundBytes,
], $project->getId());
}
}
break;
case 'authentication':
@@ -865,14 +927,27 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
}
$user = $response->output($user, Response::MODEL_ACCOUNT);
$server->send([$connection], json_encode([
$authResponsePayloadJson = json_encode([
'type' => 'response',
'data' => [
'to' => 'authentication',
'success' => true,
'user' => $user
]
]));
]);
$server->send([$connection], $authResponsePayloadJson);
if ($project !== null && !$project->isEmpty()) {
$authOutboundBytes = \strlen($authResponsePayloadJson);
if ($authOutboundBytes > 0) {
triggerStats([
METRIC_REALTIME_OUTBOUND => $authOutboundBytes,
], $project->getId());
}
}
break;
@@ -913,6 +988,12 @@ $server->onClose(function (int $connection) use ($realtime, $stats, $register) {
if (array_key_exists($connection, $realtime->connections)) {
$stats->decr($realtime->connections[$connection]['projectId'], 'connectionsTotal');
$register->get('telemetry.connectionCounter')->add(-1);
$projectId = $realtime->connections[$connection]['projectId'];
triggerStats([
METRIC_REALTIME_CONNECTIONS => -1,
], $projectId);
}
$realtime->unsubscribe($connection);
+1 -1
View File
@@ -70,7 +70,7 @@
"utopia-php/locale": "0.8.*",
"utopia-php/logger": "0.6.*",
"utopia-php/messaging": "0.20.*",
"utopia-php/migration": "1.6.*",
"utopia-php/migration": "1.7.*",
"utopia-php/platform": "0.7.*",
"utopia-php/pools": "1.*",
"utopia-php/span": "1.1.*",
Generated
+78 -79
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "1cc64e07484256225f56bd525674c3b8",
"content-hash": "b99693284208ff3d006260a089a4f7b9",
"packages": [
{
"name": "adhocore/jwt",
@@ -3850,16 +3850,16 @@
},
{
"name": "utopia-php/database",
"version": "5.3.7",
"version": "5.3.8",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
"reference": "438cc82af2981cd41ad200dd9b0df5bf00f3046a"
"reference": "4920bb60afb98d4bd81f4d331765716ae1d40255"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/database/zipball/438cc82af2981cd41ad200dd9b0df5bf00f3046a",
"reference": "438cc82af2981cd41ad200dd9b0df5bf00f3046a",
"url": "https://api.github.com/repos/utopia-php/database/zipball/4920bb60afb98d4bd81f4d331765716ae1d40255",
"reference": "4920bb60afb98d4bd81f4d331765716ae1d40255",
"shasum": ""
},
"require": {
@@ -3902,9 +3902,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/database/issues",
"source": "https://github.com/utopia-php/database/tree/5.3.7"
"source": "https://github.com/utopia-php/database/tree/5.3.8"
},
"time": "2026-03-09T04:28:56+00:00"
"time": "2026-03-11T01:03:34+00:00"
},
{
"name": "utopia-php/detector",
@@ -4058,16 +4058,16 @@
},
{
"name": "utopia-php/domains",
"version": "1.0.5",
"version": "1.0.2",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/domains.git",
"reference": "0edf6bb2b07f30db849a267027077bf5abb994c6"
"reference": "b4896a6746f0fbe29dfd5e32f7790bd94c1af1e6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/domains/zipball/0edf6bb2b07f30db849a267027077bf5abb994c6",
"reference": "0edf6bb2b07f30db849a267027077bf5abb994c6",
"url": "https://api.github.com/repos/utopia-php/domains/zipball/b4896a6746f0fbe29dfd5e32f7790bd94c1af1e6",
"reference": "b4896a6746f0fbe29dfd5e32f7790bd94c1af1e6",
"shasum": ""
},
"require": {
@@ -4114,9 +4114,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/domains/issues",
"source": "https://github.com/utopia-php/domains/tree/1.0.5"
"source": "https://github.com/utopia-php/domains/tree/1.0.2"
},
"time": "2026-03-03T09:20:50+00:00"
"time": "2026-02-25T08:18:25+00:00"
},
{
"name": "utopia-php/dsn",
@@ -4517,16 +4517,16 @@
},
{
"name": "utopia-php/migration",
"version": "1.6.3",
"version": "1.7.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/migration.git",
"reference": "c2d016944cb029fa5ff822ceee704785a06ef289"
"reference": "97583ae502e40621ea91a71de19d053c5ae2e558"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/migration/zipball/c2d016944cb029fa5ff822ceee704785a06ef289",
"reference": "c2d016944cb029fa5ff822ceee704785a06ef289",
"url": "https://api.github.com/repos/utopia-php/migration/zipball/97583ae502e40621ea91a71de19d053c5ae2e558",
"reference": "97583ae502e40621ea91a71de19d053c5ae2e558",
"shasum": ""
},
"require": {
@@ -4566,9 +4566,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/migration/issues",
"source": "https://github.com/utopia-php/migration/tree/1.6.3"
"source": "https://github.com/utopia-php/migration/tree/1.7.0"
},
"time": "2026-03-04T07:08:22+00:00"
"time": "2026-03-10T06:36:27+00:00"
},
{
"name": "utopia-php/mongo",
@@ -5215,16 +5215,16 @@
},
{
"name": "utopia-php/vcs",
"version": "2.0.1",
"version": "2.0.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/vcs.git",
"reference": "92a1650824ba0c5e6a1bc46e622ac87c50a08920"
"reference": "058049326e04a2a0c2f0ce8ad00c7e84825aba14"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/vcs/zipball/92a1650824ba0c5e6a1bc46e622ac87c50a08920",
"reference": "92a1650824ba0c5e6a1bc46e622ac87c50a08920",
"url": "https://api.github.com/repos/utopia-php/vcs/zipball/058049326e04a2a0c2f0ce8ad00c7e84825aba14",
"reference": "058049326e04a2a0c2f0ce8ad00c7e84825aba14",
"shasum": ""
},
"require": {
@@ -5258,9 +5258,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/vcs/issues",
"source": "https://github.com/utopia-php/vcs/tree/2.0.1"
"source": "https://github.com/utopia-php/vcs/tree/2.0.0"
},
"time": "2026-02-27T12:18:49+00:00"
"time": "2026-02-25T11:36:45+00:00"
},
{
"name": "utopia-php/websocket",
@@ -5438,16 +5438,16 @@
"packages-dev": [
{
"name": "appwrite/sdk-generator",
"version": "1.11.6",
"version": "1.11.1",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-generator.git",
"reference": "f80e302d000cdc2f98b4bb5ff2fc3bd0bdff7b38"
"reference": "6ff411f26f2750eea05c7598c14bb3a2ada898cb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/f80e302d000cdc2f98b4bb5ff2fc3bd0bdff7b38",
"reference": "f80e302d000cdc2f98b4bb5ff2fc3bd0bdff7b38",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/6ff411f26f2750eea05c7598c14bb3a2ada898cb",
"reference": "6ff411f26f2750eea05c7598c14bb3a2ada898cb",
"shasum": ""
},
"require": {
@@ -5483,22 +5483,22 @@
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
"support": {
"issues": "https://github.com/appwrite/sdk-generator/issues",
"source": "https://github.com/appwrite/sdk-generator/tree/1.11.6"
"source": "https://github.com/appwrite/sdk-generator/tree/1.11.1"
},
"time": "2026-03-09T07:12:51+00:00"
"time": "2026-02-25T07:15:19+00:00"
},
{
"name": "brianium/paratest",
"version": "v7.19.1",
"version": "v7.19.0",
"source": {
"type": "git",
"url": "https://github.com/paratestphp/paratest.git",
"reference": "95b03194f4cdf5c83175ceead673e21cb66465e7"
"reference": "7c6c29af7c4b406b49ce0c6b0a3a81d3684474e6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paratestphp/paratest/zipball/95b03194f4cdf5c83175ceead673e21cb66465e7",
"reference": "95b03194f4cdf5c83175ceead673e21cb66465e7",
"url": "https://api.github.com/repos/paratestphp/paratest/zipball/7c6c29af7c4b406b49ce0c6b0a3a81d3684474e6",
"reference": "7c6c29af7c4b406b49ce0c6b0a3a81d3684474e6",
"shasum": ""
},
"require": {
@@ -5512,7 +5512,7 @@
"phpunit/php-code-coverage": "^12.5.3 || ^13.0.1",
"phpunit/php-file-iterator": "^6.0.1 || ^7",
"phpunit/php-timer": "^8 || ^9",
"phpunit/phpunit": "^12.5.14 || ^13.0.5",
"phpunit/phpunit": "^12.5.9 || ^13",
"sebastian/environment": "^8.0.3 || ^9",
"symfony/console": "^7.4.4 || ^8.0.4",
"symfony/process": "^7.4.5 || ^8.0.5"
@@ -5522,10 +5522,10 @@
"ext-pcntl": "*",
"ext-pcov": "*",
"ext-posix": "*",
"phpstan/phpstan": "^2.1.40",
"phpstan/phpstan-deprecation-rules": "^2.0.4",
"phpstan/phpstan-phpunit": "^2.0.16",
"phpstan/phpstan-strict-rules": "^2.0.10",
"phpstan/phpstan": "^2.1.38",
"phpstan/phpstan-deprecation-rules": "^2.0.3",
"phpstan/phpstan-phpunit": "^2.0.12",
"phpstan/phpstan-strict-rules": "^2.0.8",
"symfony/filesystem": "^7.4.0 || ^8.0.1"
},
"bin": [
@@ -5566,7 +5566,7 @@
],
"support": {
"issues": "https://github.com/paratestphp/paratest/issues",
"source": "https://github.com/paratestphp/paratest/tree/v7.19.1"
"source": "https://github.com/paratestphp/paratest/tree/v7.19.0"
},
"funding": [
{
@@ -5578,7 +5578,7 @@
"type": "paypal"
}
],
"time": "2026-02-25T14:53:45+00:00"
"time": "2026-02-06T10:53:26+00:00"
},
{
"name": "czproject/git-php",
@@ -6398,16 +6398,16 @@
},
{
"name": "phpbench/phpbench",
"version": "1.5.1",
"version": "1.4.3",
"source": {
"type": "git",
"url": "https://github.com/phpbench/phpbench.git",
"reference": "9a28fd0833f11171b949843c6fd663eb69b6d14c"
"reference": "b641dde59d969ea42eed70a39f9b51950bc96878"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpbench/phpbench/zipball/9a28fd0833f11171b949843c6fd663eb69b6d14c",
"reference": "9a28fd0833f11171b949843c6fd663eb69b6d14c",
"url": "https://api.github.com/repos/phpbench/phpbench/zipball/b641dde59d969ea42eed70a39f9b51950bc96878",
"reference": "b641dde59d969ea42eed70a39f9b51950bc96878",
"shasum": ""
},
"require": {
@@ -6418,7 +6418,7 @@
"ext-reflection": "*",
"ext-spl": "*",
"ext-tokenizer": "*",
"php": "^8.2",
"php": "^8.1",
"phpbench/container": "^2.2",
"psr/log": "^1.1 || ^2.0 || ^3.0",
"seld/jsonlint": "^1.1",
@@ -6438,9 +6438,8 @@
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan": "^1.0",
"phpstan/phpstan-phpunit": "^1.0",
"phpunit/phpunit": "^11.5",
"phpunit/phpunit": "^10.4 || ^11.0",
"rector/rector": "^1.2",
"sebastian/exporter": "^6.3.2",
"symfony/error-handler": "^6.1 || ^7.0 || ^8.0",
"symfony/var-dumper": "^6.1 || ^7.0 || ^8.0"
},
@@ -6485,7 +6484,7 @@
],
"support": {
"issues": "https://github.com/phpbench/phpbench/issues",
"source": "https://github.com/phpbench/phpbench/tree/1.5.1"
"source": "https://github.com/phpbench/phpbench/tree/1.4.3"
},
"funding": [
{
@@ -6493,15 +6492,15 @@
"type": "github"
}
],
"time": "2026-03-05T08:18:58+00:00"
"time": "2025-11-06T19:07:31+00:00"
},
{
"name": "phpstan/phpstan",
"version": "1.12.33",
"version": "1.12.32",
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/37982d6fc7cbb746dda7773530cda557cdf119e1",
"reference": "37982d6fc7cbb746dda7773530cda557cdf119e1",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/2770dcdf5078d0b0d53f94317e06affe88419aa8",
"reference": "2770dcdf5078d0b0d53f94317e06affe88419aa8",
"shasum": ""
},
"require": {
@@ -6546,7 +6545,7 @@
"type": "github"
}
],
"time": "2026-02-28T20:30:03+00:00"
"time": "2025-09-30T10:16:31+00:00"
},
{
"name": "phpunit/php-code-coverage",
@@ -8096,16 +8095,16 @@
},
{
"name": "symfony/console",
"version": "v8.0.7",
"version": "v8.0.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a"
"reference": "ace03c4cf9805080ff40cbeec69fca180c339a3b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a",
"reference": "15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a",
"url": "https://api.github.com/repos/symfony/console/zipball/ace03c4cf9805080ff40cbeec69fca180c339a3b",
"reference": "ace03c4cf9805080ff40cbeec69fca180c339a3b",
"shasum": ""
},
"require": {
@@ -8162,7 +8161,7 @@
"terminal"
],
"support": {
"source": "https://github.com/symfony/console/tree/v8.0.7"
"source": "https://github.com/symfony/console/tree/v8.0.4"
},
"funding": [
{
@@ -8182,20 +8181,20 @@
"type": "tidelift"
}
],
"time": "2026-03-06T14:06:22+00:00"
"time": "2026-01-13T13:06:50+00:00"
},
{
"name": "symfony/filesystem",
"version": "v8.0.6",
"version": "v8.0.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
"reference": "7bf9162d7a0dff98d079b72948508fa48018a770"
"reference": "d937d400b980523dc9ee946bb69972b5e619058d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/7bf9162d7a0dff98d079b72948508fa48018a770",
"reference": "7bf9162d7a0dff98d079b72948508fa48018a770",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/d937d400b980523dc9ee946bb69972b5e619058d",
"reference": "d937d400b980523dc9ee946bb69972b5e619058d",
"shasum": ""
},
"require": {
@@ -8232,7 +8231,7 @@
"description": "Provides basic utilities for the filesystem",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/filesystem/tree/v8.0.6"
"source": "https://github.com/symfony/filesystem/tree/v8.0.1"
},
"funding": [
{
@@ -8252,20 +8251,20 @@
"type": "tidelift"
}
],
"time": "2026-02-25T16:59:43+00:00"
"time": "2025-12-01T09:13:36+00:00"
},
{
"name": "symfony/finder",
"version": "v8.0.6",
"version": "v8.0.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
"reference": "441404f09a54de6d1bd6ad219e088cdf4c91f97c"
"reference": "8bd576e97c67d45941365bf824e18dc8538e6eb0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/441404f09a54de6d1bd6ad219e088cdf4c91f97c",
"reference": "441404f09a54de6d1bd6ad219e088cdf4c91f97c",
"url": "https://api.github.com/repos/symfony/finder/zipball/8bd576e97c67d45941365bf824e18dc8538e6eb0",
"reference": "8bd576e97c67d45941365bf824e18dc8538e6eb0",
"shasum": ""
},
"require": {
@@ -8300,7 +8299,7 @@
"description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/finder/tree/v8.0.6"
"source": "https://github.com/symfony/finder/tree/v8.0.5"
},
"funding": [
{
@@ -8320,7 +8319,7 @@
"type": "tidelift"
}
],
"time": "2026-01-29T09:41:02+00:00"
"time": "2026-01-26T15:08:38+00:00"
},
{
"name": "symfony/options-resolver",
@@ -8790,16 +8789,16 @@
},
{
"name": "symfony/string",
"version": "v8.0.6",
"version": "v8.0.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
"reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4"
"reference": "758b372d6882506821ed666032e43020c4f57194"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/6c9e1108041b5dce21a9a4984b531c4923aa9ec4",
"reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4",
"url": "https://api.github.com/repos/symfony/string/zipball/758b372d6882506821ed666032e43020c4f57194",
"reference": "758b372d6882506821ed666032e43020c4f57194",
"shasum": ""
},
"require": {
@@ -8856,7 +8855,7 @@
"utf8"
],
"support": {
"source": "https://github.com/symfony/string/tree/v8.0.6"
"source": "https://github.com/symfony/string/tree/v8.0.4"
},
"funding": [
{
@@ -8876,7 +8875,7 @@
"type": "tidelift"
}
],
"time": "2026-02-09T10:14:57+00:00"
"time": "2026-01-12T12:37:40+00:00"
},
{
"name": "textalk/websocket",
+144
View File
@@ -0,0 +1,144 @@
# Dev tools for local development only.
# This file is automatically loaded by `docker compose` alongside docker-compose.yml.
# CI sets COMPOSE_FILE=docker-compose.yml explicitly, so these services are excluded from test runs.
services:
appwrite-mongo-express:
profiles: ["mongodb"]
image: mongo-express
container_name: appwrite-mongo-express
networks:
- appwrite
ports:
- "8082:8081"
environment:
ME_CONFIG_MONGODB_URL: "mongodb://root:${_APP_DB_ROOT_PASS}@appwrite-mongodb:27017/?replicaSet=rs0&directConnection=true"
ME_CONFIG_BASICAUTH_USERNAME: ${_APP_DB_USER}
ME_CONFIG_BASICAUTH_PASSWORD: ${_APP_DB_PASS}
depends_on:
- mongodb
adminer:
image: adminer
container_name: appwrite-adminer
restart: always
ports:
- 9506:8080
networks:
- appwrite
- gateway
environment:
- ADMINER_DESIGN=pepa-linha
- ADMINER_DEFAULT_SERVER=mariadb
- ADMINER_DEFAULT_USERNAME=root
- ADMINER_DEFAULT_PASSWORD=rootsecretpassword
- ADMINER_DEFAULT_DB=appwrite
configs:
- source: adminer-index.php
target: /var/www/html/index.php
mode: 0755
labels:
- "traefik.enable=true"
- "traefik.constraint-label-stack=appwrite"
- "traefik.docker.network=gateway"
- "traefik.http.services.appwrite_adminer.loadbalancer.server.port=8080"
- "traefik.http.routers.appwrite_adminer_http.entrypoints=appwrite_web"
- "traefik.http.routers.appwrite_adminer_http.rule=Host(`mysql.localhost`)"
- "traefik.http.routers.appwrite_adminer_http.service=appwrite_adminer"
- "traefik.http.routers.appwrite_adminer_https.entrypoints=appwrite_websecure"
- "traefik.http.routers.appwrite_adminer_https.rule=Host(`mysql.localhost`)"
- "traefik.http.routers.appwrite_adminer_https.service=appwrite_adminer"
- "traefik.http.routers.appwrite_adminer_https.tls=true"
redis-insight:
image: redis/redisinsight:latest
restart: unless-stopped
networks:
- appwrite
- gateway
environment:
- RI_PRE_SETUP_DATABASES_PATH=/mnt/connections.json
configs:
- source: redisinsight-connections.json
target: /mnt/connections.json
mode: 0755
labels:
- "traefik.enable=true"
- "traefik.constraint-label-stack=appwrite"
- "traefik.docker.network=gateway"
- "traefik.http.services.appwrite_redisinsight.loadbalancer.server.port=5540"
- "traefik.http.routers.appwrite_redisinsight_http.entrypoints=appwrite_web"
- "traefik.http.routers.appwrite_redisinsight_http.rule=Host(`redis.localhost`)"
- "traefik.http.routers.appwrite_redisinsight_http.service=appwrite_redisinsight"
- "traefik.http.routers.appwrite_redisinsight_https.entrypoints=appwrite_websecure"
- "traefik.http.routers.appwrite_redisinsight_https.rule=Host(`redis.localhost`)"
- "traefik.http.routers.appwrite_redisinsight_https.service=appwrite_redisinsight"
- "traefik.http.routers.appwrite_redisinsight_https.tls=true"
ports:
- "8081:5540"
graphql-explorer:
container_name: appwrite-graphql-explorer
image: appwrite/altair:0.3.0
restart: unless-stopped
networks:
- appwrite
ports:
- "9509:3000"
environment:
- SERVER_URL=http://localhost/v1/graphql
configs:
redisinsight-connections.json:
content: |
[
{
"compressor": "NONE",
"id": "104dc90a-21ef-4d5e-8912-b30baabb152f",
"host": "redis",
"port": 6379,
"name": "redis:6379",
"db": 0,
"username": "default",
"password": null,
"connectionType": "STANDALONE",
"nameFromProvider": null,
"provider": "REDIS",
"lastConnection": "2025-10-16T09:22:02.591Z",
"modules": [
{
"name": "ReJSON",
"version": 20808,
"semanticVersion": "2.8.8"
},
{
"name": "search",
"version": 21015,
"semanticVersion": "2.10.15"
}
],
"tls": false,
"tlsServername": null,
"verifyServerCert": null,
"caCert": null,
"clientCert": null,
"ssh": false,
"sshOptions": null,
"forceStandalone": false,
"tags": []
}
]
adminer-index.php:
content: |
<?php
if(!count($$_GET)) {
$$_POST['auth'] = [
'server' => $$_ENV['ADMINER_DEFAULT_SERVER'],
'driver' => 'server', /* seems to autodetect the driver from server settings */
'username' => $$_ENV['ADMINER_DEFAULT_USERNAME'],
'password' => $$_ENV['ADMINER_DEFAULT_PASSWORD'],
'db' => $$_ENV['ADMINER_DEFAULT_DB'],
];
}
include './adminer.php';
+58 -172
View File
@@ -10,6 +10,15 @@ x-logging: &x-logging
max-file: "5"
max-size: "10m"
x-build: &x-build
build:
context: .
target: development
args:
DEBUG: false
TESTING: true
VERSION: dev
services:
traefik:
image: traefik:3.6
@@ -50,15 +59,13 @@ services:
appwrite:
container_name: appwrite
<<: *x-logging
<<: [*x-logging, *x-build]
image: appwrite-dev
build:
context: .
target: development
args:
DEBUG: false
TESTING: true
VERSION: dev
healthcheck:
test: ["CMD", "doctor"]
interval: 5s
timeout: 5s
retries: 12
ports:
- 9501:80
networks:
@@ -101,10 +108,10 @@ services:
- ./dev:/usr/src/code/dev
depends_on:
- ${_APP_DB_HOST:-mongodb}
- redis
- coredns
# - clamav
redis:
condition: service_healthy
coredns:
condition: service_started
entrypoint:
- php
- -e
@@ -260,7 +267,7 @@ services:
appwrite-realtime:
entrypoint: realtime
<<: *x-logging
<<: [*x-logging, *x-build]
container_name: appwrite-realtime
image: appwrite-dev
restart: unless-stopped
@@ -312,7 +319,7 @@ services:
appwrite-worker-audits:
entrypoint: worker-audits
<<: *x-logging
<<: [*x-logging, *x-build]
container_name: appwrite-worker-audits
image: appwrite-dev
networks:
@@ -343,7 +350,7 @@ services:
appwrite-worker-webhooks:
entrypoint: worker-webhooks
<<: *x-logging
<<: [*x-logging, *x-build]
container_name: appwrite-worker-webhooks
image: appwrite-dev
networks:
@@ -378,7 +385,7 @@ services:
appwrite-worker-deletes:
entrypoint: worker-deletes
<<: *x-logging
<<: [*x-logging, *x-build]
container_name: appwrite-worker-deletes
image: appwrite-dev
networks:
@@ -443,7 +450,7 @@ services:
appwrite-worker-databases:
entrypoint: worker-databases
<<: *x-logging
<<: [*x-logging, *x-build]
container_name: appwrite-worker-databases
image: appwrite-dev
networks:
@@ -476,7 +483,7 @@ services:
appwrite-worker-builds:
entrypoint: worker-builds
<<: *x-logging
<<: [*x-logging, *x-build]
container_name: appwrite-worker-builds
image: appwrite-dev
networks:
@@ -551,7 +558,7 @@ services:
appwrite-worker-screenshots:
entrypoint: worker-screenshots
<<: *x-logging
<<: [*x-logging, *x-build]
container_name: appwrite-worker-screenshots
image: appwrite-dev
networks:
@@ -614,7 +621,7 @@ services:
appwrite-worker-certificates:
entrypoint: worker-certificates
<<: *x-logging
<<: [*x-logging, *x-build]
container_name: appwrite-worker-certificates
image: appwrite-dev
networks:
@@ -656,7 +663,7 @@ services:
appwrite-worker-executions:
entrypoint: worker-executions
<<: *x-logging
<<: [*x-logging, *x-build]
container_name: appwrite-worker-executions
image: appwrite-dev
networks:
@@ -686,7 +693,7 @@ services:
appwrite-worker-functions:
entrypoint: worker-functions
<<: *x-logging
<<: [*x-logging, *x-build]
container_name: appwrite-worker-functions
image: appwrite-dev
networks:
@@ -730,7 +737,7 @@ services:
appwrite-worker-mails:
entrypoint: worker-mails
<<: *x-logging
<<: [*x-logging, *x-build]
container_name: appwrite-worker-mails
image: appwrite-dev
networks:
@@ -772,7 +779,7 @@ services:
appwrite-worker-messaging:
entrypoint: worker-messaging
<<: *x-logging
<<: [*x-logging, *x-build]
container_name: appwrite-worker-messaging
restart: unless-stopped
image: appwrite-dev
@@ -829,7 +836,7 @@ services:
appwrite-worker-migrations:
entrypoint: worker-migrations
<<: *x-logging
<<: [*x-logging, *x-build]
container_name: appwrite-worker-migrations
restart: unless-stopped
image: appwrite-dev
@@ -874,7 +881,7 @@ services:
appwrite-task-maintenance:
entrypoint: maintenance
<<: *x-logging
<<: [*x-logging, *x-build]
container_name: appwrite-task-maintenance
image: appwrite-dev
networks:
@@ -920,7 +927,7 @@ services:
appwrite-task-interval:
entrypoint: interval
<<: *x-logging
<<: [*x-logging, *x-build]
container_name: appwrite-task-interval
image: appwrite-dev
networks:
@@ -961,7 +968,7 @@ services:
appwrite-task-stats-resources:
container_name: appwrite-task-stats-resources
entrypoint: stats-resources
<<: *x-logging
<<: [*x-logging, *x-build]
image: appwrite-dev
networks:
- appwrite
@@ -993,7 +1000,7 @@ services:
appwrite-worker-stats-resources:
entrypoint: worker-stats-resources
<<: *x-logging
<<: [*x-logging, *x-build]
container_name: appwrite-worker-stats-resources
image: appwrite-dev
networks:
@@ -1026,7 +1033,7 @@ services:
appwrite-worker-stats-usage:
entrypoint: worker-stats-usage
<<: *x-logging
<<: [*x-logging, *x-build]
container_name: appwrite-worker-stats-usage
image: appwrite-dev
networks:
@@ -1059,7 +1066,7 @@ services:
appwrite-task-scheduler-functions:
entrypoint: schedule-functions
<<: *x-logging
<<: [*x-logging, *x-build]
container_name: appwrite-task-scheduler-functions
image: appwrite-dev
networks:
@@ -1089,7 +1096,7 @@ services:
appwrite-task-scheduler-executions:
entrypoint: schedule-executions
<<: *x-logging
<<: [*x-logging, *x-build]
container_name: appwrite-task-scheduler-executions
image: appwrite-dev
networks:
@@ -1118,7 +1125,7 @@ services:
appwrite-task-scheduler-messages:
entrypoint: schedule-messages
<<: *x-logging
<<: [*x-logging, *x-build]
container_name: appwrite-task-scheduler-messages
image: appwrite-dev
networks:
@@ -1237,6 +1244,11 @@ services:
- MYSQL_PASSWORD=${_APP_DB_PASS}
- MARIADB_AUTO_UPGRADE=1
command: "mysqld --innodb-flush-method=fsync"
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 5s
timeout: 5s
retries: 12
mongodb:
profiles: ["mongodb"]
@@ -1275,20 +1287,7 @@ services:
retries: 10
start_period: 30s
appwrite-mongo-express:
profiles: ["mongodb"]
image: mongo-express
container_name: appwrite-mongo-express
networks:
- appwrite
ports:
- "8082:8081"
environment:
ME_CONFIG_MONGODB_URL: "mongodb://root:${_APP_DB_ROOT_PASS}@appwrite-mongodb:27017/?replicaSet=rs0&directConnection=true"
ME_CONFIG_BASICAUTH_USERNAME: ${_APP_DB_USER}
ME_CONFIG_BASICAUTH_PASSWORD: ${_APP_DB_PASS}
depends_on:
- mongodb
postgresql:
profiles: ["postgresql"]
@@ -1309,6 +1308,11 @@ services:
- POSTGRES_USER=${_APP_DB_USER}
- POSTGRES_PASSWORD=${_APP_DB_PASS}
command: "postgres"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${_APP_DB_USER}"]
interval: 5s
timeout: 5s
retries: 12
redis:
image: redis:7.4.7-alpine
@@ -1325,6 +1329,11 @@ services:
- appwrite
volumes:
- appwrite-redis:/data:rw
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 12
coredns: # DNS server for testing purposes (Proxy APIs)
image: coredns/coredns:1.12.4
@@ -1396,78 +1405,8 @@ services:
networks:
- appwrite
adminer:
image: adminer
container_name: appwrite-adminer
<<: *x-logging
restart: always
ports:
- 9506:8080
networks:
- appwrite
- gateway
environment:
- ADMINER_DESIGN=pepa-linha
- ADMINER_DEFAULT_SERVER=mariadb
- ADMINER_DEFAULT_USERNAME=root
- ADMINER_DEFAULT_PASSWORD=rootsecretpassword
- ADMINER_DEFAULT_DB=appwrite
configs:
- source: adminer-index.php
target: /var/www/html/index.php
mode: 0755
labels:
- "traefik.enable=true"
- "traefik.constraint-label-stack=appwrite"
- "traefik.docker.network=gateway"
- "traefik.http.services.appwrite_adminer.loadbalancer.server.port=8080"
- "traefik.http.routers.appwrite_adminer_http.entrypoints=appwrite_web"
- "traefik.http.routers.appwrite_adminer_http.rule=Host(`mysql.localhost`)"
- "traefik.http.routers.appwrite_adminer_http.service=appwrite_adminer"
- "traefik.http.routers.appwrite_adminer_https.entrypoints=appwrite_websecure"
- "traefik.http.routers.appwrite_adminer_https.rule=Host(`mysql.localhost`)"
- "traefik.http.routers.appwrite_adminer_https.service=appwrite_adminer"
- "traefik.http.routers.appwrite_adminer_https.tls=true"
redis-insight:
image: redis/redisinsight:latest
restart: unless-stopped
networks:
- appwrite
- gateway
environment:
- RI_PRE_SETUP_DATABASES_PATH=/mnt/connections.json
configs:
- source: redisinsight-connections.json
target: /mnt/connections.json
mode: 0755
labels:
- "traefik.enable=true"
- "traefik.constraint-label-stack=appwrite"
- "traefik.docker.network=gateway"
- "traefik.http.services.appwrite_redisinsight.loadbalancer.server.port=5540"
- "traefik.http.routers.appwrite_redisinsight_http.entrypoints=appwrite_web"
- "traefik.http.routers.appwrite_redisinsight_http.rule=Host(`redis.localhost`)"
- "traefik.http.routers.appwrite_redisinsight_http.service=appwrite_redisinsight"
- "traefik.http.routers.appwrite_redisinsight_https.entrypoints=appwrite_websecure"
- "traefik.http.routers.appwrite_redisinsight_https.rule=Host(`redis.localhost`)"
- "traefik.http.routers.appwrite_redisinsight_https.service=appwrite_redisinsight"
- "traefik.http.routers.appwrite_redisinsight_https.tls=true"
ports:
- "8081:5540"
graphql-explorer:
container_name: appwrite-graphql-explorer
image: appwrite/altair:0.3.0
restart: unless-stopped
networks:
- appwrite
ports:
- "9509:3000"
environment:
- SERVER_URL=http://localhost/v1/graphql
# Dev Tools End ------------------------------------------------------------------------------------------
# Dev tools (adminer, redis-insight, mongo-express, graphql-explorer)
# are defined in docker-compose.override.yml
networks:
gateway:
@@ -1480,60 +1419,7 @@ networks:
runtimes:
name: runtimes
configs:
redisinsight-connections.json:
content: |
[
{
"compressor": "NONE",
"id": "104dc90a-21ef-4d5e-8912-b30baabb152f",
"host": "redis",
"port": 6379,
"name": "redis:6379",
"db": 0,
"username": "default",
"password": null,
"connectionType": "STANDALONE",
"nameFromProvider": null,
"provider": "REDIS",
"lastConnection": "2025-10-16T09:22:02.591Z",
"modules": [
{
"name": "ReJSON",
"version": 20808,
"semanticVersion": "2.8.8"
},
{
"name": "search",
"version": 21015,
"semanticVersion": "2.10.15"
}
],
"tls": false,
"tlsServername": null,
"verifyServerCert": null,
"caCert": null,
"clientCert": null,
"ssh": false,
"sshOptions": null,
"forceStandalone": false,
"tags": []
}
]
adminer-index.php:
content: |
<?php
if(!count($$_GET)) {
$$_POST['auth'] = [
'server' => $$_ENV['ADMINER_DEFAULT_SERVER'],
'driver' => 'server', /* seems to autodetect the driver from server settings */
'username' => $$_ENV['ADMINER_DEFAULT_USERNAME'],
'password' => $$_ENV['ADMINER_DEFAULT_PASSWORD'],
'db' => $$_ENV['ADMINER_DEFAULT_DB'],
];
}
include './adminer.php';
volumes:
appwrite-mariadb:
+1
View File
@@ -214,6 +214,7 @@ class Mails extends Action
$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.) */
@@ -317,6 +317,16 @@ class Migrations extends Action
'sites.write',
'tokens.read',
'tokens.write',
'providers.read',
'providers.write',
'topics.read',
'topics.write',
'subscribers.read',
'subscribers.write',
'messages.read',
'messages.write',
'targets.read',
'targets.write',
]
]);
+1 -1
View File
@@ -160,7 +160,7 @@ class StatsUsage extends Action
}
$this->stats[$projectId]['project'] = $project;
$this->stats[$projectId]['receivedAt'] = DateTime::now();
$this->stats[$projectId]['receivedAt'] = DateTime::format(new \DateTime('@' . $message->getTimestamp()));
foreach ($payload['metrics'] ?? [] as $metric) {
$this->keys++;
if (!isset($this->stats[$projectId]['keys'][$metric['key']])) {
@@ -59,6 +59,30 @@ class MigrationReport extends Model
'default' => 0,
'example' => 5,
])
->addRule(Resource::TYPE_PROVIDER, [
'type' => self::TYPE_INTEGER,
'description' => 'Number of providers to be migrated.',
'default' => 0,
'example' => 5,
])
->addRule(Resource::TYPE_TOPIC, [
'type' => self::TYPE_INTEGER,
'description' => 'Number of topics to be migrated.',
'default' => 0,
'example' => 10,
])
->addRule(Resource::TYPE_SUBSCRIBER, [
'type' => self::TYPE_INTEGER,
'description' => 'Number of subscribers to be migrated.',
'default' => 0,
'example' => 100,
])
->addRule(Resource::TYPE_MESSAGE, [
'type' => self::TYPE_INTEGER,
'description' => 'Number of messages to be migrated.',
'default' => 0,
'example' => 50,
])
->addRule('size', [
'type' => self::TYPE_INTEGER,
'description' => 'Size of files to be migrated in mb.',
+1 -1
View File
@@ -251,7 +251,7 @@ class Comment
$json = \base64_decode($state);
$builds = \json_decode($json, true);
$this->builds = $builds;
$this->builds = \is_array($builds) ? $builds : [];
return $this;
}
@@ -686,7 +686,7 @@ class FunctionsConsoleClientTest extends Scope
$stdout = '';
$stderr = '';
$code = Console::execute("docker exec appwrite time-travel --projectId={$this->getProject()['$id']} --resourceType=deployment --resourceId={$deploymentIdInactiveOld} --createdAt=2020-01-01T00:00:00Z", '', $stdout, $stderr);
$code = Console::execute("docker exec appwrite task-time-travel --projectId={$this->getProject()['$id']} --resourceType=deployment --resourceId={$deploymentIdInactiveOld} --createdAt=2020-01-01T00:00:00Z", '', $stdout, $stderr);
$this->assertSame(0, $code, "Time-travel command failed with code $code: $stderr ($stdout)");
$stdout = '';
@@ -1694,4 +1694,842 @@ trait MigrationsBase
'x-appwrite-key' => $this->getProject()['apiKey']
]);
}
/**
* Messaging
*/
public function testAppwriteMigrationMessagingProvider(): void
{
$provider = $this->client->call(Client::METHOD_POST, '/messaging/providers/sendgrid', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'providerId' => ID::unique(),
'name' => 'Migration Sendgrid',
'apiKey' => 'my-apikey',
'from' => 'migration@test.com',
]);
$this->assertEquals(201, $provider['headers']['status-code']);
$this->assertNotEmpty($provider['body']['$id']);
$providerId = $provider['body']['$id'];
$result = $this->performMigrationSync([
'resources' => [
Resource::TYPE_PROVIDER,
],
'endpoint' => $this->webEndpoint,
'projectId' => $this->getProject()['$id'],
'apiKey' => $this->getProject()['apiKey'],
]);
$this->assertEquals('completed', $result['status']);
$this->assertEquals([Resource::TYPE_PROVIDER], $result['resources']);
$this->assertArrayHasKey(Resource::TYPE_PROVIDER, $result['statusCounters']);
$this->assertEquals(0, $result['statusCounters'][Resource::TYPE_PROVIDER]['error']);
$this->assertEquals(0, $result['statusCounters'][Resource::TYPE_PROVIDER]['pending']);
$this->assertGreaterThanOrEqual(1, $result['statusCounters'][Resource::TYPE_PROVIDER]['success']);
$this->assertEquals(0, $result['statusCounters'][Resource::TYPE_PROVIDER]['processing']);
$this->assertEquals(0, $result['statusCounters'][Resource::TYPE_PROVIDER]['warning']);
$response = $this->client->call(Client::METHOD_GET, '/messaging/providers/' . $providerId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getDestinationProject()['$id'],
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals($providerId, $response['body']['$id']);
$this->assertEquals('Migration Sendgrid', $response['body']['name']);
$this->assertEquals('email', $response['body']['type']);
// Cleanup
$this->client->call(Client::METHOD_DELETE, '/messaging/providers/' . $providerId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]);
$this->client->call(Client::METHOD_DELETE, '/messaging/providers/' . $providerId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getDestinationProject()['$id'],
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
]);
}
public function testAppwriteMigrationMessagingProviderSMTP(): void
{
$provider = $this->client->call(Client::METHOD_POST, '/messaging/providers/smtp', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'providerId' => ID::unique(),
'name' => 'Migration SMTP',
'host' => 'smtp.test.com',
'port' => 587,
'from' => 'migration-smtp@test.com',
]);
$this->assertEquals(201, $provider['headers']['status-code']);
$providerId = $provider['body']['$id'];
$result = $this->performMigrationSync([
'resources' => [
Resource::TYPE_PROVIDER,
],
'endpoint' => $this->webEndpoint,
'projectId' => $this->getProject()['$id'],
'apiKey' => $this->getProject()['apiKey'],
]);
$this->assertEquals('completed', $result['status']);
$this->assertArrayHasKey(Resource::TYPE_PROVIDER, $result['statusCounters']);
$this->assertEquals(0, $result['statusCounters'][Resource::TYPE_PROVIDER]['error']);
$this->assertGreaterThanOrEqual(1, $result['statusCounters'][Resource::TYPE_PROVIDER]['success']);
$response = $this->client->call(Client::METHOD_GET, '/messaging/providers/' . $providerId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getDestinationProject()['$id'],
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals($providerId, $response['body']['$id']);
$this->assertEquals('Migration SMTP', $response['body']['name']);
$this->assertEquals('email', $response['body']['type']);
$this->assertEquals('smtp', $response['body']['provider']);
// Cleanup
$this->client->call(Client::METHOD_DELETE, '/messaging/providers/' . $providerId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]);
$this->client->call(Client::METHOD_DELETE, '/messaging/providers/' . $providerId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getDestinationProject()['$id'],
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
]);
}
public function testAppwriteMigrationMessagingProviderTwilio(): void
{
$provider = $this->client->call(Client::METHOD_POST, '/messaging/providers/twilio', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'providerId' => ID::unique(),
'name' => 'Migration Twilio',
'from' => '+15551234567',
'accountSid' => 'test-account-sid',
'authToken' => 'test-auth-token',
]);
$this->assertEquals(201, $provider['headers']['status-code']);
$providerId = $provider['body']['$id'];
$result = $this->performMigrationSync([
'resources' => [
Resource::TYPE_PROVIDER,
],
'endpoint' => $this->webEndpoint,
'projectId' => $this->getProject()['$id'],
'apiKey' => $this->getProject()['apiKey'],
]);
$this->assertEquals('completed', $result['status']);
$this->assertArrayHasKey(Resource::TYPE_PROVIDER, $result['statusCounters']);
$this->assertEquals(0, $result['statusCounters'][Resource::TYPE_PROVIDER]['error']);
$this->assertGreaterThanOrEqual(1, $result['statusCounters'][Resource::TYPE_PROVIDER]['success']);
$response = $this->client->call(Client::METHOD_GET, '/messaging/providers/' . $providerId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getDestinationProject()['$id'],
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals($providerId, $response['body']['$id']);
$this->assertEquals('Migration Twilio', $response['body']['name']);
$this->assertEquals('sms', $response['body']['type']);
$this->assertEquals('twilio', $response['body']['provider']);
// Cleanup
$this->client->call(Client::METHOD_DELETE, '/messaging/providers/' . $providerId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]);
$this->client->call(Client::METHOD_DELETE, '/messaging/providers/' . $providerId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getDestinationProject()['$id'],
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
]);
}
public function testAppwriteMigrationMessagingTopic(): void
{
$provider = $this->client->call(Client::METHOD_POST, '/messaging/providers/sendgrid', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'providerId' => ID::unique(),
'name' => 'Migration Sendgrid Topic',
'apiKey' => 'my-apikey',
'from' => 'migration-topic@test.com',
]);
$this->assertEquals(201, $provider['headers']['status-code']);
$providerId = $provider['body']['$id'];
$topic = $this->client->call(Client::METHOD_POST, '/messaging/topics', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'topicId' => ID::unique(),
'name' => 'Migration Topic',
]);
$this->assertEquals(201, $topic['headers']['status-code']);
$this->assertNotEmpty($topic['body']['$id']);
$topicId = $topic['body']['$id'];
$result = $this->performMigrationSync([
'resources' => [
Resource::TYPE_PROVIDER,
Resource::TYPE_TOPIC,
],
'endpoint' => $this->webEndpoint,
'projectId' => $this->getProject()['$id'],
'apiKey' => $this->getProject()['apiKey'],
]);
$this->assertEquals('completed', $result['status']);
$this->assertArrayHasKey(Resource::TYPE_TOPIC, $result['statusCounters']);
$this->assertEquals(0, $result['statusCounters'][Resource::TYPE_TOPIC]['error']);
$this->assertEquals(0, $result['statusCounters'][Resource::TYPE_TOPIC]['pending']);
$this->assertGreaterThanOrEqual(1, $result['statusCounters'][Resource::TYPE_TOPIC]['success']);
$this->assertEquals(0, $result['statusCounters'][Resource::TYPE_TOPIC]['processing']);
$this->assertEquals(0, $result['statusCounters'][Resource::TYPE_TOPIC]['warning']);
$response = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topicId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getDestinationProject()['$id'],
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals($topicId, $response['body']['$id']);
$this->assertEquals('Migration Topic', $response['body']['name']);
// Cleanup
$this->client->call(Client::METHOD_DELETE, '/messaging/topics/' . $topicId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]);
$this->client->call(Client::METHOD_DELETE, '/messaging/topics/' . $topicId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getDestinationProject()['$id'],
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
]);
$this->client->call(Client::METHOD_DELETE, '/messaging/providers/' . $providerId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]);
$this->client->call(Client::METHOD_DELETE, '/messaging/providers/' . $providerId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getDestinationProject()['$id'],
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
]);
}
public function testAppwriteMigrationMessagingSubscriber(): void
{
$user = $this->client->call(Client::METHOD_POST, '/users', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'userId' => ID::unique(),
'email' => uniqid() . '-migration-sub@test.com',
'password' => 'password',
]);
$this->assertEquals(201, $user['headers']['status-code']);
$userId = $user['body']['$id'];
$this->assertEquals(1, \count($user['body']['targets']));
$targetId = $user['body']['targets'][0]['$id'];
$provider = $this->client->call(Client::METHOD_POST, '/messaging/providers/sendgrid', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'providerId' => ID::unique(),
'name' => 'Migration Sendgrid Subscriber',
'apiKey' => 'my-apikey',
'from' => uniqid() . '-migration-sub@test.com',
]);
$this->assertEquals(201, $provider['headers']['status-code']);
$providerId = $provider['body']['$id'];
$topic = $this->client->call(Client::METHOD_POST, '/messaging/topics', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'topicId' => ID::unique(),
'name' => 'Migration Subscriber Topic',
]);
$this->assertEquals(201, $topic['headers']['status-code']);
$topicId = $topic['body']['$id'];
$subscriber = $this->client->call(Client::METHOD_POST, '/messaging/topics/' . $topicId . '/subscribers', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'subscriberId' => ID::unique(),
'targetId' => $targetId,
]);
$this->assertEquals(201, $subscriber['headers']['status-code']);
$result = $this->performMigrationSync([
'resources' => [
Resource::TYPE_USER,
Resource::TYPE_PROVIDER,
Resource::TYPE_TOPIC,
Resource::TYPE_SUBSCRIBER,
],
'endpoint' => $this->webEndpoint,
'projectId' => $this->getProject()['$id'],
'apiKey' => $this->getProject()['apiKey'],
]);
$this->assertEquals('completed', $result['status']);
$this->assertArrayHasKey(Resource::TYPE_SUBSCRIBER, $result['statusCounters']);
$this->assertEquals(0, $result['statusCounters'][Resource::TYPE_SUBSCRIBER]['error']);
$this->assertEquals(0, $result['statusCounters'][Resource::TYPE_SUBSCRIBER]['pending']);
$this->assertGreaterThanOrEqual(1, $result['statusCounters'][Resource::TYPE_SUBSCRIBER]['success']);
$this->assertEquals(0, $result['statusCounters'][Resource::TYPE_SUBSCRIBER]['processing']);
$this->assertEquals(0, $result['statusCounters'][Resource::TYPE_SUBSCRIBER]['warning']);
$response = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topicId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getDestinationProject()['$id'],
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals($topicId, $response['body']['$id']);
$this->assertGreaterThanOrEqual(1, $response['body']['emailTotal']);
// Cleanup
$this->client->call(Client::METHOD_DELETE, '/messaging/topics/' . $topicId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]);
$this->client->call(Client::METHOD_DELETE, '/messaging/topics/' . $topicId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getDestinationProject()['$id'],
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
]);
$this->client->call(Client::METHOD_DELETE, '/messaging/providers/' . $providerId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]);
$this->client->call(Client::METHOD_DELETE, '/messaging/providers/' . $providerId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getDestinationProject()['$id'],
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
]);
$this->client->call(Client::METHOD_DELETE, '/users/' . $userId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]);
$this->client->call(Client::METHOD_DELETE, '/users/' . $userId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getDestinationProject()['$id'],
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
]);
}
public function testAppwriteMigrationMessagingMessage(): void
{
$this->getDestinationProject(true);
$user = $this->client->call(Client::METHOD_POST, '/users', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'userId' => ID::unique(),
'email' => uniqid() . '-migration-msg@test.com',
'password' => 'password',
]);
$this->assertEquals(201, $user['headers']['status-code']);
$userId = $user['body']['$id'];
$this->assertEquals(1, \count($user['body']['targets']));
$targetId = $user['body']['targets'][0]['$id'];
$provider = $this->client->call(Client::METHOD_POST, '/messaging/providers/sendgrid', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'providerId' => ID::unique(),
'name' => 'Migration Sendgrid Message',
'apiKey' => 'my-apikey',
'from' => 'migration-msg@test.com',
]);
$this->assertEquals(201, $provider['headers']['status-code']);
$providerId = $provider['body']['$id'];
$topic = $this->client->call(Client::METHOD_POST, '/messaging/topics', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'topicId' => ID::unique(),
'name' => 'Migration Message Topic',
]);
$this->assertEquals(201, $topic['headers']['status-code']);
$topicId = $topic['body']['$id'];
$message = $this->client->call(Client::METHOD_POST, '/messaging/messages/email', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'messageId' => ID::unique(),
'targets' => [$targetId],
'topics' => [$topicId],
'subject' => 'Migration Test Email',
'content' => 'This is a migration test email',
'draft' => true,
]);
$this->assertEquals(201, $message['headers']['status-code']);
$this->assertNotEmpty($message['body']['$id']);
$messageId = $message['body']['$id'];
$result = $this->performMigrationSync([
'resources' => [
Resource::TYPE_USER,
Resource::TYPE_PROVIDER,
Resource::TYPE_TOPIC,
Resource::TYPE_SUBSCRIBER,
Resource::TYPE_MESSAGE,
],
'endpoint' => $this->webEndpoint,
'projectId' => $this->getProject()['$id'],
'apiKey' => $this->getProject()['apiKey'],
]);
$this->assertEquals('completed', $result['status']);
$this->assertArrayHasKey(Resource::TYPE_MESSAGE, $result['statusCounters']);
$this->assertEquals(0, $result['statusCounters'][Resource::TYPE_MESSAGE]['error']);
$this->assertEquals(0, $result['statusCounters'][Resource::TYPE_MESSAGE]['pending']);
$this->assertGreaterThanOrEqual(1, $result['statusCounters'][Resource::TYPE_MESSAGE]['success']);
$this->assertEquals(0, $result['statusCounters'][Resource::TYPE_MESSAGE]['processing']);
$this->assertEquals(0, $result['statusCounters'][Resource::TYPE_MESSAGE]['warning']);
$response = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $messageId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getDestinationProject()['$id'],
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals($messageId, $response['body']['$id']);
$this->assertEquals('draft', $response['body']['status']);
$this->assertEquals('Migration Test Email', $response['body']['data']['subject']);
$this->assertEquals('This is a migration test email', $response['body']['data']['content']);
$this->assertContains($topicId, $response['body']['topics']);
// Cleanup
$this->client->call(Client::METHOD_DELETE, '/messaging/messages/' . $messageId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]);
$this->client->call(Client::METHOD_DELETE, '/messaging/messages/' . $messageId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getDestinationProject()['$id'],
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
]);
$this->client->call(Client::METHOD_DELETE, '/messaging/topics/' . $topicId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]);
$this->client->call(Client::METHOD_DELETE, '/messaging/topics/' . $topicId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getDestinationProject()['$id'],
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
]);
$this->client->call(Client::METHOD_DELETE, '/messaging/providers/' . $providerId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]);
$this->client->call(Client::METHOD_DELETE, '/messaging/providers/' . $providerId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getDestinationProject()['$id'],
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
]);
$this->client->call(Client::METHOD_DELETE, '/users/' . $userId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]);
$this->client->call(Client::METHOD_DELETE, '/users/' . $userId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getDestinationProject()['$id'],
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
]);
}
public function testAppwriteMigrationMessagingSmsMessage(): void
{
$this->getDestinationProject(true);
$user = $this->client->call(Client::METHOD_POST, '/users', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'userId' => ID::unique(),
'email' => uniqid() . '-migration-sms@test.com',
'phone' => '+1' . str_pad((string) rand(200000000, 999999999), 10, '0', STR_PAD_LEFT),
'password' => 'password',
]);
$this->assertEquals(201, $user['headers']['status-code']);
$userId = $user['body']['$id'];
$this->assertGreaterThanOrEqual(1, \count($user['body']['targets']));
$smsTarget = null;
foreach ($user['body']['targets'] as $target) {
if ($target['providerType'] === 'sms') {
$smsTarget = $target;
break;
}
}
$this->assertNotNull($smsTarget);
$targetId = $smsTarget['$id'];
$provider = $this->client->call(Client::METHOD_POST, '/messaging/providers/twilio', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'providerId' => ID::unique(),
'name' => 'Migration Twilio SMS Msg',
'from' => '+15559876543',
'accountSid' => 'test-account-sid',
'authToken' => 'test-auth-token',
]);
$this->assertEquals(201, $provider['headers']['status-code']);
$providerId = $provider['body']['$id'];
$topic = $this->client->call(Client::METHOD_POST, '/messaging/topics', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'topicId' => ID::unique(),
'name' => 'Migration SMS Topic',
]);
$this->assertEquals(201, $topic['headers']['status-code']);
$topicId = $topic['body']['$id'];
$message = $this->client->call(Client::METHOD_POST, '/messaging/messages/sms', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'messageId' => ID::unique(),
'targets' => [$targetId],
'topics' => [$topicId],
'content' => 'Migration SMS test content',
'draft' => true,
]);
$this->assertEquals(201, $message['headers']['status-code']);
$messageId = $message['body']['$id'];
$result = $this->performMigrationSync([
'resources' => [
Resource::TYPE_USER,
Resource::TYPE_PROVIDER,
Resource::TYPE_TOPIC,
Resource::TYPE_SUBSCRIBER,
Resource::TYPE_MESSAGE,
],
'endpoint' => $this->webEndpoint,
'projectId' => $this->getProject()['$id'],
'apiKey' => $this->getProject()['apiKey'],
]);
$this->assertEquals('completed', $result['status']);
$this->assertArrayHasKey(Resource::TYPE_MESSAGE, $result['statusCounters']);
$this->assertEquals(0, $result['statusCounters'][Resource::TYPE_MESSAGE]['error']);
$this->assertGreaterThanOrEqual(1, $result['statusCounters'][Resource::TYPE_MESSAGE]['success']);
$response = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $messageId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getDestinationProject()['$id'],
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals($messageId, $response['body']['$id']);
$this->assertEquals('draft', $response['body']['status']);
$this->assertEquals('Migration SMS test content', $response['body']['data']['content']);
// Cleanup
$this->client->call(Client::METHOD_DELETE, '/messaging/messages/' . $messageId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]);
$this->client->call(Client::METHOD_DELETE, '/messaging/messages/' . $messageId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getDestinationProject()['$id'],
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
]);
$this->client->call(Client::METHOD_DELETE, '/messaging/topics/' . $topicId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]);
$this->client->call(Client::METHOD_DELETE, '/messaging/topics/' . $topicId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getDestinationProject()['$id'],
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
]);
$this->client->call(Client::METHOD_DELETE, '/messaging/providers/' . $providerId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]);
$this->client->call(Client::METHOD_DELETE, '/messaging/providers/' . $providerId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getDestinationProject()['$id'],
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
]);
$this->client->call(Client::METHOD_DELETE, '/users/' . $userId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]);
$this->client->call(Client::METHOD_DELETE, '/users/' . $userId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getDestinationProject()['$id'],
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
]);
}
public function testAppwriteMigrationMessagingScheduledMessage(): void
{
$this->getDestinationProject(true);
$user = $this->client->call(Client::METHOD_POST, '/users', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'userId' => ID::unique(),
'email' => uniqid() . '-migration-sched@test.com',
'password' => 'password',
]);
$this->assertEquals(201, $user['headers']['status-code']);
$userId = $user['body']['$id'];
$targetId = $user['body']['targets'][0]['$id'];
$provider = $this->client->call(Client::METHOD_POST, '/messaging/providers/sendgrid', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'providerId' => ID::unique(),
'name' => 'Migration Sendgrid Scheduled',
'apiKey' => 'my-apikey',
'from' => 'migration-sched@test.com',
]);
$this->assertEquals(201, $provider['headers']['status-code']);
$providerId = $provider['body']['$id'];
$topic = $this->client->call(Client::METHOD_POST, '/messaging/topics', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'topicId' => ID::unique(),
'name' => 'Migration Scheduled Topic',
]);
$this->assertEquals(201, $topic['headers']['status-code']);
$topicId = $topic['body']['$id'];
$subscriber = $this->client->call(Client::METHOD_POST, '/messaging/topics/' . $topicId . '/subscribers', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'subscriberId' => ID::unique(),
'targetId' => $targetId,
]);
$this->assertEquals(201, $subscriber['headers']['status-code']);
// Create a scheduled message with a future date using topics only
// Direct targets use source IDs which won't resolve in the destination via API
$futureDate = (new \DateTime('+1 year'))->format(\DateTime::ATOM);
$message = $this->client->call(Client::METHOD_POST, '/messaging/messages/email', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'messageId' => ID::unique(),
'topics' => [$topicId],
'subject' => 'Migration Scheduled Email',
'content' => 'This is a scheduled migration test email',
'scheduledAt' => $futureDate,
]);
$this->assertEquals(201, $message['headers']['status-code']);
$messageId = $message['body']['$id'];
$this->assertEquals('scheduled', $message['body']['status']);
$result = $this->performMigrationSync([
'resources' => [
Resource::TYPE_USER,
Resource::TYPE_PROVIDER,
Resource::TYPE_TOPIC,
Resource::TYPE_SUBSCRIBER,
Resource::TYPE_MESSAGE,
],
'endpoint' => $this->webEndpoint,
'projectId' => $this->getProject()['$id'],
'apiKey' => $this->getProject()['apiKey'],
]);
$this->assertEquals('completed', $result['status']);
$this->assertArrayHasKey(Resource::TYPE_MESSAGE, $result['statusCounters']);
$this->assertEquals(0, $result['statusCounters'][Resource::TYPE_MESSAGE]['error']);
$this->assertGreaterThanOrEqual(1, $result['statusCounters'][Resource::TYPE_MESSAGE]['success']);
$response = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $messageId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getDestinationProject()['$id'],
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals($messageId, $response['body']['$id']);
$this->assertEquals('scheduled', $response['body']['status']);
$this->assertEquals('Migration Scheduled Email', $response['body']['data']['subject']);
$this->assertEquals(
(new \DateTime($futureDate))->getTimestamp(),
(new \DateTime($response['body']['scheduledAt']))->getTimestamp(),
);
// Cleanup
$this->client->call(Client::METHOD_DELETE, '/messaging/messages/' . $messageId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]);
$this->client->call(Client::METHOD_DELETE, '/messaging/messages/' . $messageId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getDestinationProject()['$id'],
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
]);
$this->client->call(Client::METHOD_DELETE, '/messaging/topics/' . $topicId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]);
$this->client->call(Client::METHOD_DELETE, '/messaging/topics/' . $topicId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getDestinationProject()['$id'],
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
]);
$this->client->call(Client::METHOD_DELETE, '/messaging/providers/' . $providerId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]);
$this->client->call(Client::METHOD_DELETE, '/messaging/providers/' . $providerId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getDestinationProject()['$id'],
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
]);
$this->client->call(Client::METHOD_DELETE, '/users/' . $userId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]);
$this->client->call(Client::METHOD_DELETE, '/users/' . $userId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getDestinationProject()['$id'],
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
]);
}
}
@@ -180,12 +180,12 @@ class SitesConsoleClientTest extends Scope
$stdout = '';
$stderr = '';
$code = Console::execute("docker exec appwrite-task-maintenance time-travel --projectId={$this->getProject()['$id']} --resourceType=deployment --resourceId={$deploymentIdInactiveOld} --createdAt=2020-01-01T00:00:00Z", '', $stdout, $stderr);
$code = Console::execute("docker exec appwrite task-time-travel --projectId={$this->getProject()['$id']} --resourceType=deployment --resourceId={$deploymentIdInactiveOld} --createdAt=2020-01-01T00:00:00Z", '', $stdout, $stderr);
$this->assertSame(0, $code, "Time-travel command failed with code $code: $stderr ($stdout)");
$stdout = '';
$stderr = '';
$code = Console::execute("docker exec appwrite-task-maintenance maintenance --type=trigger", '', $stdout, $stderr);
$code = Console::execute("docker exec appwrite maintenance --type=trigger", '', $stdout, $stderr);
$this->assertSame(0, $code, "Maintenance command failed with code $code: $stderr ($stdout)");
$this->assertEventually(function () use ($siteId) {
+54 -2
View File
@@ -1596,7 +1596,7 @@ trait UsersBase
]);
$this->assertEquals($user['headers']['status-code'], 200);
$this->assertEquals($user['body']['phone'], $updatedNumber);
$this->assertEmpty($user['body']['phone'] ?? '');
$user = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'], array_merge([
'content-type' => 'application/json',
@@ -1604,7 +1604,7 @@ trait UsersBase
], $this->getHeaders()));
$this->assertEquals($user['headers']['status-code'], 200);
$this->assertEquals($user['body']['phone'], $updatedNumber);
$this->assertEmpty($user['body']['phone'] ?? '');
$updatedNumber = "+910000000000"; //dummy number
$user = $this->client->call(Client::METHOD_PATCH, '/users/' . $data['userId'] . '/phone', array_merge([
@@ -1648,6 +1648,58 @@ trait UsersBase
static::$userNumberUpdated = true;
}
public function testUpdateTwoUsersPhoneToEmpty(): void
{
$projectId = $this->getProject()['$id'];
$headers = array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $this->getHeaders());
// Create two users with distinct valid phone numbers
$user1 = $this->client->call(Client::METHOD_POST, '/users', $headers, [
'userId' => ID::unique(),
'email' => 'user1-phone-empty-test@appwrite.io',
'password' => 'password',
'name' => 'User One',
'phone' => '+16175551201',
]);
$this->assertEquals(201, $user1['headers']['status-code']);
$this->assertEquals('+16175551201', $user1['body']['phone']);
$user2 = $this->client->call(Client::METHOD_POST, '/users', $headers, [
'userId' => ID::unique(),
'email' => 'user2-phone-empty-test@appwrite.io',
'password' => 'password',
'name' => 'User Two',
'phone' => '+16175551202',
]);
$this->assertEquals(201, $user2['headers']['status-code']);
$this->assertEquals('+16175551202', $user2['body']['phone']);
// Update first user's phone to empty - must succeed
$response1 = $this->client->call(Client::METHOD_PATCH, '/users/' . $user1['body']['$id'] . '/phone', $headers, [
'number' => '',
]);
$this->assertEquals(200, $response1['headers']['status-code'], 'First user phone should update to empty');
$this->assertEmpty($response1['body']['phone'] ?? '');
// Update second user's phone to empty - must succeed (would fail with duplicate if empty was stored as '')
$response2 = $this->client->call(Client::METHOD_PATCH, '/users/' . $user2['body']['$id'] . '/phone', $headers, [
'number' => '',
]);
$this->assertEquals(200, $response2['headers']['status-code'], 'Second user phone should update to empty without duplicate error');
$this->assertEmpty($response2['body']['phone'] ?? '');
// Verify both users have empty phone via GET
$get1 = $this->client->call(Client::METHOD_GET, '/users/' . $user1['body']['$id'], $headers);
$get2 = $this->client->call(Client::METHOD_GET, '/users/' . $user2['body']['$id'], $headers);
$this->assertEquals(200, $get1['headers']['status-code']);
$this->assertEquals(200, $get2['headers']['status-code']);
$this->assertEmpty($get1['body']['phone'] ?? '');
$this->assertEmpty($get2['body']['phone'] ?? '');
}
public function testUpdateUserNumberSearch(): void
{
$data = $this->ensureUserNumberUpdated();