2021-01-14 05:51:02 +13:00
|
|
|
<?php
|
|
|
|
|
|
|
|
namespace Appwrite\Migration;
|
|
|
|
|
2021-01-25 21:26:52 +13:00
|
|
|
use Swoole\Runtime;
|
2022-01-19 00:05:04 +13:00
|
|
|
use Utopia\Database\Document;
|
|
|
|
use Utopia\Database\Database;
|
2022-08-12 11:53:52 +12:00
|
|
|
use Utopia\Database\Query;
|
2021-01-15 01:05:49 +13:00
|
|
|
use Utopia\CLI\Console;
|
2022-01-19 00:05:04 +13:00
|
|
|
use Utopia\Config\Config;
|
2022-02-07 12:24:49 +13:00
|
|
|
use Exception;
|
2022-05-14 00:49:31 +12:00
|
|
|
use Utopia\App;
|
2022-08-14 22:33:36 +12:00
|
|
|
use Utopia\Database\ID;
|
2022-05-14 00:49:31 +12:00
|
|
|
use Utopia\Database\Validator\Authorization;
|
2021-01-14 05:51:02 +13:00
|
|
|
|
2022-09-09 04:46:18 +12:00
|
|
|
Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
|
|
|
|
|
2021-01-14 05:51:02 +13:00
|
|
|
abstract class Migration
|
|
|
|
{
|
2021-01-19 03:43:55 +13:00
|
|
|
/**
|
|
|
|
* @var int
|
|
|
|
*/
|
2022-05-19 21:53:06 +12:00
|
|
|
protected int $limit = 100;
|
2021-12-09 06:50:04 +13:00
|
|
|
|
|
|
|
/**
|
2022-01-19 00:05:04 +13:00
|
|
|
* @var Document
|
2021-12-09 06:50:04 +13:00
|
|
|
*/
|
2022-01-19 00:05:04 +13:00
|
|
|
protected Document $project;
|
2021-01-15 03:09:48 +13:00
|
|
|
|
2021-01-19 03:43:55 +13:00
|
|
|
/**
|
2022-01-19 00:05:04 +13:00
|
|
|
* @var Database
|
2021-01-19 03:43:55 +13:00
|
|
|
*/
|
2022-01-19 00:05:04 +13:00
|
|
|
protected Database $projectDB;
|
2021-01-19 03:43:55 +13:00
|
|
|
|
|
|
|
/**
|
2022-01-19 00:05:04 +13:00
|
|
|
* @var Database
|
2021-01-19 03:43:55 +13:00
|
|
|
*/
|
2022-01-19 00:05:04 +13:00
|
|
|
protected Database $consoleDB;
|
2021-01-15 01:05:49 +13:00
|
|
|
|
2021-07-02 21:09:02 +12:00
|
|
|
/**
|
|
|
|
* @var array
|
|
|
|
*/
|
|
|
|
public static array $versions = [
|
2022-09-14 19:12:17 +12:00
|
|
|
'1.0.0-RC1' => 'V15',
|
2022-09-15 07:03:00 +12:00
|
|
|
'1.0.0' => 'V15',
|
|
|
|
'1.0.1' => 'V15',
|
2022-11-15 10:42:48 +13:00
|
|
|
'1.0.3' => 'V15',
|
2022-11-16 01:59:35 +13:00
|
|
|
'1.1.0' => 'V16',
|
2021-07-02 21:09:02 +12:00
|
|
|
];
|
|
|
|
|
2022-02-18 07:24:50 +13:00
|
|
|
/**
|
|
|
|
* @var array
|
|
|
|
*/
|
|
|
|
protected array $collections;
|
|
|
|
|
|
|
|
public function __construct()
|
|
|
|
{
|
2022-05-14 00:49:31 +12:00
|
|
|
Authorization::disable();
|
|
|
|
Authorization::setDefaultStatus(false);
|
2022-09-07 20:43:05 +12:00
|
|
|
|
2022-02-18 07:24:50 +13:00
|
|
|
$this->collections = array_merge([
|
|
|
|
'_metadata' => [
|
2022-08-14 22:33:36 +12:00
|
|
|
'$id' => ID::custom('_metadata'),
|
2022-05-14 00:49:31 +12:00
|
|
|
'$collection' => Database::METADATA
|
2022-02-18 07:24:50 +13:00
|
|
|
],
|
|
|
|
'audit' => [
|
2022-08-14 22:33:36 +12:00
|
|
|
'$id' => ID::custom('audit'),
|
2022-05-14 00:49:31 +12:00
|
|
|
'$collection' => Database::METADATA
|
2022-02-18 07:24:50 +13:00
|
|
|
],
|
|
|
|
'abuse' => [
|
2022-08-14 22:33:36 +12:00
|
|
|
'$id' => ID::custom('abuse'),
|
2022-05-14 00:49:31 +12:00
|
|
|
'$collection' => Database::METADATA
|
2022-02-18 07:24:50 +13:00
|
|
|
]
|
|
|
|
], Config::getParam('collections', []));
|
|
|
|
}
|
|
|
|
|
2021-01-15 01:05:49 +13:00
|
|
|
/**
|
|
|
|
* Set project for migration.
|
2021-10-06 20:53:33 +13:00
|
|
|
*
|
2022-01-19 00:05:04 +13:00
|
|
|
* @param Document $project
|
|
|
|
* @param Database $projectDB
|
|
|
|
* @param Database $oldConsoleDB
|
2021-10-06 20:53:33 +13:00
|
|
|
*
|
2021-12-09 06:50:04 +13:00
|
|
|
* @return self
|
2021-01-15 01:05:49 +13:00
|
|
|
*/
|
2022-01-19 00:05:04 +13:00
|
|
|
public function setProject(Document $project, Database $projectDB, Database $consoleDB): self
|
2021-01-15 01:05:49 +13:00
|
|
|
{
|
|
|
|
$this->project = $project;
|
2022-01-19 00:05:04 +13:00
|
|
|
$this->projectDB = $projectDB;
|
2022-02-18 07:24:50 +13:00
|
|
|
$this->projectDB->setNamespace('_' . $this->project->getId());
|
2021-12-09 06:50:04 +13:00
|
|
|
|
2022-01-19 00:05:04 +13:00
|
|
|
$this->consoleDB = $consoleDB;
|
2021-12-09 06:50:04 +13:00
|
|
|
|
2021-01-15 01:05:49 +13:00
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Iterates through every document.
|
2021-10-06 20:53:33 +13:00
|
|
|
*
|
2021-01-19 03:43:55 +13:00
|
|
|
* @param callable $callback
|
2021-01-15 01:05:49 +13:00
|
|
|
*/
|
2021-01-21 22:57:15 +13:00
|
|
|
public function forEachDocument(callable $callback): void
|
2021-01-15 01:05:49 +13:00
|
|
|
{
|
2022-02-18 07:24:50 +13:00
|
|
|
foreach ($this->collections as $collection) {
|
2022-05-24 02:54:50 +12:00
|
|
|
if ($collection['$collection'] !== Database::METADATA) {
|
2022-06-23 01:52:21 +12:00
|
|
|
continue;
|
2022-05-24 02:54:50 +12:00
|
|
|
}
|
2022-06-28 04:22:29 +12:00
|
|
|
|
2022-01-19 00:05:04 +13:00
|
|
|
Console::log('Migrating Collection ' . $collection['$id'] . ':');
|
|
|
|
|
2022-09-09 04:46:18 +12:00
|
|
|
\Co\run(function (array $collection, callable $callback) {
|
|
|
|
foreach ($this->documentsIterator($collection['$id']) as $document) {
|
|
|
|
go(function (Document $document, callable $callback) {
|
|
|
|
if (empty($document->getId()) || empty($document->getCollection())) {
|
|
|
|
return;
|
|
|
|
}
|
2022-01-19 00:05:04 +13:00
|
|
|
|
2022-09-09 04:46:18 +12:00
|
|
|
$old = $document->getArrayCopy();
|
|
|
|
$new = call_user_func($callback, $document);
|
2022-01-19 00:05:04 +13:00
|
|
|
|
2022-09-09 04:46:18 +12:00
|
|
|
if (is_null($new) || !self::hasDifference($new->getArrayCopy(), $old)) {
|
|
|
|
return;
|
|
|
|
}
|
2021-01-25 21:26:52 +13:00
|
|
|
|
2022-09-09 04:46:18 +12:00
|
|
|
try {
|
|
|
|
$new = $this->projectDB->updateDocument($document->getCollection(), $document->getId(), $document);
|
|
|
|
} catch (\Throwable $th) {
|
|
|
|
Console::error('Failed to update document: ' . $th->getMessage());
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}, $document, $callback);
|
|
|
|
}
|
|
|
|
}, $collection, $callback);
|
|
|
|
}
|
|
|
|
}
|
2021-01-25 21:26:52 +13:00
|
|
|
|
2022-09-09 04:46:18 +12:00
|
|
|
/**
|
2022-09-10 00:44:04 +12:00
|
|
|
* Provides an iterator for all documents on a collection.
|
|
|
|
*
|
|
|
|
* @param string $collectionId
|
2022-09-09 04:46:18 +12:00
|
|
|
* @return iterable<Document>
|
2022-09-10 00:44:04 +12:00
|
|
|
* @throws \Exception
|
2022-09-09 04:46:18 +12:00
|
|
|
*/
|
|
|
|
public function documentsIterator(string $collectionId): iterable
|
|
|
|
{
|
|
|
|
$sum = 0;
|
|
|
|
$nextDocument = null;
|
|
|
|
$collectionCount = $this->projectDB->count($collectionId);
|
|
|
|
|
|
|
|
do {
|
|
|
|
$queries = [Query::limit($this->limit)];
|
|
|
|
if ($nextDocument !== null) {
|
|
|
|
$queries[] = Query::cursorAfter($nextDocument);
|
|
|
|
}
|
|
|
|
$documents = $this->projectDB->find($collectionId, $queries);
|
|
|
|
$count = count($documents);
|
|
|
|
$sum += $count;
|
2021-01-25 21:26:52 +13:00
|
|
|
|
2022-09-09 04:46:18 +12:00
|
|
|
Console::log($sum . ' / ' . $collectionCount);
|
|
|
|
foreach ($documents as $document) {
|
|
|
|
yield $document;
|
|
|
|
}
|
2022-01-19 00:05:04 +13:00
|
|
|
|
2022-09-09 04:46:18 +12:00
|
|
|
if ($count !== $this->limit) {
|
|
|
|
$nextDocument = null;
|
|
|
|
} else {
|
|
|
|
$nextDocument = end($documents);
|
|
|
|
}
|
|
|
|
} while (!is_null($nextDocument));
|
2021-01-15 01:05:49 +13:00
|
|
|
}
|
|
|
|
|
2021-12-09 06:50:04 +13:00
|
|
|
/**
|
|
|
|
* Checks 2 arrays for differences.
|
2022-05-19 23:53:11 +12:00
|
|
|
*
|
|
|
|
* @param array $array1
|
|
|
|
* @param array $array2
|
|
|
|
* @return bool
|
2021-12-09 06:50:04 +13:00
|
|
|
*/
|
2022-05-20 00:17:18 +12:00
|
|
|
public static function hasDifference(array $array1, array $array2): bool
|
2021-10-06 20:53:33 +13:00
|
|
|
{
|
2022-05-19 21:29:01 +12:00
|
|
|
foreach ($array1 as $key => $value) {
|
|
|
|
if (is_array($value)) {
|
|
|
|
if (!isset($array2[$key]) || !is_array($array2[$key])) {
|
|
|
|
return true;
|
|
|
|
} else {
|
2022-05-20 00:17:18 +12:00
|
|
|
if (self::hasDifference($value, $array2[$key])) {
|
2022-05-19 21:29:01 +12:00
|
|
|
return true;
|
|
|
|
}
|
2021-04-16 19:08:42 +12:00
|
|
|
}
|
2022-05-19 21:29:01 +12:00
|
|
|
} elseif (!array_key_exists($key, $array2) || $array2[$key] !== $value) {
|
|
|
|
return true;
|
2021-04-16 19:08:42 +12:00
|
|
|
}
|
|
|
|
}
|
2021-10-16 05:11:20 +13:00
|
|
|
|
2022-05-19 21:29:01 +12:00
|
|
|
return false;
|
2021-04-16 19:08:42 +12:00
|
|
|
}
|
|
|
|
|
2022-05-14 00:49:31 +12:00
|
|
|
/**
|
|
|
|
* Creates colletion from the config collection.
|
|
|
|
*
|
|
|
|
* @param string $id
|
|
|
|
* @param string|null $name
|
|
|
|
* @return void
|
|
|
|
* @throws \Throwable
|
|
|
|
*/
|
|
|
|
protected function createCollection(string $id, string $name = null): void
|
|
|
|
{
|
|
|
|
$name ??= $id;
|
|
|
|
|
|
|
|
if (!$this->projectDB->exists(App::getEnv('_APP_DB_SCHEMA', 'appwrite'), $name)) {
|
|
|
|
$attributes = [];
|
|
|
|
$indexes = [];
|
|
|
|
$collection = $this->collections[$id];
|
|
|
|
|
|
|
|
foreach ($collection['attributes'] as $attribute) {
|
|
|
|
$attributes[] = new Document([
|
|
|
|
'$id' => $attribute['$id'],
|
|
|
|
'type' => $attribute['type'],
|
|
|
|
'size' => $attribute['size'],
|
|
|
|
'required' => $attribute['required'],
|
|
|
|
'signed' => $attribute['signed'],
|
|
|
|
'array' => $attribute['array'],
|
|
|
|
'filters' => $attribute['filters'],
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($collection['indexes'] as $index) {
|
|
|
|
$indexes[] = new Document([
|
|
|
|
'$id' => $index['$id'],
|
|
|
|
'type' => $index['type'],
|
|
|
|
'attributes' => $index['attributes'],
|
|
|
|
'lengths' => $index['lengths'],
|
|
|
|
'orders' => $index['orders'],
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
$this->projectDB->createCollection($name, $attributes, $indexes);
|
|
|
|
} catch (\Throwable $th) {
|
|
|
|
throw $th;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-21 22:17:18 +12:00
|
|
|
/**
|
|
|
|
* Creates attribute from collections.php
|
|
|
|
*
|
|
|
|
* @param \Utopia\Database\Database $database
|
|
|
|
* @param string $collectionId
|
|
|
|
* @param string $attributeId
|
|
|
|
* @return void
|
|
|
|
* @throws \Exception
|
|
|
|
* @throws \Utopia\Database\Exception\Duplicate
|
|
|
|
* @throws \Utopia\Database\Exception\Limit
|
|
|
|
*/
|
2022-06-23 01:52:21 +12:00
|
|
|
public function createAttributeFromCollection(Database $database, string $collectionId, string $attributeId, string $from = null): void
|
2022-06-21 22:17:18 +12:00
|
|
|
{
|
2022-06-23 01:52:21 +12:00
|
|
|
$from ??= $collectionId;
|
|
|
|
$collection = Config::getParam('collections', [])[$from] ?? null;
|
2022-06-21 22:17:18 +12:00
|
|
|
if (is_null($collection)) {
|
|
|
|
throw new Exception("Collection {$collectionId} not found");
|
|
|
|
}
|
|
|
|
$attributes = $collection['attributes'];
|
|
|
|
|
|
|
|
$attributeKey = array_search($attributeId, array_column($attributes, '$id'));
|
|
|
|
|
2022-06-22 01:46:05 +12:00
|
|
|
if ($attributeKey === false) {
|
2022-06-21 22:17:18 +12:00
|
|
|
throw new Exception("Attribute {$attributeId} not found");
|
|
|
|
}
|
|
|
|
|
|
|
|
$attribute = $attributes[$attributeKey];
|
2022-09-09 04:46:18 +12:00
|
|
|
$filters = $attribute['filters'] ?? [];
|
|
|
|
$default = $attribute['default'] ?? null;
|
2022-06-21 22:17:18 +12:00
|
|
|
|
|
|
|
$database->createAttribute(
|
|
|
|
collection: $collectionId,
|
|
|
|
id: $attributeId,
|
|
|
|
type: $attribute['type'],
|
|
|
|
size: $attribute['size'],
|
|
|
|
required: $attribute['required'] ?? false,
|
2022-09-09 04:46:18 +12:00
|
|
|
default: in_array('json', $filters) ? json_encode($default) : $default,
|
2022-06-21 22:17:18 +12:00
|
|
|
signed: $attribute['signed'] ?? false,
|
|
|
|
array: $attribute['array'] ?? false,
|
|
|
|
format: $attribute['format'] ?? '',
|
|
|
|
formatOptions: $attribute['formatOptions'] ?? [],
|
2022-09-09 04:46:18 +12:00
|
|
|
filters: $filters,
|
2022-06-21 22:17:18 +12:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates index from collections.php
|
2022-09-10 00:44:04 +12:00
|
|
|
*
|
2022-06-21 22:17:18 +12:00
|
|
|
* @param \Utopia\Database\Database $database
|
|
|
|
* @param string $collectionId
|
|
|
|
* @param string $indexId
|
2022-09-09 04:46:18 +12:00
|
|
|
* @param string|null $from
|
2022-06-21 22:17:18 +12:00
|
|
|
* @return void
|
|
|
|
* @throws \Exception
|
|
|
|
* @throws \Utopia\Database\Exception\Duplicate
|
|
|
|
* @throws \Utopia\Database\Exception\Limit
|
|
|
|
*/
|
2022-09-09 04:46:18 +12:00
|
|
|
public function createIndexFromCollection(Database $database, string $collectionId, string $indexId, string $from = null): void
|
2022-06-21 22:17:18 +12:00
|
|
|
{
|
2022-09-09 04:46:18 +12:00
|
|
|
$from ??= $collectionId;
|
2022-06-21 22:17:18 +12:00
|
|
|
$collection = Config::getParam('collections', [])[$collectionId] ?? null;
|
|
|
|
|
|
|
|
if (is_null($collection)) {
|
|
|
|
throw new Exception("Collection {$collectionId} not found");
|
|
|
|
}
|
|
|
|
$indexes = $collection['indexes'];
|
|
|
|
|
|
|
|
$indexKey = array_search($indexId, array_column($indexes, '$id'));
|
|
|
|
|
2022-06-22 05:39:05 +12:00
|
|
|
if ($indexKey === false) {
|
2022-06-21 22:17:18 +12:00
|
|
|
throw new Exception("Attribute {$indexId} not found");
|
|
|
|
}
|
|
|
|
|
|
|
|
$index = $indexes[$indexKey];
|
|
|
|
|
|
|
|
$database->createIndex(
|
|
|
|
collection: $collectionId,
|
|
|
|
id: $indexId,
|
|
|
|
type: $index['type'],
|
|
|
|
attributes: $index['attributes'],
|
|
|
|
lengths: $index['lengths'] ?? [],
|
|
|
|
orders: $index['orders'] ?? []
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-01-15 01:05:49 +13:00
|
|
|
/**
|
|
|
|
* Executes migration for set project.
|
|
|
|
*/
|
|
|
|
abstract public function execute(): void;
|
2021-01-14 05:51:02 +13:00
|
|
|
}
|