Compare commits

..
Author SHA1 Message Date
loks0nandClaude Sonnet 4.5 b6793dc0b5 Fix remaining blocked account test assertions
Update testBlockedAccount and testSelfBlockedAccount to expect 403
instead of 401 for blocked user responses. These were missed in the
previous test assertion update.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-06 17:43:36 +00:00
loks0nandClaude Sonnet 4.5 a2ad25a00a Update test assertions for blocked user status code
Change expected status code from 401 to 403 for USER_BLOCKED errors
to match the semantic change in error codes.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-06 17:30:51 +00:00
loks0nandClaude Sonnet 4.5 16ad05792d Change blocked user/resource errors from 401 to 403
- Update USER_BLOCKED from 401 to 403 and simplify description
- Update GENERAL_RESOURCE_BLOCKED from 401 to 403

Rationale: 403 Forbidden is the correct HTTP status for authorization
failures where the user is authenticated but not permitted access.
401 Unauthorized is for authentication failures.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-06 17:30:51 +00:00
Luke B. SilverandGitHub e5f0bc2df6 Merge pull request #11457 from appwrite/fix-migration-errors
Fix migration errors
2026-03-06 17:20:39 +00:00
Chirag AggarwalandGitHub 0d3d14d9e1 Merge pull request #11465 from appwrite/optimize-update-document-calls
perf: optimize updateDocument() calls to use sparse documents
2026-03-06 21:17:31 +05:30
Chirag Aggarwal f2826189c6 fix: remove asserting z does not exist in truncated logs 2026-03-06 18:24:04 +05:30
Chirag Aggarwal 8b026d3459 perf: optimize updateDocument() calls to use sparse documents
Optimize updateDocument() calls across the codebase to pass only changed
attributes as sparse Document objects rather than full documents. This is
more efficient because updateDocument() internally performs array_merge().

Changes:
- Updated 58 files to use sparse Document objects
- Added Performance Patterns section to AGENTS.md with optimization guidelines
- Applied pattern to Workers, Functions, Sites, Teams, VCS modules
- Updated app/controllers/api files (account, users, messaging)
- Updated app infrastructure files (realtime, general, init/resources, shared/api)

Exceptions maintained:
- Migration files (need full document updates by design)
- Cases with 6+ attributes (marginal benefit)
- Complex nested relationship logic
2026-03-06 17:05:19 +05:30
Jake Barnby 2306072dda Merge branch 'fix-ce-test-flakes' into 1.8.x 2026-03-06 14:18:37 +13:00
Jake BarnbyandClaude Opus 4.6 91edf82060 fix: database index length assertion and email race in password recovery
- Remove specific index length number from testPatchAttribute assertion
  since the value differs between shared/non-shared table modes (767 vs
  768) and the console API returns the console project's value, not the
  user project's
- Use getLastEmailByAddress in testPasswordRecoveryUrlParams to avoid
  retrieving emails from parallel test classes sharing the same maildev

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 03:41:35 +13:00
Chirag AggarwalandGitHub 3fc50e4d8d Merge pull request #11464 from appwrite/fix/schedule-base-project-access-update
fix: Update in-memory project document after accessedAt update
2026-03-05 20:07:49 +05:30
Chirag Aggarwal 71dff441ed fix: Update in-memory project document after accessedAt update
Previously, the updateProjectAccess method updated the database with the new

accessedAt timestamp but did not update the in-memory project document. This

caused the if statement to constantly evaluate to true on subsequent calls,

triggering unnecessary database updates.
2026-03-05 20:03:42 +05:30
Jake BarnbyandClaude Opus 4.6 65780d75f9 fix: test flakes — correct index length fallback, add retries
- Fix getMaxIndexLength() fallback from 768 to 767 to match MariaDB's
  actual InnoDB index key limit
- Add retry logic to getConsoleVariables() to handle intermittent 401s
- Add retry logic to API key creation in ProjectCustom to prevent
  cascading 401 failures in test methods

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 03:01:23 +13:00
Jake BarnbyandClaude Opus 4.6 ee107d30b3 fix: make Projects tests parallel-safe
- Use getLastEmailByAddress for SMTP tests instead of getLastEmail(2) to
  avoid shared mail server state issues under parallel execution
