diff --git a/src/Appwrite/Messaging/Adapter/Realtime.php b/src/Appwrite/Messaging/Adapter/Realtime.php index 7c51bbb2e5..05de941cb3 100644 --- a/src/Appwrite/Messaging/Adapter/Realtime.php +++ b/src/Appwrite/Messaging/Adapter/Realtime.php @@ -270,16 +270,16 @@ class Realtime extends MessagingAdapter $payload = $event['data']['payload'] ?? []; - foreach ($this->subscriptions[$event['project']] as $role => $subscription) { + foreach ($this->subscriptions[$event['project']] as $role => $subscriptionsByChannel) { foreach ($event['data']['channels'] as $channel) { if ( - !\array_key_exists($channel, $this->subscriptions[$event['project']][$role]) + !\array_key_exists($channel, $subscriptionsByChannel) || (!\in_array($role, $event['roles']) && !\in_array(Role::any()->toString(), $event['roles'])) ) { continue; } - foreach ($this->subscriptions[$event['project']][$role][$channel] as $id => $subscriptions) { + foreach ($subscriptionsByChannel[$channel] as $id => $subscriptions) { $matched = []; foreach ($subscriptions as $subscriptionId => $data) { @@ -298,7 +298,6 @@ class Realtime extends MessagingAdapter $receivers[$id] += $matched; } } - break; } } diff --git a/src/Appwrite/Utopia/Database/RuntimeQuery.php b/src/Appwrite/Utopia/Database/RuntimeQuery.php index fe2f14fc0a..cfc7fbfd7d 100644 --- a/src/Appwrite/Utopia/Database/RuntimeQuery.php +++ b/src/Appwrite/Utopia/Database/RuntimeQuery.php @@ -32,6 +32,22 @@ class RuntimeQuery extends Query Query::TYPE_SELECT ]; + /** + * Checks if a query is a select("*") query. + * + * @param Query $query + * @return bool + */ + public static function isSelectAll(Query $query): bool + { + if ($query->getMethod() !== Query::TYPE_SELECT) { + return false; + } + + $values = $query->getValues(); + return count($values) === 1 && $values[0] === '*'; + } + /** * Validates a select query - only select("*") is allowed in Realtime * @@ -44,10 +60,7 @@ class RuntimeQuery extends Query return; } - $values = $query->getValues(); - $isSelectAll = count($values) === 1 && $values[0] === '*'; - - if (!$isSelectAll) { + if (!self::isSelectAll($query)) { throw new \InvalidArgumentException( 'Only select("*") is allowed in Realtime queries. select("*") means "listen to all events".' ); diff --git a/tests/unit/Utopia/Database/Query/RuntimeQueryTest.php b/tests/unit/Utopia/Database/Query/RuntimeQueryTest.php index 51d3a307da..7745d535cf 100644 --- a/tests/unit/Utopia/Database/Query/RuntimeQueryTest.php +++ b/tests/unit/Utopia/Database/Query/RuntimeQueryTest.php @@ -16,10 +16,19 @@ class RuntimeQueryTest extends TestCase { } + /** + * Helper to compile and filter queries in one step for tests. + */ + private function compileAndFilter(array $queries, array $payload): array + { + $compiled = RuntimeQuery::compile($queries); + return RuntimeQuery::filter($compiled, $payload); + } + public function testFilterEmptyQueries(): void { $payload = ['name' => 'John', 'age' => 30]; - $result = RuntimeQuery::filter([], $payload); + $result = $this->compileAndFilter([], $payload); $this->assertEquals($payload, $result); } @@ -27,7 +36,7 @@ class RuntimeQueryTest extends TestCase { $queries = [Query::equal('name', ['Jane'])]; $payload = ['name' => 'John', 'age' => 30]; - $result = RuntimeQuery::filter($queries, $payload); + $result = $this->compileAndFilter($queries, $payload); $this->assertEquals([], $result); } @@ -35,7 +44,7 @@ class RuntimeQueryTest extends TestCase { $queries = [Query::equal('name', ['John'])]; $payload = ['name' => 'John', 'age' => 30]; - $result = RuntimeQuery::filter($queries, $payload); + $result = $this->compileAndFilter($queries, $payload); $this->assertEquals($payload, $result); } @@ -44,7 +53,7 @@ class RuntimeQueryTest extends TestCase { $query = Query::equal('name', ['John']); $payload = ['name' => 'John']; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals($payload, $result); } @@ -52,7 +61,7 @@ class RuntimeQueryTest extends TestCase { $query = Query::equal('name', ['Jane']); $payload = ['name' => 'John']; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals([], $result); } @@ -60,7 +69,7 @@ class RuntimeQueryTest extends TestCase { $query = Query::equal('status', ['active', 'pending', 'approved']); $payload = ['status' => 'active']; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals($payload, $result); } @@ -68,7 +77,7 @@ class RuntimeQueryTest extends TestCase { $query = Query::equal('status', ['active', 'pending', 'approved']); $payload = ['status' => 'rejected']; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals([], $result); } @@ -76,7 +85,7 @@ class RuntimeQueryTest extends TestCase { $query = Query::equal('age', [30, 25, 35]); $payload = ['age' => 30]; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals($payload, $result); } @@ -84,7 +93,7 @@ class RuntimeQueryTest extends TestCase { $query = Query::equal('active', [true]); $payload = ['active' => true]; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals($payload, $result); } @@ -92,7 +101,7 @@ class RuntimeQueryTest extends TestCase { $query = Query::equal('missing', ['value']); $payload = ['name' => 'John']; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals([], $result); } @@ -101,7 +110,7 @@ class RuntimeQueryTest extends TestCase { $query = Query::notEqual('name', ['Jane']); $payload = ['name' => 'John']; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals($payload, $result); } @@ -109,7 +118,7 @@ class RuntimeQueryTest extends TestCase { $query = Query::notEqual('name', ['John']); $payload = ['name' => 'John']; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals([], $result); } @@ -119,12 +128,12 @@ class RuntimeQueryTest extends TestCase // and Query::parse will be done first and parse doesn't allow multiple notEqual values $query = Query::notEqual('status', ['rejected', 'cancelled']); $payload = ['status' => 'active']; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals($payload, $result); $query = Query::notEqual('status', ['active', 'pending']); $payload = ['status' => 'active']; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals([], $result); } @@ -133,7 +142,7 @@ class RuntimeQueryTest extends TestCase { $query = Query::lessThan('age', 30); $payload = ['age' => 25]; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals($payload, $result); } @@ -141,7 +150,7 @@ class RuntimeQueryTest extends TestCase { $query = Query::lessThan('age', 30); $payload = ['age' => 35]; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals([], $result); } @@ -149,7 +158,7 @@ class RuntimeQueryTest extends TestCase { $query = Query::lessThan('age', 30); $payload = ['age' => 30]; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals([], $result); } @@ -159,7 +168,7 @@ class RuntimeQueryTest extends TestCase // This test uses a single value as Query class requires $query = Query::lessThan('age', 30); $payload = ['age' => 25]; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals($payload, $result); } @@ -167,7 +176,7 @@ class RuntimeQueryTest extends TestCase { $query = Query::lessThan('name', 'M'); $payload = ['name' => 'A']; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals($payload, $result); } @@ -176,7 +185,7 @@ class RuntimeQueryTest extends TestCase { $query = Query::lessThanEqual('age', 30); $payload = ['age' => 25]; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals($payload, $result); } @@ -184,7 +193,7 @@ class RuntimeQueryTest extends TestCase { $query = Query::lessThanEqual('age', 30); $payload = ['age' => 30]; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals($payload, $result); } @@ -192,7 +201,7 @@ class RuntimeQueryTest extends TestCase { $query = Query::lessThanEqual('age', 30); $payload = ['age' => 35]; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals([], $result); } @@ -201,7 +210,7 @@ class RuntimeQueryTest extends TestCase // Note: Query::lessThanEqual only accepts single value $query = Query::lessThanEqual('age', 30); $payload = ['age' => 30]; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals($payload, $result); } @@ -210,7 +219,7 @@ class RuntimeQueryTest extends TestCase { $query = Query::greaterThan('age', 30); $payload = ['age' => 35]; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals($payload, $result); } @@ -218,7 +227,7 @@ class RuntimeQueryTest extends TestCase { $query = Query::greaterThan('age', 30); $payload = ['age' => 25]; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals([], $result); } @@ -226,7 +235,7 @@ class RuntimeQueryTest extends TestCase { $query = Query::greaterThan('age', 30); $payload = ['age' => 30]; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals([], $result); } @@ -235,7 +244,7 @@ class RuntimeQueryTest extends TestCase // Note: Query::greaterThan only accepts single value $query = Query::greaterThan('age', 20); $payload = ['age' => 35]; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals($payload, $result); } @@ -244,7 +253,7 @@ class RuntimeQueryTest extends TestCase { $query = Query::greaterThanEqual('age', 30); $payload = ['age' => 35]; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals($payload, $result); } @@ -252,7 +261,7 @@ class RuntimeQueryTest extends TestCase { $query = Query::greaterThanEqual('age', 30); $payload = ['age' => 30]; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals($payload, $result); } @@ -260,7 +269,7 @@ class RuntimeQueryTest extends TestCase { $query = Query::greaterThanEqual('age', 30); $payload = ['age' => 25]; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals([], $result); } @@ -269,7 +278,7 @@ class RuntimeQueryTest extends TestCase // Note: Query::greaterThanEqual only accepts single value $query = Query::greaterThanEqual('age', 20); $payload = ['age' => 30]; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals($payload, $result); } @@ -278,7 +287,7 @@ class RuntimeQueryTest extends TestCase { $query = Query::isNull('description'); $payload = ['description' => null]; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals($payload, $result); } @@ -286,7 +295,7 @@ class RuntimeQueryTest extends TestCase { $query = Query::isNull('description'); $payload = ['description' => 'Some text']; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals([], $result); } @@ -294,7 +303,7 @@ class RuntimeQueryTest extends TestCase { $query = Query::isNull('missing'); $payload = ['name' => 'John']; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals([], $result); } @@ -303,7 +312,7 @@ class RuntimeQueryTest extends TestCase { $query = Query::isNotNull('description'); $payload = ['description' => 'Some text']; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals($payload, $result); } @@ -311,7 +320,7 @@ class RuntimeQueryTest extends TestCase { $query = Query::isNotNull('description'); $payload = ['description' => null]; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals([], $result); } @@ -319,7 +328,7 @@ class RuntimeQueryTest extends TestCase { $query = Query::isNotNull('missing'); $payload = ['name' => 'John']; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals([], $result); } @@ -331,7 +340,7 @@ class RuntimeQueryTest extends TestCase Query::equal('age', [30]) ]); $payload = ['name' => 'John', 'age' => 30]; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals($payload, $result); } @@ -342,7 +351,7 @@ class RuntimeQueryTest extends TestCase Query::equal('age', [25]) ]); $payload = ['name' => 'John', 'age' => 30]; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals([], $result); } @@ -353,7 +362,7 @@ class RuntimeQueryTest extends TestCase Query::equal('age', [25]) ]); $payload = ['name' => 'John', 'age' => 30]; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals([], $result); } @@ -365,7 +374,7 @@ class RuntimeQueryTest extends TestCase Query::isNotNull('email') ]); $payload = ['status' => 'active', 'age' => 25, 'email' => 'test@example.com']; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals($payload, $result); } @@ -379,7 +388,7 @@ class RuntimeQueryTest extends TestCase ]) ]); $payload = ['name' => 'John', 'age' => 30, 'status' => 'active']; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals($payload, $result); } @@ -391,7 +400,7 @@ class RuntimeQueryTest extends TestCase Query::equal('name', ['Jane']) ]); $payload = ['name' => 'John']; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals($payload, $result); } @@ -402,7 +411,7 @@ class RuntimeQueryTest extends TestCase Query::equal('status', ['pending']) ]); $payload = ['status' => 'active']; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals($payload, $result); } @@ -413,7 +422,7 @@ class RuntimeQueryTest extends TestCase Query::equal('age', [25]) ]); $payload = ['name' => 'John', 'age' => 30]; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals([], $result); } @@ -425,7 +434,7 @@ class RuntimeQueryTest extends TestCase Query::equal('status', ['approved']) ]); $payload = ['status' => 'pending']; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals($payload, $result); } @@ -439,7 +448,7 @@ class RuntimeQueryTest extends TestCase ]) ]); $payload = ['name' => 'Bob']; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals($payload, $result); } @@ -450,7 +459,7 @@ class RuntimeQueryTest extends TestCase Query::equal('email', ['john@example.com']) ]); $payload = ['name' => 'Jane', 'email' => 'john@example.com']; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals($payload, $result); } @@ -465,7 +474,7 @@ class RuntimeQueryTest extends TestCase ]) ]); $payload = ['type' => 'user', 'status' => 'active']; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals($payload, $result); } @@ -482,7 +491,7 @@ class RuntimeQueryTest extends TestCase ]) ]); $payload = ['name' => 'John', 'age' => 30]; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals($payload, $result); } @@ -494,7 +503,7 @@ class RuntimeQueryTest extends TestCase Query::equal('age', [30]) ]; $payload = ['name' => 'John', 'age' => 30]; - $result = RuntimeQuery::filter($queries, $payload); + $result = $this->compileAndFilter($queries, $payload); $this->assertEquals($payload, $result); } @@ -505,7 +514,7 @@ class RuntimeQueryTest extends TestCase Query::equal('age', [25]) ]; $payload = ['name' => 'John', 'age' => 30]; - $result = RuntimeQuery::filter($queries, $payload); + $result = $this->compileAndFilter($queries, $payload); // With AND logic, if first matches but second doesn't, should return empty $this->assertEquals([], $result); } @@ -517,7 +526,7 @@ class RuntimeQueryTest extends TestCase Query::equal('age', [30]) ]; $payload = ['name' => 'John', 'age' => 30]; - $result = RuntimeQuery::filter($queries, $payload); + $result = $this->compileAndFilter($queries, $payload); // With AND logic, if second matches but first doesn't, should return empty $this->assertEquals([], $result); } @@ -529,7 +538,7 @@ class RuntimeQueryTest extends TestCase Query::equal('age', [25]) ]; $payload = ['name' => 'John', 'age' => 30]; - $result = RuntimeQuery::filter($queries, $payload); + $result = $this->compileAndFilter($queries, $payload); $this->assertEquals([], $result); } @@ -537,7 +546,7 @@ class RuntimeQueryTest extends TestCase { $query = Query::equal('name', ['John']); $payload = []; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals([], $result); } @@ -545,7 +554,7 @@ class RuntimeQueryTest extends TestCase { $query = Query::and([]); $payload = ['name' => 'John']; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); // Empty AND should return true (all conditions pass vacuously) $this->assertEquals($payload, $result); } @@ -554,7 +563,7 @@ class RuntimeQueryTest extends TestCase { $query = Query::or([]); $payload = ['name' => 'John']; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); // Empty OR should return false (no conditions match) $this->assertEquals([], $result); } @@ -564,7 +573,7 @@ class RuntimeQueryTest extends TestCase { $query = Query::equal('count', [0]); $payload = ['count' => 0]; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals($payload, $result); } @@ -572,7 +581,7 @@ class RuntimeQueryTest extends TestCase { $query = Query::equal('name', ['']); $payload = ['name' => '']; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals($payload, $result); } @@ -580,7 +589,7 @@ class RuntimeQueryTest extends TestCase { $query = Query::equal('active', [false]); $payload = ['active' => false]; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals($payload, $result); } @@ -588,7 +597,7 @@ class RuntimeQueryTest extends TestCase { $query = Query::greaterThan('score', 8.5); $payload = ['score' => 9.2]; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals($payload, $result); } @@ -596,7 +605,7 @@ class RuntimeQueryTest extends TestCase { $query = Query::lessThan('version', '10'); $payload = ['version' => '9']; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals($payload, $result); } @@ -667,7 +676,7 @@ class RuntimeQueryTest extends TestCase { $query = Query::select(['*']); $payload = ['name' => 'John', 'age' => 30]; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals($payload, $result); } @@ -679,7 +688,7 @@ class RuntimeQueryTest extends TestCase Query::equal('name', ['Jane']), // This would normally fail ]; $payload = ['name' => 'John', 'age' => 30]; - $result = RuntimeQuery::filter($queries, $payload); + $result = $this->compileAndFilter($queries, $payload); // select("*") takes precedence - returns payload $this->assertEquals($payload, $result); } @@ -688,7 +697,7 @@ class RuntimeQueryTest extends TestCase { $query = Query::select(['*']); $payload = []; - $result = RuntimeQuery::filter([$query], $payload); + $result = $this->compileAndFilter([$query], $payload); $this->assertEquals($payload, $result); } }