1
0
Fork 0
mirror of synced 2024-06-18 18:54:55 +12:00

Merge pull request #2637 from appwrite/feat-migration-0-13

feat: migration for 0.13
This commit is contained in:
Torsten Dittmann 2022-02-08 11:17:44 +01:00 committed by GitHub
commit 6796aabf9c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
58 changed files with 361 additions and 7920 deletions

File diff suppressed because it is too large Load diff

View file

@ -2317,7 +2317,7 @@ $collections = [
],
],
],
'realtime' => [
'$collection' => Database::METADATA,
'$id' => 'realtime',
@ -2369,4 +2369,4 @@ $collections = [
],
];
return $collections;
return $collections;

View file

@ -3,7 +3,6 @@
use Ahc\Jwt\JWT;
use Appwrite\Auth\Auth;
use Appwrite\Auth\Validator\Password;
use Appwrite\Database\Validator\CustomId;
use Appwrite\Detector\Detector;
use Appwrite\Network\Validator\Email;
use Appwrite\Network\Validator\Host;
@ -12,6 +11,7 @@ use Appwrite\OpenSSL\OpenSSL;
use Appwrite\Template\Template;
use Appwrite\URL\URL as URLParser;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Database\Validator\CustomId;
use Utopia\App;
use Utopia\Audit\Audit;
use Utopia\Config\Config;

View file

@ -27,10 +27,10 @@ use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Exception\Limit as LimitException;
use Utopia\Database\Exception\Structure as StructureException;
use Appwrite\Auth\Auth;
use Appwrite\Database\Validator\CustomId;
use Appwrite\Network\Validator\Email;
use Appwrite\Network\Validator\IP;
use Appwrite\Network\Validator\URL;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Response;
use Appwrite\Detector\Detector;
use Appwrite\Event\Event;

View file

@ -2,7 +2,7 @@
use Ahc\Jwt\JWT;
use Appwrite\Auth\Auth;
use Appwrite\Database\Validator\CustomId;
use Appwrite\Utopia\Database\Validator\CustomId;
use Utopia\Database\Validator\UID;
use Utopia\Storage\Storage;
use Utopia\Storage\Validator\File;

View file