- Add retry logic to setupProject, setupProjectData, and
  setupScheduleProjectData for intermittent 401 errors

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 01:20:09 +13:00
Shmuel FogelandGitHub 499ca71c28 Merge pull request #11459 from appwrite/stop-on-error
Phpunit stop on error
2026-03-05 14:06:04 +02:00
fogelito cfe60dd2a2 Merge branch '1.8.x' of https://github.com/appwrite/appwrite into stop-on-error 2026-03-05 12:32:33 +02:00
Jake BarnbyandClaude Opus 4.6 ab85942276 fix: add retry and session verification to getRoot() for parallel tests
getRoot() now retries up to 5 times with session verification to handle
race conditions when multiple paratest workers initialize simultaneously.
Previously, if account creation or session creation failed under load,
all subsequent test requests would fail with 401.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 23:19:54 +13:00
Chirag AggarwalandGitHub 0214f22d20 Merge pull request #11461 from appwrite/fix/stale-project-overwrites-oauth
fix: prevent stale project documents from overwriting OAuth providers
2026-03-05 15:46:51 +05:30
Chirag Aggarwal e0269e268f fix: re-read project from DB before updating accessedAt to prevent stale writes
Stale in-memory project documents in ScheduleBase (and request-scoped
copies in api.php/general.php) were overwriting current DB state when
updateProjectAccess triggered. Because Database::updateDocument uses
array_merge with the passed document taking priority, cached projects
missing recent OAuth provider changes would silently disable them.

