Conversation
… operations and simplify presence setup logic.
| ->setHttpPath('/v1/presences/:presenceId') | ||
| ->desc('Delete presence') | ||
| ->groups(['api', 'presences']) | ||
| ->label('scope', 'documents.write') |
There was a problem hiding this comment.
adding document scopes for now for easy start, will change it to presence scope
Greptile SummaryThis PR introduces a new Presence API module (
Confidence Score: 2/5Not safe to merge — two of the five endpoints have bugs that cause fatal PHP errors or silently corrupt data on the privileged code path. Three P0/P1 findings affect the core write paths: a missing HTTP trait that prevents endpoint registration, a PHP type error in src/Appwrite/Platform/Modules/Presences/HTTP/Update.php and src/Appwrite/Platform/Modules/Presences/HTTP/Upsert.php require fixes before this can ship. Important Files Changed
Reviews (1): Last reviewed commit: "Refactor presence tests to handle unauth..." | Re-trigger Greptile |
| use Utopia\Validator\Nullable; | ||
| use Utopia\Validator\Text; | ||
|
|
||
| class Update extends PresenceAction |
There was a problem hiding this comment.
Missing
use HTTP; trait — endpoint will not be registered
Update extends PresenceAction (which extends PlatformAction) but never declares use HTTP;. Every other HTTP action in the codebase that calls setHttpMethod / setHttpPath uses this trait (e.g. Teams/Http/Teams/Create.php, Presences/HTTP/Upsert.php). Without it the Utopia framework will not register PATCH /v1/presences/:presenceId as an HTTP route, so all update calls will silently 404 or be ignored.
| class Update extends PresenceAction | |
| class Update extends PresenceAction | |
| { | |
| use HTTP; | |
There was a problem hiding this comment.
not needed its alreay present
| $user = $dbForProject->getDocument('users', $userId); | ||
| if ($user->isEmpty()) { | ||
| throw new Exception(Exception::USER_NOT_FOUND, params: [$userId]); | ||
| } | ||
|
|
||
| $userInternalId = $user->getId(); | ||
| $resolvedUserId = $user->getId(); | ||
| } |
There was a problem hiding this comment.
$user variable overwritten — fatal type error on API-key path
In the else branch, $user (typed as Appwrite\Utopia\Database\Documents\User) is overwritten with the return value of getDocument(...), which is a plain Utopia\Database\Document. This overwritten value is then passed to setPermission(Document $document, ?array $permissions, User $user, ...) on line 116, which type-hints the third argument as User. PHP 8 will throw a fatal TypeError on every API-key / privileged upsert that provides permissions.
// Store the fetched document separately; keep $user intact for setPermission
$fetchedUser = $dbForProject->getDocument('users', $userId);
if ($fetchedUser->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND, params: [$userId]);
}
$userInternalId = $fetchedUser->getSequence();
$resolvedUserId = $fetchedUser->getId();| $resolvedUserId = $userId; | ||
| if (!$isAPIKey && !$isPrivilegedUser) { | ||
| $userInternalId = $user->getId(); | ||
| $resolvedUserId = $user->getId(); | ||
| } else { | ||
| $user = $dbForProject->getDocument('users', $userId); | ||
| if ($user->isEmpty()) { | ||
| throw new Exception(Exception::USER_NOT_FOUND, params: [$userId]); | ||
| } | ||
|
|
||
| $userInternalId = $user->getId(); | ||
| $resolvedUserId = $user->getId(); | ||
| } |
There was a problem hiding this comment.
userInternalId set with getId() instead of getSequence()
Every other module that writes a userInternalId field (Account MFA, Teams memberships, VCS installations) uses $user->getSequence(), not $user->getId(). getId() returns the external $id, making userInternalId identical to userId — the field stores the wrong identifier and indexes on _key_userInternal become useless.
| $resolvedUserId = $userId; | |
| if (!$isAPIKey && !$isPrivilegedUser) { | |
| $userInternalId = $user->getId(); | |
| $resolvedUserId = $user->getId(); | |
| } else { | |
| $user = $dbForProject->getDocument('users', $userId); | |
| if ($user->isEmpty()) { | |
| throw new Exception(Exception::USER_NOT_FOUND, params: [$userId]); | |
| } | |
| $userInternalId = $user->getId(); | |
| $resolvedUserId = $user->getId(); | |
| } | |
| if (!$isAPIKey && !$isPrivilegedUser) { | |
| $userInternalId = $user->getSequence(); | |
| $resolvedUserId = $user->getId(); | |
| } else { | |
| $fetchedUser = $dbForProject->getDocument('users', $userId); | |
| if ($fetchedUser->isEmpty()) { | |
| throw new Exception(Exception::USER_NOT_FOUND, params: [$userId]); | |
| } | |
| $userInternalId = $fetchedUser->getSequence(); | |
| $resolvedUserId = $fetchedUser->getId(); | |
| } |
There was a problem hiding this comment.
its already updated
|
|
||
| // Naming it presenceLogs as later it might be only be used as a presence events table only and not for the actual presence | ||
| // TODO: check between var_string and var_id => userInternalId, userId(they can be null as well in case of guest login) | ||
| 'presenceLogs' => [ |
There was a problem hiding this comment.
Multiple TODO comments indicate unresolved design decisions
The collection definition contains at least five // TODO: comments (type choice for userId/userInternalId, missing expiry default, hostname vs region semantics, source default field, perms_md5 index strategy). Unresolved TODOs in a merged schema definition create a migration burden — changes after the fact require a migration script for all existing rows.
| Authorization $authorization | ||
| ): void { | ||
| $isAPIKey = $user->isApp($authorization->getRoles()); | ||
| $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); | ||
|
|
||
| if ($userId && !$isAPIKey && !$isPrivilegedUser) { | ||
| throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, 'userId is not allowed for non-API key and non-privileged users'); |
There was a problem hiding this comment.
Requiring
userId on every PATCH is unnecessarily restrictive
The presence record already stores userId; a PATCH endpoint should update only the supplied fields. Forcing server-side callers to re-supply userId on every update (or receive a 400) makes partial updates awkward and diverges from the sparse-update pattern described in AGENTS.md. Consider removing the mandatory userId check from the update endpoint and only applying it to the upsert/create path.
There was a problem hiding this comment.
yes that is what happening, we are not forcing server side to provide userId everytime but if its provided, it must be the server side and not client
🔄 PHP-Retry SummaryFlaky tests detected across commits: Commit
|
| Test | Retries | Total Time | Details |
|---|---|---|---|
UsageTest::testFunctionsStats |
1 | 10.27s | Logs |
UsageTest::testPrepareSitesStats |
1 | 8ms | Logs |
UsageTest::testEmbeddingsTextUsageDoesNotBreakProjectUsage |
1 | 7ms | Logs |
LegacyCustomClientTest::testAttributeResponseModels |
1 | 243.01s | Logs |
LegacyCustomServerTest::testAttributeResponseModels |
1 | 242.96s | Logs |
VectorsDBConsoleClientTest::testGetCollectionLogs |
1 | 13ms | Logs |
DatabasesConsoleClientTest::testGetCollectionLogs |
1 | 8ms | Logs |
Commit 4fea92c - 6 flaky tests
| Test | Retries | Total Time | Details |
|---|---|---|---|
LegacyCustomClientTest::testCreateIndexes |
1 | 240.98s | Logs |
VectorsDBConsoleClientTest::testGetCollectionLogs |
1 | 4ms | Logs |
DatabasesConsoleClientTest::testGetCollectionLogs |
1 | 18ms | Logs |
UsageTest::testFunctionsStats |
1 | 10.21s | Logs |
UsageTest::testPrepareSitesStats |
1 | 7ms | Logs |
UsageTest::testEmbeddingsTextUsageDoesNotBreakProjectUsage |
1 | 5ms | Logs |
Commit 70f42a0 - 4 flaky tests
| Test | Retries | Total Time | Details |
|---|---|---|---|
UsageTest::testFunctionsStats |
1 | 10.20s | Logs |
UsageTest::testPrepareSitesStats |
1 | 8ms | Logs |
UsageTest::testEmbeddingsTextUsageDoesNotBreakProjectUsage |
1 | 6ms | Logs |
TablesDBConsoleClientTest::testNotContains |
1 | 240.56s | Logs |
Commit 1d32faf - 28 flaky tests
| Test | Retries | Total Time | Details |
|---|---|---|---|
UsageTest::testFunctionsStats |
1 | 10.26s | Logs |
UsageTest::testPrepareSitesStats |
1 | 8ms | Logs |
UsageTest::testEmbeddingsTextUsageDoesNotBreakProjectUsage |
1 | 6ms | Logs |
WebhooksCustomServerTest::testDeleteDeployment |
1 | 16ms | Logs |
WebhooksCustomServerTest::testDeleteFunction |
1 | 13ms | Logs |
WebhooksCustomServerTest::testCreateCollection |
1 | 12ms | Logs |
WebhooksCustomServerTest::testCreateAttributes |
1 | 14ms | Logs |
WebhooksCustomServerTest::testCreateDocument |
1 | 37ms | Logs |
WebhooksCustomServerTest::testUpdateDocument |
1 | 11ms | Logs |
WebhooksCustomServerTest::testDeleteDocument |
1 | 14ms | Logs |
WebhooksCustomServerTest::testCreateTable |
1 | 14ms | Logs |
WebhooksCustomServerTest::testCreateColumns |
1 | 13ms | Logs |
WebhooksCustomServerTest::testCreateRow |
1 | 10ms | Logs |
WebhooksCustomServerTest::testUpdateRow |
1 | 15ms | Logs |
WebhooksCustomServerTest::testDeleteRow |
1 | 17ms | Logs |
WebhooksCustomServerTest::testCreateStorageBucket |
1 | 7ms | Logs |
WebhooksCustomServerTest::testUpdateStorageBucket |
1 | 11ms | Logs |
WebhooksCustomServerTest::testCreateBucketFile |
1 | 14ms | Logs |
WebhooksCustomServerTest::testUpdateBucketFile |
1 | 8ms | Logs |
WebhooksCustomServerTest::testDeleteBucketFile |
1 | 7ms | Logs |
WebhooksCustomServerTest::testDeleteStorageBucket |
1 | 15ms | Logs |
WebhooksCustomServerTest::testCreateTeam |
1 | 8ms | Logs |
WebhooksCustomServerTest::testUpdateTeam |
1 | 14ms | Logs |
WebhooksCustomServerTest::testUpdateTeamPrefs |
1 | 20ms | Logs |
WebhooksCustomServerTest::testDeleteTeam |
1 | 7ms | Logs |
WebhooksCustomServerTest::testCreateTeamMembership |
1 | 22ms | Logs |
WebhooksCustomServerTest::testDeleteTeamMembership |
1 | 23ms | Logs |
WebhooksCustomServerTest::testWebhookAutoDisable |
1 | 31ms | Logs |
Commit 7167fe2 - 8 flaky tests
| Test | Retries | Total Time | Details |
|---|---|---|---|
UsageTest::testFunctionsStats |
1 | 10.25s | Logs |
UsageTest::testPrepareSitesStats |
1 | 7ms | Logs |
UsageTest::testEmbeddingsTextUsageDoesNotBreakProjectUsage |
1 | 6ms | Logs |
LegacyConsoleClientTest::testListDocumentsWithCache |
1 | 648ms | Logs |
LegacyConsoleClientTest::testListDocumentsCachePurgedByUpdate |
1 | 1.26s | Logs |
LegacyCustomClientTest::testAttributeResponseModels |
1 | 242.68s | Logs |
VectorsDBConsoleClientTest::testGetCollectionLogs |
1 | 9ms | Logs |
DatabasesConsoleClientTest::testGetCollectionLogs |
1 | 7ms | Logs |
Note: Flaky test results are tracked for the last 5 commits
✨ Benchmark results
⚡ Benchmark Comparison
|
…presence actions. Add automated tests for expired presence deletion during maintenance.
…tion in realtime messages
This comment has been minimized.
This comment has been minimized.
…ests. Updated authorization roles synchronization in the realtime connection and added exception handling for user retrieval. Improved connection pool size logic for PubSub workers and added comprehensive tests for presence events to ensure correct message ordering and validation.
…eption handling for MongoDB adapter to update existing documents instead of creating new ones when presenceId mismatches occur.
|
|
||
| try { | ||
| if ($this->getSupportForUniqueIndexBasedUpsert()) { | ||
| return $this->transactionalUpsertForUser($dbForProject, $presenceDocument, $presenceId, $userId); |
There was a problem hiding this comment.
mongodb and postgres adapter doesn't work on upsert based on unique indexes so manual upsert logic for this to have consistent and deterministic result
Failing cases might be -> user sending an unique $id for an already present user which will lead to create but due to unique index it will throw duplicate exception
And due to duplicate exception we again have to do this manually
Better to do it from start for mongodb and postgres
| 'source' => 'rest', | ||
| 'expiry' => $expiry ?? DateTime::addSeconds(new \DateTime(), 15 * 60), | ||
| // TODO: finding a way to find hostname | ||
| // 'hostname' => $hostname, |
There was a problem hiding this comment.
@abnegate can you share what can be in the hostname, is it the region? or the pod?(https://github.com/appwrite/rfc/pull/61/changes#diff-fb6f9b5a0d2472eb42988a79c2869de8bcb62540594d0d6f0aa85f5a775bddd5R100)
| ], | ||
| contentType: ContentType::NONE, | ||
| )) | ||
| ->param('presenceId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Presence unique ID.', false, ['dbForProject']) |
There was a problem hiding this comment.
@abnegate I think we should proivide here the bulk deletion rather? In case of upsert, update, etc it kind of not useful
But in the delete, the admin might want to delete in bulk
WDYT?
What does this PR do?
(Provide a description of what this PR does and why it's needed.)
Test Plan
(Write your test plan here. If you changed any code, please provide us with clear instructions on how you verified your changes work. Screenshots may also be helpful.)
Related PRs and Issues
Checklist