Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 59 additions & 7 deletions src/Migration/Destination.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

abstract class Destination extends Target
{
private ?ResourceSelector $resourceSelector = null;

/**
* Source
*/
Expand Down Expand Up @@ -34,14 +36,64 @@ public function run(
string $rootResourceId = '',
string $rootResourceType = '',
): void {
$this->source->run(
$resources,
function (array $resources) use ($callback) {
$this->import($resources, $callback);
},
$rootResourceId,
$rootResourceType,
$import = function (array $resources) use ($callback) {
$this->import($resources, $callback);
};

if ($this->resourceSelector !== null) {
$this->source->runWithResourceSelector(
$resources,
$import,
$this->resourceSelector->resourceId,
$this->resourceSelector->resourceInternalId,
$this->resourceSelector->resourceType,
$this->resourceSelector->parentResourceId,
$this->resourceSelector->parentResourceInternalId,
$this->resourceSelector->parentResourceType,
);

return;
}

$this->source->run($resources, $import, $rootResourceId, $rootResourceType);
}

/**
* Transfer resources using Appwrite's canonical resource relation fields.
*
* @param array<string> $resources Resources to transfer
* @param callable(array<Resource>): void $callback to run after transfer
*/
public function runWithResourceSelector(
array $resources,
callable $callback,
string $resourceId,
string $resourceInternalId,
string $resourceType,
string $parentResourceId,
string $parentResourceInternalId,
string $parentResourceType,
): void {
$previousResourceSelector = $this->resourceSelector;
$this->resourceSelector = new ResourceSelector(
$resourceId,
$resourceInternalId,
$resourceType,
$parentResourceId,
$parentResourceInternalId,
$parentResourceType,
);

try {
$this->run(
$resources,
$callback,
$this->resourceSelector->getScopeId(),
$this->resourceSelector->getScopeType(),
);
} finally {
$this->resourceSelector = $previousResourceSelector;
}
}

/**
Expand Down
18 changes: 16 additions & 2 deletions src/Migration/Destinations/Appwrite.php
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,9 @@ class Appwrite extends Destination
*/
private array $provisioningDatabases = [];

/** Whether the destination project's database metadata supports lifecycle status. */
private ?bool $databaseStatusSupported = null;

/**
* @param string $project
* @param string $endpoint
Expand Down Expand Up @@ -241,11 +244,22 @@ private function getSupportForDatabaseStatus(): bool
// $source is a non-nullable typed property with no default; it is only set when the
// destination runs through Transfer. Guard so direct createDatabase() calls (e.g. tests)
// don't hit "must not be accessed before initialization".
if (! isset($this->source)) {
if (! isset($this->source) || ! $this->getSource()->supportsDatabaseStatus()) {
return false;
}

return $this->getSource()->supportsDatabaseStatus();
if ($this->databaseStatusSupported !== null) {
return $this->databaseStatusSupported;
}

$collection = $this->dbForProject->getCollection(self::META_DATABASES);
foreach ($collection->getAttribute('attributes', []) as $attribute) {
if ($attribute->getId() === 'status') {
return $this->databaseStatusSupported = true;
}
}

return $this->databaseStatusSupported = false;
}

/** Orphan cleanup runs only after a successful migration — a mid-run throw preserves the destination as-is. */
Expand Down
31 changes: 30 additions & 1 deletion src/Migration/Destinations/CSV.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class CSV extends Destination
{
protected Device $deviceForFiles;
protected string $resourceId;
private ?string $resourceChildId = null;
protected string $directory;
protected string $outputFile;
protected Local $local;
Expand Down Expand Up @@ -54,6 +55,34 @@ public function __construct(
}
}

public static function fromResourceIds(
Device $deviceForFiles,
string $databaseId,
string $tableId,
string $directory,
string $filename,
array $allowedColumns = [],
string $delimiter = ',',
string $enclosure = '"',
string $escape = '"',
bool $includeHeaders = true,
): self {
$destination = new self(
deviceForFiles: $deviceForFiles,
resourceId: $databaseId,
directory: $directory,
filename: $filename,
allowedColumns: $allowedColumns,
delimiter: $delimiter,
enclosure: $enclosure,
escape: $escape,
includeHeaders: $includeHeaders,
);
$destination->resourceChildId = $tableId;

return $destination;
}

public static function getName(): string
{
return 'CSV';
Expand Down Expand Up @@ -193,7 +222,7 @@ public function shutdown(): void
UtopiaResource::TYPE_ROW,
Transfer::GROUP_DATABASES,
'Error cleaning up: ' . $this->local->getRoot(),
$this->resourceId
$this->resourceChildId ?? $this->resourceId
));
}
}
Expand Down
23 changes: 22 additions & 1 deletion src/Migration/Destinations/JSON.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class JSON extends Destination
{
protected Device $deviceForFiles;
protected string $resourceId;
private ?string $resourceChildId = null;
protected string $directory;
protected string $outputFile;
protected Local $local;
Expand Down Expand Up @@ -56,6 +57,26 @@ public function __construct(
}
}

