mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
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:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user