mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Improve code quality
This commit is contained in:
@@ -6,17 +6,13 @@ use Appwrite\Extend\Exception;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\Method;
|
||||
use Appwrite\SDK\Response as SDKResponse;
|
||||
use Appwrite\Utopia\Database\InMemoryQuery;
|
||||
use Appwrite\Utopia\Database\Validator\Queries\ProjectTemplates;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Exception\Query as QueryException;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Database\Validator\Queries;
|
||||
use Utopia\Database\Validator\Query\Filter;
|
||||
use Utopia\Database\Validator\Query\Limit;
|
||||
use Utopia\Database\Validator\Query\Offset;
|
||||
use Utopia\Database\Validator\Query\Order;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\Validator\Boolean;
|
||||
@@ -25,17 +21,6 @@ class XList extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
private const ALLOWED_ATTRIBUTES = [
|
||||
'type' => Database::VAR_STRING,
|
||||
'locale' => Database::VAR_STRING,
|
||||
'subject' => Database::VAR_STRING,
|
||||
'message' => Database::VAR_STRING,
|
||||
'senderName' => Database::VAR_STRING,
|
||||
'senderEmail' => Database::VAR_STRING,
|
||||
'replyTo' => Database::VAR_STRING,
|
||||
'custom' => Database::VAR_BOOLEAN,
|
||||
];
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'listProjectEmailTemplates';
|
||||
@@ -43,22 +28,6 @@ class XList extends Action
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$attributes = [];
|
||||
foreach (self::ALLOWED_ATTRIBUTES as $key => $type) {
|
||||
$attributes[] = new Document([
|
||||
'key' => $key,
|
||||
'type' => $type,
|
||||
'array' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
$queriesValidator = new Queries([
|
||||
new Limit(),
|
||||
new Offset(),
|
||||
new Filter($attributes, Database::VAR_STRING, APP_DATABASE_QUERY_MAX_VALUES),
|
||||
new Order($attributes),
|
||||
]);
|
||||
|
||||
$this
|
||||
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/project/templates/email')
|
||||
@@ -80,7 +49,7 @@ class XList extends Action
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('queries', [], $queriesValidator, 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter and order on the following attributes: ' . implode(', ', array_keys(self::ALLOWED_ATTRIBUTES)), true)
|
||||
->param('queries', [], new ProjectTemplates(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter and order on the following attributes: ' . implode(', ', array_keys(ProjectTemplates::ALLOWED_ATTRIBUTES)), true)
|
||||
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
|
||||
->inject('project')
|
||||
->inject('response')
|
||||
@@ -106,15 +75,6 @@ class XList extends Action
|
||||
}
|
||||
|
||||
$grouped = Query::groupByType($queries);
|
||||
$limit = $grouped['limit'] ?? APP_LIMIT_COUNT;
|
||||
$offset = $grouped['offset'] ?? 0;
|
||||
|
||||
/** @var array<Query> $filters */
|
||||
$filters = $grouped['filters'] ?? [];
|
||||
/** @var array<string> $orderAttributes */
|
||||
$orderAttributes = $grouped['orderAttributes'] ?? [];
|
||||
/** @var array<string> $orderTypes */
|
||||
$orderTypes = $grouped['orderTypes'] ?? [];
|
||||
|
||||
$types = Config::getParam('locale-templates')['email'] ?? [];
|
||||
$projectTemplates = $project->getAttribute('templates', []);
|
||||
@@ -138,91 +98,15 @@ class XList extends Action
|
||||
}
|
||||
}
|
||||
|
||||
$templates = $this->applyFilters($templates, $filters);
|
||||
$templates = $this->applyOrder($templates, $orderAttributes, $orderTypes);
|
||||
$templates = InMemoryQuery::filter($templates, $grouped['filters']);
|
||||
$templates = InMemoryQuery::order($templates, $grouped['orderAttributes'], $grouped['orderTypes']);
|
||||
|
||||
$total = $includeTotal ? \count($templates) : 0;
|
||||
$templates = \array_slice($templates, $offset, $limit);
|
||||
$templates = InMemoryQuery::paginate($templates, $grouped['limit'] ?? APP_LIMIT_COUNT, $grouped['offset']);
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'templates' => $templates,
|
||||
'total' => $total,
|
||||
]), Response::MODEL_EMAIL_TEMPLATE_LIST);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<Document> $templates
|
||||
* @param array<Query> $filters
|
||||
* @return array<Document>
|
||||
*/
|
||||
private function applyFilters(array $templates, array $filters): array
|
||||
{
|
||||
if (empty($filters)) {
|
||||
return $templates;
|
||||
}
|
||||
|
||||
return \array_values(\array_filter($templates, function (Document $template) use ($filters) {
|
||||
foreach ($filters as $filter) {
|
||||
if (!$this->matches($template, $filter)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}));
|
||||
}
|
||||
|
||||
private function matches(Document $template, Query $filter): bool
|
||||
{
|
||||
$attribute = $filter->getAttribute();
|
||||
$values = $filter->getValues();
|
||||
$actual = $template->getAttribute($attribute);
|
||||
$needle = (string) ($values[0] ?? '');
|
||||
|
||||
return match ($filter->getMethod()) {
|
||||
Query::TYPE_EQUAL => \in_array($actual, $values, false),
|
||||
Query::TYPE_NOT_EQUAL => !\in_array($actual, $values, false),
|
||||
Query::TYPE_STARTS_WITH => \is_string($actual) && \str_starts_with($actual, $needle),
|
||||
Query::TYPE_NOT_STARTS_WITH => \is_string($actual) && !\str_starts_with($actual, $needle),
|
||||
Query::TYPE_ENDS_WITH => \is_string($actual) && \str_ends_with($actual, $needle),
|
||||
Query::TYPE_NOT_ENDS_WITH => \is_string($actual) && !\str_ends_with($actual, $needle),
|
||||
Query::TYPE_CONTAINS => \is_string($actual) && \str_contains($actual, $needle),
|
||||
Query::TYPE_NOT_CONTAINS => \is_string($actual) && !\str_contains($actual, $needle),
|
||||
Query::TYPE_SEARCH => \is_string($actual) && \stripos($actual, $needle) !== false,
|
||||
Query::TYPE_NOT_SEARCH => \is_string($actual) && \stripos($actual, $needle) === false,
|
||||
Query::TYPE_IS_NULL => $actual === null || $actual === '',
|
||||
Query::TYPE_IS_NOT_NULL => $actual !== null && $actual !== '',
|
||||
default => throw new Exception(Exception::GENERAL_QUERY_INVALID, 'Query method not supported for email templates: ' . $filter->getMethod()),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<Document> $templates
|
||||
* @param array<string> $orderAttributes
|
||||
* @param array<string> $orderTypes
|
||||
* @return array<Document>
|
||||
*/
|
||||
private function applyOrder(array $templates, array $orderAttributes, array $orderTypes): array
|
||||
{
|
||||
if (empty($orderAttributes)) {
|
||||
return $templates;
|
||||
}
|
||||
|
||||
\usort($templates, function (Document $a, Document $b) use ($orderAttributes, $orderTypes) {
|
||||
foreach ($orderAttributes as $index => $attribute) {
|
||||
$direction = \strtoupper($orderTypes[$index] ?? Database::ORDER_ASC);
|
||||
$valueA = $a->getAttribute($attribute);
|
||||
$valueB = $b->getAttribute($attribute);
|
||||
|
||||
$cmp = $valueA <=> $valueB;
|
||||
if ($cmp === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return $direction === Database::ORDER_DESC ? -$cmp : $cmp;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
return $templates;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Database;
|
||||
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Query;
|
||||
|
||||
/**
|
||||
* Applies filter, order, and pagination queries to an in-memory array of Documents.
|
||||
*
|
||||
* Intended for list endpoints whose full dataset is materialized in memory (e.g. built
|
||||
* from config and project attributes rather than a database collection).
|
||||
*/
|
||||
class InMemoryQuery
|
||||
{
|
||||
/**
|
||||
* Filter documents using AND-combined query filters.
|
||||
*
|
||||
* @param array<Document> $documents
|
||||
* @param array<Query> $filters
|
||||
* @return array<Document>
|
||||
*/
|
||||
public static function filter(array $documents, array $filters): array
|
||||
{
|
||||
if (empty($filters)) {
|
||||
return \array_values($documents);
|
||||
}
|
||||
|
||||
return \array_values(\array_filter($documents, function (Document $document) use ($filters) {
|
||||
foreach ($filters as $filter) {
|
||||
if (!self::matches($document, $filter)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate a single filter query against a document.
|
||||
*/
|
||||
public static function matches(Document $document, Query $filter): bool
|
||||
{
|
||||
$attribute = $filter->getAttribute();
|
||||
$values = $filter->getValues();
|
||||
$actual = $document->getAttribute($attribute);
|
||||
$needle = (string) ($values[0] ?? '');
|
||||
|
||||
return match ($filter->getMethod()) {
|
||||
Query::TYPE_EQUAL => \in_array($actual, $values, false),
|
||||
Query::TYPE_NOT_EQUAL => !\in_array($actual, $values, false),
|
||||
Query::TYPE_LESSER => self::compareScalar($actual, $values[0] ?? null) < 0,
|
||||
Query::TYPE_LESSER_EQUAL => self::compareScalar($actual, $values[0] ?? null) <= 0,
|
||||
Query::TYPE_GREATER => self::compareScalar($actual, $values[0] ?? null) > 0,
|
||||
Query::TYPE_GREATER_EQUAL => self::compareScalar($actual, $values[0] ?? null) >= 0,
|
||||
Query::TYPE_BETWEEN => self::compareScalar($actual, $values[0] ?? null) >= 0 && self::compareScalar($actual, $values[1] ?? null) <= 0,
|
||||
Query::TYPE_NOT_BETWEEN => self::compareScalar($actual, $values[0] ?? null) < 0 || self::compareScalar($actual, $values[1] ?? null) > 0,
|
||||
Query::TYPE_STARTS_WITH => \is_string($actual) && \str_starts_with($actual, $needle),
|
||||
Query::TYPE_NOT_STARTS_WITH => \is_string($actual) && !\str_starts_with($actual, $needle),
|
||||
Query::TYPE_ENDS_WITH => \is_string($actual) && \str_ends_with($actual, $needle),
|
||||
Query::TYPE_NOT_ENDS_WITH => \is_string($actual) && !\str_ends_with($actual, $needle),
|
||||
Query::TYPE_CONTAINS => self::containsValue($actual, $values),
|
||||
Query::TYPE_NOT_CONTAINS => !self::containsValue($actual, $values),
|
||||
Query::TYPE_SEARCH => \is_string($actual) && $needle !== '' && \stripos($actual, $needle) !== false,
|
||||
Query::TYPE_NOT_SEARCH => \is_string($actual) && ($needle === '' || \stripos($actual, $needle) === false),
|
||||
Query::TYPE_IS_NULL => $actual === null,
|
||||
Query::TYPE_IS_NOT_NULL => $actual !== null,
|
||||
default => throw new \InvalidArgumentException('Unsupported query method: ' . $filter->getMethod()),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort documents by one or more attributes.
|
||||
*
|
||||
* @param array<Document> $documents
|
||||
* @param array<string> $orderAttributes
|
||||
* @param array<string> $orderTypes
|
||||
* @return array<Document>
|
||||
*/
|
||||
public static function order(array $documents, array $orderAttributes, array $orderTypes): array
|
||||
{
|
||||
if (empty($orderAttributes)) {
|
||||
return \array_values($documents);
|
||||
}
|
||||
|
||||
$documents = \array_values($documents);
|
||||
|
||||
\usort($documents, function (Document $a, Document $b) use ($orderAttributes, $orderTypes) {
|
||||
foreach ($orderAttributes as $index => $attribute) {
|
||||
$direction = \strtoupper($orderTypes[$index] ?? Database::ORDER_ASC);
|
||||
$cmp = self::compareScalar($a->getAttribute($attribute), $b->getAttribute($attribute));
|
||||
if ($cmp !== 0) {
|
||||
return $direction === Database::ORDER_DESC ? -$cmp : $cmp;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
return $documents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply limit and offset.
|
||||
*
|
||||
* @param array<Document> $documents
|
||||
* @return array<Document>
|
||||
*/
|
||||
public static function paginate(array $documents, ?int $limit, ?int $offset): array
|
||||
{
|
||||
return \array_slice(\array_values($documents), $offset ?? 0, $limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two scalars in a way that handles null consistently (null sorts before any value).
|
||||
*/
|
||||
private static function compareScalar(mixed $a, mixed $b): int
|
||||
{
|
||||
if ($a === null && $b === null) {
|
||||
return 0;
|
||||
}
|
||||
if ($a === null) {
|
||||
return -1;
|
||||
}
|
||||
if ($b === null) {
|
||||
return 1;
|
||||
}
|
||||
return $a <=> $b;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an attribute contains any of the given values. Handles both array attributes
|
||||
* (checks membership) and string attributes (checks substring).
|
||||
*
|
||||
* @param array<mixed> $values
|
||||
*/
|
||||
private static function containsValue(mixed $actual, array $values): bool
|
||||
{
|
||||
if (\is_array($actual)) {
|
||||
foreach ($values as $value) {
|
||||
if (\in_array($value, $actual, false)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (\is_string($actual)) {
|
||||
foreach ($values as $value) {
|
||||
if (\str_contains($actual, (string) $value)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Database\Validator\Queries;
|
||||
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Queries;
|
||||
use Utopia\Database\Validator\Query\Filter;
|
||||
use Utopia\Database\Validator\Query\Limit;
|
||||
use Utopia\Database\Validator\Query\Offset;
|
||||
use Utopia\Database\Validator\Query\Order;
|
||||
|
||||
/**
|
||||
* Queries validator for list endpoints whose dataset is materialized in memory rather
|
||||
* than backed by a database collection. Pairs with {@see \Appwrite\Utopia\Database\InMemoryQuery}
|
||||
* to apply the validated queries.
|
||||
*/
|
||||
class BaseInMemory extends Queries
|
||||
{
|
||||
/**
|
||||
* @param array<string, string> $allowedAttributes Map of attribute key to Database::VAR_* type
|
||||
*/
|
||||
public function __construct(array $allowedAttributes)
|
||||
{
|
||||
$attributes = [];
|
||||
foreach ($allowedAttributes as $key => $type) {
|
||||
$attributes[] = new Document([
|
||||
'key' => $key,
|
||||
'type' => $type,
|
||||
'array' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
parent::__construct([
|
||||
new Limit(),
|
||||
new Offset(),
|
||||
new Filter($attributes, Database::VAR_STRING, APP_DATABASE_QUERY_MAX_VALUES),
|
||||
new Order($attributes),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Utopia\Database\Validator\Queries;
|
||||
|
||||
use Utopia\Database\Database;
|
||||
|
||||
class ProjectTemplates extends BaseInMemory
|
||||
{
|
||||
public const ALLOWED_ATTRIBUTES = [
|
||||
'type' => Database::VAR_STRING,
|
||||
'locale' => Database::VAR_STRING,
|
||||
'subject' => Database::VAR_STRING,
|
||||
'message' => Database::VAR_STRING,
|
||||
'senderName' => Database::VAR_STRING,
|
||||
'senderEmail' => Database::VAR_STRING,
|
||||
'replyTo' => Database::VAR_STRING,
|
||||
'custom' => Database::VAR_BOOLEAN,
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(self::ALLOWED_ATTRIBUTES);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user