public static function fromResourceIds(
Device $deviceForFiles,
string $databaseId,
string $tableId,
string $directory,
string $filename,
array $allowedColumns = [],
): self {
$destination = new self(
deviceForFiles: $deviceForFiles,
resourceId: $databaseId,
directory: $directory,
filename: $filename,
allowedColumns: $allowedColumns,
);
$destination->resourceChildId = $tableId;

return $destination;
}

public static function getName(): string
{
return 'JSON';
Expand Down Expand Up @@ -217,7 +238,7 @@ public function shutdown(): void
UtopiaResource::TYPE_ROW,
Transfer::GROUP_DATABASES,
'Error cleaning up: ' . $this->local->getRoot(),
$this->resourceId
$this->resourceChildId ?? $this->resourceId
));
}
}
Expand Down
27 changes: 27 additions & 0 deletions src/Migration/ResourceSelector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

namespace Utopia\Migration;

final readonly class ResourceSelector
{
public function __construct(
public string $resourceId,
public string $resourceInternalId,
public string $resourceType,
public string $parentResourceId,
public string $parentResourceInternalId,
public string $parentResourceType,
) {
}

public function getScopeId(): string
{
return $this->parentResourceId ?: $this->resourceId;
}

public function getScopeType(): string
{
return $this->parentResourceType ?: $this->resourceType;
}

}
37 changes: 36 additions & 1 deletion src/Migration/Source.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ public function callback(array $resources): void
*/
public function run(array $resources, callable $callback, string $rootResourceId = '', string $rootResourceType = ''): void
{
$previousRootResourceId = $this->rootResourceId;
$previousRootResourceType = $this->rootResourceType;
$this->rootResourceId = $rootResourceId;
$this->rootResourceType = $rootResourceType;

Expand All @@ -115,7 +117,40 @@ public function run(array $resources, callable $callback, string $rootResourceId
$this->cache->addAll($prunedResources);
};

$this->exportResources($resources);
try {
$this->exportResources($resources);
} finally {
$this->rootResourceId = $previousRootResourceId;
$this->rootResourceType = $previousRootResourceType;
}
}

/**
* Transfer resources using Appwrite's canonical resource relation fields.
*
* @param array<string> $resources Resources to transfer
* @param callable $callback Callback to run after transfer
*/
public function runWithResourceSelector(
array $resources,
callable $callback,
string $resourceId,
string $resourceInternalId,
string $resourceType,
string $parentResourceId,
string $parentResourceInternalId,
string $parentResourceType,
): void {
$selector = new ResourceSelector(
$resourceId,
$resourceInternalId,
$resourceType,
$parentResourceId,
$parentResourceInternalId,
$parentResourceType,
);

$this->run($resources, $callback, $selector->getScopeId(), $selector->getScopeType());
}

/**
Expand Down
Loading
Loading