mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Merge remote-tracking branch 'upstream/1.8.x' into spatial-type-attributes
This commit is contained in:
@@ -85,4 +85,11 @@ class StatsUsage extends Event
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
public function reset(): Event
|
||||
{
|
||||
$this->metrics = [];
|
||||
parent::reset();
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
+1
-1
@@ -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')
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user