Merge branch '1.8.x' of github.com:appwrite/appwrite into add-webhooks-and-functions-events

This commit is contained in:
shimon
2026-01-06 18:32:00 +02:00
21 changed files with 576 additions and 167 deletions
+11 -11
View File
@@ -223,7 +223,7 @@ jobs:
-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/${{ matrix.service }} --debug --exclude-group devKeys,screenshots
appwrite test /usr/src/code/tests/e2e/Services/${{ matrix.service }} --debug --exclude-group abuseEnabled,screenshots
- name: Failure Logs
if: failure()
@@ -312,7 +312,7 @@ jobs:
-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/${{ matrix.service }} --debug --exclude-group devKeys,screenshots
appwrite test /usr/src/code/tests/e2e/Services/${{ matrix.service }} --debug --exclude-group abuseEnabled,screenshots
- name: Failure Logs
if: failure()
@@ -322,8 +322,8 @@ jobs:
echo "=== OpenRuntimes Executor Logs ==="
docker compose logs openruntimes-executor
e2e_dev_keys:
name: E2E Service Test (Dev Keys)
e2e_abuse_enabled:
name: E2E Service Test (Abuse enabled)
runs-on: ubuntu-latest
needs: setup
steps:
@@ -344,7 +344,7 @@ jobs:
docker compose up -d
sleep 30
- name: Run Projects tests with dev keys in dedicated table mode
- name: Run Projects tests in dedicated table mode
run: |
echo "Using project tables"
export _APP_DATABASE_SHARED_TABLES=
@@ -354,7 +354,7 @@ jobs:
-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=devKeys
appwrite test /usr/src/code/tests/e2e/Services/Projects --debug --group=abuseEnabled
- name: Failure Logs
if: failure()
@@ -364,8 +364,8 @@ jobs:
echo "=== OpenRuntimes Executor Logs ==="
docker compose logs openruntimes-executor
e2e_dev_keys_shared_mode:
name: E2E Shared Mode Service Test (Dev Keys)
e2e_abuse_enabled_shared_mode:
name: E2E Shared Mode Service Test (Abuse enabled)
runs-on: ubuntu-latest
needs: [ setup, check_database_changes ]
if: needs.check_database_changes.outputs.database_changed == 'true'
@@ -394,7 +394,7 @@ jobs:
docker compose up -d
sleep 30
- name: Run Projects tests with dev keys in ${{ matrix.tables-mode }} table mode
- name: Run Projects tests in ${{ matrix.tables-mode }} table mode
run: |
if [ "${{ matrix.tables-mode }}" == "Shared V1" ]; then
echo "Using shared tables V1"
@@ -410,7 +410,7 @@ jobs:
-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=devKeys
appwrite test /usr/src/code/tests/e2e/Services/Projects --debug --group=abuseEnabled
- name: Failure Logs
if: failure()
@@ -420,7 +420,7 @@ jobs:
echo "=== OpenRuntimes Executor Logs ==="
docker compose logs openruntimes-executor
e2e_screenshots_keys:
e2e_screenshots:
name: E2E Service Test (Site Screenshots)
runs-on: ubuntu-latest
needs: setup
+24 -13
View File
@@ -330,7 +330,18 @@ $platformCollections = [
'default' => null,
'array' => false,
'filters' => ['datetime'],
]
],
[
'$id' => ID::custom('labels'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 128,
'signed' => true,
'required' => false,
'default' => [],
'array' => true,
'filters' => [],
],
],
'indexes' => [
[
@@ -1402,21 +1413,21 @@ $platformCollections = [
'$id' => '_key_type',
'type' => Database::INDEX_KEY,
'attributes' => ['type'],
'lengths' => [32],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => '_key_trigger',
'type' => Database::INDEX_KEY,
'attributes' => ['trigger'],
'lengths' => [32],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => '_key_deploymentResourceType',
'type' => Database::INDEX_KEY,
'attributes' => ['deploymentResourceType'],
'lengths' => [32],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
@@ -1458,23 +1469,23 @@ $platformCollections = [
'$id' => ID::custom('_key_owner'),
'type' => Database::INDEX_KEY,
'attributes' => ['owner'],
'lengths' => [16],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_region'),
'type' => Database::INDEX_KEY,
'attributes' => ['region'],
'lengths' => [16],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_piid_riid_rt'),
'$id' => ID::custom('_key_piid_diid_drt'),
'type' => Database::INDEX_KEY,
'attributes' => ['projectInternalId', 'deploymentInternalId', 'deploymentResourceType'],
'lengths' => [],
'orders' => [],
],
[
'$id' => '_key_region_status_createdAt',
'type' => Database::INDEX_KEY,
'attributes' => ['region', 'status', '$createdAt'],
'lengths' => [],
'orders' => [],
],
],
],
+3
View File
@@ -959,6 +959,7 @@ App::post('/v1/account/sessions/email')
))
->label('abuse-limit', 10)
->label('abuse-key', 'url:{url},email:{param-email}')
->label('abuse-reset', [201])
->param('email', '', new EmailValidator(), 'User email.')
->param('password', '', new Password(), 'User password. Must be at least 8 chars.')
->inject('request')
@@ -1257,6 +1258,7 @@ App::post('/v1/account/sessions/token')
))
->label('abuse-limit', 10)
->label('abuse-key', 'ip:{ip},userId:{param-userId}')
->label('abuse-reset', [201])
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('secret', '', new Text(256), 'Secret of a token generated by login methods. For example, the `createMagicURLToken` or `createPhoneToken` methods.')
->inject('request')
@@ -2645,6 +2647,7 @@ App::put('/v1/account/sessions/magic-url')
))
->label('abuse-limit', 10)
->label('abuse-key', 'ip:{ip},userId:{param-userId}')
->label('abuse-reset', [201])
->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('secret', '', new Text(256), 'Valid verification token.')
->inject('request')
+1 -1
View File
@@ -697,7 +697,7 @@ App::get('/v1/avatars/screenshots')
}
$client = new Client();
$client->setTimeout(30);
$client->setTimeout(30 * 1000); // 30 seconds
$client->addHeader('content-type', Client::CONTENT_TYPE_APPLICATION_JSON);
// Convert indexed array to empty array (should not happen due to Assoc validator)
+10 -28
View File
@@ -204,6 +204,7 @@ App::post('/v1/projects')
'accessedAt' => DateTime::now(),
'search' => implode(' ', [$projectId, $name]),
'database' => $dsn,
'labels' => [],
]));
} catch (Duplicate) {
throw new Exception(Exception::PROJECT_ALREADY_EXISTS);
@@ -1500,6 +1501,7 @@ App::post('/v1/projects/:projectId/keys')
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
// TODO: @hmacr Remove `projectInternalId` and `projectId` column writes before deleting the column.
'projectInternalId' => $project->getSequence(),
'projectId' => $project->getId(),
'resourceInternalId' => $project->getSequence(),
@@ -1552,13 +1554,8 @@ App::get('/v1/projects/:projectId/keys')
}
$keys = $dbForPlatform->find('keys', [
Query::or([
Query::equal('projectInternalId', [$project->getSequence()]),
Query::and([
Query::equal('resourceType', ['projects']),
Query::equal('resourceInternalId', [$project->getSequence()]),
])
]),
Query::equal('resourceType', ['projects']),
Query::equal('resourceInternalId', [$project->getSequence()]),
Query::limit(5000),
]);
@@ -1599,13 +1596,8 @@ App::get('/v1/projects/:projectId/keys/:keyId')
$key = $dbForPlatform->findOne('keys', [
Query::equal('$id', [$keyId]),
Query::or([
Query::equal('projectInternalId', [$project->getSequence()]),
Query::and([
Query::equal('resourceType', ['projects']),
Query::equal('resourceInternalId', [$project->getSequence()]),
])
])
Query::equal('resourceType', ['projects']),
Query::equal('resourceInternalId', [$project->getSequence()]),
]);
if ($key->isEmpty()) {
@@ -1649,13 +1641,8 @@ App::put('/v1/projects/:projectId/keys/:keyId')
$key = $dbForPlatform->findOne('keys', [
Query::equal('$id', [$keyId]),
Query::or([
Query::equal('projectInternalId', [$project->getSequence()]),
Query::and([
Query::equal('resourceType', ['projects']),
Query::equal('resourceInternalId', [$project->getSequence()]),
])
])
Query::equal('resourceType', ['projects']),
Query::equal('resourceInternalId', [$project->getSequence()]),
]);
if ($key->isEmpty()) {
@@ -1706,13 +1693,8 @@ App::delete('/v1/projects/:projectId/keys/:keyId')
$key = $dbForPlatform->findOne('keys', [
Query::equal('$id', [$keyId]),
Query::or([
Query::equal('projectInternalId', [$project->getSequence()]),
Query::and([
Query::equal('resourceType', ['projects']),
Query::equal('resourceInternalId', [$project->getSequence()]),
])
])
Query::equal('resourceType', ['projects']),
Query::equal('resourceInternalId', [$project->getSequence()]),
]);
if ($key->isEmpty()) {
+4
View File
@@ -200,8 +200,12 @@ App::post('/v1/mock/api-key-unprefixed')
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
// TODO: @hmacr Remove `projectInternalId` and `projectId` column writes before deleting the column.
'projectInternalId' => $project->getSequence(),
'projectId' => $project->getId(),
'resourceInternalId' => $project->getSequence(),
'resourceId' => $project->getId(),
'resourceType' => 'projects',
'name' => 'Outdated key',
'scopes' => $scopes,
'expire' => null,
+37 -1
View File
@@ -845,7 +845,8 @@ App::shutdown()
->inject('queueForWebhooks')
->inject('queueForRealtime')
->inject('dbForProject')
->action(function (App $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Audit $queueForAudits, StatsUsage $queueForStatsUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject) use ($parseLabel) {
->inject('timelimit')
->action(function (App $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Audit $queueForAudits, StatsUsage $queueForStatsUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, callable $timelimit) use ($parseLabel) {
$responsePayload = $response->getPayload();
@@ -879,6 +880,41 @@ App::shutdown()
$route = $utopia->getRoute();
$requestParams = $route->getParamsValues();
/**
* Abuse labels
*/
$abuseEnabled = System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled';
$abuseResetCode = $route->getLabel('abuse-reset', []);
$abuseResetCode = \is_array($abuseResetCode) ? $abuseResetCode : [$abuseResetCode];
if ($abuseEnabled && \count($abuseResetCode) > 0 && \in_array($response->getStatusCode(), $abuseResetCode)) {
$abuseKeyLabel = $route->getLabel('abuse-key', 'url:{url},ip:{ip}');
$abuseKeyLabel = (!is_array($abuseKeyLabel)) ? [$abuseKeyLabel] : $abuseKeyLabel;
foreach ($abuseKeyLabel as $abuseKey) {
$start = $request->getContentRangeStart();
$end = $request->getContentRangeEnd();
$timeLimit = $timelimit($abuseKey, $route->getLabel('abuse-limit', 0), $route->getLabel('abuse-time', 3600));
$timeLimit
->setParam('{projectId}', $project->getId())
->setParam('{userId}', $user->getId())
->setParam('{userAgent}', $request->getUserAgent(''))
->setParam('{ip}', $request->getIP())
->setParam('{url}', $request->getHostname() . $route->getPath())
->setParam('{method}', $request->getMethod())
->setParam('{chunkId}', (int)($start / ($end + 1 - $start)));
foreach ($request->getParams() as $key => $value) { // Set request params as potential abuse keys
if (!empty($value)) {
$timeLimit->setParam('{param-' . $key . '}', (\is_array($value)) ? \json_encode($value) : $value);
}
}
$abuse = new Abuse($timeLimit);
$abuse->reset();
}
}
/**
* Audit labels
*/
+2 -7
View File
@@ -136,13 +136,8 @@ Database::addFilter(
function (mixed $value, Document $document, Database $database) {
return $database
->find('keys', [
Query::or([
Query::equal('projectInternalId', [$document->getSequence()]),
Query::and([
Query::equal('resourceType', ['projects']),
Query::equal('resourceInternalId', [$document->getSequence()]),
])
]),
Query::equal('resourceType', ['projects']),
Query::equal('resourceInternalId', [$document->getSequence()]),
Query::limit(APP_LIMIT_SUBQUERY),
]);
}
+2
View File
@@ -360,6 +360,8 @@ $register->set('smtp', function () {
$mail->SMTPSecure = System::getEnv('_APP_SMTP_SECURE', '');
$mail->SMTPAutoTLS = false;
$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);
+4 -4
View File
@@ -45,9 +45,9 @@
"ext-sockets": "*",
"appwrite/php-runtimes": "0.19.*",
"appwrite/php-clamav": "2.0.*",
"utopia-php/abuse": "1.*",
"utopia-php/abuse": "1.*.*",
"utopia-php/analytics": "0.10.*",
"utopia-php/audit": "2.0.2-rc1",
"utopia-php/audit": "2.0.2-rc3",
"utopia-php/auth": "0.5.*",
"utopia-php/cache": "0.13.*",
"utopia-php/cli": "0.15.*",
@@ -59,12 +59,12 @@
"utopia-php/dns": "1.4.*",
"utopia-php/dsn": "0.2.1",
"utopia-php/framework": "0.33.*",
"utopia-php/fetch": "0.4.*",
"utopia-php/fetch": "0.5.*",
"utopia-php/image": "0.8.*",
"utopia-php/locale": "0.8.*",
"utopia-php/logger": "0.6.*",
"utopia-php/messaging": "0.20.*",
"utopia-php/migration": "1.3.*",
"utopia-php/migration": "1.*.*",
"utopia-php/orchestration": "0.9.*",
"utopia-php/platform": "0.7.*",
"utopia-php/pools": "0.8.*",
Generated
+71 -71
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": "b873febd2b03c32ec61a57b690cc44a2",
"content-hash": "9bac4d8946e35357efa46fd087c9484e",
"packages": [
{
"name": "adhocore/jwt",
@@ -69,16 +69,16 @@
},
{
"name": "appwrite/appwrite",
"version": "15.1.0",
"version": "19.1.0",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-for-php.git",
"reference": "c438b3885071ac7c0329199dce5e6f6a24dd215b"
"reference": "8738e812062f899c85b2598eef43d6a247f08a56"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/c438b3885071ac7c0329199dce5e6f6a24dd215b",
"reference": "c438b3885071ac7c0329199dce5e6f6a24dd215b",
"url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/8738e812062f899c85b2598eef43d6a247f08a56",
"reference": "8738e812062f899c85b2598eef43d6a247f08a56",
"shasum": ""
},
"require": {
@@ -87,7 +87,7 @@
"php": ">=7.1.0"
},
"require-dev": {
"mockery/mockery": "^1.6.6",
"mockery/mockery": "^1.6.12",
"phpunit/phpunit": "^10"
},
"type": "library",
@@ -104,10 +104,10 @@
"support": {
"email": "team@appwrite.io",
"issues": "https://github.com/appwrite/sdk-for-php/issues",
"source": "https://github.com/appwrite/sdk-for-php/tree/15.1.0",
"source": "https://github.com/appwrite/sdk-for-php/tree/19.1.0",
"url": "https://appwrite.io/support"
},
"time": "2025-08-01T04:50:51+00:00"
"time": "2025-12-18T08:07:43+00:00"
},
{
"name": "appwrite/php-clamav",
@@ -3455,24 +3455,25 @@
},
{
"name": "utopia-php/abuse",
"version": "1.0.2",
"version": "1.2.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/abuse.git",
"reference": "611fa66a97e87c0dbbc133a717d970da7a5ca828"
"reference": "3339d057c6bb1fa3e5ac5b2598923f6938425ec2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/abuse/zipball/611fa66a97e87c0dbbc133a717d970da7a5ca828",
"reference": "611fa66a97e87c0dbbc133a717d970da7a5ca828",
"url": "https://api.github.com/repos/utopia-php/abuse/zipball/3339d057c6bb1fa3e5ac5b2598923f6938425ec2",
"reference": "3339d057c6bb1fa3e5ac5b2598923f6938425ec2",
"shasum": ""
},
"require": {
"appwrite/appwrite": "19.*.*",
"ext-curl": "*",
"ext-pdo": "*",
"ext-redis": "*",
"php": ">=8.0",
"utopia-php/database": "*"
"utopia-php/database": "3.*.*"
},
"require-dev": {
"laravel/pint": "1.*",
@@ -3500,9 +3501,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/abuse/issues",
"source": "https://github.com/utopia-php/abuse/tree/1.0.2"
"source": "https://github.com/utopia-php/abuse/tree/1.2.0"
},
"time": "2025-10-20T07:18:33+00:00"
"time": "2026-01-05T21:29:10+00:00"
},
{
"name": "utopia-php/analytics",
@@ -3552,23 +3553,23 @@
},
{
"name": "utopia-php/audit",
"version": "2.0.2-rc1",
"version": "2.0.2-rc3",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/audit.git",
"reference": "7b35dab40bce66bda56eeeacd2bbcbf1e823f05f"
"reference": "f60a298b516300f56a328403b334b7d62a96e7e7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/audit/zipball/7b35dab40bce66bda56eeeacd2bbcbf1e823f05f",
"reference": "7b35dab40bce66bda56eeeacd2bbcbf1e823f05f",
"url": "https://api.github.com/repos/utopia-php/audit/zipball/f60a298b516300f56a328403b334b7d62a96e7e7",
"reference": "f60a298b516300f56a328403b334b7d62a96e7e7",
"shasum": ""
},
"require": {
"php": ">=8.0",
"utopia-php/database": "3.*",
"utopia-php/fetch": "^0.4.2",
"utopia-php/validators": "^0.1.0"
"utopia-php/fetch": "0.5.*",
"utopia-php/validators": "0.1.*"
},
"require-dev": {
"laravel/pint": "1.*",
@@ -3595,9 +3596,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/audit/issues",
"source": "https://github.com/utopia-php/audit/tree/2.0.2-rc1"
"source": "https://github.com/utopia-php/audit/tree/2.0.2-rc3"
},
"time": "2025-12-24T01:20:43+00:00"
"time": "2026-01-06T15:32:52+00:00"
},
{
"name": "utopia-php/auth",
@@ -4167,23 +4168,23 @@
},
{
"name": "utopia-php/emails",
"version": "0.6.3",
"version": "0.6.4",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/emails.git",
"reference": "9524d7f7bd1651a06fef8a3d964f774b04fe2918"
"reference": "fb2bd5c428e88f645b0f7ede0dd29ac0d120ec52"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/emails/zipball/9524d7f7bd1651a06fef8a3d964f774b04fe2918",
"reference": "9524d7f7bd1651a06fef8a3d964f774b04fe2918",
"url": "https://api.github.com/repos/utopia-php/emails/zipball/fb2bd5c428e88f645b0f7ede0dd29ac0d120ec52",
"reference": "fb2bd5c428e88f645b0f7ede0dd29ac0d120ec52",
"shasum": ""
},
"require": {
"php": ">=8.0",
"utopia-php/cli": "^0.15",
"utopia-php/domains": "^0.9",
"utopia-php/fetch": "^0.4",
"utopia-php/fetch": "^0.5",
"utopia-php/validators": "0.*"
},
"require-dev": {
@@ -4221,26 +4222,26 @@
],
"support": {
"issues": "https://github.com/utopia-php/emails/issues",
"source": "https://github.com/utopia-php/emails/tree/0.6.3"
"source": "https://github.com/utopia-php/emails/tree/0.6.4"
},
"time": "2025-11-26T12:27:47+00:00"
"time": "2025-12-18T16:36:50+00:00"
},
{
"name": "utopia-php/fetch",
"version": "0.4.2",
"version": "0.5.1",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/fetch.git",
"reference": "83986d1be75a2fae4e684107fe70dd78a8e19b77"
"reference": "a96a010e1c273f3888765449687baf58cbc61fcd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/fetch/zipball/83986d1be75a2fae4e684107fe70dd78a8e19b77",
"reference": "83986d1be75a2fae4e684107fe70dd78a8e19b77",
"url": "https://api.github.com/repos/utopia-php/fetch/zipball/a96a010e1c273f3888765449687baf58cbc61fcd",
"reference": "a96a010e1c273f3888765449687baf58cbc61fcd",
"shasum": ""
},
"require": {
"php": ">=8.0"
"php": ">=8.1"
},
"require-dev": {
"laravel/pint": "^1.5.0",
@@ -4260,9 +4261,9 @@
"description": "A simple library that provides an interface for making HTTP Requests.",
"support": {
"issues": "https://github.com/utopia-php/fetch/issues",
"source": "https://github.com/utopia-php/fetch/tree/0.4.2"
"source": "https://github.com/utopia-php/fetch/tree/0.5.1"
},
"time": "2025-04-25T13:48:02+00:00"
"time": "2025-12-18T16:25:10+00:00"
},
{
"name": "utopia-php/framework",
@@ -4515,20 +4516,20 @@
},
{
"name": "utopia-php/migration",
"version": "1.3.9",
"version": "1.3.10",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/migration.git",
"reference": "c55ec67c74663190cda10fd79297422147be7e85"
"reference": "cb357c42a5a5614605b546effbea1204ed64c6b0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/migration/zipball/c55ec67c74663190cda10fd79297422147be7e85",
"reference": "c55ec67c74663190cda10fd79297422147be7e85",
"url": "https://api.github.com/repos/utopia-php/migration/zipball/cb357c42a5a5614605b546effbea1204ed64c6b0",
"reference": "cb357c42a5a5614605b546effbea1204ed64c6b0",
"shasum": ""
},
"require": {
"appwrite/appwrite": "15.*",
"appwrite/appwrite": "19.*",
"ext-curl": "*",
"ext-openssl": "*",
"php": ">=8.1",
@@ -4564,9 +4565,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/migration/issues",
"source": "https://github.com/utopia-php/migration/tree/1.3.9"
"source": "https://github.com/utopia-php/migration/tree/1.3.10"
},
"time": "2025-12-08T08:45:09+00:00"
"time": "2026-01-06T10:47:11+00:00"
},
{
"name": "utopia-php/mongo",
@@ -4837,23 +4838,23 @@
},
{
"name": "utopia-php/queue",
"version": "0.11.2",
"version": "0.11.3",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/queue.git",
"reference": "a854f7c4abc18e0eca55fc5608cd7088d71eb19f"
"reference": "f3b2623efe87595c9ed907b3efd587e77c622d3d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/queue/zipball/a854f7c4abc18e0eca55fc5608cd7088d71eb19f",
"reference": "a854f7c4abc18e0eca55fc5608cd7088d71eb19f",
"url": "https://api.github.com/repos/utopia-php/queue/zipball/f3b2623efe87595c9ed907b3efd587e77c622d3d",
"reference": "f3b2623efe87595c9ed907b3efd587e77c622d3d",
"shasum": ""
},
"require": {
"php": ">=8.3",
"php-amqplib/php-amqplib": "^3.7",
"utopia-php/cli": "0.15.*",
"utopia-php/fetch": "0.4.*",
"utopia-php/fetch": "0.5.*",
"utopia-php/framework": "0.33.*",
"utopia-php/pools": "0.8.*",
"utopia-php/telemetry": "*"
@@ -4897,9 +4898,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/queue/issues",
"source": "https://github.com/utopia-php/queue/tree/0.11.2"
"source": "https://github.com/utopia-php/queue/tree/0.11.3"
},
"time": "2025-12-17T09:32:35+00:00"
"time": "2025-12-19T10:56:22+00:00"
},
{
"name": "utopia-php/registry",
@@ -5566,30 +5567,29 @@
},
{
"name": "doctrine/instantiator",
"version": "2.0.0",
"version": "2.1.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/instantiator.git",
"reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0"
"reference": "23da848e1a2308728fe5fdddabf4be17ff9720c7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0",
"reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0",
"url": "https://api.github.com/repos/doctrine/instantiator/zipball/23da848e1a2308728fe5fdddabf4be17ff9720c7",
"reference": "23da848e1a2308728fe5fdddabf4be17ff9720c7",
"shasum": ""
},
"require": {
"php": "^8.1"
"php": "^8.4"
},
"require-dev": {
"doctrine/coding-standard": "^11",
"doctrine/coding-standard": "^14",
"ext-pdo": "*",
"ext-phar": "*",
"phpbench/phpbench": "^1.2",
"phpstan/phpstan": "^1.9.4",
"phpstan/phpstan-phpunit": "^1.3",
"phpunit/phpunit": "^9.5.27",
"vimeo/psalm": "^5.4"
"phpstan/phpstan": "^2.1",
"phpstan/phpstan-phpunit": "^2.0",
"phpunit/phpunit": "^10.5.58"
},
"type": "library",
"autoload": {
@@ -5616,7 +5616,7 @@
],
"support": {
"issues": "https://github.com/doctrine/instantiator/issues",
"source": "https://github.com/doctrine/instantiator/tree/2.0.0"
"source": "https://github.com/doctrine/instantiator/tree/2.1.0"
},
"funding": [
{
@@ -5632,7 +5632,7 @@
"type": "tidelift"
}
],
"time": "2022-12-30T00:23:10+00:00"
"time": "2026-01-05T06:47:08+00:00"
},
{
"name": "doctrine/lexer",
@@ -5713,16 +5713,16 @@
},
{
"name": "laravel/pint",
"version": "v1.26.0",
"version": "v1.27.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/pint.git",
"reference": "69dcca060ecb15e4b564af63d1f642c81a241d6f"
"reference": "c67b4195b75491e4dfc6b00b1c78b68d86f54c90"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/pint/zipball/69dcca060ecb15e4b564af63d1f642c81a241d6f",
"reference": "69dcca060ecb15e4b564af63d1f642c81a241d6f",
"url": "https://api.github.com/repos/laravel/pint/zipball/c67b4195b75491e4dfc6b00b1c78b68d86f54c90",
"reference": "c67b4195b75491e4dfc6b00b1c78b68d86f54c90",
"shasum": ""
},
"require": {
@@ -5733,9 +5733,9 @@
"php": "^8.2.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.90.0",
"illuminate/view": "^12.40.1",
"larastan/larastan": "^3.8.0",
"friendsofphp/php-cs-fixer": "^3.92.4",
"illuminate/view": "^12.44.0",
"larastan/larastan": "^3.8.1",
"laravel-zero/framework": "^12.0.4",
"mockery/mockery": "^1.6.12",
"nunomaduro/termwind": "^2.3.3",
@@ -5776,7 +5776,7 @@
"issues": "https://github.com/laravel/pint/issues",
"source": "https://github.com/laravel/pint"
},
"time": "2025-11-25T21:15:52+00:00"
"time": "2026-01-05T16:49:17+00:00"
},
{
"name": "matthiasmullie/minify",
@@ -939,7 +939,7 @@ class Builds extends Action
}
$client = new FetchClient();
$client->setTimeout(\intval($resource->getAttribute('timeout', '15')));
$client->setTimeout(\intval($resource->getAttribute('timeout', '15')) * 1000);
$client->addHeader('content-type', FetchClient::CONTENT_TYPE_APPLICATION_JSON);
$bucket = Authorization::skip(fn () => $dbForPlatform->getDocument('buckets', 'screenshots'));
@@ -0,0 +1,86 @@
<?php
namespace Appwrite\Platform\Modules\Projects\Http\Projects\Labels;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\Queries\Projects;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Text;
class Update extends Action
{
use HTTP;
public static function getName()
{
return 'updateProjectLabels';
}
protected function getQueriesValidator(): Validator
{
return new Projects();
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PUT)
->setHttpPath('/v1/projects/:projectId/labels')
->desc('Update project labels')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'projects',
name: 'updateLabels',
description: <<<EOT
Update the project labels by its unique ID. Labels can be used to easily filter projects in an organization.
EOT,
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROJECT
)
],
contentType: ContentType::JSON
))
->param('projectId', '', new UID(), 'Project unique ID.')
->param('labels', [], new ArrayList(new Text(36, allowList: [...Text::NUMBERS, ...Text::ALPHABET_UPPER, ...Text::ALPHABET_LOWER]), APP_LIMIT_ARRAY_LABELS_SIZE), 'Array of project labels. Replaces the previous labels. Maximum of ' . APP_LIMIT_ARRAY_LABELS_SIZE . ' labels are allowed, each up to 36 alphanumeric characters long.')
->inject('response')
->inject('dbForPlatform')
->callback($this->action(...));
}
/**
* @param array<string> $labels
*/
public function action(
string $projectId,
array $labels,
Response $response,
Database $dbForPlatform
): void {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$project->setAttribute('labels', (array) \array_values(\array_unique($labels)));
$project = $dbForPlatform->updateDocument('projects', $project->getId(), $project);
$response->dynamic($project, Response::MODEL_PROJECT);
}
}
@@ -7,6 +7,7 @@ use Appwrite\Platform\Modules\Projects\Http\DevKeys\Delete as DeleteDevKey;
use Appwrite\Platform\Modules\Projects\Http\DevKeys\Get as GetDevKey;
use Appwrite\Platform\Modules\Projects\Http\DevKeys\Update as UpdateDevKey;
use Appwrite\Platform\Modules\Projects\Http\DevKeys\XList as ListDevKeys;
use Appwrite\Platform\Modules\Projects\Http\Projects\Labels\Update as UpdateProjectLabels;
use Appwrite\Platform\Modules\Projects\Http\Projects\XList as ListProjects;
use Utopia\Platform\Service;
@@ -22,5 +23,6 @@ class Http extends Service
$this->addAction(DeleteDevKey::getName(), new DeleteDevKey());
$this->addAction(ListProjects::getName(), new ListProjects());
$this->addAction(UpdateProjectLabels::getName(), new UpdateProjectLabels());
}
}
+6 -12
View File
@@ -148,7 +148,7 @@ class Deletes extends Action
break;
case DELETE_TYPE_AUDIT:
if (!$project->isEmpty()) {
$this->deleteAuditLogs($project, $auditRetention, $getAudit);
$this->deleteAuditLogs($project, $getAudit, $auditRetention);
}
break;
case DELETE_TYPE_REALTIME:
@@ -568,13 +568,8 @@ class Deletes extends Action
// Delete Keys
$this->deleteByGroup('keys', [
Query::or([
Query::equal('projectInternalId', [$projectInternalId]),
Query::and([
Query::equal('resourceType', ['projects']),
Query::equal('resourceInternalId', [$projectInternalId]),
])
]),
Query::equal('resourceType', ['projects']),
Query::equal('resourceInternalId', [$projectInternalId]),
Query::orderAsc()
], $dbForPlatform);
@@ -783,14 +778,13 @@ class Deletes extends Action
}
/**
* @param Database $dbForPlatform
* @param callable $getProjectDB
* @param string $auditRetention
* @param Document $project
* @param callable $getAudit
* @param string $auditRetention
* @return void
* @throws Exception
*/
private function deleteAuditLogs(Document $project, string $auditRetention, callable $getAudit): void
private function deleteAuditLogs(Document $project, callable $getAudit, string $auditRetention): void
{
$projectId = $project->getId();
/** @var Audit $audit */
+7 -1
View File
@@ -68,7 +68,8 @@ class Mails extends Action
throw new Exception('Skipped mail processing. No SMTP configuration has been set.');
}
$log->addTag('type', empty($smtp) ? 'cloud' : 'smtp');
$type = empty($smtp) ? 'cloud' : 'smtp';
$log->addTag('type', $type);
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
$hostname = System::getEnv('_APP_CONSOLE_DOMAIN');
@@ -182,6 +183,9 @@ class Mails extends Action
try {
$mail->send();
} catch (\Throwable $error) {
if ($type === 'smtp') {
throw new Exception('Error sending mail: ' . $error->getMessage(), 401);
}
throw new Exception('Error sending mail: ' . $error->getMessage(), 500);
}
}
@@ -209,6 +213,8 @@ class Mails extends Action
$mail->SMTPSecure = $smtp['secure'];
$mail->SMTPAutoTLS = false;
$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']);
@@ -111,13 +111,8 @@ class StatsResources extends Action
Query::equal('projectInternalId', [$project->getSequence()])
]);
$keys = $dbForPlatform->count('keys', [
Query::or([
Query::equal('projectInternalId', [$project->getSequence()]),
Query::and([
Query::equal('resourceType', ['projects']),
Query::equal('resourceInternalId', [$project->getSequence()]),
])
]),
Query::equal('resourceType', ['projects']),
Query::equal('resourceInternalId', [$project->getSequence()]),
]);
$domains = $dbForPlatform->count('rules', [
@@ -6,7 +6,8 @@ class Projects extends Base
{
public const ALLOWED_ATTRIBUTES = [
'name',
'teamId'
'teamId',
'labels',
];
/**
@@ -276,6 +276,13 @@ class Project extends Model
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('labels', [
'type' => self::TYPE_STRING,
'description' => 'Labels for the project.',
'default' => [],
'example' => ['vip'],
'array' => true,
])
;
$services = Config::getParam('services', []);
@@ -363,4 +363,77 @@ trait AccountBase
$this->assertEquals($response['headers']['status-code'], 201);
$this->assertEquals('191.0.113.195', $response['body']['clientIp'] ?? $response['body']['ip'] ?? '');
}
/**
* @group abuseEnabled
*/
public function testAccountAbuseReset(): void
{
$email = \uniqid() . '.abuse.reset.test@example.com';
$password = 'password';
$account = $this->client->call(Client::METHOD_POST, '/account', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'userId' => ID::unique(),
'email' => $email,
'password' => $password,
'name' => 'Abuse Reset Test',
]);
$this->assertEquals($account['headers']['status-code'], 201);
// 20 successful requests won't get blocked
for ($i = 0; $i < 20; $i++) {
$session = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'email' => $email,
'password' => $password,
]);
$this->assertEquals($session['headers']['status-code'], 201);
}
// 10 failures are OK
for ($i = 0; $i < 10; $i++) {
$session = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'email' => $email,
'password' => 'wrongPassword',
]);
$this->assertEquals($session['headers']['status-code'], 401);
}
// 11th request gets limited
$session = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'email' => $email,
'password' => 'wrongPassword',
]);
$this->assertEquals($session['headers']['status-code'], 429);
// Even correct password is now blocked, correctness doesn't matter
$session = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'email' => $email,
'password' => $password,
]);
$this->assertEquals($session['headers']['status-code'], 429);
}
}
@@ -4783,7 +4783,7 @@ class ProjectsConsoleClientTest extends Scope
*/
/**
* @group devKeys
* @group abuseEnabled
*/
public function testCreateProjectDevKey(): void
{
@@ -4844,7 +4844,7 @@ class ProjectsConsoleClientTest extends Scope
/**
* @group devKeys
* @group abuseEnabled
*/
public function testListProjectDevKey(): void
{
@@ -4935,7 +4935,7 @@ class ProjectsConsoleClientTest extends Scope
/**
* @group devKeys
* @group abuseEnabled
*/
public function testGetProjectDevKey(): void
{
@@ -4979,7 +4979,7 @@ class ProjectsConsoleClientTest extends Scope
}
/**
* @group devKeys
* @group abuseEnabled
*/
public function testGetDevKeyWithSdks(): void
{
@@ -5036,7 +5036,7 @@ class ProjectsConsoleClientTest extends Scope
}
/**
* @group devKeys
* @group abuseEnabled
*/
public function testNoHostValidationWithDevKey(): void
{
@@ -5117,7 +5117,7 @@ class ProjectsConsoleClientTest extends Scope
}
/**
* @group devKeys
* @group abuseEnabled
*/
public function testCorsWithDevKey(): void
{
@@ -5174,7 +5174,7 @@ class ProjectsConsoleClientTest extends Scope
}
/**
* @group devKeys
* @group abuseEnabled
*/
public function testNoRateLimitWithDevKey(): void
{
@@ -5279,7 +5279,7 @@ class ProjectsConsoleClientTest extends Scope
}
/**
* @group devKeys
* @group abuseEnabled
*/
public function testUpdateProjectDevKey(): void
{
@@ -5324,7 +5324,7 @@ class ProjectsConsoleClientTest extends Scope
}
/**
* @group devKeys
* @group abuseEnabled
*/
public function testDeleteProjectDevKey(): void
{
@@ -5382,4 +5382,216 @@ class ProjectsConsoleClientTest extends Scope
/**
* Devkeys Tests ends here ------------------------------------------------
*/
public function testProjectLabels(): void
{
// Setup: Prepare team
$team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'teamId' => ID::unique(),
'name' => 'Query Select Test Team',
]);
$this->assertEquals(201, $team['headers']['status-code']);
$teamId = $team['body']['$id'];
// Setup: Prepare project
$project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Test project - Labels 1',
'teamId' => $teamId,
'region' => System::getEnv('_APP_REGION', 'default')
]);
$this->assertEquals(201, $project['headers']['status-code']);
$this->assertIsArray($project['body']['labels']);
$this->assertCount(0, $project['body']['labels']);
$projectId = $project['body']['$id'];
// Apply labels
$project = $this->client->call(Client::METHOD_PUT, '/projects/' . $projectId . '/labels', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'labels' => ['vip', 'imagine', 'blocked']
]);
$this->assertEquals(200, $project['headers']['status-code']);
$this->assertIsArray($project['body']['labels']);
$this->assertCount(3, $project['body']['labels']);
$this->assertEquals('vip', $project['body']['labels'][0]);
$this->assertEquals('imagine', $project['body']['labels'][1]);
$this->assertEquals('blocked', $project['body']['labels'][2]);
// Update labels
$project = $this->client->call(Client::METHOD_PUT, '/projects/' . $projectId . '/labels', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'labels' => ['nonvip', 'imagine']
]);
$this->assertEquals(200, $project['headers']['status-code']);
$this->assertIsArray($project['body']['labels']);
$this->assertCount(2, $project['body']['labels']);
$this->assertEquals('nonvip', $project['body']['labels'][0]);
$this->assertEquals('imagine', $project['body']['labels'][1]);
// Filter by labels
$projects = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::contains('labels', ['nonvip'])->toString(),
]
]);
$this->assertEquals(200, $projects['headers']['status-code']);
$this->assertEquals(1, $projects['body']['total']);
$this->assertEquals($projectId, $projects['body']['projects'][0]['$id']);
$projects = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::contains('labels', ['vip'])->toString(),
]
]);
$this->assertEquals(200, $projects['headers']['status-code']);
$this->assertEquals(0, $projects['body']['total']);
$projects = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::contains('labels', ['imagine'])->toString(),
]
]);
$this->assertEquals(200, $projects['headers']['status-code']);
$this->assertEquals(1, $projects['body']['total']);
$this->assertEquals($projectId, $projects['body']['projects'][0]['$id']);
$projects = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::contains('labels', ['nonvip', 'imagine'])->toString(),
]
]);
$this->assertEquals(200, $projects['headers']['status-code']);
$this->assertEquals(1, $projects['body']['total']);
$this->assertEquals($projectId, $projects['body']['projects'][0]['$id']);
// Setup: Second project with only imagine label
$project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Test project - Labels 2',
'teamId' => $teamId,
'region' => System::getEnv('_APP_REGION', 'default')
]);
$this->assertEquals(201, $project['headers']['status-code']);
$this->assertIsArray($project['body']['labels']);
$this->assertCount(0, $project['body']['labels']);
$projectId2 = $project['body']['$id'];
$project = $this->client->call(Client::METHOD_PUT, '/projects/' . $projectId2 . '/labels', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'labels' => ['vip', 'imagine']
]);
$this->assertEquals(200, $project['headers']['status-code']);
$this->assertIsArray($project['body']['labels']);
$this->assertCount(2, $project['body']['labels']);
$this->assertEquals('vip', $project['body']['labels'][0]);
$this->assertEquals('imagine', $project['body']['labels'][1]);
// List of imagine has both
$projects = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::contains('labels', ['imagine'])->toString(),
]
]);
$this->assertEquals(200, $projects['headers']['status-code']);
$this->assertEquals(2, $projects['body']['total']);
$this->assertEquals($projectId, $projects['body']['projects'][0]['$id']);
$this->assertEquals($projectId2, $projects['body']['projects'][1]['$id']);
// List of vip only has second
$projects = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::contains('labels', ['vip'])->toString(),
]
]);
$this->assertEquals(200, $projects['headers']['status-code']);
$this->assertEquals(1, $projects['body']['total']);
$this->assertEquals($projectId2, $projects['body']['projects'][0]['$id']);
// List of vip and imagine has second
$projects = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::contains('labels', ['vip'])->toString(),
Query::contains('labels', ['imagine'])->toString(),
]
]);
$this->assertEquals(200, $projects['headers']['status-code']);
$this->assertEquals(1, $projects['body']['total']);
$this->assertEquals($projectId2, $projects['body']['projects'][0]['$id']);
// List of vip or imagine has second
$projects = $this->client->call(Client::METHOD_GET, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::contains('labels', ['vip', 'imagine'])->toString(),
]
]);
$this->assertEquals(200, $projects['headers']['status-code']);
$this->assertEquals(2, $projects['body']['total']);
$this->assertEquals($projectId, $projects['body']['projects'][0]['$id']);
$this->assertEquals($projectId2, $projects['body']['projects'][1]['$id']);
// Cleanup
$response = $this->client->call(Client::METHOD_DELETE, '/projects/' . $projectId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(204, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_DELETE, '/projects/' . $projectId2, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(204, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_DELETE, '/teams/' . $teamId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(204, $response['headers']['status-code']);
}
}