diff --git a/.env b/.env index 1f0e7a152..27cf4ce71 100644 --- a/.env +++ b/.env @@ -23,6 +23,8 @@ _APP_DB_SCHEMA=appwrite _APP_DB_USER=user _APP_DB_PASS=password _APP_DB_ROOT_PASS=rootsecretpassword +_APP_PROJECT_DB=db_fra1_02=mysql://user:password@mariadb:3306/appwrite +_APP_CONSOLE_DB=db_fra1_01=mysql://user:password@mariadb:3306/appwrite _APP_STORAGE_DEVICE=Local _APP_STORAGE_S3_ACCESS_KEY= _APP_STORAGE_S3_SECRET= diff --git a/app/config/collections.php b/app/config/collections.php index 171463c03..910538432 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -390,6 +390,17 @@ $collections = [ 'array' => false, 'filters' => [], ], + [ + '$id' => 'database', + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 16384, + 'signed' => true, + 'required' => true, + 'default' => null, + 'array' => false, + 'filters' => [], + ], [ '$id' => 'logo', 'type' => Database::VAR_STRING, diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index c0f0fdd96..d4507ff56 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -104,6 +104,7 @@ App::post('/v1/projects') 'domains' => null, 'auths' => $auths, 'search' => implode(' ', [$projectId, $name]), + 'database' ])); /** @var array $collections */ $collections = Config::getParam('collections', []); diff --git a/app/http.php b/app/http.php index a7d10b9e0..5a49e49e8 100644 --- a/app/http.php +++ b/app/http.php @@ -66,7 +66,7 @@ $http->on('start', function (Server $http) use ($payloadSize, $register) { do { try { $attempts++; - $db = $register->get('dbPool')->get(); + $pool = $register->get('poolForConsole')->get(); $redis = $register->get('redisPool')->get(); break; // leave the do-while if successful } catch (\Exception $e) { diff --git a/app/init.php b/app/init.php index 0e1848b2c..8910fc34b 100644 --- a/app/init.php +++ b/app/init.php @@ -23,6 +23,8 @@ use Ahc\Jwt\JWT; use Ahc\Jwt\JWTException; use Appwrite\Extend\Exception; use Appwrite\Auth\Auth; +use Appwrite\Database\DatabasePool; +use Appwrite\DSN\DSN; use Appwrite\Event\Audit; use Appwrite\Event\Database as EventDatabase; use Appwrite\Event\Delete; @@ -442,29 +444,98 @@ $register->set('logger', function () { $adapter = new $classname($providerConfig); return new Logger($adapter); }); -$register->set('dbPool', function () { + +$register->set('poolForConsole', function () { + $dbs = App::getEnv('_APP_CONSOLE_DB', ''); + $dbs = explode(',', $dbs); + + $pools = new DatabasePool(); + foreach ($dbs as $db) { + $db = explode('=', $db); + $name = $db[0]; + $dsn = new DSN($db[1]); + + // var_dump($dsn->getHost(), $dsn->getPort(), $dsn->getDatabase(), $dsn->getUser(), $dsn->getPassword()); + + $pool = new PDOPool( + (new PDOConfig()) + ->withHost($dsn->getHost()) + ->withPort($dsn->getPort()) + ->withDbName($dsn->getDatabase()) + ->withCharset('utf8mb4') + ->withUsername($dsn->getUser()) + ->withPassword($dsn->getPassword()) + ->withOptions([ + PDO::ATTR_ERRMODE => App::isDevelopment() ? PDO::ERRMODE_WARNING : PDO::ERRMODE_SILENT, // If in production mode, warnings are not displayed + ]), + 64 + ); + + $pools->add($name, $pool); + } + + return $pools; +}); + +$register->set('poolForProject', function () { // Register DB connection - $dbHost = App::getEnv('_APP_DB_HOST', ''); - $dbPort = App::getEnv('_APP_DB_PORT', ''); - $dbUser = App::getEnv('_APP_DB_USER', ''); - $dbPass = App::getEnv('_APP_DB_PASS', ''); - $dbScheme = App::getEnv('_APP_DB_SCHEMA', ''); + // $dbHost = App::getEnv('_APP_DB_HOST', ''); + // $dbPort = App::getEnv('_APP_DB_PORT', ''); + // $dbUser = App::getEnv('_APP_DB_USER', ''); + // $dbPass = App::getEnv('_APP_DB_PASS', ''); + // $dbScheme = App::getEnv('_APP_DB_SCHEMA', ''); - $pool = new PDOPool( - (new PDOConfig()) - ->withHost($dbHost) - ->withPort($dbPort) - ->withDbName($dbScheme) - ->withCharset('utf8mb4') - ->withUsername($dbUser) - ->withPassword($dbPass) - ->withOptions([ - PDO::ATTR_ERRMODE => App::isDevelopment() ? PDO::ERRMODE_WARNING : PDO::ERRMODE_SILENT, // If in production mode, warnings are not displayed - ]), - 64 - ); + // var_dump($dbHost, $dbPort, $dbScheme, $dbUser, $dbPass); + // $pool = new PDOPool( + // (new PDOConfig()) + // ->withHost($dbHost) + // ->withPort($dbPort) + // ->withDbName($dbScheme) + // ->withCharset('utf8mb4') + // ->withUsername($dbUser) + // ->withPassword($dbPass) + // ->withOptions([ + // PDO::ATTR_ERRMODE => App::isDevelopment() ? PDO::ERRMODE_WARNING : PDO::ERRMODE_SILENT, // If in production mode, warnings are not displayed + // ]), + // 64 + // ); - return $pool; + // return $pool; + + // $dbForConsole = App::getEnv('_APP_CONSOLE_DB', ''); + // $dbForConsole = explode('=', $dbForConsole); + // $name = $dbForConsole[0]; + // $dsn = new DSN($dbForConsole[1]); + + $dbs = App::getEnv('_APP_PROJECT_DB', ''); + $dbs = explode(',', $dbs); + + $pools = new DatabasePool(); + foreach ($dbs as $db) { + $db = explode('=', $db); + $name = $db[0]; + $dsn = new DSN($db[1]); + + // var_dump($dsn->getHost(), $dsn->getPort(), $dsn->getDatabase(), $dsn->getUser(), $dsn->getPassword()); + + $pool = new PDOPool( + (new PDOConfig()) + ->withHost($dsn->getHost()) + ->withPort($dsn->getPort()) + ->withDbName($dsn->getDatabase()) + ->withCharset('utf8mb4') + ->withUsername($dsn->getUser()) + ->withPassword($dsn->getPassword()) + ->withOptions([ + PDO::ATTR_ERRMODE => App::isDevelopment() ? PDO::ERRMODE_WARNING : PDO::ERRMODE_SILENT, // If in production mode, warnings are not displayed + ]), + 64 + ); + + $pools->add($name, $pool); + } + + return $pools; }); $register->set('redisPool', function () { $redisHost = App::getEnv('_APP_REDIS_HOST', ''); @@ -867,6 +938,10 @@ App::setResource('console', function () { App::setResource('dbForProject', function ($db, $cache, $project) { $cache = new Cache(new RedisCache($cache)); + // Get name of database from the projects collection in the console DB + + // $dbName = $project->getAttribute('database',''); + $database = new Database(new MariaDB($db), $cache); $database->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite')); $database->setNamespace("_{$project->getId()}"); diff --git a/composer.lock b/composer.lock index f9dcd577c..097de5c02 100644 --- a/composer.lock +++ b/composer.lock @@ -689,16 +689,16 @@ }, { "name": "guzzlehttp/psr7", - "version": "2.2.1", + "version": "2.2.2", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "c94a94f120803a18554c1805ef2e539f8285f9a2" + "reference": "a119247127ff95789a2d95c347cd74721fbedaa4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/c94a94f120803a18554c1805ef2e539f8285f9a2", - "reference": "c94a94f120803a18554c1805ef2e539f8285f9a2", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/a119247127ff95789a2d95c347cd74721fbedaa4", + "reference": "a119247127ff95789a2d95c347cd74721fbedaa4", "shasum": "" }, "require": { @@ -784,7 +784,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.2.1" + "source": "https://github.com/guzzle/psr7/tree/2.2.2" }, "funding": [ { @@ -800,7 +800,7 @@ "type": "tidelift" } ], - "time": "2022-03-20T21:55:58+00:00" + "time": "2022-06-08T19:55:23+00:00" }, { "name": "influxdb/influxdb-php", @@ -1704,88 +1704,6 @@ ], "time": "2022-02-25T11:15:52+00:00" }, - { - "name": "symfony/polyfill-ctype", - "version": "v1.26.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4", - "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "provide": { - "ext-ctype": "*" - }, - "suggest": { - "ext-ctype": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.26-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for ctype functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "ctype", - "polyfill", - "portable" - ], - "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.26.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2022-05-24T11:49:31+00:00" - }, { "name": "symfony/polyfill-php80", "version": "v1.26.0", @@ -2905,21 +2823,21 @@ }, { "name": "webmozart/assert", - "version": "1.10.0", + "version": "1.11.0", "source": { "type": "git", "url": "https://github.com/webmozarts/assert.git", - "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25" + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/6964c76c7804814a842473e0c8fd15bab0f18e25", - "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0", - "symfony/polyfill-ctype": "^1.8" + "ext-ctype": "*", + "php": "^7.2 || ^8.0" }, "conflict": { "phpstan/phpstan": "<0.12.20", @@ -2957,9 +2875,9 @@ ], "support": { "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.10.0" + "source": "https://github.com/webmozarts/assert/tree/1.11.0" }, - "time": "2021-03-09T10:59:23+00:00" + "time": "2022-06-03T18:03:27+00:00" } ], "packages-dev": [ @@ -5086,6 +5004,88 @@ ], "time": "2022-04-18T20:38:04+00:00" }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.26.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4", + "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.26-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.26.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-24T11:49:31+00:00" + }, { "name": "symfony/polyfill-mbstring", "version": "v1.26.0", diff --git a/docker-compose.yml b/docker-compose.yml index b6e1ed68b..e9c4a0e7e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -143,6 +143,8 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS + - _APP_PROJECT_DB + - _APP_CONSOLE_DB - _APP_SMTP_HOST - _APP_SMTP_PORT - _APP_SMTP_SECURE @@ -221,6 +223,7 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS + - _APP_DB - _APP_USAGE_STATS - _APP_LOGGING_PROVIDER - _APP_LOGGING_CONFIG @@ -251,6 +254,7 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS + - _APP_DB - _APP_LOGGING_PROVIDER - _APP_LOGGING_CONFIG @@ -311,6 +315,7 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS + - _APP_DB - *x-env-storage - _APP_LOGGING_PROVIDER - _APP_LOGGING_CONFIG @@ -344,6 +349,7 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS + - _APP_DB - _APP_LOGGING_PROVIDER - _APP_LOGGING_CONFIG @@ -375,6 +381,7 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS + - _APP_DB - _APP_LOGGING_PROVIDER - _APP_LOGGING_CONFIG @@ -409,6 +416,7 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS + - _APP_DB - _APP_LOGGING_PROVIDER - _APP_LOGGING_CONFIG @@ -439,6 +447,7 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS + - _APP_DB - _APP_FUNCTIONS_TIMEOUT - _APP_EXECUTOR_SECRET - _APP_EXECUTOR_HOST @@ -551,6 +560,7 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS + - _APP_DB - _APP_MAINTENANCE_INTERVAL - _APP_MAINTENANCE_RETENTION_EXECUTION - _APP_MAINTENANCE_RETENTION_ABUSE @@ -576,11 +586,8 @@ services: environment: - _APP_ENV - _APP_OPENSSL_KEY_V1 - - _APP_DB_HOST - - _APP_DB_PORT - - _APP_DB_SCHEMA - - _APP_DB_USER - - _APP_DB_PASS + + - _APP_DB - _APP_INFLUXDB_HOST - _APP_INFLUXDB_PORT - _APP_USAGE_AGGREGATION_INTERVAL diff --git a/src/Appwrite/DSN/DSN.php b/src/Appwrite/DSN/DSN.php new file mode 100644 index 000000000..48a766d59 --- /dev/null +++ b/src/Appwrite/DSN/DSN.php @@ -0,0 +1,134 @@ +scheme = $parts['scheme'] ?? null; + $this->user = $parts['user'] ?? null; + $this->password = $parts['pass'] ?? null; + $this->host = $parts['host'] ?? null; + $this->port = $parts['port'] ?? null; + $this->database = $parts['path'] ?? null; + $this->query = $parts['query'] ?? null; + } + + /** + * Return the scheme. + * + * @return string + */ + public function getScheme(): string + { + return $this->scheme; + } + + /** + * Return the user. + * + * @return ?string + */ + public function getUser(): ?string + { + return $this->user; + } + + /** + * Return the password. + * + * @return ?string + */ + public function getPassword(): ?string + { + return $this->password; + } + + /** + * Return the host + * + * @return string + */ + public function getHost(): string + { + return $this->host; + } + + /** + * Return the port + * + * @return ?string + */ + public function getPort(): ?string + { + return $this->port; + } + + /** + * Return the database + * + * @return ?string + */ + public function getDatabase(): ?string + { + return ltrim($this->database, '/'); + } + + /** + * Return the query string + * + * @return ?string + */ + public function getQuery(): ?string + { + return $this->query; + } +} \ No newline at end of file diff --git a/src/Appwrite/Database/DatabasePool.php b/src/Appwrite/Database/DatabasePool.php new file mode 100644 index 000000000..6678ebd20 --- /dev/null +++ b/src/Appwrite/Database/DatabasePool.php @@ -0,0 +1,37 @@ +pools[$name] = $dbPool; + } + + public function get(string $name = 'console'): ?PDOProxy + { + $pool = $this->pools[$name] ?? null; + if ($pool === null) { + throw new Exception("Database Pool with name : $name not found. Please check the value of _APP_PROJECT_DB in .env", 500); + } + return $pool->get(); + } + + public function put(PDOProxy $db, string $name = 'console'): void + { + $pool = $this->pools[$name] ?? null; + if ($pool === null) { + throw new Exception("Database Pool with name : $name not found. Cannot put", 500); + } + $pool->put($db); + } + +} \ No newline at end of file diff --git a/tests/unit/DSN/DSNTest.php b/tests/unit/DSN/DSNTest.php new file mode 100644 index 000000000..6ffbf76b2 --- /dev/null +++ b/tests/unit/DSN/DSNTest.php @@ -0,0 +1,90 @@ +assertEquals("mariadb", $dsn->getScheme()); + $this->assertEquals("user", $dsn->getUser()); + $this->assertEquals("password", $dsn->getPassword()); + $this->assertEquals("localhost", $dsn->getHost()); + $this->assertEquals("3306", $dsn->getPort()); + $this->assertEquals("database", $dsn->getDatabase()); + $this->assertEquals("charset=utf8&timezone=UTC", $dsn->getQuery()); + + $dsn = new DSN("mariadb://user@localhost:3306/database?charset=utf8&timezone=UTC"); + $this->assertEquals("mariadb", $dsn->getScheme()); + $this->assertEquals("user", $dsn->getUser()); + $this->assertNull($dsn->getPassword()); + $this->assertEquals("localhost", $dsn->getHost()); + $this->assertEquals("3306", $dsn->getPort()); + $this->assertEquals("database", $dsn->getDatabase()); + $this->assertEquals("charset=utf8&timezone=UTC", $dsn->getQuery()); + + $dsn = new DSN("mariadb://user@localhost/database?charset=utf8&timezone=UTC"); + $this->assertEquals("mariadb", $dsn->getScheme()); + $this->assertEquals("user", $dsn->getUser()); + $this->assertNull($dsn->getPassword()); + $this->assertEquals("localhost", $dsn->getHost()); + $this->assertNull($dsn->getPort()); + $this->assertEquals("database", $dsn->getDatabase()); + $this->assertEquals("charset=utf8&timezone=UTC", $dsn->getQuery()); + + $dsn = new DSN("mariadb://user@localhost?charset=utf8&timezone=UTC"); + $this->assertEquals("mariadb", $dsn->getScheme()); + $this->assertEquals("user", $dsn->getUser()); + $this->assertNull($dsn->getPassword()); + $this->assertEquals("localhost", $dsn->getHost()); + $this->assertNull($dsn->getPort()); + $this->assertEmpty($dsn->getDatabase()); + $this->assertEquals("charset=utf8&timezone=UTC", $dsn->getQuery()); + + $dsn = new DSN("mariadb://user@localhost"); + $this->assertEquals("mariadb", $dsn->getScheme()); + $this->assertEquals("user", $dsn->getUser()); + $this->assertNull($dsn->getPassword()); + $this->assertEquals("localhost", $dsn->getHost()); + $this->assertNull($dsn->getPort()); + $this->assertEmpty($dsn->getDatabase()); + $this->assertNull($dsn->getQuery()); + + $dsn = new DSN("mariadb://user:@localhost"); + $this->assertEquals("mariadb", $dsn->getScheme()); + $this->assertEquals("user", $dsn->getUser()); + $this->assertEmpty($dsn->getPassword()); + $this->assertEquals("localhost", $dsn->getHost()); + $this->assertNull($dsn->getPort()); + $this->assertEmpty($dsn->getDatabase()); + $this->assertNull($dsn->getQuery()); + + $dsn = new DSN("mariadb://localhost"); + $this->assertEquals("mariadb", $dsn->getScheme()); + $this->assertNull($dsn->getUser()); + $this->assertNull($dsn->getPassword()); + $this->assertEquals("localhost", $dsn->getHost()); + $this->assertNull($dsn->getPort()); + $this->assertEmpty($dsn->getDatabase()); + $this->assertNull($dsn->getQuery()); + } + + public function testFail(): void + { + $this->expectException(\InvalidArgumentException::class); + $dsn = new DSN("mariadb://"); + } +} \ No newline at end of file