Skip to content
Open
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
49 changes: 49 additions & 0 deletions src/Migration/Destinations/Appwrite.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
use Utopia\Migration\Resources\Functions\Deployment;
use Utopia\Migration\Resources\Functions\EnvVar;
use Utopia\Migration\Resources\Functions\Func;
use Utopia\Migration\Resources\Integrations\ApiKey;
use Utopia\Migration\Resources\Integrations\Platform;
use Utopia\Migration\Resources\Messaging\Message;
use Utopia\Migration\Resources\Messaging\Provider;
Expand Down Expand Up @@ -275,6 +276,7 @@ public static function getSupportedResources(): array

// Integrations
Resource::TYPE_PLATFORM,
Resource::TYPE_API_KEY,

// Backups
Resource::TYPE_BACKUP_POLICY,
Expand Down Expand Up @@ -3074,6 +3076,10 @@ public function importIntegrationsResource(Resource $resource): Resource
/** @var Platform $resource */
$this->createPlatform($resource);
break;
case Resource::TYPE_API_KEY:
/** @var ApiKey $resource */
$this->createApiKey($resource);
break;
}

if ($resource->getStatus() !== Resource::STATUS_SKIPPED) {
Expand Down Expand Up @@ -3126,6 +3132,49 @@ protected function createPlatform(Platform $resource): bool
return true;
}

protected function createApiKey(ApiKey $resource): bool
{
$existing = $this->dbForPlatform->findOne('keys', [
Query::equal('resourceType', ['projects']),
Query::equal('resourceInternalId', [$this->projectInternalId]),
Query::equal('name', [$resource->getApiKeyName()]),
]);

if ($existing !== false && !$existing->isEmpty()) {
$resource->setStatus(Resource::STATUS_SKIPPED, 'API key already exists');
return false;
}

$createdAt = $this->normalizeDateTime($resource->getCreatedAt());
$updatedAt = $this->normalizeDateTime($resource->getUpdatedAt(), $createdAt);
$expire = $resource->getExpire();

try {
$this->dbForPlatform->createDocument('keys', new UtopiaDocument([
'$id' => ID::unique(),
'$permissions' => $resource->getPermissions(),
'resourceInternalId' => $this->projectInternalId,
'resourceId' => $this->project,
'resourceType' => 'projects',
'name' => $resource->getApiKeyName(),
'scopes' => $resource->getScopes(),
'expire' => empty($expire) ? null : $expire,
'sdks' => $resource->getSdks(),
'accessedAt' => null,
'secret' => 'standard_' . \bin2hex(\random_bytes(128)),
'$createdAt' => $createdAt,
'$updatedAt' => $updatedAt,
]));
} catch (DuplicateException) {
$resource->setStatus(Resource::STATUS_SKIPPED, 'API key already exists');
return false;
}

$this->dbForPlatform->purgeCachedDocument('projects', $this->project);

return true;
}

private function validateFieldsForIndexes(Index $resource, UtopiaDocument $table, array &$lengths)
{
/**
Expand Down
2 changes: 2 additions & 0 deletions src/Migration/Resource.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ abstract class Resource implements \JsonSerializable

// Integrations
public const TYPE_PLATFORM = 'platform';
public const TYPE_API_KEY = 'api-key';
public const TYPE_SUBSCRIBER = 'subscriber';

public const TYPE_MESSAGE = 'message';
Expand Down Expand Up @@ -112,6 +113,7 @@ abstract class Resource implements \JsonSerializable
self::TYPE_TEAM,
self::TYPE_MEMBERSHIP,
self::TYPE_PLATFORM,
self::TYPE_API_KEY,
self::TYPE_PROVIDER,
self::TYPE_TOPIC,
self::TYPE_SUBSCRIBER,
Expand Down
110 changes: 110 additions & 0 deletions src/Migration/Resources/Integrations/ApiKey.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<?php

namespace Utopia\Migration\Resources\Integrations;

use Utopia\Migration\Resource;
use Utopia\Migration\Transfer;

class ApiKey extends Resource
{
/**
* @param string $id
* @param string $name
* @param array<string> $scopes
* @param string $expire
* @param string $accessedAt
* @param array<string> $sdks
* @param string $createdAt
* @param string $updatedAt
*/
public function __construct(
string $id,
private readonly string $name,
private readonly array $scopes = [],
private readonly string $expire = '',
private readonly string $accessedAt = '',
private readonly array $sdks = [],
string $createdAt = '',
string $updatedAt = '',
) {
$this->id = $id;
$this->createdAt = $createdAt;
$this->updatedAt = $updatedAt;
}

/**
* @param array<string, mixed> $array
* @return self
*/
public static function fromArray(array $array): self
{
return new self(
$array['id'],
$array['name'],
$array['scopes'] ?? [],
$array['expire'] ?? '',
$array['accessedAt'] ?? '',
$array['sdks'] ?? [],
createdAt: $array['createdAt'] ?? '',
updatedAt: $array['updatedAt'] ?? '',
);
}

/**
* @return array<string, mixed>
*/
public function jsonSerialize(): array
{
return [
'id' => $this->id,
'name' => $this->name,
'scopes' => $this->scopes,
'expire' => $this->expire,
'accessedAt' => $this->accessedAt,
'sdks' => $this->sdks,
'createdAt' => $this->createdAt,
'updatedAt' => $this->updatedAt,
];
}

public static function getName(): string
{
return Resource::TYPE_API_KEY;
}

public function getGroup(): string
{
return Transfer::GROUP_INTEGRATIONS;
}

public function getApiKeyName(): string
{
return $this->name;
}

/**
* @return array<string>
*/
public function getScopes(): array
{
return $this->scopes;
}

public function getExpire(): string
{
return $this->expire;
}

public function getAccessedAt(): string
{
return $this->accessedAt;
}

/**
* @return array<string>
*/
public function getSdks(): array
{
return $this->sdks;
}
}
78 changes: 78 additions & 0 deletions src/Migration/Sources/Appwrite.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
use Utopia\Migration\Resources\Functions\Deployment;
use Utopia\Migration\Resources\Functions\EnvVar;
use Utopia\Migration\Resources\Functions\Func;
use Utopia\Migration\Resources\Integrations\ApiKey;
use Utopia\Migration\Resources\Integrations\Platform;
use Utopia\Migration\Resources\Messaging\Message;
use Utopia\Migration\Resources\Messaging\Provider;
Expand Down Expand Up @@ -207,6 +208,7 @@ public static function getSupportedResources(): array