Now fetches a fresh project document from the DB before writing, so only
accessedAt is updated without clobbering other fields.
2026-03-05 15:44:21 +05:30
fogelito a4844599c6 Merge branch '1.8.x' of https://github.com/appwrite/appwrite into stop-on-error 2026-03-05 12:00:07 +02:00
Shmuel FogelandGitHub ce876adb3a Merge pull request #11460 from appwrite/stats-count-buckets
Stats count image transformations errors
2026-03-05 11:58:40 +02:00
fogelito 59edcf1934 Merge branch '1.8.x' of https://github.com/appwrite/appwrite into stop-on-error 2026-03-05 11:42:16 +02:00
fogelito 5821832da6 count_for_image_transformations_bucket_ 2026-03-05 11:08:45 +02:00
Chirag AggarwalandGitHub d4df2c51de Merge pull request #11442 from appwrite/fix/sdk-beta-versioning
refactor: improve SDK generation task reliability and maintainability
2026-03-05 14:17:25 +05:30
fogelito dbbc57bc37 Merge branch '1.8.x' of https://github.com/appwrite/appwrite into stop-on-error 2026-03-05 10:31:32 +02:00
fogelito d772220414 stopOnError 2026-03-05 10:28:42 +02:00
Chirag AggarwalandGitHub 5510231b8e Merge branch '1.8.x' into fix/sdk-beta-versioning 2026-03-05 09:52:47 +05:30
Chirag Aggarwal 152de6c584 remove goto 2026-03-05 09:32:07 +05:30
Chirag Aggarwal 161c5af66f fixes 2026-03-04 19:35:30 +05:30
Chirag Aggarwal c497a68613 fixes 2026-03-04 19:33:41 +05:30
Chirag Aggarwal 50f9c67862 fixes 2026-03-04 19:10:39 +05:30
Chirag Aggarwal 198f9a64a3 refactoring 2026-03-04 19:03:21 +05:30
Chirag Aggarwal 300aaeb251 fix: escape shell arguments in SDK git and gh CLI commands
Replace raw string interpolation with escapeshellarg() for all
arguments passed to exec/shell_exec calls that build git commit,
gh pr create, gh api, and gh release commands. This prevents
shell injection from AI-generated changelog text or any other
dynamically constructed values.
2026-03-04 18:46:49 +05:30
Chirag Aggarwal f989ccde57 fix: add post-parse guard for beta SDK version bumps
Add a programmatic guard after parsing the AI response that rejects
major bumps or versions >= 1.0.0 for beta SDKs. When triggered, the
SDK is skipped with a warning instead of proceeding with an invalid
version.
2026-03-04 18:44:29 +05:30
Chirag Aggarwal fc88d4b4ab feat: auto-generate commit messages for SDK pushes
Remove the manual commit message prompt. When AI is available and
produces a changelog, use it as the commit message. Otherwise fall
back to a descriptive message based on SDK name and version. A
manually provided --message flag still takes priority.
2026-03-04 13:19:48 +05:30
Chirag Aggarwal df604cb2ef refactor: derive supported SDKs from config instead of hardcoded array
Replace the static supportedSDKS array with a getSupportedSDKs() method
that reads SDK keys from the sdks.php config file. This eliminates the
need to maintain the list in two places.
2026-03-04 13:16:55 +05:30
Chirag Aggarwal a6d58a847d fix: handle beta SDK versioning in DiffCheck prompt
Beta SDKs (version < 1.0.0) should not be bumped to 1.0.0. The prompt
now checks the beta flag from sdk config and instructs the AI to use
minor bumps for both breaking changes and new features, and patch for
bug fixes only. Also adds a rule to wrap code identifiers in backticks
for better changelog rendering.
2026-03-04 13:15:26 +05:30
76 changed files with 1163 additions and 626 deletions
+30
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
+3 -3
View File
@@ -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,
+1 -1
View File
@@ -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,
+37 -15
View File
@@ -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());
+44 -16
View File
@@ -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());
+7 -3
View File
@@ -132,8 +132,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()
])));
}
/**
@@ -1649,7 +1650,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
+15 -6
View File
@@ -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']);
}
+10 -3
View File
@@ -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([
+4 -1
View File
@@ -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");
}
+1
View File
@@ -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.*"
},
Generated
+66 -2
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "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",
@@ -9043,7 +9107,7 @@
],
"aliases": [],
"minimum-stability": "dev",
"stability-flags": [],
"stability-flags": {},
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
-18
View File
@@ -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:
+1
View File
@@ -6,6 +6,7 @@
colors="true"
processIsolation="false"
stopOnFailure="false"
stopOnError="false"
cacheDirectory=".phpunit.cache"
>
<extensions>
@@ -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());
@@ -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())
@@ -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());
@@ -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());
@@ -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());
+12 -2
View File
@@ -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;
@@ -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']);
@@ -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);
@@ -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'),
]));
}
}
}
@@ -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,
]));
}
}
@@ -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', ''),
])
);
}
@@ -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)
@@ -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 {
@@ -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);
@@ -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'),
])));
}
}
@@ -283,7 +283,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', ''));
@@ -321,12 +326,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();
@@ -347,12 +352,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)
@@ -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
@@ -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
@@ -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)
@@ -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();
}
@@ -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);
}
@@ -279,7 +279,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', '')]));
@@ -366,7 +369,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())
@@ -480,7 +487,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())
@@ -528,7 +541,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())
@@ -543,7 +560,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', '')]));
@@ -819,7 +838,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())
@@ -905,7 +926,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();
@@ -916,12 +944,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');
@@ -1109,7 +1140,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 */
@@ -1160,7 +1195,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', '')]));
@@ -1467,7 +1507,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())
@@ -1483,10 +1525,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())
@@ -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());
@@ -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);
}
@@ -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);
@@ -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,
]));
}
}
@@ -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', ''),
])
);
}
@@ -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'];
@@ -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 {
@@ -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;
@@ -193,9 +193,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());
@@ -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
@@ -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)
@@ -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();
}
@@ -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);
}
@@ -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);
}
@@ -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'),
]));
}
}
@@ -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());
@@ -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
@@ -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());
@@ -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');
@@ -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.';
@@ -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();
@@ -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])));
}
}
}
@@ -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 {
+1 -3
View File
@@ -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++;
+294 -214
View File
@@ -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 = <<<PROMPT
You are a technical writer generating a changelog for the {$language['name']} SDK release.
Analyze the git diff below and return a JSON response with the version bump type, new version number, and changelog.
## Versioning
Current version: {$language['version']}
Determine the semantic version bump:
- `major`: Breaking changes (removed/renamed public APIs, changed method signatures, dropped support)
- `minor`: New features that are backward-compatible (new methods, new optional parameters, new classes)
- `patch`: Bug fixes, documentation updates, refactors with no API surface change
{$betaNote}
When multiple change types are present, use the highest severity bump.
## Changelog guidelines
@@ -810,6 +842,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
- Only include changes visible to SDK users (public API, behavior, docs, examples, CLI)
- Ignore: CI/CD pipelines (.github/), internal tooling, code formatting, test infrastructure
- Consolidate related changes into one entry (e.g., "Added `timeout`, `retries`, and `baseUrl` options" not three separate lines)
- Wrap all method names, parameter names, class names, and code identifiers in backticks (e.g., `listDocuments`, `ttl`)
- If the diff contains zero user-facing changes, return a single entry: "No user-facing SDK changes"
- Do not speculate only document what the diff explicitly shows
@@ -856,7 +889,6 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
if (empty(trim($responseContent))) {
Console::warning('AI returned empty response');
return null;
}
@@ -866,7 +898,6 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
Console::warning('Failed to parse AI response as JSON: ' . json_last_error_msg());
Console::log('Raw response:');
Console::log($responseContent);
return null;
}
@@ -875,6 +906,12 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
return null;
}
// Guard: beta SDKs must not be bumped to >= 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]);
}
}
}
+5 -2
View File
@@ -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);
}
}
}
@@ -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)
+7 -1
View File
@@ -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) {
+4 -5
View File
@@ -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,
@@ -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 {
@@ -227,9 +227,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;
+7 -3
View File
@@ -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)
+2 -2
View File
@@ -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']);
/**
+72 -60
View File
@@ -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']);
+72 -37
View File
@@ -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');
}
/**
@@ -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
@@ -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
@@ -2771,7 +2771,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
@@ -2779,7 +2778,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);
+62 -34
View File
@@ -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'];
@@ -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']);
@@ -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'];