Merge branch '1.8.x' of https://github.com/appwrite/appwrite into users-add-attributes

# Conflicts:
#	composer.lock
This commit is contained in:
fogelito
2025-11-10 14:52:25 +02:00
817 changed files with 26974 additions and 4408 deletions
+3 -1
View File
@@ -53,7 +53,9 @@ class Google extends OAuth2
'redirect_uri' => $this->callback,
'scope' => \implode(' ', $this->getScopes()),
'state' => \json_encode($this->state),
'response_type' => 'code'
'response_type' => 'code',
'access_type' => 'offline',
'prompt' => 'consent'
]);
}
+8 -1
View File
@@ -331,9 +331,16 @@ class Mapper
break;
case 'Utopia\Validator\Integer':
case 'Utopia\Validator\Numeric':
case 'Utopia\Validator\Range':
$type = Type::int();
break;
case 'Utopia\Validator\Range':
// Check if the Range validator is for float or integer
if ($validator instanceof \Utopia\Validator\Range && $validator->getType() === \Utopia\Validator\Range::TYPE_FLOAT) {
$type = Type::float();
} else {
$type = Type::int();
}
break;
case 'Utopia\Validator\FloatValidator':
$type = Type::float();
break;
+63 -95
View File
@@ -3,118 +3,65 @@
namespace Appwrite\Network\Validator;
use Utopia\DNS\Client;
use Utopia\DNS\Message;
use Utopia\DNS\Message\Question;
use Utopia\DNS\Message\Record;
use Utopia\Domains\Domain;
use Utopia\System\System;
use Utopia\Validator;
class DNS extends Validator
{
public const RECORD_A = 'A';
public const RECORD_AAAA = 'AAAA';
public const RECORD_CNAME = 'CNAME';
public const RECORD_CAA = 'CAA'; // You can provide domain only (as $target) for CAA validation
/**
* @var mixed
*/
protected mixed $logs;
/**
* @var string
*/
protected string $dnsServer;
/**
* @param string $target
*/
public function __construct(protected string $target, protected string $type = self::RECORD_CNAME, string $dnsServer = '')
{
if (empty($dnsServer)) {
$dnsServer = System::getEnv('_APP_DNS', '8.8.8.8');
}
$this->dnsServer = $dnsServer;
public function __construct(
protected string $target,
protected int $type = Record::TYPE_CNAME,
protected string $server = ''
) {
$this->server = $server ?: System::getEnv('_APP_DNS', '8.8.8.8');
}
/**
* @return string
*/
public function getDescription(): string
{
return 'Invalid DNS record';
return 'Invalid DNS record.';
}
/**
* @return mixed
*/
public function getLogs(): mixed
{
return $this->logs;
}
/**
* Check if DNS record value matches specific value
*
* @param mixed $domain
* @return bool
*/
public function isValid($value): bool
{
if (!is_string($value)) {
if (!is_string($value) || trim($value) === '') {
return false;
}
$dns = new Client($this->dnsServer);
$client = new Client($this->server);
try {
$rawQuery = $dns->query($value, $this->type);
// Some DNS servers return all records, not only type that's asked for
// Likely occurs when no records of specific type are found
$query = array_filter($rawQuery, function ($record) {
return $record->getTypeName() === $this->type;
});
$this->logs = $query;
} catch (\Exception $e) {
$this->logs = ['error' => $e->getMessage()];
$response = $client->query(Message::query(
new Question($value, $this->type)
));
} catch (\Throwable) {
return false;
}
if (empty($query)) {
// CAA records inherit from parent (custom CAA behaviour)
if ($this->type === self::RECORD_CAA) {
$domain = new Domain($value);
if ($domain->get() === $domain->getApex()) {
return true; // No CAA on apex domain means anyone can issue certificate
}
$typeMatches = array_filter(
$response->answers,
fn (Record $record) => $record->type === $this->type
);
// Recursive validation by parent domain
$parts = \explode('.', $value);
\array_shift($parts);
$parentDomain = \implode('.', $parts);
$validator = new DNS($this->target, DNS::RECORD_CAA, $this->dnsServer);
return $validator->isValid($parentDomain);
if (empty($typeMatches)) {
if ($this->type === Record::TYPE_CAA) {
return $this->validateParentCAA($value);
}
return false;
}
foreach ($query as $record) {
// CAA validation only needs to ensure domain
if ($this->type === self::RECORD_CAA) {
// Extract domain; comments showcase extraction steps in most complex scenario
$rdata = $record->getRdata(); // 255 issuewild "certainly.com;validationmethods=tls-alpn-01;retrytimeout=3600"
$rdata = \explode(' ', $rdata, 3)[2] ?? ''; // "certainly.com;validationmethods=tls-alpn-01;retrytimeout=3600"
$rdata = \trim($rdata, '"'); // certainly.com;validationmethods=tls-alpn-01;retrytimeout=3600
$rdata = \explode(';', $rdata, 2)[0] ?? ''; // certainly.com
if ($rdata === $this->target) {
foreach ($typeMatches as $record) {
if ($this->type === Record::TYPE_CAA) {
$valuePart = $this->extractCAAValue($record->rdata);
if ($valuePart !== '' && $valuePart === $this->target) {
return true;
}
}
if ($record->getRdata() === $this->target) {
if ($record->rdata === $this->target) {
return true;
}
}
@@ -122,25 +69,46 @@ class DNS extends Validator
return false;
}
/**
* Is array
*
* Function will return true if object is array.
*
* @return bool
*/
private function validateParentCAA(string $domain): bool
{
try {
$domainInfo = new Domain($domain);
} catch (\Throwable) {
return false;
}
if ($domainInfo->get() === $domainInfo->getApex()) {
return true;
}
$parts = explode('.', $domainInfo->get());
array_shift($parts);
$parent = implode('.', $parts);
if ($parent === '') {
return false;
}
$validator = new self($this->target, Record::TYPE_CAA, $this->server);
return $validator->isValid($parent);
}
private function extractCAAValue(string $rdata): string
{
$parts = explode(' ', $rdata, 3);
if (count($parts) < 3) {
return '';
}
$value = trim($parts[2], '"');
return explode(';', $value)[0] ?? '';
}
public function isArray(): bool
{
return false;
}
/**
* Get Type
*
* Returns validator type.
*
* @return string
*/
public function getType(): string
{
return self::TYPE_STRING;
@@ -18,6 +18,7 @@ use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
class XList extends Action
{
@@ -60,12 +61,13 @@ class XList extends Action
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID.')
->param('queries', [], new Attributes(), '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 on the following attributes: ' . implode(', ', Attributes::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('response')
->inject('dbForProject')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, array $queries, UtopiaResponse $response, Database $dbForProject): void
public function action(string $databaseId, string $collectionId, array $queries, bool $includeTotal, UtopiaResponse $response, Database $dbForProject): void
{
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
@@ -122,7 +124,7 @@ class XList extends Action
try {
$attributes = $dbForProject->find('attributes', $queries);
$total = $dbForProject->count('attributes', $queries, APP_LIMIT_COUNT);
$total = $includeTotal ? $dbForProject->count('attributes', $queries, APP_LIMIT_COUNT) : 0;
} catch (OrderException $e) {
$documents = $this->isCollectionsAPI() ? 'documents' : 'rows';
$attribute = $this->isCollectionsAPI() ? 'attribute' : 'column';
@@ -7,6 +7,7 @@ use Appwrite\Extend\Exception;
use Appwrite\Platform\Action as AppwriteAction;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Operator;
use Utopia\Database\Validator\Authorization;
abstract class Action extends AppwriteAction
@@ -338,6 +339,53 @@ abstract class Action extends AppwriteAction
return true;
}
/**
* Parse operator strings in data array and convert them to Operator objects.
*
* @param array $data The data array that may contain operator JSON strings
* @param Document $collection The collection document to check for relationship attributes
* @return array The data array with operators converted to Operator objects
* @throws Exception If an operator string is invalid
*/
protected function parseOperators(array $data, Document $collection): array
{
$relationshipKeys = [];
foreach ($collection->getAttribute('attributes', []) as $attribute) {
if ($attribute->getAttribute('type') === Database::VAR_RELATIONSHIP) {
$relationshipKeys[$attribute->getAttribute('key')] = true;
}
}
foreach ($data as $key => $value) {
if (\str_starts_with($key, '$')) {
continue;
}
if (isset($relationshipKeys[$key])) {
continue;
}
if (\is_string($value)) {
$decoded = \json_decode($value, true);
if (
\is_array($decoded) &&
isset($decoded['method']) &&
\is_string($decoded['method']) &&
Operator::isMethod($decoded['method'])
) {
try {
$data[$key] = Operator::parse($value);
} catch (\Exception $e) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid operator for attribute "' . $key . '": ' . $e->getMessage());
}
}
}
}
return $data;
}
/**
* For triggering different queues for each document for a bulk documents
* @param string $event
@@ -107,6 +107,8 @@ class Update extends Action
throw new Exception($this->getParentNotFoundException());
}
$data = $this->parseOperators($data, $collection);
$hasRelationships = \array_filter(
$collection->getAttribute('attributes', []),
fn ($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP
@@ -107,6 +107,7 @@ class Upsert extends Action
}
foreach ($documents as $key => $document) {
$document = $this->parseOperators($document, $collection);
$document = $this->removeReadonlyAttributes($document, privileged: true);
$documents[$key] = new Document($document);
}
@@ -112,6 +112,8 @@ class Update extends Action
throw new Exception($this->getParentNotFoundException());
}
$data = $this->parseOperators($data, $collection);
// Read permission should not be required for update
/** @var Document $document */
$collectionTableId = 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence();
@@ -118,6 +118,8 @@ class Upsert extends Action
throw new Exception($this->getParentNotFoundException());
}
$data = $this->parseOperators($data, $collection);
$allowedPermissions = [
Database::PERMISSION_READ,
Database::PERMISSION_UPDATE,
@@ -22,6 +22,7 @@ use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
class XList extends Action
@@ -67,6 +68,7 @@ class XList extends Action
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('queries', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), '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.', true)
->param('transactionId', null, new UID(), 'Transaction ID to read uncommitted changes within the transaction.', 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('response')
->inject('dbForProject')
->inject('queueForStatsUsage')
@@ -74,7 +76,7 @@ class XList extends Action
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, array $queries, ?string $transactionId, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, TransactionState $transactionState): void
public function action(string $databaseId, string $collectionId, array $queries, ?string $transactionId, bool $includeTotal, UtopiaResponse $response, Database $dbForProject, StatsUsage $queueForStatsUsage, TransactionState $transactionState): void
{
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
@@ -129,16 +131,16 @@ class XList extends Action
// Use transaction-aware document retrieval if transactionId is provided
if ($transactionId !== null) {
$documents = $transactionState->listDocuments($collectionTableId, $transactionId, $queries);
$total = $transactionState->countDocuments($collectionTableId, $transactionId, $queries);
$total = $includeTotal ? $transactionState->countDocuments($collectionTableId, $transactionId, $queries) : 0;
} elseif (! empty($selectQueries)) {
// has selects, allow relationship on documents
$documents = $dbForProject->find($collectionTableId, $queries);
$total = $dbForProject->count($collectionTableId, $queries, APP_LIMIT_COUNT);
$total = $includeTotal ? $dbForProject->count($collectionTableId, $queries, APP_LIMIT_COUNT) : 0;
} else {
// has no selects, disable relationship loading on documents
/* @type Document[] $documents */
$documents = $dbForProject->skipRelationships(fn () => $dbForProject->find($collectionTableId, $queries));
$total = $dbForProject->count($collectionTableId, $queries, APP_LIMIT_COUNT);
$total = $includeTotal ? $dbForProject->count($collectionTableId, $queries, APP_LIMIT_COUNT) : 0;
}
} catch (OrderException $e) {
$documents = $this->isCollectionsAPI() ? 'documents' : 'rows';
@@ -19,6 +19,7 @@ use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
class XList extends Action
{
@@ -62,12 +63,13 @@ class XList extends Action
->param('databaseId', '', new UID(), 'Database ID.')
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('queries', [], new Indexes(), '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 on the following attributes: ' . implode(', ', Indexes::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('response')
->inject('dbForProject')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, array $queries, UtopiaResponse $response, Database $dbForProject): void
public function action(string $databaseId, string $collectionId, array $queries, bool $includeTotal, UtopiaResponse $response, Database $dbForProject): void
{
/** @var Document $database */
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
@@ -125,7 +127,7 @@ class XList extends Action
}
try {
$total = $dbForProject->count('indexes', $queries, APP_LIMIT_COUNT);
$total = $includeTotal ? $dbForProject->count('indexes', $queries, APP_LIMIT_COUNT) : 0;
$indexes = $dbForProject->find('indexes', $queries);
} catch (OrderException $e) {
$documents = $this->isCollectionsAPI() ? 'documents' : 'rows';
@@ -19,6 +19,7 @@ use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
class XList extends Action
@@ -63,12 +64,13 @@ class XList extends Action
->param('databaseId', '', new UID(), 'Database ID.')
->param('queries', [], new Collections(), '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 on the following attributes: ' . implode(', ', Collections::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', 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('response')
->inject('dbForProject')
->callback($this->action(...));
}
public function action(string $databaseId, array $queries, string $search, UtopiaResponse $response, Database $dbForProject): void
public function action(string $databaseId, array $queries, string $search, bool $includeTotal, UtopiaResponse $response, Database $dbForProject): void
{
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
@@ -112,7 +114,7 @@ class XList extends Action
try {
$collections = $dbForProject->find('database_' . $database->getSequence(), $queries);
$total = $dbForProject->count('database_' . $database->getSequence(), $queries, APP_LIMIT_COUNT);
$total = $includeTotal ? $dbForProject->count('database_' . $database->getSequence(), $queries, APP_LIMIT_COUNT) : 0;
} catch (OrderException) {
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL);
} catch (QueryException) {
@@ -18,6 +18,7 @@ use Utopia\Database\Query;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Platform\Action;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
class XList extends Action
@@ -58,12 +59,13 @@ class XList extends Action
])
->param('queries', [], new Databases(), '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 on the following attributes: ' . implode(', ', Databases::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', 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('response')
->inject('dbForProject')
->callback($this->action(...));
}
public function action(array $queries, string $search, UtopiaResponse $response, Database $dbForProject): void
public function action(array $queries, string $search, bool $includeTotal, UtopiaResponse $response, Database $dbForProject): void
{
$queries = Query::parseQueries($queries);
@@ -98,7 +100,7 @@ class XList extends Action
try {
$databases = $dbForProject->find('databases', $queries);
$total = $dbForProject->count('databases', $queries, APP_LIMIT_COUNT);
$total = $includeTotal ? $dbForProject->count('databases', $queries, APP_LIMIT_COUNT) : 0;
} catch (OrderException $e) {
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order column '{$e->getAttribute()}' had a null value. Cursor pagination requires all rows order column values are non-null.");
} catch (QueryException) {
@@ -10,6 +10,7 @@ use Appwrite\Utopia\Database\Validator\Queries\Columns;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
class XList extends AttributesXList
{
@@ -48,6 +49,7 @@ class XList extends AttributesXList
->param('databaseId', '', new UID(), 'Database ID.')
->param('tableId', '', new UID(), 'Table ID.')
->param('queries', [], new Columns(), '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 on the following columns: ' . implode(', ', Columns::ALLOWED_COLUMNS), 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('response')
->inject('dbForProject')
->callback($this->action(...));
@@ -11,6 +11,7 @@ use Appwrite\Utopia\Database\Validator\Queries\Indexes;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
class XList extends IndexXList
{
@@ -50,6 +51,7 @@ class XList extends IndexXList
->param('databaseId', '', new UID(), 'Database ID.')
->param('tableId', '', new UID(), 'Table ID. You can create a new table using the Database service [server integration](https://appwrite.io/docs/references/cloud/server-dart/tablesDB#createTable).')
->param('queries', [], new Indexes(), '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 on the following columns: ' . implode(', ', Indexes::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('response')
->inject('dbForProject')
->callback($this->action(...));
@@ -11,6 +11,7 @@ use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
class XList extends DocumentXList
@@ -52,6 +53,7 @@ class XList extends DocumentXList
->param('tableId', '', new UID(), 'Table ID. You can create a new table using the TablesDB service [server integration](https://appwrite.io/docs/products/databases/tables#create-table).')
->param('queries', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), '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.', true)
->param('transactionId', null, new UID(), 'Transaction ID to read uncommitted changes within the transaction.', 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('response')
->inject('dbForProject')
->inject('queueForStatsUsage')
@@ -11,6 +11,7 @@ use Appwrite\Utopia\Database\Validator\Queries\Tables;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
class XList extends CollectionXList
@@ -51,6 +52,7 @@ class XList extends CollectionXList
->param('databaseId', '', new UID(), 'Database ID.')
->param('queries', [], new Tables(), '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 on the following columns: ' . implode(', ', Tables::ALLOWED_COLUMNS), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', 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('response')
->inject('dbForProject')
->callback($this->action(...));
@@ -10,6 +10,7 @@ use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\Queries\Databases;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
class XList extends DatabaseXList
@@ -44,6 +45,7 @@ class XList extends DatabaseXList
))
->param('queries', [], new Databases(), '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 on the following columns: ' . implode(', ', Databases::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', 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('response')
->inject('dbForProject')
->callback($this->action(...));
@@ -19,6 +19,7 @@ use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
class XList extends Base
@@ -57,6 +58,7 @@ class XList extends Base
->param('functionId', '', new UID(), 'Function ID.')
->param('queries', [], new Deployments(), '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 on the following attributes: ' . implode(', ', Deployments::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', 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('request')
->inject('response')
->inject('dbForProject')
@@ -67,6 +69,7 @@ class XList extends Base
string $functionId,
array $queries,
string $search,
bool $includeTotal,
Request $request,
Response $response,
Database $dbForProject
@@ -120,7 +123,7 @@ class XList extends Base
try {
$results = $dbForProject->find('deployments', $queries);
$total = $dbForProject->count('deployments', $filterQueries, APP_LIMIT_COUNT);
$total = $includeTotal ? $dbForProject->count('deployments', $filterQueries, APP_LIMIT_COUNT) : 0;
} catch (OrderException $e) {
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
}
@@ -20,6 +20,7 @@ use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
class XList extends Base
{
@@ -56,6 +57,7 @@ class XList extends Base
))
->param('functionId', '', new UID(), 'Function ID.')
->param('queries', [], new Executions(), '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 on the following attributes: ' . implode(', ', Executions::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('response')
->inject('dbForProject')
->callback($this->action(...));
@@ -64,6 +66,7 @@ class XList extends Base
public function action(
string $functionId,
array $queries,
bool $includeTotal,
Response $response,
Database $dbForProject
) {
@@ -115,7 +118,7 @@ class XList extends Base
try {
$results = $dbForProject->find('executions', $queries);
$total = $dbForProject->count('executions', $filterQueries, APP_LIMIT_COUNT);
$total = $includeTotal ? $dbForProject->count('executions', $filterQueries, APP_LIMIT_COUNT) : 0;
} catch (OrderException $e) {
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
}
@@ -17,6 +17,7 @@ use Utopia\Database\Query;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
class XList extends Base
@@ -54,6 +55,7 @@ class XList extends Base
))
->param('queries', [], new Functions(), '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 on the following attributes: ' . implode(', ', Functions::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', 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('response')
->inject('dbForProject')
->callback($this->action(...));
@@ -62,6 +64,7 @@ class XList extends Base
public function action(
array $queries,
string $search,
bool $includeTotal,
Response $response,
Database $dbForProject
) {
@@ -104,7 +107,7 @@ class XList extends Base
try {
$functions = $dbForProject->find('functions', $queries);
$total = $dbForProject->count('functions', $filterQueries, APP_LIMIT_COUNT);
$total = $includeTotal ? $dbForProject->count('functions', $filterQueries, APP_LIMIT_COUNT) : 0;
} catch (OrderException $e) {
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
}
@@ -12,6 +12,7 @@ use Utopia\Database\Document;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
use Utopia\Validator\Range;
use Utopia\Validator\WhiteList;
@@ -52,11 +53,12 @@ class XList extends Base
->param('useCases', [], new ArrayList(new WhiteList(['dev-tools','starter','databases','ai','messaging','utilities']), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of use cases allowed for filtering function templates. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' use cases are allowed.', true)
->param('limit', 25, new Range(1, 5000), 'Limit the number of templates returned in the response. Default limit is 25, and maximum limit is 5000.', true)
->param('offset', 0, new Range(0, 5000), 'Offset the list of returned templates. Maximum offset is 5000.', 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('response')
->callback($this->action(...));
}
public function action(array $runtimes, array $usecases, int $limit, int $offset, Response $response)
public function action(array $runtimes, array $usecases, int $limit, int $offset, bool $includeTotal, Response $response)
{
$templates = Config::getParam('templates-function', []);
@@ -76,7 +78,7 @@ class XList extends Base
return $b['score'] <=> $a['score'];
});
$total = \count($templates);
$total = $includeTotal ? \count($templates) : 0;
$templates = \array_slice($templates, $offset, $limit);
$response->dynamic(new Document([
'templates' => $templates,
@@ -589,7 +589,10 @@ class Builds extends Action
// Some runtimes/frameworks can't compile with less memory than this
$minMemory = $resource->getCollection() === 'sites' ? 2048 : 1024;
if ($resource->getAttribute('framework', '') === 'analog') {
if (
$resource->getAttribute('framework', '') === 'analog' ||
$resource->getAttribute('framework', '') === 'tanstack-start'
) {
$minMemory = 4096;
}
@@ -988,7 +991,7 @@ class Builds extends Action
$config['sleep'] = $framework['screenshotSleep'];
}
$browserEndpoint = Config::getParam('_APP_BROWSER_HOST', 'http://appwrite-browser:3000/v1');
$browserEndpoint = System::getEnv('_APP_BROWSER_HOST', 'http://appwrite-browser:3000/v1');
$fetchResponse = $client->fetch(
url: $browserEndpoint . '/screenshots',
method: 'POST',
@@ -18,6 +18,7 @@ use Utopia\Database\Validator\Query\Cursor;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
class XList extends Action
@@ -59,12 +60,13 @@ class XList extends Action
))
->param('queries', [], $this->getQueriesValidator(), '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 on the following attributes: ' . implode(', ', Projects::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', 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('response')
->inject('dbForPlatform')
->callback($this->action(...));
}
public function action(array $queries, string $search, Response $response, Database $dbForPlatform)
public function action(array $queries, string $search, bool $includeTotal, Response $response, Database $dbForPlatform)
{
try {
$queries = Query::parseQueries($queries);
@@ -104,7 +106,7 @@ class XList extends Action
$filterQueries = Query::groupByType($queries)['filters'];
try {
$projects = $dbForPlatform->find('projects', $queries);
$total = $dbForPlatform->count('projects', $filterQueries, APP_LIMIT_COUNT);
$total = $includeTotal ? $dbForPlatform->count('projects', $filterQueries, APP_LIMIT_COUNT) : 0;
} catch (Order $e) {
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
}
@@ -14,6 +14,7 @@ use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Helpers\ID;
use Utopia\DNS\Message\Record;
use Utopia\Domains\Domain;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
@@ -135,13 +136,13 @@ class Create extends Action
$validators = [];
$targetCNAME = new Domain(System::getEnv('_APP_DOMAIN_TARGET_CNAME', ''));
if ($targetCNAME->isKnown() && !$targetCNAME->isTest()) {
$validators[] = new DNS($targetCNAME->get(), DNS::RECORD_CNAME);
$validators[] = new DNS($targetCNAME->get(), Record::TYPE_CNAME);
}
if ((new IP(IP::V4))->isValid(System::getEnv('_APP_DOMAIN_TARGET_A', ''))) {
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_A', ''), DNS::RECORD_A);
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_A', ''), Record::TYPE_A);
}
if ((new IP(IP::V6))->isValid(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''))) {
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), DNS::RECORD_AAAA);
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), Record::TYPE_AAAA);
}
if (empty($validators)) {
@@ -15,6 +15,7 @@ use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\UID;
use Utopia\DNS\Message\Record;
use Utopia\Domains\Domain;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
@@ -147,13 +148,13 @@ class Create extends Action
$validators = [];
$targetCNAME = new Domain(System::getEnv('_APP_DOMAIN_TARGET_CNAME', ''));
if ($targetCNAME->isKnown() && !$targetCNAME->isTest()) {
$validators[] = new DNS($targetCNAME->get(), DNS::RECORD_CNAME);
$validators[] = new DNS($targetCNAME->get(), Record::TYPE_CNAME);
}
if ((new IP(IP::V4))->isValid(System::getEnv('_APP_DOMAIN_TARGET_A', ''))) {
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_A', ''), DNS::RECORD_A);
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_A', ''), Record::TYPE_A);
}
if ((new IP(IP::V6))->isValid(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''))) {
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), DNS::RECORD_AAAA);
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), Record::TYPE_AAAA);
}
if (empty($validators)) {
@@ -15,6 +15,7 @@ use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\UID;
use Utopia\DNS\Message\Record;
use Utopia\Domains\Domain;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
@@ -152,13 +153,13 @@ class Create extends Action
$validators = [];
$targetCNAME = new Domain(System::getEnv('_APP_DOMAIN_TARGET_CNAME', ''));
if ($targetCNAME->isKnown() && !$targetCNAME->isTest()) {
$validators[] = new DNS($targetCNAME->get(), DNS::RECORD_CNAME);
$validators[] = new DNS($targetCNAME->get(), Record::TYPE_CNAME);
}
if ((new IP(IP::V4))->isValid(System::getEnv('_APP_DOMAIN_TARGET_A', ''))) {
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_A', ''), DNS::RECORD_A);
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_A', ''), Record::TYPE_A);
}
if ((new IP(IP::V6))->isValid(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''))) {
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), DNS::RECORD_AAAA);
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), Record::TYPE_AAAA);
}
if (empty($validators)) {
@@ -15,6 +15,7 @@ use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\UID;
use Utopia\DNS\Message\Record;
use Utopia\Domains\Domain;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
@@ -147,13 +148,13 @@ class Create extends Action
$validators = [];
$targetCNAME = new Domain(System::getEnv('_APP_DOMAIN_TARGET_CNAME', ''));
if ($targetCNAME->isKnown() && !$targetCNAME->isTest()) {
$validators[] = new DNS($targetCNAME->get(), DNS::RECORD_CNAME);
$validators[] = new DNS($targetCNAME->get(), Record::TYPE_CNAME);
}
if ((new IP(IP::V4))->isValid(System::getEnv('_APP_DOMAIN_TARGET_A', ''))) {
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_A', ''), DNS::RECORD_A);
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_A', ''), Record::TYPE_A);
}
if ((new IP(IP::V6))->isValid(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''))) {
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), DNS::RECORD_AAAA);
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), Record::TYPE_AAAA);
}
if (empty($validators)) {
@@ -13,6 +13,7 @@ use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\UID;
use Utopia\DNS\Message\Record;
use Utopia\Domains\Domain;
use Utopia\Logger\Log;
use Utopia\Platform\Action;
@@ -113,15 +114,15 @@ class Update extends Action
if (!is_null($targetCNAME)) {
if ($targetCNAME->isKnown() && !$targetCNAME->isTest()) {
$validators[] = new DNS($targetCNAME->get(), DNS::RECORD_CNAME);
$validators[] = new DNS($targetCNAME->get(), Record::TYPE_CNAME);
}
}
if ((new IP(IP::V4))->isValid(System::getEnv('_APP_DOMAIN_TARGET_A', ''))) {
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_A', ''), DNS::RECORD_A);
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_A', ''), Record::TYPE_A);
}
if ((new IP(IP::V6))->isValid(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''))) {
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), DNS::RECORD_AAAA);
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), Record::TYPE_AAAA);
}
if (empty($validators)) {
@@ -139,24 +140,13 @@ class Update extends Action
if (!$validator->isValid($domain->get())) {
$log->addExtra('dnsTiming', \strval(\microtime(true) - $validationStart));
$log->addTag('dnsDomain', $domain->get());
$errors = [];
foreach ($validators as $validator) {
if (!empty($validator->getLogs())) {
$errors[] = $validator->getLogs();
}
}
$error = \implode("\n", $errors);
$log->addExtra('dnsResponse', \is_array($error) ? \json_encode($error) : \strval($error));
throw new Exception(Exception::RULE_VERIFICATION_FAILED);
}
// Ensure CAA won't block certificate issuance
if (!empty(System::getEnv('_APP_DOMAIN_TARGET_CAA', ''))) {
$validationStart = \microtime(true);
$validator = new DNS(System::getEnv('_APP_DOMAIN_TARGET_CAA', ''), DNS::RECORD_CAA);
$validator = new DNS(System::getEnv('_APP_DOMAIN_TARGET_CAA', ''), Record::TYPE_CAA);
if (!$validator->isValid($domain->get())) {
$log->addExtra('dnsTimingCaa', \strval(\microtime(true) - $validationStart));
$log->addTag('dnsDomain', $domain->get());
@@ -15,6 +15,7 @@ use Utopia\Database\Query;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
class XList extends Action
@@ -51,6 +52,7 @@ class XList extends Action
))
->param('queries', [], new Rules(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Rules::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', 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('response')
->inject('project')
->inject('dbForPlatform')
@@ -60,6 +62,7 @@ class XList extends Action
public function action(
array $queries,
string $search,
bool $includeTotal,
Response $response,
Document $project,
Database $dbForPlatform
@@ -112,7 +115,7 @@ class XList extends Action
$response->dynamic(new Document([
'rules' => $rules,
'total' => $dbForPlatform->count('rules', $filterQueries, APP_LIMIT_COUNT),
'total' => $includeTotal ? $dbForPlatform->count('rules', $filterQueries, APP_LIMIT_COUNT) : 0,
]), Response::MODEL_PROXY_RULE_LIST);
}
}
@@ -57,7 +57,7 @@ class Create extends Action
group: 'deployments',
name: 'createDeployment',
description: <<<EOT
Create a new site code deployment. Use this endpoint to upload a new version of your site code. To activate your newly uploaded code, you'll need to update the function's deployment to use your new deployment ID.
Create a new site code deployment. Use this endpoint to upload a new version of your site code. To activate your newly uploaded code, you'll need to update the site's deployment to use your new deployment ID.
EOT,
auth: [AuthType::KEY],
responses: [
@@ -19,6 +19,7 @@ use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
class XList extends Base
@@ -57,6 +58,7 @@ class XList extends Base
->param('siteId', '', new UID(), 'Site ID.')
->param('queries', [], new Deployments(), '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 on the following attributes: ' . implode(', ', Deployments::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', 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('request')
->inject('response')
->inject('dbForProject')
@@ -67,6 +69,7 @@ class XList extends Base
string $siteId,
array $queries,
string $search,
bool $includeTotal,
Request $request,
Response $response,
Database $dbForProject
@@ -120,7 +123,7 @@ class XList extends Base
try {
$results = $dbForProject->find('deployments', $queries);
$total = $dbForProject->count('deployments', $filterQueries, APP_LIMIT_COUNT);
$total = $includeTotal ? $dbForProject->count('deployments', $filterQueries, APP_LIMIT_COUNT) : 0;
} catch (OrderException $e) {
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
}
@@ -19,6 +19,7 @@ use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
class XList extends Base
{
@@ -55,12 +56,13 @@ class XList extends Base
))
->param('siteId', '', new UID(), 'Site ID.')
->param('queries', [], new Logs(), '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 on the following attributes: ' . implode(', ', Executions::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('response')
->inject('dbForProject')
->callback($this->action(...));
}
public function action(string $siteId, array $queries, Response $response, Database $dbForProject)
public function action(string $siteId, array $queries, bool $includeTotal, Response $response, Database $dbForProject)
{
$site = $dbForProject->getDocument('sites', $siteId);
@@ -107,7 +109,7 @@ class XList extends Base
try {
$results = $dbForProject->find('executions', $queries);
$total = $dbForProject->count('executions', $filterQueries, APP_LIMIT_COUNT);
$total = $includeTotal ? $dbForProject->count('executions', $filterQueries, APP_LIMIT_COUNT) : 0;
} catch (OrderException $e) {
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
}
@@ -17,6 +17,7 @@ use Utopia\Database\Query;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
class XList extends Base
@@ -54,12 +55,13 @@ class XList extends Base
))
->param('queries', [], new Sites(), '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 on the following attributes: ' . implode(', ', Sites::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', 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('response')
->inject('dbForProject')
->callback($this->action(...));
}
public function action(array $queries, string $search, Response $response, Database $dbForProject)
public function action(array $queries, string $search, bool $includeTotal, Response $response, Database $dbForProject)
{
try {
$queries = Query::parseQueries($queries);
@@ -100,7 +102,7 @@ class XList extends Base
try {
$sites = $dbForProject->find('sites', $queries);
$total = $dbForProject->count('sites', $filterQueries, APP_LIMIT_COUNT);
$total = $includeTotal ? $dbForProject->count('sites', $filterQueries, APP_LIMIT_COUNT) : 0;
} catch (OrderException $e) {
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
}
@@ -15,6 +15,7 @@ use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
class XList extends Action
{
@@ -53,12 +54,13 @@ class XList extends Action
->param('bucketId', '', new UID(), 'Storage bucket unique ID. You can create a new storage bucket using the Storage service [server integration](https://appwrite.io/docs/server/storage#createBucket).')
->param('fileId', '', new UID(), 'File unique ID.')
->param('queries', [], new FileTokens(), '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 on the following attributes: ' . implode(', ', FileTokens::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('response')
->inject('dbForProject')
->callback($this->action(...));
}
public function action(string $bucketId, string $fileId, array $queries, Response $response, Database $dbForProject)
public function action(string $bucketId, string $fileId, array $queries, bool $includeTotal, Response $response, Database $dbForProject)
{
['bucket' => $bucket, 'file' => $file] = $this->getFileAndBucket($dbForProject, $bucketId, $fileId);
@@ -86,7 +88,7 @@ class XList extends Action
$response->dynamic(new Document([
'tokens' => $dbForProject->find('resourceTokens', $queries),
'total' => $dbForProject->count('resourceTokens', $filterQueries, APP_LIMIT_COUNT),
'total' => $includeTotal ? $dbForProject->count('resourceTokens', $filterQueries, APP_LIMIT_COUNT) : 0,
]), Response::MODEL_RESOURCE_TOKEN_LIST);
}
}
+56 -27
View File
@@ -48,13 +48,18 @@ class SDKs extends Action
->param('message', null, new Nullable(new Text(256)), 'Commit Message', optional: true)
->param('release', null, new Nullable(new WhiteList(['yes', 'no'])), 'Should we create releases?', optional: true)
->param('commit', null, new Nullable(new WhiteList(['yes', 'no'])), 'Actually create releases (yes) or dry-run (no)?', optional: true)
->param('sdks', null, new Nullable(new Text(256)), 'Selected SDKs', optional: true)
->callback($this->action(...));
}
public function action(?string $selectedPlatform, ?string $selectedSDK, ?string $version, ?string $git, ?string $production, ?string $message, ?string $release, ?string $commit): void
public function action(?string $selectedPlatform, ?string $selectedSDK, ?string $version, ?string $git, ?string $production, ?string $message, ?string $release, ?string $commit, ?string $sdks): void
{
$selectedPlatform ??= Console::confirm('Choose Platform ("' . APP_PLATFORM_CLIENT . '", "' . APP_PLATFORM_SERVER . '", "' . APP_PLATFORM_CONSOLE . '" or "*" for all):');
$selectedSDK ??= \strtolower(Console::confirm('Choose SDK ("*" for all):'));
if (!$sdks) {
$selectedPlatform ??= Console::confirm('Choose Platform ("' . APP_PLATFORM_CLIENT . '", "' . APP_PLATFORM_SERVER . '", "' . APP_PLATFORM_CONSOLE . '" or "*" for all):');
$selectedSDK ??= \strtolower(Console::confirm('Choose SDK ("*" for all):'));
} else {
$sdks = explode(',', $sdks);
}
$version ??= Console::confirm('Choose an Appwrite version');
$createRelease = ($release === 'yes');
@@ -104,12 +109,12 @@ class SDKs extends Action
$platforms = Config::getParam('platforms');
foreach ($platforms as $key => $platform) {
if ($selectedPlatform !== $key && $selectedPlatform !== '*') {
if ($selectedPlatform !== $key && $selectedPlatform !== '*' && ($sdks === null)) {
continue;
}
foreach ($platform['sdks'] as $language) {
if ($selectedSDK !== $language['key'] && $selectedSDK !== '*') {
if ($selectedSDK !== $language['key'] && $selectedSDK !== '*' && ($sdks === null || !\in_array($language['key'], $sdks))) {
continue;
}
@@ -254,6 +259,8 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
}
if ($createRelease) {
Console::execute('git config --global user.email "$GIT_EMAIL"', stdin: '', stdout: '', stderr: '');
$releaseVersion = $language['version'];
$repoName = $language['gitUserName'] . '/' . $language['gitRepoName'];
@@ -472,38 +479,60 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
$errorMessage = implode("\n", $prOutput);
if (strpos($errorMessage, 'already exists') !== false) {
Console::warning("Pull request already exists for {$language['name']} SDK, updating title and body...");
$updateCommand = 'cd ' . $target . ' && \
gh pr edit "' . $gitBranch . '" \
$prNumberCommand = 'cd ' . $target . ' && \
gh pr list \
--repo "' . $repoName . '" \
--title "' . $prTitle . '" \
--body "' . $prBody . '" \
--head "' . $gitBranch . '" \
--json number \
--jq ".[0].number" \
2>&1';
$updateOutput = [];
$updateReturnCode = 0;
\exec($updateCommand, $updateOutput, $updateReturnCode);
$prNumberOutput = [];
$prNumberReturnCode = 0;
\exec($prNumberCommand, $prNumberOutput, $prNumberReturnCode);
if ($updateReturnCode === 0) {
Console::success("Successfully updated pull request for {$language['name']} SDK");
if ($prNumberReturnCode === 0 && !empty($prNumberOutput[0])) {
$prNumber = trim($prNumberOutput[0]);
$prUrlCommand = 'cd ' . $target . ' && \
gh pr view "' . $gitBranch . '" \
--repo "' . $repoName . '" \
--json url \
--jq .url \
// Use API directly to update PR to avoid deprecated projectCards field
$updateCommand = 'cd ' . $target . ' && \
gh api \
--method PATCH \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
/repos/' . $repoName . '/pulls/' . $prNumber . ' \
-f title="' . $prTitle . '" \
-f body="' . $prBody . '" \
2>&1';
$prUrlOutput = [];
$prUrlReturnCode = 0;
\exec($prUrlCommand, $prUrlOutput, $prUrlReturnCode);
$updateOutput = [];
$updateReturnCode = 0;
\exec($updateCommand, $updateOutput, $updateReturnCode);
if ($prUrlReturnCode === 0 && !empty($prUrlOutput)) {
$prUrls[$language['name']] = $prUrlOutput[0];
if ($updateReturnCode === 0) {
Console::success("Successfully updated pull request for {$language['name']} SDK");
$prUrlCommand = 'cd ' . $target . ' && \
gh pr list \
--repo "' . $repoName . '" \
--head "' . $gitBranch . '" \
--json url \
--jq ".[0].url" \
2>&1';
$prUrlOutput = [];
$prUrlReturnCode = 0;
\exec($prUrlCommand, $prUrlOutput, $prUrlReturnCode);
if ($prUrlReturnCode === 0 && !empty($prUrlOutput)) {
$prUrls[$language['name']] = trim($prUrlOutput[0]);
}
} else {
$updateErrorMessage = implode("\n", $updateOutput);
Console::error("Failed to update pull request for {$language['name']} SDK: " . $updateErrorMessage);
}
} else {
$updateErrorMessage = implode("\n", $updateOutput);
Console::error("Failed to update pull request for {$language['name']} SDK: " . $updateErrorMessage);
Console::error("Failed to get PR number for {$language['name']} SDK");
}
} else {
Console::error("Failed to create pull request for {$language['name']} SDK: " . $errorMessage);
+5 -15
View File
@@ -22,6 +22,7 @@ use Utopia\Database\Exception\Conflict;
use Utopia\Database\Exception\Structure;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Query;
use Utopia\DNS\Message\Record;
use Utopia\Domains\Domain;
use Utopia\Locale\Locale;
use Utopia\Logger\Log;
@@ -313,13 +314,13 @@ class Certificates extends Action
$validators = [];
$targetCNAME = new Domain(System::getEnv('_APP_DOMAIN_TARGET_CNAME', ''));
if ($targetCNAME->isKnown() && !$targetCNAME->isTest()) {
$validators[] = new DNS($targetCNAME->get(), DNS::RECORD_CNAME);
$validators[] = new DNS($targetCNAME->get(), Record::TYPE_CNAME);
}
if ((new IP(IP::V4))->isValid(System::getEnv('_APP_DOMAIN_TARGET_A', ''))) {
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_A', ''), DNS::RECORD_A);
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_A', ''), Record::TYPE_A);
}
if ((new IP(IP::V6))->isValid(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''))) {
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), DNS::RECORD_AAAA);
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), Record::TYPE_AAAA);
}
// Validate if domain target is properly configured
@@ -332,24 +333,13 @@ class Certificates extends Action
if (!$validator->isValid($domain->get())) {
$log->addExtra('dnsTiming', \strval(\microtime(true) - $validationStart));
$log->addTag('dnsDomain', $domain->get());
$errors = [];
foreach ($validators as $validator) {
if (!empty($validator->getLogs())) {
$errors[] = $validator->getLogs();
}
}
$error = \implode("\n", $errors);
$log->addExtra('dnsResponse', \is_array($error) ? \json_encode($error) : \strval($error));
throw new Exception('Failed to verify domain DNS records.');
}
// Ensure CAA won't block certificate issuance
if (!empty(System::getEnv('_APP_DOMAIN_TARGET_CAA', ''))) {
$validationStart = \microtime(true);
$validator = new DNS(System::getEnv('_APP_DOMAIN_TARGET_CAA', ''), DNS::RECORD_CAA);
$validator = new DNS(System::getEnv('_APP_DOMAIN_TARGET_CAA', ''), Record::TYPE_CAA);
if (!$validator->isValid($domain->get())) {
$log->addExtra('dnsTimingCaa', \strval(\microtime(true) - $validationStart));
$log->addTag('dnsDomain', $domain->get());
+354 -101
View File
@@ -3,8 +3,10 @@
namespace Appwrite\Platform\Workers;
use Ahc\Jwt\JWT;
use Appwrite\Event\Mail;
use Appwrite\Event\Realtime;
use Exception;
use Appwrite\Extend\Exception;
use Appwrite\Template\Template;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Database\Database;
@@ -13,10 +15,15 @@ use Utopia\Database\Exception\Authorization;
use Utopia\Database\Exception\Conflict;
use Utopia\Database\Exception\Restricted;
use Utopia\Database\Exception\Structure;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Query;
use Utopia\Locale\Locale;
use Utopia\Migration\Destination;
use Utopia\Migration\Destinations\Appwrite as DestinationAppwrite;
use Utopia\Migration\Destinations\CSV as DestinationCSV;
use Utopia\Migration\Exception as MigrationException;
use Utopia\Migration\Source;
use Utopia\Migration\Sources\Appwrite;
use Utopia\Migration\Sources\Appwrite as SourceAppwrite;
use Utopia\Migration\Sources\CSV;
use Utopia\Migration\Sources\Firebase;
@@ -25,6 +32,7 @@ use Utopia\Migration\Sources\Supabase;
use Utopia\Migration\Transfer;
use Utopia\Platform\Action;
use Utopia\Queue\Message;
use Utopia\Storage\Compression\Compression;
use Utopia\Storage\Device;
use Utopia\System\System;
@@ -34,13 +42,14 @@ class Migrations extends Action
protected Database $dbForPlatform;
protected Device $deviceForImports;
protected Device $deviceForMigrations;
protected Device $deviceForFiles;
protected Document $project;
protected array $plan;
/**
* Cached for performance.
*
* @var array<string, int>
*/
protected array $sourceReport = [];
@@ -68,23 +77,38 @@ class Migrations extends Action
->inject('dbForPlatform')
->inject('logError')
->inject('queueForRealtime')
->inject('deviceForImports')
->inject('deviceForMigrations')
->inject('deviceForFiles')
->inject('queueForMails')
->inject('plan')
->callback($this->action(...));
}
/**
* @throws Exception
*/
public function action(Message $message, Document $project, Database $dbForProject, Database $dbForPlatform, callable $logError, Realtime $queueForRealtime, Device $deviceForImports): void
{
public function action(
Message $message,
Document $project,
Database $dbForProject,
Database $dbForPlatform,
callable $logError,
Realtime $queueForRealtime,
Device $deviceForMigrations,
Device $deviceForFiles,
Mail $queueForMails,
array $plan,
): void {
$payload = $message->getPayload() ?? [];
$this->deviceForImports = $deviceForImports;
$this->deviceForMigrations = $deviceForMigrations;
$this->deviceForFiles = $deviceForFiles;
$this->plan = $plan;
if (empty($payload)) {
throw new Exception('Missing payload');
}
$events = $payload['events'] ?? [];
$events = $payload['events'] ?? [];
$migration = new Document($payload['migration'] ?? []);
if ($project->getId() === 'console') {
@@ -96,14 +120,11 @@ class Migrations extends Action
$this->project = $project;
$this->logError = $logError;
/**
* Handle Event execution.
*/
if (! empty($events)) {
if (!empty($events)) {
return;
}
$this->processMigration($migration, $queueForRealtime);
$this->processMigration($migration, $queueForRealtime, $queueForMails);
}
/**
@@ -112,9 +133,19 @@ class Migrations extends Action
protected function processSource(Document $migration): Source
{
$source = $migration->getAttribute('source');
$destination = $migration->getAttribute('destination');
$resourceId = $migration->getAttribute('resourceId');
$credentials = $migration->getAttribute('credentials');
$migrationOptions = $migration->getAttribute('options');
$dataSource = Appwrite::SOURCE_API;
$database = null;
$queries = [];
if ($source === Appwrite::getName() && $destination === DestinationCSV::getName()) {
$dataSource = Appwrite::SOURCE_DATABASE;
$database = $this->dbForProject;
$queries = Query::parseQueries($migrationOptions['queries']);
}
$migrationSource = match ($source) {
Firebase::getName() => new Firebase(
@@ -142,11 +173,14 @@ class Migrations extends Action
$credentials['projectId'],
$credentials['endpoint'] === 'http://localhost/v1' ? 'http://appwrite/v1' : $credentials['endpoint'],
$credentials['apiKey'],
$dataSource,
$database,
$queries,
),
CSV::getName() => new CSV(
$resourceId,
$migrationOptions['path'],
$this->deviceForImports,
$this->deviceForMigrations,
$this->dbForProject
),
default => throw new \Exception('Invalid source type'),
@@ -163,6 +197,7 @@ class Migrations extends Action
protected function processDestination(Document $migration, string $apiKey): Destination
{
$destination = $migration->getAttribute('destination');
$options = $migration->getAttribute('options', []);
return match ($destination) {
DestinationAppwrite::getName() => new DestinationAppwrite(
@@ -172,6 +207,17 @@ class Migrations extends Action
$this->dbForProject,
Config::getParam('collections', [])['databases']['collections'],
),
DestinationCSV::getName() => new DestinationCSV(
$this->deviceForFiles,
$migration->getAttribute('resourceId'),
$options['bucketId'],
$options['filename'],
$options['columns'],
$options['delimiter'],
$options['enclosure'],
$options['escape'],
$options['header'],
),
default => throw new \Exception('Invalid destination type'),
};
}
@@ -185,35 +231,19 @@ class Migrations extends Action
*/
protected function updateMigrationDocument(Document $migration, Document $project, Realtime $queueForRealtime): Document
{
$errorMessages = [];
$clonedMigrationDocument = clone $migration;
// we cannot use #sensitive because
// `errors` is nested which requires an override.
$errors = $clonedMigrationDocument->getAttribute('errors', []);
foreach ($errors as $error) {
$decoded = json_decode($error, true);
if (is_array($decoded) && isset($decoded['trace'])) {
unset($decoded['trace']);
$errorMessages[] = json_encode($decoded);
}
}
// set the errors back without trace
$clonedMigrationDocument->setAttribute('errors', $errorMessages);
/** Trigger Realtime Events */
$queueForRealtime
->setProject($project)
->setSubscribers(['console', $project->getId()])
->setEvent('migrations.[migrationId].update')
->setParam('migrationId', $migration->getId())
->setPayload($clonedMigrationDocument->getArrayCopy(), ['options', 'credentials'])
->setPayload($migration->getArrayCopy(), sensitive: ['credentials'])
->trigger();
return $this->dbForProject->updateDocument('migrations', $migration->getId(), $migration);
return $this->dbForProject->updateDocument(
'migrations',
$migration->getId(),
$migration
);
}
/**
@@ -243,13 +273,6 @@ class Migrations extends Action
'files.write',
'functions.read',
'functions.write',
'databases.read',
'collections.read',
'tables.read',
'documents.read',
'documents.write',
'rows.read',
'rows.write',
'tokens.read',
'tokens.write',
]
@@ -266,11 +289,13 @@ class Migrations extends Action
* @throws \Utopia\Database\Exception
* @throws Exception
*/
protected function processMigration(Document $migration, Realtime $queueForRealtime): void
{
$project = $this->project;
$projectDocument = $this->dbForPlatform->getDocument('projects', $project->getId());
$tempAPIKey = $this->generateAPIKey($projectDocument);
protected function processMigration(
Document $migration,
Realtime $queueForRealtime,
Mail $queueForMails,
): void {
$project = $this->dbForPlatform->getDocument('projects', $this->project->getId());
$tempAPIKey = $this->generateAPIKey($project);
$transfer = $source = $destination = null;
@@ -280,17 +305,15 @@ class Migrations extends Action
empty($migration->getAttribute('credentials', []))
) {
$credentials = $migration->getAttribute('credentials', []);
$credentials['projectId'] = $credentials['projectId'] ?? $projectDocument->getId();
$credentials['projectId'] = $credentials['projectId'] ?? $project->getId();
$credentials['endpoint'] = $credentials['endpoint'] ?? 'http://appwrite/v1';
$credentials['apiKey'] = $credentials['apiKey'] ?? $tempAPIKey;
$migration->setAttribute('credentials', $credentials);
}
$migration->setAttribute('stage', 'processing');
$migration->setAttribute('status', 'processing');
$this->updateMigrationDocument($migration, $projectDocument, $queueForRealtime);
$this->updateMigrationDocument($migration, $project, $queueForRealtime);
$source = $this->processSource($migration);
$destination = $this->processDestination($migration, $tempAPIKey);
@@ -303,40 +326,30 @@ class Migrations extends Action
/** Start Transfer */
if (empty($source->getErrors())) {
$migration->setAttribute('stage', 'migrating');
$this->updateMigrationDocument($migration, $projectDocument, $queueForRealtime);
$this->updateMigrationDocument($migration, $project, $queueForRealtime);
$transfer->run(
$migration->getAttribute('resources'),
function () use ($migration, $transfer, $projectDocument, $queueForRealtime) {
function () use ($migration, $transfer, $project, $queueForRealtime) {
$migration->setAttribute('resourceData', json_encode($transfer->getCache()));
$migration->setAttribute('statusCounters', json_encode($transfer->getStatusCounters()));
$this->updateMigrationDocument($migration, $projectDocument, $queueForRealtime);
$this->updateMigrationDocument($migration, $project, $queueForRealtime);
},
$migration->getAttribute('resourceId'),
$migration->getAttribute('resourceType')
);
}
$destination->shutDown();
$source->shutDown();
$destination->shutdown();
$source->shutdown();
$sourceErrors = $source->getErrors();
$destinationErrors = $destination->getErrors();
if (! empty($sourceErrors) || ! empty($destinationErrors)) {
if (!empty($sourceErrors) || ! empty($destinationErrors)) {
$migration->setAttribute('status', 'failed');
$migration->setAttribute('stage', 'finished');
$errorMessages = [];
foreach ($sourceErrors as $error) {
$errorMessages[] = json_encode($error);
}
foreach ($destinationErrors as $error) {
$errorMessages[] = json_encode($error);
}
$migration->setAttribute('errors', $errorMessages);
$migration->setAttribute('errors', $this->sanitizeErrors($sourceErrors, $destinationErrors));
return;
}
@@ -362,58 +375,298 @@ class Migrations extends Action
if ($transfer) {
$sourceErrors = $source->getErrors();
$destinationErrors = $destination->getErrors();
$errorMessages = [];
foreach ($sourceErrors as $error) {
$errorMessages[] = json_encode($error);
}
foreach ($destinationErrors as $error) {
$errorMessages[] = json_encode($error);
}
$migration->setAttribute('errors', $errorMessages);
$migration->setAttribute('errors', $this->sanitizeErrors($sourceErrors, $destinationErrors));
}
} finally {
$this->updateMigrationDocument($migration, $projectDocument, $queueForRealtime);
$this->updateMigrationDocument($migration, $project, $queueForRealtime);
if ($migration->getAttribute('status', '') === 'failed') {
Console::error('Migration('.$migration->getSequence().':'.$migration->getId().') failed, Project('.$this->project->getSequence().':'.$this->project->getId().')');
if ($destination) {
$destination->error();
$sourceErrors = $source?->getErrors() ?? [];
$destinationErrors = $destination?->getErrors() ?? [];
foreach ($destination->getErrors() as $error) {
/** @var MigrationException $error */
call_user_func($this->logError, $error, 'appwrite-worker', 'appwrite-queue-' . self::getName(), [
foreach ([...$sourceErrors, ...$destinationErrors] as $error) {
/** @var MigrationException $error */
if ($error->getCode() === 0 || $error->getCode() >= 500) {
($this->logError)($error, 'appwrite-worker', 'appwrite-queue-' . self::getName(), [
'migrationId' => $migration->getId(),
'source' => $migration->getAttribute('source') ?? '',
'destination' => $migration->getAttribute('destination') ?? '',
'resourceName' => $error->getResourceName(),
'resourceGroup' => $error->getResourceGroup()
'resourceGroup' => $error->getResourceGroup(),
]);
}
}
if ($source) {
$source->error();
foreach ($source->getErrors() as $error) {
/** @var MigrationException $error */
call_user_func($this->logError, $error, 'appwrite-worker', 'appwrite-queue-' . self::getName(), [
'migrationId' => $migration->getId(),
'source' => $migration->getAttribute('source') ?? '',
'destination' => $migration->getAttribute('destination') ?? '',
'resourceName' => $error->getResourceName(),
'resourceGroup' => $error->getResourceGroup()
]);
}
}
$source?->error();
$destination?->error();
}
if ($migration->getAttribute('status', '') === 'completed') {
$destination?->success();
$source?->success();
if ($migration->getAttribute('destination') === DestinationCSV::getName()) {
$this->handleCSVExportComplete($project, $migration, $queueForMails);
}
}
}
}
/**
* Handle actions to be performed when a CSV export migration is successfully completed
*
* @param Document $project
* @param Document $migration
* @param Mail $queueForMails
* @return void
* @throws Authorization
* @throws Structure
* @throws \Utopia\Database\Exception
* @throws Exception
*/
protected function handleCSVExportComplete(
Document $project,
Document $migration,
Mail $queueForMails
): void {
$options = $migration->getAttribute('options', []);
$bucketId = $options['bucketId'] ?? null;
$filename = $options['filename'] ?? 'export_' . \time();
$userInternalId = $options['userInternalId'] ?? '';
$bucket = $this->dbForProject->getDocument('buckets', $bucketId);
if ($bucket->isEmpty()) {
throw new \Exception("Bucket not found: $bucketId");
}
$path = $this->deviceForFiles->getPath($bucketId . '/' . $this->sanitizeFilename($filename) . '.csv');
$size = $this->deviceForFiles->getFileSize($path);
$mime = $this->deviceForFiles->getFileMimeType($path);
$hash = $this->deviceForFiles->getFileHash($path);
$algorithm = Compression::NONE;
$fileId = ID::unique();
$sizeMB = \round($size / (1000 * 1000), 2);
$planFileSize = empty($this->plan['fileSize'])
? PHP_INT_MAX
: $this->plan['fileSize'];
if ($sizeMB > $planFileSize) {
try {
$this->deviceForFiles->delete($path);
} finally {
$message = "Export file size {$sizeMB}MB exceeds your plan limit.";
$this->dbForProject->updateDocument('migrations', $migration->getId(), $migration->setAttribute(
'errors',
json_encode(['code' => 0, 'message' => $message]),
Document::SET_TYPE_APPEND,
));
$this->sendCSVEmail(
success: false,
project: $project,
userInternalId: $userInternalId,
options: $options,
queueForMails: $queueForMails,
sizeMB: $sizeMB
);
throw new \Exception($message);
}
}
$this->dbForProject->createDocument('bucket_' . $bucket->getSequence(), new Document([
'$id' => $fileId,
'$permissions' => [],
'bucketId' => $bucket->getId(),
'bucketInternalId' => $bucket->getSequence(),
'name' => $filename,
'path' => $path,
'signature' => $hash,
'mimeType' => $mime,
'sizeOriginal' => $size,
'sizeActual' => $size,
'algorithm' => $algorithm,
'comment' => '',
'chunksTotal' => 1,
'chunksUploaded' => 1,
'openSSLVersion' => null,
'openSSLCipher' => null,
'openSSLTag' => null,
'openSSLIV' => null,
'search' => \implode(' ', [$fileId, $filename]),
'metadata' => ['content_type' => $mime]
]));
Console::info("Created file document in bucket: $fileId");
// Generate JWT valid for 1 hour
$maxAge = 60 * 60;
$encoder = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $maxAge, 0);
$jwt = $encoder->encode([
'bucketId' => $bucketId,
'fileId' => $fileId,
'projectId' => $project->getId(),
]);
// Generate download URL with JWT
$endpoint = System::getEnv('_APP_DOMAIN', '');
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS', 'disabled') === 'enabled' ? 'https' : 'http';
$downloadUrl = "{$protocol}://{$endpoint}/v1/storage/buckets/{$bucketId}/files/{$fileId}/push?project={$project->getId()}&jwt={$jwt}";
$this->sendCSVEmail(
success: true,
project: $project,
userInternalId: $userInternalId,
options: $options,
queueForMails: $queueForMails,
downloadUrl: $downloadUrl
);
}
/**
* Send CSV export notification email
*
* @param bool $success Whether the export was successful
* @param Document $project
* @param string $userInternalId Internal ID of the user
* @param array $options Migration options
* @param Mail $queueForMails
* @param string $downloadUrl Download URL for successful exports
* @param float $sizeMB File size in MB for failed exports
* @return void
* @throws \Exception
*/
protected function sendCSVEmail(
bool $success,
Document $project,
string $userInternalId,
array $options,
Mail $queueForMails,
string $downloadUrl = '',
float $sizeMB = 0.0
): void {
if (!($options['notify'] ?? false)) {
return;
}
$user = $this->dbForPlatform->findOne('users', [
Query::equal('$sequence', [$userInternalId])
]);
if ($user->isEmpty()) {
Console::warning("User not found for CSV export notification: $userInternalId");
return;
}
$locale = new Locale(System::getEnv('_APP_LOCALE', 'en'));
$locale->setFallback(System::getEnv('_APP_LOCALE', 'en'));
$emailType = $success
? 'success'
: 'failure';
// Get localized email content
$subject = $locale->getText("emails.csvExport.{$emailType}.subject");
$preview = $locale->getText("emails.csvExport.{$emailType}.preview");
$hello = $locale->getText("emails.csvExport.{$emailType}.hello");
$body = $locale->getText("emails.csvExport.{$emailType}.body");
$footer = $locale->getText("emails.csvExport.{$emailType}.footer");
$thanks = $locale->getText("emails.csvExport.{$emailType}.thanks");
$signature = $locale->getText("emails.csvExport.{$emailType}.signature");
$buttonText = $success ? $locale->getText("emails.csvExport.{$emailType}.buttonText") : '';
// Build email body using appropriate template
$templatePath = $success
? __DIR__ . '/../../../../app/config/locale/templates/email-inner-base.tpl'
: __DIR__ . '/../../../../app/config/locale/templates/email-export-failed.tpl';
$message = Template::fromFile($templatePath)
->setParam('{{body}}', $body, escapeHtml: false)
->setParam('{{hello}}', $hello)
->setParam('{{footer}}', $footer)
->setParam('{{thanks}}', $thanks)
->setParam('{{signature}}', $signature)
->setParam('{{direction}}', $locale->getText('settings.direction'))
->setParam('{{project}}', $project->getAttribute('name'))
->setParam('{{user}}', $user->getAttribute('name', $user->getAttribute('email')))
->setParam('{{size}}', $success ? '' : (string)$sizeMB);
if ($success) {
$message
->setParam('{{buttonText}}', $buttonText)
->setParam('{{redirect}}', $downloadUrl);
}
$emailBody = $message->render();
$emailVariables = [
'direction' => $locale->getText('settings.direction'),
'logoUrl' => $this->plan['logoUrl'] ?? APP_EMAIL_LOGO_URL,
'accentColor' => $this->plan['accentColor'] ?? APP_EMAIL_ACCENT_COLOR,
'twitterUrl' => $this->plan['twitterUrl'] ?? APP_SOCIAL_TWITTER,
'discordUrl' => $this->plan['discordUrl'] ?? APP_SOCIAL_DISCORD,
'githubUrl' => $this->plan['githubUrl'] ?? APP_SOCIAL_GITHUB_APPWRITE,
'termsUrl' => $this->plan['termsUrl'] ?? APP_EMAIL_TERMS_URL,
'privacyUrl' => $this->plan['privacyUrl'] ?? APP_EMAIL_PRIVACY_URL,
];
$queueForMails
->setSubject($subject)
->setPreview($preview)
->setBody($emailBody)
->setBodyTemplate(__DIR__ . '/../../../../app/config/locale/templates/email-base-styled.tpl')
->setVariables($emailVariables)
->setName($user->getAttribute('name', $user->getAttribute('email')))
->setRecipient($user->getAttribute('email'))
->trigger();
Console::info("CSV export {$emailType} notification email sent to " . $user->getAttribute('email'));
}
/**
* Sanitize a filename to make it filesystem-safe
*
* @param string $filename
* @return string
*/
protected function sanitizeFilename(string $filename): string
{
// Replace problematic characters with underscores
$sanitized = \preg_replace('/[:\/<>"|*?]/', '_', $filename);
$sanitized = \preg_replace('/[^\x20-\x7E]/', '_', $sanitized);
$sanitized = \trim($sanitized);
return empty($sanitized) ? 'export' : $sanitized;
}
/**
* Sanitize migration errors, removing sensitive information like stack traces
*
* @param array $sourceErrors
* @param array $destinationErrors
* @return array
*/
protected function sanitizeErrors(
array $sourceErrors,
array $destinationErrors,
): array {
$errors = [];
foreach ([...$sourceErrors, ...$destinationErrors] as $error) {
$encoded = \json_decode(\json_encode($error), true);
if (\is_array($encoded)) {
if (isset($encoded['trace'])) {
unset($encoded['trace']);
}
$errors[] = \json_encode($encoded);
} else {
$errors[] = \json_encode($error);
}
}
return $errors;
}
}
@@ -335,7 +335,11 @@ class StatsResources extends Action
$this->createStatsDocuments($region, str_replace("{resourceType}", RESOURCE_TYPE_FUNCTIONS, METRIC_RESOURCE_TYPE_DEPLOYMENTS), $deployments);
$this->createStatsDocuments($region, str_replace("{resourceType}", RESOURCE_TYPE_FUNCTIONS, METRIC_RESOURCE_TYPE_BUILDS), $deployments);
$this->foreachDocument($dbForProject, 'functions', [], function (Document $function) use ($dbForProject, $region) {
// Count runtimes
$runtimes = [];
$this->foreachDocument($dbForProject, 'functions', [], function (Document $function) use ($dbForProject, $region, &$runtimes) {
$functionDeploymentsStorage = $dbForProject->sum('deployments', 'sourceSize', [
Query::equal('resourceInternalId', [$function->getSequence()]),
Query::equal('resourceType', [RESOURCE_TYPE_FUNCTIONS]),
@@ -364,7 +368,19 @@ class StatsResources extends Action
});
$this->createStatsDocuments($region, str_replace(['{resourceType}','{resourceInternalId}'], [RESOURCE_TYPE_FUNCTIONS,$function->getSequence()], METRIC_RESOURCE_TYPE_ID_BUILDS_STORAGE), $functionBuildsStorage);
// Runtimes count
$runtime = $function->getAttribute('runtime');
if (!empty($runtime)) {
$runtimes[$runtime] = ($runtimes[$runtime] ?? 0) + 1;
}
});
// Write runtimes counts
foreach ($runtimes as $runtime => $count) {
$this->createStatsDocuments($region, str_replace('{runtime}', $runtime, METRIC_FUNCTIONS_RUNTIME), $count);
}
}
protected function countForSites(Database $dbForProject, string $region)
@@ -385,7 +401,10 @@ class StatsResources extends Action
$this->createStatsDocuments($region, str_replace("{resourceType}", RESOURCE_TYPE_SITES, METRIC_RESOURCE_TYPE_DEPLOYMENTS), $deployments);
$this->createStatsDocuments($region, str_replace("{resourceType}", RESOURCE_TYPE_SITES, METRIC_RESOURCE_TYPE_BUILDS), $deployments);
$this->foreachDocument($dbForProject, 'sites', [], function (Document $site) use ($dbForProject, $region) {
// Count frameworks
$frameworks = [];
$this->foreachDocument($dbForProject, 'sites', [], function (Document $site) use ($dbForProject, $region, &$frameworks) {
$siteDeploymentsStorage = $dbForProject->sum('deployments', 'sourceSize', [
Query::equal('resourceInternalId', [$site->getSequence()]),
Query::equal('resourceType', [RESOURCE_TYPE_SITES]),
@@ -410,7 +429,18 @@ class StatsResources extends Action
]);
$this->createStatsDocuments($region, str_replace(['{resourceType}','{resourceInternalId}'], [RESOURCE_TYPE_SITES,$site->getSequence()], METRIC_RESOURCE_TYPE_ID_BUILDS_STORAGE), $siteBuildsStorage);
// Frameworks count
$framework = $site->getAttribute('framework');
if (!empty($framework)) {
$frameworks[$framework] = ($frameworks[$framework] ?? 0) + 1;
}
});
// Write frameworks counts
foreach ($frameworks as $framework => $count) {
$this->createStatsDocuments($region, str_replace('{framework}', $framework, METRIC_SITES_FRAMEWORK), $count);
}
}
protected function createStatsDocuments(string $region, string $metric, int $value)
+4 -5
View File
@@ -5,7 +5,6 @@ namespace Appwrite\Utopia\Request\Filters;
use Appwrite\Extend\Exception;
use Appwrite\Utopia\Request\Filter;
use Utopia\Database\Database;
use Utopia\Database\Exception\NotFound;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
@@ -146,8 +145,8 @@ class V20 extends Filter
if ($database->isEmpty()) {
return [];
}
} catch (NotFound) {
throw new Exception(Exception::DATABASE_NOT_FOUND);
} catch (\Throwable) {
return [];
}
try {
@@ -158,8 +157,8 @@ class V20 extends Filter
if ($collection->isEmpty()) {
return [];
}
} catch (NotFound) {
throw new Exception(Exception::COLLECTION_NOT_FOUND);
} catch (\Throwable) {
return [];
}
$attributes = $collection->getAttribute('attributes', []);
@@ -86,6 +86,12 @@ class Migration extends Model
'default' => [],
'example' => [],
])
->addRule('options', [
'type' => self::TYPE_JSON,
'description' => 'Migration options used during the migration process.',
'default' => [],
'example' => '{"bucketId": "exports", "notify": false}',
])
;
}
@@ -117,18 +123,16 @@ class Migration extends Model
}
foreach ($errors as $index => $error) {
$decoded = json_decode($error, true);
$decoded = \json_decode($error, true);
// frontend doesn't need too many details.
if (is_array($decoded)) {
$errors[$index] = json_encode([
'code' => $decoded['code'] ?? 0,
'message' => $decoded['message'] ?? null,
]);
if (\is_array($decoded)) {
if (isset($decoded['trace'])) {
unset($decoded['trace']);
}
$errors[$index] = \json_encode($decoded);
}
}
// errors now only have code and message.
$document->setAttribute('errors', $errors);
return $document;
+30 -3
View File
@@ -11,9 +11,36 @@ class Comment
{
// TODO: Add more tips
protected array $tips = [
'Appwrite has a Discord community with over 16 000 members.',
'You can use Avatars API to generate QR code for any text or URLs.',
'Cursor pagination performs better than offset pagination when loading further pages.',
'Appwrite has crossed the 50K GitHub stars milestone with hundreds of active contributors',
'Our Discord community has grown to 24K developers, and counting',
'Sites auto-generate unique domains with the pattern https://randomstring.appwrite.network',
'Every Git commit and branch gets its own deployment URL automatically',
'Custom domains work with both CNAME for subdomains and NS records for apex domains',
'HTTPS and SSL certificates are handled automatically for all your Sites',
'Functions can run for up to 15 minutes before timing out',
'Schedule functions to run as often as every minute with cron expressions',
'Environment variables can be scoped per function or shared across your project',
'Function scopes give you fine-grained control over API permissions',
'Sites support three domain rule types: Active deployment, Git branch, and Redirect',
'Preview deployments create instant URLs for every branch and commit',
'Trigger functions via HTTP, SDKs, events, webhooks, or scheduled cron jobs',
'Each function runs in its own isolated container with custom environment variables',
'Build commands execute in runtime containers during deployment',
'Dynamic API keys are generated automatically for each function execution',
'JWT tokens let functions act on behalf of users while preserving their permissions',
'Storage files get ClamAV malware scanning and encryption by default',
'Roll back Sites deployments instantly by switching between versions',
'Git integration provides automatic deployments with optional PR comments',
'Silent mode disables those chatty PR comments if you prefer peace and quiet',
'Environment variable changes require redeployment to take effect',
'SSR frameworks are fully supported with configurable build runtimes',
'Global CDN and DDoS protection come free with every Sites deployment',
'Deploy functions via zip upload or connect directly to your Git repo',
'Realtime gives you live updates for users, storage, functions, and databases',
'GraphQL API works alongside REST and WebSocket protocols',
'Messaging handles push notifications, emails, and SMS through one unified API',
'Teams feature lets you group users with membership management and role permissions',
'MCP server integration brings LLM superpowers to Claude Desktop and Cursor IDE',
];
protected string $statePrefix = '[appwrite]: #';