@ -2,10 +2,10 @@
use Appwrite\Auth\Auth;
use Appwrite\Auth\Validator\Password;
use Appwrite\Database\Validator\CustomId;
use Appwrite\Network\Validator\CNAME;
use Appwrite\Network\Validator\Domain as DomainValidator;
use Appwrite\Network\Validator\URL;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Response;
use Utopia\Abuse\Adapters\TimeLimit;
use Utopia\App;
@ -537,7 +537,7 @@ App::delete('/v1/projects/:projectId')
->inject('deletes')
->action(function ($projectId, $password, $response, $user, $dbForConsole, $deletes) {
/** @var Appwrite\Utopia\Response $response */
/** @var Appwrite\Database\Document $user */
/** @var Utopia\Database\Document $user */
/** @var Utopia\Database\Database $dbForConsole */
/** @var Appwrite\Event\Event $deletes */

View file

@ -1,31 +1,31 @@
<?php
use Appwrite\Auth\Auth;
use Appwrite\ClamAV\Network;
use Appwrite\OpenSSL\OpenSSL;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Exception;
use Utopia\Validator\ArrayList;
use Utopia\Validator\WhiteList;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\HexColor;
use Utopia\Cache\Cache;
use Utopia\Cache\Adapter\Filesystem;
use Appwrite\ClamAV\Network;
use Utopia\Config\Config;
use Utopia\Database\Validator\Authorization;
use Appwrite\Database\Validator\CustomId;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Database\Validator\UID;
use Utopia\Image\Image;
use Utopia\Storage\Storage;
use Utopia\Storage\Validator\File;
use Utopia\Storage\Validator\FileSize;
use Utopia\Storage\Validator\Upload;
use Utopia\Storage\Compression\Algorithms\GZIP;
use Utopia\Image\Image;
use Appwrite\OpenSSL\OpenSSL;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Query;
use Utopia\Validator\ArrayList;
use Utopia\Validator\WhiteList;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\HexColor;
App::post('/v1/storage/files')
->desc('Create File')

View file

@ -1,19 +1,14 @@
<?php
use Appwrite\Auth\Auth;
use Appwrite\Database\Validator\CustomId;
use Appwrite\Detector\Detector;
use Appwrite\Template\Template;
use Appwrite\Utopia\Response;
use Appwrite\Network\Validator\Email;
use Appwrite\Network\Validator\Host;
use Appwrite\Template\Template;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Exception;
use Utopia\Config\Config;
use Utopia\Validator\Text;
use Utopia\Validator\Range;
use Utopia\Validator\ArrayList;
use Utopia\Validator\WhiteList;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Authorization as AuthorizationException;
@ -22,6 +17,11 @@ use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Exception;
use Utopia\Validator\Text;
use Utopia\Validator\Range;
use Utopia\Validator\ArrayList;
use Utopia\Validator\WhiteList;
App::post('/v1/teams')
->desc('Create Team')

View file

@ -2,25 +2,25 @@
use Appwrite\Auth\Auth;
use Appwrite\Auth\Validator\Password;
use Appwrite\Detector\Detector;
use Appwrite\Network\Validator\Email;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Exception;
use Utopia\Validator\Assoc;
use Utopia\Validator\WhiteList;
use Appwrite\Network\Validator\Email;
use Utopia\Validator\Text;
use Utopia\Validator\Range;
use Utopia\Validator\Boolean;
use Utopia\Audit\Audit;
use Utopia\Config\Config;
use Utopia\Exception;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Validator\UID;
use Appwrite\Detector\Detector;
use Appwrite\Database\Validator\CustomId;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Validator\Assoc;
use Utopia\Validator\WhiteList;
use Utopia\Validator\Text;
use Utopia\Validator\Range;
use Utopia\Validator\Boolean;
App::post('/v1/users')
->desc('Create User')

View file

@ -13,9 +13,6 @@ use Utopia\Config\Config;
use Utopia\Domains\Domain;
use Appwrite\Auth\Auth;
use Appwrite\Network\Validator\Origin;
use Appwrite\Utopia\Response\Filters\V06;
use Appwrite\Utopia\Response\Filters\V07;
use Appwrite\Utopia\Response\Filters\V08;
use Appwrite\Utopia\Response\Filters\V11;
use Utopia\CLI\Console;
use Utopia\Database\Document;
@ -159,15 +156,6 @@ App::init(function ($utopia, $request, $response, $console, $project, $dbForCons
$responseFormat = $request->getHeader('x-appwrite-response-format', App::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
if ($responseFormat) {
switch($responseFormat) {
case version_compare ($responseFormat , '0.6.2', '<=') :
Response::setFilter(new V06());
break;
case version_compare ($responseFormat , '0.7.2', '<=') :
Response::setFilter(new V07());
break;
case version_compare ($responseFormat , '0.8.0', '<=') :
Response::setFilter(new V08());
break;
case version_compare ($responseFormat , '0.11.0', '<=') :
Response::setFilter(new V11());
break;
@ -337,8 +325,8 @@ App::error(function ($error, $utopia, $request, $response, $layout, $project, $l
if($logger) {
if($error->getCode() >= 500 || $error->getCode() === 0) {
try {
/** @var Utopia\Database\Document $user */
$user = $utopia->getResource('user');
/** @var Appwrite\Database\Document $user */
} catch(\Throwable $th) {
// All good, user is optional information for logger
}

View file

@ -1,13 +1,13 @@
<?php
use Appwrite\Auth\Auth;
use Appwrite\Database\Validator\Authorization;
use Appwrite\Messaging\Adapter\Realtime;
use Utopia\App;
use Utopia\Exception;
use Utopia\Abuse\Abuse;
use Utopia\Abuse\Adapters\TimeLimit;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Exception;
use Utopia\Storage\Device\Local;
use Utopia\Storage\Storage;

View file

@ -193,8 +193,8 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo
$logger = $app->getResource("logger");
if($logger) {
try {
/** @var Utopia\Database\Document $user */
$user = $app->getResource('user');
/** @var Appwrite\Database\Document $user */
} catch(\Throwable $_th) {
// All good, user is optional information for logger
}

View file

@ -21,7 +21,6 @@ use Appwrite\Extend\PDO;
use Ahc\Jwt\JWT;
use Ahc\Jwt\JWTException;
use Appwrite\Auth\Auth;
use Appwrite\Database\Database as DatabaseOld;
use Appwrite\Event\Event;
use Appwrite\Network\Validator\Email;
use Appwrite\Network\Validator\IP;
@ -62,7 +61,7 @@ const APP_PAGING_LIMIT = 12;
const APP_LIMIT_COUNT = 5000;
const APP_LIMIT_USERS = 10000;
const APP_CACHE_BUSTER = 201;
const APP_VERSION_STABLE = '0.12.1';
const APP_VERSION_STABLE = '0.13.0';
const APP_DATABASE_ATTRIBUTE_EMAIL = 'email';
const APP_DATABASE_ATTRIBUTE_ENUM = 'enum';
const APP_DATABASE_ATTRIBUTE_IP = 'ip';
@ -158,43 +157,6 @@ if(!empty($user) || !empty($pass)) {
Resque::setBackend(App::getEnv('_APP_REDIS_HOST', '').':'.App::getEnv('_APP_REDIS_PORT', ''));
}
/**
* Old DB Filters
*/
DatabaseOld::addFilter('json',
function($value) {
if(!is_array($value)) {
return $value;
}
return json_encode($value);
},
function($value) {
return json_decode($value, true);
}
);
DatabaseOld::addFilter('encrypt',
function($value) {
$key = App::getEnv('_APP_OPENSSL_KEY_V1');
$iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM));
$tag = null;
return json_encode([
'data' => OpenSSL::encrypt($value, OpenSSL::CIPHER_AES_128_GCM, $key, 0, $iv, $tag),
'method' => OpenSSL::CIPHER_AES_128_GCM,
'iv' => bin2hex($iv),
'tag' => bin2hex($tag),
'version' => '1',
]);
},
function($value) {
$value = json_decode($value, true);
$key = App::getEnv('_APP_OPENSSL_KEY_V'.$value['version']);
return OpenSSL::decrypt($value['data'], $value['method'], $key, 0, hex2bin($value['iv']), hex2bin($value['tag']));
}
);
/**
* New DB Filters
*/

View file

@ -1,18 +1,17 @@
<?php
global $cli, $register, $projectDB, $console;
global $cli, $register;
use Utopia\Config\Config;
use Utopia\CLI\Console;
use Appwrite\Database\Database;
use Appwrite\Database\Validator\Authorization;
use Appwrite\Database\Adapter\MySQL as MySQLAdapter;
use Appwrite\Database\Adapter\Redis as RedisAdapter;
use Appwrite\Migration\Migration;
use Utopia\App;
use Utopia\Cache\Cache;
use Utopia\Cache\Adapter\Redis as RedisCache;
use Utopia\Database\Adapter\MariaDB;
use Utopia\Database\Database;
use Utopia\Database\Validator\Authorization;
use Utopia\Validator\Text;
Config::load('collections.old', __DIR__ . '/../config/collections.old.php');
$cli
->task('migrate')
->param('version', APP_VERSION_STABLE, new Text(8), 'Version to migrate to.', true)
@ -23,86 +22,36 @@ $cli
Console::exit(1);
return;
}
$options = [];
if (str_starts_with($version, '0.12.')) {
Console::error('--------------------');
Console::error('WARNING');
Console::error('--------------------');
Console::warning('Migrating to Version 0.12.x introduces a major breaking change within the Database Service!');
Console::warning('Before migrating, please read about the breaking changes here:');
Console::info('https://dev.to/appwrite/appwrite-012-migration-post-3cha');
$confirm = Console::confirm("If you want to proceed, type 'yes':");
if ($confirm != 'yes') {
Console::exit(1);
return;
}
Console::log('');
Console::log('Collections');
Console::log('--------------------');
Console::warning('Be aware that following actions will happen during the migration:');
Console::warning('- Nested Document rules will be migrated to String attributes');
Console::warning('- Numeric rules will be migrated to float attributes');
Console::warning('- Wildcard and Markdown rules will be converted to string attributes');
Console::info("Do you want to migrate your Database Collections?");
$options['migrateCollections'] = Console::confirm("Type 'yes' or 'no':");
if ($options['migrateCollections'] === 'yes') {
Console::log('');
Console::log('Documents');
Console::log('------------------');
Console::warning('Be aware that following actions will happen during the migration:');
Console::warning('- Nested Documents will be stored as JSON values');
Console::warning('- All Numeric values will be converted to float');
Console::warning('- All Wildcard and Markdown values will be converted to string');
Console::info("Do you want to migrate your Database Documents?");
$options['migrateDocuments'] = Console::confirm("Type 'yes' or 'no':");
} else {
$options['migrateDocuments'] = 'no';
}
if (
!in_array($options['migrateDocuments'], ['yes', 'no'])
|| !in_array($options['migrateCollections'], ['yes', 'no'])
) {
Console::error("You must reply with 'yes' or 'no'!");
Console::exit(1);
return;
}
}
Config::load('collectionsold', __DIR__ . '/../config/collections.old.php');
$app = new App('UTC');
Console::success('Starting Data Migration to version ' . $version);
$db = $register->get('db', true);
$cache = $register->get('cache', true);
$cache->flushAll();
$consoleDB = new Database();
$consoleDB
->setAdapter(new RedisAdapter(new MySQLAdapter($db, $cache), $cache))
->setNamespace('app_console') // Main DB
->setMocks(Config::getParam('collectionsold', []));
$cache = new Cache(new RedisCache($cache));
$projectDB = new Database();
$projectDB
->setAdapter(new RedisAdapter(new MySQLAdapter($db, $cache), $cache))
->setMocks(Config::getParam('collectionsold', []));
$projectDB = new Database(new MariaDB($db), $cache);
$projectDB->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
$console = $consoleDB->getDocument('console');
$consoleDB = new Database(new MariaDB($db), $cache);
$consoleDB->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
$consoleDB->setNamespace('_project_console');
$console = $app->getResource('console');
$limit = 30;
$sum = 30;
$offset = 0;
$projects = [$console];
$count = 0;
$totalProjects = $consoleDB->count('projects') + 1;
$class = 'Appwrite\\Migration\\Version\\' . Migration::$versions[$version];
$migration = new $class($register->get('db'), $register->get('cache'), $options);
$migration = new $class();
while ($sum > 0) {
while (!empty($projects)) {
foreach ($projects as $project) {
try {
$migration
@ -114,23 +63,14 @@ $cli
}
}
$projects = $consoleDB->getCollection([
'limit' => $limit,
'offset' => $offset,
'filters' => [
'$collection=' . Database::SYSTEM_COLLECTION_PROJECTS,
],
]);
$sum = \count($projects);
$projects = $consoleDB->find('projects', limit: $limit, offset: $offset);
$offset = $offset + $limit;
$count = $count + $sum;
if ($sum > 0) {
Console::log('Fetched ' . $count . '/' . $consoleDB->getSum() . ' projects...');
}
Console::log('Migrated ' . $count . '/' . $totalProjects . ' projects...');
}
$cache->flushAll();
Swoole\Event::wait(); // Wait for Coroutines to finish
Console::success('Data Migration Completed');

View file

@ -1,161 +0,0 @@
<?php
namespace Appwrite\Database;
use Exception;
abstract class Adapter
{
/**
* @var string
*/
protected $namespace = '';
/**
* Set Namespace.
*
* Set namespace to divide different scope of data sets
*
* @param $namespace
*
* @throws Exception
*
* @return bool
*/
public function setNamespace($namespace)
{
if (empty($namespace)) {
throw new Exception('Missing namespace');
}
$this->namespace = $namespace;
return true;
}
/**
* Get Namespace.
*
* Get namespace of current set scope
*
* @throws Exception
*
* @return string
*/
public function getNamespace()
{
if (empty($this->namespace)) {
throw new Exception('Missing namespace');
}
return $this->namespace;
}
/**
* Get Document.
*
* @param string $id
*
* @return array
*/
abstract public function getDocument($id);
/**
* Create Document
**.
*
* @param array $data
*
* @return array
*/
abstract public function createDocument(array $data = [], array $unique = []);
/**
* Update Document.
*
* @param array $data
*
* @return array
*/
abstract public function updateDocument(array $data = []);
/**
* Delete Node.
*
* @param string $id
*
* @return array
*/
abstract public function deleteDocument(string $id);
/**
* Delete Unique Key.
*
* @param string $key
*
* @return array
*/
abstract public function deleteUniqueKey($key);
/**
* Add Unique Key.
*
* @param string $key
*
* @return array
*/
abstract public function addUniqueKey($key);
/**
* Create Namespace.
*
* @param string $namespace
*
* @return bool
*/
abstract public function createNamespace($namespace);
/**
* Delete Namespace.
*
* @param string $namespace
*
* @return bool
*/
abstract public function deleteNamespace($namespace);
/**
* Filter.
*
* Filter data sets using chosen queries
*
* @param array $options
* @param array $filterTypes
*
* @return array
*/
abstract public function getCollection(array $options, array $filterTypes = []);
/**
* @param array $options
*
* @return int
*/
abstract public function getCount(array $options);
/**
* Last Modified.
*
* Return Unix timestamp of last time a node queried in corrent session has been changed
*
* @return int
*/
abstract public function lastModified();
/**
* Get Debug Data.
*
* @return array
*/
abstract public function getDebug();
}

View file

@ -1,986 +0,0 @@
<?php
namespace Appwrite\Database\Adapter;
use Appwrite\Database\Adapter;
use Appwrite\Database\Exception\Duplicate;
use Appwrite\Database\Validator\Authorization;
use Exception;
use PDO;
use Redis;
class MySQL extends Adapter
{
const DATA_TYPE_STRING = 'string';
const DATA_TYPE_INTEGER = 'integer';
const DATA_TYPE_FLOAT = 'float';
const DATA_TYPE_BOOLEAN = 'boolean';
const DATA_TYPE_OBJECT = 'object';
const DATA_TYPE_DICTIONARY = 'dictionary';
const DATA_TYPE_ARRAY = 'array';
const DATA_TYPE_NULL = 'null';
const OPTIONS_LIMIT_ATTRIBUTES = 1000;
/**
* Last modified.
*
* Read node with most recent changes
*
* @var int
*/
protected $lastModified = -1;
/**
* @var array
*/
protected $debug = [];
/**
* @var PDO
*/
protected $pdo;
/**
* @var Redis
*/
protected $redis;
/**
* Constructor.
*
* Set connection and settings
*
* @param PDO $pdo
* @param Redis $redis
*/
public function __construct($pdo, Redis $redis)
{
$this->pdo = $pdo;
$this->redis = $redis;
}
/**
* Get Document.
*
* @param string $id
*
* @return array
*
* @throws Exception
*/
public function getDocument($id)
{
// Get fields abstraction
$st = $this->getPDO()->prepare('SELECT * FROM `'.$this->getNamespace().'.database.documents` a
WHERE a.uid = :uid AND a.status = 0
ORDER BY a.updatedAt DESC LIMIT 10;
');
$st->bindValue(':uid', $id, PDO::PARAM_STR);
$st->execute();
$document = $st->fetch();
if (empty($document)) { // Not Found
return [];
}
// Get fields abstraction
$st = $this->getPDO()->prepare('SELECT * FROM `'.$this->getNamespace().'.database.properties` a
WHERE a.documentUid = :documentUid AND a.documentRevision = :documentRevision
ORDER BY `order`
');
$st->bindParam(':documentUid', $document['uid'], PDO::PARAM_STR, 32);
$st->bindParam(':documentRevision', $document['revision'], PDO::PARAM_STR, 32);
$st->execute();
$properties = $st->fetchAll();
$output = [
'$id' => null,
'$collection' => null,
'$permissions' => (!empty($document['permissions'])) ? \json_decode($document['permissions'], true) : [],
];
foreach ($properties as &$property) {
\settype($property['value'], $property['primitive']);
if ($property['array']) {
$output[$property['key']][] = $property['value'];
} else {
$output[$property['key']] = $property['value'];
}
}
// Get fields abstraction
$st = $this->getPDO()->prepare('SELECT * FROM `'.$this->getNamespace().'.database.relationships` a
WHERE a.start = :start AND revision = :revision
ORDER BY `order`
');
$st->bindParam(':start', $document['uid'], PDO::PARAM_STR, 32);
$st->bindParam(':revision', $document['revision'], PDO::PARAM_STR, 32);
$st->execute();
$output['temp-relations'] = $st->fetchAll();
return $output;
}
/**
* Create Document.
*
* @param array $data
*
* @throws \Exception
*
* @return array
*/
public function createDocument(array $data = [], array $unique = [])
{
$order = 0;
$data = \array_merge(['$id' => null, '$permissions' => []], $data); // Merge data with default params
$signature = \md5(\json_encode($data));
$revision = \uniqid('', true);
$data['$id'] = (empty($data['$id'])) ? null : $data['$id'];
/*
* When updating node, check if there are any changes to update
* by comparing data md5 signatures
*/
if (null !== $data['$id']) {
$st = $this->getPDO()->prepare('SELECT signature FROM `'.$this->getNamespace().'.database.documents` a
WHERE a.uid = :uid AND a.status = 0
ORDER BY a.updatedAt DESC LIMIT 1;
');
$st->bindValue(':uid', $data['$id'], PDO::PARAM_STR);
$st->execute();
$result = $st->fetch();
if ($result && isset($result['signature'])) {
$oldSignature = $result['signature'];
if ($signature === $oldSignature) {
return $data;
}
}
}
/**
* Check Unique Keys
*/
foreach ($unique as $key => $value) {
$st = $this->getPDO()->prepare('INSERT INTO `'.$this->getNamespace().'.database.unique`
SET `key` = :key;
');
$st->bindValue(':key', \md5($data['$collection'].':'.$key.'='.$value), PDO::PARAM_STR);
if (!$st->execute()) {
throw new Duplicate('Duplicated Property: '.$key.'='.$value);
}
}
// Add or update fields abstraction level
$st1 = $this->getPDO()->prepare('INSERT INTO `'.$this->getNamespace().'.database.documents`
SET uid = :uid, createdAt = :createdAt, updatedAt = :updatedAt, signature = :signature, revision = :revision, permissions = :permissions, status = 0
ON DUPLICATE KEY UPDATE uid = :uid, updatedAt = :updatedAt, signature = :signature, revision = :revision, permissions = :permissions;
');
// Adding fields properties
if (null === $data['$id'] || !isset($data['$id'])) { // Get new fields UID
$data['$id'] = $this->getId();
}
$st1->bindValue(':uid', $data['$id'], PDO::PARAM_STR);
$st1->bindValue(':revision', $revision, PDO::PARAM_STR);
$st1->bindValue(':signature', $signature, PDO::PARAM_STR);
$st1->bindValue(':createdAt', \date('Y-m-d H:i:s', \time()), PDO::PARAM_STR);
$st1->bindValue(':updatedAt', \date('Y-m-d H:i:s', \time()), PDO::PARAM_STR);
$st1->bindValue(':permissions', \json_encode($data['$permissions']), PDO::PARAM_STR);
$st1->execute();
// Delete old properties
$rms1 = $this->getPDO()->prepare('DELETE FROM `'.$this->getNamespace().'.database.properties` WHERE documentUid = :documentUid AND documentRevision != :documentRevision');
$rms1->bindValue(':documentUid', $data['$id'], PDO::PARAM_STR);
$rms1->bindValue(':documentRevision', $revision, PDO::PARAM_STR);
$rms1->execute();
// Delete old relationships
$rms2 = $this->getPDO()->prepare('DELETE FROM `'.$this->getNamespace().'.database.relationships` WHERE start = :start AND revision != :revision');
$rms2->bindValue(':start', $data['$id'], PDO::PARAM_STR);
$rms2->bindValue(':revision', $revision, PDO::PARAM_STR);
$rms2->execute();
// Create new properties
$st2 = $this->getPDO()->prepare('INSERT INTO `'.$this->getNamespace().'.database.properties`
(`documentUid`, `documentRevision`, `key`, `value`, `primitive`, `array`, `order`)
VALUES (:documentUid, :documentRevision, :key, :value, :primitive, :array, :order)');
$props = [];
foreach ($data as $key => $value) { // Prepare properties data
if (\in_array($key, ['$permissions'])) {
continue;
}
$type = $this->getDataType($value);
// Handle array of relations
if (self::DATA_TYPE_ARRAY === $type) {
if (!is_array($value)) { // Property should be of type array, if not = skip
continue;
}
foreach ($value as $i => $child) {
if (self::DATA_TYPE_DICTIONARY !== $this->getDataType($child)) { // not dictionary
$props[] = [
'type' => $this->getDataType($child),
'key' => $key,
'value' => $child,
'array' => true,
'order' => $order++,
];
continue;
}
$data[$key][$i] = $this->createDocument($child);
$this->createRelationship($revision, $data['$id'], $data[$key][$i]['$id'], $key, true, $i);
}
continue;
}
// Handle relation
if (self::DATA_TYPE_DICTIONARY === $type) {
$value = $this->createDocument($value);
$this->createRelationship($revision, $data['$id'], $value['$id'], $key); //xxx
continue;
}
// Handle empty values
if (self::DATA_TYPE_NULL === $type) {
continue;
}
$props[] = [
'type' => $type,
'key' => $key,
'value' => $value,
'array' => false,
'order' => $order++,
];
}
foreach ($props as $prop) {
if (\is_array($prop['value'])) {
throw new Exception('Value can\'t be an array: '.\json_encode($prop['value']));
}
if (\is_bool($prop['value'])) {
$prop['value'] = (int) $prop['value'];
}
$st2->bindValue(':documentUid', $data['$id'], PDO::PARAM_STR);
$st2->bindValue(':documentRevision', $revision, PDO::PARAM_STR);
$st2->bindValue(':key', $prop['key'], PDO::PARAM_STR);
$st2->bindValue(':value', $prop['value'], PDO::PARAM_STR);
$st2->bindValue(':primitive', $prop['type'], PDO::PARAM_STR);
$st2->bindValue(':array', $prop['array'], PDO::PARAM_BOOL);
$st2->bindValue(':order', $prop['order'], PDO::PARAM_STR);
$st2->execute();
}
// TODO remove this dependency (check if related to nested documents)
$this->getRedis()->expire($this->getNamespace().':document-'.$data['$id'], 0);
$this->getRedis()->expire($this->getNamespace().':document-'.$data['$id'], 0);
return $data;
}
/**
* Update Document.
*
* @param array $data
*
* @return array
*
* @throws Exception
*/
public function updateDocument(array $data = [])
{
return $this->createDocument($data);
}
/**
* Delete Document.
*
* @param string $id
*
* @return array
*
* @throws Exception
*/
public function deleteDocument(string $id)
{
$st1 = $this->getPDO()->prepare('DELETE FROM `'.$this->getNamespace().'.database.documents`
WHERE uid = :id
');
$st1->bindValue(':id', $id, PDO::PARAM_STR);
$st1->execute();
$st2 = $this->getPDO()->prepare('DELETE FROM `'.$this->getNamespace().'.database.properties`
WHERE documentUid = :id
');
$st2->bindValue(':id', $id, PDO::PARAM_STR);
$st2->execute();
$st3 = $this->getPDO()->prepare('DELETE FROM `'.$this->getNamespace().'.database.relationships`
WHERE start = :id OR end = :id
');
$st3->bindValue(':id', $id, PDO::PARAM_STR);
$st3->execute();
return [];
}
/**
* Delete Unique Key.
*
* @param string $key
*
* @return array
*
* @throws Exception
*/
public function deleteUniqueKey($key)
{
$st1 = $this->getPDO()->prepare('DELETE FROM `'.$this->getNamespace().'.database.unique` WHERE `key` = :key');
$st1->bindValue(':key', $key, PDO::PARAM_STR);
$st1->execute();
return [];
}
/**
* Add Unique Key.
*
* @param string $key
*
* @return array
*
* @throws Exception
*/
public function addUniqueKey($key)
{
$st = $this->getPDO()->prepare('INSERT INTO `'.$this->getNamespace().'.database.unique`
SET `key` = :key;
');
$st->bindValue(':key', $key, PDO::PARAM_STR);
if (!$st->execute()) {
throw new Duplicate('Duplicated Property: '.$key);
}
return [];
}
/**
* Create Relation.
*
* Adds a new relationship between different nodes
*
* @param string $revision
* @param int $start
* @param int $end
* @param string $key
* @param bool $isArray
* @param int $order
*
* @return array
*
* @throws Exception
*/
protected function createRelationship($revision, $start, $end, $key, $isArray = false, $order = 0)
{
$st2 = $this->getPDO()->prepare('INSERT INTO `'.$this->getNamespace().'.database.relationships`
(`revision`, `start`, `end`, `key`, `array`, `order`)
VALUES (:revision, :start, :end, :key, :array, :order)');
$st2->bindValue(':revision', $revision, PDO::PARAM_STR);
$st2->bindValue(':start', $start, PDO::PARAM_STR);
$st2->bindValue(':end', $end, PDO::PARAM_STR);
$st2->bindValue(':key', $key, PDO::PARAM_STR);
$st2->bindValue(':array', $isArray, PDO::PARAM_INT);
$st2->bindValue(':order', $order, PDO::PARAM_INT);
$st2->execute();
return [];
}
/**
* Create Namespace.
*
* @param $namespace
*
* @throws Exception
*
* @return bool
*/
public function createNamespace($namespace)
{
if (empty($namespace)) {
throw new Exception('Empty namespace');
}
$documents = 'app_'.$namespace.'.database.documents';
$properties = 'app_'.$namespace.'.database.properties';
$relationships = 'app_'.$namespace.'.database.relationships';
$unique = 'app_'.$namespace.'.database.unique';
$audit = 'app_'.$namespace.'.audit.audit';
$abuse = 'app_'.$namespace.'.abuse.abuse';
try {
$this->getPDO()->prepare('CREATE TABLE `'.$documents.'` LIKE `template.database.documents`;')->execute();
$this->getPDO()->prepare('CREATE TABLE `'.$properties.'` LIKE `template.database.properties`;')->execute();
$this->getPDO()->prepare('CREATE TABLE `'.$relationships.'` LIKE `template.database.relationships`;')->execute();
$this->getPDO()->prepare('CREATE TABLE `'.$unique.'` LIKE `template.database.unique`;')->execute();
$this->getPDO()->prepare('CREATE TABLE `'.$audit.'` LIKE `template.audit.audit`;')->execute();
$this->getPDO()->prepare('CREATE TABLE `'.$abuse.'` LIKE `template.abuse.abuse`;')->execute();
} catch (Exception $e) {
throw $e;
}
return true;
}
/**
* Delete Namespace.
*
* @param $namespace
*
* @throws Exception
*
* @return bool
*/
public function deleteNamespace($namespace)
{
if (empty($namespace)) {
throw new Exception('Empty namespace');
}
$unique = 'app_'.$namespace.'.database.unique';
$documents = 'app_'.$namespace.'.database.documents';
$properties = 'app_'.$namespace.'.database.properties';
$relationships = 'app_'.$namespace.'.database.relationships';
$audit = 'app_'.$namespace.'.audit.audit';
$abuse = 'app_'.$namespace.'.abuse.abuse';
try {
$this->getPDO()->prepare('DROP TABLE `'.$unique.'`;')->execute();
$this->getPDO()->prepare('DROP TABLE `'.$documents.'`;')->execute();
$this->getPDO()->prepare('DROP TABLE `'.$properties.'`;')->execute();
$this->getPDO()->prepare('DROP TABLE `'.$relationships.'`;')->execute();
$this->getPDO()->prepare('DROP TABLE `'.$audit.'`;')->execute();
$this->getPDO()->prepare('DROP TABLE `'.$abuse.'`;')->execute();
} catch (Exception $e) {
throw $e;
}
return true;
}
/**
* Get Collection.
*
* @param array $options
* @param array $filterTypes
*
* @throws Exception
*
* @return array
*/
public function getCollection(array $options, array $filterTypes = [])
{
$start = \microtime(true);
$orderCastMap = [
'int' => 'UNSIGNED',
'string' => 'CHAR',
'date' => 'DATE',
'time' => 'TIME',
'datetime' => 'DATETIME',
];
$orderTypeMap = ['DESC', 'ASC'];
$options['orderField'] = (empty($options['orderField'])) ? '' : $options['orderField']; // Set default order field
$options['orderCast'] = (empty($options['orderCast'])) ? 'string' : $options['orderCast']; // Set default order field
if (!\array_key_exists($options['orderCast'], $orderCastMap)) {
throw new Exception('Invalid order cast');
}
if (!\in_array($options['orderType'], $orderTypeMap)) {
throw new Exception('Invalid order type');
}
$where = [];
$join = [];
$sorts = [];
$search = '';
// Filters
foreach ($options['filters'] as $i => $filter) {
$filter = $this->parseFilter($filter);
$key = $filter['key'];
$value = $filter['value'];
$operator = $filter['operator'];
$path = \explode('.', $key);
$original = $path;
if (1 < \count($path)) {
$key = \array_pop($path);
} else {
$path = [];
}
//$path = implode('.', $path);
if (array_key_exists($key, $filterTypes) && $filterTypes[$key] === 'numeric') {
$value = (float) $value;
} else {
$value = $this->getPDO()->quote($value, PDO::PARAM_STR);
}
$key = $this->getPDO()->quote($key, PDO::PARAM_STR);
//$path = $this->getPDO()->quote($path, PDO::PARAM_STR);
$options['offset'] = (int) $options['offset'];
$options['limit'] = (int) $options['limit'];
if (empty($path)) {
//if($path == "''") { // Handle direct attributes queries
$where[] = 'JOIN `'.$this->getNamespace().".database.properties` b{$i} ON a.uid IS NOT NULL AND b{$i}.documentUid = a.uid AND (b{$i}.key = {$key} AND b{$i}.value {$operator} {$value})";
} else { // Handle direct child attributes queries
$len = \count($original);
$prev = 'c'.$i;
foreach ($original as $y => $part) {
$part = $this->getPDO()->quote($part, PDO::PARAM_STR);
if (0 === $y) { // First key
$join[$i] = 'JOIN `'.$this->getNamespace().".database.relationships` c{$i} ON a.uid IS NOT NULL AND c{$i}.start = a.uid AND c{$i}.key = {$part}";
} elseif ($y == $len - 1) { // Last key
$join[$i] .= 'JOIN `'.$this->getNamespace().".database.properties` e{$i} ON e{$i}.documentUid = {$prev}.end AND e{$i}.key = {$part} AND e{$i}.value {$operator} {$value}";
} else {
$join[$i] .= 'JOIN `'.$this->getNamespace().".database.relationships` d{$i}{$y} ON d{$i}{$y}.start = {$prev}.end AND d{$i}{$y}.key = {$part}";
$prev = 'd'.$i.$y;
}
}
//$join[] = "JOIN `" . $this->getNamespace() . ".database.relationships` c{$i} ON a.uid IS NOT NULL AND c{$i}.start = a.uid AND c{$i}.key = {$path}
// JOIN `" . $this->getNamespace() . ".database.properties` d{$i} ON d{$i}.documentUid = c{$i}.end AND d{$i}.key = {$key} AND d{$i}.value {$operator} {$value}";
}
}
// Sorting
if (!empty($options['orderField'])) {
$orderPath = \explode('.', $options['orderField']);
$len = \count($orderPath);
$orderKey = 'order_b';
$part = $this->getPDO()->quote(\implode('', $orderPath), PDO::PARAM_STR);
$orderSelect = "CASE WHEN {$orderKey}.key = {$part} THEN CAST({$orderKey}.value AS {$orderCastMap[$options['orderCast']]}) END AS sort_ff";
if (1 === $len) {
//if($path == "''") { // Handle direct attributes queries
$sorts[] = 'LEFT JOIN `'.$this->getNamespace().".database.properties` order_b ON a.uid IS NOT NULL AND order_b.documentUid = a.uid AND (order_b.key = {$part})";
} else { // Handle direct child attributes queries
$prev = 'c';
$orderKey = 'order_e';
foreach ($orderPath as $y => $part) {
$part = $this->getPDO()->quote($part, PDO::PARAM_STR);
$x = $y - 1;
if (0 === $y) { // First key
$sorts[] = 'JOIN `'.$this->getNamespace().".database.relationships` order_c{$y} ON a.uid IS NOT NULL AND order_c{$y}.start = a.uid AND order_c{$y}.key = {$part}";
} elseif ($y == $len - 1) { // Last key
$sorts[] .= 'JOIN `'.$this->getNamespace().".database.properties` order_e ON order_e.documentUid = order_{$prev}{$x}.end AND order_e.key = {$part}";
} else {
$sorts[] .= 'JOIN `'.$this->getNamespace().".database.relationships` order_d{$y} ON order_d{$y}.start = order_{$prev}{$x}.end AND order_d{$y}.key = {$part}";
$prev = 'd';
}
}
}
} else {
$orderSelect = 'a.uid AS sort_ff';
}
/*
* Workaround for a MySQL bug as reported here:
* https://bugs.mysql.com/bug.php?id=78485
*/
$options['search'] = ($options['search'] === '*') ? '' : $options['search'];
// Search
if (!empty($options['search'])) { // Handle free search
$where[] = 'LEFT JOIN `'.$this->getNamespace().".database.properties` b_search ON a.uid IS NOT NULL AND b_search.documentUid = a.uid AND b_search.primitive = 'string'
LEFT JOIN
`".$this->getNamespace().'.database.relationships` c_search ON c_search.start = b_search.documentUid
LEFT JOIN
`'.$this->getNamespace().".database.properties` d_search ON d_search.documentUid = c_search.end AND d_search.primitive = 'string'
\n";
$search = "AND (MATCH (b_search.value) AGAINST ({$this->getPDO()->quote($options['search'], PDO::PARAM_STR)} IN BOOLEAN MODE)
OR MATCH (d_search.value) AGAINST ({$this->getPDO()->quote($options['search'], PDO::PARAM_STR)} IN BOOLEAN MODE)
)";
}
$select = 'DISTINCT a.uid';
$where = \implode("\n", $where);
$join = \implode("\n", $join);
$sorts = \implode("\n", $sorts);
$range = "LIMIT {$options['offset']}, {$options['limit']}";
$roles = [];
foreach (Authorization::getRoles() as $role) {
$roles[] = 'JSON_CONTAINS(REPLACE(a.permissions, \'{self}\', a.uid), \'"'.$role.'"\', \'$.read\')';
}
if (false === Authorization::$status) { // FIXME temporary solution (hopefully)
$roles = ['1=1'];
}
$query = "SELECT %s, {$orderSelect}
FROM `".$this->getNamespace().".database.documents` a {$where}{$join}{$sorts}
WHERE status = 0
{$search}
AND (".\implode('||', $roles).")
ORDER BY sort_ff {$options['orderType']} %s";
$st = $this->getPDO()->prepare(\sprintf($query, $select, $range));
$st->execute();
$results = ['data' => []];
// Get entire fields data for each id
foreach ($st->fetchAll() as $node) {
$results['data'][] = $node['uid'];
}
$count = $this->getPDO()->prepare(\sprintf($query, 'count(DISTINCT a.uid) as sum', ''));
$count->execute();
$count = $count->fetch();
$this->resetDebug();
$this
->setDebug('query', \preg_replace('/\s+/', ' ', \sprintf($query, $select, $range)))
->setDebug('time', \microtime(true) - $start)
->setDebug('filters', \count($options['filters']))
->setDebug('joins', \substr_count($query, 'JOIN'))
->setDebug('count', \count($results['data']))
->setDebug('sum', (int) $count['sum'])
;
return $results['data'];
}
/**
* Get Collection.
*
* @param array $options
*
* @throws Exception
*
* @return int
*/
public function getCount(array $options)
{
$start = \microtime(true);
$where = [];
$join = [];
$options = array_merge([
'attribute' => '',
'filters' => [],
], $options);
// Filters
foreach ($options['filters'] as $i => $filter) {
$filter = $this->parseFilter($filter);
$key = $filter['key'];
$value = $filter['value'];
$operator = $filter['operator'];
$path = \explode('.', $key);
$original = $path;
if (1 < \count($path)) {
$key = \array_pop($path);
} else {
$path = [];
}
$key = $this->getPDO()->quote($key, PDO::PARAM_STR);
$value = $this->getPDO()->quote($value, PDO::PARAM_STR);
if (empty($path)) {
//if($path == "''") { // Handle direct attributes queries
$where[] = 'JOIN `'.$this->getNamespace().".database.properties` b{$i} ON a.uid IS NOT NULL AND b{$i}.documentUid = a.uid AND (b{$i}.key = {$key} AND b{$i}.value {$operator} {$value})";
} else { // Handle direct child attributes queries
$len = \count($original);
$prev = 'c'.$i;
foreach ($original as $y => $part) {
$part = $this->getPDO()->quote($part, PDO::PARAM_STR);
if (0 === $y) { // First key
$join[$i] = 'JOIN `'.$this->getNamespace().".database.relationships` c{$i} ON a.uid IS NOT NULL AND c{$i}.start = a.uid AND c{$i}.key = {$part}";
} elseif ($y == $len - 1) { // Last key
$join[$i] .= 'JOIN `'.$this->getNamespace().".database.properties` e{$i} ON e{$i}.documentUid = {$prev}.end AND e{$i}.key = {$part} AND e{$i}.value {$operator} {$value}";
} else {
$join[$i] .= 'JOIN `'.$this->getNamespace().".database.relationships` d{$i}{$y} ON d{$i}{$y}.start = {$prev}.end AND d{$i}{$y}.key = {$part}";
$prev = 'd'.$i.$y;
}
}
}
}
$where = \implode("\n", $where);
$join = \implode("\n", $join);
$attribute = $this->getPDO()->quote($options['attribute'], PDO::PARAM_STR);
$func = 'JOIN `'.$this->getNamespace().".database.properties` b_func ON a.uid IS NOT NULL
AND a.uid = b_func.documentUid
AND (b_func.key = {$attribute})";
$roles = [];
foreach (Authorization::getRoles() as $role) {
$roles[] = 'JSON_CONTAINS(REPLACE(a.permissions, \'{self}\', a.uid), \'"'.$role.'"\', \'$.read\')';
}
if (false === Authorization::$status) { // FIXME temporary solution (hopefully)
$roles = ['1=1'];
}
$query = "SELECT SUM(b_func.value) as result
FROM `".$this->getNamespace().".database.documents` a {$where}{$join}{$func}
WHERE status = 0
AND (".\implode('||', $roles).')';
$st = $this->getPDO()->prepare(\sprintf($query));
$st->execute();
$result = $st->fetch();
$this->resetDebug();
$this
->setDebug('query', \preg_replace('/\s+/', ' ', \sprintf($query)))
->setDebug('time', \microtime(true) - $start)
->setDebug('filters', \count($options['filters']))
->setDebug('joins', \substr_count($query, 'JOIN'))
;
return (isset($result['result'])) ? (int)$result['result'] : 0;
}
/**
* Get Unique Document ID.
*
* @return string
*/
public function getId(): string
{
$unique = \uniqid();
$attempts = 5;
for ($i = 1; $i <= $attempts; ++$i) {
$document = $this->getDocument($unique);
if (empty($document) || $document['$id'] !== $unique) {
return $unique;
}
}
throw new Exception('Failed to create a unique ID ('.$attempts.' attempts)');
}
/**
* Last Modified.
*
* Return Unix timestamp of last time a node queried in corrent session has been changed
*
* @return int
*/
public function lastModified()
{
return $this->lastModified;
}
/**
* Parse Filter.
*
* @param string $filter
*
* @return array
*
* @throws Exception
*/
protected function parseFilter($filter)
{
$operatorsMap = ['!=', '>=', '<=', '=', '>', '<']; // Do not edit order of this array
//FIXME bug with >= <= operators
$operator = null;
foreach ($operatorsMap as $node) {
if (\strpos($filter, $node) !== false) {
$operator = $node;
break;
}
}
if (empty($operator)) {
throw new Exception('Invalid operator');
}
$filter = \explode($operator, $filter);
if (\count($filter) != 2) {
throw new Exception('Invalid filter expression');
}
return [
'key' => $filter[0],
'value' => $filter[1],
'operator' => $operator,
];
}
/**
* Get Data Type.
*
* Check value data type. return value can be on of the following:
* string, integer, float, boolean, object, list or null
*
* @param $value
*
* @return string
*
* @throws \Exception
*/
protected function getDataType($value)
{
switch (\gettype($value)) {
case 'string':
return self::DATA_TYPE_STRING;
break;
case 'integer':
return self::DATA_TYPE_INTEGER;
break;
case 'double':
return self::DATA_TYPE_FLOAT;
break;
case 'boolean':
return self::DATA_TYPE_BOOLEAN;
break;
case 'array':
if ((bool) \count(\array_filter(\array_keys($value), 'is_string'))) {
return self::DATA_TYPE_DICTIONARY;
}
return self::DATA_TYPE_ARRAY;
break;
case 'NULL':
return self::DATA_TYPE_NULL;
break;
}
throw new Exception('Unknown data type: '.$value.' ('.\gettype($value).')');
}
/**
* @param string $key
* @param mixed $value
*
* @return $this
*/
public function setDebug(string $key, $value): self
{
$this->debug[$key] = $value;
return $this;
}
/**
* @return array
*/
public function getDebug(): array
{
return $this->debug;
}
/**
* return $this;.
*
* @return void
*/
public function resetDebug(): void
{
$this->debug = [];
}
/**
* @return PDO
*
* @throws Exception
*/
protected function getPDO()
{
return $this->pdo;
}
/**
* @throws Exception
*
* @return Redis
*/
protected function getRedis(): Redis
{
return $this->redis;
}
}

View file

@ -1,300 +0,0 @@
<?php
namespace Appwrite\Database\Adapter;
use Appwrite\Database\Adapter;
use Exception;
use Redis as Client;
class Redis extends Adapter
{
/**
* @var Client
*/
protected $redis;
/**
* @var Adapter
*/
protected $adapter;
/**
* Redis constructor.
*
* @param Adapter $adapter
* @param Client $redis
*/
public function __construct(Adapter $adapter, Client $redis)
{
$this->redis = $redis;
$this->adapter = $adapter;
}
/**
* Get Document.
*
* @param string $id
*
* @return array
*
* @throws Exception
*/
public function getDocument($id)
{
$output = \json_decode($this->getRedis()->get($this->getNamespace().':document-'.$id), true);
if (!$output) {
$output = $this->adapter->getDocument($id);
$this->getRedis()->set($this->getNamespace().':document-'.$id, \json_encode($output, JSON_UNESCAPED_UNICODE));
}
$output = $this->parseRelations($output);
return $output;
}
/**
* @param $output
*
* @return mixed
*
* @throws Exception
*/
protected function parseRelations($output)
{
$keys = [];
if (empty($output) || !isset($output['temp-relations'])) {
return $output;
}
foreach ($output['temp-relations'] as $relationship) {
$keys[] = $this->getNamespace().':document-'.$relationship['end'];
}
$nodes = (!empty($keys)) ? $this->getRedis()->mget($keys) : [];
foreach ($output['temp-relations'] as $i => $relationship) {
$node = $relationship['end'];
$node = (!empty($nodes[$i])) ? $this->parseRelations(\json_decode($nodes[$i], true)) : $this->getDocument($node);
if (empty($node)) {
continue;
}
if ($relationship['array']) {
$output[$relationship['key']][] = $node;
} else {
$output[$relationship['key']] = $node;
}
}
unset($output['temp-relations']);
return $output;
}
/**
* Create Document.
*
* @param array $data
*
* @return array
*
* @throws Exception
*/
public function createDocument(array $data = [], array $unique = [])
{
$data = $this->adapter->createDocument($data, $unique);
$this->getRedis()->expire($this->getNamespace().':document-'.$data['$id'], 0);
$this->getRedis()->expire($this->getNamespace().':document-'.$data['$id'], 0);
return $data;
}
/**
* Update Document.
*
* @param array $data
*
* @return array
*
* @throws Exception
*/
public function updateDocument(array $data = [])
{
$data = $this->adapter->updateDocument($data);
$this->getRedis()->expire($this->getNamespace().':document-'.$data['$id'], 0);
$this->getRedis()->expire($this->getNamespace().':document-'.$data['$id'], 0);
return $data;
}
/**
* Delete Document.
*
* @param string $id
*
* @return array
*
* @throws Exception
*/
public function deleteDocument(string $id)
{
$data = $this->adapter->deleteDocument($id);
$this->getRedis()->expire($this->getNamespace().':document-'.$id, 0);
$this->getRedis()->expire($this->getNamespace().':document-'.$id, 0);
return $data;
}
/**
* Delete Unique Key.
*
* @param $key
*
* @return array
*
* @throws Exception
*/
public function deleteUniqueKey($key)
{
$data = $this->adapter->deleteUniqueKey($key);
return $data;
}
/**
* Add Unique Key.
*
* @param $key
*
* @return array
*
* @throws Exception
*/
public function addUniqueKey($key)
{
$data = $this->adapter->addUniqueKey($key);
return $data;
}
/**
* Create Namespace.
*
* @param string $namespace
*
* @return bool
*/
public function createNamespace($namespace)
{
return $this->adapter->createNamespace($namespace);
}
/**
* Delete Namespace.
*
* @param string $namespace
*
* @return bool
*/
public function deleteNamespace($namespace)
{
return $this->adapter->deleteNamespace($namespace);
}
/**
* @param array $options
* @param array $filterTypes
*
* @return array
*
* @throws Exception
*/
public function getCollection(array $options, array $filterTypes = [])
{
$data = $this->adapter->getCollection($options, $filterTypes);
$keys = [];
foreach ($data as $node) {
$keys[] = $this->getNamespace().':document-'.$node;
}
$nodes = (!empty($keys)) ? $this->getRedis()->mget($keys) : [];
foreach ($data as $i => &$node) {
$temp = (!empty($nodes[$i])) ? $this->parseRelations(\json_decode($nodes[$i], true)) : $this->getDocument($node);
if (!empty($temp)) {
$node = $temp;
}
}
return $data;
}
/**
* @param array $options
*
* @return int
*
* @throws Exception
*/
public function getCount(array $options)
{
return $this->adapter->getCount($options);
}
/**
* Last Modified.
*
* Return Unix timestamp of last time a node queried in current session has been changed
*
* @return int
*/
public function lastModified()
{
return 0;
}
/**
* @return array
*/
public function getDebug()
{
return $this->adapter->getDebug();
}
/**
* @throws Exception
*
* @return Client
*/
protected function getRedis(): Client
{
return $this->redis;
}
/**
* Set Namespace.
*
* Set namespace to divide different scope of data sets
*
* @param $namespace
*
* @return bool
*
* @throws Exception
*/
public function setNamespace($namespace)
{
$this->adapter->setNamespace($namespace);
return parent::setNamespace($namespace);
}
}

View file

@ -1,628 +0,0 @@
<?php
namespace Appwrite\Database;
use Exception;
use Appwrite\Database\Validator\Authorization;
use Appwrite\Database\Validator\Structure;
use Appwrite\Database\Exception\Authorization as AuthorizationException;
use Appwrite\Database\Exception\Structure as StructureException;
class Database
{
// System Core
const SYSTEM_COLLECTION_COLLECTIONS = 0;
const SYSTEM_COLLECTION_RULES = 'rules';
// Project
const SYSTEM_COLLECTION_PROJECTS = 'projects';
const SYSTEM_COLLECTION_WEBHOOKS = 'webhooks';
const SYSTEM_COLLECTION_KEYS = 'keys';
const SYSTEM_COLLECTION_TASKS = 'tasks';
const SYSTEM_COLLECTION_PLATFORMS = 'platforms';
const SYSTEM_COLLECTION_USAGES = 'usages'; // TODO add structure
const SYSTEM_COLLECTION_DOMAINS = 'domains';
const SYSTEM_COLLECTION_CERTIFICATES = 'certificates';
const SYSTEM_COLLECTION_RESERVED = 'reserved';
// Auth, Account and Users (private to user)
const SYSTEM_COLLECTION_USERS = 'users';
const SYSTEM_COLLECTION_SESSIONS = 'sessions';
const SYSTEM_COLLECTION_TOKENS = 'tokens';
// Teams (shared among team members)
const SYSTEM_COLLECTION_MEMBERSHIPS = 'memberships';
const SYSTEM_COLLECTION_TEAMS = 'teams';
// Storage
const SYSTEM_COLLECTION_FILES = 'files';
// Functions
const SYSTEM_COLLECTION_FUNCTIONS = 'functions';
const SYSTEM_COLLECTION_TAGS = 'tags';
const SYSTEM_COLLECTION_EXECUTIONS = 'executions';
// Realtime
const SYSTEM_COLLECTION_CONNECTIONS = 'connections';
// Var Types
const SYSTEM_VAR_TYPE_TEXT = 'text';
const SYSTEM_VAR_TYPE_NUMERIC = 'numeric';
const SYSTEM_VAR_TYPE_BOOLEAN = 'boolean';
const SYSTEM_VAR_TYPE_DOCUMENT = 'document';
const SYSTEM_VAR_TYPE_WILDCARD = 'wildcard';
const SYSTEM_VAR_TYPE_EMAIL = 'email';
const SYSTEM_VAR_TYPE_IP = 'ip';
const SYSTEM_VAR_TYPE_URL = 'url';
const SYSTEM_VAR_TYPE_KEY = 'key';
/**
* @var array
*/
protected static $filters = [];
/**
* @var bool
*/
protected static $statusFilters = true;
/**
* @var array
*/
protected $mocks = [];
/**
* @var Adapter
*/
protected $adapter;
/**
* Set Adapter.
*
* @param Adapter $adapter
*
* @return $this
*/
public function setAdapter(Adapter $adapter)
{
$this->adapter = $adapter;
return $this;
}
/**
* Set Namespace.
*
* Set namespace to divide different scope of data sets
*
* @param $namespace
*
* @return $this
*
* @throws Exception
*/
public function setNamespace($namespace)
{
$this->adapter->setNamespace($namespace);
return $this;
}
/**
* Get Namespace.
*
* Get namespace of current set scope
*
* @return string
*
* @throws Exception
*/
public function getNamespace()
{
return $this->adapter->getNamespace();
}
/**
* Create Namespace.
*
* @param string $namespace
*
* @return bool
*/
public function createNamespace($namespace)
{
return $this->adapter->createNamespace($namespace);
}
/**
* Delete Namespace.
*
* @param string $namespace
*
* @return bool
*/
public function deleteNamespace($namespace)
{
return $this->adapter->deleteNamespace($namespace);
}
/**
* @param array $options
* @param array $filterTypes
*
* @return Document[]
*/
public function getCollection(array $options, array $filterTypes = [])
{
$options = \array_merge([
'offset' => 0,
'limit' => 15,
'search' => '',
'relations' => true,
'orderField' => '',
'orderType' => 'ASC',
'orderCast' => 'int',
'filters' => [],
], $options);
$results = $this->adapter->getCollection($options, $filterTypes);
foreach ($results as &$node) {
$node = $this->decode(new Document($node));
}
return $results;
}
/**
* @param array $options
*
* @return Document
*/
public function getCollectionFirst(array $options)
{
$results = $this->getCollection($options);
return \reset($results);
}
/**
* @param array $options
*
* @return Document
*/
public function getCollectionLast(array $options)
{
$results = $this->getCollection($options);
return \end($results);
}
/**
* @param string $id
* @param bool $mock is mocked data allowed?
* @param bool $decode enable decoding?
*
* @return Document
*/
public function getDocument($id, bool $mock = true, bool $decode = true)
{
if (\is_null($id)) {
return new Document();
}
$document = new Document((isset($this->mocks[$id]) && $mock) ? $this->mocks[$id] : $this->adapter->getDocument($id));
$validator = new Authorization($document, 'read');
if (!$validator->isValid($document->getPermissions())) { // Check if user has read access to this document
return new Document();
}
$document = ($decode) ? $this->decode($document) : $document;
return $document;
}
/**
* @param array $data
*
* @return Document
*
* @throws AuthorizationException
* @throws StructureException
*/
public function createDocument(array $data, array $unique = [])
{
$document = new Document($data);
$validator = new Authorization($document, 'write');
if (!$validator->isValid($document->getPermissions())) { // Check if user has write access to this document
throw new AuthorizationException($validator->getDescription());
}
$validator = new Structure($this);
$document = $this->encode($document);
if (!$validator->isValid($document)) {
throw new StructureException($validator->getDescription()); // var_dump($validator->getDescription()); return false;
}
$document = new Document($this->adapter->createDocument($document->getArrayCopy(), $unique));
$document = $this->decode($document);
return $document;
}
/**
* @param array $data
*
* @return Document|false
*
* @throws Exception
*/
public function updateDocument(array $data)
{
if (!isset($data['$id'])) {
throw new Exception('Must define $id attribute');
}
$document = $this->getDocument($data['$id']); // TODO make sure user don\'t need read permission for write operations
// Make sure reserved keys stay constant
$data['$id'] = $document->getId();
$data['$collection'] = $document->getCollection();
$validator = new Authorization($document, 'write');
if (!$validator->isValid($document->getPermissions())) { // Check if user has write access to this document
throw new AuthorizationException($validator->getDescription()); // var_dump($validator->getDescription()); return false;
}
$new = new Document($data);
if (!$validator->isValid($new->getPermissions())) { // Check if user has write access to this document
throw new AuthorizationException($validator->getDescription()); // var_dump($validator->getDescription()); return false;
}
$new = $this->encode($new);
$validator = new Structure($this);
if (!$validator->isValid($new)) { // Make sure updated structure still apply collection rules (if any)
throw new StructureException($validator->getDescription()); // var_dump($validator->getDescription()); return false;
}
$new = new Document($this->adapter->updateDocument($new->getArrayCopy()));
$new = $this->decode($new);
return $new;
}
/**
* @param array $data
*
* @return Document|false
*
* @throws Exception
*/
public function overwriteDocument(array $data)
{
if (!isset($data['$id'])) {
throw new Exception('Must define $id attribute');
}
$document = $this->getDocument($data['$id']); // TODO make sure user don\'t need read permission for write operations
$validator = new Authorization($document, 'write');
if (!$validator->isValid($document->getPermissions())) { // Check if user has write access to this document
throw new AuthorizationException($validator->getDescription()); // var_dump($validator->getDescription()); return false;
}
$new = new Document($data);
if (!$validator->isValid($new->getPermissions())) { // Check if user has write access to this document
throw new AuthorizationException($validator->getDescription()); // var_dump($validator->getDescription()); return false;
}
$new = $this->encode($new);
$validator = new Structure($this);
if (!$validator->isValid($new)) { // Make sure updated structure still apply collection rules (if any)
throw new StructureException($validator->getDescription()); // var_dump($validator->getDescription()); return false;
}
$new = new Document($this->adapter->updateDocument($new->getArrayCopy()));
$new = $this->decode($new);
return $new;
}
/**
* @param string $id
*
* @return Document|false
*
* @throws AuthorizationException
*/
public function deleteDocument(string $id)
{
$document = $this->getDocument($id);
$validator = new Authorization($document, 'write');
if (!$validator->isValid($document->getPermissions())) { // Check if user has write access to this document
throw new AuthorizationException($validator->getDescription());
}
return new Document($this->adapter->deleteDocument($id));
}
/**
* @param int $key
*
* @return Document|false
*
* @throws AuthorizationException
*/
public function deleteUniqueKey($key)
{
return new Document($this->adapter->deleteUniqueKey($key));
}
/**
* @param int $key
*
* @return Document|false
*
* @throws AuthorizationException
*/
public function addUniqueKey($key)
{
return new Document($this->adapter->addUniqueKey($key));
}
/**
* @return array
*/
public function getDebug()
{
return $this->adapter->getDebug();
}
/**
* @return int
*/
public function getSum()
{
$debug = $this->getDebug();
return (isset($debug['sum'])) ? $debug['sum'] : 0;
}
/**
* @param array $options
*
* @return int
*/
public function getCount(array $options)
{
$options = \array_merge([
'filters' => [],
], $options);
$results = $this->adapter->getCount($options);
return $results;
}
/**
* @param string $key
* @param string $value
*
* @return self
*/
public function setMock($key, $value): self
{
$this->mocks[$key] = $value;
return $this;
}
/**
* @param array $mocks
*
* @return self
*/
public function setMocks(array $mocks): self
{
$this->mocks = $mocks;
return $this;
}
/**
* @return array
*/
public function getMocks()
{
return $this->mocks;
}
/**
* Add Attribute Filter
*
* @param string $name
* @param callable $encode
* @param callable $decode
*
* @return void
*/
public static function addFilter(string $name, callable $encode, callable $decode): void
{
self::$filters[$name] = [
'encode' => $encode,
'decode' => $decode,
];
}
/**
* Disable Attribute decoding
*
* @return void
*/
public static function disableFilters(): void
{
self::$statusFilters = false;
}
/**
* Enable Attribute decoding
*
* @return void
*/
public static function enableFilters(): void
{
self::$statusFilters = true;
}
public function encode(Document $document):Document
{
if (!self::$statusFilters) {
return $document;
}
$collection = $this->getDocument($document->getCollection(), true, false);
$rules = $collection->getAttribute('rules', []);
foreach ($rules as $key => $rule) {
$key = $rule->getAttribute('key', null);
$type = $rule->getAttribute('type', null);
$array = $rule->getAttribute('array', false);
$filters = $rule->getAttribute('filter', []);
$value = $document->getAttribute($key, null);
if (($value !== null)) {
if ($type === self::SYSTEM_VAR_TYPE_DOCUMENT) {
if ($array) {
$list = [];
foreach ($value as $child) {
$list[] = $this->encode($child);
}
$document->setAttribute($key, $list);
} else {
$document->setAttribute($key, $this->encode($value));
}
} else {
foreach ($filters as $filter) {
$value = $this->encodeAttribute($filter, $value);
$document->setAttribute($key, $value);
}
}
}
}
return $document;
}
public function decode(Document $document):Document
{
if (!self::$statusFilters) {
return $document;
}
$collection = $this->getDocument($document->getCollection(), true, false);
$rules = $collection->getAttribute('rules', []);
foreach ($rules as $key => $rule) {
$key = $rule->getAttribute('key', null);
$type = $rule->getAttribute('type', null);
$array = $rule->getAttribute('array', false);
$filters = $rule->getAttribute('filter', []);
$value = $document->getAttribute($key, null);
if (($value !== null)) {
if ($type === self::SYSTEM_VAR_TYPE_DOCUMENT) {
if ($array) {
$list = [];
foreach ($value as $child) {
$list[] = $this->decode($child);
}
$document->setAttribute($key, $list);
} else {
$document->setAttribute($key, $this->decode($value));
}
} else {
foreach (array_reverse($filters) as $filter) {
$value = $this->decodeAttribute($filter, $value);
$document->setAttribute($key, $value);
}
}
}
}
return $document;
}
/**
* Encode Attribute
*
* @param string $name
* @param mixed $value
*/
protected static function encodeAttribute(string $name, $value)
{
if (!isset(self::$filters[$name])) {
return $value;
throw new Exception("Filter '{$name}' not found");
}
try {
$value = self::$filters[$name]['encode']($value);
} catch (\Throwable $th) {
$value = null;
}
return $value;
}
/**
* Decode Attribute
*
* @param string $name
* @param mixed $value
*/
protected static function decodeAttribute(string $name, $value)
{
if (!isset(self::$filters[$name])) {
return $value;
throw new Exception("Filter '{$name}' not found");
}
try {
$value = self::$filters[$name]['decode']($value);
} catch (\Throwable $th) {
$value = null;
}
return $value;
}
/**
* Get Last Modified.
*
* Return Unix timestamp of last time a node queried in current session has been changed
*
* @return int
*/
public function lastModified()
{
return $this->adapter->lastModified();
}
}

View file

@ -1,274 +0,0 @@
<?php
namespace Appwrite\Database;
use ArrayObject;
class Document extends ArrayObject
{
const SET_TYPE_ASSIGN = 'assign';
const SET_TYPE_PREPEND = 'prepend';
const SET_TYPE_APPEND = 'append';
/**
* Construct.
*
* Construct a new fields object
*
* @see ArrayObject::__construct
*
* @param array $input
* @param int $flags
* @param string $iterator_class
*/
public function __construct($input = [], $flags = 0, $iterator_class = 'ArrayIterator')
{
foreach ($input as $key => &$value) {
if (\is_array($value)) {
if ((isset($value['$id']) || isset($value['$collection'])) && (!$value instanceof self)) {
$input[$key] = new self($value);
} else {
foreach ($value as $childKey => $child) {
if ((isset($child['$id']) || isset($child['$collection'])) && (!$child instanceof self)) {
$value[$childKey] = new self($child);
}
}
}
}
}
parent::__construct($input, $flags, $iterator_class);
}
/**
* @return string|null
*/
public function getId()
{
return $this->getAttribute('$id', null);
}
/**
* @return string
*/
public function getCollection()
{
return $this->getAttribute('$collection', null);
}
/**
* @return array
*/
public function getPermissions()
{
return $this->getAttribute('$permissions', []);
}
/**
* Get Attribute.
*
* Method for getting a specific fields attribute. If $name is not found $default value will be returned.
*
* @param string $name
* @param mixed $default
*
* @return mixed
*/
public function getAttribute($name, $default = null)
{
$name = \explode('.', $name);
$temp = &$this;
foreach ($name as $key) {
if (!isset($temp[$key])) {
return $default;
}
$temp = &$temp[$key];
}
return $temp;
}
/**
* Get Document Attributes
*
* @return array
*/
public function getAttributes(): array
{
$attributes = [];
foreach ($this as $attribute => $value) {
if(array_key_exists($attribute, ['$id' => true, '$permissions' => true, '$collection' => true, '$execute' => []])) {
continue;
}
$attributes[$attribute] = $value;
}
return $attributes;
}
/**
* Set Attribute.
*
* Method for setting a specific field attribute
*
* @param string $key
* @param mixed $value
* @param string $type
*
* @return mixed
*/
public function setAttribute($key, $value, $type = self::SET_TYPE_ASSIGN)
{
switch ($type) {
case self::SET_TYPE_ASSIGN:
$this[$key] = $value;
break;
case self::SET_TYPE_APPEND:
$this[$key] = (!isset($this[$key]) || !\is_array($this[$key])) ? [] : $this[$key];
\array_push($this[$key], $value);
break;
case self::SET_TYPE_PREPEND:
$this[$key] = (!isset($this[$key]) || !\is_array($this[$key])) ? [] : $this[$key];
\array_unshift($this[$key], $value);
break;
}
return $this;
}
/**
* Remove Attribute.
*
* Method for removing a specific field attribute
*
* @param string $key
* @param mixed $value
* @param string $type
*
* @return mixed
*/
public function removeAttribute($key)
{
if (isset($this[$key])) {
unset($this[$key]);
}
return $this;
}
/**
* Search.
*
* Get array child by key and value match
*
* @param $key
* @param $value
* @param array|null $scope
*
* @return Document|Document[]|mixed|null|array
*/
public function search($key, $value, $scope = null)
{
$array = (!\is_null($scope)) ? $scope : $this;
if (\is_array($array) || $array instanceof self) {
if (isset($array[$key]) && $array[$key] == $value) {
return $array;
}
foreach ($array as $k => $v) {
if ((\is_array($v) || $v instanceof self) && (!empty($v))) {
$result = $this->search($key, $value, $v);
if (!empty($result)) {
return $result;
}
} else {
if ($k === $key && $v === $value) {
return $array;
}
}
}
}
if ($array === $value) {
return $array;
}
return;
}
/**
* Checks if document has data.
*
* @return bool
*/
public function isEmpty()
{
return empty($this->getId());
}
/**
* Checks if a document key is set.
*
* @param string $key
*
* @return bool
*/
public function isSet($key)
{
return isset($this[$key]);
}
/**
* Get Array Copy.
*
* Outputs entity as a PHP array
*
* @param array $whitelist
* @param array $blacklist
*
* @return array
*/
public function getArrayCopy(array $whitelist = [], array $blacklist = []): array
{
$array = parent::getArrayCopy();
$output = [];
foreach ($array as $key => &$value) {
if (!empty($whitelist) && !\in_array($key, $whitelist)) { // Export only whitelisted fields
continue;
}
if (!empty($blacklist) && \in_array($key, $blacklist)) { // Don't export blacklisted fields
continue;
}
if ($value instanceof self) {
$output[$key] = $value->getArrayCopy($whitelist, $blacklist);
} elseif (\is_array($value)) {
foreach ($value as $childKey => &$child) {
if ($child instanceof self) {
$output[$key][$childKey] = $child->getArrayCopy($whitelist, $blacklist);
} else {
$output[$key][$childKey] = $child;
}
}
if (empty($value)) {
$output[$key] = $value;
}
} else {
$output[$key] = $value;
}
}
return $output;
}
}

View file

@ -1,7 +0,0 @@
<?php
namespace Appwrite\Database\Exception;
class Authorization extends \Exception
{
}

View file

@ -1,7 +0,0 @@
<?php
namespace Appwrite\Database\Exception;
class Duplicate extends \Exception
{
}

View file

@ -1,7 +0,0 @@
<?php
namespace Appwrite\Database\Exception;
class Structure extends \Exception
{
}

View file

@ -1,215 +0,0 @@
<?php
namespace Appwrite\Database\Validator;
use Appwrite\Database\Document;
use Utopia\Validator;
class Authorization extends Validator
{
/**
* @var array
*/
public static $roles = ['*' => true];
/**
* @var Document
*/
protected $document;
/**
* @var string
*/
protected $action = '';
/**
* @var string
*/
protected $message = 'Authorization Error';
/**
* Structure constructor.
*
* @param Document $document
* @param string $action
*/
public function __construct(Document $document, $action)
{
$this->document = $document;
$this->action = $action;
}
/**
* Get Description.
*
* Returns validator description
*
* @return string
*/
public function getDescription(): string
{
return $this->message;
}
/**
* Is valid.
*
* Returns true if valid or false if not.
*
* @param mixed $permissions
*
* @return bool
*/
public function isValid($permissions): bool
{
if (!self::$status) {
return true;
}
if (!isset($permissions[$this->action])) {
$this->message = 'Missing action key: "'.$this->action.'"';
return false;
}
$permission = null;
foreach ($permissions[$this->action] as $permission) {
$permission = \str_replace(':{self}', ':'.$this->document->getId(), $permission);
if (\array_key_exists($permission, self::$roles)) {
return true;
}
}
$this->message = 'Missing "'.$this->action.'" permission for role "'.$permission.'". Only this scopes "'.\json_encode(self::getRoles()).'" are given and only this are allowed "'.\json_encode($permissions[$this->action]).'".';
return false;
}
/**
* @param string $role
*
* @return void
*/
public static function setRole(string $role): void
{
self::$roles[$role] = true;
}
/**
* @param string $role
*
* @return void
*/
public static function unsetRole(string $role): void
{
unset(self::$roles[$role]);
}
/**
* @return array
*/
public static function getRoles(): array
{
return \array_keys(self::$roles);
}
/**
* @return void
*/
public static function cleanRoles(): void
{
self::$roles = [];
}
/**
* @param string $role
*
* @return bool
*/
public static function isRole(string $role): bool
{
return (\array_key_exists($role, self::$roles));
}
/**
* @var bool
*/
public static $status = true;
/**
* Default value in case we need
* to reset Authorization status
*
* @var bool
*/
public static $statusDefault = true;
/**
* Change default status.
* This will be used for the
* value set on the self::reset() method
*
* @return void
*/
public static function setDefaultStatus($status): void
{
self::$statusDefault = $status;
self::$status = $status;
}
/**
* Enable Authorization checks
*
* @return void
*/
public static function enable(): void
{
self::$status = true;
}
/**
* Disable Authorization checks
*
* @return void
*/
public static function disable(): void
{
self::$status = false;
}
/**
* Disable Authorization checks
*
* @return void
*/
public static function reset(): void
{
self::$status = self::$statusDefault;
}
/**
* Is array
*
* Function will return true if object is array.
*
* @return bool
*/
public function isArray(): bool
{
return false;
}
/**
* Get Type
*
* Returns validator type.
*
* @return string
*/
public function getType(): string
{
return self::TYPE_ARRAY;
}
}

View file

@ -1,62 +0,0 @@
<?php
namespace Appwrite\Database\Validator;
use Appwrite\Database\Database;
use Appwrite\Database\Document;
class Collection extends Structure
{
/**
* @var array
*/
protected $collections = [];
/**
* @var array
*/
protected $merge = [];
/**
* @param Database $database
* @param array $collections
* @param array $merge
*/
public function __construct(Database $database, array $collections, array $merge = [])
{
$this->collections = $collections;
$this->merge = $merge;
return parent::__construct($database);
}
/**
* Is valid.
*
* Returns true if valid or false if not.
*
* @param mixed $document
*
* @return bool
*/
public function isValid($document): bool
{
$document = new Document(
\array_merge($this->merge, ($document instanceof Document) ? $document->getArrayCopy() : $document)
);
if (\is_null($document->getCollection())) {
$this->message = 'Missing collection attribute $collection';
return false;
}
if (!\in_array($document->getCollection(), $this->collections)) {
$this->message = 'Collection is not allowed';
return false;
}
return parent::isValid($document);
}
}

View file

@ -1,105 +0,0 @@
<?php
namespace Appwrite\Database\Validator;
use Appwrite\Database\Database;
use Appwrite\Database\Document;
use Utopia\Validator;
class DocumentId extends Validator
{
/**
* @var string
*/
protected $message = 'Document not found.';
/**
* @var Database
*/
protected $database;
/**
* @var string
*/
protected $collection = '';
/**
* Structure constructor.
*
* @param Database $database
* @param string $collection
*/
public function __construct(Database $database, string $collection = '')
{
$this->database = $database;
$this->collection = $collection;
}
/**
* Get Description.
*
* Returns validator description
*
* @return string
*/
public function getDescription(): string
{
return $this->message;
}
/**
* Is valid.
*
* Returns true if valid or false if not.
*
* @param $value
*
* @return bool
*/
public function isValid($id): bool
{
$document = $this->database->getDocument($id);
if (!$document) {
return false;
}
if (!$document instanceof Document) {
return false;
}
if (!$document->getId()) {
return false;
}
if ($document->getCollection() !== $this->collection) {
return false;
}
return true;
}
/**
* Is array
*
* Function will return true if object is array.
*
* @return bool
*/
public function isArray(): bool
{
return false;
}
/**
* Get Type
*
* Returns validator type.
*
* @return string
*/
public function getType(): string
{
return self::TYPE_STRING;
}
}

View file

@ -1,74 +0,0 @@
<?php
namespace Appwrite\Database\Validator;
use Utopia\Validator;
class Key extends Validator
{
/**
* @var string
*/
protected $message = 'Parameter must contain only letters with no spaces or special chars and be shorter than 32 chars';
/**
* Get Description.
*
* Returns validator description
*
* @return string
*/
public function getDescription(): string
{
return $this->message;
}
/**
* Is valid.
*
* Returns true if valid or false if not.
*
* @param $value
*
* @return bool
*/
public function isValid($value): bool
{
if (!\is_string($value)) {
return false;
}
if (\preg_match('/[^A-Za-z0-9\-\_]/', $value)) {
return false;
}
if (\mb_strlen($value) > 32) {
return false;
}
return true;
}
/**
* Is array
*
* Function will return true if object is array.
*
* @return bool
*/
public function isArray(): bool
{
return false;
}
/**
* Get Type
*
* Returns validator type.
*
* @return string
*/
public function getType(): string
{
return self::TYPE_STRING;
}
}

View file

@ -1,100 +0,0 @@
<?php
namespace Appwrite\Database\Validator;
use Appwrite\Database\Document;
use Utopia\Validator;
class Permissions extends Validator
{
/**
* @var string
*/
protected $message = 'Permissions Error';
/**
* @var Document
*/
protected $document;
/**
* Structure constructor.
*
* @param Document $document
*/
public function __construct(Document $document)
{
$this->document = $document;
}
/**
* Get Description.
*
* Returns validator description
*
* @return string
*/
public function getDescription(): string
{
return $this->message;
}
/**
* Is valid.
*
* Returns true if valid or false if not.
*
* @param mixed $value
*
* @return bool
*/
public function isValid($value): bool
{
if (!\is_array($value) && !empty($value)) {
$this->message = 'Invalid permissions data structure';
return false;
}
foreach ($value as $action => $roles) {
if (!\in_array($action, ['read', 'write', 'execute'])) {
$this->message = 'Unknown action ("'.$action.'")';
return false;
}
foreach ($roles as $role) {
if (!\is_string($role)) {
$this->message = 'Permissions role must be of type string.';
return false;
}
}
}
return true;
}
/**
* Is array
*
* Function will return true if object is array.
*
* @return bool
*/
public function isArray(): bool
{
return false;
}
/**
* Get Type
*
* Returns validator type.
*
* @return string
*/
public function getType(): string
{
return self::TYPE_ARRAY;
}
}

View file

@ -1,313 +0,0 @@
<?php
namespace Appwrite\Database\Validator;
use Appwrite\Database\Database;
use Appwrite\Database\Document;
use Appwrite\Network\Validator as NetworkValidator;
use Utopia\Validator;
class Structure extends Validator
{
const RULE_TYPE_ID = 'id';
const RULE_TYPE_PERMISSIONS = 'permissions';
const RULE_TYPE_KEY = 'key';
const RULE_TYPE_TEXT = 'text';
const RULE_TYPE_MARKDOWN = 'markdown';
const RULE_TYPE_NUMERIC = 'numeric';
const RULE_TYPE_BOOLEAN = 'boolean';
const RULE_TYPE_EMAIL = 'email';
const RULE_TYPE_URL = 'url';
const RULE_TYPE_IP = 'ip';
const RULE_TYPE_WILDCARD = 'wildcard';
const RULE_TYPE_DOCUMENT = 'document';
const RULE_TYPE_DOCUMENTID = 'documentId';
const RULE_TYPE_FILEID = 'fileId';
/**
* @var Database
*/
protected $database;
/**
* @var string
*/
protected $id = '';
/**
* Basic rules to apply on all documents.
*
* @var array
*/
protected $rules = [
[
'label' => '$id',
'$collection' => Database::SYSTEM_COLLECTION_RULES,
'key' => '$id',
'type' => 'id',
'default' => null,
'required' => false,
'array' => false,
],
[
'label' => '$collection',
'$collection' => Database::SYSTEM_COLLECTION_RULES,
'key' => '$collection',
'type' => 'id',
'default' => null,
'required' => true,
'array' => false,
],
[
'label' => '$permissions',
'$collection' => Database::SYSTEM_COLLECTION_RULES,
'key' => '$permissions',
'type' => 'permissions',
'default' => null,
'required' => true,
'array' => false,
],
[
'label' => '$createdAt',
'$collection' => Database::SYSTEM_COLLECTION_RULES,
'key' => '$createdAt',
'type' => 'numeric',
'default' => null,
'required' => false,
'array' => false,
],
[
'label' => '$updatedAt',
'$collection' => Database::SYSTEM_COLLECTION_RULES,
'key' => '$updatedAt',
'type' => 'numeric',
'default' => null,
'required' => false,
'array' => false,
],
];
/**
* @var string
*/
protected $message = 'General Error';
/**
* Structure constructor.
*
* @param Database $database
*/
public function __construct(Database $database)
{
$this->database = $database;
}
/**
* Get Description.
*
* Returns validator description
*
* @return string
*/
public function getDescription(): string
{
return 'Invalid document structure: '.$this->message;
}
/**
* Is valid.
*
* Returns true if valid or false if not.
*
* @param mixed $document
*
* @return bool
*/
public function isValid($document): bool
{
$document = (\is_array($document)) ? new Document($document) : $document;
$this->id = $document->getId();
if (\is_null($document->getCollection())) {
$this->message = 'Missing collection attribute $collection';
return false;
}
$collection = $this->getCollection($document->getCollection());
if (\is_null($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) {
$this->message = 'Collection not found';
return false;
}
$array = $document->getArrayCopy();
$rules = \array_merge($this->rules, $collection->getAttribute('rules', []));
foreach ($rules as $rule) { // Check all required keys are set
if (isset($rule['key']) && !isset($array[$rule['key']])
&& isset($rule['required']) && true == $rule['required']) {
$this->message = 'Missing required key "'.$rule['key'].'"';
return false;
}
}
foreach ($array as $key => $value) {
$rule = $collection->search('key', $key, $rules);
if (!$rule) {
continue;
}
$ruleType = $rule['type'] ?? '';
$ruleRequired = $rule['required'] ?? true;
$ruleArray = $rule['array'] ?? false;
$validator = null;
switch ($ruleType) {
case self::RULE_TYPE_ID:
$validator = new UID();
break;
case self::RULE_TYPE_PERMISSIONS:
$validator = new Permissions($document); //$validator = ($this->forcePermissions) ? new Authorization($original, 'write') : new Validator\Mock();
break;
case self::RULE_TYPE_KEY:
$validator = new Key();
break;
case self::RULE_TYPE_TEXT:
case self::RULE_TYPE_MARKDOWN:
$validator = new Validator\Text(0);
break;
case self::RULE_TYPE_NUMERIC:
$validator = new Validator\Numeric();
break;
case self::RULE_TYPE_BOOLEAN:
$validator = new Validator\Boolean();
break;
case self::RULE_TYPE_EMAIL:
$validator = new NetworkValidator\Email();
break;
case self::RULE_TYPE_URL:
$validator = new NetworkValidator\URL();
break;
case self::RULE_TYPE_IP:
$validator = new NetworkValidator\IP();
break;
case self::RULE_TYPE_WILDCARD:
$validator = new Validator\Wildcard();
break;
case self::RULE_TYPE_DOCUMENT:
$validator = new Collection($this->database, (isset($rule['list'])) ? $rule['list'] : []);
$value = $document->getAttribute($key);
break;
case self::RULE_TYPE_DOCUMENTID:
$validator = new DocumentId($this->database, (isset($rule['list']) && isset($rule['list'][0])) ? $rule['list'][0] : '');
$value = $document->getAttribute($key);
break;
case self::RULE_TYPE_FILEID:
$validator = new DocumentId($this->database, Database::SYSTEM_COLLECTION_FILES);
$value = $document->getAttribute($key);
break;
}
if (empty($validator)) { // Error creating validator for property
$this->message = 'Unknown rule type "'.$ruleType.'" for property "'.\htmlspecialchars($key, ENT_QUOTES, 'UTF-8').'"';
if (empty($ruleType)) {
$this->message = 'Unknown property "'.$key.'" type'.
'. Make sure to follow '.\strtolower($collection->getAttribute('name', 'unknown')).' collection structure';
}
return false;
}
if ($ruleRequired && ('' === $value || null === $value)) {
$this->message = 'Required property "'.$key.'" has no value';
return false;
}
if (!$ruleRequired && empty($value)) {
unset($array[$key]);
unset($rule);
continue;
}
if ($ruleArray) { // Array of values validation
if (!\is_array($value)) {
$this->message = 'Property "'.$key.'" must be an array';
return false;
}
// TODO add is required check here
foreach ($value as $node) {
if (!$validator->isValid($node)) { // Check if property is valid, if not required can also be empty
$this->message = 'Property "'.$key.'" has invalid input. '.$validator->getDescription();
return false;
}
}
} else { // Single value validation
if ((!$validator->isValid($value)) && !('' === $value && !$ruleRequired)) { // Error when value is not valid, and is not optional and empty
$this->message = 'Property "'.$key.'" has invalid input. '.$validator->getDescription();
return false;
}
}
unset($array[$key]);
unset($rule);
}
if (!empty($array)) { // No fields should be left unvalidated
$this->message = 'Unknown properties are not allowed ('.\implode(', ', \array_keys($array)).') for this collection'.
'. Make sure to follow '.\strtolower($collection->getAttribute('name', 'unknown')).' collection structure';
return false;
}
return true;
}
/**
* Get Collection
*
* Get Collection by unique ID
*
* @return Document
*/
protected function getCollection($id): Document
{
return $this->database->getDocument($id);
}
/**
* Is array
*
* Function will return true if object is array.
*
* @return bool
*/
public function isArray(): bool
{
return false;
}
/**
* Get Type
*
* Returns validator type.
*
* @return string
*/
public function getType(): string
{
return self::TYPE_OBJECT;
}
}

View file

@ -1,70 +0,0 @@
<?php
namespace Appwrite\Database\Validator;
use Utopia\Validator;
class UID extends Validator
{
/**
* Get Description.
*
* Returns validator description
*
* @return string
*/
public function getDescription(): string
{
return 'Invalid UID format';
}
/**
* Is valid.
*
* Returns true if valid or false if not.
*
* @param mixed $value
*
* @return bool
*/
public function isValid($value): bool
{
if ($value === 0) { // TODO Deprecate confition when we get the chance.
return true;
}
if (!is_string($value)) {
return false;
}
if (mb_strlen($value) > 32) {
return false;
}
return true;
}
/**
* Is array
*
* Function will return true if object is array.
*
* @return bool
*/
public function isArray(): bool
{
return false;
}
/**
* Get Type
*
* Returns validator type.
*
* @return string
*/
public function getType(): string
{
return self::TYPE_STRING;
}
}

View file

@ -2,107 +2,58 @@
namespace Appwrite\Migration;
use Appwrite\Database\Document as OldDocument;
use Appwrite\Database\Database as OldDatabase;
use PDO;
use Redis;
use Swoole\Runtime;
use Utopia\Database\Document;
use Utopia\Database\Database;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Exception;
abstract class Migration
{
/**
* @var array
*/
protected array $options;
/**
* @var PDO
*/
protected PDO $db;
/**
* @var Redis
*/
protected Redis $cache;
/**
* @var int
*/
protected int $limit = 500;
protected int $limit = 100;
/**
* @var OldDocument
* @var Document
*/
protected OldDocument $project;
protected Document $project;
/**
* @var OldDatabase
* @var Database
*/
protected OldDatabase $oldProjectDB;
protected Database $projectDB;
/**
* @var OldDatabase
* @var Database
*/
protected OldDatabase $oldConsoleDB;
protected Database $consoleDB;
/**
* @var array
*/
public static array $versions = [
'0.6.0' => 'V05',
'0.7.0' => 'V06',
'0.8.0' => 'V07',
'0.9.0' => 'V08',
'0.9.1' => 'V08',
'0.9.2' => 'V08',
'0.9.3' => 'V08',
'0.9.4' => 'V08',
'0.10.0' => 'V09',
'0.10.1' => 'V09',
'0.10.2' => 'V09',
'0.10.3' => 'V09',
'0.10.4' => 'V09',
'0.11.0' => 'V10',
'0.12.0' => 'V11',
'0.12.1' => 'V11',
'0.13.0' => 'V12',
];
/**
* Migration constructor.
*
* @param PDO $db
* @param Redis|null $cache
* @param array $options
* @return void
*/
public function __construct(PDO $db, Redis $cache = null, array $options = [])
{
$this->options = $options;
$this->db = $db;
if (!is_null($cache)) {
$this->cache = $cache;
}
}
/**
* Set project for migration.
*
* @param OldDocument $project
* @param OldDatabase $projectDB
* @param OldDatabase $oldConsoleDB
* @param Document $project
* @param Database $projectDB
* @param Database $oldConsoleDB
*
* @return self
*/
public function setProject(OldDocument $project, OldDatabase $projectDB, OldDatabase $oldConsoleDB): self
public function setProject(Document $project, Database $projectDB, Database $consoleDB): self
{
$this->project = $project;
$this->projectDB = $projectDB;
$this->projectDB->setNamespace('_project_' . $this->project->getId());
$this->oldProjectDB = $projectDB;
$this->oldProjectDB->setNamespace('app_' . $project->getId());
$this->oldConsoleDB = $oldConsoleDB;
$this->consoleDB = $consoleDB;
return $this;
}
@ -114,52 +65,72 @@ abstract class Migration
*/
public function forEachDocument(callable $callback): void
{
$sum = $this->limit;
$offset = 0;
Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
while ($sum >= $this->limit) {
$all = $this->projectDB->getCollection([
'limit' => $this->limit,
'offset' => $offset,
'orderType' => 'DESC',
]);
/** @var array $collections */
$collections = Config::getParam('collections', []);
$sum = \count($all);
Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
foreach ($collections as $collection) {
$sum = 0;
$nextDocument = null;
$collectionCount = $this->projectDB->count($collection['$id']);
Console::log('Migrating Collection ' . $collection['$id'] . ':');
Console::log('Migrating: ' . $offset . ' / ' . $this->projectDB->getSum());
\Co\run(function () use ($all, $callback) {
foreach ($all as $document) {
go(function () use ($document, $callback) {
if (empty($document->getId()) || empty($document->getCollection())) {
if ($document->getCollection() !== 0) {
Console::warning('Skipped Document due to missing ID or Collection.');
do {
$documents = $this->projectDB->find($collection['$id'], limit: $this->limit, cursor: $nextDocument);
$count = count($documents);
$sum += $count;
Console::log($sum . ' / ' . $collectionCount);
\Co\run(function (array $documents, callable $callback) {
foreach ($documents as $document) {
go(function (Document $document, callable $callback) {
if (empty($document->getId()) || empty($document->getCollection())) {
return;
}
return;
}
$old = $document->getArrayCopy();
$new = call_user_func($callback, $document);
$old = $document->getArrayCopy();
$new = call_user_func($callback, $document);
if (!$this->check_diff_multi($new->getArrayCopy(), $old)) {
return;
}
foreach ($document as &$attr) {
if ($attr instanceof Document) {
$attr = call_user_func($callback, $attr);
}
try {
$new = $this->projectDB->overwriteDocument($new->getArrayCopy());
} catch (\Throwable $th) {
Console::error('Failed to update document: ' . $th->getMessage());
return;
if ($document && $new->getId() !== $document->getId()) {
throw new Exception('Duplication Error');
if (\is_array($attr)) {
foreach ($attr as &$child) {
if ($child instanceof Document) {
$child = call_user_func($callback, $child);
}
}
}
}
}
});
if (!$this->check_diff_multi($new->getArrayCopy(), $old)) {
return;
}
try {
$new = $this->projectDB->updateDocument($document->getCollection(), $document->getId(), $document);
} catch (\Throwable $th) {
Console::error('Failed to update document: ' . $th->getMessage());
return;
if ($document && $new->getId() !== $document->getId()) {
throw new Exception('Duplication Error');
}
}
}, $document, $callback);
}
}, $documents, $callback);
if ($count !== $this->limit) {
$nextDocument = null;
} else {
$nextDocument = end($documents);
}
});
$offset += $this->limit;
} while (!is_null($nextDocument));
}
}

View file

@ -1,126 +0,0 @@
<?php
namespace Appwrite\Migration\Version;
use Appwrite\Migration\Migration;
use Utopia\Config\Config;
use Utopia\CLI\Console;
use Appwrite\Database\Database;
use Appwrite\Database\Document;
class V05 extends Migration
{
public function execute(): void
{
$db = $this->db;
$project = $this->project;
Console::log('Migrating project: ' . $project->getAttribute('name') . ' (' . $project->getId() . ')');
// Update all documents $uid -> $id
$this->forEachDocument([$this, 'fixDocument']);
$schema = $_SERVER['_APP_DB_SCHEMA'] ?? '';
try {
$statement = $db->prepare("
CREATE TABLE IF NOT EXISTS `template.database.unique` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`key` varchar(128) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `index1` (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `{$schema}`.`app_{$project->getId()}.database.unique` LIKE `template.database.unique`;
ALTER TABLE `{$schema}`.`app_{$project->getId()}.audit.audit` DROP COLUMN IF EXISTS `userType`;
ALTER TABLE `{$schema}`.`app_{$project->getId()}.audit.audit` DROP INDEX IF EXISTS `index_1`;
ALTER TABLE `{$schema}`.`app_{$project->getId()}.audit.audit` ADD INDEX IF NOT EXISTS `index_1` (`userId` ASC);
");
$statement->closeCursor();
$statement->execute();
} catch (\Exception $e) {
Console::error('Failed to alter table for project: ' . $project->getId() . ' with message: ' . $e->getMessage() . '/');
}
}
protected function fixDocument(Document $document)
{
$providers = Config::getParam('providers');
switch ($document->getAttribute('$collection')) {
case Database::SYSTEM_COLLECTION_PROJECTS:
foreach ($providers as $key => $provider) {
if (!empty($document->getAttribute('usersOauth' . \ucfirst($key) . 'Appid'))) {
$document
->setAttribute('usersOauth2' . \ucfirst($key) . 'Appid', $document->getAttribute('usersOauth' . \ucfirst($key) . 'Appid', ''))
->removeAttribute('usersOauth' . \ucfirst($key) . 'Appid');
}
if (!empty($document->getAttribute('usersOauth' . \ucfirst($key) . 'Secret'))) {
$document
->setAttribute('usersOauth2' . \ucfirst($key) . 'Secret', $document->getAttribute('usersOauth' . \ucfirst($key) . 'Secret', ''))
->removeAttribute('usersOauth' . \ucfirst($key) . 'Secret');
}
}
$document->setAttribute('security', $document->getAttribute('security') ? true : false);
break;
case Database::SYSTEM_COLLECTION_TASKS:
$document->setAttribute('security', $document->getAttribute('security') ? true : false);
break;
case Database::SYSTEM_COLLECTION_USERS:
foreach ($providers as $key => $provider) {
if (!empty($document->getAttribute('oauth' . \ucfirst($key)))) {
$document
->setAttribute('oauth2' . \ucfirst($key), $document->getAttribute('oauth' . \ucfirst($key), ''))
->removeAttribute('oauth' . \ucfirst($key));
}
if (!empty($document->getAttribute('oauth' . \ucfirst($key) . 'AccessToken'))) {
$document
->setAttribute('oauth2' . \ucfirst($key) . 'AccessToken', $document->getAttribute('oauth' . \ucfirst($key) . 'AccessToken', ''))
->removeAttribute('oauth' . \ucfirst($key) . 'AccessToken');
}
}
if ($document->getAttribute('confirm', null) !== null) {
$document
->setAttribute('emailVerification', $document->getAttribute('confirm', $document->getAttribute('emailVerification', false)))
->removeAttribute('confirm');
}
break;
case Database::SYSTEM_COLLECTION_PLATFORMS:
if ($document->getAttribute('url', null) !== null) {
$document
->setAttribute('hostname', \parse_url($document->getAttribute('url', $document->getAttribute('hostname', '')), PHP_URL_HOST))
->removeAttribute('url');
}
break;
}
$document
->setAttribute('$id', $document->getAttribute('$uid', $document->getAttribute('$id')))
->removeAttribute('$uid');
foreach ($document as &$attr) { // Handle child documents
if ($attr instanceof Document) {
$attr = $this->fixDocument($attr);
}
if (\is_array($attr)) {
foreach ($attr as &$child) {
if ($child instanceof Document) {
$child = $this->fixDocument($child);
}
}
}
}
return $document;
}
}

View file

@ -1,58 +0,0 @@
<?php
namespace Appwrite\Migration\Version;
use Utopia\App;
use Utopia\CLI\Console;
use Appwrite\Database\Database;
use Appwrite\Database\Document;
use Appwrite\Migration\Migration;
use Appwrite\OpenSSL\OpenSSL;
class V06 extends Migration
{
public function execute(): void
{
$project = $this->project;
Console::log('Migrating project: ' . $project->getAttribute('name') . ' (' . $project->getId() . ')');
$this->projectDB->disableFilters();
$this->forEachDocument([$this, 'fixDocument']);
$this->projectDB->enableFilters();
}
protected function fixDocument(Document $document)
{
switch ($document->getAttribute('$collection')) {
case Database::SYSTEM_COLLECTION_USERS:
if ($document->isSet('password-update')) {
$document
->setAttribute('passwordUpdate', $document->getAttribute('password-update', $document->getAttribute('passwordUpdate', '')))
->removeAttribute('password-update');
}
break;
case Database::SYSTEM_COLLECTION_KEYS:
if ($document->getAttribute('secret', null)) {
$json = \json_decode($document->getAttribute('secret'), true);
if (is_array($json)) {
Console::log('Secret already encrypted. Skipped: ' . $document->getId());
break;
}
$key = App::getEnv('_APP_OPENSSL_KEY_V1');
$iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM));
$tag = null;
$document->setAttribute('secret', json_encode([
'data' => OpenSSL::encrypt($document->getAttribute('secret'), OpenSSL::CIPHER_AES_128_GCM, $key, 0, $iv, $tag),
'method' => OpenSSL::CIPHER_AES_128_GCM,
'iv' => \bin2hex($iv),
'tag' => \bin2hex($tag ?? ''),
'version' => '1',
]));
}
break;
}
return $document;
}
}

View file

@ -1,68 +0,0 @@
<?php
namespace Appwrite\Migration\Version;
use Appwrite\Migration\Migration;
use Utopia\Config\Config;
use Utopia\CLI\Console;
use Appwrite\Auth\Auth;
use Appwrite\Database\Database;
use Appwrite\Database\Document;
class V07 extends Migration
{
public function execute(): void
{
$project = $this->project;
Console::log('Migrating project: ' . $project->getAttribute('name') . ' (' . $project->getId() . ')');
$this->forEachDocument([$this, 'fixDocument']);
}
protected function fixDocument(Document $document)
{
$providers = Config::getParam('providers');
switch ($document->getAttribute('$collection')) {
case Database::SYSTEM_COLLECTION_USERS:
/**
* Remove deprecated OAuth2 properties in the Users Documents.
*/
foreach ($providers as $key => $provider) {
if (!empty($document->getAttribute('oauth2' . \ucfirst($key)))) {
$document->removeAttribute('oauth2' . \ucfirst($key));
}
if (!empty($document->getAttribute('oauth2' . \ucfirst($key) . 'AccessToken'))) {
$document->removeAttribute('oauth2' . \ucfirst($key) . 'AccessToken');
}
}
/**
* Invalidate all Login Tokens, since they can't be migrated to the new structure.
* Reason for it is the missing distinction between E-Mail and OAuth2 tokens.
*/
$tokens = array_filter($document->getAttribute('tokens', []), function ($token) {
return ($token->getAttribute('type') != Auth::TOKEN_TYPE_LOGIN);
});
$document->setAttribute('tokens', array_values($tokens));
break;
}
foreach ($document as &$attr) { // Handle child documents
if ($attr instanceof Document) {
$attr = $this->fixDocument($attr);
}
if (\is_array($attr)) {
foreach ($attr as &$child) {
if ($child instanceof Document) {
$child = $this->fixDocument($child);
}
}
}
}
return $document;
}
}

View file

@ -1,60 +0,0 @@
<?php
namespace Appwrite\Migration\Version;
use Appwrite\Migration\Migration;
use Utopia\Config\Config;
use Utopia\CLI\Console;
use Appwrite\Database\Database;
use Appwrite\Database\Document;
class V08 extends Migration
{
public function execute(): void
{
$project = $this->project;
Console::log('Migrating project: ' . $project->getAttribute('name') . ' (' . $project->getId() . ')');
$this->forEachDocument([$this, 'fixDocument']);
}
protected function fixDocument(Document $document)
{
switch ($document->getAttribute('$collection')) {
/**
* Rename env attribute to runtime.
*/
case Database::SYSTEM_COLLECTION_FUNCTIONS:
if ($document->isSet('env')) {
$document
->setAttribute('runtime', $document->getAttribute('env', $document->getAttribute('env', '')))
->removeAttribute('env');
}
break;
/**
* Add version reference to database.
*/
case Database::SYSTEM_COLLECTION_PROJECTS:
$document->setAttribute('version', '0.9.0');
break;
}
foreach ($document as &$attr) {
if ($attr instanceof Document) {
$attr = $this->fixDocument($attr);
}
if (\is_array($attr)) {
foreach ($attr as &$child) {
if ($child instanceof Document) {
$child = $this->fixDocument($child);
}
}
}
}
return $document;
}
}

View file

@ -1,48 +0,0 @@
<?php
namespace Appwrite\Migration\Version;
use Appwrite\Migration\Migration;
use Utopia\CLI\Console;
use Appwrite\Database\Database;
use Appwrite\Database\Document;
class V09 extends Migration
{
public function execute(): void
{
$project = $this->project;
Console::log('Migrating project: ' . $project->getAttribute('name') . ' (' . $project->getId() . ')');
$this->forEachDocument([$this, 'fixDocument']);
}
protected function fixDocument(Document $document)
{
switch ($document->getAttribute('$collection')) {
/**
* Add version reference to database.
*/
case Database::SYSTEM_COLLECTION_PROJECTS:
$document->setAttribute('version', '0.10.0');
break;
}
foreach ($document as &$attr) {
if ($attr instanceof Document) {
$attr = $this->fixDocument($attr);
}
if (\is_array($attr)) {
foreach ($attr as &$child) {
if ($child instanceof Document) {
$child = $this->fixDocument($child);
}
}
}
}
return $document;
}
}

View file

@ -1,48 +0,0 @@
<?php
namespace Appwrite\Migration\Version;
use Appwrite\Migration\Migration;
use Utopia\CLI\Console;
use Appwrite\Database\Database;
use Appwrite\Database\Document;
class V10 extends Migration
{
public function execute(): void
{
$project = $this->project;
Console::log('Migrating project: ' . $project->getAttribute('name') . ' (' . $project->getId() . ')');
$this->forEachDocument([$this, 'fixDocument']);
}
protected function fixDocument(Document $document)
{
switch ($document->getAttribute('$collection')) {
/**
* Add version reference to database.
*/
case Database::SYSTEM_COLLECTION_PROJECTS:
$document->setAttribute('version', '0.11.0');
break;
}
foreach ($document as &$attr) {
if ($attr instanceof Document) {
$attr = $this->fixDocument($attr);
}
if (\is_array($attr)) {
foreach ($attr as &$child) {
if ($child instanceof Document) {
$child = $this->fixDocument($child);
}
}
}
}
return $document;
}
}

View file

@ -1,821 +0,0 @@
<?php
namespace Appwrite\Migration\Version;
use Appwrite\Database\Database as OldDatabase;
use Appwrite\Database\Document as OldDocument;
use Appwrite\Migration\Migration;
use Exception;
use PDO;
use Redis;
use Swoole\Runtime;
use Throwable;
use Utopia\Abuse\Adapters\TimeLimit;
use Utopia\App;
use Utopia\Audit\Audit;
use Utopia\Cache\Cache;
use Utopia\CLI\Console;
use Utopia\Cache\Adapter\Redis as RedisCache;
use Utopia\Config\Config;
use Utopia\Database\Adapter\MariaDB;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Limit;
use Utopia\Database\Exception\Authorization as ExceptionAuthorization;
use Utopia\Database\Exception\Structure;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
global $register;
class V11 extends Migration
{
protected Database $dbProject;
protected Database $dbConsole;
protected array $oldCollections;
protected array $newCollections;
public function __construct(PDO $db, Redis $cache = null, array $options = [])
{
parent::__construct($db, $cache, $options);
$this->options = array_map(fn ($option) => $option === 'yes' ? true : false, $this->options);
if (!is_null($cache)) {
$this->cache->flushAll();
$cacheAdapter = new Cache(new RedisCache($this->cache));
$this->dbProject = new Database(new MariaDB($this->db), $cacheAdapter); // namespace is set on execution
$this->dbConsole = new Database(new MariaDB($this->db), $cacheAdapter);
$this->dbProject->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
$this->dbConsole->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite'));
$this->dbConsole->setNamespace('_project_console');
}
$this->newCollections = Config::getParam('collections', []);
$this->oldCollections = Config::getParam('collectionsold', []);
}
public function execute(): void
{
Authorization::disable();
Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
$oldProject = $this->project;
$this->dbProject->setNamespace('_project_' . $oldProject->getId());
$this->dbConsole->setNamespace('_project_console');
Console::info('');
Console::info('------------------------------------');
Console::info('Migrating project ' . $oldProject->getAttribute('name'));
Console::info('------------------------------------');
/**
* Create internal/external structure for projects and skip the console project.
*/
if ($oldProject->getId() !== 'console') {
try {
$project = $this->dbConsole->getDocument(collection: 'projects', id: $oldProject->getId());
} catch (\Throwable $th) {
Console::error($th->getTraceAsString());
}
/**
* Migrate Project Document.
*/
if ($project->isEmpty()) {
$newProject = $this->fixDocument($oldProject);
$newProject->setAttribute('version', '0.12.0');
$project = $this->dbConsole->createDocument('projects', $newProject);
Console::log('Created project document: ' . $oldProject->getAttribute('name') . ' (' . $oldProject->getId() . ')');
}
/**
* Create internal tables
*/
try {
Console::log('Created internal tables for : ' . $project->getAttribute('name') . ' (' . $project->getId() . ')');
$this->dbProject->createMetadata();
} catch (\Throwable $th) {
}
/**
* Create Audit tables
*/
if ($this->dbProject->getCollection(Audit::COLLECTION)->isEmpty()) {
$audit = new Audit($this->dbProject);
$audit->setup();
Console::log('Created audit tables for : ' . $project->getAttribute('name') . ' (' . $project->getId() . ')');
}
/**
* Create Abuse tables
*/
if ($this->dbProject->getCollection(TimeLimit::COLLECTION)->isEmpty()) {
$adapter = new TimeLimit("", 0, 1, $this->dbProject);
$adapter->setup();
Console::log('Created abuse tables for : ' . $project->getAttribute('name') . ' (' . $project->getId() . ')');
}
/**
* Create internal collections for Project
*/
foreach ($this->newCollections as $key => $collection) {
if (!$this->dbProject->getCollection($key)->isEmpty()) continue; // Skip if project collection already exists
$attributes = [];
$indexes = [];
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'],
]);
}
$this->dbProject->createCollection($key, $attributes, $indexes);
}
if ($this->options['migrateCollections']) {
$this->migrateExternalCollections();
}
} else {
Console::log('Skipped console project migration.');
}
$sum = $this->limit;
$offset = 0;
$total = 0;
/**
* Migrate internal documents
*/
while ($sum >= $this->limit) {
$all = $this->oldProjectDB->getCollection([
'limit' => $this->limit,
'offset' => $offset,
'orderType' => 'DESC',
'filters' => [
'$collection!=' . OldDatabase::SYSTEM_COLLECTION_COLLECTIONS,
'$collection!=' . OldDatabase::SYSTEM_COLLECTION_RULES,
'$collection!=' . OldDatabase::SYSTEM_COLLECTION_TASKS,
'$collection!=' . OldDatabase::SYSTEM_COLLECTION_PROJECTS,
'$collection!=' . OldDatabase::SYSTEM_COLLECTION_CONNECTIONS,
'$collection!=' . OldDatabase::SYSTEM_COLLECTION_RESERVED,
'$collection!=' . OldDatabase::SYSTEM_COLLECTION_TOKENS,
]
]);
$sum = \count($all);
Console::log('Migrating Internal Documents: ' . $offset . ' / ' . $this->oldProjectDB->getSum());
foreach ($all as $document) {
if (
!array_key_exists($document->getCollection(), $this->oldCollections)
) {
continue;
}
$new = $this->fixDocument($document);
if (is_null($new) || empty($new->getId())) {
Console::warning('Skipped Document due to missing ID.');
continue;
}
try {
if ($this->dbProject->getDocument($new->getCollection(), $new->getId())->isEmpty()) {
$this->dbProject->createDocument($new->getCollection(), $new);
}
} catch (\Throwable $th) {
Console::error("Failed to migrate document ({$new->getId()}) from collection ({$new->getCollection()}): " . $th->getMessage());
continue;
}
}
$offset += $this->limit;
$total += $sum;
}
Console::log('Migrated ' . $total . ' Internal Documents.');
}
/**
* Migrate external collections for Project
*
* @return void
* @throws Exception
* @throws Throwable
* @throws Limit
* @throws ExceptionAuthorization
* @throws Structure
*/
protected function migrateExternalCollections(): void
{
$sum = $this->limit;
$offset = 0;
while ($sum >= $this->limit) {
$databaseCollections = $this->oldProjectDB->getCollection([
'limit' => $this->limit,
'offset' => $offset,
'orderType' => 'DESC',
'filters' => [
'$collection=' . OldDatabase::SYSTEM_COLLECTION_COLLECTIONS,
]
]);
$sum = \count($databaseCollections);
Console::log('Migrating Collections: ' . $offset . ' / ' . $this->oldProjectDB->getSum());
foreach ($databaseCollections as $oldCollection) {
$id = $oldCollection->getId();
$permissions = $oldCollection->getPermissions();
$name = $oldCollection->getAttribute('name');
$newCollection = $this->dbProject->getCollection('collection_' . $id);
if ($newCollection->isEmpty()) {
$this->dbProject->createCollection('collection_' . $id);
/**
* Migrate permissions
*/
$read = $this->migrateWildcardPermissions($permissions['read'] ?? []);
$write = $this->migrateWildcardPermissions($permissions['write'] ?? []);
/**
* Suffix collection name with a subsequent number to make it unique if possible.
*/
$suffix = 1;
while ($this->dbProject->findOne('collections', [
new Query('name', Query::TYPE_EQUAL, [$name])
])) {
$name .= ' - ' . $suffix++;
}
$this->dbProject->createDocument('collections', new Document([
'$id' => $id,
'$read' => [],
'$write' => [],
'permission' => 'document',
'dateCreated' => time(),
'dateUpdated' => time(),
'name' => substr($name, 0, 256),
'enabled' => true,
'search' => implode(' ', [$id, $name]),
]));
} else {
Console::warning('Skipped Collection ' . $newCollection->getId() . ' from ' . $newCollection->getCollection());
}
/**
* Migrate collection rules to attributes
*/
$attributes = $this->getCollectionAttributes($oldCollection);
foreach ($attributes as $attribute) {
try {
$this->dbProject->createAttribute(
collection: 'collection_' . $attribute['$collection'],
id: $attribute['$id'],
type: $attribute['type'],
size: $attribute['size'],
required: $attribute['required'],
default: $attribute['default'],
signed: $attribute['signed'],
array: $attribute['array'],
format: $attribute['format'] ?? null,
formatOptions: $attribute['formatOptions'] ?? [],
filters: $attribute['filters']
);
$this->dbProject->createDocument('attributes', new Document([
'$id' => $attribute['$collection'] . '_' . $attribute['$id'],
'key' => $attribute['$id'],
'collectionId' => $attribute['$collection'],
'type' => $attribute['type'],
'status' => 'available',
'size' => $attribute['size'],
'required' => $attribute['required'],
'signed' => $attribute['signed'],
'default' => $attribute['default'],
'array' => $attribute['array'],
'format' => $attribute['format'] ?? null,
'formatOptions' => $attribute['formatOptions'] ?? null,
'filters' => $attribute['filters']
]));
Console::log('Created "' . $attribute['$id'] . '" attribute in collection: ' . $name);
} catch (\Throwable $th) {
Console::log($th->getMessage() . ' - ("' . $attribute['$id'] . '" attribute in collection ' . $name . ')');
}
}
if ($this->options['migrateDocuments']) {
$this->migrateExternalDocuments(collection: $id);
}
}
$offset += $this->limit;
}
}
/**
* Migrate all external documents
*
* @return void
* @throws Exception
* @throws Throwable
* @throws ExceptionAuthorization
* @throws Structure
*/
protected function migrateExternalDocuments(string $collection): void
{
$sum = $this->limit;
$offset = 0;
while ($sum >= $this->limit) {
$allDocs = $this->oldProjectDB->getCollection([
'limit' => $this->limit,
'offset' => $offset,
'orderType' => 'DESC',
'filters' => [
'$collection=' . $collection
]
]);
$sum = \count($allDocs);
Console::log('Migrating External Documents for Collection ' . $collection . ': ' . $offset . ' / ' . $this->oldProjectDB->getSum());
foreach ($allDocs as $document) {
if (!$this->dbProject->getDocument('collection_' . $collection, $document->getId())->isEmpty()) {
continue;
}
go(function ($document) {
foreach ($document as $key => $attr) {
/**
* Convert nested Document to JSON strings.
*/
if ($document->getAttribute($key) instanceof OldDocument) {
$document[$key] = json_encode($this->fixDocument($attr)->getArrayCopy());
}
/**
* Convert numeric Attributes to float.
*/
if (!is_string($attr) && is_numeric($attr)) {
$document[$key] = floatval($attr);
}
if (\is_array($attr)) {
foreach ($attr as $index => $child) {
/**
* Convert array of nested Document to array JSON strings.
*/
if ($document->getAttribute($key)[$index] instanceof OldDocument) {
$document[$key][$index] = json_encode($this->fixDocument($child)->getArrayCopy());
}
/**
* Convert array of numeric Attributes to array float.
*/
if (!is_string($child) && is_numeric($child)) {
$document[$key][$index] = floatval($child); // Convert any numeric to float
}
}
}
}
}, $document);
$document = new Document($document->getArrayCopy());
$document = $this->migratePermissions($document);
try {
$this->dbProject->createDocument('collection_' . $collection, $document);
} catch (\Throwable $th) {
Console::error("Failed to migrate document ({$document->getId()}): " . $th->getMessage());
continue;
}
}
$offset += $this->limit;
}
}
/**
* Migrates single docuemnt.
*
* @param OldDocument $oldDocument
* @return Document|null
* @throws Exception
*/
protected function fixDocument(OldDocument $oldDocument): Document|null
{
$document = new Document($oldDocument->getArrayCopy());
$document = $this->migratePermissions($document);
/**
* Check attributes and set their default values.
*/
if (array_key_exists($document->getCollection(), $this->oldCollections)) {
foreach ($this->newCollections[$document->getCollection()]['attributes'] ?? [] as $attr) {
if (
(!$attr['array'] ||
($attr['array'] && array_key_exists('filter', $attr)
&& in_array('json', $attr['filter'])))
&& empty($document->getAttribute($attr['$id'], null))
) {
$document->setAttribute($attr['$id'], $attr['default'] ?? null);
}
}
}
switch ($document->getAttribute('$collection')) {
case OldDatabase::SYSTEM_COLLECTION_PROJECTS:
$newProviders = [];
$newAuths = [];
$providers = Config::getParam('providers', []);
$auths = Config::getParam('auth', []);
/**
* Remove Tasks
*/
$document->removeAttribute('tasks');
/*
* Add enabled OAuth2 providers to default data rules
*/
foreach ($providers as $index => $provider) {
$appId = $document->getAttribute('usersOauth2' . \ucfirst($index) . 'Appid');
$appSecret = $document->getAttribute('usersOauth2' . \ucfirst($index) . 'Secret');
if (!is_null($appId) || !is_null($appId)) {
$newProviders[$appId] = $appSecret;
}
$document
->removeAttribute('usersOauth2' . \ucfirst($index) . 'Appid')
->removeAttribute('usersOauth2' . \ucfirst($index) . 'Secret');
}
$document->setAttribute('providers', $newProviders);
/*
* Migrate User providers settings
*/
$oldAuths = [
'email-password' => 'usersAuthEmailPassword',
'magic-url' => 'usersAuthMagicURL',
'anonymous' => 'usersAuthAnonymous',
'invites' => 'usersAuthInvites',
'jwt' => 'usersAuthJWT',
'phone' => 'usersAuthPhone'
];
foreach ($oldAuths as $index => $auth) {
$enabled = $document->getAttribute($auth, true);
$newAuths['auth' . \ucfirst($auths[$index]['key'])] = $enabled;
$document->removeAttribute($auth);
}
if (!empty($document->getAttribute('usersAuthLimit'))) {
$newAuths['limit'] = $document->getAttribute('usersAuthLimit');
}
$document->removeAttribute('usersAuthLimit');
$document->setAttribute('auths', $newProviders);
break;
case OldDatabase::SYSTEM_COLLECTION_PLATFORMS:
$projectId = $this->getProjectIdFromReadPermissions($document);
if (is_null($projectId)) {
return null;
}
/**
* Set Project ID
*/
if ($document->getAttribute('projectId') === null) {
$document->setAttribute('projectId', $projectId);
}
/**
* Set empty key and store if null
*/
if ($document->getAttribute('key') === null) {
$document->setAttribute('key', '');
}
if ($document->getAttribute('store') === null) {
$document->setAttribute('store', '');
}
/**
* Reset Permissions
*/
$document->setAttribute('$read', ['role:all']);
$document->setAttribute('$write', ['role:all']);
break;
case OldDatabase::SYSTEM_COLLECTION_CERTIFICATES:
/**
* Replace certificateId attribute.
*/
if ($document->getAttribute('certificateId') !== null) {
$document->setAttribute('$id', $document->getAttribute('certificateId'));
$document->removeAttribute('certificateId');
}
break;
case OldDatabase::SYSTEM_COLLECTION_DOMAINS:
$projectId = $this->getProjectIdFromReadPermissions($document);
if (is_null($projectId)) {
return null;
}
/**
* Set Project ID
*/
if ($document->getAttribute('projectId') === null) {
$document->setAttribute('projectId', $projectId);
}
/**
* Set empty verification if null
*/
if ($document->getAttribute('verification') === null) {
$document->setAttribute('verification', false);
}
/**
* Reset Permissions
*/
$document->setAttribute('$read', ['role:all']);
$document->setAttribute('$write', ['role:all']);
break;
case OldDatabase::SYSTEM_COLLECTION_KEYS:
$projectId = $this->getProjectIdFromReadPermissions($document);
if (is_null($projectId)) {
return null;
}
/**
* Set Project ID
*/
if ($document->getAttribute('projectId') === null) {
$document->setAttribute('projectId', $projectId);
}
/**
* Set scopes if empty
*/
if (empty($document->getAttribute('scopes', []))) {
$document->setAttribute('scopes', []);
}
/**
* Reset Permissions
*/
$document->setAttribute('$read', ['role:all']);
$document->setAttribute('$write', ['role:all']);
break;
case OldDatabase::SYSTEM_COLLECTION_FUNCTIONS:
$document->setAttribute('events', $document->getAttribute('events', []));
break;
case OldDatabase::SYSTEM_COLLECTION_WEBHOOKS:
$projectId = $this->getProjectIdFromReadPermissions($document);
if (is_null($projectId)) {
return null;
}
/**
* Set Project ID
*/
if ($document->getAttribute('projectId') === null) {
$document->setAttribute('projectId', $projectId);
}
$document->setAttribute('events', $document->getAttribute('events', []));
/**
* Reset Permissions
*/
$document->setAttribute('$read', ['role:all']);
$document->setAttribute('$write', ['role:all']);
break;
case OldDatabase::SYSTEM_COLLECTION_USERS:
/**
* Set deleted attribute to false
*/
if ($document->getAttribute('deleted') === null) {
$document->setAttribute('deleted', false);
}
/**
* Remove deprecated user status 0 and replace with boolean.
*/
if ($document->getAttribute('status') === 2) {
$document->setAttribute('status', false);
} else {
$document->setAttribute('status', true);
}
/**
* Set default values for arrays if not set.
*/
if (empty($document->getAttribute('prefs', []))) {
$document->setAttribute('prefs', new \stdClass());
}
if (empty($document->getAttribute('sessions', []))) {
$document->setAttribute('sessions', []);
}
if (empty($document->getAttribute('tokens', []))) {
$document->setAttribute('tokens', []);
}
if (empty($document->getAttribute('memberships', []))) {
$document->setAttribute('memberships', []);
}
/**
* Replace user:{self} with user:USER_ID
*/
$write = $document->getWrite();
$document->setAttribute('$write', str_replace('user:{self}', "user:{$document->getId()}", $write));
break;
case OldDatabase::SYSTEM_COLLECTION_TEAMS:
/**
* Replace team:{self} with team:TEAM_ID
*/
$read = $document->getWrite();
$write = $document->getWrite();
$document->setAttribute('$read', str_replace('team:{self}', "team:{$document->getId()}", $read));
$document->setAttribute('$write', str_replace('team:{self}', "team:{$document->getId()}", $write));
break;
case OldDatabase::SYSTEM_COLLECTION_FILES:
/**
* Migrating breakind changes on Files.
*/
if (!empty($document->getAttribute('fileOpenSSLVersion', null))) {
$document
->setAttribute('openSSLVersion', $document->getAttribute('fileOpenSSLVersion'))
->removeAttribute('fileOpenSSLVersion');
}
if (!empty($document->getAttribute('fileOpenSSLCipher', null))) {
$document
->setAttribute('openSSLCipher', $document->getAttribute('fileOpenSSLCipher'))
->removeAttribute('fileOpenSSLCipher');
}
if (!empty($document->getAttribute('fileOpenSSLTag', null))) {
$document
->setAttribute('openSSLTag', $document->getAttribute('fileOpenSSLTag'))
->removeAttribute('fileOpenSSLTag');
}
if (!empty($document->getAttribute('fileOpenSSLIV', null))) {
$document
->setAttribute('openSSLIV', $document->getAttribute('fileOpenSSLIV'))
->removeAttribute('fileOpenSSLIV');
}
/**
* Remove deprecated attributes.
*/
$document->removeAttribute('folderId');
$document->removeAttribute('token');
break;
}
return $document;
}
/**
* Migrates $permissions to independent $read and $write.
* @param Document $document
* @return Document
*/
protected function migratePermissions(Document $document): Document
{
if ($document->isSet('$permissions')) {
$permissions = $document->getAttribute('$permissions', []);
$read = $this->migrateWildcardPermissions($permissions['read'] ?? []);
$write = $this->migrateWildcardPermissions($permissions['write'] ?? []);
$document->setAttribute('$read', $read);
$document->setAttribute('$write', $write);
$document->removeAttribute('$permissions');
}
return $document;
}
/**
* Takes a permissions array and replaces wildcard * with role:all.
* @param array $permissions
* @return array
*/
protected function migrateWildcardPermissions(array $permissions): array
{
return array_map(function ($permission) {
if ($permission === '*') return 'role:all';
return $permission;
}, $permissions);
}
/**
* Get new collection attributes from old collection rules.
* @param OldDocument $collection
* @return array
*/
protected function getCollectionAttributes(OldDocument $collection): array
{
$attributes = [];
foreach ($collection->getAttribute('rules', []) as $key => $value) {
$collectionId = $collection->getId();
$id = $value['key'];
$array = $value['array'] ?? false;
$required = $value['required'] ?? false;
$default = $value['default'] ?? null;
$default = match ($value['type']) {
OldDatabase::SYSTEM_VAR_TYPE_NUMERIC => floatval($default),
default => $default
};
$type = match ($value['type']) {
OldDatabase::SYSTEM_VAR_TYPE_TEXT => Database::VAR_STRING,
OldDatabase::SYSTEM_VAR_TYPE_EMAIL => Database::VAR_STRING,
OldDatabase::SYSTEM_VAR_TYPE_DOCUMENT => Database::VAR_STRING,
OldDatabase::SYSTEM_VAR_TYPE_IP => Database::VAR_STRING,
OldDatabase::SYSTEM_VAR_TYPE_URL => Database::VAR_STRING,
OldDatabase::SYSTEM_VAR_TYPE_WILDCARD => Database::VAR_STRING,
OldDatabase::SYSTEM_VAR_TYPE_NUMERIC => Database::VAR_FLOAT,
OldDatabase::SYSTEM_VAR_TYPE_BOOLEAN => Database::VAR_BOOLEAN,
default => Database::VAR_STRING
};
$size = $type === Database::VAR_STRING ? 65_535 : 0; // Max size of text in MariaDB
if ($required) {
$default = null;
}
$attributes[$key] = [
'$collection' => $collectionId,
'$id' => $id,
'type' => $type,
'size' => $size,
'required' => $required,
'default' => $default,
'array' => $array,
'signed' => true,
'filters' => []
];
if ($type === Database::VAR_FLOAT) {
$attributes[$key]['format'] = APP_DATABASE_ATTRIBUTE_FLOAT_RANGE;
$attributes[$key]['formatOptions'] = [];
$attributes[$key]['formatOptions']['min'] = -PHP_FLOAT_MAX;
$attributes[$key]['formatOptions']['max'] = PHP_FLOAT_MAX;
}
}
return $attributes;
}
/**
* @param Document $document
* @return string|null
* @throws Exception
*/
protected function getProjectIdFromReadPermissions(Document $document): string|null
{
$readPermissions = $document->getRead();
$teamId = str_replace('team:', '', reset($readPermissions));
$project = $this->oldConsoleDB->getCollectionFirst([
'filters' => [
'$collection=' . OldDatabase::SYSTEM_COLLECTION_PROJECTS,
'teamId=' . $teamId
]
]);
if (!$project) {
return null;
}
return $project->getId();
}
}

View file

@ -0,0 +1,113 @@
<?php
namespace Appwrite\Migration\Version;
use Appwrite\Migration\Migration;
use Utopia\CLI\Console;
use Utopia\Database\Document;
class V12 extends Migration
{
public function execute(): void
{
Console::log('Migrating project: ' . $this->project->getAttribute('name') . ' (' . $this->project->getId() . ')');
$this->forEachDocument([$this, 'fixDocument']);
}
protected function fixDocument(Document $document)
{
switch ($document->getCollection()) {
case 'projects':
/**
* Bump Project version number.
*/
$document->setAttribute('version', '0.13.0');
/**
* Populate search string from Migration to 0.12.
*/
if (empty($document->getAttribute('search'))) {
$document->setAttribute('search', $this->buildSearchAttribute(['$id', 'name'], $document));
}
break;
case 'users':
/**
* Populate search string from Migration to 0.12.
*/
if (empty($document->getAttribute('search'))) {
$document->setAttribute('search', $this->buildSearchAttribute(['$id', 'email', 'name'], $document));
}
break;
case 'teams':
/**
* Populate search string from Migration to 0.12.
*/
if (empty($document->getAttribute('search'))) {
$document->setAttribute('search', $this->buildSearchAttribute(['$id', 'name'], $document));
}
break;
case 'files':
/**
* Populate search string from Migration to 0.12.
*/
if (empty($document->getAttribute('search'))) {
$document->setAttribute('search', $this->buildSearchAttribute(['$id', 'name'], $document));
}
break;
case 'functions':
/**
* Populate search string from Migration to 0.12.
*/
if (empty($document->getAttribute('search'))) {
$document->setAttribute('search', $this->buildSearchAttribute(['$id', 'name', 'runtime'], $document));
}
break;
case 'tags':
/**
* Populate search string from Migration to 0.12.
*/
if (empty($document->getAttribute('search'))) {
$document->setAttribute('search', $this->buildSearchAttribute(['$id', 'command'], $document));
}
break;
case 'executions':
/**
* Populate search string from Migration to 0.12.
*/
if (empty($document->getAttribute('search'))) {
$document->setAttribute('search', $this->buildSearchAttribute(['$id', 'functionId'], $document));
}
break;
}
return $document;
}
/**
* Builds a search string for a fulltext index.
*
* @param array $values
* @param Document $document
* @return string
*/
private function buildSearchAttribute(array $values, Document $document): string
{
$values = array_filter(array_map(fn (string $value) => $document->getAttribute($value) ?? '', $values));
return implode(' ', $values);
}
}

View file

@ -275,7 +275,7 @@ class OpenAPI3 extends Format
$node['schema']['x-example'] = false;
break;
case 'Utopia\Database\Validator\UID':
case 'Appwrite\Database\Validator\CustomId':
case 'Appwrite\Utopia\Database\Validator\CustomId':
$node['schema']['type'] = $validator->getType();
$node['schema']['x-example'] = '['.\strtoupper(Template::fromCamelCaseToSnake($node['name'])).']';
break;

View file

@ -263,7 +263,7 @@ class Swagger2 extends Format
$node['x-example'] = false;
break;
case 'Utopia\Database\Validator\UID':
case 'Appwrite\Database\Validator\CustomId':
case 'Appwrite\Utopia\Database\Validator\CustomId':
$node['type'] = $validator->getType();
$node['x-example'] = '['.\strtoupper(Template::fromCamelCaseToSnake($node['name'])).']';
break;

View file

@ -1,5 +1,5 @@
<?php
namespace Appwrite\Database\Validator;
namespace Appwrite\Utopia\Database\Validator;
use Utopia\Database\Validator\Key;

View file

@ -1,334 +0,0 @@
<?php
namespace Appwrite\Utopia\Response\Filters;
use Appwrite\Auth\Auth;
use Appwrite\Database\Database;
use Appwrite\Database\Validator\Authorization;
use Appwrite\OpenSSL\OpenSSL;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Filter;
use Exception;
use Utopia\Config\Config;
use Utopia\Locale\Locale as Locale;
class V06 extends Filter
{
// Convert 0.7 Data format to 0.6 format
public function parse(array $content, string $model): array
{
$parsedResponse = [];
switch ($model) {
case Response::MODEL_DOCUMENT_LIST:
$parsedResponse = $content;
break;
case Response::MODEL_COLLECTION:
$parsedResponse = $this->parseCollection($content);
break;
case Response::MODEL_COLLECTION_LIST:
$parsedResponse = $this->parseCollectionList($content);
break;
case Response::MODEL_FILE:
$parsedResponse = $this->parseFile($content);
break;
case Response::MODEL_FILE_LIST:
$parsedResponse = $content;
break;
case Response::MODEL_USER:
$parsedResponse = $this->parseUser($content);
break;
case Response::MODEL_USER_LIST:
$parsedResponse = $this->parseUserList($content);
break;
case Response::MODEL_TEAM:
$parsedResponse = $this->parseTeam($content);
break;
case Response::MODEL_TEAM_LIST:
$parsedResponse = $this->parseTeamList($content);
break;
case Response::MODEL_MEMBERSHIP:
$parsedResponse = $content;
break;
case Response::MODEL_MEMBERSHIP_LIST:
$parsedResponse = $content['memberships'];
break;
case Response::MODEL_SESSION:
$parsedResponse = $this->parseSession($content);
break;
case Response::MODEL_SESSION_LIST:
$parsedResponse = $this->parseSessionList($content);
break;
case Response::MODEL_LOG_LIST:
$parsedResponse = $this->parseLogList($content);
break;
case Response::MODEL_TOKEN:
$parsedResponse = $this->parseToken($content);
break;
case Response::MODEL_LOCALE:
$parsedResponse = $this->parseLocale($content);
break;
case Response::MODEL_COUNTRY_LIST:
$parsedResponse = $this->parseCountryList($content);
break;
case Response::MODEL_PHONE_LIST:
$parsedResponse = $this->parsePhoneList($content);
break;
case Response::MODEL_CONTINENT_LIST:
$parsedResponse = $this->parseContinentList($content);
break;
case Response::MODEL_CURRENCY_LIST:
$parsedResponse = $this->parseCurrencyList($content);
break;
case Response::MODEL_LANGUAGE_LIST:
$parsedResponse = $content;
break;
case Response::MODEL_ANY:
case Response::MODEL_DOCUMENT:
case Response::MODEL_PREFERENCES:
$parsedResponse = $content;
break;
default:
throw new Exception('Received invalid model : ' . $model);
}
return $parsedResponse;
}
private function parseCollectionList(array $content)
{
foreach ($content['collections'] as $key => $collection) {
$content['collections'][$key] = $this->parseCollection($collection);
}
return $content;
}
private function parseCollection(array $content)
{
$content['$collection'] = Database::SYSTEM_COLLECTION_COLLECTIONS;
$content['structure'] = true;
return $content;
}
private function parseFile(array $content)
{
$content['$collection'] = Database::SYSTEM_COLLECTION_FILES;
$content['algorithm'] = 'gzip';
$content['comment'] = '';
$content['fileOpenSSLCipher'] = OpenSSL::CIPHER_AES_128_GCM;
$content['fileOpenSSLIV'] = '';
$content['fileOpenSSLTag'] = '';
$content['fileOpenSSLVersion'] = '';
$content['folderId'] = '';
$content['path'] = '';
$content['sizeActual'] = $content['sizeOriginal'];
$content['token'] = '';
return $content;
}
private function parseCurrencyList(array $content)
{
$content['locations'] = [];
$currencies = $content['currencies'];
$parsedResponse = [];
foreach ($currencies as $currency) {
$currency['locations'] = [];
$parsedResponse[] = $currency;
}
$content['currencies'] = $parsedResponse;
return $content;
}
private function parseContinentList(array $content)
{
$continents = $content['continents'];
$parsedResponse = [];
foreach ($continents as $continent) {
$parsedResponse[$continent['code']] = $continent['name'];
}
$content['continents'] = $parsedResponse;
return $content;
}
private function parsePhoneList(array $content)
{
$phones = $content['phones'];
$parsedResponse = [];
foreach ($phones as $phone) {
$parsedResponse[$phone['countryCode']] = $phone['code'];
}
$content['phones'] = $parsedResponse;
return $content;
}
private function parseCountryList(array $content)
{
$countries = $content['countries'];
$parsedResponse = [];
foreach ($countries as $country) {
$parsedResponse[$country['code']] = $country['name'];
}
$content['countries'] = $parsedResponse;
return $content;
}
private function parseLocale(array $content)
{
$content['ip'] = $content['ip'] ?? '';
$content['countryCode'] = $content['countryCode'] ?? '--';
$content['country'] = $content['country'] ?? Locale::getText('locale.country.unknown');
$content['continent'] = $content['continent'] ?? Locale::getText('locale.country.unknown');
$content['continentCode'] = $content['continentCode'] ?? '--';
$content['eu'] = $content['eu'] ?? false;
$content['currency'] = $content['currency'] ?? null;
return $content;
}
private function parseToken(array $content)
{
$content['type'] = Auth::TOKEN_TYPE_RECOVERY;
return $content;
}
private function parseTeam(array $content)
{
$content['$collection'] = Database::SYSTEM_COLLECTION_TEAMS;
$content['$permissions'] = [];
return $content;
}
private function parseTeamList(array $content)
{
$teams = $content['teams'];
$parsedResponse = [];
foreach ($teams as $team) {
$parsedResponse[] = $this->parseTeam($team);
}
$content['teams'] = $parsedResponse;
return $content;
}
private function parseLogList(array $content)
{
$logs = $content['logs'];
$parsedResponse = [];
foreach ($logs as $log) {
$parsedResponse[] = [
'brand' => $log['deviceBrand'],
'device' => $log['deviceName'],
'event' => $log['event'],
'ip' => $log['ip'],
'model' => $log['deviceModel'],
'time' => $log['time'],
'geo' => [
'isoCode' => empty($log['countryCode']) ? '---' : $log['countryCode'],
'country' => empty($log['countryName']) ? Locale::getText('locale.country.unknown') : $log['countryName']
],
'OS' => [
'name' => $log['osName'],
'platform' => '',
'short_name' => $log['osCode'],
'version' => $log['osVersion']
],
'client' => [
'engine' => $log['clientEngine'],
'name' => $log['clientName'],
'short_name' => $log['clientCode'],
'type' => $log['clientType'],
'version' => $log['clientVersion']
]
];
}
$content['logs'] = $parsedResponse;
return $content;
}
private function parseSessionList(array $content)
{
$sessions = $content['sessions'];
$parsedResponse = [];
foreach ($sessions as $session) {
$parsedResponse[] = [
'$id' => $session['$id'],
'brand' => $session['deviceBrand'],
'current' => $session['current'],
'device' => $session['deviceName'],
'ip' => $session['ip'],
'model' => $session['deviceModel'],
'geo' => [
'isoCode' => empty($session['countryCode']) ? '---' : $session['countryCode'],
'country' => empty($session['countryName']) ? Locale::getText('locale.country.unknown') : $session['countryName']
],
'OS' => [
'name' => $session['osName'],
'platform' => '',
'short_name' => $session['osCode'],
'version' => $session['osVersion']
],
'client' => [
'engine' => $session['clientEngine'],
'name' => $session['clientName'],
'short_name' => $session['clientCode'],
'type' => $session['clientType'],
'version' => $session['clientVersion']
]
];
}
$content['sessions'] = $parsedResponse;
return $content;
}
private function parseSession(array $content)
{
$content['type'] = Auth::TOKEN_TYPE_LOGIN;
return $content;
}
private function parseUserList(array $content)
{
$users = $content['users'];
$parsedResponse = [];
foreach ($users as $user) {
$parsedResponse[] = $this->parseUser($user);
}
$content['users'] = $parsedResponse;
return $content;
}
private function parseUser(array $content)
{
foreach (Config::getParam('providers', []) as $key => $provider) {
if (!$provider['enabled']) {
continue;
}
$content['oauth2' . ucfirst($key)] = '';
$content['oauth2' . ucfirst($key) . 'AccessToken'] = '';
}
$content['status'] = $content['status'] ? 0 : 2;
$content['roles'] = Authorization::getRoles() ?? [];
return $content;
}
}

View file

@ -1,110 +0,0 @@
<?php
namespace Appwrite\Utopia\Response\Filters;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Filter;
use Exception;
class V07 extends Filter
{
// Convert 0.8 Data format to 0.7 format
public function parse(array $content, string $model): array
{
$parsedResponse = [];
switch ($model) {
case Response::MODEL_DOCUMENT_LIST: /** ANY was replaced by DOCUMENT in 0.8.x but this is backward compatible with 0.7.x */
// no break
case Response::MODEL_DOCUMENT: /** ANY was replaced by DOCUMENT in 0.8.x but this is backward compatible with 0.7.x */
// no break
case Response::MODEL_USER_LIST: /** [FIELDS ADDED in 0.8.x] passwordUpdate */
// no break
case Response::MODEL_USER: /** [FIELDS ADDED in 0.8.x] passwordUpdate */
// no break
case Response::MODEL_COLLECTION_LIST:
case Response::MODEL_COLLECTION:
case Response::MODEL_FILE_LIST:
case Response::MODEL_FILE:
case Response::MODEL_TAG_LIST:
case Response::MODEL_TAG:
case Response::MODEL_EXECUTION_LIST:
case Response::MODEL_EXECUTION:
case Response::MODEL_TEAM_LIST:
case Response::MODEL_TEAM:
case Response::MODEL_MEMBERSHIP_LIST:
case Response::MODEL_MEMBERSHIP:
case Response::MODEL_SESSION_LIST: /** [FIELDS ADDED in 0.8.x] provider, providerUid, providerToken */
// no break
case Response::MODEL_SESSION: /** [FIELDS ADDED in 0.8.x] provider, providerUid, providerToken */
// no break
case Response::MODEL_JWT:
case Response::MODEL_LOG:
case Response::MODEL_LOG_LIST:
case Response::MODEL_TOKEN:
case Response::MODEL_LOCALE:
case Response::MODEL_COUNTRY:
case Response::MODEL_COUNTRY_LIST:
case Response::MODEL_PHONE:
case Response::MODEL_PHONE_LIST:
case Response::MODEL_CONTINENT:
case Response::MODEL_CONTINENT_LIST:
case Response::MODEL_CURRENCY:
case Response::MODEL_CURRENCY_LIST:
case Response::MODEL_LANGUAGE:
case Response::MODEL_LANGUAGE_LIST:
case Response::MODEL_PROJECT:
case Response::MODEL_PROJECT_LIST:
case Response::MODEL_PLATFORM:
case Response::MODEL_PLATFORM_LIST:
case Response::MODEL_DOMAIN:
case Response::MODEL_DOMAIN_LIST:
case Response::MODEL_KEY:
case Response::MODEL_KEY_LIST:
case Response::MODEL_PERMISSIONS:
case Response::MODEL_RULE:
case Response::MODEL_TASK:
case Response::MODEL_WEBHOOK:
case Response::MODEL_WEBHOOK_LIST:
case Response::MODEL_MOCK:
case Response::MODEL_ANY:
case Response::MODEL_PREFERENCES: /** ANY was replaced by PREFERENCES in 0.8.x but this is backward compatible with 0.7.x */
// no break
case Response::MODEL_NONE:
case Response::MODEL_ERROR:
case Response::MODEL_ERROR_DEV:
$parsedResponse = $content;
break;
case Response::MODEL_FUNCTION_LIST: /** Function property env was renamed to runtime in 0.9.x */
$parsedResponse = $this->parseFunctionList($content);
break;
case Response::MODEL_FUNCTION:
$parsedResponse = $this->parseFunctionList($content); /** Function property env was renamed to runtime in 0.9.x */
break;
default:
throw new Exception('Received invalid model : '.$model);
}
return $parsedResponse;
}
protected function parseFunction(array $content)
{
$content['env'] = $content['runtime'];
unset($content['runtime']);
return $content;
}
protected function parseFunctionList(array $content)
{
$functions = $content['functions'];
$parsedResponse = [];
foreach ($functions as $function) {
$parsedResponse[] = $this->parseFunction($function);
}
$content['functions'] = $parsedResponse;
return $content;
}
}

View file

@ -1,103 +0,0 @@
<?php
namespace Appwrite\Utopia\Response\Filters;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Filter;
use Exception;
class V08 extends Filter
{
// Convert 0.9 Data format to 0.8 format
public function parse(array $content, string $model): array
{
$parsedResponse = [];
switch ($model) {
case Response::MODEL_DOCUMENT_LIST:
case Response::MODEL_DOCUMENT:
case Response::MODEL_USER_LIST:
case Response::MODEL_USER:
case Response::MODEL_COLLECTION_LIST:
case Response::MODEL_COLLECTION:
case Response::MODEL_FILE_LIST:
case Response::MODEL_FILE:
case Response::MODEL_TAG_LIST:
case Response::MODEL_TAG:
case Response::MODEL_EXECUTION_LIST:
case Response::MODEL_EXECUTION:
case Response::MODEL_TEAM_LIST:
case Response::MODEL_TEAM:
case Response::MODEL_MEMBERSHIP_LIST:
case Response::MODEL_MEMBERSHIP:
case Response::MODEL_SESSION_LIST:
case Response::MODEL_SESSION:
case Response::MODEL_JWT:
case Response::MODEL_LOG:
case Response::MODEL_LOG_LIST:
case Response::MODEL_TOKEN:
case Response::MODEL_LOCALE:
case Response::MODEL_COUNTRY:
case Response::MODEL_COUNTRY_LIST:
case Response::MODEL_PHONE:
case Response::MODEL_PHONE_LIST:
case Response::MODEL_CONTINENT:
case Response::MODEL_CONTINENT_LIST:
case Response::MODEL_CURRENCY:
case Response::MODEL_CURRENCY_LIST:
case Response::MODEL_LANGUAGE:
case Response::MODEL_LANGUAGE_LIST:
case Response::MODEL_PROJECT:
case Response::MODEL_PROJECT_LIST:
case Response::MODEL_PLATFORM:
case Response::MODEL_PLATFORM_LIST:
case Response::MODEL_DOMAIN:
case Response::MODEL_DOMAIN_LIST:
case Response::MODEL_KEY:
case Response::MODEL_KEY_LIST:
case Response::MODEL_PERMISSIONS:
case Response::MODEL_RULE:
case Response::MODEL_TASK:
case Response::MODEL_WEBHOOK:
case Response::MODEL_WEBHOOK_LIST:
case Response::MODEL_MOCK:
case Response::MODEL_ANY:
case Response::MODEL_PREFERENCES:
case Response::MODEL_NONE:
case Response::MODEL_ERROR:
case Response::MODEL_ERROR_DEV:
$parsedResponse = $content;
break;
case Response::MODEL_FUNCTION_LIST: /** Function property env was renamed to runtime in 0.9.x */
$parsedResponse = $this->parseFunctionList($content);
break;
case Response::MODEL_FUNCTION: /** Function property env was renamed to runtime in 0.9.x */
$parsedResponse = $this->parseFunctionList($content);
break;
default:
throw new Exception('Received invalid model : '.$model);
}
return $parsedResponse;
}
protected function parseFunction(array $content)
{
$content['env'] = $content['runtime'];
unset($content['runtime']);
return $content;
}
protected function parseFunctionList(array $content)
{
$functions = $content['functions'];
$parsedResponse = [];
foreach ($functions as $function) {
$parsedResponse[] = $this->parseFunction($function);
}
$content['functions'] = $parsedResponse;
return $content;
}
}

View file

@ -160,45 +160,7 @@ class HTTPTest extends Scope
}
}
public function testResponseHeader() {
/**
* Test without header
*/
$response = $this->client->call(Client::METHOD_GET, '/locale/continents', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => 'console',
], $this->getHeaders()));
$body = $response['body'];
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals($body['sum'], 7);
$this->assertEquals($body['continents'][0]['name'], 'Africa');
$this->assertEquals($body['continents'][0]['code'], 'AF');
$this->assertEquals($body['continents'][1]['name'], 'Antarctica');
$this->assertEquals($body['continents'][1]['code'], 'AN');
$this->assertEquals($body['continents'][2]['name'], 'Asia');
$this->assertEquals($body['continents'][2]['code'], 'AS');
/**
* Test with header
*/
$response = $this->client->call(Client::METHOD_GET, '/locale/continents', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => 'console',
'x-appwrite-response-format' => '0.6.2'
], $this->getHeaders()));
$body = $response['body'];
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals($body['sum'], 7);
$this->assertEquals($body['continents']['AF'], 'Africa');
$this->assertEquals($body['continents']['AN'], 'Antarctica');
$this->assertEquals($body['continents']['AS'], 'Asia');
}
public function testVersions() {
/**
* Test without header
*/

View file

@ -1,36 +0,0 @@
<?php
namespace Appwrite\Tests;
use Appwrite\Database\Validator\Key;
use PHPUnit\Framework\TestCase;
class KeyTest extends TestCase
{
/**
* @var Key
*/
protected $object = null;
public function setUp(): void
{
$this->object = new Key();
}
public function tearDown(): void
{
}
public function testValues()
{
$this->assertEquals($this->object->isValid('dasda asdasd'), false);
$this->assertEquals($this->object->isValid('asdasdasdas'), true);
$this->assertEquals($this->object->isValid('as$$5dasdasdas'), false);
$this->assertEquals($this->object->isValid(false), false);
$this->assertEquals($this->object->isValid(null), false);
$this->assertEquals($this->object->isValid('socialAccountForYoutubeSubscribers'), false);
$this->assertEquals($this->object->isValid('socialAccountForYoutubeSubscriber'), false);
$this->assertEquals($this->object->isValid('socialAccountForYoutubeSubscribe'), true);
$this->assertEquals($this->object->isValid('socialAccountForYoutubeSubscrib'), true);
}
}

View file

@ -1,30 +0,0 @@
<?php
namespace Appwrite\Tests;
use Appwrite\Database\Validator\UID;
use PHPUnit\Framework\TestCase;
class UIDTest extends TestCase
{
/**
* @var UID
*/
protected $object = null;
public function setUp(): void
{
$this->object = new UID();
}
public function tearDown(): void
{
}
public function testValues()
{
$this->assertEquals($this->object->isValid('5f058a8925807'), true);
$this->assertEquals($this->object->isValid('5f058a89258075f058a89258075f058t'), true);
$this->assertEquals($this->object->isValid('5f058a89258075f058a89258075f058tx'), false);
}
}

View file

@ -2,18 +2,13 @@
namespace Appwrite\Tests;
use Appwrite\Database\Document;
use Appwrite\Migration\Migration;
use PHPUnit\Framework\TestCase;
use ReflectionMethod;
use Utopia\Database\Document;
abstract class MigrationTest extends TestCase
{
/**
* @var PDO
*/
protected \PDO $pdo;
/**
* @var Migration
*/
@ -43,7 +38,7 @@ abstract class MigrationTest extends TestCase
{
require_once __DIR__.'/../../../app/init.php';
foreach (Migration::$versions as $version => $class) {
foreach (Migration::$versions as $class) {
$this->assertTrue(class_exists('Appwrite\\Migration\\Version\\'.$class));
}
// Test if current version exists

View file

@ -1,86 +0,0 @@
<?php
namespace Appwrite\Tests;
use ReflectionClass;
use Appwrite\Migration\Version\V05;
use Appwrite\Database\Database;
use Appwrite\Database\Document;
use Utopia\Config\Config;
class MigrationV05Test extends MigrationTest
{
public function setUp(): void
{
Config::load('providers', __DIR__ . '/../../../app/config/providers.php');
$this->pdo = new \PDO('sqlite::memory:');
$this->migration = new V05($this->pdo);
$reflector = new ReflectionClass('Appwrite\Migration\Version\V05');
$this->method = $reflector->getMethod('fixDocument');
$this->method->setAccessible(true);
}
public function testMigration()
{
$document = $this->fixDocument(new Document([
'$uid' => 'unique',
'$collection' => Database::SYSTEM_COLLECTION_PROJECTS,
'usersOauthGithubAppid' => 123,
'usersOauthGithubSecret' => 456
]));
$this->assertEquals($document->getAttribute('$uid', null), null);
$this->assertEquals($document->getAttribute('$id', null), 'unique');
$this->assertEquals($document->getAttribute('usersOauthGithubAppid', null), null);
$this->assertEquals($document->getAttribute('usersOauth2GithubAppid', null), 123);
$this->assertEquals($document->getAttribute('usersOauthGithubSecret', null), null);
$this->assertEquals($document->getAttribute('usersOauth2GithubSecret', null), 456);
$this->assertEquals($document->getAttribute('security', true), false);
$document = $this->fixDocument(new Document([
'$uid' => 'unique',
'$collection' => Database::SYSTEM_COLLECTION_TASKS
]));
$this->assertEquals($document->getAttribute('$uid', null), null);
$this->assertEquals($document->getAttribute('$id', null), 'unique');
$this->assertEquals($document->getAttribute('security', true), false);
$document = $this->fixDocument(new Document([
'$uid' => 'unique',
'$collection' => Database::SYSTEM_COLLECTION_USERS,
'oauthGithub' => 'id',
'oauthGithubAccessToken' => 'token',
'confirm' => false
]));
$this->assertEquals($document->getAttribute('$uid', null), null);
$this->assertEquals($document->getAttribute('$id', null), 'unique');
$this->assertEquals($document->getAttribute('confirm', null), null);
$this->assertEquals($document->getAttribute('emailVerification', true), false);
$this->assertEquals($document->getAttribute('oauthGithub', null), null);
$this->assertEquals($document->getAttribute('oauth2Github', null), 'id');
$this->assertEquals($document->getAttribute('oauthGithubAccessToken', null), null);
$this->assertEquals($document->getAttribute('oauth2GithubAccessToken', null), 'token');
$document = $this->fixDocument(new Document([
'$uid' => 'unique',
'$collection' => Database::SYSTEM_COLLECTION_PLATFORMS,
'url' => 'https://appwrite.io'
]));
$this->assertEquals($document->getAttribute('$uid', null), null);
$this->assertEquals($document->getAttribute('$id', null), 'unique');
$this->assertEquals($document->getAttribute('url', null), null);
$this->assertEquals($document->getAttribute('hostname', null), 'appwrite.io');
}
}

View file

@ -1,48 +0,0 @@
<?php
namespace Appwrite\Tests;
use ReflectionClass;
use Appwrite\Migration\Version\V06;
use Appwrite\Database\Database;
use Appwrite\Database\Document;
class MigrationV06Test extends MigrationTest
{
public function setUp(): void
{
$this->pdo = new \PDO('sqlite::memory:');
$this->migration = new V06($this->pdo);
$reflector = new ReflectionClass('Appwrite\Migration\Version\V06');
$this->method = $reflector->getMethod('fixDocument');
$this->method->setAccessible(true);
}
public function testMigration()
{
$document = $this->fixDocument(new Document([
'$id' => uniqid(),
'$collection' => Database::SYSTEM_COLLECTION_USERS,
'password-update' => 123
]));
$this->assertEquals($document->getAttribute('password-update', null), null);
$this->assertEquals($document->getAttribute('passwordUpdate', null), 123);
$document = $this->fixDocument(
new Document([
'$id' => uniqid(),
'$collection' => Database::SYSTEM_COLLECTION_KEYS,
'secret' => 123
])
);
$encrypted = json_decode($document->getAttribute('secret', null));
$this->assertObjectHasAttribute('data', $encrypted);
$this->assertObjectHasAttribute('method', $encrypted);
$this->assertObjectHasAttribute('iv', $encrypted);
$this->assertObjectHasAttribute('tag', $encrypted);
$this->assertObjectHasAttribute('version', $encrypted);
}
}

View file

@ -1,69 +0,0 @@
<?php
namespace Appwrite\Tests;
use ReflectionClass;
use Appwrite\Migration\Version\V07;
use Appwrite\Database\Database;
use Appwrite\Database\Document;
use Appwrite\Auth\Auth;
use Utopia\Config\Config;
class MigrationV07Test extends MigrationTest
{
public function setUp(): void
{
Config::load('providers', __DIR__ . '/../../../app/config/providers.php');
$this->pdo = new \PDO('sqlite::memory:');
$this->migration = new V07($this->pdo);
$reflector = new ReflectionClass('Appwrite\Migration\Version\V07');
$this->method = $reflector->getMethod('fixDocument');
$this->method->setAccessible(true);
}
public function testMigration()
{
$document = $this->fixDocument(new Document([
'$id' => 'unique',
'$collection' => Database::SYSTEM_COLLECTION_USERS,
'oauth2Github' => 123,
'oauth2GithubAccessToken' => 456,
'tokens' => [
new Document([
'$collection' => Database::SYSTEM_COLLECTION_TOKENS,
'userId' => 'unique',
'type' => Auth::TOKEN_TYPE_LOGIN,
'secret' => 'login',
]),
new Document([
'$collection' => Database::SYSTEM_COLLECTION_TOKENS,
'userId' => 'unique',
'type' => Auth::TOKEN_TYPE_INVITE,
'secret' => 'invite',
]),
new Document([
'$collection' => Database::SYSTEM_COLLECTION_TOKENS,
'userId' => 'unique',
'type' => Auth::TOKEN_TYPE_RECOVERY,
'secret' => 'recovery',
]),
new Document([
'$collection' => Database::SYSTEM_COLLECTION_TOKENS,
'userId' => 'unique',
'type' => Auth::TOKEN_TYPE_VERIFICATION,
'secret' => 'verification',
]),
]
]));
$this->assertEquals($document->getAttribute('oauth2Github', null), null);
$this->assertEquals($document->getAttribute('oauth2GithubAccessToken', null), null);
$this->assertCount(3, $document->getAttribute('tokens', []));
$this->assertEquals(Auth::TOKEN_TYPE_INVITE, $document->getAttribute('tokens', [])[0]['type']);
$this->assertEquals(Auth::TOKEN_TYPE_RECOVERY, $document->getAttribute('tokens', [])[1]['type']);
$this->assertEquals(Auth::TOKEN_TYPE_VERIFICATION, $document->getAttribute('tokens', [])[2]['type']);
}
}

View file

@ -1,40 +0,0 @@
<?php
namespace Appwrite\Tests;
use ReflectionClass;
use Appwrite\Database\Database;
use Appwrite\Database\Document;
use Appwrite\Auth\Auth;
use Appwrite\Migration\Version\V08;
class MigrationV08Test extends MigrationTest
{
public function setUp(): void
{
$this->pdo = new \PDO('sqlite::memory:');
$this->migration = new V08($this->pdo);
$reflector = new ReflectionClass('Appwrite\Migration\Version\V08');
$this->method = $reflector->getMethod('fixDocument');
$this->method->setAccessible(true);
}
public function testMigration()
{
$document = $this->fixDocument(new Document([
'$id' => 'unique',
'$collection' => Database::SYSTEM_COLLECTION_FUNCTIONS,
'env' => 'node-16'
]));
$this->assertEquals($document->getAttribute('env', null), null);
$this->assertEquals($document->getAttribute('runtime', null), 'node-16');
$document = $this->fixDocument(new Document([
'$id' => 'project',
'$collection' => Database::SYSTEM_COLLECTION_PROJECTS
]));
$this->assertEquals($document->getAttribute('version', null), '0.9.0');
}
}

View file

@ -1,31 +0,0 @@
<?php
namespace Appwrite\Tests;
use ReflectionClass;
use Appwrite\Database\Database;
use Appwrite\Database\Document;
use Appwrite\Migration\Version\V09;
class MigrationV09Test extends MigrationTest
{
public function setUp(): void
{
$this->pdo = new \PDO('sqlite::memory:');
$this->migration = new V09($this->pdo);
$reflector = new ReflectionClass('Appwrite\Migration\Version\V09');
$this->method = $reflector->getMethod('fixDocument');
$this->method->setAccessible(true);
}
public function testMigration()
{
$document = $this->fixDocument(new Document([
'$id' => 'project',
'$collection' => Database::SYSTEM_COLLECTION_PROJECTS,
'version' => '0.9.0'
]));
$this->assertEquals($document->getAttribute('version', '0.9.0'), '0.10.0');
}
}

View file

@ -1,31 +0,0 @@
<?php
namespace Appwrite\Tests;
use ReflectionClass;
use Appwrite\Database\Database;
use Appwrite\Database\Document;
use Appwrite\Migration\Version\V10;
class MigrationV10Test extends MigrationTest
{
public function setUp(): void
{
$this->pdo = new \PDO('sqlite::memory:');
$this->migration = new V10($this->pdo);
$reflector = new ReflectionClass('Appwrite\Migration\Version\V10');
$this->method = $reflector->getMethod('fixDocument');
$this->method->setAccessible(true);
}
public function testMigration()
{
$document = $this->fixDocument(new Document([
'$id' => 'project',
'$collection' => Database::SYSTEM_COLLECTION_PROJECTS,
'version' => '0.10.0'
]));
$this->assertEquals($document->getAttribute('version', '0.10.0'), '0.11.0');
}
}

View file

@ -0,0 +1,100 @@
<?php
namespace Appwrite\Tests;
use ReflectionClass;
use Appwrite\Migration\Version\V12;
use Utopia\Database\Document;
class MigrationV12Test extends MigrationTest
{
public function setUp(): void
{
$this->migration = new V12();
$reflector = new ReflectionClass('Appwrite\Migration\Version\V12');
$this->method = $reflector->getMethod('fixDocument');
$this->method->setAccessible(true);
}
public function testMigrationProjects()
{
$document = $this->fixDocument(new Document([
'$id' => 'project',
'$collection' => 'projects',
'name' => 'Appwrite',
'version' => '0.12.0',
'search' => ''
]));
$this->assertEquals($document->getAttribute('version'), '0.13.0');
$this->assertEquals($document->getAttribute('search'), 'project Appwrite');
}
public function testMigrationUsers()
{
$document = $this->fixDocument(new Document([
'$id' => 'user',
'$collection' => 'users',
'email' => 'test@appwrite.io',
'name' => 'Torsten Dittmann'
]));
$this->assertEquals($document->getAttribute('search'), 'user test@appwrite.io Torsten Dittmann');
}
public function testMigrationTeams()
{
$document = $this->fixDocument(new Document([
'$id' => 'team',
'$collection' => 'teams',
'name' => 'Appwrite'
]));
$this->assertEquals($document->getAttribute('search'), 'team Appwrite');
}
public function testMigrationFiles()
{
$document = $this->fixDocument(new Document([
'$id' => 'file',
'$collection' => 'files',
'name' => 'Dog.jpeg'
]));
$this->assertEquals($document->getAttribute('search'), 'file Dog.jpeg');
}
public function testMigrationFunctions()
{
$document = $this->fixDocument(new Document([
'$id' => 'function',
'$collection' => 'functions',
'name' => 'My Function',
'runtime' => 'php-8.0'
]));
$this->assertEquals($document->getAttribute('search'), 'function My Function php-8.0');
}
public function testMigrationTags()
{
$document = $this->fixDocument(new Document([
'$id' => 'tag',
'$collection' => 'tags',
'command' => 'php main.php'
]));
$this->assertEquals($document->getAttribute('search'), 'tag php main.php');
}
public function testMigrationExecutions()
{
$document = $this->fixDocument(new Document([
'$id' => 'execution',
'$collection' => 'executions',
'functionId' => 'function'
]));
$this->assertEquals($document->getAttribute('search'), 'execution function');
}
}

View file

@ -2,7 +2,7 @@
namespace Appwrite\Tests;
use Appwrite\Database\Validator\CustomId;
use Appwrite\Utopia\Database\Validator\CustomId;
use PHPUnit\Framework\TestCase;
class CustomIdTest extends TestCase

View file

@ -1,518 +0,0 @@
<?php
namespace Appwrite\Tests;
use Appwrite\Auth\Auth;
use Appwrite\Database\Database;
use Appwrite\Database\Validator\Authorization;
use Appwrite\OpenSSL\OpenSSL;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Filters\V06;
use PHPUnit\Framework\TestCase;
use Utopia\Config\Config;
class V06Test extends TestCase
{
/**
* @var Filter
*/
protected $filter = null;
public function setUp(): void
{
$this->filter = new V06();
}
public function testParseUser()
{
$content = [
'$id' => '5e5ea5c16897e',
'name' => 'John Doe',
'registration' => 1592981250,
'status' => true,
'email' => 'john@appwrite.io',
'emailVerification' => false,
'prefs' => [
'theme' => 'pink',
'timezone' => 'UTC'
]
];
Config::load('providers', __DIR__.'/../../../../app/config/providers.php');
$model = Response::MODEL_USER;
$parsedResponse = $this->filter->parse($content, $model);
$this->assertEquals($parsedResponse['$id'], '5e5ea5c16897e');
$this->assertEquals($parsedResponse['name'], 'John Doe');
$this->assertEquals($parsedResponse['registration'], 1592981250);
$this->assertEquals($parsedResponse['status'], 0);
$this->assertEquals($parsedResponse['email'], 'john@appwrite.io');
$this->assertEquals($parsedResponse['emailVerification'], false);
$this->assertEquals($parsedResponse['prefs'], ['theme' => 'pink', 'timezone' => 'UTC']);
$this->assertEquals($parsedResponse['roles'], Authorization::getRoles() ?? []);
}
public function testParseUserList()
{
$content = [
'sum' => 1,
'users' => [
0 => [
'$id' => '5e5ea5c16897e',
'name' => 'John Doe',
'registration' => 1592981250,
'status' => true,
'email' => 'john@appwrite.io',
'emailVerification' => false,
'prefs' => [
'theme' => 'pink',
'timezone' => 'UTC'
]
]
]
];
Config::load('providers', __DIR__.'/../../../../app/config/providers.php');
$model = Response::MODEL_USER_LIST;
$parsedResponse = $this->filter->parse($content, $model);
$this->assertEquals($parsedResponse['sum'], 1);
$this->assertEquals($parsedResponse['users'][0]['$id'], '5e5ea5c16897e');
$this->assertEquals($parsedResponse['users'][0]['name'], 'John Doe');
$this->assertEquals($parsedResponse['users'][0]['registration'], 1592981250);
$this->assertEquals($parsedResponse['users'][0]['status'], 0);
$this->assertEquals($parsedResponse['users'][0]['email'], 'john@appwrite.io');
$this->assertEquals($parsedResponse['users'][0]['emailVerification'], false);
$this->assertEquals($parsedResponse['users'][0]['prefs'], ['theme' => 'pink', 'timezone' => 'UTC']);
$this->assertEquals($parsedResponse['users'][0]['roles'], Authorization::getRoles() ?? []);
}
public function testParseSession()
{
$content = [
'$id' => '5e5ea5c16897e',
'userId' => '5e5bb8c16897e',
'expire' => 1592981250,
'ip' => '127.0.0.1',
'osCode' => 'Mac',
'osName' => 'Mac',
'osVersion' => 'Mac',
'clientType' => 'browser',
'clientCode' => 'CM',
'clientName' => 'Chrome Mobile iOS',
'clientVersion' => '84.0',
'clientEngine' => 'WebKit',
'clientEngineVersion' => '605.1.15',
'deviceName' => 'smartphone',
'deviceBrand' => 'Google',
'deviceModel' => 'Nexus 5',
'countryCode' => 'US',
'countryName' => 'United States',
'current' => true
];
$model = Response::MODEL_SESSION;
$parsedResponse = $this->filter->parse($content, $model);
$this->assertEquals($parsedResponse['$id'], '5e5ea5c16897e');
$this->assertEquals($parsedResponse['userId'], '5e5bb8c16897e');
$this->assertEquals($parsedResponse['expire'], 1592981250);
$this->assertEquals($parsedResponse['ip'], '127.0.0.1');
$this->assertEquals($parsedResponse['osCode'], 'Mac');
$this->assertEquals($parsedResponse['osName'], 'Mac');
$this->assertEquals($parsedResponse['osVersion'], 'Mac');
$this->assertEquals($parsedResponse['clientType'], 'browser');
$this->assertEquals($parsedResponse['clientCode'], 'CM');
$this->assertEquals($parsedResponse['clientName'], 'Chrome Mobile iOS');
$this->assertEquals($parsedResponse['clientVersion'], '84.0');
$this->assertEquals($parsedResponse['clientEngine'], 'WebKit');
$this->assertEquals($parsedResponse['clientEngineVersion'], '605.1.15');
$this->assertEquals($parsedResponse['deviceName'], 'smartphone');
$this->assertEquals($parsedResponse['deviceBrand'], 'Google');
$this->assertEquals($parsedResponse['deviceModel'], 'Nexus 5');
$this->assertEquals($parsedResponse['countryCode'], 'US');
$this->assertEquals($parsedResponse['countryName'], 'United States');
$this->assertEquals($parsedResponse['current'], true);
$this->assertEquals($parsedResponse['type'], Auth::TOKEN_TYPE_LOGIN);
}
public function testParseSessionList()
{
$content = [
'sum' => 1,
'sessions' => [
0 => [
'$id' => '5e5ea5c16897e',
'userId' => '5e5bb8c16897e',
'expire' => 1592981250,
'ip' => '127.0.0.1',
'osCode' => 'Mac',
'osName' => 'Mac',
'osVersion' => 'Mac',
'clientType' => 'browser',
'clientCode' => 'CM',
'clientName' => 'Chrome Mobile iOS',
'clientVersion' => '84.0',
'clientEngine' => 'WebKit',
'clientEngineVersion' => '605.1.15',
'deviceName' => 'smartphone',
'deviceBrand' => 'Google',
'deviceModel' => 'Nexus 5',
'countryCode' => 'US',
'countryName' => 'United States',
'current' => true
]
]
];
$model = Response::MODEL_SESSION_LIST;
$parsedResponse = $this->filter->parse($content, $model);
$this->assertEquals($parsedResponse['sum'], 1);
$this->assertEquals($parsedResponse['sessions'][0]['$id'], '5e5ea5c16897e');
$this->assertEquals($parsedResponse['sessions'][0]['brand'], 'Google');
$this->assertEquals($parsedResponse['sessions'][0]['current'], true);
$this->assertEquals($parsedResponse['sessions'][0]['device'], 'smartphone');
$this->assertEquals($parsedResponse['sessions'][0]['ip'], '127.0.0.1');
$this->assertEquals($parsedResponse['sessions'][0]['model'], 'Nexus 5');
$this->assertEquals($parsedResponse['sessions'][0]['OS']['name'], 'Mac');
$this->assertEquals($parsedResponse['sessions'][0]['OS']['platform'], '');
$this->assertEquals($parsedResponse['sessions'][0]['OS']['short_name'], 'Mac');
$this->assertEquals($parsedResponse['sessions'][0]['OS']['version'], 'Mac');
$this->assertEquals($parsedResponse['sessions'][0]['client']['engine'], 'WebKit');
$this->assertEquals($parsedResponse['sessions'][0]['client']['name'], 'Chrome Mobile iOS');
$this->assertEquals($parsedResponse['sessions'][0]['client']['short_name'], 'CM');
$this->assertEquals($parsedResponse['sessions'][0]['client']['type'], 'browser');
$this->assertEquals($parsedResponse['sessions'][0]['client']['version'], '84.0');
$this->assertEquals($parsedResponse['sessions'][0]['geo']['isoCode'], 'US');
$this->assertEquals($parsedResponse['sessions'][0]['geo']['country'], 'United States');
}
public function testParseLogList()
{
$content = [
'sum' => 1,
'logs' => [
0 => [
'event' => 'account.sessions.create',
'ip' => '127.0.0.1',
'time' => 1592981250,
'osCode' => 'Mac',
'osName' => 'Mac',
'osVersion' => 'Mac',
'clientType' => 'browser',
'clientCode' => 'CM',
'clientName' => 'Chrome Mobile iOS',
'clientVersion' => '84.0',
'clientEngine' => 'WebKit',
'clientEngineVersion' => '605.1.15',
'deviceName' => 'smartphone',
'deviceBrand' => 'Google',
'deviceModel' => 'Nexus 5',
'countryCode' => 'US',
'countryName' => 'United States'
]
]
];
$model = Response::MODEL_LOG_LIST;
$parsedResponse = $this->filter->parse($content, $model);
$this->assertEquals($parsedResponse['sum'], 1);
$this->assertEquals($parsedResponse['logs'][0]['brand'], 'Google');
$this->assertEquals($parsedResponse['logs'][0]['device'], 'smartphone');
$this->assertEquals($parsedResponse['logs'][0]['event'], 'account.sessions.create');
$this->assertEquals($parsedResponse['logs'][0]['ip'], '127.0.0.1');
$this->assertEquals($parsedResponse['logs'][0]['model'], 'Nexus 5');
$this->assertEquals($parsedResponse['logs'][0]['time'], 1592981250);
$this->assertEquals($parsedResponse['logs'][0]['OS']['name'], 'Mac');
$this->assertEquals($parsedResponse['logs'][0]['OS']['platform'], '');
$this->assertEquals($parsedResponse['logs'][0]['OS']['short_name'], 'Mac');
$this->assertEquals($parsedResponse['logs'][0]['OS']['version'], 'Mac');
$this->assertEquals($parsedResponse['logs'][0]['client']['engine'], 'WebKit');
$this->assertEquals($parsedResponse['logs'][0]['client']['name'], 'Chrome Mobile iOS');
$this->assertEquals($parsedResponse['logs'][0]['client']['short_name'], 'CM');
$this->assertEquals($parsedResponse['logs'][0]['client']['type'], 'browser');
$this->assertEquals($parsedResponse['logs'][0]['client']['version'], '84.0');
$this->assertEquals($parsedResponse['logs'][0]['geo']['isoCode'], 'US');
$this->assertEquals($parsedResponse['logs'][0]['geo']['country'], 'United States');
}
public function testParseTeam()
{
$content = [
'$id' => '5ff45ef261829',
'name' => 'test',
'dateCreated' => 1592981250,
'sum' => 7
];
$model = Response::MODEL_TEAM;
$parsedResponse = $this->filter->parse($content, $model);
$this->assertEquals($parsedResponse['$id'], '5ff45ef261829');
$this->assertEquals($parsedResponse['name'], 'test');
$this->assertEquals($parsedResponse['dateCreated'], 1592981250);
$this->assertEquals($parsedResponse['sum'], 7);
$this->assertEquals($parsedResponse['$collection'], 'teams');
$this->assertEquals($parsedResponse['$permissions'], []);
}
public function testParseTeamList()
{
$content = [
'sum' => 1,
'teams' => [
0 => [
'$id' => '5ff45ef261829',
'name' => 'test',
'dateCreated' => 1592981250,
'sum' => 7
]
]
];
$model = Response::MODEL_TEAM_LIST;
$parsedResponse = $this->filter->parse($content, $model);
$this->assertEquals($parsedResponse['sum'], 1);
$this->assertEquals($parsedResponse['teams'][0]['$id'], '5ff45ef261829');
$this->assertEquals($parsedResponse['teams'][0]['name'], 'test');
$this->assertEquals($parsedResponse['teams'][0]['dateCreated'], 1592981250);
$this->assertEquals($parsedResponse['teams'][0]['sum'], 7);
$this->assertEquals($parsedResponse['teams'][0]['$collection'], 'teams');
$this->assertEquals($parsedResponse['teams'][0]['$permissions'], []);
}
public function testParseToken()
{
$content = [
'$id' => 'bb8ea5c16897e',
'userId' => '5e5ea5c168bb8',
'secret' => '',
'expire' => 1592981250
];
$model = Response::MODEL_TOKEN;
$parsedResponse = $this->filter->parse($content, $model);
$this->assertEquals($parsedResponse['$id'], 'bb8ea5c16897e');
$this->assertEquals($parsedResponse['userId'], '5e5ea5c168bb8');
$this->assertEquals($parsedResponse['expire'], 1592981250);
$this->assertEquals($parsedResponse['secret'], '');
$this->assertEquals($parsedResponse['type'], Auth::TOKEN_TYPE_RECOVERY);
}
public function testParseLocale()
{
$content = [
'ip' => '127.0.0.1',
'countryCode' => 'US',
'country' => 'United States',
'continentCode' => 'NA',
'continent' => 'North America',
'eu' => false,
'currency' => 'USD'
];
$model = Response::MODEL_LOCALE;
$parsedResponse = $this->filter->parse($content, $model);
$this->assertEquals($parsedResponse['ip'], '127.0.0.1');
$this->assertEquals($parsedResponse['countryCode'], 'US');
$this->assertEquals($parsedResponse['country'], 'United States');
$this->assertEquals($parsedResponse['continentCode'], 'NA');
$this->assertEquals($parsedResponse['continent'], 'North America');
$this->assertEquals($parsedResponse['eu'], false);
$this->assertEquals($parsedResponse['currency'], 'USD');
}
public function testParseCountryList()
{
$content = [
'sum' => 1,
'countries' => [
0 => [
'name' => 'United States',
'code' => 'US'
]
]
];
$model = Response::MODEL_COUNTRY_LIST;
$parsedResponse = $this->filter->parse($content, $model);
$this->assertEquals($parsedResponse['sum'], 1);
$this->assertEquals($parsedResponse['countries']['US'], 'United States');
}
public function testParsePhoneList()
{
$content = [
'sum' => 1,
'phones' => [
0 => [
'code' => '+1',
'countryCode' => 'US',
'countryName' => 'United States'
]
]
];
$model = Response::MODEL_PHONE_LIST;
$parsedResponse = $this->filter->parse($content, $model);
$this->assertEquals($parsedResponse['sum'], 1);
$this->assertEquals($parsedResponse['phones']['US'], '+1');
}
public function testParseContinentList()
{
$content = [
'sum' => 1,
'continents' => [
0 => [
'name' => 'Europe',
'code' => 'EU',
]
]
];
$model = Response::MODEL_CONTINENT_LIST;
$parsedResponse = $this->filter->parse($content, $model);
$this->assertEquals($parsedResponse['sum'], 1);
$this->assertEquals($parsedResponse['continents']['EU'], 'Europe');
}
public function testParseCurrencyList()
{
$content = [
'sum' => 1,
'currencies' => [
0 => [
'symbol' => '$',
'name' => 'US dollar',
'symbolNative' => '$',
'decimalDigits' => 2,
'rounding' => 0,
'code' => 'USD',
'namePlural' => 'US Dollars'
]
]
];
$model = Response::MODEL_CURRENCY_LIST;
$parsedResponse = $this->filter->parse($content, $model);
$this->assertEquals($parsedResponse['sum'], 1);
$this->assertEquals($parsedResponse['currencies'][0]['symbol'], '$');
$this->assertEquals($parsedResponse['currencies'][0]['name'], 'US dollar');
$this->assertEquals($parsedResponse['currencies'][0]['symbolNative'], '$');
$this->assertEquals($parsedResponse['currencies'][0]['decimalDigits'], 2);
$this->assertEquals($parsedResponse['currencies'][0]['rounding'], 0);
$this->assertEquals($parsedResponse['currencies'][0]['code'], 'USD');
$this->assertEquals($parsedResponse['currencies'][0]['namePlural'], 'US Dollars');
$this->assertEquals($parsedResponse['currencies'][0]['locations'], []);
}
public function testParseFile()
{
$content = [
'$id' => '5e5ea5c16897e',
'$permissions' => ['read' => ['role:all'], 'write' => ['role:all']],
'name' => 'Pink.png',
'dateCreated' => 1592981250,
'signature' => '5d529fd02b544198ae075bd57c1762bb',
'mimeType' => 'image/png',
'sizeOriginal' => 17890
];
$model = Response::MODEL_FILE;
$parsedResponse = $this->filter->parse($content, $model);
$this->assertEquals($parsedResponse['$id'], '5e5ea5c16897e');
$this->assertEquals($parsedResponse['$permissions'], ['read' => ['role:all'], 'write' => ['role:all']]);
$this->assertEquals($parsedResponse['name'], 'Pink.png');
$this->assertEquals($parsedResponse['dateCreated'], 1592981250);
$this->assertEquals($parsedResponse['signature'], '5d529fd02b544198ae075bd57c1762bb');
$this->assertEquals($parsedResponse['mimeType'], 'image/png');
$this->assertEquals($parsedResponse['sizeOriginal'], 17890);
$this->assertEquals($parsedResponse['$collection'], Database::SYSTEM_COLLECTION_FILES);
$this->assertEquals($parsedResponse['algorithm'], 'gzip');
$this->assertEquals($parsedResponse['comment'], '');
$this->assertEquals($parsedResponse['fileOpenSSLCipher'], OpenSSL::CIPHER_AES_128_GCM);
$this->assertEquals($parsedResponse['fileOpenSSLIV'], '');
$this->assertEquals($parsedResponse['fileOpenSSLTag'], '');
$this->assertEquals($parsedResponse['fileOpenSSLVersion'], '');
$this->assertEquals($parsedResponse['folderId'], '');
$this->assertEquals($parsedResponse['path'], '');
$this->assertEquals($parsedResponse['sizeActual'], $content['sizeOriginal']);
$this->assertEquals($parsedResponse['token'], '');
}
public function testParseCollection()
{
$content = [
'$id' => '5e5ea5c16897e',
'$permissions' => ['read' => ['role:all'], 'write' => ['role:all']],
'name' => 'Movies',
'dateCreated' => 1592981250,
'dateUpdated' => '5d529fd02b544198ae075bd57c1762bb',
'rules' => []
];
$model = Response::MODEL_COLLECTION;
$parsedResponse = $this->filter->parse($content, $model);
$this->assertEquals($parsedResponse['$id'], '5e5ea5c16897e');
$this->assertEquals($parsedResponse['$permissions'], ['read' => ['role:all'], 'write' => ['role:all']]);
$this->assertEquals($parsedResponse['name'], 'Movies');
$this->assertEquals($parsedResponse['dateCreated'], 1592981250);
$this->assertEquals($parsedResponse['dateUpdated'], '5d529fd02b544198ae075bd57c1762bb');
$this->assertEquals($parsedResponse['rules'], []);
$this->assertEquals($parsedResponse['$collection'], Database::SYSTEM_COLLECTION_COLLECTIONS);
$this->assertEquals($parsedResponse['structure'], true);
}
public function testParseCollectionList()
{
$content = [
'sum' => 1,
'collections' => [
0 => [
'$id' => '5e5ea5c16897e',
'$permissions' => ['read' => ['role:all'], 'write' => ['role:all']],
'name' => 'Movies',
'dateCreated' => 1592981250,
'dateUpdated' => '5d529fd02b544198ae075bd57c1762bb',
'rules' => []
]
]
];
$model = Response::MODEL_COLLECTION_LIST;
$parsedResponse = $this->filter->parse($content, $model);
$this->assertEquals($parsedResponse['sum'], 1);
$this->assertEquals($parsedResponse['collections'][0]['$id'], '5e5ea5c16897e');
$this->assertEquals($parsedResponse['collections'][0]['$permissions'], ['read' => ['role:all'], 'write' => ['role:all']]);
$this->assertEquals($parsedResponse['collections'][0]['name'], 'Movies');
$this->assertEquals($parsedResponse['collections'][0]['dateCreated'], 1592981250);
$this->assertEquals($parsedResponse['collections'][0]['dateUpdated'], '5d529fd02b544198ae075bd57c1762bb');
$this->assertEquals($parsedResponse['collections'][0]['rules'], []);
$this->assertEquals($parsedResponse['collections'][0]['$collection'], Database::SYSTEM_COLLECTION_COLLECTIONS);
$this->assertEquals($parsedResponse['collections'][0]['structure'], true);
}
}

View file

@ -3,7 +3,7 @@
namespace Appwrite\Tests;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Filters\V06;
use Appwrite\Utopia\Response\Filters\V11;
use PHPUnit\Framework\TestCase;
use Swoole\Http\Response as SwooleResponse;
@ -25,7 +25,7 @@ class ResponseTest extends TestCase
$this->assertEquals($this->object->hasFilter(), false);
$this->assertEquals($this->object->getFilter(), null);
$filter = new V06();
$filter = new V11();
$this->object->setFilter($filter);
$this->assertEquals($this->object->hasFilter(), true);