// Integrations
Resource::TYPE_PLATFORM,
Resource::TYPE_API_KEY,

// Backups
Resource::TYPE_BACKUP_POLICY,
Expand Down Expand Up @@ -2239,6 +2241,19 @@ private function reportIntegrations(array $resources, array &$report, array $res
$report[Resource::TYPE_PLATFORM] = 0;
}
}

if (\in_array(Resource::TYPE_API_KEY, $resources)) {
$keyQueries = $this->buildQueries(
resourceType: Resource::TYPE_API_KEY,
resourceIds: $resourceIds,
limit: 1
);
try {
$report[Resource::TYPE_API_KEY] = $this->project->listKeys($keyQueries)->total;
} catch (\Throwable) {
$report[Resource::TYPE_API_KEY] = 0;
}
}
}

/**
Expand Down Expand Up @@ -2284,6 +2299,20 @@ protected function exportGroupIntegrations(int $batchSize, array $resources): vo
));
}
}

if (\in_array(Resource::TYPE_API_KEY, $resources)) {
try {
$this->exportApiKeys($batchSize);
} catch (\Throwable $e) {
$this->addError(new Exception(
Resource::TYPE_API_KEY,
Transfer::GROUP_INTEGRATIONS,
message: $e->getMessage(),
code: $e->getCode(),
previous: $e
));
}
}
}

/**
Expand Down Expand Up @@ -2367,6 +2396,55 @@ private function exportPlatforms(int $batchSize): void
}
}

/**
* @throws AppwriteException
*/
private function exportApiKeys(int $batchSize): void
{
$lastId = null;

while (true) {
$queries = [Query::limit($batchSize)];

if ($this->rootResourceId !== '' && $this->rootResourceType === Resource::TYPE_API_KEY) {
$queries[] = Query::equal('$id', $this->rootResourceId);
$queries[] = Query::limit(1);
}

if ($lastId !== null) {
$queries[] = Query::cursorAfter($lastId);
}

$response = $this->project->listKeys($queries);
if ($response->total === 0) {
break;
}

$apiKeys = [];

foreach ($response->keys as $key) {
$apiKeys[] = new ApiKey(
$key->id,
$key->name,
$key->scopes,
$key->expire,
$key->accessedAt,
$key->sdks,
createdAt: $key->createdAt,
updatedAt: $key->updatedAt,
);
Comment thread
greptile-apps[bot] marked this conversation as resolved.

$lastId = $key->id;
}

$this->callback($apiKeys);

if (\count($response->keys) < $batchSize) {
break;
}
}
}

/**
* eg.,documents/attributes
* @param string $databaseType
Expand Down
3 changes: 3 additions & 0 deletions src/Migration/Transfer.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ class Transfer

public const GROUP_INTEGRATIONS_RESOURCES = [
Resource::TYPE_PLATFORM,
Resource::TYPE_API_KEY,
];
public const GROUP_DOCUMENTSDB_RESOURCES = [
Resource::TYPE_DATABASE_DOCUMENTSDB,
Expand Down Expand Up @@ -129,6 +130,7 @@ class Transfer

// Integrations
Resource::TYPE_PLATFORM,
Resource::TYPE_API_KEY,

// legacy
Resource::TYPE_DOCUMENT,
Expand All @@ -146,6 +148,7 @@ class Transfer
Resource::TYPE_USER,
Resource::TYPE_TEAM,
Resource::TYPE_PLATFORM,
Resource::TYPE_API_KEY,
Resource::TYPE_PROVIDER,
Resource::TYPE_TOPIC,
Resource::TYPE_MESSAGE,
Expand Down
1 change: 1 addition & 0 deletions tests/Migration/Unit/Adapters/MockDestination.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public static function getSupportedResources(): array
Resource::TYPE_TEAM,
Resource::TYPE_MEMBERSHIP,
Resource::TYPE_PLATFORM,
Resource::TYPE_API_KEY,
Resource::TYPE_PROVIDER,
Resource::TYPE_TOPIC,
Resource::TYPE_SUBSCRIBER,
Expand Down
1 change: 1 addition & 0 deletions tests/Migration/Unit/Adapters/MockSource.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ public static function getSupportedResources(): array
Resource::TYPE_TEAM,
Resource::TYPE_MEMBERSHIP,
Resource::TYPE_PLATFORM,
Resource::TYPE_API_KEY,
Resource::TYPE_PROVIDER,
Resource::TYPE_TOPIC,
Resource::TYPE_SUBSCRIBER,
Expand Down
Loading