diff --git a/AGENTS.md b/AGENTS.md index 993b0b5ad0..bb24d9f4fe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -47,6 +47,36 @@ Examples: 'resourceType' => 'deployments' ``` +## Performance Patterns + +### Document Update Optimization + +When updating documents, always pass only the changed attributes as a sparse `Document` rather than the full document. This is more efficient because `updateDocument()` internally performs `array_merge($old, $new)`. + +**Correct Pattern:** +```php +// Good: Pass only changed attributes directly +$user = $dbForProject->updateDocument('users', $user->getId(), new Document([ + 'name' => $name, + 'email' => $email, +])); +``` + +**Incorrect Pattern:** +```php +$user->setAttribute('name', $name); +$user->setAttribute('email', $email); + +// Bad: Passing full document is inefficient +$user = $dbForProject->updateDocument('users', $user->getId(), $user); +``` + +**Exceptions:** +- Migration files (need full document updates by design) +- Cases already using `array_merge()` with `getArrayCopy()` +- Updates where almost all attributes of the document change at once (sparse update provides little benefit compared to passing the full document) +- Complex nested relationship logic where full document state is required + ## Security Considerations ### Critical Security Practices diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..43c994c2d3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/app/config/errors.php b/app/config/errors.php index bf0f4461f6..e8519fd797 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -27,7 +27,7 @@ return [ Exception::GENERAL_RESOURCE_BLOCKED => [ 'name' => Exception::GENERAL_RESOURCE_BLOCKED, 'description' => 'Access to this resource is blocked.', - 'code' => 401, + 'code' => 403, ], Exception::GENERAL_UNKNOWN_ORIGIN => [ 'name' => Exception::GENERAL_UNKNOWN_ORIGIN, @@ -168,8 +168,8 @@ return [ ], Exception::USER_BLOCKED => [ 'name' => Exception::USER_BLOCKED, - 'description' => 'The current user has been blocked. You can unblock the user by making a request to the User API\'s "Update User Status" endpoint or in the Appwrite Console\'s Auth section.', - 'code' => 401, + 'description' => 'The current user has been blocked.', + 'code' => 403, ], Exception::USER_INVALID_TOKEN => [ 'name' => Exception::USER_INVALID_TOKEN, diff --git a/app/config/sdks.php b/app/config/sdks.php index 77db161b5c..1a808aa10a 100644 --- a/app/config/sdks.php +++ b/app/config/sdks.php @@ -231,7 +231,7 @@ return [ 'url' => 'https://github.com/appwrite/sdk-for-cli', 'package' => 'https://www.npmjs.com/package/appwrite-cli', 'enabled' => true, - 'beta' => true, + 'beta' => false, 'dev' => false, 'hidden' => false, 'family' => APP_SDK_PLATFORM_CONSOLE, diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 0adf1b2d57..b58a9b4185 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -288,7 +288,10 @@ $createSession = function (string $userId, string $secret, Request $request, Res } try { - $dbForProject->updateDocument('users', $user->getId(), $user); + $dbForProject->updateDocument('users', $user->getId(), new Document([ + 'emailVerification' => $user->getAttribute('emailVerification'), + 'phoneVerification' => $user->getAttribute('phoneVerification'), + ])); } catch (\Throwable $th) { throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed saving user to DB'); } @@ -1032,7 +1035,11 @@ Http::post('/v1/account/sessions/email') ->setAttribute('password', $proofForPasswordUpdated->hash($password)) ->setAttribute('hash', $proofForPasswordUpdated->getHash()->getName()) ->setAttribute('hashOptions', $proofForPasswordUpdated->getHash()->getOptions()); - $dbForProject->updateDocument('users', $user->getId(), $user); + $dbForProject->updateDocument('users', $user->getId(), new Document([ + 'password' => $user->getAttribute('password'), + 'hash' => $user->getAttribute('hash'), + 'hashOptions' => $user->getAttribute('hashOptions'), + ])); } $dbForProject->purgeCachedDocument('users', $user->getId()); @@ -1822,7 +1829,11 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect') ->setAttribute('providerAccessToken', $accessToken) ->setAttribute('providerRefreshToken', $refreshToken) ->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int) $accessTokenExpiry)); - $dbForProject->updateDocument('identities', $identity->getId(), $identity); + $dbForProject->updateDocument('identities', $identity->getId(), new Document([ + 'providerAccessToken' => $identity->getAttribute('providerAccessToken'), + 'providerRefreshToken' => $identity->getAttribute('providerRefreshToken'), + 'providerAccessTokenExpiry' => $identity->getAttribute('providerAccessTokenExpiry'), + ])); } if (empty($user->getAttribute('email'))) { @@ -1960,7 +1971,10 @@ Http::get('/v1/account/sessions/oauth2/:provider/redirect') ->setAttribute('sessionId', $session->getId()) ->setAttribute('sessionInternalId', $session->getSequence()); - $dbForProject->updateDocument('targets', $target->getId(), $target); + $dbForProject->updateDocument('targets', $target->getId(), new Document([ + 'sessionId' => $target->getAttribute('sessionId'), + 'sessionInternalId' => $target->getAttribute('sessionInternalId'), + ])); } } @@ -3145,7 +3159,9 @@ Http::patch('/v1/account/name') $user->setAttribute('name', $name); - $user = $dbForProject->updateDocument('users', $user->getId(), $user); + $user = $dbForProject->updateDocument('users', $user->getId(), new Document([ + 'name' => $user->getAttribute('name'), + ])); $queueForEvents->setParam('userId', $user->getId()); @@ -3798,13 +3814,15 @@ Http::put('/v1/account/recovery') $hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, true]); - $profile = $dbForProject->updateDocument('users', $profile->getId(), $profile - ->setAttribute('password', $newPassword) - ->setAttribute('passwordHistory', $history) - ->setAttribute('passwordUpdate', DateTime::now()) - ->setAttribute('hash', $proofForPassword->getHash()->getName()) - ->setAttribute('hashOptions', $proofForPassword->getHash()->getOptions()) - ->setAttribute('emailVerification', true)); + $profile = $dbForProject->updateDocument('users', $profile->getId(), new Document( + [ + 'password' => $newPassword, + 'passwordHistory' => $history, + 'passwordUpdate' => DateTime::now(), + 'hash' => $proofForPassword->getHash()->getName(), + 'hashOptions' => $proofForPassword->getHash()->getOptions(), + 'emailVerification' => true] + )); $user->setAttributes($profile->getArrayCopy()); @@ -4126,7 +4144,7 @@ Http::put('/v1/account/verifications/email') $authorization->addRole(Role::user($profile->getId())->toString()); - $profile = $dbForProject->updateDocument('users', $profile->getId(), $profile->setAttribute('emailVerification', true)); + $profile = $dbForProject->updateDocument('users', $profile->getId(), new Document(['emailVerification' => true])); $user->setAttributes($profile->getArrayCopy()); @@ -4342,7 +4360,7 @@ Http::put('/v1/account/verifications/phone') $authorization->addRole(Role::user($profile->getId())->toString()); - $profile = $dbForProject->updateDocument('users', $profile->getId(), $profile->setAttribute('phoneVerification', true)); + $profile = $dbForProject->updateDocument('users', $profile->getId(), new Document(['phoneVerification' => true])); $user->setAttributes($profile->getArrayCopy()); @@ -4500,7 +4518,11 @@ Http::put('/v1/account/targets/:targetId/push') $target->setAttribute('name', "{$device['deviceBrand']} {$device['deviceModel']}"); - $target = $dbForProject->updateDocument('targets', $target->getId(), $target); + $target = $dbForProject->updateDocument('targets', $target->getId(), new Document([ + 'identifier' => $target->getAttribute('identifier'), + 'expired' => $target->getAttribute('expired'), + 'name' => $target->getAttribute('name'), + ])); $dbForProject->purgeCachedDocument('users', $user->getId()); diff --git a/app/controllers/api/migrations.php b/app/controllers/api/migrations.php index 5a864ab928..bfb73189b5 100644 --- a/app/controllers/api/migrations.php +++ b/app/controllers/api/migrations.php @@ -717,22 +717,14 @@ Http::get('/v1/migrations/appwrite/report') ->inject('project') ->inject('user') ->action(function (array $resources, string $endpoint, string $projectID, string $key, Response $response) { - - $appwrite = new Appwrite($projectID, $endpoint, $key); - try { + $appwrite = new Appwrite($projectID, $endpoint, $key); $report = $appwrite->report($resources); } catch (\Throwable $e) { - switch ($e->getCode()) { - case 401: - throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, 'Source Error: ' . $e->getMessage()); - case 429: - throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Source Error: Rate Limit Exceeded, Is your Cloud Provider blocking Appwrite\'s IP?'); - case 500: - throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage()); - } - - throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage()); + throw new Exception( + Exception::MIGRATION_PROVIDER_ERROR, + 'Unable to connect to the migration source. Please verify your credentials and ensure the source is reachable from this server. Check for network restrictions such as firewalls, IP allowlists, or outbound connectivity limits.' + ); } $response @@ -771,21 +763,14 @@ Http::get('/v1/migrations/firebase/report') throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Invalid Service Account JSON'); } - $firebase = new Firebase($serviceAccount); - try { + $firebase = new Firebase($serviceAccount); $report = $firebase->report($resources); } catch (\Throwable $e) { - switch ($e->getCode()) { - case 401: - throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, 'Source Error: ' . $e->getMessage()); - case 429: - throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Source Error: Rate Limit Exceeded, Is your Cloud Provider blocking Appwrite\'s IP?'); - case 500: - throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage()); - } - - throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage()); + throw new Exception( + Exception::MIGRATION_PROVIDER_ERROR, + 'Unable to connect to the migration source. Please verify your credentials and ensure the source is reachable from this server. Check for network restrictions such as firewalls, IP allowlists, or outbound connectivity limits.' + ); } $response @@ -820,21 +805,14 @@ Http::get('/v1/migrations/supabase/report') ->inject('response') ->inject('dbForProject') ->action(function (array $resources, string $endpoint, string $apiKey, string $databaseHost, string $username, string $password, int $port, Response $response) { - $supabase = new Supabase($endpoint, $apiKey, $databaseHost, 'postgres', $username, $password, $port); - try { + $supabase = new Supabase($endpoint, $apiKey, $databaseHost, 'postgres', $username, $password, $port); $report = $supabase->report($resources); } catch (\Throwable $e) { - switch ($e->getCode()) { - case 401: - throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, 'Source Error: ' . $e->getMessage()); - case 429: - throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Source Error: Rate Limit Exceeded, Is your Cloud Provider blocking Appwrite\'s IP?'); - case 500: - throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage()); - } - - throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage()); + throw new Exception( + Exception::MIGRATION_PROVIDER_ERROR, + 'Unable to connect to the migration source. Please verify your credentials and ensure the source is reachable from this server. Check for network restrictions such as firewalls, IP allowlists, or outbound connectivity limits.' + ); } $response @@ -869,21 +847,14 @@ Http::get('/v1/migrations/nhost/report') ->param('port', 5432, new Integer(true), 'Source\'s Database Port.', true) ->inject('response') ->action(function (array $resources, string $subdomain, string $region, string $adminSecret, string $database, string $username, string $password, int $port, Response $response) { - $nhost = new NHost($subdomain, $region, $adminSecret, $database, $username, $password, $port); - try { + $nhost = new NHost($subdomain, $region, $adminSecret, $database, $username, $password, $port); $report = $nhost->report($resources); } catch (\Throwable $e) { - switch ($e->getCode()) { - case 401: - throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, 'Source Error: ' . $e->getMessage()); - case 429: - throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Source Error: Rate Limit Exceeded, Is your Cloud Provider blocking Appwrite\'s IP?'); - case 500: - throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage()); - } - - throw new Exception(Exception::MIGRATION_PROVIDER_ERROR, 'Source Error: ' . $e->getMessage()); + throw new Exception( + Exception::MIGRATION_PROVIDER_ERROR, + 'Unable to connect to the migration source. Please verify your credentials and ensure the source is reachable from this server. Check for network restrictions such as firewalls, IP allowlists, or outbound connectivity limits.' + ); } $response diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index 38c0e87b44..d0e5e19a51 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -1161,7 +1161,7 @@ Http::patch('/v1/users/:userId/status') throw new Exception(Exception::USER_NOT_FOUND); } - $user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('status', (bool) $status)); + $user = $dbForProject->updateDocument('users', $user->getId(), new Document(['status' => (bool) $status])); $queueForEvents ->setParam('userId', $user->getId()); @@ -1204,7 +1204,7 @@ Http::put('/v1/users/:userId/labels') $user->setAttribute('labels', (array) \array_values(\array_unique($labels))); - $user = $dbForProject->updateDocument('users', $user->getId(), $user); + $user = $dbForProject->updateDocument('users', $user->getId(), new Document(['labels' => $user->getAttribute('labels')])); $queueForEvents ->setParam('userId', $user->getId()); @@ -1245,7 +1245,7 @@ Http::patch('/v1/users/:userId/verification/phone') throw new Exception(Exception::USER_NOT_FOUND); } - $user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('phoneVerification', $phoneVerification)); + $user = $dbForProject->updateDocument('users', $user->getId(), new Document(['phoneVerification' => $phoneVerification])); $queueForEvents ->setParam('userId', $user->getId()); @@ -1289,7 +1289,7 @@ Http::patch('/v1/users/:userId/name') $user->setAttribute('name', $name); - $user = $dbForProject->updateDocument('users', $user->getId(), $user); + $user = $dbForProject->updateDocument('users', $user->getId(), new Document(['name' => $user->getAttribute('name')])); $queueForEvents->setParam('userId', $user->getId()); @@ -1344,7 +1344,10 @@ Http::patch('/v1/users/:userId/password') ->setAttribute('password', '') ->setAttribute('passwordUpdate', DateTime::now()); - $user = $dbForProject->updateDocument('users', $user->getId(), $user); + $user = $dbForProject->updateDocument('users', $user->getId(), new Document([ + 'password' => $user->getAttribute('password'), + 'passwordUpdate' => $user->getAttribute('passwordUpdate'), + ])); $queueForEvents->setParam('userId', $user->getId()); $response->dynamic($user, Response::MODEL_USER); } @@ -1377,7 +1380,13 @@ Http::patch('/v1/users/:userId/password') ->setAttribute('hash', $hasher->getName()) ->setAttribute('hashOptions', $hasher->getOptions()); - $user = $dbForProject->updateDocument('users', $user->getId(), $user); + $user = $dbForProject->updateDocument('users', $user->getId(), new Document([ + 'password' => $user->getAttribute('password'), + 'passwordHistory' => $user->getAttribute('passwordHistory'), + 'passwordUpdate' => $user->getAttribute('passwordUpdate'), + 'hash' => $user->getAttribute('hash'), + 'hashOptions' => $user->getAttribute('hashOptions'), + ])); $sessions = $user->getAttribute('sessions', []); $invalidate = $project->getAttribute('auths', default: [])['invalidateSessions'] ?? false; @@ -1469,7 +1478,15 @@ Http::patch('/v1/users/:userId/email') ; try { - $user = $dbForProject->updateDocument('users', $user->getId(), $user); + $user = $dbForProject->updateDocument('users', $user->getId(), new Document([ + 'email' => $user->getAttribute('email'), + 'emailVerification' => $user->getAttribute('emailVerification'), + 'emailCanonical' => $user->getAttribute('emailCanonical'), + 'emailIsCanonical' => $user->getAttribute('emailIsCanonical'), + 'emailIsCorporate' => $user->getAttribute('emailIsCorporate'), + 'emailIsDisposable' => $user->getAttribute('emailIsDisposable'), + 'emailIsFree' => $user->getAttribute('emailIsFree'), + ])); /** * @var Document $oldTarget */ @@ -1477,7 +1494,8 @@ Http::patch('/v1/users/:userId/email') if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) { if (\strlen($email) !== 0) { - $dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $email)); + $dbForProject->updateDocument('targets', $oldTarget->getId(), new Document(['identifier' => $email])); + $oldTarget->setAttribute('identifier', $email); } else { $dbForProject->deleteDocument('targets', $oldTarget->getId()); } @@ -1558,7 +1576,10 @@ Http::patch('/v1/users/:userId/phone') } try { - $user = $dbForProject->updateDocument('users', $user->getId(), $user); + $user = $dbForProject->updateDocument('users', $user->getId(), new Document([ + 'phone' => $user->getAttribute('phone'), + 'phoneVerification' => $user->getAttribute('phoneVerification'), + ])); /** * @var Document $oldTarget */ @@ -1566,7 +1587,8 @@ Http::patch('/v1/users/:userId/phone') if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) { if (\strlen($number) !== 0) { - $dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $number)); + $dbForProject->updateDocument('targets', $oldTarget->getId(), new Document(['identifier' => $number])); + $oldTarget->setAttribute('identifier', $number); } else { $dbForProject->deleteDocument('targets', $oldTarget->getId()); } @@ -1630,7 +1652,7 @@ Http::patch('/v1/users/:userId/verification') throw new Exception(Exception::USER_NOT_FOUND); } - $user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('emailVerification', $emailVerification)); + $user = $dbForProject->updateDocument('users', $user->getId(), new Document(['emailVerification' => $emailVerification])); $queueForEvents->setParam('userId', $user->getId()); @@ -1668,7 +1690,7 @@ Http::patch('/v1/users/:userId/prefs') throw new Exception(Exception::USER_NOT_FOUND); } - $user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('prefs', $prefs)); + $user = $dbForProject->updateDocument('users', $user->getId(), new Document(['prefs' => $prefs])); $queueForEvents ->setParam('userId', $user->getId()); @@ -1768,7 +1790,13 @@ Http::patch('/v1/users/:userId/targets/:targetId') $target->setAttribute('name', $name); } - $target = $dbForProject->updateDocument('targets', $target->getId(), $target); + $target = $dbForProject->updateDocument('targets', $target->getId(), new Document([ + 'identifier' => $target->getAttribute('identifier'), + 'expired' => $target->getAttribute('expired'), + 'providerId' => $target->getAttribute('providerId'), + 'providerInternalId' => $target->getAttribute('providerInternalId'), + 'name' => $target->getAttribute('name'), + ])); $dbForProject->purgeCachedDocument('users', $user->getId()); $queueForEvents @@ -1836,7 +1864,7 @@ Http::patch('/v1/users/:userId/mfa') $user->setAttribute('mfa', $mfa); - $user = $dbForProject->updateDocument('users', $user->getId(), $user); + $user = $dbForProject->updateDocument('users', $user->getId(), new Document(['mfa' => $user->getAttribute('mfa')])); $queueForEvents->setParam('userId', $user->getId()); @@ -2024,7 +2052,7 @@ Http::patch('/v1/users/:userId/mfa/recovery-codes') $mfaRecoveryCodes = Type::generateBackupCodes(); $user->setAttribute('mfaRecoveryCodes', $mfaRecoveryCodes); - $dbForProject->updateDocument('users', $user->getId(), $user); + $dbForProject->updateDocument('users', $user->getId(), new Document(['mfaRecoveryCodes' => $mfaRecoveryCodes])); $queueForEvents->setParam('userId', $user->getId()); @@ -2096,7 +2124,7 @@ Http::put('/v1/users/:userId/mfa/recovery-codes') $mfaRecoveryCodes = Type::generateBackupCodes(); $user->setAttribute('mfaRecoveryCodes', $mfaRecoveryCodes); - $dbForProject->updateDocument('users', $user->getId(), $user); + $dbForProject->updateDocument('users', $user->getId(), new Document(['mfaRecoveryCodes' => $mfaRecoveryCodes])); $queueForEvents->setParam('userId', $user->getId()); diff --git a/app/controllers/general.php b/app/controllers/general.php index 0bb02a57d1..6d21059eab 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -134,8 +134,9 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S if (!$project->isEmpty() && $project->getId() !== 'console') { $accessedAt = $project->getAttribute('accessedAt', 0); if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) { - $project->setAttribute('accessedAt', DateTime::now()); - $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $project)); + $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), new Document([ + 'accessedAt' => DateTime::now() + ]))); } /** @@ -1662,7 +1663,10 @@ Http::get('/v1/ping') ->setAttribute('pingedAt', $pingedAt); $authorization->skip(function () use ($dbForPlatform, $project) { - $dbForPlatform->updateDocument('projects', $project->getId(), $project); + $dbForPlatform->updateDocument('projects', $project->getId(), new Document([ + 'pingCount' => $project->getAttribute('pingCount'), + 'pingedAt' => $project->getAttribute('pingedAt') + ])); }); $queueForEvents diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index d7b2c7339e..d2abfe53a2 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -360,8 +360,9 @@ Http::init() if (!$project->isEmpty() && $project->getId() !== 'console') { $accessedAt = $project->getAttribute('accessedAt', 0); if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) { - $project->setAttribute('accessedAt', DateTime::now()); - $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $project)); + $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), new Document([ + 'accessedAt' => DateTime::now() + ]))); } } @@ -371,9 +372,13 @@ Http::init() $user->setAttribute('accessedAt', DateTime::now()); if ($project->getId() !== 'console' && APP_MODE_ADMIN !== $mode) { - $dbForProject->updateDocument('users', $user->getId(), $user); + $dbForProject->updateDocument('users', $user->getId(), new Document([ + 'accessedAt' => $user->getAttribute('accessedAt') + ])); } else { - $authorization->skip(fn () => $dbForPlatform->updateDocument('users', $user->getId(), $user)); + $authorization->skip(fn () => $dbForPlatform->updateDocument('users', $user->getId(), new Document([ + 'accessedAt' => $user->getAttribute('accessedAt') + ]))); } } } @@ -649,7 +654,9 @@ Http::init() $transformedAt = $file->getAttribute('transformedAt', ''); if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $transformedAt) { $file->setAttribute('transformedAt', DateTime::now()); - $authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $file->getAttribute('bucketInternalId'), $file->getId(), $file)); + $authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $file->getAttribute('bucketInternalId'), $file->getId(), new Document([ + 'transformedAt' => $file->getAttribute('transformedAt') + ]))); } } } @@ -948,7 +955,9 @@ Http::shutdown() } } elseif (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_CACHE_UPDATE)) > $accessedAt) { $cacheLog->setAttribute('accessedAt', $now); - $authorization->skip(fn () => $dbForProject->updateDocument('cache', $cacheLog->getId(), $cacheLog)); + $authorization->skip(fn () => $dbForProject->updateDocument('cache', $cacheLog->getId(), new Document([ + 'accessedAt' => $cacheLog->getAttribute('accessedAt') + ]))); // Overwrite the file every APP_CACHE_UPDATE seconds to update the file modified time that is used in the TTL checks in cache->load() $cache->save($key, $data['payload']); } diff --git a/app/init/resources.php b/app/init/resources.php index 36aadc9707..d5486c2a49 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -1235,7 +1235,9 @@ Http::setResource('devKey', function (Request $request, Document $project, array $accessedAt = $key->getAttribute('accessedAt', 0); if (empty($accessedAt) || DatabaseDateTime::formatTz(DatabaseDateTime::addSeconds(new \DateTime(), -APP_KEY_ACCESS)) > $accessedAt) { $key->setAttribute('accessedAt', DatabaseDateTime::now()); - $authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), $key)); + $authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), new Document([ + 'accessedAt' => $key->getAttribute('accessedAt') + ]))); $dbForPlatform->purgeCachedDocument('projects', $project->getId()); } @@ -1252,7 +1254,10 @@ Http::setResource('devKey', function (Request $request, Document $project, array /** Update access time as well */ $key->setAttribute('accessedAt', DatabaseDateTime::now()); - $key = $authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), $key)); + $key = $authorization->skip(fn () => $dbForPlatform->updateDocument('devKeys', $key->getId(), new Document([ + 'sdks' => $key->getAttribute('sdks'), + 'accessedAt' => $key->getAttribute('accessedAt') + ]))); $dbForPlatform->purgeCachedDocument('projects', $project->getId()); } } @@ -1409,7 +1414,9 @@ Http::setResource('resourceToken', function ($project, $dbForProject, $request, $accessedAt = $token->getAttribute('accessedAt', 0); if (empty($accessedAt) || DatabaseDateTime::formatTz(DatabaseDateTime::addSeconds(new \DateTime(), -APP_RESOURCE_TOKEN_ACCESS)) > $accessedAt) { $token->setAttribute('accessedAt', DatabaseDateTime::now()); - $authorization->skip(fn () => $dbForProject->updateDocument('resourceTokens', $token->getId(), $token)); + $authorization->skip(fn () => $dbForProject->updateDocument('resourceTokens', $token->getId(), new Document([ + 'accessedAt' => $token->getAttribute('accessedAt') + ]))); } return new Document([ diff --git a/app/realtime.php b/app/realtime.php index 7ec24d03c8..e0591a2596 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -356,7 +356,10 @@ $server->onStart(function () use ($stats, $register, $containerId, &$statsDocume ->setAttribute('timestamp', DateTime::now()) ->setAttribute('value', json_encode($payload)); - $database->getAuthorization()->skip(fn () => $database->updateDocument('realtime', $statsDocument->getId(), $statsDocument)); + $database->getAuthorization()->skip(fn () => $database->updateDocument('realtime', $statsDocument->getId(), new Document([ + 'timestamp' => $statsDocument->getAttribute('timestamp'), + 'value' => $statsDocument->getAttribute('value') + ]))); } catch (Throwable $th) { logError($th, "updateWorkerDocument"); } diff --git a/composer.json b/composer.json index 1422bd5d0a..d7f0ae66b5 100644 --- a/composer.json +++ b/composer.json @@ -100,6 +100,7 @@ "swoole/ide-helper": "6.*", "phpstan/phpstan": "1.12.*", "textalk/websocket": "1.5.*", + "czproject/git-php": "4.*", "laravel/pint": "1.*", "phpbench/phpbench": "1.*" }, diff --git a/composer.lock b/composer.lock index 054ebcfc94..6621ed62d0 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1fb043a556550f62ab27a0aad0b9332a", + "content-hash": "1cc64e07484256225f56bd525674c3b8", "packages": [ { "name": "adhocore/jwt", @@ -5580,6 +5580,70 @@ ], "time": "2026-02-25T14:53:45+00:00" }, + { + "name": "czproject/git-php", + "version": "v4.6.0", + "source": { + "type": "git", + "url": "https://github.com/czproject/git-php.git", + "reference": "1f1ecc92aea9ee31120f4f5b759f5aa947420b0a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/czproject/git-php/zipball/1f1ecc92aea9ee31120f4f5b759f5aa947420b0a", + "reference": "1f1ecc92aea9ee31120f4f5b759f5aa947420b0a", + "shasum": "" + }, + "require": { + "php": "8.0 - 8.5" + }, + "require-dev": { + "nette/tester": "^2.5" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jan Pecha", + "email": "janpecha@email.cz" + } + ], + "description": "Library for work with Git repository in PHP.", + "keywords": [ + "git" + ], + "support": { + "issues": "https://github.com/czproject/git-php/issues", + "source": "https://github.com/czproject/git-php/tree/v4.6.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/janpecha", + "type": "github" + }, + { + "url": "https://www.janpecha.cz/donate/git-php/", + "type": "other" + }, + { + "url": "https://donate.stripe.com/7sIcO2a9maTSg2A9AA", + "type": "stripe" + }, + { + "url": "https://thanks.dev/u/gh/czproject", + "type": "thanks.dev" + } + ], + "time": "2025-11-10T07:24:07+00:00" + }, { "name": "doctrine/annotations", "version": "2.0.2", diff --git a/docker-compose.yml b/docker-compose.yml index bdf77a46b4..4a38757737 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -152,7 +152,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_SMTP_HOST - _APP_SMTP_PORT - _APP_SMTP_SECURE @@ -305,7 +304,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_USAGE_STATS - _APP_LOGGING_CONFIG - _APP_LOGGING_CONFIG_REALTIME @@ -340,7 +338,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_LOGGING_CONFIG - _APP_DATABASE_SHARED_TABLES @@ -371,7 +368,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_REDIS_HOST - _APP_REDIS_PORT - _APP_REDIS_USER @@ -414,7 +410,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_STORAGE_DEVICE - _APP_STORAGE_S3_ACCESS_KEY - _APP_STORAGE_S3_SECRET @@ -474,7 +469,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_LOGGING_CONFIG - _APP_WORKERS_NUM - _APP_QUEUE_NAME @@ -513,7 +507,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_LOGGING_CONFIG - _APP_VCS_GITHUB_APP_NAME - _APP_VCS_GITHUB_PRIVATE_KEY @@ -658,7 +651,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_LOGGING_CONFIG - _APP_DATABASE_SHARED_TABLES @@ -722,7 +714,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_FUNCTIONS_TIMEOUT - _APP_SITES_TIMEOUT - _APP_COMPUTE_BUILD_TIMEOUT @@ -808,7 +799,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_LOGGING_CONFIG - _APP_SMS_FROM - _APP_SMS_PROVIDER @@ -875,7 +865,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_LOGGING_CONFIG - _APP_MIGRATIONS_FIREBASE_CLIENT_ID - _APP_MIGRATIONS_FIREBASE_CLIENT_SECRET @@ -918,7 +907,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_MAINTENANCE_INTERVAL - _APP_MAINTENANCE_RETENTION_EXECUTION - _APP_MAINTENANCE_RETENTION_CACHE @@ -994,7 +982,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_REDIS_HOST - _APP_REDIS_PORT - _APP_REDIS_USER @@ -1028,7 +1015,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_REDIS_HOST - _APP_REDIS_PORT - _APP_REDIS_USER @@ -1062,7 +1048,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_REDIS_HOST - _APP_REDIS_PORT - _APP_REDIS_USER @@ -1100,7 +1085,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_DATABASE_SHARED_TABLES appwrite-task-scheduler-executions: @@ -1131,7 +1115,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER appwrite-task-scheduler-messages: entrypoint: schedule-messages @@ -1161,7 +1144,6 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_DB_ADAPTER - _APP_DATABASE_SHARED_TABLES appwrite-assistant: diff --git a/phpunit.xml b/phpunit.xml index e2876bb486..030d89af8d 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -6,6 +6,7 @@ colors="true" processIsolation="false" stopOnFailure="false" + stopOnError="false" cacheDirectory=".phpunit.cache" > diff --git a/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Authenticators/Update.php b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Authenticators/Update.php index bd961ffbc2..f7226d915e 100644 --- a/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Authenticators/Update.php +++ b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Authenticators/Update.php @@ -119,7 +119,7 @@ class Update extends Action $authenticator->setAttribute('verified', true); - $dbForProject->updateDocument('authenticators', $authenticator->getId(), $authenticator); + $dbForProject->updateDocument('authenticators', $authenticator->getId(), new Document(['verified' => true])); $dbForProject->purgeCachedDocument('users', $user->getId()); $factors = $session->getAttribute('factors', []); @@ -127,7 +127,7 @@ class Update extends Action $factors = \array_values(\array_unique($factors)); $session->setAttribute('factors', $factors); - $dbForProject->updateDocument('sessions', $session->getId(), $session); + $dbForProject->updateDocument('sessions', $session->getId(), new Document(['factors' => $factors])); $queueForEvents->setParam('userId', $user->getId()); diff --git a/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Challenges/Update.php b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Challenges/Update.php index 3f3532cf16..5f6b7a1186 100644 --- a/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Challenges/Update.php +++ b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Challenges/Update.php @@ -117,7 +117,7 @@ class Update extends Action $mfaRecoveryCodes = \array_diff($mfaRecoveryCodes, [$otp]); $mfaRecoveryCodes = \array_values($mfaRecoveryCodes); $user->setAttribute('mfaRecoveryCodes', $mfaRecoveryCodes); - $dbForProject->updateDocument('users', $user->getId(), $user); + $dbForProject->updateDocument('users', $user->getId(), new Document(['mfaRecoveryCodes' => $mfaRecoveryCodes])); return true; } @@ -147,11 +147,13 @@ class Update extends Action $factors[] = $type; $factors = \array_values(\array_unique($factors)); + $mfaUpdatedAt = DateTime::now(); + $session ->setAttribute('factors', $factors) - ->setAttribute('mfaUpdatedAt', DateTime::now()); + ->setAttribute('mfaUpdatedAt', $mfaUpdatedAt); - $dbForProject->updateDocument('sessions', $session->getId(), $session); + $dbForProject->updateDocument('sessions', $session->getId(), new Document(['factors' => $factors, 'mfaUpdatedAt' => $mfaUpdatedAt])); $queueForEvents ->setParam('userId', $user->getId()) diff --git a/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/RecoveryCodes/Create.php b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/RecoveryCodes/Create.php index 969cf9c262..0d1191aaea 100644 --- a/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/RecoveryCodes/Create.php +++ b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/RecoveryCodes/Create.php @@ -93,7 +93,7 @@ class Create extends Action $mfaRecoveryCodes = Type::generateBackupCodes(); $user->setAttribute('mfaRecoveryCodes', $mfaRecoveryCodes); - $dbForProject->updateDocument('users', $user->getId(), $user); + $dbForProject->updateDocument('users', $user->getId(), new Document(['mfaRecoveryCodes' => $mfaRecoveryCodes])); $queueForEvents->setParam('userId', $user->getId()); diff --git a/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/RecoveryCodes/Update.php b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/RecoveryCodes/Update.php index 9b9f9b7e00..40051cfebc 100644 --- a/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/RecoveryCodes/Update.php +++ b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/RecoveryCodes/Update.php @@ -91,7 +91,7 @@ class Update extends Action $mfaRecoveryCodes = Type::generateBackupCodes(); $user->setAttribute('mfaRecoveryCodes', $mfaRecoveryCodes); - $dbForProject->updateDocument('users', $user->getId(), $user); + $dbForProject->updateDocument('users', $user->getId(), new Document(['mfaRecoveryCodes' => $mfaRecoveryCodes])); $queueForEvents->setParam('userId', $user->getId()); diff --git a/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Update.php b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Update.php index 227062caa3..a578149654 100644 --- a/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Update.php +++ b/src/Appwrite/Platform/Modules/Account/Http/Account/MFA/Update.php @@ -72,7 +72,7 @@ class Update extends Action ): void { $user->setAttribute('mfa', $mfa); - $user = $dbForProject->updateDocument('users', $user->getId(), $user); + $user = $dbForProject->updateDocument('users', $user->getId(), new Document(['mfa' => $mfa])); if ($mfa) { $factors = $session->getAttribute('factors', []); @@ -89,7 +89,7 @@ class Update extends Action $factors = \array_values(\array_unique($factors)); $session->setAttribute('factors', $factors); - $dbForProject->updateDocument('sessions', $session->getId(), $session); + $dbForProject->updateDocument('sessions', $session->getId(), new Document(['factors' => $factors])); } $queueForEvents->setParam('userId', $user->getId()); diff --git a/src/Appwrite/Platform/Modules/Compute/Base.php b/src/Appwrite/Platform/Modules/Compute/Base.php index 08b4bbced8..f388e46f83 100644 --- a/src/Appwrite/Platform/Modules/Compute/Base.php +++ b/src/Appwrite/Platform/Modules/Compute/Base.php @@ -143,7 +143,12 @@ class Base extends Action ->setAttribute('latestDeploymentInternalId', $deployment->getSequence()) ->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt()) ->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', '')); - $dbForProject->updateDocument('functions', $function->getId(), $function); + $dbForProject->updateDocument('functions', $function->getId(), new Document([ + 'latestDeploymentId' => $deployment->getId(), + 'latestDeploymentInternalId' => $deployment->getSequence(), + 'latestDeploymentCreatedAt' => $deployment->getCreatedAt(), + 'latestDeploymentStatus' => $deployment->getAttribute('status', ''), + ])); $queueForBuilds ->setType(BUILD_TYPE_DEPLOYMENT) @@ -249,7 +254,12 @@ class Base extends Action ->setAttribute('latestDeploymentInternalId', $deployment->getSequence()) ->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt()) ->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', '')); - $dbForProject->updateDocument('sites', $site->getId(), $site); + $dbForProject->updateDocument('sites', $site->getId(), new Document([ + 'latestDeploymentId' => $deployment->getId(), + 'latestDeploymentInternalId' => $deployment->getSequence(), + 'latestDeploymentCreatedAt' => $deployment->getCreatedAt(), + 'latestDeploymentStatus' => $deployment->getAttribute('status', ''), + ])); $sitesDomain = $platform['sitesDomain']; $domain = ID::unique() . "." . $sitesDomain; diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Delete.php index f860fc68af..38b96e67bc 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Delete.php @@ -12,6 +12,7 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; +use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\IndexDependency as IndexDependencyValidator; use Utopia\Database\Validator\Key; @@ -98,7 +99,8 @@ class Delete extends Action } if ($attribute->getAttribute('status') === 'available') { - $attribute = $dbForProject->updateDocument('attributes', $attribute->getId(), $attribute->setAttribute('status', 'deleting')); + $attribute->setAttribute('status', 'deleting'); + $attribute = $dbForProject->updateDocument('attributes', $attribute->getId(), new Document(['status' => 'deleting'])); } $dbForProject->purgeCachedDocument('database_' . $db->getSequence(), $collectionId); @@ -118,7 +120,8 @@ class Delete extends Action } if ($relatedAttribute->getAttribute('status') === 'available') { - $dbForProject->updateDocument('attributes', $relatedAttribute->getId(), $relatedAttribute->setAttribute('status', 'deleting')); + $relatedAttribute->setAttribute('status', 'deleting'); + $dbForProject->updateDocument('attributes', $relatedAttribute->getId(), new Document(['status' => 'deleting'])); } $dbForProject->purgeCachedDocument('database_' . $db->getSequence(), $options['relatedCollection']); diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Delete.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Delete.php index e1c8ad928d..dea62bfc16 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Delete.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Indexes/Delete.php @@ -12,6 +12,7 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response as UtopiaResponse; use Utopia\Database\Database; +use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Key; use Utopia\Database\Validator\UID; @@ -96,7 +97,8 @@ class Delete extends Action // Only update status if removing available index if ($index->getAttribute('status') === 'available') { - $index = $dbForProject->updateDocument('indexes', $index->getId(), $index->setAttribute('status', 'deleting')); + $index->setAttribute('status', 'deleting'); + $index = $dbForProject->updateDocument('indexes', $index->getId(), new Document(['status' => 'deleting'])); } $dbForProject->purgeCachedDocument('database_' . $db->getSequence(), $collectionId); diff --git a/src/Appwrite/Platform/Modules/Databases/Workers/Databases.php b/src/Appwrite/Platform/Modules/Databases/Workers/Databases.php index 9d686f72ed..60d70b7942 100644 --- a/src/Appwrite/Platform/Modules/Databases/Workers/Databases.php +++ b/src/Appwrite/Platform/Modules/Databases/Workers/Databases.php @@ -322,13 +322,19 @@ class Databases extends Action $dbForProject->updateDocument( 'attributes', $attribute->getId(), - $attribute->setAttribute('status', 'stuck') + new Document([ + 'error' => $attribute->getAttribute('error'), + 'status' => 'stuck', + ]) ); if (!$relatedAttribute->isEmpty()) { $dbForProject->updateDocument( 'attributes', $relatedAttribute->getId(), - $relatedAttribute->setAttribute('status', 'stuck') + new Document([ + 'error' => $relatedAttribute->getAttribute('error'), + 'status' => 'stuck', + ]) ); } @@ -382,7 +388,11 @@ class Databases extends Action if ($exists) { // Delete the duplicate if created, else update in db $this->deleteIndex($database, $collection, $index, $project, $dbForPlatform, $dbForProject, $queueForRealtime); } else { - $dbForProject->updateDocument('indexes', $index->getId(), $index); + $dbForProject->updateDocument('indexes', $index->getId(), new Document([ + 'attributes' => $index->getAttribute('attributes'), + 'lengths' => $index->getAttribute('lengths'), + 'orders' => $index->getAttribute('orders'), + ])); } } } diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Create.php index ddb1f486d7..65b6ffd5bb 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Create.php @@ -227,7 +227,9 @@ class Create extends Action foreach ($activeDeployments as $activeDeployment) { $activeDeployment->setAttribute('activate', false); - $dbForProject->updateDocument('deployments', $activeDeployment->getId(), $activeDeployment); + $dbForProject->updateDocument('deployments', $activeDeployment->getId(), new Document([ + 'activate' => false, + ])); } } @@ -255,14 +257,17 @@ class Create extends Action 'type' => $type ])); - $function = $function - ->setAttribute('latestDeploymentId', $deployment->getId()) - ->setAttribute('latestDeploymentInternalId', $deployment->getSequence()) - ->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt()) - ->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', '')); - $dbForProject->updateDocument('functions', $function->getId(), $function); + $function = $dbForProject->updateDocument('functions', $function->getId(), new Document([ + 'latestDeploymentId' => $deployment->getId(), + 'latestDeploymentInternalId' => $deployment->getSequence(), + 'latestDeploymentCreatedAt' => $deployment->getCreatedAt(), + 'latestDeploymentStatus' => $deployment->getAttribute('status', ''), + ])); } else { - $deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment->setAttribute('sourceSize', $fileSize)->setAttribute('sourceMetadata', $metadata)); + $deployment = $dbForProject->updateDocument('deployments', $deploymentId, new Document([ + 'sourceSize' => $fileSize, + 'sourceMetadata' => $metadata, + ])); } // Start the build @@ -295,14 +300,17 @@ class Create extends Action 'type' => $type ])); - $function = $function - ->setAttribute('latestDeploymentId', $deployment->getId()) - ->setAttribute('latestDeploymentInternalId', $deployment->getSequence()) - ->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt()) - ->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', '')); - $dbForProject->updateDocument('functions', $function->getId(), $function); + $function = $dbForProject->updateDocument('functions', $function->getId(), new Document([ + 'latestDeploymentId' => $deployment->getId(), + 'latestDeploymentInternalId' => $deployment->getSequence(), + 'latestDeploymentCreatedAt' => $deployment->getCreatedAt(), + 'latestDeploymentStatus' => $deployment->getAttribute('status', ''), + ])); } else { - $deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment->setAttribute('sourceChunksUploaded', $chunksUploaded)->setAttribute('sourceMetadata', $metadata)); + $deployment = $dbForProject->updateDocument('deployments', $deploymentId, new Document([ + 'sourceChunksUploaded' => $chunksUploaded, + 'sourceMetadata' => $metadata, + ])); } } diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Delete.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Delete.php index 5cab00c0fa..3d75919eb8 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Delete.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Delete.php @@ -107,11 +107,12 @@ class Delete extends Action $function = $dbForProject->updateDocument( 'functions', $function->getId(), - $function - ->setAttribute('latestDeploymentCreatedAt', $latestDeployment->isEmpty() ? '' : $latestDeployment->getCreatedAt()) - ->setAttribute('latestDeploymentInternalId', $latestDeployment->isEmpty() ? '' : $latestDeployment->getSequence()) - ->setAttribute('latestDeploymentId', $latestDeployment->isEmpty() ? '' : $latestDeployment->getId()) - ->setAttribute('latestDeploymentStatus', $latestDeployment->isEmpty() ? '' : $latestDeployment->getAttribute('status', '')) + new Document([ + 'latestDeploymentCreatedAt' => $latestDeployment->isEmpty() ? '' : $latestDeployment->getCreatedAt(), + 'latestDeploymentInternalId' => $latestDeployment->isEmpty() ? '' : $latestDeployment->getSequence(), + 'latestDeploymentId' => $latestDeployment->isEmpty() ? '' : $latestDeployment->getId(), + 'latestDeploymentStatus' => $latestDeployment->isEmpty() ? '' : $latestDeployment->getAttribute('status', ''), + ]) ); } diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Duplicate/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Duplicate/Create.php index 2769d3dc1e..9884b12dba 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Duplicate/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Duplicate/Create.php @@ -10,6 +10,7 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Database\Database; +use Utopia\Database\Document; use Utopia\Database\Helpers\ID; use Utopia\Database\Validator\UID; use Utopia\Platform\Action; @@ -119,7 +120,12 @@ class Create extends Action ->setAttribute('latestDeploymentInternalId', $deployment->getSequence()) ->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt()) ->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', '')); - $dbForProject->updateDocument('functions', $function->getId(), $function); + $dbForProject->updateDocument('functions', $function->getId(), new Document([ + 'latestDeploymentId' => $function->getAttribute('latestDeploymentId'), + 'latestDeploymentInternalId' => $function->getAttribute('latestDeploymentInternalId'), + 'latestDeploymentCreatedAt' => $function->getAttribute('latestDeploymentCreatedAt'), + 'latestDeploymentStatus' => $function->getAttribute('latestDeploymentStatus'), + ])); $queueForBuilds ->setType(BUILD_TYPE_DEPLOYMENT) diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Status/Update.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Status/Update.php index c3cd93830a..dab477ef1f 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Status/Update.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Status/Update.php @@ -91,7 +91,7 @@ class Update extends Action $endTime = new \DateTime('now'); $duration = $endTime->getTimestamp() - $startTime->getTimestamp(); - $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment->setAttributes([ + $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([ 'buildEndedAt' => DateTime::now(), 'buildDuration' => $duration, 'status' => 'canceled' @@ -99,7 +99,9 @@ class Update extends Action if ($deployment->getSequence() === $function->getAttribute('latestDeploymentInternalId', '')) { $function = $function->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', '')); - $dbForProject->updateDocument('functions', $function->getId(), $function); + $dbForProject->updateDocument('functions', $function->getId(), new Document([ + 'latestDeploymentStatus' => $function->getAttribute('latestDeploymentStatus'), + ])); } try { diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php index 28226cc98a..53af82e701 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Deployments/Template/Create.php @@ -174,7 +174,12 @@ class Create extends Base ->setAttribute('latestDeploymentInternalId', $deployment->getSequence()) ->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt()) ->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', '')); - $dbForProject->updateDocument('functions', $function->getId(), $function); + $dbForProject->updateDocument('functions', $function->getId(), new Document([ + 'latestDeploymentId' => $function->getAttribute('latestDeploymentId'), + 'latestDeploymentInternalId' => $function->getAttribute('latestDeploymentInternalId'), + 'latestDeploymentCreatedAt' => $function->getAttribute('latestDeploymentCreatedAt'), + 'latestDeploymentStatus' => $function->getAttribute('latestDeploymentStatus'), + ])); $this->updateEmptyManualRule($project, $function, $deployment, $dbForPlatform, $authorization); diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Delete.php b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Delete.php index 4a04afa119..21ec3c66ce 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Delete.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Delete.php @@ -12,6 +12,7 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Database\Database; use Utopia\Database\DateTime; +use Utopia\Database\Document; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; @@ -119,7 +120,10 @@ class Delete extends Base ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('active', false); - $authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); + $authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), new Document([ + 'resourceUpdatedAt' => $schedule->getAttribute('resourceUpdatedAt'), + 'active' => $schedule->getAttribute('active'), + ]))); } } diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php index 9dbf090fda..db11136742 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php @@ -292,7 +292,12 @@ class Create extends Base $function->setAttribute('repositoryInternalId', $repository->getSequence()); } - $function = $dbForProject->updateDocument('functions', $function->getId(), $function); + $function = $dbForProject->updateDocument('functions', $function->getId(), new Document([ + 'scheduleId' => $function->getAttribute('scheduleId'), + 'scheduleInternalId' => $function->getAttribute('scheduleInternalId'), + 'repositoryId' => $function->getAttribute('repositoryId'), + 'repositoryInternalId' => $function->getAttribute('repositoryInternalId'), + ])); // Backwards compatibility with 1.6 behaviour $requestFormat = $request->getHeader('x-appwrite-response-format', System::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', '')); @@ -330,12 +335,12 @@ class Create extends Base referenceType: 'branch' ); - $function = $function - ->setAttribute('latestDeploymentId', $deployment->getId()) - ->setAttribute('latestDeploymentInternalId', $deployment->getSequence()) - ->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt()) - ->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', '')); - $dbForProject->updateDocument('functions', $function->getId(), $function); + $function = $dbForProject->updateDocument('functions', $function->getId(), new Document([ + 'latestDeploymentId' => $deployment->getId(), + 'latestDeploymentInternalId' => $deployment->getSequence(), + 'latestDeploymentCreatedAt' => $deployment->getCreatedAt(), + 'latestDeploymentStatus' => $deployment->getAttribute('status', ''), + ])); } elseif (!$template->isEmpty()) { // Deploy non-VCS from template $deploymentId = ID::unique(); @@ -356,12 +361,12 @@ class Create extends Base 'activate' => true, ])); - $function = $function - ->setAttribute('latestDeploymentId', $deployment->getId()) - ->setAttribute('latestDeploymentInternalId', $deployment->getSequence()) - ->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt()) - ->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', '')); - $dbForProject->updateDocument('functions', $function->getId(), $function); + $function = $dbForProject->updateDocument('functions', $function->getId(), new Document([ + 'latestDeploymentId' => $deployment->getId(), + 'latestDeploymentInternalId' => $deployment->getSequence(), + 'latestDeploymentCreatedAt' => $deployment->getCreatedAt(), + 'latestDeploymentStatus' => $deployment->getAttribute('status', ''), + ])); $queueForBuilds ->setType(BUILD_TYPE_DEPLOYMENT) diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Delete.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Delete.php index 1d108c425f..fb45cee82f 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Delete.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Delete.php @@ -13,6 +13,7 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Database\Database; use Utopia\Database\DateTime; +use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; use Utopia\Platform\Action; @@ -90,7 +91,10 @@ class Delete extends Base $schedule ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('active', false); - $authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); + $authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), new Document([ + 'resourceUpdatedAt' => $schedule->getAttribute('resourceUpdatedAt'), + 'active' => $schedule->getAttribute('active'), + ]))); } $queueForDeletes diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Deployment/Update.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Deployment/Update.php index e78b17bd3f..6b6eda36ab 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Deployment/Update.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Deployment/Update.php @@ -103,7 +103,11 @@ class Update extends Base ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('schedule', $function->getAttribute('schedule')) ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deploymentId'))); - $authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); + $authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), new Document([ + 'resourceUpdatedAt' => $schedule->getAttribute('resourceUpdatedAt'), + 'schedule' => $schedule->getAttribute('schedule'), + 'active' => $schedule->getAttribute('active'), + ]))); $queries = [ Query::equal('trigger', ['manual']), @@ -119,7 +123,10 @@ class Update extends Base ->setAttribute('deploymentId', $deployment->getId()) ->setAttribute('deploymentInternalId', $deployment->getSequence()); - $authorization->skip(fn () => $dbForPlatform->updateDocument('rules', $rule->getId(), $rule)); + $authorization->skip(fn () => $dbForPlatform->updateDocument('rules', $rule->getId(), new Document([ + 'deploymentId' => $rule->getAttribute('deploymentId'), + 'deploymentInternalId' => $rule->getAttribute('deploymentInternalId'), + ]))); }, $queries)); $queueForEvents diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Variables/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Create.php index fa5ca39e7e..fee5b0095d 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Variables/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Create.php @@ -105,7 +105,8 @@ class Create extends Base throw new Exception(Exception::VARIABLE_ALREADY_EXISTS); } - $dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false)); + $function->setAttribute('live', false); + $dbForProject->updateDocument('functions', $function->getId(), new Document(['live' => false])); // Inform scheduler to pull the latest changes $schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId')); @@ -113,7 +114,11 @@ class Create extends Base ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('schedule', $function->getAttribute('schedule')) ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deploymentId'))); - $authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); + $authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), new Document([ + 'resourceUpdatedAt' => $schedule->getAttribute('resourceUpdatedAt'), + 'schedule' => $schedule->getAttribute('schedule'), + 'active' => $schedule->getAttribute('active'), + ]))); $response ->setStatusCode(Response::STATUS_CODE_CREATED) diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Variables/Delete.php b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Delete.php index c447bef81d..5648596826 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Variables/Delete.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Delete.php @@ -11,6 +11,7 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Database\Database; use Utopia\Database\DateTime; +use Utopia\Database\Document; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; use Utopia\Platform\Action; @@ -86,7 +87,8 @@ class Delete extends Base $dbForProject->deleteDocument('variables', $variable->getId()); - $dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false)); + $function->setAttribute('live', false); + $dbForProject->updateDocument('functions', $function->getId(), new Document(['live' => false])); // Inform scheduler to pull the latest changes $schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId')); @@ -94,7 +96,11 @@ class Delete extends Base ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('schedule', $function->getAttribute('schedule')) ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deploymentId'))); - $authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); + $authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), new Document([ + 'resourceUpdatedAt' => $schedule->getAttribute('resourceUpdatedAt'), + 'schedule' => $schedule->getAttribute('schedule'), + 'active' => $schedule->getAttribute('active'), + ]))); $response->noContent(); } diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Variables/Update.php b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Update.php index 9c57a3b9c7..acb066ca9c 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Variables/Update.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Variables/Update.php @@ -10,6 +10,7 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Database\Database; use Utopia\Database\DateTime; +use Utopia\Database\Document; use Utopia\Database\Exception\Duplicate as DuplicateException; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; @@ -99,12 +100,18 @@ class Update extends Base ->setAttribute('search', implode(' ', [$variableId, $function->getId(), $key, 'function'])); try { - $dbForProject->updateDocument('variables', $variable->getId(), $variable); + $dbForProject->updateDocument('variables', $variable->getId(), new Document([ + 'key' => $key, + 'value' => $value ?? $variable->getAttribute('value'), + 'secret' => $secret ?? $variable->getAttribute('secret'), + 'search' => implode(' ', [$variableId, $function->getId(), $key, 'function']), + ])); } catch (DuplicateException $th) { throw new Exception(Exception::VARIABLE_ALREADY_EXISTS); } - $dbForProject->updateDocument('functions', $function->getId(), $function->setAttribute('live', false)); + $function->setAttribute('live', false); + $dbForProject->updateDocument('functions', $function->getId(), new Document(['live' => false])); // Inform scheduler to pull the latest changes $schedule = $dbForPlatform->getDocument('schedules', $function->getAttribute('scheduleId')); @@ -112,7 +119,11 @@ class Update extends Base ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('schedule', $function->getAttribute('schedule')) ->setAttribute('active', !empty($function->getAttribute('schedule')) && !empty($function->getAttribute('deploymentId'))); - $authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule)); + $authorization->skip(fn () => $dbForPlatform->updateDocument('schedules', $schedule->getId(), new Document([ + 'resourceUpdatedAt' => $schedule->getAttribute('resourceUpdatedAt'), + 'schedule' => $schedule->getAttribute('schedule'), + 'active' => $schedule->getAttribute('active'), + ]))); $response->dynamic($variable, Response::MODEL_VARIABLE); } diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php index b4c1e5a84a..5cabb37699 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php @@ -280,7 +280,10 @@ class Builds extends Action $deployment->setAttribute('buildStartedAt', $startTime); $deployment->setAttribute('status', 'processing'); - $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment); + $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([ + 'buildStartedAt' => $startTime, + 'status' => 'processing', + ])); if ($deployment->getSequence() === $resource->getAttribute('latestDeploymentInternalId', '')) { $resource = $dbForProject->updateDocument($resource->getCollection(), $resource->getId(), new Document(['latestDeploymentStatus' => $deployment->getAttribute('status', '')])); @@ -367,7 +370,11 @@ class Builds extends Action ->setAttribute('sourcePath', $source) ->setAttribute('sourceSize', $directorySize) ->setAttribute('totalSize', $directorySize); - $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment); + $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([ + 'sourcePath' => $deployment->getAttribute('sourcePath'), + 'sourceSize' => $deployment->getAttribute('sourceSize'), + 'totalSize' => $deployment->getAttribute('totalSize'), + ])); $queueForRealtime ->setPayload($deployment->getArrayCopy()) @@ -481,7 +488,13 @@ class Builds extends Action $deployment->setAttribute('providerCommitAuthor', APP_VCS_GITHUB_USERNAME); $deployment->setAttribute('providerCommitMessage', "Create '" . $resource->getAttribute('name', '') . "' function"); $deployment->setAttribute('providerCommitUrl', "https://github.com/$cloneOwner/$cloneRepository/commit/$providerCommitHash"); - $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment); + $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([ + 'providerCommitHash' => $deployment->getAttribute('providerCommitHash'), + 'providerCommitAuthorUrl' => $deployment->getAttribute('providerCommitAuthorUrl'), + 'providerCommitAuthor' => $deployment->getAttribute('providerCommitAuthor'), + 'providerCommitMessage' => $deployment->getAttribute('providerCommitMessage'), + 'providerCommitUrl' => $deployment->getAttribute('providerCommitUrl'), + ])); $queueForRealtime ->setPayload($deployment->getArrayCopy()) @@ -529,7 +542,11 @@ class Builds extends Action ->setAttribute('sourcePath', $source) ->setAttribute('sourceSize', $directorySize) ->setAttribute('totalSize', $directorySize); - $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment); + $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([ + 'sourcePath' => $deployment->getAttribute('sourcePath'), + 'sourceSize' => $deployment->getAttribute('sourceSize'), + 'totalSize' => $deployment->getAttribute('totalSize'), + ])); $queueForRealtime ->setPayload($deployment->getArrayCopy()) @@ -544,7 +561,9 @@ class Builds extends Action /** Request the executor to build the code... */ $deployment->setAttribute('status', 'building'); - $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment); + $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([ + 'status' => 'building', + ])); if ($deployment->getSequence() === $resource->getAttribute('latestDeploymentInternalId', '')) { $resource = $dbForProject->updateDocument($resource->getCollection(), $resource->getId(), new Document(['latestDeploymentStatus' => $deployment->getAttribute('status', '')])); @@ -820,7 +839,9 @@ class Builds extends Action if ($affected) { $deployment = $deployment->setAttribute('buildLogs', $currentLogs); - $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment); + $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([ + 'buildLogs' => $currentLogs, + ])); $queueForRealtime ->setPayload($deployment->getArrayCopy()) @@ -906,7 +927,14 @@ class Builds extends Action } } - $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment); + $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([ + 'buildPath' => $deployment->getAttribute('buildPath'), + 'buildSize' => $deployment->getAttribute('buildSize'), + 'totalSize' => $deployment->getAttribute('totalSize'), + 'buildLogs' => $deployment->getAttribute('buildLogs'), + 'adapter' => $deployment->getAttribute('adapter'), + 'fallbackFile' => $deployment->getAttribute('fallbackFile'), + ])); $queueForRealtime ->setPayload($deployment->getArrayCopy()) ->trigger(); @@ -917,12 +945,15 @@ class Builds extends Action $logs = $deployment->getAttribute('buildLogs', ''); $date = \date('H:i:s'); - $logs .= "[$date] [appwrite] Deployment finished. \n"; + $logs .= "\033[90m[$date] \033[90m[\033[0mappwrite\033[90m]\033[32m Deployment finished. \033[0m\n"; $deployment->setAttribute('buildLogs', $logs); /** Update the status */ $deployment->setAttribute('status', 'ready'); - $deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment); + $deployment = $dbForProject->updateDocument('deployments', $deploymentId, new Document([ + 'buildLogs' => $deployment->getAttribute('buildLogs'), + 'status' => 'ready', + ])); Console::log('Status marked as ready'); @@ -1110,7 +1141,11 @@ class Builds extends Action ->setAttribute('resourceUpdatedAt', DateTime::now()) ->setAttribute('schedule', $resource->getAttribute('schedule')) ->setAttribute('active', !empty($resource->getAttribute('schedule')) && !empty($resource->getAttribute('deploymentId'))); - $dbForPlatform->updateDocument('schedules', $schedule->getId(), $schedule); + $dbForPlatform->updateDocument('schedules', $schedule->getId(), new Document([ + 'resourceUpdatedAt' => $schedule->getAttribute('resourceUpdatedAt'), + 'schedule' => $schedule->getAttribute('schedule'), + 'active' => $schedule->getAttribute('active'), + ])); } /** Screenshot site */ @@ -1161,7 +1196,12 @@ class Builds extends Action $deployment->setAttribute('status', 'failed'); $deployment->setAttribute('buildLogs', $message); - $deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment); + $deployment = $dbForProject->updateDocument('deployments', $deploymentId, new Document([ + 'buildEndedAt' => $deployment->getAttribute('buildEndedAt'), + 'buildDuration' => $deployment->getAttribute('buildDuration'), + 'status' => 'failed', + 'buildLogs' => $message, + ])); if ($deployment->getSequence() === $resource->getAttribute('latestDeploymentInternalId', '')) { $resource = $dbForProject->updateDocument($resource->getCollection(), $resource->getId(), new Document(['latestDeploymentStatus' => $deployment->getAttribute('status', '')])); @@ -1469,7 +1509,9 @@ class Builds extends Action $logs .= "[$date] [appwrite] Git action failed. Deployment will continue. \n"; $deployment->setAttribute('buildLogs', $logs); - $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment); + $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([ + 'buildLogs' => $deployment->getAttribute('buildLogs'), + ])); $queueForRealtime ->setPayload($deployment->getArrayCopy()) @@ -1485,10 +1527,12 @@ class Builds extends Action $logs = $deployment->getAttribute('buildLogs', ''); $date = \date('H:i:s'); - $logs .= "[$date] [appwrite] Build has been canceled. \n"; + $logs .= "\033[90m[$date] \033[90m[\033[0mappwrite\033[90m]\033[33m Build has been canceled. \033[0m\n"; $deployment->setAttribute('buildLogs', $logs); - $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment); + $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([ + 'buildLogs' => $deployment->getAttribute('buildLogs'), + ])); $queueForRealtime ->setPayload($deployment->getArrayCopy()) diff --git a/src/Appwrite/Platform/Modules/Projects/Http/DevKeys/Update.php b/src/Appwrite/Platform/Modules/Projects/Http/DevKeys/Update.php index 925416d5d7..f3e47f80ba 100644 --- a/src/Appwrite/Platform/Modules/Projects/Http/DevKeys/Update.php +++ b/src/Appwrite/Platform/Modules/Projects/Http/DevKeys/Update.php @@ -9,6 +9,7 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Database\Database; +use Utopia\Database\Document; use Utopia\Database\Validator\Datetime as DatetimeValidator; use Utopia\Database\Validator\UID; use Utopia\Platform\Action; @@ -73,7 +74,7 @@ class Update extends Action ->setAttribute('name', $name) ->setAttribute('expire', $expire); - $dbForPlatform->updateDocument('devKeys', $key->getId(), $key); + $dbForPlatform->updateDocument('devKeys', $key->getId(), new Document(['name' => $name, 'expire' => $expire])); $dbForPlatform->purgeCachedDocument('projects', $project->getId()); diff --git a/src/Appwrite/Platform/Modules/Projects/Http/Projects/Labels/Update.php b/src/Appwrite/Platform/Modules/Projects/Http/Projects/Labels/Update.php index 1a06c1ee84..de11bb0091 100644 --- a/src/Appwrite/Platform/Modules/Projects/Http/Projects/Labels/Update.php +++ b/src/Appwrite/Platform/Modules/Projects/Http/Projects/Labels/Update.php @@ -11,6 +11,7 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Database\Validator\Queries\Projects; use Appwrite\Utopia\Response; use Utopia\Database\Database; +use Utopia\Database\Document; use Utopia\Database\Validator\UID; use Utopia\Platform\Scope\HTTP; use Utopia\Validator; @@ -77,9 +78,9 @@ class Update extends Action throw new Exception(Exception::PROJECT_NOT_FOUND); } - $project->setAttribute('labels', (array) \array_values(\array_unique($labels))); + $labels = (array) \array_values(\array_unique($labels)); - $project = $dbForPlatform->updateDocument('projects', $project->getId(), $project); + $project = $dbForPlatform->updateDocument('projects', $project->getId(), new Document(['labels' => $labels])); $response->dynamic($project, Response::MODEL_PROJECT); } diff --git a/src/Appwrite/Platform/Modules/Projects/Http/Projects/Team/Update.php b/src/Appwrite/Platform/Modules/Projects/Http/Projects/Team/Update.php index df5b2b6245..ab92d1a15f 100644 --- a/src/Appwrite/Platform/Modules/Projects/Http/Projects/Team/Update.php +++ b/src/Appwrite/Platform/Modules/Projects/Http/Projects/Team/Update.php @@ -10,6 +10,7 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Database\Validator\Queries\Projects; use Appwrite\Utopia\Response; use Utopia\Database\Database; +use Utopia\Database\Document; use Utopia\Database\Query; use Utopia\Database\Validator\UID; use Utopia\Platform\Scope\HTTP; @@ -72,34 +73,31 @@ class Update extends Action $permissions = $this->getPermissions($teamId, $projectId); - $project - ->setAttribute('teamId', $teamId) - ->setAttribute('teamInternalId', $team->getSequence()) - ->setAttribute('$permissions', $permissions); - $project = $dbForPlatform->updateDocument('projects', $project->getId(), $project); + $project = $dbForPlatform->updateDocument('projects', $project->getId(), new Document([ + 'teamId' => $teamId, + 'teamInternalId' => $team->getSequence(), + '$permissions' => $permissions, + ])); $installations = $dbForPlatform->find('installations', [ Query::equal('projectInternalId', [$project->getSequence()]), ]); foreach ($installations as $installation) { - $installation->setAttribute('$permissions', $permissions); - $dbForPlatform->updateDocument('installations', $installation->getId(), $installation); + $dbForPlatform->updateDocument('installations', $installation->getId(), new Document(['$permissions' => $permissions])); } $repositories = $dbForPlatform->find('repositories', [ Query::equal('projectInternalId', [$project->getSequence()]), ]); foreach ($repositories as $repository) { - $repository->setAttribute('$permissions', $permissions); - $dbForPlatform->updateDocument('repositories', $repository->getId(), $repository); + $dbForPlatform->updateDocument('repositories', $repository->getId(), new Document(['$permissions' => $permissions])); } $vcsComments = $dbForPlatform->find('vcsComments', [ Query::equal('projectInternalId', [$project->getSequence()]), ]); foreach ($vcsComments as $vcsComment) { - $vcsComment->setAttribute('$permissions', $permissions); - $dbForPlatform->updateDocument('vcsComments', $vcsComment->getId(), $vcsComment); + $dbForPlatform->updateDocument('vcsComments', $vcsComment->getId(), new Document(['$permissions' => $permissions])); } $response->dynamic($project, Response::MODEL_PROJECT); diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php index 0234073882..8a6964209f 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php @@ -237,7 +237,7 @@ class Create extends Action foreach ($activeDeployments as $activeDeployment) { $activeDeployment->setAttribute('activate', false); - $dbForProject->updateDocument('deployments', $activeDeployment->getId(), $activeDeployment); + $dbForProject->updateDocument('deployments', $activeDeployment->getId(), new Document(['activate' => false])); } } @@ -272,7 +272,12 @@ class Create extends Action ->setAttribute('latestDeploymentInternalId', $deployment->getSequence()) ->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt()) ->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', '')); - $dbForProject->updateDocument('sites', $site->getId(), $site); + $dbForProject->updateDocument('sites', $site->getId(), new Document([ + 'latestDeploymentId' => $deployment->getId(), + 'latestDeploymentInternalId' => $deployment->getSequence(), + 'latestDeploymentCreatedAt' => $deployment->getCreatedAt(), + 'latestDeploymentStatus' => $deployment->getAttribute('status', ''), + ])); $sitesDomain = $platform['sitesDomain']; $domain = ID::unique() . "." . $sitesDomain; @@ -302,7 +307,10 @@ class Create extends Action ])) ); } else { - $deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment->setAttribute('sourceSize', $fileSize)->setAttribute('sourceMetadata', $metadata)); + $deployment = $dbForProject->updateDocument('deployments', $deploymentId, new Document([ + 'sourceSize' => $fileSize, + 'sourceMetadata' => $metadata, + ])); } // Start the build @@ -342,7 +350,12 @@ class Create extends Action ->setAttribute('latestDeploymentInternalId', $deployment->getSequence()) ->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt()) ->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', '')); - $dbForProject->updateDocument('sites', $site->getId(), $site); + $dbForProject->updateDocument('sites', $site->getId(), new Document([ + 'latestDeploymentId' => $site->getAttribute('latestDeploymentId'), + 'latestDeploymentInternalId' => $site->getAttribute('latestDeploymentInternalId'), + 'latestDeploymentCreatedAt' => $site->getAttribute('latestDeploymentCreatedAt'), + 'latestDeploymentStatus' => $site->getAttribute('latestDeploymentStatus'), + ])); $sitesDomain = $platform['sitesDomain']; $domain = ID::unique() . "." . $sitesDomain; @@ -368,7 +381,10 @@ class Create extends Action ])) ); } else { - $deployment = $dbForProject->updateDocument('deployments', $deploymentId, $deployment->setAttribute('sourceChunksUploaded', $chunksUploaded)->setAttribute('sourceMetadata', $metadata)); + $deployment = $dbForProject->updateDocument('deployments', $deploymentId, new Document([ + 'sourceChunksUploaded' => $chunksUploaded, + 'sourceMetadata' => $metadata, + ])); } } diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Delete.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Delete.php index 7339c510b5..efea79395f 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Delete.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Delete.php @@ -107,11 +107,12 @@ class Delete extends Action $site = $dbForProject->updateDocument( 'sites', $site->getId(), - $site - ->setAttribute('latestDeploymentCreatedAt', $latestDeployment->isEmpty() ? '' : $latestDeployment->getCreatedAt()) - ->setAttribute('latestDeploymentInternalId', $latestDeployment->isEmpty() ? '' : $latestDeployment->getSequence()) - ->setAttribute('latestDeploymentId', $latestDeployment->isEmpty() ? '' : $latestDeployment->getId()) - ->setAttribute('latestDeploymentStatus', $latestDeployment->isEmpty() ? '' : $latestDeployment->getAttribute('status', '')) + new Document([ + 'latestDeploymentCreatedAt' => $latestDeployment->isEmpty() ? '' : $latestDeployment->getCreatedAt(), + 'latestDeploymentInternalId' => $latestDeployment->isEmpty() ? '' : $latestDeployment->getSequence(), + 'latestDeploymentId' => $latestDeployment->isEmpty() ? '' : $latestDeployment->getId(), + 'latestDeploymentStatus' => $latestDeployment->isEmpty() ? '' : $latestDeployment->getAttribute('status', ''), + ]) ); } diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Duplicate/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Duplicate/Create.php index d5918f5f12..546549604b 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Duplicate/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Duplicate/Create.php @@ -142,7 +142,12 @@ class Create extends Action ->setAttribute('latestDeploymentInternalId', $deployment->getSequence()) ->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt()) ->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', '')); - $dbForProject->updateDocument('sites', $site->getId(), $site); + $dbForProject->updateDocument('sites', $site->getId(), new Document([ + 'latestDeploymentId' => $site->getAttribute('latestDeploymentId'), + 'latestDeploymentInternalId' => $site->getAttribute('latestDeploymentInternalId'), + 'latestDeploymentCreatedAt' => $site->getAttribute('latestDeploymentCreatedAt'), + 'latestDeploymentStatus' => $site->getAttribute('latestDeploymentStatus'), + ])); // Preview deployments for sites $sitesDomain = $platform['sitesDomain']; diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Status/Update.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Status/Update.php index 45c842bcd0..7ea5cc87cb 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Status/Update.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Status/Update.php @@ -89,7 +89,7 @@ class Update extends Action $endTime = new \DateTime('now'); $duration = $endTime->getTimestamp() - $startTime->getTimestamp(); - $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment->setAttributes([ + $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([ 'buildEndedAt' => DateTime::now(), 'buildDuration' => $duration, 'status' => 'canceled' @@ -97,7 +97,9 @@ class Update extends Action if ($deployment->getSequence() === $site->getAttribute('latestDeploymentInternalId', '')) { $site = $site->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', '')); - $dbForProject->updateDocument('sites', $site->getId(), $site); + $dbForProject->updateDocument('sites', $site->getId(), new Document([ + 'latestDeploymentStatus' => $site->getAttribute('latestDeploymentStatus'), + ])); } try { diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php index 3b79bedbe7..f648c57a83 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/Template/Create.php @@ -187,7 +187,12 @@ class Create extends Base ->setAttribute('latestDeploymentInternalId', $deployment->getSequence()) ->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt()) ->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', '')); - $dbForProject->updateDocument('sites', $site->getId(), $site); + $dbForProject->updateDocument('sites', $site->getId(), new Document([ + 'latestDeploymentId' => $site->getAttribute('latestDeploymentId'), + 'latestDeploymentInternalId' => $site->getAttribute('latestDeploymentInternalId'), + 'latestDeploymentCreatedAt' => $site->getAttribute('latestDeploymentCreatedAt'), + 'latestDeploymentStatus' => $site->getAttribute('latestDeploymentStatus'), + ])); $sitesDomain = $platform['sitesDomain']; $domain = ID::unique() . "." . $sitesDomain; diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php index 680261c691..d01d0d8ca7 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php @@ -203,9 +203,12 @@ class Create extends Base $repository = $dbForPlatform->createDocument('repositories', $repository); $site->setAttribute('repositoryId', $repository->getId()); $site->setAttribute('repositoryInternalId', $repository->getSequence()); - } - $site = $dbForProject->updateDocument('sites', $site->getId(), $site); + $site = $dbForProject->updateDocument('sites', $site->getId(), new Document([ + 'repositoryId' => $repository->getId(), + 'repositoryInternalId' => $repository->getSequence(), + ])); + } $queueForEvents->setParam('siteId', $site->getId()); diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Deployment/Update.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Deployment/Update.php index 630b9e7c5f..18f80ca53f 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Deployment/Update.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Deployment/Update.php @@ -111,7 +111,10 @@ class Update extends Base ->setAttribute('deploymentId', $deployment->getId()) ->setAttribute('deploymentInternalId', $deployment->getSequence()); - $authorization->skip(fn () => $dbForPlatform->updateDocument('rules', $rule->getId(), $rule)); + $authorization->skip(fn () => $dbForPlatform->updateDocument('rules', $rule->getId(), new Document([ + 'deploymentId' => $rule->getAttribute('deploymentId'), + 'deploymentInternalId' => $rule->getAttribute('deploymentInternalId'), + ]))); }, $queries)); $queueForEvents diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Variables/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Variables/Create.php index 4a3254ee23..04b30fbc9c 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Variables/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Variables/Create.php @@ -92,7 +92,9 @@ class Create extends Base throw new Exception(Exception::VARIABLE_ALREADY_EXISTS); } - $dbForProject->updateDocument('sites', $site->getId(), $site->setAttribute('live', false)); + $dbForProject->updateDocument('sites', $site->getId(), new Document([ + 'live' => false, + ])); $response ->setStatusCode(Response::STATUS_CODE_CREATED) diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Variables/Delete.php b/src/Appwrite/Platform/Modules/Sites/Http/Variables/Delete.php index 3e7a2642e6..703806f1aa 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Variables/Delete.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Variables/Delete.php @@ -10,6 +10,7 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Database\Database; +use Utopia\Database\Document; use Utopia\Database\Validator\UID; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; @@ -76,7 +77,9 @@ class Delete extends Base $dbForProject->deleteDocument('variables', $variable->getId()); - $dbForProject->updateDocument('sites', $site->getId(), $site->setAttribute('live', false)); + $dbForProject->updateDocument('sites', $site->getId(), new Document([ + 'live' => false, + ])); $response->noContent(); } diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Variables/Update.php b/src/Appwrite/Platform/Modules/Sites/Http/Variables/Update.php index 4b16ba17a4..99f68a45df 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Variables/Update.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Variables/Update.php @@ -9,6 +9,7 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Database\Database; +use Utopia\Database\Document; use Utopia\Database\Exception\Duplicate as DuplicateException; use Utopia\Database\Validator\UID; use Utopia\Platform\Action; @@ -93,12 +94,19 @@ class Update extends Base ->setAttribute('search', implode(' ', [$variableId, $site->getId(), $key, 'site'])); try { - $dbForProject->updateDocument('variables', $variable->getId(), $variable); + $dbForProject->updateDocument('variables', $variable->getId(), new Document([ + 'key' => $variable->getAttribute('key'), + 'value' => $variable->getAttribute('value'), + 'secret' => $variable->getAttribute('secret'), + 'search' => $variable->getAttribute('search'), + ])); } catch (DuplicateException $th) { throw new Exception(Exception::VARIABLE_ALREADY_EXISTS); } - $dbForProject->updateDocument('sites', $site->getId(), $site->setAttribute('live', false)); + $dbForProject->updateDocument('sites', $site->getId(), new Document([ + 'live' => false, + ])); $response->dynamic($variable, Response::MODEL_VARIABLE); } diff --git a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php index 282d78ea0c..3bf597eaca 100644 --- a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php +++ b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Create.php @@ -266,17 +266,22 @@ class Create extends Action $authorization->skip(fn () => $dbForProject->increaseDocumentAttribute('teams', $team->getId(), 'total', 1)); } } elseif ($membership->getAttribute('confirm') === false) { - $membership->setAttribute('secret', $proofForToken->hash($secret)); - $membership->setAttribute('invited', DateTime::now()); + $secretHash = $proofForToken->hash($secret); + $invitedTime = DateTime::now(); if ($isPrivilegedUser || $isAppUser) { - $membership->setAttribute('joined', DateTime::now()); - $membership->setAttribute('confirm', true); + $membership = $authorization->skip(fn () => $dbForProject->updateDocument('memberships', $membership->getId(), new Document([ + 'secret' => $secretHash, + 'invited' => $invitedTime, + 'joined' => DateTime::now(), + 'confirm' => true + ]))); + } else { + $membership = $dbForProject->updateDocument('memberships', $membership->getId(), new Document([ + 'secret' => $secretHash, + 'invited' => $invitedTime + ])); } - - $membership = ($isPrivilegedUser || $isAppUser) ? - $authorization->skip(fn () => $dbForProject->updateDocument('memberships', $membership->getId(), $membership)) : - $dbForProject->updateDocument('memberships', $membership->getId(), $membership); } else { throw new Exception(Exception::MEMBERSHIP_ALREADY_CONFIRMED); } diff --git a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Delete.php b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Delete.php index 56f5cd8cb5..3b516c2d60 100644 --- a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Delete.php +++ b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Delete.php @@ -131,7 +131,10 @@ class Delete extends Action if (!$membership->isEmpty()) { $team->setAttribute('userId', $membership->getAttribute('userId')); $team->setAttribute('userInternalId', $membership->getAttribute('userInternalId')); - $dbForProject->updateDocument('teams', $team->getId(), $team); + $dbForProject->updateDocument('teams', $team->getId(), new Document([ + 'userId' => $membership->getAttribute('userId'), + 'userInternalId' => $membership->getAttribute('userInternalId'), + ])); } } diff --git a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Status/Update.php b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Status/Update.php index eac516c6fe..46b6c3cacf 100644 --- a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Status/Update.php +++ b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Status/Update.php @@ -189,7 +189,7 @@ class Update extends Action ; } - $membership = $dbForProject->updateDocument('memberships', $membership->getId(), $membership); + $membership = $dbForProject->updateDocument('memberships', $membership->getId(), new Document(['joined' => $membership->getAttribute('joined'), 'confirm' => true])); $dbForProject->purgeCachedDocument('users', $user->getId()); diff --git a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Update.php b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Update.php index 98f342cecd..a935055163 100644 --- a/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Update.php +++ b/src/Appwrite/Platform/Modules/Teams/Http/Memberships/Update.php @@ -116,7 +116,7 @@ class Update extends Action * Update the roles */ $membership->setAttribute('roles', $roles); - $membership = $dbForProject->updateDocument('memberships', $membership->getId(), $membership); + $membership = $dbForProject->updateDocument('memberships', $membership->getId(), new Document(['roles' => $roles])); /** * Replace membership on profile diff --git a/src/Appwrite/Platform/Modules/Teams/Http/Teams/Name/Update.php b/src/Appwrite/Platform/Modules/Teams/Http/Teams/Name/Update.php index 4b058c58e1..ebe751ee1c 100644 --- a/src/Appwrite/Platform/Modules/Teams/Http/Teams/Name/Update.php +++ b/src/Appwrite/Platform/Modules/Teams/Http/Teams/Name/Update.php @@ -10,6 +10,7 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Database\Database; +use Utopia\Database\Document; use Utopia\Database\Validator\UID; use Utopia\Platform\Scope\HTTP; use Utopia\Validator\Text; @@ -67,7 +68,7 @@ class Update extends Action ->setAttribute('name', $name) ->setAttribute('search', implode(' ', [$teamId, $name])); - $team = $dbForProject->updateDocument('teams', $team->getId(), $team); + $team = $dbForProject->updateDocument('teams', $team->getId(), new Document(['name' => $name, 'search' => implode(' ', [$teamId, $name])])); $queueForEvents->setParam('teamId', $team->getId()); diff --git a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Authorize/External/Update.php b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Authorize/External/Update.php index 6b521e56d0..4a34ffd36a 100644 --- a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Authorize/External/Update.php +++ b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Authorize/External/Update.php @@ -98,9 +98,8 @@ class Update extends Action } $providerPullRequestIds = \array_unique(\array_merge($repository->getAttribute('providerPullRequestIds', []), [$providerPullRequestId])); - $repository = $repository->setAttribute('providerPullRequestIds', $providerPullRequestIds); - $repository = $authorization->skip(fn () => $dbForPlatform->updateDocument('repositories', $repository->getId(), $repository)); + $repository = $authorization->skip(fn () => $dbForPlatform->updateDocument('repositories', $repository->getId(), new Document(['providerPullRequestIds' => $providerPullRequestIds]))); $privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY'); $githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID'); diff --git a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Callback/Get.php b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Callback/Get.php index cb45efbf39..914bcaa93e 100644 --- a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Callback/Get.php +++ b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Callback/Get.php @@ -152,7 +152,13 @@ class Get extends Action ->setAttribute('personalRefreshToken', $refreshToken) ->setAttribute('personalAccessToken', $accessToken) ->setAttribute('personalAccessTokenExpiry', $accessTokenExpiry); - $installation = $dbForPlatform->updateDocument('installations', $installation->getId(), $installation); + $installation = $dbForPlatform->updateDocument('installations', $installation->getId(), new Document([ + 'organization' => $installation->getAttribute('organization'), + 'personal' => $installation->getAttribute('personal'), + 'personalRefreshToken' => $installation->getAttribute('personalRefreshToken'), + 'personalAccessToken' => $installation->getAttribute('personalAccessToken'), + 'personalAccessTokenExpiry' => $installation->getAttribute('personalAccessTokenExpiry'), + ])); } } else { $error = 'Installation of the Appwrite GitHub App on organization accounts is restricted to organization owners. As a member of the organization, you do not have the necessary permissions to install this GitHub App. Please contact the organization owner to create the installation from the Appwrite console.'; diff --git a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php index 6d493e2cdb..c9904bb32b 100644 --- a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php +++ b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Deployment.php @@ -326,7 +326,12 @@ trait Deployment ->setAttribute('latestDeploymentInternalId', $deployment->getSequence()) ->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt()) ->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', '')); - $authorization->skip(fn () => $dbForProject->updateDocument($resource->getCollection(), $resource->getId(), $resource)); + $authorization->skip(fn () => $dbForProject->updateDocument($resource->getCollection(), $resource->getId(), new Document([ + 'latestDeploymentId' => $resource->getAttribute('latestDeploymentId'), + 'latestDeploymentInternalId' => $resource->getAttribute('latestDeploymentInternalId'), + 'latestDeploymentCreatedAt' => $resource->getAttribute('latestDeploymentCreatedAt'), + 'latestDeploymentStatus' => $resource->getAttribute('latestDeploymentStatus'), + ]))); if ($resource->getCollection() === 'sites') { $projectId = $project->getId(); diff --git a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php index de6750fe64..c614c80041 100644 --- a/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php +++ b/src/Appwrite/Platform/Modules/VCS/Http/GitHub/Events/Create.php @@ -9,6 +9,7 @@ use Appwrite\Platform\Modules\VCS\Http\GitHub\Deployment; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; use Utopia\Database\Database; +use Utopia\Database\Document; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; use Utopia\Platform\Scope\HTTP; @@ -235,7 +236,7 @@ class Create extends Action if (\in_array($providerPullRequestId, $providerPullRequestIds)) { $providerPullRequestIds = \array_diff($providerPullRequestIds, [$providerPullRequestId]); $repository = $repository->setAttribute('providerPullRequestIds', $providerPullRequestIds); - $repository = $authorization->skip(fn () => $dbForPlatform->updateDocument('repositories', $repository->getId(), $repository)); + $repository = $authorization->skip(fn () => $dbForPlatform->updateDocument('repositories', $repository->getId(), new Document(['providerPullRequestIds' => $providerPullRequestIds]))); } } } diff --git a/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Create.php b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Create.php index bba062a730..04003812f8 100644 --- a/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Create.php +++ b/src/Appwrite/Platform/Modules/VCS/Http/Installations/Repositories/Create.php @@ -114,7 +114,11 @@ class Create extends Action ->setAttribute('personalRefreshToken', $refreshToken) ->setAttribute('personalAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$oauth2->getAccessTokenExpiry(''))); - $dbForPlatform->updateDocument('installations', $installation->getId(), $installation); + $dbForPlatform->updateDocument('installations', $installation->getId(), new Document([ + 'personalAccessToken' => $installation->getAttribute('personalAccessToken'), + 'personalRefreshToken' => $installation->getAttribute('personalRefreshToken'), + 'personalAccessTokenExpiry' => $installation->getAttribute('personalAccessTokenExpiry'), + ])); } try { diff --git a/src/Appwrite/Platform/Tasks/Interval.php b/src/Appwrite/Platform/Tasks/Interval.php index 38fc611465..a7d16e0a52 100644 --- a/src/Appwrite/Platform/Tasks/Interval.php +++ b/src/Appwrite/Platform/Tasks/Interval.php @@ -159,9 +159,7 @@ class Interval extends Action } foreach ($staleExecutions as $execution) { - $execution->setAttribute('status', 'failed'); - $execution->setAttribute('errors', 'Execution timed out'); - $dbForProject->updateDocument('executions', $execution->getId(), $execution); + $dbForProject->updateDocument('executions', $execution->getId(), new Document(['status' => 'failed', 'errors' => 'Execution timed out'])); } $processed++; diff --git a/src/Appwrite/Platform/Tasks/SDKs.php b/src/Appwrite/Platform/Tasks/SDKs.php index 949dbb3e6b..528084f4ea 100644 --- a/src/Appwrite/Platform/Tasks/SDKs.php +++ b/src/Appwrite/Platform/Tasks/SDKs.php @@ -25,6 +25,7 @@ use Appwrite\SDK\Language\Swift; use Appwrite\SDK\Language\Web; use Appwrite\SDK\SDK; use Appwrite\Spec\Swagger2; +use CzProject\GitPhp\Git; use Utopia\Agents\Adapters\OpenAI; use Utopia\Agents\DiffCheck\DiffCheck; use Utopia\Agents\DiffCheck\Options as DiffCheckOptions; @@ -41,29 +42,6 @@ use Utopia\Validator\WhiteList; class SDKs extends Action { - protected array $supportedSDKS = [ - 'web', - 'cli', - 'php', - 'nodejs', - 'deno', - 'python', - 'ruby', - 'flutter', - 'react-native', - 'dart', - 'go', - 'swift', - 'apple', - 'dotnet', - 'android', - 'graphql', - 'rest', - 'markdown', - 'agent-skills', - 'cursor-plugin' - ]; - public static function getName(): string { return 'sdks'; @@ -74,6 +52,19 @@ class SDKs extends Action return Specs::getPlatforms(); } + protected function getSdkConfigPath(): string + { + return __DIR__ . '/../../../../app/config/sdks.php'; + } + + protected function getSupportedSDKs(): array + { + return \array_unique(\array_merge(...\array_values(\array_map( + fn ($platform) => \array_column($platform['sdks'], 'key'), + Config::getParam('sdks') + )))); + } + public function __construct() { $this @@ -100,8 +91,9 @@ class SDKs extends Action if (! $sdks) { $selectedPlatform ??= Console::confirm('Choose Platform ("' . implode('", "', static::getPlatforms()) . '" or "*" for all):'); $selectedSDK ??= \strtolower(Console::confirm('Choose SDK ("*" for all):')); - if ($selectedSDK !== '*' && ! \in_array($selectedSDK, $this->supportedSDKS)) { - throw new \Exception('Unknown SDK "' . $selectedSDK . '" given. Options are: ' . implode(', ', $this->supportedSDKS)); + $supportedSDKs = $this->getSupportedSDKs(); + if ($selectedSDK !== '*' && ! \in_array($selectedSDK, $supportedSDKs)) { + throw new \Exception('Unknown SDK "' . $selectedSDK . '" given. Options are: ' . implode(', ', $supportedSDKs)); } } else { $sdks = explode(',', $sdks); @@ -117,9 +109,6 @@ class SDKs extends Action $prUrls = []; - if ($git) { - $message ??= Console::confirm('Please enter your commit message:'); - } } elseif ($examplesOnly) { $git = false; $prUrls = []; @@ -337,7 +326,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND } // Check if release already exists - $checkReleaseCommand = 'gh release view "' . $releaseVersion . '" --repo "' . $repoName . '" --json url --jq ".url" 2>/dev/null'; + $checkReleaseCommand = 'gh release view ' . \escapeshellarg($releaseVersion) . ' --repo ' . \escapeshellarg($repoName) . ' --json url --jq ".url" 2>/dev/null'; $existingReleaseUrl = trim(\shell_exec($checkReleaseCommand) ?? ''); if (! empty($existingReleaseUrl)) { @@ -368,7 +357,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND } $previousVersion = ''; - $tagListCommand = 'gh release list --repo "' . $repoName . '" --limit 1 --json tagName --jq ".[0].tagName" 2>&1'; + $tagListCommand = 'gh release list --repo ' . \escapeshellarg($repoName) . ' --limit 1 --json tagName --jq ".[0].tagName" 2>&1'; $previousVersion = trim(\shell_exec($tagListCommand) ?? ''); $formattedNotes = "## What's Changed\n\n"; @@ -396,11 +385,11 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND $tempNotesFile = \tempnam(\sys_get_temp_dir(), 'release_notes_'); \file_put_contents($tempNotesFile, $formattedNotes); - $releaseCommand = 'gh release create "' . $releaseVersion . '" \ - --repo "' . $repoName . '" \ - --title "' . $releaseTitle . '" \ - --notes-file "' . $tempNotesFile . '" \ - --target "' . $releaseTarget . '" \ + $releaseCommand = 'gh release create ' . \escapeshellarg($releaseVersion) . ' \ + --repo ' . \escapeshellarg($repoName) . ' \ + --title ' . \escapeshellarg($releaseTitle) . ' \ + --notes-file ' . \escapeshellarg($tempNotesFile) . ' \ + --target ' . \escapeshellarg($releaseTarget) . ' \ 2>&1'; $releaseOutput = []; @@ -491,7 +480,10 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND Console::info("Analyzing SDK changes with AI..."); $aiResult = $this->generateVersionAndChangelog($language, $result); - if ($aiResult !== null) { + if (!empty($aiResult['skip'])) { + Console::warning("Skipping {$language['name']} SDK generation"); + continue; + } elseif ($aiResult !== null) { $newVersion = $aiResult['version']; $newChangelog = $aiResult['changelog']; $aiChangelog = $newChangelog; // Store for PR description @@ -499,19 +491,17 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND // Update the version in the config $this->updateSdkVersion($key, $language['key'], $newVersion); - // Update the changelog file + // Update the source changelog file $this->updateChangelogFile($language['changelog'], $newVersion, $newChangelog); - // Also update CHANGELOG.md in the generated SDK directory - $sdkChangelogPath = $result . '/CHANGELOG.md'; - if (file_exists($sdkChangelogPath)) { - $this->updateChangelogFile($sdkChangelogPath, $newVersion, $newChangelog); - } + // Re-read updated changelog so regeneration includes the new entry + $updatedChangelog = \file_get_contents($language['changelog']); + $sdk->setChangelog($updatedChangelog); // Reload the language config with updated values $language['version'] = $newVersion; - // Regenerate SDK with new version + // Regenerate SDK with new version and updated changelog $sdk->setVersion($newVersion); try { $sdk->generate($result); @@ -527,159 +517,26 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND $gitBranch = $language['gitBranch']; $repoBranch = $language['repoBranch'] ?? 'main'; - if ($git && ! empty($gitUrl)) { - Console::info("Preparing {$language['name']} SDK repository..."); - - \exec('rm -rf ' . $target . ' && \ - mkdir -p ' . $target . ' && \ - cd ' . $target . ' && \ - git init --quiet && \ - git config core.ignorecase false && \ - git config pull.rebase false && \ - git config advice.defaultBranchName false && \ - git remote add origin ' . $gitUrl . ' && \ - git fetch origin --quiet --no-tags --depth 1 ' . $repoBranch . ' 2>&1 | grep -v "^remote:" | grep -v "^From " | grep -v "^ \* " || true && \ - (git checkout -f ' . $repoBranch . ' 2>/dev/null || git checkout -b ' . $repoBranch . ') && \ - git pull origin ' . $repoBranch . ' --quiet --no-tags 2>&1 | grep -v "^From " | grep -v "^ \* " || true && \ - (git checkout -f ' . $gitBranch . ' 2>/dev/null || git checkout -b ' . $gitBranch . ') && \ - (git fetch origin ' . $gitBranch . ' --quiet --no-tags --depth 1 2>/dev/null || git push -u origin ' . $gitBranch . ' --quiet 2>&1 | grep -v "^remote:" || true) && \ - git reset --hard origin/' . $gitBranch . ' 2>/dev/null || true && \ - (if [ -d .github ]; then cp -r .github /tmp/.github-backup-$$ 2>/dev/null; fi) && \ - git rm -rf --cached . 2>/dev/null && \ - git clean -fdx -e .git -e .github 2>/dev/null && \ - cp -r ' . $result . '/. ' . $target . '/ && \ - (if [ -d /tmp/.github-backup-$$/.github ]; then cp -rn /tmp/.github-backup-$$/.github . 2>/dev/null && rm -rf /tmp/.github-backup-$$; fi) && \ - git add -A && \ - git commit -m "' . $message . '" --quiet && \ - git push -u origin ' . $gitBranch . ' --quiet 2>&1 | grep -E "^(To | |[0-9a-f]+\\.\\.[0-9a-f]+)" || true - ', $gitOutput, $gitReturnCode); - - if ($gitReturnCode !== 0) { - Console::warning("Git operations completed with warnings (exit code: {$gitReturnCode})"); + if ($git && !empty($gitUrl)) { + // Generate commit message: use provided message, AI changelog, or fallback + if (! empty($message)) { + $commitMessage = $message; + } elseif (! empty($aiChangelog) && $aiChangelog !== '* No user-facing SDK changes.') { + $commitMessage = "feat: update {$language['name']} SDK to {$language['version']}\n\n{$aiChangelog}"; + } else { + $commitMessage = "chore: update {$language['name']} SDK to {$language['version']}"; } - Console::success("Pushed {$language['name']} SDK to {$gitUrl}"); - if ($git) { - $prTitle = "feat: {$language['name']} SDK update for version {$language['version']}"; + $pushSuccess = $this->pushToGit($language, $target, $result, $gitUrl, $gitBranch, $repoBranch, $commitMessage); - // Build PR body with AI changelog if available - $prBody = "This PR contains updates to the {$language['name']} SDK for version {$language['version']}."; - if (!empty($aiChangelog) && $aiChangelog !== '* No user-facing SDK changes.') { - $prBody .= "\n\n## Changes\n\n{$aiChangelog}"; - } - $repoName = $language['gitUserName'] . '/' . $language['gitRepoName']; - - Console::info("Creating pull request for {$language['name']} SDK..."); - - $prCommand = 'cd ' . $target . ' && \ - gh pr create \ - --repo "' . $repoName . '" \ - --title "' . $prTitle . '" \ - --body "' . $prBody . '" \ - --base "' . $repoBranch . '" \ - --head "' . $gitBranch . '" \ - 2>&1'; - - $prOutput = []; - $prReturnCode = 0; - \exec($prCommand, $prOutput, $prReturnCode); - - if ($prReturnCode === 0) { - Console::success("Successfully created pull request for {$language['name']} SDK"); - if (! empty($prOutput)) { - $prUrls[$language['name']] = end($prOutput); - } - } else { - $errorMessage = implode("\n", $prOutput); - if (strpos($errorMessage, 'already exists') !== false) { - Console::warning("Pull request already exists for {$language['name']} SDK, updating title and body..."); - $prNumberCommand = 'cd ' . $target . ' && \ - gh pr list \ - --repo "' . $repoName . '" \ - --head "' . $gitBranch . '" \ - --json number \ - --jq ".[0].number" \ - 2>&1'; - - $prNumberOutput = []; - $prNumberReturnCode = 0; - \exec($prNumberCommand, $prNumberOutput, $prNumberReturnCode); - - if ($prNumberReturnCode === 0 && ! empty($prNumberOutput[0])) { - $prNumber = trim($prNumberOutput[0]); - - // Use API directly to update PR to avoid deprecated projectCards field - $updateCommand = 'cd ' . $target . ' && \ - gh api \ - --method PATCH \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - /repos/' . $repoName . '/pulls/' . $prNumber . ' \ - -f title="' . $prTitle . '" \ - -f body="' . $prBody . '" \ - 2>&1'; - - $updateOutput = []; - $updateReturnCode = 0; - \exec($updateCommand, $updateOutput, $updateReturnCode); - - if ($updateReturnCode === 0) { - Console::success("Successfully updated pull request for {$language['name']} SDK"); - - $prUrlCommand = 'cd ' . $target . ' && \ - gh pr list \ - --repo "' . $repoName . '" \ - --head "' . $gitBranch . '" \ - --json url \ - --jq ".[0].url" \ - 2>&1'; - - $prUrlOutput = []; - $prUrlReturnCode = 0; - \exec($prUrlCommand, $prUrlOutput, $prUrlReturnCode); - - if ($prUrlReturnCode === 0 && ! empty($prUrlOutput)) { - $prUrls[$language['name']] = trim($prUrlOutput[0]); - } - } else { - $updateErrorMessage = implode("\n", $updateOutput); - Console::error("Failed to update pull request for {$language['name']} SDK: " . $updateErrorMessage); - } - } else { - Console::error("Failed to get PR number for {$language['name']} SDK"); - } - } else { - Console::error("Failed to create pull request for {$language['name']} SDK: " . $errorMessage); - } - } + if ($pushSuccess) { + $this->createPullRequest($language, $target, $gitBranch, $repoBranch, $aiChangelog, $prUrls); } - \exec('chmod -R u+w ' . $target . ' && rm -rf ' . $target); - Console::success("Remove temp directory '{$target}' for {$language['name']} SDK"); + $this->cleanupTarget($target, $language['name']); } - $docDirectories = $language['docDirectories'] ?? ['']; - - if ($version === 'latest') { - continue; - } - - foreach ($docDirectories as $languageTitle => $path) { - $languagePath = strtolower($languageTitle !== 0 ? '/' . $languageTitle : ''); - $examplesSource = $result . '/docs/examples' . $languagePath; - - if (! \is_dir($examplesSource)) { - Console::warning("No code examples found for {$language['name']} SDK at: {$examplesSource}. Skipping copy."); - - continue; - } - - \exec( - 'mkdir -p ' . $resultExamples . $languagePath . ' && \ - cp -r ' . $examplesSource . ' ' . $resultExamples - ); - Console::success("Copied code examples for {$language['name']} SDK to: {$resultExamples}"); - } + $this->copyExamples($language, $version, $result, $resultExamples); } } @@ -693,6 +550,176 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND } } + private function pushToGit(array $language, string $target, string $result, string $gitUrl, string $gitBranch, string $repoBranch, string $commitMessage): bool + { + Console::info("Preparing {$language['name']} SDK repository..."); + + try { + // Init fresh repo + \exec('rm -rf ' . \escapeshellarg($target)); + \mkdir($target, 0755, true); + + $gitClient = new Git(); + $repo = $gitClient->init($target); + + $repo->execute('config', 'core.ignorecase', 'false'); + $repo->execute('config', 'pull.rebase', 'false'); + $repo->execute('config', 'advice.defaultBranchName', 'false'); + $repo->addRemote('origin', $gitUrl); + + // Fetch and checkout base branch (or create if new repo) + try { + $repo->execute('fetch', 'origin', '--quiet', '--no-tags', '--depth', '1', $repoBranch); + try { + $repo->execute('checkout', '-f', $repoBranch); + } catch (\Throwable) { + $repo->execute('checkout', '-b', $repoBranch); + } + } catch (\Throwable) { + $repo->execute('checkout', '-b', $repoBranch); + } + + try { + $repo->execute('pull', 'origin', $repoBranch, '--quiet', '--no-tags'); + } catch (\Throwable) { + } + + // Checkout dev branch (or create if it doesn't exist) + try { + $repo->execute('checkout', '-f', $gitBranch); + } catch (\Throwable) { + $repo->execute('checkout', '-b', $gitBranch); + } + + // Fetch dev branch, or push to create it on remote + try { + $repo->execute('fetch', 'origin', $gitBranch, '--quiet', '--no-tags', '--depth', '1'); + } catch (\Throwable) { + try { + $repo->execute('push', '-u', 'origin', $gitBranch, '--quiet'); + } catch (\Throwable) { + } + } + + // Sync with remote dev branch + try { + $repo->execute('reset', '--hard', "origin/{$gitBranch}"); + } catch (\Throwable) { + } + + // Backup .github before cleaning working tree + $githubDir = $target . '/.github'; + $githubBackup = \sys_get_temp_dir() . '/.github-backup-' . \getmypid(); + $hasGithubDir = \is_dir($githubDir); + if ($hasGithubDir) { + \exec('cp -r ' . \escapeshellarg($githubDir) . ' ' . \escapeshellarg($githubBackup)); + } + + // Clean working tree + try { + $repo->execute('rm', '-rf', '--cached', '.'); + } catch (\Throwable) { + } + try { + $repo->execute('clean', '-fdx', '-e', '.git', '-e', '.github'); + } catch (\Throwable) { + } + + // Copy generated SDK and restore .github + \exec('cp -r ' . \escapeshellarg($result . '/.') . ' ' . \escapeshellarg($target . '/')); + + if ($hasGithubDir && \is_dir($githubBackup)) { + \exec('cp -rn ' . \escapeshellarg($githubBackup . '/.github') . ' ' . \escapeshellarg($target . '/') . ' 2>/dev/null'); + \exec('rm -rf ' . \escapeshellarg($githubBackup)); + } + + // Stage, commit, push + $repo->addAllChanges(); + $repo->commit($commitMessage); + $repo->execute('push', '-u', 'origin', $gitBranch, '--quiet'); + } catch (\Throwable $e) { + Console::warning("Git operations failed for {$language['name']} SDK: " . $e->getMessage()); + return false; + } + + Console::success("Pushed {$language['name']} SDK to {$gitUrl}"); + return true; + } + + private function createPullRequest(array $language, string $target, string $gitBranch, string $repoBranch, string $aiChangelog, array &$prUrls): void + { + $prTitle = "feat: {$language['name']} SDK update for version {$language['version']}"; + $prBody = "This PR contains updates to the {$language['name']} SDK for version {$language['version']}."; + if (!empty($aiChangelog) && $aiChangelog !== '* No user-facing SDK changes.') { + $prBody .= "\n\n## Changes\n\n{$aiChangelog}"; + } + $repoName = $language['gitUserName'] . '/' . $language['gitRepoName']; + + Console::info("Creating pull request for {$language['name']} SDK..."); + + $prCommand = 'cd ' . $target . ' && \ + gh pr create \ + --repo ' . \escapeshellarg($repoName) . ' \ + --title ' . \escapeshellarg($prTitle) . ' \ + --body ' . \escapeshellarg($prBody) . ' \ + --base ' . \escapeshellarg($repoBranch) . ' \ + --head ' . \escapeshellarg($gitBranch) . ' \ + 2>&1'; + + $prOutput = []; + $prReturnCode = 0; + \exec($prCommand, $prOutput, $prReturnCode); + + if ($prReturnCode === 0) { + Console::success("Successfully created pull request for {$language['name']} SDK"); + foreach ($prOutput as $line) { + if (\str_starts_with(trim($line), 'https://')) { + $prUrls[$language['name']] = trim($line); + break; + } + } + } else { + $errorMessage = implode("\n", $prOutput); + if (strpos($errorMessage, 'already exists') === false) { + Console::error("Failed to create pull request for {$language['name']} SDK: " . $errorMessage); + } else { + $this->updateExistingPr($target, $repoName, $gitBranch, $prTitle, $prBody, $language['name'], $prUrls); + } + } + } + + private function cleanupTarget(string $target, string $languageName): void + { + \exec('chmod -R u+w ' . $target . ' && rm -rf ' . $target); + Console::success("Remove temp directory '{$target}' for {$languageName} SDK"); + } + + private function copyExamples(array $language, string $version, string $result, string $resultExamples): void + { + $docDirectories = $language['docDirectories'] ?? ['']; + + if ($version === 'latest') { + return; + } + + foreach ($docDirectories as $languageTitle => $path) { + $languagePath = strtolower($languageTitle !== 0 ? '/' . $languageTitle : ''); + $examplesSource = $result . '/docs/examples' . $languagePath; + + if (! \is_dir($examplesSource)) { + Console::warning("No code examples found for {$language['name']} SDK at: {$examplesSource}. Skipping copy."); + + continue; + } + + \exec( + 'mkdir -p ' . $resultExamples . $languagePath . ' && \ + cp -r ' . $examplesSource . ' ' . $resultExamples + ); + Console::success("Copied code examples for {$language['name']} SDK to: {$resultExamples}"); + } + } + /** * Extract release notes from changelog for a specific version */ @@ -780,20 +807,25 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND required: $object->getNames() ); + $isBeta = !empty($language['beta']); + $betaNote = $isBeta + ? "\n Note: This SDK is in beta (version < 1.0.0). Do NOT bump to 1.0.0. Use `minor` for both breaking changes and new features, `patch` for bug fixes only." + : ''; + $prompt = <<= 1.0.0 + if ($isBeta && ($parsed['versionBump'] === 'major' || \version_compare($parsed['version'], '1.0.0', '>='))) { + Console::warning("Beta SDK {$language['name']} cannot have a major bump or version >= 1.0.0 (AI suggested {$parsed['version']}), skipping"); + return ['skip' => true]; + } + Console::success("✓ Analysis complete"); Console::log(" Version: {$language['version']} → {$parsed['version']} ({$parsed['versionBump']} bump)"); Console::log(" Changelog:"); @@ -891,21 +928,10 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ]; } catch (\Throwable $e) { Console::error('Error generating version and changelog: ' . $e->getMessage()); - return null; } } - /** - * Get the SDK config file path - * - * @return string Path to the SDK config file - */ - protected function getSdkConfigPath(): string - { - return __DIR__ . '/../../../../app/config/sdks.php'; - } - /** * Update SDK version in the config file * @@ -920,7 +946,6 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND if (! file_exists($configPath)) { Console::error("Config file not found: {$configPath}"); - return false; } @@ -936,11 +961,9 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND if (file_put_contents($configPath, $newContent) !== false) { Console::success("Updated {$sdkKey} version from {$oldVersion} to {$newVersion} in config"); - return true; } else { Console::error('Failed to write config file'); - return false; } } @@ -956,11 +979,9 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND if (file_put_contents($configPath, $newContent) !== false) { Console::success("Updated {$sdkKey} version from {$oldVersion} to {$newVersion} in config"); - return true; } else { Console::error('Failed to write config file'); - return false; } } @@ -1019,12 +1040,71 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND if (file_put_contents($changelogPath, $newContent) !== false) { Console::success("Updated changelog at {$changelogPath} with version {$version}"); - return true; } else { Console::error('Failed to write changelog file'); - return false; } } + + private function updateExistingPr(string $target, string $repoName, string $gitBranch, string $prTitle, string $prBody, string $sdkName, array &$prUrls): void + { + Console::warning("Pull request already exists for {$sdkName} SDK, updating title and body..."); + + $prNumberCommand = 'cd ' . $target . ' && \ + gh pr list \ + --repo ' . \escapeshellarg($repoName) . ' \ + --head ' . \escapeshellarg($gitBranch) . ' \ + --json number \ + --jq ".[0].number" \ + 2>&1'; + + $prNumberOutput = []; + $prNumberReturnCode = 0; + \exec($prNumberCommand, $prNumberOutput, $prNumberReturnCode); + + if ($prNumberReturnCode !== 0 || empty($prNumberOutput[0])) { + Console::error("Failed to get PR number for {$sdkName} SDK"); + return; + } + + $prNumber = trim($prNumberOutput[0]); + $apiPath = "/repos/{$repoName}/pulls/{$prNumber}"; + $updateCommand = 'cd ' . $target . ' && \ + gh api \ + --method PATCH \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + ' . \escapeshellarg($apiPath) . ' \ + -f title=' . \escapeshellarg($prTitle) . ' \ + -f body=' . \escapeshellarg($prBody) . ' \ + 2>&1'; + + $updateOutput = []; + $updateReturnCode = 0; + \exec($updateCommand, $updateOutput, $updateReturnCode); + + if ($updateReturnCode !== 0) { + Console::error("Failed to update pull request for {$sdkName} SDK: " . implode("\n", $updateOutput)); + return; + } + + Console::success("Successfully updated pull request for {$sdkName} SDK"); + + $prUrlCommand = 'cd ' . $target . ' && \ + gh pr list \ + --repo ' . \escapeshellarg($repoName) . ' \ + --head ' . \escapeshellarg($gitBranch) . ' \ + --json url \ + --jq ".[0].url" \ + 2>&1'; + + $prUrlOutput = []; + $prUrlReturnCode = 0; + \exec($prUrlCommand, $prUrlOutput, $prUrlReturnCode); + + if ($prUrlReturnCode === 0 && ! empty($prUrlOutput)) { + $prUrls[$sdkName] = trim($prUrlOutput[0]); + } + } } diff --git a/src/Appwrite/Platform/Tasks/ScheduleBase.php b/src/Appwrite/Platform/Tasks/ScheduleBase.php index cfcdc2503e..c55e3d4a6a 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleBase.php +++ b/src/Appwrite/Platform/Tasks/ScheduleBase.php @@ -59,8 +59,11 @@ abstract class ScheduleBase extends Action if (!$project->isEmpty() && $project->getId() !== 'console') { $accessedAt = $project->getAttribute('accessedAt', 0); if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) { - $project->setAttribute('accessedAt', DateTime::now()); - $dbForPlatform->updateDocument('projects', $project->getId(), $project); + $now = DateTime::now(); + $dbForPlatform->updateDocument('projects', $project->getId(), new Document([ + 'accessedAt' => $now + ])); + $project->setAttribute('accessedAt', $now); } } } diff --git a/src/Appwrite/Platform/Workers/Certificates.php b/src/Appwrite/Platform/Workers/Certificates.php index 7de67a2b66..73509819a9 100644 --- a/src/Appwrite/Platform/Workers/Certificates.php +++ b/src/Appwrite/Platform/Workers/Certificates.php @@ -423,7 +423,11 @@ class Certificates extends Action Func $queueForFunctions, Realtime $queueForRealtime ): void { - $rule = $dbForPlatform->updateDocument('rules', $rule->getId(), $rule); + $rule = $dbForPlatform->updateDocument('rules', $rule->getId(), new Document([ + 'status' => $rule->getAttribute('status'), + 'certificateId' => $rule->getAttribute('certificateId'), + 'logs' => $rule->getAttribute('logs'), + ])); $projectId = $rule->getAttribute('projectId'); // Skip events for console project (triggered by auto-ssl generation for 1 click setups) diff --git a/src/Appwrite/Platform/Workers/Messaging.php b/src/Appwrite/Platform/Workers/Messaging.php index 3fd15478d5..d866cc2bd0 100644 --- a/src/Appwrite/Platform/Workers/Messaging.php +++ b/src/Appwrite/Platform/Workers/Messaging.php @@ -365,7 +365,13 @@ class Messaging extends Action $message->setAttribute('deliveredTotal', $deliveredTotal); $message->setAttribute('deliveredAt', DateTime::now()); - $dbForProject->updateDocument('messages', $message->getId(), $message); + $dbForProject->updateDocument('messages', $message->getId(), new Document([ + 'deliveryErrors' => $message->getAttribute('deliveryErrors'), + 'status' => $message->getAttribute('status'), + 'search' => $message->getAttribute('search'), + 'deliveredTotal' => $message->getAttribute('deliveredTotal'), + 'deliveredAt' => $message->getAttribute('deliveredAt'), + ])); // Delete any attachments that were downloaded to local storage if ($provider->getAttribute('type') === MESSAGE_TYPE_EMAIL) { diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index 7bc6fe8d32..d87edaf788 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -571,11 +571,10 @@ class Migrations extends Action } finally { $message = "Export file size {$sizeMB}MB exceeds your plan limit."; - $this->dbForProject->updateDocument('migrations', $migration->getId(), $migration->setAttribute( - 'errors', - json_encode(['code' => 0, 'message' => $message]), - Document::SET_TYPE_APPEND, - )); + $errors = $migration->getAttribute('errors', []); + $errors[] = json_encode(['code' => 0, 'message' => $message]); + $migration->setAttribute('errors', $errors); + $migration = $this->updateMigrationDocument($migration, $project, $queueForRealtime); $this->sendCSVEmail( success: false, diff --git a/src/Appwrite/Platform/Workers/StatsResources.php b/src/Appwrite/Platform/Workers/StatsResources.php index 858370c421..e464455470 100644 --- a/src/Appwrite/Platform/Workers/StatsResources.php +++ b/src/Appwrite/Platform/Workers/StatsResources.php @@ -175,7 +175,7 @@ class StatsResources extends Action try { $this->countImageTransformations($dbForProject, $dbForLogs, $region); } catch (Throwable $th) { - call_user_func_array($this->logError, [$th, "StatsResources", "count_for_buckets_{$project->getId()}"]); + call_user_func_array($this->logError, [$th, "StatsResources", "count_for_image_transformations_{$project->getId()}"]); } try { @@ -202,12 +202,23 @@ class StatsResources extends Action $totalFiles = 0; $totalStorage = 0; $this->foreachDocument($dbForProject, 'buckets', [], function ($bucket) use ($dbForProject, $dbForLogs, $region, &$totalFiles, &$totalStorage) { - $files = $dbForProject->count('bucket_' . $bucket->getSequence()); + try { + $files = $dbForProject->count('bucket_' . $bucket->getSequence()); + } catch (Throwable $th) { + call_user_func_array($this->logError, [$th, "StatsResources", "count_for_bucket_{$bucket->getSequence()}"]); + return; + } $metric = str_replace('{bucketInternalId}', $bucket->getSequence(), METRIC_BUCKET_ID_FILES); $this->createStatsDocuments($region, $metric, $files); - $storage = $dbForProject->sum('bucket_' . $bucket->getSequence(), 'sizeActual'); + try { + $storage = $dbForProject->sum('bucket_' . $bucket->getSequence(), 'sizeActual'); + } catch (Throwable $th) { + call_user_func_array($this->logError, [$th, "StatsResources", "sum_for_bucket_{$bucket->getSequence()}"]); + return; + } + $metric = str_replace('{bucketInternalId}', $bucket->getSequence(), METRIC_BUCKET_ID_FILES_STORAGE); $this->createStatsDocuments($region, $metric, $storage); @@ -227,9 +238,15 @@ class StatsResources extends Action $totalImageTransformations = 0; $last30Days = (new \DateTime())->sub(\DateInterval::createFromDateString('30 days'))->format('Y-m-d 00:00:00'); $this->foreachDocument($dbForProject, 'buckets', [], function ($bucket) use ($dbForProject, $last30Days, $region, &$totalImageTransformations) { - $imageTransformations = $dbForProject->count('bucket_' . $bucket->getSequence(), [ - Query::greaterThanEqual('transformedAt', $last30Days), - ]); + try { + $imageTransformations = $dbForProject->count('bucket_' . $bucket->getSequence(), [ + Query::greaterThanEqual('transformedAt', $last30Days), + ]); + } catch (Throwable $th) { + call_user_func_array($this->logError, [$th, "StatsResources", "count_for_image_transformations_bucket_{$bucket->getSequence()}"]); + return; + } + $metric = str_replace('{bucketInternalId}', $bucket->getSequence(), METRIC_BUCKET_ID_FILES_IMAGES_TRANSFORMED); $this->createStatsDocuments($region, $metric, $imageTransformations); $totalImageTransformations += $imageTransformations; diff --git a/src/Appwrite/Platform/Workers/Webhooks.php b/src/Appwrite/Platform/Workers/Webhooks.php index 56839058de..4855a1d4d8 100644 --- a/src/Appwrite/Platform/Workers/Webhooks.php +++ b/src/Appwrite/Platform/Workers/Webhooks.php @@ -168,12 +168,15 @@ class Webhooks extends Action $webhook->setAttribute('logs', $logs); + $updatePayload = ['logs' => $logs]; + if ($attempts >= \intval(System::getEnv('_APP_WEBHOOK_MAX_FAILED_ATTEMPTS', '10'))) { $webhook->setAttribute('enabled', false); + $updatePayload['enabled'] = false; $this->sendEmailAlert($attempts, $statusCode, $webhook, $project, $dbForPlatform, $queueForMails, $plan); } - $dbForPlatform->updateDocument('webhooks', $webhook->getId(), $webhook); + $dbForPlatform->updateDocument('webhooks', $webhook->getId(), new Document($updatePayload)); $dbForPlatform->purgeCachedDocument('projects', $project->getId()); $this->errors[] = $logs; @@ -184,8 +187,9 @@ class Webhooks extends Action } else { - $webhook->setAttribute('attempts', 0); // Reset attempts on success - $dbForPlatform->updateDocument('webhooks', $webhook->getId(), $webhook); + $dbForPlatform->updateDocument('webhooks', $webhook->getId(), new Document([ + 'attempts' => 0, + ])); $dbForPlatform->purgeCachedDocument('projects', $project->getId()); $queueForStatsUsage ->addMetric(METRIC_WEBHOOKS_SENT, 1) diff --git a/tests/e2e/General/HooksTest.php b/tests/e2e/General/HooksTest.php index 1e7d87608f..812fb61496 100644 --- a/tests/e2e/General/HooksTest.php +++ b/tests/e2e/General/HooksTest.php @@ -128,7 +128,7 @@ class HooksTest extends Scope 'cookie' => $cookie, ]); - $this->assertEquals(401, $response['headers']['status-code']); + $this->assertEquals(403, $response['headers']['status-code']); /** * Test for api controllers @@ -140,7 +140,7 @@ class HooksTest extends Scope 'cookie' => $cookie, ]); - $this->assertEquals(401, $response['headers']['status-code']); + $this->assertEquals(403, $response['headers']['status-code']); $this->assertEquals(Exception::USER_BLOCKED, $response['body']['type']); /** diff --git a/tests/e2e/Scopes/ProjectCustom.php b/tests/e2e/Scopes/ProjectCustom.php index fa317a2b48..c7e3a520b6 100644 --- a/tests/e2e/Scopes/ProjectCustom.php +++ b/tests/e2e/Scopes/ProjectCustom.php @@ -102,67 +102,79 @@ trait ProjectCustom $this->assertEquals(201, $project['headers']['status-code'], 'Project creation failed with status: ' . $project['headers']['status-code']); $this->assertNotEmpty($project['body']); - $key = $this->client->call(Client::METHOD_POST, '/projects/' . $project['body']['$id'] . '/keys', [ - 'origin' => 'http://localhost', - 'content-type' => 'application/json', - 'cookie' => 'a_session_console=' . $this->getRoot()['session'], - 'x-appwrite-project' => 'console', - ], [ - 'keyId' => ID::unique(), - 'name' => 'Demo Project Key', - 'scopes' => [ - 'users.read', - 'users.write', - 'teams.read', - 'teams.write', - 'databases.read', - 'databases.write', - 'collections.read', - 'collections.write', - 'tables.read', - 'tables.write', - 'documents.read', - 'documents.write', - 'rows.read', - 'rows.write', - 'files.read', - 'files.write', - 'buckets.read', - 'buckets.write', - 'sites.read', - 'sites.write', - 'functions.read', - 'functions.write', - 'sites.read', - 'sites.write', - 'execution.read', - 'execution.write', - 'log.read', - 'log.write', - 'locale.read', - 'avatars.read', - 'health.read', - 'rules.read', - 'rules.write', - 'sessions.write', - 'targets.read', - 'targets.write', - 'providers.read', - 'providers.write', - 'messages.read', - 'messages.write', - 'topics.write', - 'topics.read', - 'subscribers.write', - 'subscribers.read', - 'migrations.write', - 'migrations.read', - 'tokens.read', - 'tokens.write', - ], - ]); + $key = null; + for ($i = 0; $i < $maxRetries; $i++) { + $key = $this->client->call(Client::METHOD_POST, '/projects/' . $project['body']['$id'] . '/keys', [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'cookie' => 'a_session_console=' . $this->getRoot()['session'], + 'x-appwrite-project' => 'console', + ], [ + 'keyId' => ID::unique(), + 'name' => 'Demo Project Key', + 'scopes' => [ + 'users.read', + 'users.write', + 'teams.read', + 'teams.write', + 'databases.read', + 'databases.write', + 'collections.read', + 'collections.write', + 'tables.read', + 'tables.write', + 'documents.read', + 'documents.write', + 'rows.read', + 'rows.write', + 'files.read', + 'files.write', + 'buckets.read', + 'buckets.write', + 'sites.read', + 'sites.write', + 'functions.read', + 'functions.write', + 'sites.read', + 'sites.write', + 'execution.read', + 'execution.write', + 'log.read', + 'log.write', + 'locale.read', + 'avatars.read', + 'health.read', + 'rules.read', + 'rules.write', + 'sessions.write', + 'targets.read', + 'targets.write', + 'providers.read', + 'providers.write', + 'messages.read', + 'messages.write', + 'topics.write', + 'topics.read', + 'subscribers.write', + 'subscribers.read', + 'migrations.write', + 'migrations.read', + 'tokens.read', + 'tokens.write', + ], + ]); - $this->assertEquals(201, $key['headers']['status-code']); + if ($key['headers']['status-code'] === 201) { + break; + } + + if ($key['headers']['status-code'] === 401 && $i < $maxRetries - 1) { + \usleep(500000); + continue; + } + } + + $this->assertEquals(201, $key['headers']['status-code'], 'Key creation failed with status: ' . $key['headers']['status-code']); $this->assertNotEmpty($key['body']); $this->assertNotEmpty($key['body']['secret']); diff --git a/tests/e2e/Scopes/Scope.php b/tests/e2e/Scopes/Scope.php index b0203cbf40..a8152ef77e 100644 --- a/tests/e2e/Scopes/Scope.php +++ b/tests/e2e/Scopes/Scope.php @@ -59,12 +59,21 @@ abstract class Scope extends TestCase $root = $this->getRoot(); - $response = $this->client->call(Client::METHOD_GET, '/console/variables', [ - 'origin' => 'http://localhost', - 'content-type' => 'application/json', - 'x-appwrite-project' => 'console', - 'cookie' => 'a_session_console=' . $root['session'], - ]); + for ($i = 0; $i < 3; $i++) { + $response = $this->client->call(Client::METHOD_GET, '/console/variables', [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => 'console', + 'cookie' => 'a_session_console=' . $root['session'], + ]); + + if ($response['headers']['status-code'] === 200 && !empty($response['body'])) { + self::$consoleVariables = $response['body']; + return self::$consoleVariables; + } + + \usleep(500000); + } self::$consoleVariables = $response['body'] ?? []; @@ -140,7 +149,7 @@ abstract class Scope extends TestCase */ protected function getMaxIndexLength(): int { - return $this->getConsoleVariables()['maxIndexLength'] ?? 768; + return $this->getConsoleVariables()['maxIndexLength'] ?? 767; } /** @@ -432,41 +441,67 @@ abstract class Scope extends TestCase return self::$root; } - // Use more entropy to avoid collisions in parallel test execution - $email = uniqid('', true) . getmypid() . bin2hex(random_bytes(4)) . '@localhost.test'; - $password = 'password'; - $name = 'User Name'; + $maxRetries = 5; - $root = $this->client->call(Client::METHOD_POST, '/account', [ - 'origin' => 'http://localhost', - 'content-type' => 'application/json', - 'x-appwrite-project' => 'console', - ], [ - 'userId' => ID::unique(), - 'email' => $email, - 'password' => $password, - 'name' => $name, - ]); + for ($attempt = 0; $attempt < $maxRetries; $attempt++) { + // Use more entropy to avoid collisions in parallel test execution + $email = uniqid('', true) . getmypid() . bin2hex(random_bytes(4)) . '@localhost.test'; + $password = 'password'; + $name = 'User Name'; - $this->assertEquals(201, $root['headers']['status-code']); + $root = $this->client->call(Client::METHOD_POST, '/account', [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => 'console', + ], [ + 'userId' => ID::unique(), + 'email' => $email, + 'password' => $password, + 'name' => $name, + ]); - $session = $this->client->call(Client::METHOD_POST, '/account/sessions/email', [ - 'origin' => 'http://localhost', - 'content-type' => 'application/json', - 'x-appwrite-project' => 'console', - ], [ - 'email' => $email, - 'password' => $password, - ]); + if ($root['headers']['status-code'] !== 201) { + \usleep(500000); + continue; + } - self::$root = [ - '$id' => ID::custom($root['body']['$id']), - 'name' => $root['body']['name'], - 'email' => $root['body']['email'], - 'session' => $session['cookies']['a_session_console'], - ]; + $session = $this->client->call(Client::METHOD_POST, '/account/sessions/email', [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => 'console', + ], [ + 'email' => $email, + 'password' => $password, + ]); - return self::$root; + if (empty($session['cookies']['a_session_console'])) { + \usleep(500000); + continue; + } + + // Verify session is valid before returning + $verify = $this->client->call(Client::METHOD_GET, '/account', [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'cookie' => 'a_session_console=' . $session['cookies']['a_session_console'], + 'x-appwrite-project' => 'console', + ]); + + if ($verify['headers']['status-code'] === 200) { + self::$root = [ + '$id' => ID::custom($root['body']['$id']), + 'name' => $root['body']['name'], + 'email' => $root['body']['email'], + 'session' => $session['cookies']['a_session_console'], + ]; + + return self::$root; + } + + \usleep(500000); + } + + $this->fail('Failed to create and verify root session after ' . $maxRetries . ' attempts'); } /** diff --git a/tests/e2e/Services/Account/AccountCustomClientTest.php b/tests/e2e/Services/Account/AccountCustomClientTest.php index 2ae0b908d7..ea387cff6c 100644 --- a/tests/e2e/Services/Account/AccountCustomClientTest.php +++ b/tests/e2e/Services/Account/AccountCustomClientTest.php @@ -2360,7 +2360,7 @@ class AccountCustomClientTest extends Scope 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session, ])); - $this->assertEquals(401, $response['headers']['status-code']); + $this->assertEquals(403, $response['headers']['status-code']); $response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([ 'origin' => 'http://localhost', @@ -2371,7 +2371,7 @@ class AccountCustomClientTest extends Scope 'password' => $password, ]); - $this->assertEquals(401, $response['headers']['status-code']); + $this->assertEquals(403, $response['headers']['status-code']); } @@ -2440,7 +2440,7 @@ class AccountCustomClientTest extends Scope 'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session, ])); - $this->assertEquals(401, $response['headers']['status-code']); + $this->assertEquals(403, $response['headers']['status-code']); $response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([ 'origin' => 'http://localhost', @@ -2451,7 +2451,7 @@ class AccountCustomClientTest extends Scope 'password' => $password, ]); - $this->assertEquals(401, $response['headers']['status-code']); + $this->assertEquals(403, $response['headers']['status-code']); } public function testCreateJWT(): void diff --git a/tests/e2e/Services/Databases/DatabasesBase.php b/tests/e2e/Services/Databases/DatabasesBase.php index 0c784d717f..7f23f2966c 100644 --- a/tests/e2e/Services/Databases/DatabasesBase.php +++ b/tests/e2e/Services/Databases/DatabasesBase.php @@ -1270,8 +1270,7 @@ trait DatabasesBase ]); $this->assertEquals(400, $attribute['headers']['status-code']); - $maxLength = $this->getMaxIndexLength(); - $this->assertStringContainsString('Index length is longer than the maximum: '.$maxLength, $attribute['body']['message']); + $this->assertStringContainsString('Index length is longer than the maximum:', $attribute['body']['message']); } public function testUpdateAttributeEnum(): void diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 9dff653396..b49f176401 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -2824,7 +2824,6 @@ class FunctionsCustomServerTest extends Scope $this->assertLessThanOrEqual(APP_FUNCTION_LOG_LENGTH_LIMIT, strlen($logs)); $this->assertStringStartsWith('[WARNING] Logs truncated', $logs); - $this->assertStringNotContainsString('z', $logs); $this->assertStringContainsString('a', $logs); // Verify errors are truncated and warning message is present at the beginning @@ -2832,7 +2831,6 @@ class FunctionsCustomServerTest extends Scope $this->assertLessThanOrEqual(APP_FUNCTION_ERROR_LENGTH_LIMIT, strlen($errors)); $this->assertStringStartsWith('[WARNING] Errors truncated', $errors); - $this->assertStringNotContainsString('z', $errors); $this->assertStringContainsString('a', $errors); $this->cleanupFunction($functionId); diff --git a/tests/e2e/Services/Projects/ProjectsBase.php b/tests/e2e/Services/Projects/ProjectsBase.php index e8dac843b4..dc31b7aa85 100644 --- a/tests/e2e/Services/Projects/ProjectsBase.php +++ b/tests/e2e/Services/Projects/ProjectsBase.php @@ -26,26 +26,39 @@ trait ProjectsBase return self::$cachedProjectData; } - $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' => 'Project Test', - ]); - - $this->assertEquals(201, $team['headers']['status-code']); - - $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' => 'Project Test', - 'teamId' => $team['body']['$id'], - 'region' => System::getEnv('_APP_REGION', 'default') - ]); + $teamId = ID::unique(); + $team = null; + for ($i = 0; $i < 3; $i++) { + $team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'teamId' => $teamId, + 'name' => 'Project Test', + ]); + if (\in_array($team['headers']['status-code'], [201, 409])) { + break; + } + \usleep(500000); + } + $this->assertContains($team['headers']['status-code'], [201, 409]); + $project = null; + for ($i = 0; $i < 3; $i++) { + $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' => 'Project Test', + 'teamId' => $team['body']['$id'] ?? $teamId, + 'region' => System::getEnv('_APP_REGION', 'default') + ]); + if ($project['headers']['status-code'] === 201) { + break; + } + \usleep(500000); + } $this->assertEquals(201, $project['headers']['status-code']); self::$cachedProjectData = [ @@ -396,27 +409,42 @@ trait ProjectsBase protected function setupProject(mixed $params, ?string $teamId = null, bool $newTeam = true): string { if ($newTeam) { - $team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([ + $generatedTeamId = $teamId ?? ID::unique(); + $team = null; + for ($i = 0; $i < 3; $i++) { + $team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'teamId' => $generatedTeamId, + 'name' => 'Project Test', + ]); + if (\in_array($team['headers']['status-code'], [201, 409])) { + break; + } + \usleep(500000); + } + + $this->assertContains($team['headers']['status-code'], [201, 409], 'Setup team failed with status code: ' . $team['headers']['status-code'] . ' and response: ' . json_encode($team['body'], JSON_PRETTY_PRINT)); + + $teamId = $team['body']['$id'] ?? $generatedTeamId; + } + + $project = null; + for ($i = 0; $i < 3; $i++) { + $project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ - 'teamId' => $teamId ?? ID::unique(), - 'name' => 'Project Test', + ...$params, + 'teamId' => $teamId, ]); - - $this->assertEquals(201, $team['headers']['status-code'], 'Setup team failed with status code: ' . $team['headers']['status-code'] . ' and response: ' . json_encode($team['body'], JSON_PRETTY_PRINT)); - - $teamId = $team['body']['$id']; + if ($project['headers']['status-code'] === 201) { + break; + } + \usleep(500000); } - $project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - ...$params, - 'teamId' => $teamId, - ]); - $this->assertEquals(201, $project['headers']['status-code'], 'Setup project failed with status code: ' . $project['headers']['status-code'] . ' and response: ' . json_encode($project['body'], JSON_PRETTY_PRINT)); return $project['body']['$id']; diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index f5937bccfa..52e59f3e72 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -951,26 +951,22 @@ class ProjectsConsoleClientTest extends Scope $this->assertEquals(204, $response['headers']['status-code']); - $emails = $this->getLastEmail(2); - $this->assertCount(2, $emails); - $this->assertEquals('custommailer@appwrite.io', $emails[0]['from'][0]['address']); - $this->assertEquals('Custom Mailer', $emails[0]['from'][0]['name']); - $this->assertEquals('reply@appwrite.io', $emails[0]['replyTo'][0]['address']); - $this->assertEquals('Custom Mailer', $emails[0]['replyTo'][0]['name']); - $this->assertEquals('Custom SMTP email sample', $emails[0]['subject']); - $this->assertStringContainsStringIgnoringCase('working correctly', $emails[0]['text']); - $this->assertStringContainsStringIgnoringCase('working correctly', $emails[0]['html']); - $this->assertStringContainsStringIgnoringCase('251 Little Falls Drive', $emails[0]['text']); - $this->assertStringContainsStringIgnoringCase('251 Little Falls Drive', $emails[0]['html']); + $smtpProbe = function ($email) { + $this->assertEquals('Custom SMTP email sample', $email['subject']); + }; + $email1 = $this->getLastEmailByAddress('testuser@appwrite.io', $smtpProbe); + $email2 = $this->getLastEmailByAddress('testusertwo@appwrite.io', $smtpProbe); - $to = [ - $emails[0]['to'][0]['address'], - $emails[1]['to'][0]['address'] - ]; - \sort($to); - - $this->assertEquals('testuser@appwrite.io', $to[0]); - $this->assertEquals('testusertwo@appwrite.io', $to[1]); + $this->assertEquals('custommailer@appwrite.io', $email1['from'][0]['address']); + $this->assertEquals('Custom Mailer', $email1['from'][0]['name']); + $this->assertEquals('reply@appwrite.io', $email1['replyTo'][0]['address']); + $this->assertEquals('Custom Mailer', $email1['replyTo'][0]['name']); + $this->assertEquals('Custom SMTP email sample', $email1['subject']); + $this->assertStringContainsStringIgnoringCase('working correctly', $email1['text']); + $this->assertStringContainsStringIgnoringCase('working correctly', $email1['html']); + $this->assertStringContainsStringIgnoringCase('251 Little Falls Drive', $email1['text']); + $this->assertStringContainsStringIgnoringCase('251 Little Falls Drive', $email1['html']); + $this->assertEquals('custommailer@appwrite.io', $email2['from'][0]['address']); $response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/smtp/tests', array_merge([ 'content-type' => 'application/json', @@ -6427,11 +6423,12 @@ class ProjectsConsoleClientTest extends Scope $userId = $response['body']['userId']; - $lastEmail = $this->getLastEmail(1, function ($email) use ($url) { + $userEmail = $this->getUser()['email']; + + $lastEmail = $this->getLastEmailByAddress($userEmail, function ($email) use ($url) { $this->assertStringContainsString($url, $email['html'] ?? ''); }); - $this->assertEquals($this->getUser()['email'], $lastEmail['to'][0]['address']); $this->assertEquals('Password Reset for ' . $this->getProject()['name'], $lastEmail['subject']); $expectedUrl = $url . "&userId=" . $userId . "&secret="; @@ -6450,7 +6447,7 @@ class ProjectsConsoleClientTest extends Scope ], $this->getHeaders()), [ 'userId' => ID::unique(), - 'email' => $this->getUser()['email'], + 'email' => $userEmail, 'url' => $url, ] ); @@ -6460,11 +6457,10 @@ class ProjectsConsoleClientTest extends Scope $userId = $response['body']['userId']; - $lastEmail = $this->getLastEmail(1, function ($email) use ($url) { + $lastEmail = $this->getLastEmailByAddress($userEmail, function ($email) use ($url) { $this->assertStringContainsString($url, $email['html'] ?? ''); }); - $this->assertEquals($this->getUser()['email'], $lastEmail['to'][0]['address']); $this->assertEquals('Password Reset for ' . $this->getProject()['name'], $lastEmail['subject']); $expectedUrl = $url . "&userId=" . $userId . "&secret="; @@ -6483,7 +6479,7 @@ class ProjectsConsoleClientTest extends Scope ], $this->getHeaders()), [ 'userId' => ID::unique(), - 'email' => $this->getUser()['email'], + 'email' => $userEmail, 'url' => $url, ] ); @@ -6493,11 +6489,10 @@ class ProjectsConsoleClientTest extends Scope $userId = $response['body']['userId']; - $lastEmail = $this->getLastEmail(1, function ($email) use ($url, $userId) { + $lastEmail = $this->getLastEmailByAddress($userEmail, function ($email) use ($url, $userId) { $this->assertStringContainsString($url . '?userId=' . $userId, $email['html'] ?? ''); }); - $this->assertEquals($this->getUser()['email'], $lastEmail['to'][0]['address']); $this->assertEquals('Password Reset for ' . $this->getProject()['name'], $lastEmail['subject']); $expectedUrl = $url . "?userId=" . $userId . "&secret="; @@ -6516,7 +6511,7 @@ class ProjectsConsoleClientTest extends Scope ], $this->getHeaders()), [ 'userId' => ID::unique(), - 'email' => $this->getUser()['email'], + 'email' => $userEmail, 'url' => $url, ] ); @@ -6526,11 +6521,10 @@ class ProjectsConsoleClientTest extends Scope $userId = $response['body']['userId']; - $lastEmail = $this->getLastEmail(1, function ($email) use ($url, $userId) { + $lastEmail = $this->getLastEmailByAddress($userEmail, function ($email) use ($url, $userId) { $this->assertStringContainsString($url . '?userId=' . $userId, $email['html'] ?? ''); }); - $this->assertEquals($this->getUser()['email'], $lastEmail['to'][0]['address']); $this->assertEquals('Password Reset for ' . $this->getProject()['name'], $lastEmail['subject']); $expectedUrl = $url . "?userId=" . $userId . "&secret="; @@ -6549,7 +6543,7 @@ class ProjectsConsoleClientTest extends Scope ], $this->getHeaders()), [ 'userId' => ID::unique(), - 'email' => $this->getUser()['email'], + 'email' => $userEmail, 'url' => $url, ] ); @@ -6559,11 +6553,10 @@ class ProjectsConsoleClientTest extends Scope $userId = $response['body']['userId']; - $lastEmail = $this->getLastEmail(1, function ($email) { + $lastEmail = $this->getLastEmailByAddress($userEmail, function ($email) { $this->assertStringContainsString('INJECTED', $email['html'] ?? ''); }); - $this->assertEquals($this->getUser()['email'], $lastEmail['to'][0]['address']); $this->assertEquals('Password Reset for ' . $this->getProject()['name'], $lastEmail['subject']); $this->assertStringContainsString('INJECTED', $lastEmail['html']); diff --git a/tests/e2e/Services/Projects/Schedules/SchedulesBase.php b/tests/e2e/Services/Projects/Schedules/SchedulesBase.php index cd3be80149..681e39b662 100644 --- a/tests/e2e/Services/Projects/Schedules/SchedulesBase.php +++ b/tests/e2e/Services/Projects/Schedules/SchedulesBase.php @@ -16,26 +16,39 @@ trait SchedulesBase return self::$cachedScheduleProjectData; } - $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' => 'Schedule Test Team', - ]); - - $this->assertEquals(201, $team['headers']['status-code']); - - $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' => 'Schedule Test Project', - 'teamId' => $team['body']['$id'], - 'region' => System::getEnv('_APP_REGION', 'default'), - ]); + $teamId = ID::unique(); + $team = null; + for ($i = 0; $i < 3; $i++) { + $team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'teamId' => $teamId, + 'name' => 'Schedule Test Team', + ]); + if (\in_array($team['headers']['status-code'], [201, 409])) { + break; + } + \usleep(500000); + } + $this->assertContains($team['headers']['status-code'], [201, 409]); + $project = null; + for ($i = 0; $i < 3; $i++) { + $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' => 'Schedule Test Project', + 'teamId' => $team['body']['$id'] ?? $teamId, + 'region' => System::getEnv('_APP_REGION', 'default'), + ]); + if ($project['headers']['status-code'] === 201) { + break; + } + \usleep(500000); + } $this->assertEquals(201, $project['headers']['status-code']); $projectId = $project['body']['$id'];