Enhance Realtime functionality with query support and improve tests

- Updated Realtime adapter to handle queries during subscription.
- Added query filtering capabilities in RuntimeQuery class.
- Modified RealtimeBase and RealtimeCustomClientTest to support query parameters in WebSocket connections.
- Improved test coverage for account and database channels with queries.
This commit is contained in:
ArnabChatterjee20k
2025-12-22 18:01:00 +05:30
parent 06e3a144a3
commit b7e2606b9f
5 changed files with 250 additions and 14 deletions
+42 -3
View File
@@ -2,12 +2,16 @@
namespace Appwrite\Messaging\Adapter;
use Appwrite\Extend\Exception;
use Appwrite\Messaging\Adapter as MessagingAdapter;
use Appwrite\PubSub\Adapter\Pool as PubSubPool;
use Appwrite\Utopia\Database\Query\RuntimeQuery;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
class Realtime extends MessagingAdapter
{
@@ -51,9 +55,10 @@ class Realtime extends MessagingAdapter
* @param mixed $identifier
* @param array $roles
* @param array $channels
* @param array $queries
* @return void
*/
public function subscribe(string $projectId, mixed $identifier, array $roles, array $channels): void
public function subscribe(string $projectId, mixed $identifier, array $roles, array $channels, array $queries = []): void
{
if (!isset($this->subscriptions[$projectId])) { // Init Project
$this->subscriptions[$projectId] = [];
@@ -72,7 +77,8 @@ class Realtime extends MessagingAdapter
$this->connections[$identifier] = [
'projectId' => $projectId,
'roles' => $roles,
'channels' => $channels
'channels' => $channels,
'queries' => $queries
];
}
@@ -206,7 +212,9 @@ class Realtime extends MessagingAdapter
/**
* To prevent duplicates, we save the connections as array keys.
*/
$receivers[$id] = 0;
if (!empty(RuntimeQuery::filter($this->connections[$id]['queries'], $event['data']))) {
$receivers[$id] = 0;
}
}
break;
}
@@ -217,6 +225,19 @@ class Realtime extends MessagingAdapter
return array_keys($receivers);
}
public function filterEventData(array $documents, array $queries): array
{
if (empty($queries)) {
return $documents;
}
$filteredDocuments = [];
foreach ($documents as $document) {
$doc = new Document((array) $doc);
}
return $filteredDocuments;
}
/**
* Converts the channels from the Query Params into an array.
* Also renames the account channel to account.USER_ID and removes all illegal account channel variations.
@@ -245,6 +266,24 @@ class Realtime extends MessagingAdapter
return $channels;
}
/**
* Converts the queries from the Query Params into an array.
* @param array $queries
* @return array
*/
public static function convertQueries(array $queries): array
{
$queries = Query::parseQueries($queries);
foreach ($queries as $query) {
if (!in_array($query->getMethod(), RuntimeQuery::ALLOWED_QUERIES)) {
// TODO: add better error message with which queries are allowed
throw new QueryException(Exception::REALTIME_POLICY_VIOLATION, 'Query not supported');
}
}
return $queries;
}
/**
* Create channels array based on the event name and payload.
*
@@ -0,0 +1,114 @@
<?php
namespace Appwrite\Utopia\Database\Query;
use Utopia\Database\Query;
class RuntimeQuery extends Query
{
public const ALLOWED_QUERIES = [
// Equality & comparison
Query::TYPE_EQUAL,
Query::TYPE_NOT_EQUAL,
Query::TYPE_LESSER,
Query::TYPE_LESSER_EQUAL,
Query::TYPE_GREATER,
Query::TYPE_GREATER_EQUAL,
// Null checks
Query::TYPE_IS_NULL,
Query::TYPE_IS_NOT_NULL,
// Recursive checks
Query::TYPE_AND,
Query::TYPE_OR
];
/**
* @param array<Query> $queries
* @param array<string, mixed> $payload
*/
public static function filter(array $queries, array $payload): array
{
if (empty($queries)) {
return $payload;
}
foreach ($queries as $query) {
if (self::evaluateFilter($query, $payload)) {
return $payload;
};
}
return [];
}
private static function evaluateFilter(Query $query, array $payload): bool
{
$attribute = $query->getAttribute();
$method = $query->getMethod();
$values = $query->getValues();
if (!\array_key_exists($attribute, $payload)) {
return false;
}
$payloadAttributeValue = $payload[$attribute];
switch ($method) {
case Query::TYPE_EQUAL:
return self::anyMatch($values, fn ($value) => $payloadAttributeValue === $value);
case Query::TYPE_NOT_EQUAL:
return self::anyMatch($values, fn ($value) => $payloadAttributeValue !== $value);
case Query::TYPE_LESSER:
return self::anyMatch($values, fn ($value) => $payloadAttributeValue < $value);
case Query::TYPE_LESSER_EQUAL:
return self::anyMatch($values, fn ($value) => $payloadAttributeValue <= $value);
case Query::TYPE_GREATER:
return self::anyMatch($values, fn ($value) => $payloadAttributeValue > $value);
case Query::TYPE_GREATER_EQUAL:
return self::anyMatch($values, fn ($value) => $payloadAttributeValue >= $value);
case Query::TYPE_IS_NULL:
return $payloadAttributeValue === null;
case Query::TYPE_IS_NOT_NULL:
return $payloadAttributeValue !== null;
case Query::TYPE_AND:
foreach ($query->getValues() as $subquery) {
// if any evaluation gets to false then whole and is false
if (!self::evaluateFilter($subquery, $payload)) {
return false;
}
return true;
}
// no break
case Query::TYPE_OR:
foreach ($query->getValues() as $subquery) {
// if any evaluation gets to true then whole or is true
if (self::evaluateFilter($subquery, $payload)) {
return true;
}
return false;
}
// no break
default:
throw new \InvalidArgumentException(
"Unsupported query method: {$method}"
);
}
}
private static function anyMatch(array $values, callable $fn): bool
{
foreach ($values as $value) {
if ($fn($value)) {
return true;
}
}
return false;
}
}