From d4b0ea64adf917d490aceaafc2cd73a5dc1ebeb4 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 30 Jan 2026 21:41:01 +1300 Subject: [PATCH 1/3] Fix event caching --- src/Appwrite/Functions/EventProcessor.php | 48 +++++---- .../Functions/FunctionsCustomClientTest.php | 97 +++++++++++++++++++ 2 files changed, 119 insertions(+), 26 deletions(-) diff --git a/src/Appwrite/Functions/EventProcessor.php b/src/Appwrite/Functions/EventProcessor.php index 8ed841d30d..8791cbd6ec 100644 --- a/src/Appwrite/Functions/EventProcessor.php +++ b/src/Appwrite/Functions/EventProcessor.php @@ -39,38 +39,34 @@ class EventProcessor return \json_decode($cachedFunctionEvents, true) ?? []; } - try { - $events = []; - $limit = 100; - $sum = 100; - $offset = 0; + $events = []; + $limit = 100; + $sum = 100; + $offset = 0; - while ($sum >= $limit) { - $functions = $dbForProject->find('functions', [ - Query::select(['$id', 'events']), - Query::limit($limit), - Query::offset($offset), - Query::orderAsc('$sequence'), - ]); + while ($sum >= $limit) { + $functions = $dbForProject->getAuthorization()->skip(fn () => $dbForProject->find('functions', [ + Query::select(['$id', 'events']), + Query::limit($limit), + Query::offset($offset), + Query::orderAsc('$sequence'), + ])); - $sum = \count($functions); - $offset = $offset + $limit; + $sum = \count($functions); + $offset = $offset + $limit; - foreach ($functions as $function) { - $functionEvents = $function->getAttribute('events', []); - if (!empty($functionEvents)) { - $events = array_merge($events, $functionEvents); - } + foreach ($functions as $function) { + $functionEvents = $function->getAttribute('events', []); + if (!empty($functionEvents)) { + $events = array_merge($events, $functionEvents); } } - - $uniqueEvents = \array_flip(\array_unique($events)); - $dbForProject->getCache()->save($cacheKey, \json_encode($uniqueEvents)); - - return $uniqueEvents; - } catch (\Throwable $e) { - return []; } + + $uniqueEvents = \array_flip(\array_unique($events)); + $dbForProject->getCache()->save($cacheKey, \json_encode($uniqueEvents)); + + return $uniqueEvents; } /** diff --git a/tests/e2e/Services/Functions/FunctionsCustomClientTest.php b/tests/e2e/Services/Functions/FunctionsCustomClientTest.php index 2f02fd92ba..ab94ff2433 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomClientTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomClientTest.php @@ -510,4 +510,101 @@ class FunctionsCustomClientTest extends Scope $template = $this->getTemplate('invalid-template-id'); $this->assertEquals(404, $template['headers']['status-code']); } + + /** + * Test that event-triggered functions work when the triggering request + * comes from a client SDK (session auth) that doesn't have permission + * to read the functions collection. + */ + public function testEventTriggerWithClientAuth() + { + $functionId = $this->setupFunction([ + 'functionId' => ID::unique(), + 'name' => 'Test Client Event Trigger', + 'runtime' => 'node-22', + 'entrypoint' => 'index.js', + 'events' => [ + 'databases.*.collections.*.documents.*.create', + ], + 'timeout' => 15, + ]); + + $this->setupDeployment($functionId, [ + 'code' => $this->packageFunction('event-handler'), + 'activate' => true + ]); + + $database = $this->client->call(Client::METHOD_POST, '/databases', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'databaseId' => ID::unique(), + 'name' => 'Test Database', + ]); + $this->assertEquals(201, $database['headers']['status-code']); + $databaseId = $database['body']['$id']; + + $collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'collectionId' => ID::unique(), + 'name' => 'Test Collection', + 'permissions' => [ + Role::users()->toString(), + ], + 'documentSecurity' => false, + ]); + $this->assertEquals(201, $collection['headers']['status-code']); + $collectionId = $collection['body']['$id']; + + $attribute = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'key' => 'name', + 'size' => 255, + 'required' => false, + ]); + $this->assertEquals(202, $attribute['headers']['status-code']); + + sleep(2); + + $document = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'documentId' => ID::unique(), + 'data' => ['name' => 'Test Document'], + ]); + $this->assertEquals(201, $document['headers']['status-code']); + $documentId = $document['body']['$id']; + + $this->assertEventually(function () use ($functionId, $documentId) { + $executions = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(200, $executions['headers']['status-code']); + $this->assertGreaterThan(0, count($executions['body']['executions']), 'Function should have been triggered by document creation'); + + $lastExecution = $executions['body']['executions'][0]; + $this->assertEquals('completed', $lastExecution['status']); + $this->assertEquals(204, $lastExecution['responseStatusCode']); + $this->assertStringContainsString($documentId, $lastExecution['logs']); + }, 20000, 500); + + $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->cleanupFunction($functionId); + } } From 67e43cc1a5c59e36888b5286c4aff2ff2b60b131 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 30 Jan 2026 21:48:36 +1300 Subject: [PATCH 2/3] Push vs merge --- src/Appwrite/Functions/EventProcessor.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Functions/EventProcessor.php b/src/Appwrite/Functions/EventProcessor.php index 8791cbd6ec..e9c3b7241a 100644 --- a/src/Appwrite/Functions/EventProcessor.php +++ b/src/Appwrite/Functions/EventProcessor.php @@ -58,7 +58,7 @@ class EventProcessor foreach ($functions as $function) { $functionEvents = $function->getAttribute('events', []); if (!empty($functionEvents)) { - $events = array_merge($events, $functionEvents); + \array_push($events, ...$functionEvents); } } } @@ -93,7 +93,7 @@ class EventProcessor $webhookEvents = $webhook->getAttribute('events', []); if (!empty($webhookEvents)) { - $events = array_merge($events, $webhookEvents); + \array_push($events, ...$webhookEvents); } } From 252dc6b9932bd9e97d92fbcc3619954111731efb Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 30 Jan 2026 22:44:19 +1300 Subject: [PATCH 3/3] Fix test --- tests/e2e/Services/Functions/FunctionsCustomClientTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/e2e/Services/Functions/FunctionsCustomClientTest.php b/tests/e2e/Services/Functions/FunctionsCustomClientTest.php index ab94ff2433..98013e6879 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomClientTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomClientTest.php @@ -7,6 +7,7 @@ use Tests\E2E\Scopes\ProjectCustom; use Tests\E2E\Scopes\Scope; use Tests\E2E\Scopes\SideClient; use Utopia\Database\Helpers\ID; +use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\System\System; @@ -553,7 +554,7 @@ class FunctionsCustomClientTest extends Scope 'collectionId' => ID::unique(), 'name' => 'Test Collection', 'permissions' => [ - Role::users()->toString(), + Permission::create(Role::users()), ], 'documentSecurity' => false, ]);