mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Merge branch '1.8.x' of https://github.com/appwrite/appwrite into users-add-attributes
# Conflicts: # composer.lock
This commit is contained in:
@@ -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'
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
+4
-2
@@ -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';
|
||||
|
||||
+48
@@ -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
|
||||
|
||||
+2
@@ -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
|
||||
|
||||
+1
@@ -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);
|
||||
}
|
||||
|
||||
+2
@@ -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();
|
||||
|
||||
+2
@@ -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,
|
||||
|
||||
+6
-4
@@ -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';
|
||||
|
||||
+4
-2
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]: #';
|
||||
|
||||
Reference in New Issue
Block a user