Merge remote-tracking branch 'upstream/1.8.x' into spatial-type-attributes

This commit is contained in:
ArnabChatterjee20k
2025-09-02 23:04:12 +05:30
31 changed files with 368 additions and 219 deletions
+7
View File
@@ -85,4 +85,11 @@ class StatsUsage extends Event
}),
];
}
public function reset(): Event
{
$this->metrics = [];
parent::reset();
return $this;
}
}
@@ -39,7 +39,7 @@ class Upsert extends Action
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PUT)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/documents')
->desc('Create or update documents')
->desc('Upsert documents')
->groups(['api', 'database'])
->label('scope', 'documents.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
@@ -45,7 +45,7 @@ class Upsert extends Action
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PUT)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId')
->desc('Create or update a document')
->desc('Upsert a document')
->groups(['api', 'database'])
->label('event', 'databases.[databaseId].collections.[collectionId].documents.[documentId].upsert')
->label('scope', 'documents.write')
@@ -41,7 +41,7 @@ class Create extends CollectionCreate
->label('audits.resource', 'database/{request.databaseId}/table/{response.$id}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: null,
group: 'tables',
name: self::getName(),
description: '/docs/references/tablesdb/create-table.md',
auth: [AuthType::KEY],
@@ -37,7 +37,7 @@ class Delete extends CollectionDelete
->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: null,
group: 'tables',
name: self::getName(),
description: '/docs/references/tablesdb/delete-table.md',
auth: [AuthType::KEY],
@@ -34,7 +34,7 @@ class Get extends CollectionGet
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: null,
group: 'tables',
name: self::getName(),
description: '/docs/references/tablesdb/get-table.md',
auth: [AuthType::KEY],
@@ -30,7 +30,7 @@ class Upsert extends DocumentsUpsert
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PUT)
->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/rows')
->desc('Create or update rows')
->desc('Upsert rows')
->groups(['api', 'database'])
->label('scope', ['rows.write', 'documents.write'])
->label('resourceType', RESOURCE_TYPE_DATABASES)
@@ -31,7 +31,7 @@ class Upsert extends DocumentUpsert
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PUT)
->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/rows/:rowId')
->desc('Create or update a row')
->desc('Upsert a row')
->groups(['api', 'database'])
->label('event', 'databases.[databaseId].tables.[tableId].rows.[rowId].upsert')
->label('scope', ['rows.write', 'documents.write'])
@@ -40,7 +40,7 @@ class Update extends CollectionUpdate
->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}')
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: null,
group: 'tables',
name: self::getName(),
description: '/docs/references/tablesdb/update-table.md',
auth: [AuthType::KEY],
@@ -36,7 +36,7 @@ class XList extends CollectionXList
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('sdk', new Method(
namespace: $this->getSdkNamespace(),
group: null,
group: 'tables',
name: self::getName(),
description: '/docs/references/tablesdb/list-tables.md',
auth: [AuthType::KEY],
@@ -64,14 +64,6 @@ class Databases extends Action
$collection = new Document($payload['table'] ?? $payload['collection'] ?? []);
$database = new Document($payload['database'] ?? []);
Console::info("Processing database operation: \n" . \json_encode([
'type' => $type,
'projectId' => $project->getId(),
'databaseId' => $database->getId(),
'collectionId' => $collection->getId(),
'documentId' => $document->getId(),
], JSON_PRETTY_PRINT));
$log->addTag('projectId', $project->getId());
$log->addTag('type', $type);
@@ -80,7 +80,7 @@ class Create extends Base
->param('body', '', new Text(10485760, 0), 'HTTP body of execution. Default value is empty string.', true)
->param('async', false, new Boolean(true), 'Execute code in the background. Default value is false.', true)
->param('path', '/', new Text(2048), 'HTTP path of execution. Path can include query params. Default value is /', true)
->param('method', 'POST', new Whitelist(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], true), 'HTTP method of execution. Default value is POST.', true)
->param('method', 'POST', new Whitelist(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD'], true), 'HTTP method of execution. Default value is POST.', true)
->param('headers', [], new AnyOf([new Assoc(), new Text(65535)], AnyOf::TYPE_MIXED), 'HTTP headers of execution. Defaults to empty.', true)
->param('scheduledAt', null, new Text(100), 'Scheduled execution time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future with precision in minutes.', true)
->inject('response')
+106 -4
View File
@@ -4,6 +4,7 @@ namespace Appwrite\Platform\Workers;
use Ahc\Jwt\JWT;
use Appwrite\Event\Realtime;
use Appwrite\Event\StatsUsage;
use Exception;
use Utopia\CLI\Console;
use Utopia\Config\Config;
@@ -13,9 +14,14 @@ use Utopia\Database\Exception\Authorization;
use Utopia\Database\Exception\Conflict;
use Utopia\Database\Exception\Restricted;
use Utopia\Database\Exception\Structure;
use Utopia\Database\Validator\Authorization as AuthorizationValidator;
use Utopia\Migration\Destination;
use Utopia\Migration\Destinations\Appwrite as DestinationAppwrite;
use Utopia\Migration\Exception as MigrationException;
use Utopia\Migration\Resource;
use Utopia\Migration\Resources\Database\Database as ResourceDatabase;
use Utopia\Migration\Resources\Database\Row as ResourceRow;
use Utopia\Migration\Resources\Database\Table as ResourceTable;
use Utopia\Migration\Source;
use Utopia\Migration\Sources\Appwrite as SourceAppwrite;
use Utopia\Migration\Sources\CSV;
@@ -45,6 +51,7 @@ class Migrations extends Action
*/
protected array $sourceReport = [];
private string $source;
/**
* @var callable
*/
@@ -69,13 +76,14 @@ class Migrations extends Action
->inject('logError')
->inject('queueForRealtime')
->inject('deviceForImports')
->inject('queueForStatsUsage')
->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 $deviceForImports, StatsUsage $queueForStatsUsage): void
{
$payload = $message->getPayload() ?? [];
$this->deviceForImports = $deviceForImports;
@@ -103,7 +111,7 @@ class Migrations extends Action
return;
}
$this->processMigration($migration, $queueForRealtime);
$this->processMigration($migration, $queueForRealtime, $queueForStatsUsage);
}
/**
@@ -267,7 +275,7 @@ class Migrations extends Action
* @throws \Utopia\Database\Exception
* @throws Exception
*/
protected function processMigration(Document $migration, Realtime $queueForRealtime): void
protected function processMigration(Document $migration, Realtime $queueForRealtime, StatsUsage $queueForStatsUsage): void
{
$project = $this->project;
$projectDocument = $this->dbForPlatform->getDocument('projects', $project->getId());
@@ -301,6 +309,7 @@ class Migrations extends Action
$destination
);
$aggregatedResources = [];
/** Start Transfer */
if (empty($source->getErrors())) {
$migration->setAttribute('stage', 'migrating');
@@ -308,9 +317,40 @@ class Migrations extends Action
$transfer->run(
$migration->getAttribute('resources'),
function () use ($migration, $transfer, $projectDocument, $queueForRealtime) {
function ($resources) use ($migration, $transfer, $projectDocument, $queueForRealtime, &$aggregatedResources) {
$migration->setAttribute('resourceData', json_encode($transfer->getCache()));
$migration->setAttribute('statusCounters', json_encode($transfer->getStatusCounters()));
if (!empty($resources)) {
/**
* @var Resource $resource
*/
$resource = $resources[0];
$count = count($resources);
$databaseId = null;
$tableId = null;
switch ($resource->getName()) {
case ResourceTable::getName():
/** @var ResourceTable $resource */
$databaseId = $resource->getDatabase()->getSequence();
break;
case ResourceRow::getName():
/** @var ResourceRow $resource */
$table = $resource->getTable();
$databaseId = $table->getDatabase()->getSequence();
$tableId = $table->getSequence();
break;
default:
break;
}
$aggregatedResources[] = [
'name' => $resource->getName(),
'count' => $count,
'databaseId' => $databaseId,
'tableId' => $tableId
];
}
$this->updateMigrationDocument($migration, $projectDocument, $queueForRealtime);
},
$migration->getAttribute('resourceId'),
@@ -412,9 +452,71 @@ class Migrations extends Action
}
if ($migration->getAttribute('status', '') === 'completed') {
foreach ($aggregatedResources as $resource) {
$this->processMigrationResourceStats(
$resource,
$queueForStatsUsage,
$projectDocument,
$migration->getAttribute('source'),
$migration->getAttribute('resourceId')
);
}
$destination?->success();
$source?->success();
}
}
}
private function processMigrationResourceStats(array $resources, StatsUsage $queueForStatsUsage, Document $projectDocument, string $source, ?string $resourceId)
{
$resourceName = $resources['name'];
$count = $resources['count'];
$databaseInternalId = $resources['databaseId'];
$tableInternalId = $resources['tableId'];
if ($source === CSV::getName()) {
[$databaseId, $tableId] = explode(':', $resourceId);
$database = AuthorizationValidator::skip(fn () => $this->dbForProject->getDocument('databases', $databaseId));
$table = AuthorizationValidator::skip(fn () => $this->dbForProject->getDocument('database_' . $database->getSequence(), $tableId));
$databaseInternalId = (int) $database->getSequence();
$tableInternalId = (int) $table->getSequence();
}
switch ($resourceName) {
case ResourceDatabase::getName():
$queueForStatsUsage->addMetric(METRIC_DATABASES, $count);
break;
case ResourceTable::getName():
$queueForStatsUsage
->addMetric(METRIC_COLLECTIONS, $count)
->addMetric(
str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_COLLECTIONS),
$count
);
break;
case ResourceRow::getName():
$queueForStatsUsage
->addMetric(
str_replace(
['{databaseInternalId}','{collectionInternalId}'],
[$databaseInternalId, $tableInternalId],
METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS
),
$count
)
->addMetric(
str_replace('{databaseInternalId}', $databaseInternalId, METRIC_DATABASE_ID_DOCUMENTS),
$count
);
break;
default:
break;
}
$queueForStatsUsage->setProject($projectDocument)->trigger();
$queueForStatsUsage->reset();
}
}
@@ -221,7 +221,7 @@ class OpenAPI3 extends Format
'required' => [],
'responses' => [],
'description' => ($desc) ? \file_get_contents($desc) : '',
'demo' => Template::fromCamelCaseToDash($namespace) . '/' . Template::fromCamelCaseToDash($methodObj->getMethodName()) . '.md',
'demo' => \strtolower($namespace) . '/' . Template::fromCamelCaseToDash($methodObj->getMethodName()) . '.md',
];
// add deprecation only if method has it!
@@ -230,7 +230,7 @@ class Swagger2 extends Format
'required' => [],
'responses' => [],
'description' => ($desc) ? \file_get_contents($desc) : '',
'demo' => Template::fromCamelCaseToDash($namespace) . '/' . Template::fromCamelCaseToDash($methodObj->getMethodName()) . '.md',
'demo' => \strtolower($namespace) . '/' . Template::fromCamelCaseToDash($methodObj->getMethodName()) . '.md',
];
// add deprecation only if method has it!
+52 -30
View File
@@ -32,59 +32,67 @@ class V20 extends Filter
*/
protected function manageSelectQueries(array $content): array
{
$hasWildcard = false;
if (!isset($content['queries'])) {
$hasWildcard = true;
// only query, make it json encoded!
$content['queries'] = [Query::select(['*'])->toString()];
$content['queries'] = [];
}
// Handle case where queries is an array but empty
if (\is_array($content['queries'])) {
$content['queries'] = \array_filter($content['queries'], function ($q) {
if (\is_object($q) && empty((array)$q)) {
return false;
}
if (\is_string($q) && \trim($q) === '') {
return false;
}
if (empty($q)) {
return false;
}
return true;
});
}
try {
$parsed = Query::parseQueries($content['queries']);
} catch (QueryException) {
// don't crash!
return $content;
}
$selections = Query::groupByType($parsed)['selections'] ?? [];
// If there are no select queries at all, add wildcard
if (empty($selections)) {
$hasWildcard = true;
$parsed[] = Query::select(['*']);
} elseif (!$hasWildcard) {
// check if any select includes a wildcard as we added one above
// Check if we need to add wildcard + relationships
// This happens when:
// 1. No select queries exist, OR
// 2. A wildcard select exists
$needsRelationships = empty($selections);
if (!$needsRelationships) {
foreach ($selections as $select) {
if (\in_array('*', $select->getValues(), true)) {
$hasWildcard = true;
$needsRelationships = true;
break;
}
}
}
/**
* Add `keys.*` for all model types!
* Add wildcard and relationship selects for backward compatibility
*/
if ($hasWildcard) {
if ($needsRelationships) {
$relatedKeys = $this->getRelatedCollectionKeys();
$selects = \array_values(\array_unique(\array_merge(['*'], $relatedKeys)));
if (! empty($relatedKeys)) {
$selects = \array_values(\array_unique(\array_merge(['*'], $relatedKeys)));
// Remove any existing select queries
$parsed = \array_filter(
$parsed,
fn ($query) => $query->getMethod() !== Query::TYPE_SELECT
);
// remove previous select queries
$parsed = \array_filter(
$parsed,
fn ($query) => $query->getMethod() !== Query::TYPE_SELECT
);
// add wildcard + relationship(s) selects
$parsed[] = Query::select($selects);
}
// Add wildcard + relationship(s) selects
$parsed[] = Query::select($selects);
}
$resolvedQueries = [];
foreach ($parsed as $query) {
// make em json encoded!
$resolvedQueries[] = $query->toString();
}
@@ -95,12 +103,15 @@ class V20 extends Filter
/**
* Returns all relationship attribute keys in `key.*` format for use with `Query::select`.
* Recursively includes nested relationships up to 3 levels deep.
* Prevents infinite loops by tracking all visited collections in the current path.
*/
private function getRelatedCollectionKeys(
?string $databaseId = null,
?string $collectionId = null,
?string $prefix = null,
int $depth = 1,
array $visited = []
): array {
$databaseId ??= $this->getParamValue('databaseId');
$collectionId ??= $this->getParamValue('collectionId');
@@ -113,6 +124,13 @@ class V20 extends Filter
return [];
}
// Check if we've already visited this collection in the current path to prevent cycles
if (in_array($collectionId, $visited)) {
return [];
}
$visited[] = $collectionId;
$dbForProject = $this->getDbForProject();
if ($dbForProject === null) {
return [];
@@ -144,20 +162,24 @@ class V20 extends Filter
$key = $attr['key'];
$fullKey = $prefix ? $prefix . '.' . $key : $key;
$relatedCollectionId = $attr['relatedCollection'] ?? null;
// Skip this relationship entirely if it points to an already visited collection
if ($relatedCollectionId && in_array($relatedCollectionId, $visited)) {
continue;
}
// Add the wildcard select for this relationship
$relationshipKeys[] = $fullKey . '.*';
// Get the related collection for nested relationships
$relatedCollectionId = $attr['relatedCollection'] ?? null;
// Continue recursively if we have a related collection
if ($relatedCollectionId) {
// Recursively get nested relationship keys
$nestedKeys = $this->getRelatedCollectionKeys(
$databaseId,
$relatedCollectionId,
$fullKey,
$depth + 1,
$visited
);
$relationshipKeys = \array_merge($relationshipKeys, $nestedKeys);