diff --git a/Dockerfile b/Dockerfile index 96857ea3c..c432f8de0 100755 --- a/Dockerfile +++ b/Dockerfile @@ -212,6 +212,7 @@ RUN chmod +x /usr/local/bin/doctor && \ chmod +x /usr/local/bin/vars && \ chmod +x /usr/local/bin/worker-audits && \ chmod +x /usr/local/bin/worker-certificates && \ + chmod +x /usr/local/bin/worker-database && \ chmod +x /usr/local/bin/worker-deletes && \ chmod +x /usr/local/bin/worker-functions && \ chmod +x /usr/local/bin/worker-mails && \ diff --git a/app/config/collections2.php b/app/config/collections2.php index b6dcdcd17..2e0bf087d 100644 --- a/app/config/collections2.php +++ b/app/config/collections2.php @@ -19,6 +19,7 @@ $collections = [ 'size' => 128, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -29,6 +30,7 @@ $collections = [ 'size' => 256, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -39,6 +41,7 @@ $collections = [ 'size' => Database::LENGTH_KEY, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -49,6 +52,7 @@ $collections = [ 'size' => Database::LENGTH_KEY, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -59,6 +63,7 @@ $collections = [ 'size' => 16384, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -69,6 +74,7 @@ $collections = [ 'size' => 0, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -79,6 +85,7 @@ $collections = [ 'size' => 256, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -89,6 +96,7 @@ $collections = [ 'size' => 256, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -99,6 +107,7 @@ $collections = [ 'size' => 256, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -109,6 +118,7 @@ $collections = [ 'size' => 256, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -119,6 +129,7 @@ $collections = [ 'size' => 256, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -129,6 +140,7 @@ $collections = [ 'size' => 256, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -139,6 +151,7 @@ $collections = [ 'size' => 16384, 'signed' => true, 'required' => false, + 'default' => null, 'array' => true, 'filters' => ['json'], ], @@ -149,6 +162,7 @@ $collections = [ 'size' => 16384, 'signed' => true, 'required' => false, + 'default' => null, 'array' => true, 'filters' => ['json'], ], @@ -159,6 +173,7 @@ $collections = [ 'size' => 16384, 'signed' => true, 'required' => false, + 'default' => null, 'array' => true, 'filters' => ['json'], ], @@ -169,6 +184,7 @@ $collections = [ 'size' => 16384, 'signed' => true, 'required' => false, + 'default' => null, 'array' => true, 'filters' => ['json'], ], @@ -179,6 +195,7 @@ $collections = [ 'size' => 16384, 'signed' => true, 'required' => false, + 'default' => null, 'array' => true, 'filters' => ['json'], ], @@ -206,6 +223,7 @@ $collections = [ 'size' => 256, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -216,6 +234,7 @@ $collections = [ 'size' => 1024, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -226,6 +245,7 @@ $collections = [ 'size' => 0, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -236,6 +256,7 @@ $collections = [ 'size' => 16384, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -246,6 +267,7 @@ $collections = [ 'size' => 0, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -256,6 +278,7 @@ $collections = [ 'size' => 16384, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => ['json'] ], @@ -266,6 +289,7 @@ $collections = [ 'size' => 0, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -276,6 +300,7 @@ $collections = [ 'size' => 0, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -286,6 +311,7 @@ $collections = [ 'size' => 0, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -296,6 +322,7 @@ $collections = [ 'size' => 16384, 'signed' => true, 'required' => false, + 'default' => null, 'array' => true, 'filters' => ['json'], ], @@ -306,6 +333,7 @@ $collections = [ 'size' => 16384, 'signed' => true, 'required' => false, + 'default' => null, 'array' => true, 'filters' => ['json'], ], @@ -316,6 +344,7 @@ $collections = [ 'size' => 16384, 'signed' => true, 'required' => false, + 'default' => null, 'array' => true, 'filters' => ['json'], ], @@ -343,6 +372,7 @@ $collections = [ 'size' => 128, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -353,6 +383,7 @@ $collections = [ 'size' => 0, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -363,6 +394,7 @@ $collections = [ 'size' => 0, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -390,6 +422,7 @@ $collections = [ 'size' => Database::LENGTH_KEY, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -400,6 +433,7 @@ $collections = [ 'size' => Database::LENGTH_KEY, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -410,6 +444,7 @@ $collections = [ 'size' => 128, 'signed' => true, 'required' => false, + 'default' => null, 'array' => true, 'filters' => [], ], @@ -420,6 +455,7 @@ $collections = [ 'size' => 0, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -430,6 +466,7 @@ $collections = [ 'size' => 0, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -440,6 +477,7 @@ $collections = [ 'size' => 0, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -450,6 +488,7 @@ $collections = [ 'size' => 256, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -491,6 +530,7 @@ $collections = [ 'size' => 0, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -502,6 +542,7 @@ $collections = [ 'size' => Database::LENGTH_KEY, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -512,6 +553,7 @@ $collections = [ 'size' => 2048, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -522,6 +564,7 @@ $collections = [ 'size' => 2048, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -532,6 +575,7 @@ $collections = [ 'size' => 2048, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -542,6 +586,7 @@ $collections = [ 'size' => 127, // https://tools.ietf.org/html/rfc4288#section-4.2 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -552,6 +597,7 @@ $collections = [ 'size' => 0, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -562,6 +608,7 @@ $collections = [ 'size' => 0, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -572,6 +619,7 @@ $collections = [ 'size' => 255, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -582,6 +630,7 @@ $collections = [ 'size' => 2048, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -592,6 +641,7 @@ $collections = [ 'size' => 64, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -602,6 +652,7 @@ $collections = [ 'size' => 64, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -612,6 +663,7 @@ $collections = [ 'size' => 2048, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -622,6 +674,7 @@ $collections = [ 'size' => 2048, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -656,6 +709,7 @@ $collections = [ 'size' => 128, 'signed' => true, 'required' => false, + 'default' => null, 'array' => true, 'filters' => [], ], @@ -666,6 +720,7 @@ $collections = [ 'size' => 0, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -676,6 +731,7 @@ $collections = [ 'size' => 0, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -687,6 +743,7 @@ $collections = [ 'size' => Database::LENGTH_KEY, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -697,6 +754,7 @@ $collections = [ 'size' => 2048, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -707,6 +765,7 @@ $collections = [ 'size' => 2048, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -717,6 +776,7 @@ $collections = [ 'size' => Database::LENGTH_KEY, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -727,6 +787,7 @@ $collections = [ 'size' => 8192, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => ['json', 'encrypt'], ], @@ -737,6 +798,7 @@ $collections = [ 'size' => 256, 'signed' => true, 'required' => false, + 'default' => null, 'array' => true, 'filters' => [], ], @@ -747,6 +809,7 @@ $collections = [ 'size' => 128, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -757,6 +820,7 @@ $collections = [ 'size' => 0, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -767,6 +831,7 @@ $collections = [ 'size' => 0, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -777,6 +842,7 @@ $collections = [ 'size' => 0, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -804,6 +870,7 @@ $collections = [ 'size' => 0, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -814,6 +881,7 @@ $collections = [ 'size' => Database::LENGTH_KEY, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -825,6 +893,7 @@ $collections = [ 'size' => 2048, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -835,6 +904,7 @@ $collections = [ 'size' => 2048, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -845,6 +915,7 @@ $collections = [ 'size' => 0, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -872,6 +943,7 @@ $collections = [ 'size' => 0, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -882,6 +954,7 @@ $collections = [ 'size' => Database::LENGTH_KEY, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -892,6 +965,7 @@ $collections = [ 'size' => Database::LENGTH_KEY, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -903,6 +977,7 @@ $collections = [ 'size' => 128, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -913,6 +988,7 @@ $collections = [ 'size' => 128, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -923,6 +999,7 @@ $collections = [ 'size' => 16384, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -933,6 +1010,7 @@ $collections = [ 'size' => 16384, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -943,6 +1021,7 @@ $collections = [ 'size' => 0, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -953,6 +1032,7 @@ $collections = [ 'size' => 0, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ], @@ -967,6 +1047,91 @@ $collections = [ ] ], ], + 'certificates' => [ + '$collection' => Database::COLLECTIONS, + '$id' => 'certificates', + 'name' => 'Certificates', + 'attributes' => [ + [ + '$id' => 'domain', + 'type' => Database::VAR_STRING, + 'format' => '', + // The maximum total length of a domain name or number is 255 characters. + // https://datatracker.ietf.org/doc/html/rfc2821#section-4.5.3.1 + // https://datatracker.ietf.org/doc/html/rfc5321#section-4.5.3.1.2 + 'size' => 255, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => 'issueDate', + 'type' => Database::VAR_INTEGER, + 'format' => '', + 'size' => 0, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => 'renewDate', + 'type' => Database::VAR_INTEGER, + 'format' => '', + 'size' => 0, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => 'attempts', + 'type' => Database::VAR_INTEGER, + 'format' => '', + 'size' => 0, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => 'log', + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 16384, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => 'updated', + 'type' => Database::VAR_INTEGER, + 'format' => '', + 'size' => 0, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + ], + 'indexes' => [ + [ + '$id' => '_key_domain', + 'type' => Database::INDEX_KEY, + 'attributes' => ['domain'], + 'lengths' => [255], + 'orders' => [Database::ORDER_ASC], + ], + ], + ], ]; /* @@ -984,6 +1149,7 @@ foreach ($providers as $index => $provider) { 'size' => 16384, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ]; @@ -995,6 +1161,7 @@ foreach ($providers as $index => $provider) { 'size' => 16384, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ]; @@ -1008,6 +1175,7 @@ foreach ($auth as $index => $method) { 'size' => 0, 'signed' => true, 'required' => false, + 'default' => null, 'array' => false, 'filters' => [], ]; diff --git a/app/config/events.php b/app/config/events.php index b27a5eafb..def827916 100644 --- a/app/config/events.php +++ b/app/config/events.php @@ -82,6 +82,26 @@ return [ 'model' => Response::MODEL_COLLECTION, 'note' => '', ], + 'database.attributes.create' => [ + 'description' => 'This event triggers when a collection attribute is created.', + 'model' => Response::MODEL_ATTRIBUTE, + 'note' => '', + ], + 'database.attributes.delete' => [ + 'description' => 'This event triggers when a collection attribute is deleted.', + 'model' => Response::MODEL_ATTRIBUTE, + 'note' => '', + ], + 'database.indexes.create' => [ + 'description' => 'This event triggers when a collection index is created.', + 'model' => Response::MODEL_INDEX, + 'note' => '', + ], + 'database.indexes.delete' => [ + 'description' => 'This event triggers when a collection index is deleted.', + 'model' => Response::MODEL_INDEX, + 'note' => '', + ], 'database.documents.create' => [ 'description' => 'This event triggers when a database document is created.', 'model' => Response::MODEL_DOCUMENT, diff --git a/app/config/roles.php b/app/config/roles.php index 3e06ddbfd..34af1461f 100644 --- a/app/config/roles.php +++ b/app/config/roles.php @@ -32,6 +32,10 @@ $admins = [ 'users.write', 'collections.read', 'collections.write', + 'attributes.read', + 'attributes.write', + 'indexes.read', + 'indexes.write', 'platforms.read', 'platforms.write', 'keys.read', diff --git a/app/config/scopes.php b/app/config/scopes.php index 088502c5d..cf40fb076 100644 --- a/app/config/scopes.php +++ b/app/config/scopes.php @@ -19,6 +19,18 @@ return [ // List of publicly visible scopes 'collections.write' => [ 'description' => 'Access to create, update, and delete your project\'s database collections', ], + 'attributes.read' => [ + 'description' => 'Access to read your project\'s database collection\'s attributes', + ], + 'attributes.write' => [ + 'description' => 'Access to create, update, and delete your project\'s database collection\'s attributes', + ], + 'indexes.read' => [ + 'description' => 'Access to read your project\'s database collection\'s indexes', + ], + 'indexes.write' => [ + 'description' => 'Access to create, update, and delete your project\'s database collection\'s indexes', + ], 'documents.read' => [ 'description' => 'Access to read your project\'s database documents', ], diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 99eeccfa3..72d46ca17 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -858,7 +858,6 @@ App::get('/v1/account/logs') /** @var Utopia\Database\Database $dbForInternal */ $audit = new Audit($dbForInternal); - $countries = $locale->getText('countries'); $logs = $audit->getLogsByUserAndEvents($user->getId(), [ diff --git a/app/controllers/api/database.php b/app/controllers/api/database.php index d4cad710c..87d70167d 100644 --- a/app/controllers/api/database.php +++ b/app/controllers/api/database.php @@ -2,21 +2,26 @@ use Utopia\App; use Utopia\Exception; +use Utopia\Validator\Boolean; +use Utopia\Validator\Integer; +use Utopia\Validator\Numeric; use Utopia\Validator\Range; use Utopia\Validator\WhiteList; +use Utopia\Validator\Wildcard; use Utopia\Validator\Text; use Utopia\Validator\ArrayList; use Utopia\Validator\JSON; -use Appwrite\Database\Database; -use Appwrite\Database\Document; -use Appwrite\Database\Validator\UID; -use Appwrite\Database\Validator\Key; -use Appwrite\Database\Validator\Structure; -use Appwrite\Database\Validator\Collection; -use Appwrite\Database\Validator\Authorization; -use Appwrite\Database\Exception\Authorization as AuthorizationException; -use Appwrite\Database\Exception\Structure as StructureException; +use Utopia\Database\Validator\Key; +use Utopia\Database\Validator\Permissions; +use Utopia\Database\Validator\QueryValidator; +use Utopia\Database\Validator\Queries as QueriesValidator; +use Utopia\Database\Validator\UID; +use Utopia\Database\Exception\Authorization as AuthorizationException; +use Utopia\Database\Exception\Structure as StructureException; use Appwrite\Utopia\Response; +use Utopia\Database\Database; +use Utopia\Database\Document; +use Utopia\Database\Query; App::post('/v1/database/collections') ->desc('Create Collection') @@ -31,62 +36,38 @@ App::post('/v1/database/collections') ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_COLLECTION) ->param('name', '', new Text(128), 'Collection name. Max length: 128 chars.') - ->param('read', [], new ArrayList(new Text(64)), 'An array of strings with read permissions. By default no user is granted with any read permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.') - ->param('write', [], new ArrayList(new Text(64)), 'An array of strings with write permissions. By default no user is granted with any write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.') - ->param('rules', [], function ($projectDB) { return new ArrayList(new Collection($projectDB, [Database::SYSTEM_COLLECTION_RULES], ['$collection' => Database::SYSTEM_COLLECTION_RULES, '$permissions' => ['read' => [], 'write' => []]])); }, 'Array of [rule objects](/docs/rules). Each rule define a collection field name, data type and validation.', false, ['projectDB']) + ->param('read', null, new Permissions(), 'An array of strings with read permissions. By default no user is granted with any read permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.') + ->param('write', null, new Permissions(), 'An array of strings with write permissions. By default no user is granted with any write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.') ->inject('response') - ->inject('projectDB') + ->inject('dbForExternal') ->inject('audits') - ->action(function ($name, $read, $write, $rules, $response, $projectDB, $audits) { + ->action(function ($name, $read, $write, $response, $dbForExternal, $audits) { /** @var Appwrite\Utopia\Response $response */ - /** @var Appwrite\Database\Database $projectDB */ + /** @var Utopia\Database\Database $dbForExternal*/ /** @var Appwrite\Event\Event $audits */ - $parsedRules = []; + $id = $dbForExternal->getId(); - foreach ($rules as &$rule) { - $parsedRules[] = \array_merge([ - '$collection' => Database::SYSTEM_COLLECTION_RULES, - '$permissions' => [ - 'read' => $read, - 'write' => $write, - ], - ], $rule); - } + $collection = $dbForExternal->createCollection($id); - try { - $data = $projectDB->createDocument([ - '$collection' => Database::SYSTEM_COLLECTION_COLLECTIONS, - 'name' => $name, - 'dateCreated' => \time(), - 'dateUpdated' => \time(), - 'structure' => true, - '$permissions' => [ - 'read' => $read, - 'write' => $write, - ], - 'rules' => $parsedRules, - ]); - } catch (AuthorizationException $exception) { - throw new Exception('Unauthorized permissions', 401); - } catch (StructureException $exception) { - throw new Exception('Bad structure. '.$exception->getMessage(), 400); - } catch (\Exception $exception) { - throw new Exception('Failed saving document to DB', 500); - } + // TODO@kodumbeats what should the default permissions be? + $read = (is_null($read)) ? ($collection->getRead() ?? []) : $read; // By default inherit read permissions + $write = (is_null($write)) ? ($collection->getWrite() ?? []) : $write; // By default inherit write permissions - if (false === $data) { - throw new Exception('Failed saving collection to DB', 500); - } + $collection->setAttribute('name', $name); + $collection->setAttribute('$read', $read); + $collection->setAttribute('$write', $write); + + $dbForExternal->updateDocument(Database::COLLECTIONS, $id, $collection); $audits ->setParam('event', 'database.collections.create') - ->setParam('resource', 'database/collection/'.$data->getId()) - ->setParam('data', $data->getArrayCopy()) + ->setParam('resource', 'database/collection/'.$collection->getId()) + ->setParam('data', $collection->getArrayCopy()) ; $response->setStatusCode(Response::STATUS_CODE_CREATED); - $response->dynamic($data, Response::MODEL_COLLECTION); + $response->dynamic2($collection, Response::MODEL_COLLECTION); }); App::get('/v1/database/collections') @@ -105,24 +86,16 @@ App::get('/v1/database/collections') ->param('offset', 0, new Range(0, 40000), 'Results offset. The default value is 0. Use this param to manage pagination.', true) ->param('orderType', 'ASC', new WhiteList(['ASC', 'DESC'], true), 'Order result by ASC or DESC order.', true) ->inject('response') - ->inject('projectDB') - ->action(function ($search, $limit, $offset, $orderType, $response, $projectDB) { + ->inject('dbForExternal') + ->action(function ($search, $limit, $offset, $orderType, $response, $dbForExternal) { /** @var Appwrite\Utopia\Response $response */ - /** @var Appwrite\Database\Database $projectDB */ + /** @var Utopia\Database\Database $dbForExternal */ - $results = $projectDB->getCollection([ - 'limit' => $limit, - 'offset' => $offset, - 'orderType' => $orderType, - 'search' => $search, - 'filters' => [ - '$collection='.Database::SYSTEM_COLLECTION_COLLECTIONS, - ], - ]); + $queries = ($search) ? [new Query('name', Query::TYPE_SEARCH, [$search])] : []; - $response->dynamic(new Document([ - 'collections' => $results, - 'sum' => $projectDB->getSum(), + $response->dynamic2(new Document([ + 'collections' => $dbForExternal->find(Database::COLLECTIONS, $queries, $limit, $offset, ['_id'], [$orderType]), + 'sum' => $dbForExternal->count(Database::COLLECTIONS, $queries, APP_LIMIT_COUNT), ]), Response::MODEL_COLLECTION_LIST); }); @@ -139,18 +112,18 @@ App::get('/v1/database/collections/:collectionId') ->label('sdk.response.model', Response::MODEL_COLLECTION) ->param('collectionId', '', new UID(), 'Collection unique ID.') ->inject('response') - ->inject('projectDB') - ->action(function ($collectionId, $response, $projectDB) { + ->inject('dbForExternal') + ->action(function ($collectionId, $response, $dbForExternal) { /** @var Appwrite\Utopia\Response $response */ - /** @var Appwrite\Database\Database $projectDB */ - - $collection = $projectDB->getDocument($collectionId, false); + /** @var Utopia\Database\Database $dbForExternal */ - if (empty($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { + $collection = $dbForExternal->getCollection($collectionId); + + if ($collection->isEmpty()) { throw new Exception('Collection not found', 404); } - $response->dynamic($collection, Response::MODEL_COLLECTION); + $response->dynamic2($collection, Response::MODEL_COLLECTION); }); App::put('/v1/database/collections/:collectionId') @@ -167,59 +140,36 @@ App::put('/v1/database/collections/:collectionId') ->label('sdk.response.model', Response::MODEL_COLLECTION) ->param('collectionId', '', new UID(), 'Collection unique ID.') ->param('name', null, new Text(128), 'Collection name. Max length: 128 chars.') - ->param('read', null, new ArrayList(new Text(64)), 'An array of strings with read permissions. By default inherits the existing read permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true) - ->param('write', null, new ArrayList(new Text(64)), 'An array of strings with write permissions. By default inherits the existing write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true) - ->param('rules', [], function ($projectDB) { return new ArrayList(new Collection($projectDB, [Database::SYSTEM_COLLECTION_RULES], ['$collection' => Database::SYSTEM_COLLECTION_RULES, '$permissions' => ['read' => [], 'write' => []]])); }, 'Array of [rule objects](/docs/rules). Each rule define a collection field name, data type and validation.', true, ['projectDB']) + ->param('read', null, new Permissions(), 'An array of strings with read permissions. By default inherits the existing read permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true) + ->param('write', null, new Permissions(), 'An array of strings with write permissions. By default inherits the existing write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true) ->inject('response') - ->inject('projectDB') + ->inject('dbForExternal') ->inject('audits') - ->action(function ($collectionId, $name, $read, $write, $rules, $response, $projectDB, $audits) { + ->action(function ($collectionId, $name, $read, $write, $response, $dbForExternal, $audits) { /** @var Appwrite\Utopia\Response $response */ - /** @var Appwrite\Database\Database $projectDB */ + /** @var Utopia\Database\Database $dbForExternal */ /** @var Appwrite\Event\Event $audits */ - $collection = $projectDB->getDocument($collectionId, false); + $collection = $dbForExternal->getCollection($collectionId); - if (empty($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { + if ($collection->isEmpty()) { throw new Exception('Collection not found', 404); } - $parsedRules = []; - $read = (is_null($read)) ? ($collection->getPermissions()['read'] ?? []) : $read; // By default inherit read permissions - $write = (is_null($write)) ? ($collection->getPermissions()['write'] ?? []) : $write; // By default inherit write permissions - - foreach ($rules as &$rule) { - $parsedRules[] = \array_merge([ - '$collection' => Database::SYSTEM_COLLECTION_RULES, - '$permissions' => [ - 'read' => $read, - 'write' => $write, - ], - ], $rule); - } + $read = (is_null($read)) ? ($collection->getRead() ?? []) : $read; // By default inherit read permissions + $write = (is_null($write)) ? ($collection->getWrite() ?? []) : $write; // By default inherit write permissions try { - $collection = $projectDB->updateDocument(\array_merge($collection->getArrayCopy(), [ + $collection = $dbForExternal->updateDocument(Database::COLLECTIONS, $collection->getId(), new Document(\array_merge($collection->getArrayCopy(), [ 'name' => $name, - 'structure' => true, - 'dateUpdated' => \time(), - '$permissions' => [ - 'read' => $read, - 'write' => $write, - ], - 'rules' => $parsedRules, - ])); + '$read' => $read, + '$write' => $write + ]))); } catch (AuthorizationException $exception) { throw new Exception('Unauthorized permissions', 401); } catch (StructureException $exception) { throw new Exception('Bad structure. '.$exception->getMessage(), 400); - } catch (\Exception $exception) { - throw new Exception('Failed saving document to DB', 500); - } - - if (false === $collection) { - throw new Exception('Failed saving collection to DB', 500); - } + } $audits ->setParam('event', 'database.collections.update') @@ -227,7 +177,7 @@ App::put('/v1/database/collections/:collectionId') ->setParam('data', $collection->getArrayCopy()) ; - $response->dynamic($collection, Response::MODEL_COLLECTION); + $response->dynamic2($collection, Response::MODEL_COLLECTION); }); App::delete('/v1/database/collections/:collectionId') @@ -243,33 +193,26 @@ App::delete('/v1/database/collections/:collectionId') ->label('sdk.response.model', Response::MODEL_NONE) ->param('collectionId', '', new UID(), 'Collection unique ID.') ->inject('response') - ->inject('projectDB') + ->inject('dbForExternal') ->inject('events') ->inject('audits') ->inject('deletes') - ->action(function ($collectionId, $response, $projectDB, $events, $audits, $deletes) { + ->action(function ($collectionId, $response, $dbForExternal, $events, $audits, $deletes) { /** @var Appwrite\Utopia\Response $response */ - /** @var Appwrite\Database\Database $projectDB */ + /** @var Utopia\Database\Database $dbForExternal */ /** @var Appwrite\Event\Event $events */ /** @var Appwrite\Event\Event $audits */ - $collection = $projectDB->getDocument($collectionId, false); + $collection = $dbForExternal->getCollection($collectionId); - if (empty($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { + if ($collection->isEmpty()) { throw new Exception('Collection not found', 404); } - if (!$projectDB->deleteDocument($collectionId)) { - throw new Exception('Failed to remove collection from DB', 500); - } - - $deletes - ->setParam('type', DELETE_TYPE_DOCUMENT) - ->setParam('document', $collection) - ; + $dbForExternal->deleteCollection($collectionId); $events - ->setParam('eventData', $response->output($collection, Response::MODEL_COLLECTION)) + ->setParam('eventData', $response->output2($collection, Response::MODEL_COLLECTION)) ; $audits @@ -281,6 +224,453 @@ App::delete('/v1/database/collections/:collectionId') $response->noContent(); }); +App::post('/v1/database/collections/:collectionId/attributes') + ->desc('Create Attribute') + ->groups(['api', 'database']) + ->label('event', 'database.attributes.create') + ->label('scope', 'attributes.write') + ->label('sdk.namespace', 'database') + ->label('sdk.platform', [APP_PLATFORM_SERVER]) + ->label('sdk.method', 'createAttribute') + ->label('sdk.description', '/docs/references/database/create-attribute.md') + ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_ATTRIBUTE) + ->param('collectionId', '', new UID(), 'Collection unique ID. You can create a new collection using the Database service [server integration](/docs/server/database#createCollection).') + // TODO@kodumbeats attributeId + ->param('id', '', new Key(), 'Attribute ID.') + // TODO@kodumbeats whitelist (allowlist) + ->param('type', null, new Text(8), 'Attribute type.') + // TODO@kodumbeats hide size for ints/floats/bools + ->param('size', null, new Integer(), 'Attribute size for text attributes, in number of characters. For integers, floats, or bools, use 0.') + ->param('required', null, new Boolean(), 'Is attribute required?') + ->param('default', null, new Wildcard(), 'Default value for attribute when not provided. Cannot be set when attribute is required.', true) + ->param('array', false, new Boolean(), 'Is attribute an array?', true) + ->inject('response') + ->inject('dbForExternal') + ->inject('database') + ->inject('audits') + ->action(function ($collectionId, $id, $type, $size, $required, $default, $array, $response, $dbForExternal, $database, $audits) { + /** @var Appwrite\Utopia\Response $response */ + /** @var Utopia\Database\Database $dbForExternal*/ + /** @var Appwrite\Event\Event $database */ + /** @var Appwrite\Event\Event $audits */ + + $collection = $dbForExternal->getCollection($collectionId); + + if ($collection->isEmpty()) { + throw new Exception('Collection not found', 404); + } + + // integers are signed by default, and filters are hidden from the endpoint. + $signed = true; + $filters = []; + + $success = $dbForExternal->addAttributeInQueue($collectionId, $id, $type, $size, $required, $default, $signed, $array, $filters); + + // Database->addAttributeInQueue() does not return a document + // So we need to create one for the response + // + // TODO@kodumbeats should $signed and $filters be part of the response model? + $attribute = new Document([ + '$collection' => $collectionId, + '$id' => $id, + 'type' => $type, + 'size' => $size, + 'required' => $required, + 'default' => $default, + 'signed' => $signed, + 'array' => $array, + 'filters' => $filters + ]); + + $database + ->setParam('type', CREATE_TYPE_ATTRIBUTE) + ->setParam('document', $attribute) + ; + + $audits + ->setParam('event', 'database.attributes.create') + ->setParam('resource', 'database/attributes/'.$attribute->getId()) + ->setParam('data', $attribute) + ; + + $response->setStatusCode(Response::STATUS_CODE_CREATED); + $response->dynamic2($attribute, Response::MODEL_ATTRIBUTE); + }); + +App::get('v1/database/collections/:collectionId/attributes') + ->desc('List Attributes') + ->groups(['api', 'database']) + ->label('scope', 'attributes.read') + ->label('sdk.namespace', 'database') + ->label('sdk.platform', [APP_PLATFORM_SERVER]) + ->label('sdk.method', 'listAttributes') + ->label('sdk.description', '/docs/references/database/list-attributes.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_ATTRIBUTE_LIST) + ->param('collectionId', '', new UID(), 'Collection unique ID. You can create a new collection using the Database service [server integration](/docs/server/database#createCollection).') + ->inject('response') + ->inject('dbForExternal') + ->action(function ($collectionId, $response, $dbForExternal) { + /** @var Appwrite\Utopia\Response $response */ + /** @var Utopia\Database\Database $dbForExternal */ + + $collection = $dbForExternal->getCollection($collectionId); + + if ($collection->isEmpty()) { + throw new Exception('Collection not found', 404); + } + + $attributes = $collection->getAttributes(); + + $attributes = array_map(function ($attribute) use ($collection) { + return new Document([\array_merge($attribute, [ + 'collectionId' => $collection->getId(), + ])]); + }, $attributes); + + $response->dynamic2(new Document([ + 'sum' => \count($attributes), + 'attributes' => $attributes + ]), Response::MODEL_ATTRIBUTE_LIST); + }); + +App::get('v1/database/collections/:collectionId/attributes/:attributeId') + ->desc('Get Attribute') + ->groups(['api', 'database']) + ->label('scope', 'attributes.read') + ->label('sdk.namespace', 'database') + ->label('sdk.platform', [APP_PLATFORM_SERVER]) + ->label('sdk.method', 'listAttributes') + ->label('sdk.description', '/docs/references/database/get-attribute.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_ATTRIBUTE) + ->param('collectionId', '', new UID(), 'Collection unique ID. You can create a new collection using the Database service [server integration](/docs/server/database#createCollection).') + ->param('attributeId', '', new Key(), 'Attribute ID.') + ->inject('response') + ->inject('dbForExternal') + ->action(function ($collectionId, $attributeId, $response, $dbForExternal) { + /** @var Appwrite\Utopia\Response $response */ + /** @var Utopia\Database\Database $dbForExternal */ + + $collection = $dbForExternal->getCollection($collectionId); + + if (empty($collection)) { + throw new Exception('Collection not found', 404); + } + + $attributes = $collection->getAttributes(); + + // Search for attribute + $attributeIndex = array_search($attributeId, array_column($attributes, '$id')); + + if ($attributeIndex === false) { + throw new Exception('Attribute not found', 404); + } + + $attribute = new Document([\array_merge($attributes[$attributeIndex], [ + 'collectionId' => $collectionId, + ])]); + + $response->dynamic2($attribute, Response::MODEL_ATTRIBUTE); + }); + +App::delete('/v1/database/collections/:collectionId/attributes/:attributeId') + ->desc('Delete Attribute') + ->groups(['api', 'database']) + ->label('scope', 'attributes.write') + ->label('event', 'database.attributes.delete') + ->label('sdk.namespace', 'database') + ->label('sdk.platform', [APP_PLATFORM_SERVER]) + ->label('sdk.method', 'deleteAttribute') + ->label('sdk.description', '/docs/references/database/delete-attribute.md') + ->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT) + ->label('sdk.response.model', Response::MODEL_NONE) + ->param('collectionId', '', new UID(), 'Collection unique ID. You can create a new collection using the Database service [server integration](/docs/server/database#createCollection).') + ->param('attributeId', '', new Key(), 'Attribute ID.') + ->inject('response') + ->inject('dbForExternal') + ->inject('database') + ->inject('events') + ->inject('audits') + ->action(function ($collectionId, $attributeId, $response, $dbForExternal, $database, $events, $audits) { + /** @var Appwrite\Utopia\Response $response */ + /** @var Utopia\Database\Database $dbForExternal */ + /** @var Appwrite\Event\Event $database */ + /** @var Appwrite\Event\Event $events */ + /** @var Appwrite\Event\Event $audits */ + + $collection = $dbForExternal->getCollection($collectionId); + + if ($collection->isEmpty()) { + throw new Exception('Collection not found', 404); + } + + $attributes = $collection->getAttributes(); + + // Search for attribute + $attributeIndex = array_search($attributeId, array_column($attributes, '$id')); + + if ($attributeIndex === false) { + throw new Exception('Attribute not found', 404); + } + + $attribute = new Document([\array_merge($attributes[$attributeIndex], [ + 'collectionId' => $collectionId, + ])]); + + $database + ->setParam('type', DELETE_TYPE_ATTRIBUTE) + ->setParam('document', $attribute) + ; + + $events + ->setParam('payload', $response->output2($attribute, Response::MODEL_ATTRIBUTE)) + ; + + $audits + ->setParam('event', 'database.attributes.delete') + ->setParam('resource', 'database/attributes/'.$attribute->getId()) + ->setParam('data', $attribute->getArrayCopy()) + ; + + $response->noContent(); + }); + +App::post('/v1/database/collections/:collectionId/indexes') + ->desc('Create Index') + ->groups(['api', 'database']) + ->label('event', 'database.indexes.create') + ->label('scope', 'indexes.write') + ->label('sdk.namespace', 'database') + ->label('sdk.platform', [APP_PLATFORM_SERVER]) + ->label('sdk.method', 'createIndex') + ->label('sdk.description', '/docs/references/database/create-index.md') + ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_INDEX) + ->param('collectionId', '', new UID(), 'Collection unique ID. You can create a new collection using the Database service [server integration](/docs/server/database#createCollection).') + ->param('id', null, new Key(), 'Index ID.') + ->param('type', null, new WhiteList([Database::INDEX_KEY, Database::INDEX_FULLTEXT, Database::INDEX_UNIQUE, Database::INDEX_SPATIAL, Database::INDEX_ARRAY]), 'Index type.') + ->param('attributes', null, new ArrayList(new Key()), 'Array of attributes to index.') + // TODO@kodumbeats debug below + ->param('orders', [], new ArrayList(new WhiteList(['ASC', 'DESC'], false, Database::VAR_STRING)), 'Array of index orders.', true) + // ->param('orders', [], new ArrayList(new Text(4)), 'Array of index orders.', true) + ->inject('response') + ->inject('dbForExternal') + ->inject('database') + ->inject('audits') + ->action(function ($collectionId, $id, $type, $attributes, $orders, $response, $dbForExternal, $database, $audits) { + /** @var Appwrite\Utopia\Response $response */ + /** @var Utopia\Database\Database $dbForExternal */ + /** @var Appwrite\Event\Event $database */ + /** @var Appwrite\Event\Event $audits */ + + $collection = $dbForExternal->getCollection($collectionId); + + if ($collection->isEmpty()) { + throw new Exception('Collection not found', 404); + } + + // Convert Document[] to array of attribute metadata + $oldAttributes = \array_map(function ($a) { + return $a->getArrayCopy(); + }, $collection->getAttribute('attributes')); + + // lengths hidden by default + $lengths = []; + + // set attribute size as length for strings, null otherwise + foreach ($attributes as $key => $attribute) { + // find attribute metadata in collection document + $attributeIndex = \array_search($attribute, array_column($oldAttributes, '$id')); + + if ($attributeIndex === false) { + throw new Exception('Unknown attribute: ' . $attribute, 400); + } + + $attributeType = $oldAttributes[$attributeIndex]['type']; + $attributeSize = $oldAttributes[$attributeIndex]['size']; + + // Only set length for indexes on strings + $lengths[$key] = ($attributeType === Database::VAR_STRING) ? $attributeSize : null; + } + + $success = $dbForExternal->addIndexInQueue($collectionId, $id, $type, $attributes, $lengths, $orders); + + // Database->createIndex() does not return a document + // So we need to create one for the response + // + // TODO@kodumbeats should $lengths be a part of the response model? + $index = new Document([ + '$collection' => $collectionId, + '$id' => $id, + 'type' => $type, + 'attributes' => $attributes, + 'lengths' => $lengths, + 'orders' => $orders, + ]); + + $database + ->setParam('type', CREATE_TYPE_INDEX) + ->setParam('document', $index) + ; + + $audits + ->setParam('event', 'database.indexes.create') + ->setParam('resource', 'database/indexes/'.$index->getId()) + ->setParam('data', $index->getArrayCopy()) + ; + + $response->setStatusCode(Response::STATUS_CODE_CREATED); + $response->dynamic2($index, Response::MODEL_INDEX); + + }); + +App::get('v1/database/collections/:collectionId/indexes') + ->desc('List Indexes') + ->groups(['api', 'database']) + ->label('scope', 'indexes.read') + ->label('sdk.namespace', 'database') + ->label('sdk.platform', [APP_PLATFORM_SERVER]) + ->label('sdk.method', 'listIndexes') + ->label('sdk.description', '/docs/references/database/list-indexes.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_INDEX_LIST) + ->param('collectionId', '', new UID(), 'Collection unique ID. You can create a new collection using the Database service [server integration](/docs/server/database#createCollection).') + ->inject('response') + ->inject('dbForExternal') + ->action(function ($collectionId, $response, $dbForExternal) { + /** @var Appwrite\Utopia\Response $response */ + /** @var Utopia\Database\Database $dbForExternal */ + + $collection = $dbForExternal->getCollection($collectionId); + + if ($collection->isEmpty()) { + throw new Exception('Collection not found', 404); + } + + $indexes = $collection->getAttribute('indexes'); + + $indexes = array_map(function ($index) use ($collection) { + return new Document([\array_merge($index, [ + 'collectionId' => $collection->getId(), + ])]); + }, $indexes); + + $response->dynamic2(new Document([ + 'sum' => \count($indexes), + 'attributes' => $indexes, + ]), Response::MODEL_INDEX_LIST); + }); + +App::get('v1/database/collections/:collectionId/indexes/:indexId') + ->desc('Get Index') + ->groups(['api', 'database']) + ->label('scope', 'indexes.read') + ->label('sdk.namespace', 'database') + ->label('sdk.platform', [APP_PLATFORM_SERVER]) + ->label('sdk.method', 'listIndexes') + ->label('sdk.description', '/docs/references/database/get-index.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_INDEX) + ->param('collectionId', '', new UID(), 'Collection unique ID. You can create a new collection using the Database service [server integration](/docs/server/database#createCollection).') + ->param('indexId', null, new Key(), 'Index ID.') + ->inject('response') + ->inject('dbForExternal') + ->action(function ($collectionId, $indexId, $response, $dbForExternal) { + /** @var Appwrite\Utopia\Response $response */ + /** @var Utopia\Database\Database $dbForExternal */ + + $collection = $dbForExternal->getCollection($collectionId); + + if ($collection->isEmpty()) { + throw new Exception('Collection not found', 404); + } + + $indexes = $collection->getAttribute('indexes'); + + // // Search for index + $indexIndex = array_search($indexId, array_column($indexes, '$id')); + + if ($indexIndex === false) { + throw new Exception('Index not found', 404); + } + + $index = new Document([\array_merge($indexes[$indexIndex], [ + 'collectionId' => $collectionId, + ])]); + + $response->dynamic2($index, Response::MODEL_INDEX); + }); + +App::delete('/v1/database/collections/:collectionId/indexes/:indexId') + ->desc('Delete Index') + ->groups(['api', 'database']) + ->label('scope', 'indexes.write') + ->label('event', 'database.indexes.delete') + ->label('sdk.namespace', 'database') + ->label('sdk.platform', [APP_PLATFORM_SERVER]) + ->label('sdk.method', 'deleteIndex') + ->label('sdk.description', '/docs/references/database/delete-index.md') + ->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT) + ->label('sdk.response.model', Response::MODEL_NONE) + ->param('collectionId', null, new UID(), 'Collection unique ID. You can create a new collection using the Database service [server integration](/docs/server/database#createCollection).') + ->param('indexId', '', new Key(), 'Index ID.') + ->inject('response') + ->inject('dbForExternal') + ->inject('database') + ->inject('events') + ->inject('audits') + ->action(function ($collectionId, $indexId, $response, $dbForExternal, $database, $events, $audits) { + /** @var Appwrite\Utopia\Response $response */ + /** @var Utopia\Database\Database $dbForExternal */ + /** @var Appwrite\Event\Event $database */ + /** @var Appwrite\Event\Event $events */ + /** @var Appwrite\Event\Event $audits */ + + $collection = $dbForExternal->getCollection($collectionId); + + if ($collection->isEmpty()) { + throw new Exception('Collection not found', 404); + } + + $indexes = $collection->getAttribute('indexes'); + + // // Search for index + $indexIndex = array_search($indexId, array_column($indexes, '$id')); + + if ($indexIndex === false) { + throw new Exception('Index not found', 404); + } + + $index = new Document([\array_merge($indexes[$indexIndex], [ + 'collectionId' => $collectionId, + ])]); + + $database + ->setParam('type', DELETE_TYPE_INDEX) + ->setParam('document', $index) + ; + + $events + ->setParam('payload', $response->output2($index, Response::MODEL_INDEX)) + ; + + $audits + ->setParam('event', 'database.indexes.delete') + ->setParam('resource', 'database/indexes/'.$index->getId()) + ->setParam('data', $index->getArrayCopy()) + ; + + $response->noContent(); + }); + App::post('/v1/database/collections/:collectionId/documents') ->desc('Create Document') ->groups(['api', 'database']) @@ -295,19 +685,16 @@ App::post('/v1/database/collections/:collectionId/documents') ->label('sdk.response.model', Response::MODEL_DOCUMENT) ->param('collectionId', null, new UID(), 'Collection unique ID. You can create a new collection with validation rules using the Database service [server integration](/docs/server/database#createCollection).') ->param('data', [], new JSON(), 'Document data as JSON object.') - ->param('read', null, new ArrayList(new Text(64)), 'An array of strings with read permissions. By default only the current user is granted with read permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true) - ->param('write', null, new ArrayList(new Text(64)), 'An array of strings with write permissions. By default only the current user is granted with write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true) - ->param('parentDocument', '', new UID(), 'Parent document unique ID. Use when you want your new document to be a child of a parent document.', true) - ->param('parentProperty', '', new Key(), 'Parent document property name. Use when you want your new document to be a child of a parent document.', true) - ->param('parentPropertyType', Document::SET_TYPE_ASSIGN, new WhiteList([Document::SET_TYPE_ASSIGN, Document::SET_TYPE_APPEND, Document::SET_TYPE_PREPEND], true), 'Parent document property connection type. You can set this value to **assign**, **append** or **prepend**, default value is assign. Use when you want your new document to be a child of a parent document.', true) + ->param('read', null, new Permissions(), 'An array of strings with read permissions. By default only the current user is granted with read permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true) + ->param('write', null, new Permissions(), 'An array of strings with write permissions. By default only the current user is granted with write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true) ->inject('response') - ->inject('projectDB') + ->inject('dbForExternal') ->inject('user') ->inject('audits') - ->action(function ($collectionId, $data, $read, $write, $parentDocument, $parentProperty, $parentPropertyType, $response, $projectDB, $user, $audits) { + ->action(function ($collectionId, $data, $read, $write, $response, $dbForExternal, $user, $audits) { /** @var Appwrite\Utopia\Response $response */ - /** @var Appwrite\Database\Database $projectDB */ - /** @var Appwrite\Database\Document $user */ + /** @var Utopia\Database\Database $dbForExternal */ + /** @var Utopia\Database\Document $user */ /** @var Appwrite\Event\Event $audits */ $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array @@ -320,85 +707,31 @@ App::post('/v1/database/collections/:collectionId/documents') throw new Exception('$id is not allowed for creating new documents, try update instead', 400); } - $collection = $projectDB->getDocument($collectionId, false); + $collection = $dbForExternal->getCollection($collectionId); - if (\is_null($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { + if ($collection->isEmpty()) { throw new Exception('Collection not found', 404); } - $data['$collection'] = $collectionId; // Adding this param to make API easier for developers - $data['$permissions'] = [ - 'read' => (is_null($read) && !$user->isEmpty()) ? ['user:'.$user->getId()] : $read ?? [], // By default set read permissions for user - 'write' => (is_null($write) && !$user->isEmpty()) ? ['user:'.$user->getId()] : $write ?? [], // By default set write permissions for user - ]; - - // Read parent document + validate not 404 + validate read / write permission like patch method - // Add payload to parent document property - if ((!empty($parentDocument)) && (!empty($parentProperty))) { - $parentDocument = $projectDB->getDocument($parentDocument, false); - - if (empty($parentDocument->getArrayCopy())) { // Check empty - throw new Exception('No parent document found', 404); - } - - /* - * 1. Check child has valid structure, - * 2. Check user have write permission for parent document - * 3. Assign parent data (including child) to $data - * 4. Validate the combined result has valid structure (inside $projectDB->createDocument method) - */ - - $new = new Document($data); - - $structure = new Structure($projectDB); - - if (!$structure->isValid($new)) { - throw new Exception('Invalid data structure: '.$structure->getDescription(), 400); - } - - $authorization = new Authorization($parentDocument, 'write'); - - if (!$authorization->isValid($new->getPermissions())) { - throw new Exception('Unauthorized permissions', 401); - } - - $parentDocument - ->setAttribute($parentProperty, $data, $parentPropertyType); - - $data = $parentDocument->getArrayCopy(); - $collection = $projectDB->getDocument($parentDocument->getCollection(), false); - } - - /** - * Set default collection values - */ - foreach ($collection->getAttribute('rules') as $key => $rule) { - $key = $rule['key'] ?? ''; - $default = $rule['default'] ?? null; - - if (!isset($data[$key])) { - $data[$key] = $default; - } - } + $data['$collection'] = $collection->getId(); // Adding this param to make API easier for developers + $data['$id'] = $dbForExternal->getId(); + $data['$read'] = (is_null($read) && !$user->isEmpty()) ? ['user:'.$user->getId()] : $read ?? []; // By default set read permissions for user + $data['$write'] = (is_null($write) && !$user->isEmpty()) ? ['user:'.$user->getId()] : $write ?? []; // By default set write permissions for user try { - $data = $projectDB->createDocument($data); - } catch (AuthorizationException $exception) { - throw new Exception('Unauthorized permissions', 401); + $document = $dbForExternal->createDocument($collectionId, new Document($data)); } catch (StructureException $exception) { - throw new Exception('Bad structure. '.$exception->getMessage(), 400); - } catch (\Exception $exception) { - throw new Exception('Failed saving document to DB'.$exception->getMessage(), 500); + throw new Exception($exception->getMessage(), 400); } $audits ->setParam('event', 'database.documents.create') - ->setParam('resource', 'database/document/'.$data['$id']) - ->setParam('data', $data->getArrayCopy()) + ->setParam('resource', 'database/document/'.$document->getId()) + ->setParam('data', $document->getArrayCopy()) ; $response->setStatusCode(Response::STATUS_CODE_CREATED); - $response->dynamic($data, Response::MODEL_DOCUMENT); + $response->dynamic2($document, Response::MODEL_DOCUMENT); }); App::get('/v1/database/collections/:collectionId/documents') @@ -406,62 +739,52 @@ App::get('/v1/database/collections/:collectionId/documents') ->groups(['api', 'database']) ->label('scope', 'documents.read') ->label('sdk.namespace', 'database') - ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) - ->label('sdk.method', 'listDocuments') + ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) ->label('sdk.method', 'listDocuments') ->label('sdk.description', '/docs/references/database/list-documents.md') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_DOCUMENT_LIST) - ->param('collectionId', null, new UID(), 'Collection unique ID. You can create a new collection with validation rules using the Database service [server integration](/docs/server/database#createCollection).') - ->param('filters', [], new ArrayList(new Text(128)), 'Array of filter strings. Each filter is constructed from a key name, comparison operator (=, !=, >, <, <=, >=) and a value. You can also use a dot (.) separator in attribute names to filter by child document attributes. Examples: \'name=John Doe\' or \'category.$id>=5bed2d152c362\'.', true) + ->param('collectionId', '', new UID(), 'Collection unique ID. You can create a new collection using the Database service [server integration](/docs/server/database#createCollection).') + ->param('queries', [], new ArrayList(new Text(128)), 'Array of query strings.', true) ->param('limit', 25, new Range(0, 100), 'Maximum number of documents to return in response. Use this value to manage pagination. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true) ->param('offset', 0, new Range(0, 900000000), 'Offset value. The default value is 0. Use this param to manage pagination.', true) - ->param('orderField', '', new Text(128), 'Document field that results will be sorted by.', true) - ->param('orderType', 'ASC', new WhiteList(['DESC', 'ASC'], true), 'Order direction. Possible values are DESC for descending order, or ASC for ascending order.', true) - ->param('orderCast', 'string', new WhiteList(['int', 'string', 'date', 'time', 'datetime'], true), 'Order field type casting. Possible values are int, string, date, time or datetime. The database will attempt to cast the order field to the value you pass here. The default value is a string.', true) - ->param('search', '', new Text(256), 'Search query. Enter any free text search. The database will try to find a match against all document attributes and children. Max length: 256 chars.', true) + // TODO@kodumbeats 'after' param for pagination + ->param('orderAttributes', [], new ArrayList(new Text(128)), 'Array of attributes used to sort results.', true) + ->param('orderTypes', [], new ArrayList(new WhiteList(['DESC', 'ASC'], true)), 'Array of order directions for sorting attribtues. Possible values are DESC for descending order, or ASC for ascending order.', true) ->inject('response') - ->inject('projectDB') - ->action(function ($collectionId, $filters, $limit, $offset, $orderField, $orderType, $orderCast, $search, $response, $projectDB) { + ->inject('dbForExternal') + ->action(function ($collectionId, $queries, $limit, $offset, $orderAttributes, $orderTypes, $response, $dbForExternal) { /** @var Appwrite\Utopia\Response $response */ - /** @var Appwrite\Database\Database $projectDB */ + /** @var Utopia\Database\Database $dbForExternal */ - $collection = $projectDB->getDocument($collectionId, false); + $collection = $dbForExternal->getCollection($collectionId); - if (\is_null($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { + if ($collection->isEmpty()) { throw new Exception('Collection not found', 404); } - $list = $projectDB->getCollection([ - 'limit' => $limit, - 'offset' => $offset, - 'orderField' => $orderField, - 'orderType' => $orderType, - 'orderCast' => $orderCast, - 'search' => $search, - 'filters' => \array_merge($filters, [ - '$collection='.$collectionId, - ]), - ]); + $queries = \array_map(function ($query) { + return Query::parse($query); + }, $queries); - // if (App::isDevelopment()) { - // $collection - // ->setAttribute('debug', $projectDB->getDebug()) - // ->setAttribute('limit', $limit) - // ->setAttribute('offset', $offset) - // ->setAttribute('orderField', $orderField) - // ->setAttribute('orderType', $orderType) - // ->setAttribute('orderCast', $orderCast) - // ->setAttribute('filters', $filters) - // ; - // } + // TODO@kodumbeats find a more efficient alternative to this + $schema = $collection->getArrayCopy()['attributes']; + $indexes = $collection->getArrayCopy()['indexes']; + $indexesInQueue = $collection->getArrayCopy()['indexesInQueue']; - $collection - ->setAttribute('sum', $projectDB->getSum()) - ->setAttribute('documents', $list) - ; + // TODO@kodumbeats use strict query validation + $validator = new QueriesValidator(new QueryValidator($schema), $indexes, $indexesInQueue, false); - $response->dynamic($collection, Response::MODEL_DOCUMENT_LIST); + if (!$validator->isValid($queries)) { + throw new Exception($validator->getDescription(), 400); + } + + $documents = $dbForExternal->find($collectionId, $queries, $limit, $offset, $orderAttributes, $orderTypes); + + $response->dynamic2(new Document([ + 'sum' => \count($documents), + 'documents' => $documents, + ]), Response::MODEL_DOCUMENT_LIST); }); App::get('/v1/database/collections/:collectionId/documents/:documentId') @@ -475,22 +798,27 @@ App::get('/v1/database/collections/:collectionId/documents/:documentId') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_DOCUMENT) - ->param('collectionId', null, new UID(), 'Collection unique ID. You can create a new collection with validation rules using the Database service [server integration](/docs/server/database#createCollection).') + ->param('collectionId', null, new UID(), 'Collection unique ID. You can create a new collection using the Database service [server integration](/docs/server/database#createCollection).') ->param('documentId', null, new UID(), 'Document unique ID.') ->inject('response') - ->inject('projectDB') - ->action(function ($collectionId, $documentId, $response, $projectDB) { + ->inject('dbForExternal') + ->action(function ($collectionId, $documentId, $response, $dbForExternal) { /** @var Appwrite\Utopia\Response $response */ - /** @var Appwrite\Database\Database $projectDB */ + /** @var Utopia\Database\Database $dbForExternal */ - $document = $projectDB->getDocument($documentId, false); - $collection = $projectDB->getDocument($collectionId, false); + $collection = $dbForExternal->getCollection($collectionId); - if (empty($document->getArrayCopy()) || $document->getCollection() != $collection->getId()) { // Check empty + if ($collection->isEmpty()) { + throw new Exception('Collection not found', 404); + } + + $document = $dbForExternal->getDocument($collectionId, $documentId); + + if ($document->isEmpty()) { throw new Exception('No document found', 404); } - $response->dynamic($document, Response::MODEL_DOCUMENT); + $response->dynamic2($document, Response::MODEL_DOCUMENT); }); App::patch('/v1/database/collections/:collectionId/documents/:documentId') @@ -508,61 +836,60 @@ App::patch('/v1/database/collections/:collectionId/documents/:documentId') ->param('collectionId', null, new UID(), 'Collection unique ID. You can create a new collection with validation rules using the Database service [server integration](/docs/server/database#createCollection).') ->param('documentId', null, new UID(), 'Document unique ID.') ->param('data', [], new JSON(), 'Document data as JSON object.') - ->param('read', null, new ArrayList(new Text(64)), 'An array of strings with read permissions. By default inherits the existing read permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true) - ->param('write', null, new ArrayList(new Text(64)), 'An array of strings with write permissions. By default inherits the existing write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true) + ->param('read', null, new Permissions(), 'An array of strings with read permissions. By default inherits the existing read permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true) + ->param('write', null, new Permissions(), 'An array of strings with write permissions. By default inherits the existing write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true) ->inject('response') - ->inject('projectDB') + ->inject('dbForExternal') ->inject('audits') - ->action(function ($collectionId, $documentId, $data, $read, $write, $response, $projectDB, $audits) { + ->action(function ($collectionId, $documentId, $data, $read, $write, $response, $dbForExternal, $audits) { /** @var Appwrite\Utopia\Response $response */ - /** @var Appwrite\Database\Database $projectDB */ + /** @var Utopia\Database\Database $dbForExternal */ /** @var Appwrite\Event\Event $audits */ - $collection = $projectDB->getDocument($collectionId, false); - $document = $projectDB->getDocument($documentId, false); + $collection = $dbForExternal->getCollection($collectionId); - $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array - - if (!\is_array($data)) { - throw new Exception('Data param should be a valid JSON object', 400); - } - - if (\is_null($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { + if ($collection->isEmpty()) { throw new Exception('Collection not found', 404); } - if (empty($document->getArrayCopy()) || $document->getCollection() != $collectionId) { // Check empty - throw new Exception('No document found', 404); + $document = $dbForExternal->getDocument($collectionId, $documentId); + + if ($document->isEmpty()) { + throw new Exception('Document not found', 404); + } + + $data = (\is_string($data)) ? \json_decode($data, true) : $data; // Cast to JSON array + + if (empty($data)) { + throw new Exception('Missing payload', 400); + } + + if (!\is_array($data)) { + throw new Exception('Data param should be a valid JSON object', 400); } $data = \array_merge($document->getArrayCopy(), $data); $data['$collection'] = $collection->getId(); // Make sure user don't switch collectionID $data['$id'] = $document->getId(); // Make sure user don't switch document unique ID - $data['$permissions']['read'] = (is_null($read)) ? ($document->getPermissions()['read'] ?? []) : $read; // By default inherit read permissions - $data['$permissions']['write'] = (is_null($write)) ? ($document->getPermissions()['write'] ?? []) : $write; // By default inherit write permissions - - if (empty($data)) { - throw new Exception('Missing payload', 400); - } + $data['$read'] = (is_null($read)) ? ($document->getRead() ?? []) : $read; // By default inherit read permissions + $data['$write'] = (is_null($write)) ? ($document->getWrite() ?? []) : $write; // By default inherit write permissions try { - $data = $projectDB->updateDocument($data); + $document = $dbForExternal->updateDocument($collection->getId(), $document->getId(), new Document($data)); } catch (AuthorizationException $exception) { throw new Exception('Unauthorized permissions', 401); } catch (StructureException $exception) { throw new Exception('Bad structure. '.$exception->getMessage(), 400); - } catch (\Exception $exception) { - throw new Exception('Failed saving document to DB', 500); - } + } $audits ->setParam('event', 'database.documents.update') - ->setParam('resource', 'database/document/'.$data->getId()) - ->setParam('data', $data->getArrayCopy()) + ->setParam('resource', 'database/document/'.$document->getId()) + ->setParam('data', $document->getArrayCopy()) ; - $response->dynamic($data, Response::MODEL_DOCUMENT); + $response->dynamic2($document, Response::MODEL_DOCUMENT); }); App::delete('/v1/database/collections/:collectionId/documents/:documentId') @@ -576,41 +903,34 @@ App::delete('/v1/database/collections/:collectionId/documents/:documentId') ->label('sdk.description', '/docs/references/database/delete-document.md') ->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT) ->label('sdk.response.model', Response::MODEL_NONE) - ->param('collectionId', null, new UID(), 'Collection unique ID. You can create a new collection with validation rules using the Database service [server integration](/docs/server/database#createCollection).') + ->param('collectionId', null, new UID(), 'Collection unique ID. You can create a new collection using the Database service [server integration](/docs/server/database#createCollection).') ->param('documentId', null, new UID(), 'Document unique ID.') ->inject('response') - ->inject('projectDB') + ->inject('dbForExternal') ->inject('events') ->inject('audits') - ->action(function ($collectionId, $documentId, $response, $projectDB, $events, $audits) { + ->action(function ($collectionId, $documentId, $response, $dbForExternal, $events, $audits) { /** @var Appwrite\Utopia\Response $response */ - /** @var Appwrite\Database\Database $projectDB */ + /** @var Utopia\Database\Database $dbForExternal */ /** @var Appwrite\Event\Event $events */ /** @var Appwrite\Event\Event $audits */ - $collection = $projectDB->getDocument($collectionId, false); - $document = $projectDB->getDocument($documentId, false); + $collection = $dbForExternal->getCollection($collectionId); - if (empty($document->getArrayCopy()) || $document->getCollection() != $collectionId) { // Check empty - throw new Exception('No document found', 404); - } - - if (\is_null($collection->getId()) || Database::SYSTEM_COLLECTION_COLLECTIONS != $collection->getCollection()) { + if ($collection->isEmpty()) { throw new Exception('Collection not found', 404); } - try { - $projectDB->deleteDocument($documentId); - } catch (AuthorizationException $exception) { - throw new Exception('Unauthorized permissions', 401); - } catch (StructureException $exception) { - throw new Exception('Bad structure. '.$exception->getMessage(), 400); - } catch (\Exception $exception) { - throw new Exception('Failed to remove document from DB', 500); + $document = $dbForExternal->getDocument($collectionId, $documentId); + + if ($document->isEmpty()) { + throw new Exception('No document found', 404); } + $success = $dbForExternal->deleteDocument($collectionId, $documentId); + $events - ->setParam('eventData', $response->output($document, Response::MODEL_DOCUMENT)) + ->setParam('eventData', $response->output2($document, Response::MODEL_DOCUMENT)) ; $audits @@ -620,4 +940,4 @@ App::delete('/v1/database/collections/:collectionId/documents/:documentId') ; $response->noContent(); - }); \ No newline at end of file + }); diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index 7fc91ab1d..28694a0b1 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -1006,6 +1006,7 @@ App::post('/v1/projects/:projectId/tasks') $task = new Document([ '$id' => $dbForConsole->getId(), + 'projectId' => $project->getId(), 'name' => $name, 'status' => $status, 'schedule' => $schedule, diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index 556efebe3..8bad1bf49 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -224,8 +224,7 @@ App::get('/v1/users/:userId/logs') /** @var Utopia\Database\Database $dbForInternal */ /** @var Utopia\Locale\Locale $locale */ /** @var MaxMind\Db\Reader $geodb */ - /** @var Utopia\App $app */ - + $user = $dbForInternal->getDocument('users', $userId); if ($user->isEmpty()) { diff --git a/app/controllers/general.php b/app/controllers/general.php index e8392cde4..cfe06effb 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -16,17 +16,19 @@ use Appwrite\Utopia\Response\Filters\V06; use Appwrite\Utopia\Response\Filters\V07; use Appwrite\Utopia\Response\Filters\V08; use Utopia\CLI\Console; +use Utopia\Database\Database; use Utopia\Database\Document; +use Utopia\Database\Query; use Utopia\Database\Validator\Authorization as Authorization2; Config::setParam('domainVerification', false); Config::setParam('cookieDomain', 'localhost'); Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE); -App::init(function ($utopia, $request, $response, $console, $project, $consoleDB, $user, $locale, $clients) { +App::init(function ($utopia, $request, $response, $console, $project, $dbForConsole, $user, $locale, $clients) { /** @var Utopia\Swoole\Request $request */ /** @var Appwrite\Utopia\Response $response */ - /** @var Appwrite\Database\Database $consoleDB */ + /** @var Utopia\Database\Database $dbForConsole */ /** @var Utopia\Database\Document $console */ /** @var Utopia\Database\Document $project */ /** @var Utopia\Database\Document $user */ @@ -42,36 +44,29 @@ App::init(function ($utopia, $request, $response, $console, $project, $consoleDB $domains[$domain->get()] = false; Console::warning($domain->get() . ' is not a publicly accessible domain. Skipping SSL certificate generation.'); } else { - Authorization::disable(); - $dbDomain = $consoleDB->getCollectionFirst([ - 'limit' => 1, - 'offset' => 0, - 'filters' => [ - '$collection=' . Database::SYSTEM_COLLECTION_CERTIFICATES, - 'domain=' . $domain->get(), - ], - ]); + Authorization2::disable(); - if (empty($dbDomain)) { - $dbDomain = [ - '$collection' => Database::SYSTEM_COLLECTION_CERTIFICATES, - '$permissions' => [ - 'read' => [], - 'write' => [], - ], + $certificate = $dbForConsole->findFirst('certificates', [ + new Query('domain', QUERY::TYPE_EQUAL, [$domain->get()]) + ], /*limit*/ 1); + + if (empty($certificate)) { + $certificate = new Document([ 'domain' => $domain->get(), - ]; - $dbDomain = $consoleDB->createDocument($dbDomain); - Authorization::enable(); + ]); + $certificate = $dbForConsole->createDocument('certificates', $certificate); + Authorization2::enable(); Console::info('Issuing a TLS certificate for the master domain (' . $domain->get() . ') in a few seconds...'); // TODO move this to installation script Resque::enqueue('v1-certificates', 'CertificatesV1', [ - 'document' => $dbDomain, + 'document' => $certificate, 'domain' => $domain->get(), 'validateTarget' => false, 'validateCNAME' => false, ]); + } else { + Authorization2::enable(); // ensure authorization is reenabled } $domains[$domain->get()] = true; } @@ -286,7 +281,7 @@ App::init(function ($utopia, $request, $response, $console, $project, $consoleDB throw new Exception('Password reset is required', 412); } -}, ['utopia', 'request', 'response', 'console', 'project', 'consoleDB', 'user', 'locale', 'clients']); +}, ['utopia', 'request', 'response', 'console', 'project', 'dbForConsole', 'user', 'locale', 'clients']); App::options(function ($request, $response) { /** @var Utopia\Swoole\Request $request */ diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 0ea28fbf9..24fed9661 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -9,7 +9,7 @@ use Utopia\Abuse\Adapters\TimeLimit; use Utopia\Storage\Device\Local; use Utopia\Storage\Storage; -App::init(function ($utopia, $request, $response, $project, $user, $register, $events, $audits, $usage, $deletes, $dbForInternal) { +App::init(function ($utopia, $request, $response, $project, $user, $register, $events, $audits, $usage, $deletes, $database, $dbForInternal) { /** @var Utopia\App $utopia */ /** @var Utopia\Swoole\Request $request */ /** @var Appwrite\Utopia\Response $response */ @@ -20,6 +20,7 @@ App::init(function ($utopia, $request, $response, $project, $user, $register, $e /** @var Appwrite\Event\Event $audits */ /** @var Appwrite\Event\Event $usage */ /** @var Appwrite\Event\Event $deletes */ + /** @var Appwrite\Event\Event $database */ /** @var Appwrite\Event\Event $functions */ /** @var Utopia\Database\Database $dbForInternal */ @@ -36,7 +37,6 @@ App::init(function ($utopia, $request, $response, $project, $user, $register, $e * Abuse Check */ $timeLimit = new TimeLimit($route->getLabel('abuse-key', 'url:{url},ip:{ip}'), $route->getLabel('abuse-limit', 0), $route->getLabel('abuse-time', 3600), $dbForInternal); - $timeLimit ->setParam('{userId}', $user->getId()) ->setParam('{userAgent}', $request->getUserAgent('')) @@ -110,7 +110,11 @@ App::init(function ($utopia, $request, $response, $project, $user, $register, $e ->setParam('projectId', $project->getId()) ; -}, ['utopia', 'request', 'response', 'project', 'user', 'register', 'events', 'audits', 'usage', 'deletes', 'dbForInternal'], 'api'); + $database + ->setParam('projectId', $project->getId()) + ; +}, ['utopia', 'request', 'response', 'project', 'user', 'register', 'events', 'audits', 'usage', 'deletes', 'database', 'dbForInternal'], 'api'); + App::init(function ($utopia, $request, $response, $project, $user) { /** @var Utopia\App $utopia */ @@ -166,7 +170,7 @@ App::init(function ($utopia, $request, $response, $project, $user) { }, ['utopia', 'request', 'response', 'project', 'user'], 'auth'); -App::shutdown(function ($utopia, $request, $response, $project, $events, $audits, $usage, $deletes, $mode) { +App::shutdown(function ($utopia, $request, $response, $project, $events, $audits, $usage, $deletes, $database, $mode) { /** @var Utopia\App $utopia */ /** @var Utopia\Swoole\Request $request */ /** @var Appwrite\Utopia\Response $response */ @@ -175,6 +179,7 @@ App::shutdown(function ($utopia, $request, $response, $project, $events, $audits /** @var Appwrite\Event\Event $audits */ /** @var Appwrite\Event\Event $usage */ /** @var Appwrite\Event\Event $deletes */ + /** @var Appwrite\Event\Event $database */ /** @var Appwrite\Event\Event $functions */ /** @var bool $mode */ @@ -204,6 +209,10 @@ App::shutdown(function ($utopia, $request, $response, $project, $events, $audits if (!empty($deletes->getParam('type')) && !empty($deletes->getParam('document'))) { $deletes->trigger(); } + + if (!empty($database->getParam('type')) && !empty($database->getParam('document'))) { + $database->trigger(); + } $route = $utopia->match($request); if (App::getEnv('_APP_USAGE_STATS', 'enabled') == 'enabled' @@ -218,4 +227,4 @@ App::shutdown(function ($utopia, $request, $response, $project, $events, $audits ; } -}, ['utopia', 'request', 'response', 'project', 'events', 'audits', 'usage', 'deletes', 'mode'], 'api'); +}, ['utopia', 'request', 'response', 'project', 'events', 'audits', 'usage', 'deletes', 'database', 'mode'], 'api'); diff --git a/app/http.php b/app/http.php index abbf311ec..ee8a073b0 100644 --- a/app/http.php +++ b/app/http.php @@ -54,14 +54,6 @@ $http->on('start', function (Server $http) use ($payloadSize, $register) { $app = new App('UTC'); go(function() use ($register, $app) { - // Only retry connection once before throwing exception - try { - $db = $register->get('dbPool')->get(); - } catch (\Exception $exception) { - Console::warning('[Setup] - Database not ready. Waiting for five seconds...'); - sleep(5); - } - $db = $register->get('dbPool')->get(); $redis = $register->get('redisPool')->get(); @@ -77,6 +69,9 @@ $http->on('start', function (Server $http) use ($payloadSize, $register) { return $app; }); + // wait for database to be ready + sleep(5); + $dbForConsole = $app->getResource('dbForConsole'); /** @var Utopia\Database\Database $dbForConsole */ if(!$dbForConsole->exists()) { diff --git a/app/init.php b/app/init.php index fbb87aa95..99116b748 100644 --- a/app/init.php +++ b/app/init.php @@ -72,7 +72,12 @@ const APP_SOCIAL_DISCORD = 'https://appwrite.io/discord'; const APP_SOCIAL_DISCORD_CHANNEL = '564160730845151244'; const APP_SOCIAL_DEV = 'https://dev.to/appwrite'; const APP_SOCIAL_STACKSHARE = 'https://stackshare.io/appwrite'; +// Creation Types +const CREATE_TYPE_ATTRIBUTE = 'newAttribute'; +const CREATE_TYPE_INDEX = 'newIndex'; // Deletion Types +const DELETE_TYPE_ATTRIBUTE = 'attribute'; +const DELETE_TYPE_INDEX = 'index'; const DELETE_TYPE_DOCUMENT = 'document'; const DELETE_TYPE_EXECUTIONS = 'executions'; const DELETE_TYPE_AUDIT = 'audit'; @@ -386,6 +391,10 @@ App::setResource('deletes', function($register) { return new Event(Event::DELETE_QUEUE_NAME, Event::DELETE_CLASS_NAME); }, ['register']); +App::setResource('database', function($register) { + return new Event(Event::DATABASE_QUEUE_NAME, Event::DATABASE_CLASS_NAME); +}, ['register']); + // Test Mock App::setResource('clients', function($request, $console, $project) { $console->setAttribute('platforms', [ // Allways allow current host diff --git a/app/workers/certificates.php b/app/workers/certificates.php index bc746775c..1db2a4636 100644 --- a/app/workers/certificates.php +++ b/app/workers/certificates.php @@ -1,14 +1,16 @@ get('db'); - $cache = $register->get('cache'); + go(function() use ($register) { + $db = $register->get('dbPool')->get(); + $redis = $register->get('redisPool')->get(); - $consoleDB = new Database(); - $consoleDB->setAdapter(new RedisAdapter(new MySQLAdapter($db, $cache), $cache)); - $consoleDB->setNamespace('app_console'); // Main DB - $consoleDB->setMocks(Config::getParam('collections', [])); + $cache = new Cache(new Redis($redis)); + $dbForConsole = new Database(new MariaDB($db), $cache); + $dbForConsole->setNamespace('project_console_internal'); - /** - * 1. Get new domain document - DONE - * 1.1. Validate domain is valid, public suffix is known and CNAME records are verified - DONE - * 2. Check if a certificate already exists - DONE - * 3. Check if certificate is about to expire, if not - skip it - * 3.1. Create / renew certificate - * 3.2. Update loadblancer - * 3.3. Update database (domains, change date, expiry) - * 3.4. Set retry on failure - * 3.5. Schedule to renew certificate in 60 days - */ + /** + * 1. Get new domain document - DONE + * 1.1. Validate domain is valid, public suffix is known and CNAME records are verified - DONE + * 2. Check if a certificate already exists - DONE + * 3. Check if certificate is about to expire, if not - skip it + * 3.1. Create / renew certificate + * 3.2. Update loadblancer + * 3.3. Update database (domains, change date, expiry) + * 3.4. Set retry on failure + * 3.5. Schedule to renew certificate in 60 days + */ - Authorization::disable(); + Authorization::disable(); - // Args - $document = $this->args['document']; - $domain = $this->args['domain']; + // Args + $document = $this->args['document']; + $domain = $this->args['domain']; - // Validation Args - $validateTarget = $this->args['validateTarget'] ?? true; - $validateCNAME = $this->args['validateCNAME'] ?? true; + // Validation Args + $validateTarget = $this->args['validateTarget'] ?? true; + $validateCNAME = $this->args['validateCNAME'] ?? true; + + // Options + $domain = new Domain((!empty($domain)) ? $domain : ''); + $expiry = 60 * 60 * 24 * 30 * 2; // 60 days + $safety = 60 * 60; // 1 hour + $renew = (\time() + $expiry); + + if(empty($domain->get())) { + throw new Exception('Missing domain'); + } + + if(!$domain->isKnown() || $domain->isTest()) { + throw new Exception('Unknown public suffix for domain'); + } + + if($validateTarget) { + $target = new Domain(App::getEnv('_APP_DOMAIN_TARGET', '')); - // Options - $domain = new Domain((!empty($domain)) ? $domain : ''); - $expiry = 60 * 60 * 24 * 30 * 2; // 60 days - $safety = 60 * 60; // 1 hour - $renew = (\time() + $expiry); - - if(empty($domain->get())) { - throw new Exception('Missing domain'); - } - - if(!$domain->isKnown() || $domain->isTest()) { - throw new Exception('Unknown public suffix for domain'); - } - - if($validateTarget) { - $target = new Domain(App::getEnv('_APP_DOMAIN_TARGET', '')); - - if(!$target->isKnown() || $target->isTest()) { - throw new Exception('Unreachable CNAME target ('.$target->get().'), please use a domain with a public suffix.'); + if(!$target->isKnown() || $target->isTest()) { + throw new Exception('Unreachable CNAME target ('.$target->get().'), please use a domain with a public suffix.'); + } } - } - if($validateCNAME) { - $validator = new CNAME($target->get()); // Verify Domain with DNS records - - if(!$validator->isValid($domain->get())) { - throw new Exception('Failed to verify domain DNS records'); - } - } - - $certificate = $consoleDB->getCollectionFirst([ - 'limit' => 1, - 'offset' => 0, - 'filters' => [ - '$collection='.Database::SYSTEM_COLLECTION_CERTIFICATES, - 'domain='.$domain->get(), - ], - ]); - - // $condition = ($certificate - // && $certificate instanceof Document - // && isset($certificate['issueDate']) - // && (($certificate['issueDate'] + ($expiry)) > time())) ? 'true' : 'false'; - - // throw new Exception('cert issued at'.date('d.m.Y H:i', $certificate['issueDate']).' | renew date is: '.date('d.m.Y H:i', ($certificate['issueDate'] + ($expiry))).' | condition is '.$condition); - - $certificate = (!empty($certificate) && $certificate instanceof $certificate) ? $certificate->getArrayCopy() : []; - - if(!empty($certificate) - && isset($certificate['issueDate']) - && (($certificate['issueDate'] + ($expiry)) > \time())) { // Check last issue time - throw new Exception('Renew isn\'t required'); - } - - $staging = (App::isProduction()) ? '' : ' --dry-run'; - $email = App::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS'); - - if(empty($email)) { - throw new Exception('You must set a valid security email address (_APP_SYSTEM_SECURITY_EMAIL_ADDRESS) to issue an SSL certificate'); - } - - $stdout = ''; - $stderr = ''; - - $exit = Console::execute("certbot certonly --webroot --noninteractive --agree-tos{$staging}" - ." --email ".$email - ." -w ".APP_STORAGE_CERTIFICATES - ." -d {$domain->get()}", '', $stdout, $stderr); - - if($exit !== 0) { - throw new Exception('Failed to issue a certificate with message: '.$stderr); - } - - $path = APP_STORAGE_CERTIFICATES.'/'.$domain->get(); - - if(!\is_readable($path)) { - if (!\mkdir($path, 0755, true)) { - throw new Exception('Failed to create path...'); - } - } + if($validateCNAME) { + $validator = new CNAME($target->get()); // Verify Domain with DNS records - if(!@\rename('/etc/letsencrypt/live/'.$domain->get().'/cert.pem', APP_STORAGE_CERTIFICATES.'/'.$domain->get().'/cert.pem')) { - throw new Exception('Failed to rename certificate cert.pem: '.\json_encode($stdout)); - } - - if(!@\rename('/etc/letsencrypt/live/'.$domain->get().'/chain.pem', APP_STORAGE_CERTIFICATES.'/'.$domain->get().'/chain.pem')) { - throw new Exception('Failed to rename certificate chain.pem: '.\json_encode($stdout)); - } - - if(!@\rename('/etc/letsencrypt/live/'.$domain->get().'/fullchain.pem', APP_STORAGE_CERTIFICATES.'/'.$domain->get().'/fullchain.pem')) { - throw new Exception('Failed to rename certificate fullchain.pem: '.\json_encode($stdout)); - } - - if(!@\rename('/etc/letsencrypt/live/'.$domain->get().'/privkey.pem', APP_STORAGE_CERTIFICATES.'/'.$domain->get().'/privkey.pem')) { - throw new Exception('Failed to rename certificate privkey.pem: '.\json_encode($stdout)); - } - - $certificate = \array_merge($certificate, [ - '$collection' => Database::SYSTEM_COLLECTION_CERTIFICATES, - '$permissions' => [ - 'read' => [], - 'write' => [], - ], - 'domain' => $domain->get(), - 'issueDate' => \time(), - 'renewDate' => $renew, - 'attempts' => 0, - 'log' => \json_encode($stdout), - ]); - - $certificate = $consoleDB->createDocument($certificate); - - if(!$certificate) { - throw new Exception('Failed saving certificate to DB'); - } - - if(!empty($document)) { - $document = \array_merge($document, [ - 'updated' => \time(), - 'certificateId' => $certificate->getId(), - ]); - - $document = $consoleDB->updateDocument($document); - - if(!$document) { - throw new Exception('Failed saving domain to DB'); + if(!$validator->isValid($domain->get())) { + throw new Exception('Failed to verify domain DNS records'); + } } - } + + $certificate = $dbForConsole->findFirst('certificates', [ + new Query('domain', QUERY::TYPE_EQUAL, [$domain->get()]) + ], /*limit*/ 1); + + // $condition = ($certificate + // && $certificate instanceof Document + // && isset($certificate['issueDate']) + // && (($certificate['issueDate'] + ($expiry)) > time())) ? 'true' : 'false'; + + // throw new Exception('cert issued at'.date('d.m.Y H:i', $certificate['issueDate']).' | renew date is: '.date('d.m.Y H:i', ($certificate['issueDate'] + ($expiry))).' | condition is '.$condition); + + $certificate = (!empty($certificate) && $certificate instanceof $certificate) ? $certificate->getArrayCopy() : []; + + if(!empty($certificate) + && isset($certificate['issueDate']) + && (($certificate['issueDate'] + ($expiry)) > \time())) { // Check last issue time + throw new Exception('Renew isn\'t required'); + } + + $staging = (App::isProduction()) ? '' : ' --dry-run'; + $email = App::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS'); + + if(empty($email)) { + throw new Exception('You must set a valid security email address (_APP_SYSTEM_SECURITY_EMAIL_ADDRESS) to issue an SSL certificate'); + } + + $stdout = ''; + $stderr = ''; + + $exit = Console::execute("certbot certonly --webroot --noninteractive --agree-tos{$staging}" + ." --email ".$email + ." -w ".APP_STORAGE_CERTIFICATES + ." -d {$domain->get()}", '', $stdout, $stderr); + + if($exit !== 0) { + throw new Exception('Failed to issue a certificate with message: '.$stderr); + } + + $path = APP_STORAGE_CERTIFICATES.'/'.$domain->get(); + + if(!\is_readable($path)) { + if (!\mkdir($path, 0755, true)) { + throw new Exception('Failed to create path...'); + } + } + + if(!@\rename('/etc/letsencrypt/live/'.$domain->get().'/cert.pem', APP_STORAGE_CERTIFICATES.'/'.$domain->get().'/cert.pem')) { + throw new Exception('Failed to rename certificate cert.pem: '.\json_encode($stdout)); + } + + if(!@\rename('/etc/letsencrypt/live/'.$domain->get().'/chain.pem', APP_STORAGE_CERTIFICATES.'/'.$domain->get().'/chain.pem')) { + throw new Exception('Failed to rename certificate chain.pem: '.\json_encode($stdout)); + } + + if(!@\rename('/etc/letsencrypt/live/'.$domain->get().'/fullchain.pem', APP_STORAGE_CERTIFICATES.'/'.$domain->get().'/fullchain.pem')) { + throw new Exception('Failed to rename certificate fullchain.pem: '.\json_encode($stdout)); + } + + if(!@\rename('/etc/letsencrypt/live/'.$domain->get().'/privkey.pem', APP_STORAGE_CERTIFICATES.'/'.$domain->get().'/privkey.pem')) { + throw new Exception('Failed to rename certificate privkey.pem: '.\json_encode($stdout)); + } + + $certificate = new Document(\array_merge($certificate, [ + 'domain' => $domain->get(), + 'issueDate' => \time(), + 'renewDate' => $renew, + 'attempts' => 0, + 'log' => \json_encode($stdout), + ])); + + $certificate = $dbForConsole->createDocument('certificates', $certificate); + + if(!$certificate) { + throw new Exception('Failed saving certificate to DB'); + } + + if(!empty($document)) { + $certificate = new Document(\array_merge($document, [ + 'updated' => \time(), + 'certificateId' => $certificate->getId(), + ])); - $config = -"tls: - certificates: - - certFile: /storage/certificates/{$domain->get()}/fullchain.pem - keyFile: /storage/certificates/{$domain->get()}/privkey.pem"; + $certificate = $dbForConsole->updateDocument('certificates', $certificate->getId(), $certificate); + + if(!$certificate) { + throw new Exception('Failed saving domain to DB'); + } + } + + $config = + "tls: + certificates: + - certFile: /storage/certificates/{$domain->get()}/fullchain.pem + keyFile: /storage/certificates/{$domain->get()}/privkey.pem"; - if(!\file_put_contents(APP_STORAGE_CONFIG.'/'.$domain->get().'.yml', $config)) { - throw new Exception('Failed to save SSL configuration'); - } + if(!\file_put_contents(APP_STORAGE_CONFIG.'/'.$domain->get().'.yml', $config)) { + throw new Exception('Failed to save SSL configuration'); + } - ResqueScheduler::enqueueAt($renew + $safety, 'v1-certificates', 'CertificatesV1', [ - 'document' => [], - 'domain' => $domain->get(), - 'validateTarget' => $validateTarget, - 'validateCNAME' => $validateCNAME, - ]); // Async task rescheduale + ResqueScheduler::enqueueAt($renew + $safety, 'v1-certificates', 'CertificatesV1', [ + 'document' => [], + 'domain' => $domain->get(), + 'validateTarget' => $validateTarget, + 'validateCNAME' => $validateCNAME, + ]); // Async task rescheduale - Authorization::reset(); + Authorization::reset(); + + // Return db connections to pool + $register->get('dbPool')->put($db); + $register->get('redisPool')->put($redis); + }); } public function shutdown(): void diff --git a/app/workers/database.php b/app/workers/database.php new file mode 100644 index 000000000..b02487104 --- /dev/null +++ b/app/workers/database.php @@ -0,0 +1,183 @@ +args['projectId'] ?? ''; + $type = $this->args['type'] ?? ''; + + switch (strval($type)) { + case CREATE_TYPE_ATTRIBUTE: + $attribute = $this->args['document'] ?? ''; + $attribute = new Document($attribute); + $this->createAttribute($attribute, $projectId); + break; + case DELETE_TYPE_ATTRIBUTE: + $attribute = $this->args['document'] ?? ''; + $attribute = new Document($attribute); + $this->deleteAttribute($attribute, $projectId); + break; + case CREATE_TYPE_INDEX: + $index = $this->args['document'] ?? ''; + $index = new Document($index); + $this->createIndex($index, $projectId); + break; + case DELETE_TYPE_INDEX: + $index = $this->args['document'] ?? ''; + $index = new Document($index); + $this->deleteIndex($index, $projectId); + break; + + default: + Console::error('No database operation for type: '.$type); + break; + } + + } + + public function shutdown(): void + { + } + + /** + * @param Document $attribute + * @param string $projectId + */ + protected function createAttribute($attribute, $projectId): void + { + $dbForExternal = $this->getExternalDB($projectId); + + $collectionId = $attribute->getCollection(); + $id = $attribute->getAttribute('$id', ''); + $type = $attribute->getAttribute('type', ''); + $size = $attribute->getAttribute('size', 0); + $required = $attribute->getAttribute('required', false); + $default = $attribute->getAttribute('default', null); + $signed = $attribute->getAttribute('signed', true); + $array = $attribute->getAttribute('array', false); + $filters = $attribute->getAttribute('filters', []); + + $success = $dbForExternal->createAttribute($collectionId, $id, $type, $size, $required, $default, $signed, $array, $filters); + if ($success) { + $removed = $dbForExternal->removeAttributeInQueue($collectionId, $id); + } + } + + /** + * @param Document $attribute + * @param string $projectId + */ + protected function deleteAttribute($attribute, $projectId): void + { + $dbForExternal = $this->getExternalDB($projectId); + + $collectionId = $attribute->getCollection(); + $id = $attribute->getAttribute('$id'); + + $success = $dbForExternal->deleteAttribute($collectionId, $id); + } + + /** + * @param Document $index + * @param string $projectId + */ + protected function createIndex($index, $projectId): void + { + $dbForExternal = $this->getExternalDB($projectId); + + $collectionId = $index->getCollection(); + $id = $index->getAttribute('$id', ''); + $type = $index->getAttribute('type', ''); + $attributes = $index->getAttribute('attributes', []); + $lengths = $index->getAttribute('lengths', []); + $orders = $index->getAttribute('orders', []); + + $success = $dbForExternal->createIndex($collectionId, $id, $type, $attributes, $lengths, $orders); + if ($success) { + $dbForExternal->removeIndexInQueue($collectionId, $id); + } + } + + /** + * @param Document $index + * @param string $projectId + */ + protected function deleteIndex($index, $projectId): void + { + $dbForExternal = $this->getExternalDB($projectId); + + $collectionId = $index->getCollection(); + $id = $index->getAttribute('$id'); + + $success = $dbForExternal->deleteIndex($collectionId, $id); + } + + /** + * @param string $projectId + * + * @return Database + */ + protected function getInternalDB($projectId): Database + { + global $register; + + $dbForInternal = null; + + go(function() use ($register, $projectId, &$dbForInternal) { + $db = $register->get('dbPool')->get(); + $redis = $register->get('redisPool')->get(); + + $cache = new Cache(new RedisCache($redis)); + $dbForInternal = new Database(new MariaDB($db), $cache); + $dbForInternal->setNamespace('project_'.$projectId.'_internal'); // Main DB + + }); + + return $dbForInternal; + } + + /** + * @param string $projectId + * + * @return Database + */ + protected function getExternalDB($projectId): Database + { + global $register; + + /** @var Database $dbForExternal */ + $dbForExternal = null; + + go(function() use ($register, $projectId, &$dbForExternal) { + $db = $register->get('dbPool')->get(); + $redis = $register->get('redisPool')->get(); + + $cache = new Cache(new RedisCache($redis)); + $dbForExternal = new Database(new MariaDB($db), $cache); + $dbForExternal->setNamespace('project_'.$projectId.'_external'); // Main DB + + }); + + return $dbForExternal; + } +} diff --git a/app/workers/deletes.php b/app/workers/deletes.php index 5ca5303d0..1242d7a8a 100644 --- a/app/workers/deletes.php +++ b/app/workers/deletes.php @@ -1,12 +1,10 @@ getCollection()) { - case Database::SYSTEM_COLLECTION_PROJECTS: + // TODO@kodumbeats define these as constants somewhere + case 'projects': $this->deleteProject($document); break; - case Database::SYSTEM_COLLECTION_FUNCTIONS: + case 'functions': $this->deleteFunction($document, $projectId); break; - case Database::SYSTEM_COLLECTION_USERS: + case 'users': $this->deleteUser($document, $projectId); break; - case Database::SYSTEM_COLLECTION_COLLECTIONS: - $this->deleteDocuments($document, $projectId); - break; - case Database::SYSTEM_COLLECTION_TEAMS: + case 'teams': $this->deleteMemberships($document, $projectId); break; default: @@ -91,97 +87,91 @@ class DeletesV1 extends Worker { } - protected function deleteDocuments(Document $document, $projectId) - { - $collectionId = $document->getId(); - - // Delete Documents in the deleted collection - $this->deleteByGroup([ - '$collection='.$collectionId - ], $this->getProjectDB($projectId)); - } - + /** + * @param Document $document teams document + * @param string $projectId + */ protected function deleteMemberships(Document $document, $projectId) { + $teamId = $document->getAttribute('teamId', ''); + // Delete Memberships - $this->deleteByGroup([ - '$collection='.Database::SYSTEM_COLLECTION_MEMBERSHIPS, - 'teamId='.$document->getId(), - ], $this->getProjectDB($projectId)); + $this->deleteByGroup('memberships', [ + new Query('teamId', Query::TYPE_EQUAL, [$teamId]) + ], $this->getInternalDB($projectId)); } + /** + * @param Document $document project document + */ protected function deleteProject(Document $document) { + $projectId = $document->getId(); // Delete all DBs - $this->getConsoleDB()->deleteNamespace($document->getId()); + $this->getExternalDB($projectId)->delete(); + $this->getInternalDB($projectId)->delete(); + + // Delete all storage directories $uploads = new Local(APP_STORAGE_UPLOADS.'/app-'.$document->getId()); $cache = new Local(APP_STORAGE_CACHE.'/app-'.$document->getId()); - // Delete all storage directories $uploads->delete($uploads->getRoot(), true); $cache->delete($cache->getRoot(), true); } + /** + * @param Document $document user document + * @param string $projectId + */ protected function deleteUser(Document $document, $projectId) { - $tokens = $document->getAttribute('tokens', []); - - foreach ($tokens as $token) { - if (!$this->getProjectDB($projectId)->deleteDocument($token->getId())) { - throw new Exception('Failed to remove token from DB'); - } - } - - $sessions = $document->getAttribute('sessions', []); - - foreach ($sessions as $session) { - if (!$this->getProjectDB($projectId)->deleteDocument($session->getId())) { - throw new Exception('Failed to remove session from DB'); - } - } + $userId = $document->getId(); + // Tokens and Sessions removed with user document // Delete Memberships and decrement team membership counts - $this->deleteByGroup([ - '$collection='.Database::SYSTEM_COLLECTION_MEMBERSHIPS, - 'userId='.$document->getId(), - ], $this->getProjectDB($projectId), function(Document $document) use ($projectId) { + $this->deleteByGroup('memberships', [ + new Query('userId', Query::TYPE_EQUAL, [$userId]) + ], $this->getInternalDB($projectId), function(Document $document) use ($projectId, $userId) { if ($document->getAttribute('confirm')) { // Count only confirmed members $teamId = $document->getAttribute('teamId'); - $team = $this->getProjectDB($projectId)->getDocument($teamId); + $team = $this->getInternalDB($projectId)->getDocument('teams', $teamId); if(!$team->isEmpty()) { - $team = $this->getProjectDB($projectId)->updateDocument(\array_merge($team->getArrayCopy(), [ + $team = $this->getInternalDB($projectId)->updateDocument('teams', $teamId, new Document(\array_merge($team->getArrayCopy(), [ 'sum' => \max($team->getAttribute('sum', 0) - 1, 0), // Ensure that sum >= 0 - ])); + ]))); } } }); } + /** + * @param int $timestamp + */ protected function deleteExecutionLogs($timestamp) { $this->deleteForProjectIds(function($projectId) use ($timestamp) { - if (!($projectDB = $this->getProjectDB($projectId))) { + if (!($dbForInternal = $this->getInternalDB($projectId))) { throw new Exception('Failed to get projectDB for project '.$projectId); } // Delete Executions - $this->deleteByGroup([ - '$collection='.Database::SYSTEM_COLLECTION_EXECUTIONS, - 'dateCreated<'.$timestamp - ], $projectDB); + $this->deleteByGroup('executions', [ + new Query('dateCreated', Query::TYPE_LESSER, [$timestamp]) + ], $dbForInternal); }); } + /** + * @param int $timestamp + */ protected function deleteAbuseLogs($timestamp) { - global $register; if($timestamp == 0) { throw new Exception('Failed to delete audit logs. No timestamp provided'); } $this->deleteForProjectIds(function($projectId) use ($timestamp){ $timeLimit = new TimeLimit("", 0, 1, $this->getInternalDB($projectId)); - $abuse = new Abuse($timeLimit); $status = $abuse->cleanup($timestamp); @@ -191,13 +181,15 @@ class DeletesV1 extends Worker }); } + /** + * @param int $timestamp + */ protected function deleteAuditLogs($timestamp) { - global $register; if($timestamp == 0) { throw new Exception('Failed to delete audit logs. No timestamp provided'); } - $this->deleteForProjectIds(function($projectId) use ($register, $timestamp){ + $this->deleteForProjectIds(function($projectId) use ($timestamp){ $audit = new Audit($this->getInternalDB($projectId)); $status = $audit->cleanup($timestamp); if (!$status) { @@ -206,16 +198,19 @@ class DeletesV1 extends Worker }); } + /** + * @param Document $document function document + * @param string $projectId + */ protected function deleteFunction(Document $document, $projectId) { - $projectDB = $this->getProjectDB($projectId); + $dbForInternal = $this->getInternalDB($projectId); $device = new Local(APP_STORAGE_FUNCTIONS.'/app-'.$projectId); // Delete Tags - $this->deleteByGroup([ - '$collection='.Database::SYSTEM_COLLECTION_TAGS, - 'functionId='.$document->getId(), - ], $projectDB, function(Document $document) use ($device) { + $this->deleteByGroup('tags', [ + new Query('functionId', Query::TYPE_EQUAL, [$document->getId()]) + ], $dbForInternal, function(Document $document) use ($device) { if ($device->delete($document->getAttribute('path', ''))) { Console::success('Delete code tag: '.$document->getAttribute('path', '')); @@ -226,17 +221,25 @@ class DeletesV1 extends Worker }); // Delete Executions - $this->deleteByGroup([ - '$collection='.Database::SYSTEM_COLLECTION_EXECUTIONS, - 'functionId='.$document->getId(), - ], $projectDB); + $this->deleteByGroup('executions', [ + new Query('functionId', Query::TYPE_EQUAL, [$document->getId()]) + ], $dbForInternal); } + + /** + * @param Document $document to be deleted + * @param Database $database to delete it from + * @param callable $callback to perform after document is deleted + * + * @return bool + */ protected function deleteById(Document $document, Database $database, callable $callback = null): bool { Authorization::disable(); - if($database->deleteDocument($document->getId())) { + // TODO@kodumbeats is it better to pass objects or ID strings? + if($database->deleteDocument($document->getCollection(), $document->getId())) { Console::success('Deleted document "'.$document->getId().'" successfully'); if(is_callable($callback)) { @@ -253,6 +256,9 @@ class DeletesV1 extends Worker Authorization::reset(); } + /** + * @param callable $callback + */ protected function deleteForProjectIds(callable $callback) { $count = 0; @@ -262,19 +268,12 @@ class DeletesV1 extends Worker $sum = $limit; $executionStart = \microtime(true); - + while($sum === $limit) { $chunk++; Authorization::disable(); - $projects = $this->getConsoleDB()->getCollection([ - 'limit' => $limit, - 'orderType' => 'ASC', - 'orderCast' => 'string', - 'filters' => [ - '$collection='.Database::SYSTEM_COLLECTION_PROJECTS, - ], - ]); + $projects = $this->getConsoleDB()->find('projects', [], $limit); Authorization::reset(); $projectIds = array_map (function ($project) { @@ -294,7 +293,13 @@ class DeletesV1 extends Worker Console::info("Found {$count} projects " . ($executionEnd - $executionStart) . " seconds"); } - protected function deleteByGroup(array $filters, Database $database, callable $callback = null) + /** + * @param string $collection collectionID + * @param Query[] $queries + * @param Database $database + * @param callable $callback + */ + protected function deleteByGroup(string $collection, array $queries, Database $database, callable $callback = null) { $count = 0; $chunk = 0; @@ -303,19 +308,13 @@ class DeletesV1 extends Worker $sum = $limit; $executionStart = \microtime(true); - + while($sum === $limit) { $chunk++; Authorization::disable(); - $results = $database->getCollection([ - 'limit' => $limit, - 'orderField' => '$id', - 'orderType' => 'ASC', - 'orderCast' => 'string', - 'filters' => $filters, - ]); + $results = $database->find($collection, $queries, $limit, 0); Authorization::reset(); @@ -334,6 +333,10 @@ class DeletesV1 extends Worker Console::info("Deleted {$count} document by group in " . ($executionEnd - $executionStart) . " seconds"); } + /** + * @param Document $document certificates document + * @return Database + */ protected function deleteCertificates(Document $document) { $domain = $document->getAttribute('domain'); @@ -348,56 +351,48 @@ class DeletesV1 extends Worker Console::info("No certificate files found for {$domain}"); } } + + /** + * @param string $projectId + * @return Database + */ + protected function getInternalDB($projectId): Database + { + global $register; + + $cache = new Cache(new RedisCache($register->get('cache'))); + $dbForInternal = new Database(new MariaDB($register->get('db')), $cache); + $dbForInternal->setNamespace('project_'.$projectId.'_internal'); // Main DB + + return $dbForInternal; + } /** - * @return Database; + * @param string $projectId + * @return Database + */ + protected function getExternalDB($projectId): Database + { + global $register; + + $cache = new Cache(new RedisCache($register->get('cache'))); + $dbForExternal = new Database(new MariaDB($register->get('db')), $cache); + $dbForExternal->setNamespace('project_'.$projectId.'_external'); // Main DB + + return $dbForExternal; + } + + /** + * @return Database */ protected function getConsoleDB(): Database { global $register; - $db = $register->get('db'); - $cache = $register->get('cache'); - - if($this->consoleDB === null) { - $this->consoleDB = new Database(); - $this->consoleDB->setAdapter(new RedisAdapter(new MySQLAdapter($db, $cache), $cache));; - $this->consoleDB->setNamespace('app_console'); // Main DB - $this->consoleDB->setMocks(Config::getParam('collections', [])); - } - - return $this->consoleDB; - } - - /** - * @return Database; - */ - protected function getProjectDB($projectId): Database - { - global $register; - - $db = $register->get('db'); - $cache = $register->get('cache'); - - $projectDB = new Database(); - $projectDB->setAdapter(new RedisAdapter(new MySQLAdapter($db, $cache), $cache)); - $projectDB->setNamespace('app_'.$projectId); // Main DB - $projectDB->setMocks(Config::getParam('collections', [])); - - return $projectDB; - } - - /** - * @return Database2 - */ - protected function getInternalDB($projectId): Database2 - { - global $register; - $cache = new Cache(new RedisCache($register->get('cache'))); - $dbForInternal = new Database2(new MariaDB($register->get('db')), $cache); - $dbForInternal->setNamespace('project_'.$projectId.'_internal'); // Main DB + $dbForConsole = new Database(new MariaDB($register->get('db')), $cache); + $dbForConsole->setNamespace('project_console_internal'); // Main DB - return $dbForInternal; + return $dbForConsole; } } diff --git a/app/workers/functions.php b/app/workers/functions.php index 791014244..8cb6fafbb 100644 --- a/app/workers/functions.php +++ b/app/workers/functions.php @@ -465,10 +465,6 @@ class FunctionsV1 extends Worker Authorization::reset(); - if (false === $function) { - throw new Exception('Failed saving execution to DB', 500); - } - $executionModel = new Execution(); $executionUpdate = new Event('v1-webhooks', 'WebhooksV1'); diff --git a/app/workers/tasks.php b/app/workers/tasks.php index 0b1b33e23..cb24c65a9 100644 --- a/app/workers/tasks.php +++ b/app/workers/tasks.php @@ -1,14 +1,14 @@ get('db'); $cache = $register->get('cache'); - $consoleDB = new Database(); - $consoleDB->setAdapter(new RedisAdapter(new MySQLAdapter($db, $cache), $cache)); - $consoleDB->setNamespace('app_console'); // Main DB - $consoleDB->setMocks(Config::getParam('collections', [])); + $projectId = $this->args['projectId'] ?? null; + $taskId = $this->args['$id'] ?? null; + $updated = $this->args['updated'] ?? null; + $next = $this->args['next'] ?? null; + $delay = \time() - $next; + $errors = []; + $timeout = 60 * 5; // 5 minutes + $errorLimit = 5; + $logLimit = 5; + $alert = ''; + + $cache = new Cache(new Redis($cache)); + $dbForConsole = new Database(new MariaDB($db), $cache); + $dbForConsole->setNamespace('project_console_internal'); /* * 1. Get Original Task @@ -51,30 +61,25 @@ class TasksV1 extends Worker * If error count bigger than allowed change status to pause */ - $taskId = $this->args['$id'] ?? null; - $updated = $this->args['updated'] ?? null; - $next = $this->args['next'] ?? null; - $delay = \time() - $next; - $errors = []; - $timeout = 60 * 5; // 5 minutes - $errorLimit = 5; - $logLimit = 5; - $alert = ''; - if (empty($taskId)) { throw new Exception('Missing task $id'); } Authorization::disable(); - $task = $consoleDB->getDocument($taskId); + $project = $dbForConsole->getDocument('projects', $projectId); Authorization::reset(); - if (\is_null($task->getId()) || Database::SYSTEM_COLLECTION_TASKS !== $task->getCollection()) { + // Find the task in the $project->getAttribute('tasks') array + $taskIndex = array_search($taskId, array_column($project->getAttributes()['tasks'], '$id')); + + if ($taskIndex === false) { throw new Exception('Task Not Found'); } + $task = $project->getAttribute('tasks')[$taskIndex]; + if ($task->getAttribute('updated') !== $updated) { // Task have already been rescheduled by owner return; } @@ -193,9 +198,11 @@ class TasksV1 extends Worker ->setAttribute('delay', $delay) ; - Authorization::disable(); + $project->findAndReplace('$id', $task->getId(), $task); - if (false === $consoleDB->updateDocument($task->getArrayCopy())) { + Authorization::disable(); + + if (false === $dbForConsole->updateDocument('projects', $project->getId(), $project)) { throw new Exception('Failed saving tasks to DB'); } diff --git a/bin/worker-database b/bin/worker-database new file mode 100644 index 000000000..97e067d0d --- /dev/null +++ b/bin/worker-database @@ -0,0 +1,10 @@ +#!/bin/sh + +if [ -z "$_APP_REDIS_USER" ] && [ -z "$_APP_REDIS_PASS" ] +then + REDIS_BACKEND="${_APP_REDIS_HOST}:${_APP_REDIS_PORT}" +else + REDIS_BACKEND="redis://${_APP_REDIS_USER}:${_APP_REDIS_PASS}@${_APP_REDIS_HOST}:${_APP_REDIS_PORT}" +fi + +QUEUE='v1-database' APP_INCLUDE='/usr/src/code/app/workers/database.php' php /usr/src/code/vendor/bin/resque -dopcache.preload=opcache.preload=/usr/src/code/app/preload.php \ No newline at end of file diff --git a/composer.lock b/composer.lock index ef2a8a7ad..c0fbb00ce 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "68ed82b309f6a9dd878ef031424ac00a", + "content-hash": "ca8cf356c884e8d332b04bebf9d3b11b", "packages": [ { "name": "adhocore/jwt", @@ -115,16 +115,16 @@ }, { "name": "appwrite/php-runtimes", - "version": "0.3.0", + "version": "0.4.0", "source": { "type": "git", "url": "https://github.com/appwrite/php-runtimes.git", - "reference": "39be003cdff22c8447de151921001eb5d3bf2319" + "reference": "cc7090a67d8824c779190b38873f0f8154f906b2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/php-runtimes/zipball/39be003cdff22c8447de151921001eb5d3bf2319", - "reference": "39be003cdff22c8447de151921001eb5d3bf2319", + "url": "https://api.github.com/repos/appwrite/php-runtimes/zipball/cc7090a67d8824c779190b38873f0f8154f906b2", + "reference": "cc7090a67d8824c779190b38873f0f8154f906b2", "shasum": "" }, "require": { @@ -144,7 +144,7 @@ }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3" + "BSD-3-Clause" ], "authors": [ { @@ -164,9 +164,9 @@ ], "support": { "issues": "https://github.com/appwrite/php-runtimes/issues", - "source": "https://github.com/appwrite/php-runtimes/tree/0.3.0" + "source": "https://github.com/appwrite/php-runtimes/tree/0.4.0" }, - "time": "2021-06-15T07:52:43+00:00" + "time": "2021-06-23T07:17:12+00:00" }, { "name": "chillerlan/php-qrcode", diff --git a/docker-compose.yml b/docker-compose.yml index be056b8a6..54998b264 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -240,6 +240,31 @@ services: - _APP_DB_USER - _APP_DB_PASS + appwrite-worker-database: + entrypoint: worker-database + container_name: appwrite-worker-database + build: + context: . + networks: + - appwrite + volumes: + - ./app:/usr/src/code/app + - ./src:/usr/src/code/src + depends_on: + - redis + - mariadb + environment: + - _APP_ENV + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_REDIS_USER + - _APP_REDIS_PASS + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS + appwrite-worker-certificates: entrypoint: worker-certificates container_name: appwrite-worker-certificates diff --git a/src/Appwrite/Event/Event.php b/src/Appwrite/Event/Event.php index b2c69337a..a250be799 100644 --- a/src/Appwrite/Event/Event.php +++ b/src/Appwrite/Event/Event.php @@ -7,6 +7,9 @@ use Resque; class Event { + const DATABASE_QUEUE_NAME= 'v1-database'; + const DATABASE_CLASS_NAME = 'DatabaseV1'; + const DELETE_QUEUE_NAME = 'v1-deletes'; const DELETE_CLASS_NAME = 'DeletesV1'; diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index cc9e174f7..bbae1c3e9 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -10,6 +10,7 @@ use Appwrite\Utopia\Response\Filter; use Appwrite\Utopia\Response\Model; use Appwrite\Utopia\Response\Model\None; use Appwrite\Utopia\Response\Model\Any; +use Appwrite\Utopia\Response\Model\Attribute; use Appwrite\Utopia\Response\Model\BaseList; use Appwrite\Utopia\Response\Model\Collection; use Appwrite\Utopia\Response\Model\Continent; @@ -22,6 +23,7 @@ use Appwrite\Utopia\Response\Model\ErrorDev; use Appwrite\Utopia\Response\Model\Execution; use Appwrite\Utopia\Response\Model\File; use Appwrite\Utopia\Response\Model\Func; +use Appwrite\Utopia\Response\Model\Index; use Appwrite\Utopia\Response\Model\JWT; use Appwrite\Utopia\Response\Model\Key; use Appwrite\Utopia\Response\Model\Language; @@ -63,6 +65,10 @@ class Response extends SwooleResponse // Database const MODEL_COLLECTION = 'collection'; const MODEL_COLLECTION_LIST = 'collectionList'; + const MODEL_ATTRIBUTE = 'attribute'; + const MODEL_ATTRIBUTE_LIST = 'attributeList'; + const MODEL_INDEX = 'index'; + const MODEL_INDEX_LIST = 'indexList'; const MODEL_RULE = 'rule'; const MODEL_DOCUMENT = 'document'; const MODEL_DOCUMENT_LIST = 'documentList'; @@ -150,6 +156,8 @@ class Response extends SwooleResponse ->setModel(new ErrorDev()) // Lists ->setModel(new BaseList('Collections List', self::MODEL_COLLECTION_LIST, 'collections', self::MODEL_COLLECTION)) + ->setModel(new BaseList('Attributes List', self::MODEL_ATTRIBUTE_LIST, 'attributes', self::MODEL_ATTRIBUTE)) + ->setModel(new BaseList('Indexes List', self::MODEL_INDEX_LIST, 'indexes', self::MODEL_INDEX)) ->setModel(new BaseList('Documents List', self::MODEL_DOCUMENT_LIST, 'documents', self::MODEL_DOCUMENT)) ->setModel(new BaseList('Users List', self::MODEL_USER_LIST, 'users', self::MODEL_USER)) ->setModel(new BaseList('Sessions List', self::MODEL_SESSION_LIST, 'sessions', self::MODEL_SESSION)) @@ -174,6 +182,8 @@ class Response extends SwooleResponse // Entities ->setModel(new Permissions()) ->setModel(new Collection()) + ->setModel(new Attribute()) + ->setModel(new Index()) ->setModel(new ModelDocument()) ->setModel(new Rule()) ->setModel(new Log()) diff --git a/src/Appwrite/Utopia/Response/Model/Attribute.php b/src/Appwrite/Utopia/Response/Model/Attribute.php new file mode 100644 index 000000000..b8763be6f --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/Attribute.php @@ -0,0 +1,87 @@ +addRule('$collection', [ + 'type' => self::TYPE_STRING, + 'description' => 'Collection ID.', + 'default' => '', + 'example' => '5e5ea5c16d55', + ]) + ->addRule('$id', [ + 'type' => self::TYPE_STRING, + 'description' => 'Attribute ID.', + 'default' => '', + 'example' => '60ccf71b98a2d', + ]) + ->addRule('type', [ + 'type' => self::TYPE_STRING, + 'description' => 'Attribute type.', + 'default' => '', + 'example' => 'string', + ]) + ->addRule('size', [ + 'type' => self::TYPE_STRING, + 'description' => 'Attribute size.', + 'default' => 0, + 'example' => 128, + ]) + ->addRule('required', [ + 'type' => self::TYPE_BOOLEAN, + 'description' => 'Is attribute required?', + 'default' => false, + 'example' => true, + ]) + ->addRule('signed', [ + 'type' => self::TYPE_BOOLEAN, + 'description' => 'Is attribute signed?', + 'default' => true, + 'example' => true, + 'required' => false, + ]) + ->addRule('array', [ + 'type' => self::TYPE_BOOLEAN, + 'description' => 'Is attribute an array?', + 'default' => false, + 'example' => false, + 'required' => false + ]) + ->addRule('filters', [ + 'type' => self::TYPE_STRING, + 'description' => 'Attribute filters.', + 'default' => [], + 'example' => [], + 'array' => true, + 'required' => false, + ]) + ; + } + + /** + * Get Name + * + * @return string + */ + public function getName():string + { + return 'Attribute'; + } + + /** + * Get Collection + * + * @return string + */ + public function getType():string + { + return Response::MODEL_ATTRIBUTE; + } +} \ No newline at end of file diff --git a/src/Appwrite/Utopia/Response/Model/Collection.php b/src/Appwrite/Utopia/Response/Model/Collection.php index 9b1f184b9..4c1db0f53 100644 --- a/src/Appwrite/Utopia/Response/Model/Collection.php +++ b/src/Appwrite/Utopia/Response/Model/Collection.php @@ -4,6 +4,7 @@ namespace Appwrite\Utopia\Response\Model; use Appwrite\Utopia\Response; use Appwrite\Utopia\Response\Model; +use stdClass; class Collection extends Model { @@ -16,36 +17,53 @@ class Collection extends Model 'default' => '', 'example' => '5e5ea5c16897e', ]) - ->addRule('$permissions', [ - 'type' => Response::MODEL_PERMISSIONS, - 'description' => 'Collection permissions.', - 'default' => new \stdClass, - 'example' => new \stdClass, - 'array' => false, + ->addRule('$read', [ + 'type' => self::TYPE_STRING, + 'description' => 'Collection read permissions.', + 'default' => '', + 'example' => 'role:all', + 'array' => true + ]) + ->addRule('$write', [ + 'type' => self::TYPE_STRING, + 'description' => 'Collection write permissions.', + 'default' => '', + 'example' => 'user:608f9da25e7e1', + 'array' => true ]) ->addRule('name', [ 'type' => self::TYPE_STRING, 'description' => 'Collection name.', 'default' => '', - 'example' => 'Movies', + 'example' => '', ]) - ->addRule('dateCreated', [ - 'type' => self::TYPE_INTEGER, - 'description' => 'Collection creation date in Unix timestamp.', - 'default' => 0, - 'example' => 1592981250, - ]) - ->addRule('dateUpdated', [ - 'type' => self::TYPE_INTEGER, - 'description' => 'Collection creation date in Unix timestamp.', - 'default' => 0, - 'example' => 1592981550, - ]) - ->addRule('rules', [ - 'type' => Response::MODEL_RULE, - 'description' => 'Collection rules.', + ->addRule('attributes', [ + 'type' => Response::MODEL_ATTRIBUTE, + 'description' => 'Collection attributes.', 'default' => [], - 'array' => true, + 'example' => new stdClass, + 'array' => true + ]) + ->addRule('indexes', [ + 'type' => Response::MODEL_INDEX, + 'description' => 'Collection indexes.', + 'default' => [], + 'example' => new stdClass, + 'array' => true + ]) + ->addRule('attributesInQueue', [ + 'type' => Response::MODEL_ATTRIBUTE, + 'description' => 'Collection attributes in creation queue.', + 'default' => [], + 'example' => new stdClass, + 'array' => true + ]) + ->addRule('indexesInQueue', [ + 'type' => Response::MODEL_INDEX, + 'description' => 'Collection indexes in creation queue.', + 'default' => [], + 'example' => new stdClass, + 'array' => true ]) ; } @@ -69,4 +87,4 @@ class Collection extends Model { return Response::MODEL_COLLECTION; } -} \ No newline at end of file +} diff --git a/src/Appwrite/Utopia/Response/Model/Document.php b/src/Appwrite/Utopia/Response/Model/Document.php index a8a9f8097..2ee8e8cf4 100644 --- a/src/Appwrite/Utopia/Response/Model/Document.php +++ b/src/Appwrite/Utopia/Response/Model/Document.php @@ -41,12 +41,20 @@ class Document extends Any 'default' => '', 'example' => '5e5ea5c15117e', ]) - ->addRule('$permissions', [ - 'type' => Response::MODEL_PERMISSIONS, - 'description' => 'Document permissions.', - 'default' => new \stdClass, - 'example' => new \stdClass, - 'array' => false, - ]); + ->addRule('$read', [ + 'type' => self::TYPE_STRING, + 'description' => 'Document read permissions.', + 'default' => '', + 'example' => 'role:all', + 'array' => true, + ]) + ->addRule('$write', [ + 'type' => self::TYPE_STRING, + 'description' => 'Document write permissions.', + 'default' => '', + 'example' => 'user:608f9da25e7e1', + 'array' => true, + ]) + ; } } diff --git a/src/Appwrite/Utopia/Response/Model/Index.php b/src/Appwrite/Utopia/Response/Model/Index.php new file mode 100644 index 000000000..3ae1536aa --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/Index.php @@ -0,0 +1,76 @@ +addRule('$collection', [ + 'type' => self::TYPE_STRING, + 'description' => 'Collection ID.', + 'default' => '', + 'example' => '5e5ea5c16d55', + ]) + ->addRule('$id', [ + 'type' => self::TYPE_STRING, + 'description' => 'Index ID.', + 'default' => '', + 'example' => '', + ]) + ->addRule('type', [ + 'type' => self::TYPE_STRING, + 'description' => 'Index type.', + 'default' => '', + 'example' => '', + ]) + ->addRule('attributes', [ + 'type' => self::TYPE_STRING, + 'description' => 'Index attributes.', + 'default' => [], + 'example' => [], + 'array' => true, + ]) + ->addRule('lengths', [ + 'type' => self::TYPE_STRING, + 'description' => 'Index lengths.', + 'default' => [], + 'example' => [], + 'array' => true, + 'required' => false, + ]) + ->addRule('orders', [ + 'type' => self::TYPE_STRING, + 'description' => 'Index orders.', + 'default' => [], + 'example' => [], + 'array' => true, + 'required' => false, + ]) + ; + } + + /** + * Get Name + * + * @return string + */ + public function getName():string + { + return 'Index'; + } + + /** + * Get Collection + * + * @return string + */ + public function getType():string + { + return Response::MODEL_INDEX; + } +} diff --git a/src/Appwrite/Utopia/Response/Model/Task.php b/src/Appwrite/Utopia/Response/Model/Task.php index 10e97acad..fa5816ed1 100644 --- a/src/Appwrite/Utopia/Response/Model/Task.php +++ b/src/Appwrite/Utopia/Response/Model/Task.php @@ -21,6 +21,12 @@ class Task extends Model 'default' => '', 'example' => '5e5ea5c16897e', ]) + ->addRule('projectId', [ + 'type' => self::TYPE_STRING, + 'description' => 'Project ID.', + 'default' => '', + 'example' => '5e5ea5c16897e', + ]) ->addRule('name', [ 'type' => self::TYPE_STRING, 'description' => 'Task name.', diff --git a/tests/e2e/Scopes/ProjectCustom.php b/tests/e2e/Scopes/ProjectCustom.php index 3f8028528..51f318318 100644 --- a/tests/e2e/Scopes/ProjectCustom.php +++ b/tests/e2e/Scopes/ProjectCustom.php @@ -69,6 +69,10 @@ trait ProjectCustom 'teams.write', 'collections.read', 'collections.write', + 'attributes.read', + 'attributes.write', + 'indexes.read', + 'indexes.write', 'documents.read', 'documents.write', 'files.read', @@ -110,6 +114,10 @@ trait ProjectCustom 'database.collections.create', 'database.collections.update', 'database.collections.delete', + 'database.attributes.create', + 'database.attributes.delete', + 'database.indexes.create', + 'database.indexes.delete', 'database.documents.create', 'database.documents.update', 'database.documents.delete', diff --git a/tests/e2e/Services/Database/DatabaseBase.php b/tests/e2e/Services/Database/DatabaseBase.php index d018f29a8..21f22ba7a 100644 --- a/tests/e2e/Services/Database/DatabaseBase.php +++ b/tests/e2e/Services/Database/DatabaseBase.php @@ -11,42 +11,6 @@ trait DatabaseBase /** * Test for SUCCESS */ - $actors = $this->client->call(Client::METHOD_POST, '/database/collections', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'name' => 'Actors', - 'read' => ['role:all'], - 'write' => ['role:member', 'role:admin'], - 'rules' => [ - [ - 'label' => 'First Name', - 'key' => 'firstName', - 'type' => 'text', - 'default' => '', - 'required' => true, - 'array' => false - ], - [ - 'label' => 'Last Name', - 'key' => 'lastName', - 'type' => 'text', - 'default' => '', - 'required' => true, - 'array' => false - ], - ], - ]); - - $this->assertEquals($actors['headers']['status-code'], 201); - $this->assertEquals($actors['body']['name'], 'Actors'); - $this->assertIsArray($actors['body']['$permissions']); - $this->assertIsArray($actors['body']['$permissions']['read']); - $this->assertIsArray($actors['body']['$permissions']['write']); - $this->assertCount(1, $actors['body']['$permissions']['read']); - $this->assertCount(2, $actors['body']['$permissions']['write']); - $movies = $this->client->call(Client::METHOD_POST, '/database/collections', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -54,50 +18,142 @@ trait DatabaseBase ]), [ 'name' => 'Movies', 'read' => ['role:all'], - 'write' => ['role:member', 'role:admin'], - 'rules' => [ - [ - 'label' => 'Name', - 'key' => 'name', - 'type' => 'text', - 'default' => '', - 'required' => true, - 'array' => false - ], - [ - 'label' => 'Release Year', - 'key' => 'releaseYear', - 'type' => 'numeric', - 'default' => 0, - 'required' => false, - 'array' => false - ], - [ - 'label' => 'Actors', - 'key' => 'actors', - 'type' => 'document', - 'default' => [], - 'required' => false, - 'array' => true, - 'list' => [$actors['body']['$id']], - ], - ], + 'write' => ['role:all'], ]); $this->assertEquals($movies['headers']['status-code'], 201); $this->assertEquals($movies['body']['name'], 'Movies'); - $this->assertIsArray($movies['body']['$permissions']); - $this->assertIsArray($movies['body']['$permissions']['read']); - $this->assertIsArray($movies['body']['$permissions']['write']); - $this->assertCount(1, $movies['body']['$permissions']['read']); - $this->assertCount(2, $movies['body']['$permissions']['write']); - return array_merge(['moviesId' => $movies['body']['$id'], 'actorsId' => $actors['body']['$id']]); + return ['moviesId' => $movies['body']['$id']]; } /** * @depends testCreateCollection */ + public function testCreateAttributes(array $data): array + { + $title = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['moviesId'] . '/attributes', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'id' => 'title', + 'type' => 'string', + 'size' => 256, + 'required' => true, + ]); + + $releaseYear = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['moviesId'] . '/attributes', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'id' => 'releaseYear', + 'type' => 'integer', + 'size' => 0, + 'required' => true, + ]); + + $actors = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['moviesId'] . '/attributes', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'id' => 'actors', + 'type' => 'string', + 'size' => 256, + 'required' => false, + 'default' => null, + 'array' => true, + ]); + + $this->assertEquals($title['headers']['status-code'], 201); + $this->assertEquals($title['body']['$collection'], $data['moviesId']); + $this->assertEquals($title['body']['$id'], 'title'); + $this->assertEquals($title['body']['type'], 'string'); + $this->assertEquals($title['body']['size'], 256); + $this->assertEquals($title['body']['required'], true); + + $this->assertEquals($releaseYear['headers']['status-code'], 201); + $this->assertEquals($releaseYear['body']['$collection'], $data['moviesId']); + $this->assertEquals($releaseYear['body']['$id'], 'releaseYear'); + $this->assertEquals($releaseYear['body']['type'], 'integer'); + $this->assertEquals($releaseYear['body']['size'], 0); + $this->assertEquals($releaseYear['body']['required'], true); + + $this->assertEquals($actors['headers']['status-code'], 201); + $this->assertEquals($actors['body']['$collection'], $data['moviesId']); + $this->assertEquals($actors['body']['$id'], 'actors'); + $this->assertEquals($actors['body']['type'], 'string'); + $this->assertEquals($actors['body']['size'], 256); + $this->assertEquals($actors['body']['required'], false); + $this->assertEquals($actors['body']['array'], true); + + // wait for database worker to create attributes + sleep(10); + + $movies = $this->client->call(Client::METHOD_GET, '/database/collections/' . $data['moviesId'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), []); + + $this->assertEquals($movies['body']['$id'], $title['body']['$collection']); + $this->assertEquals($movies['body']['$id'], $releaseYear['body']['$collection']); + $this->assertEquals($movies['body']['$id'], $actors['body']['$collection']); + $this->assertIsArray($movies['body']['attributesInQueue']); + $this->assertCount(0, $movies['body']['attributesInQueue']); + $this->assertIsArray($movies['body']['attributes']); + $this->assertCount(3, $movies['body']['attributes']); + $this->assertEquals($movies['body']['attributes'][0]['$id'], $title['body']['$id']); + $this->assertEquals($movies['body']['attributes'][1]['$id'], $releaseYear['body']['$id']); + $this->assertEquals($movies['body']['attributes'][2]['$id'], $actors['body']['$id']); + + return $data; + } + + /** + * @depends testCreateAttributes + */ + public function testCreateIndexes(array $data): array + { + $titleIndex = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['moviesId'] . '/indexes', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'id' => 'titleIndex', + 'type' => 'fulltext', + 'attributes' => ['title'], + ]); + + $this->assertEquals($titleIndex['headers']['status-code'], 201); + $this->assertEquals($titleIndex['body']['$collection'], $data['moviesId']); + $this->assertEquals($titleIndex['body']['$id'], 'titleIndex'); + $this->assertEquals($titleIndex['body']['type'], 'fulltext'); + $this->assertCount(1, $titleIndex['body']['attributes']); + $this->assertEquals($titleIndex['body']['attributes'][0], 'title'); + + // wait for database worker to create index + sleep(5); + + $movies = $this->client->call(Client::METHOD_GET, '/database/collections/' . $data['moviesId'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), []); + + $this->assertEquals($movies['body']['$id'], $titleIndex['body']['$collection']); + $this->assertIsArray($movies['body']['indexes']); + $this->assertCount(1, $movies['body']['indexes']); + $this->assertEquals($movies['body']['indexes'][0]['$id'], $titleIndex['body']['$id']); + + return $data; + } + + /** + * @depends testCreateIndexes + */ public function testCreateDocument(array $data):array { $document1 = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['moviesId'] . '/documents', array_merge([ @@ -105,21 +161,11 @@ trait DatabaseBase 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'data' => [ - 'name' => 'Captain America', + 'title' => 'Captain America', 'releaseYear' => 1944, 'actors' => [ - [ - '$collection' => $data['actorsId'], - '$permissions' => ['read' => [], 'write' => []], - 'firstName' => 'Chris', - 'lastName' => 'Evans', - ], - [ - '$collection' => $data['actorsId'], - '$permissions' => ['read' => [], 'write' => []], - 'firstName' => 'Samuel', - 'lastName' => 'Jackson', - ], + 'Chris Evans', + 'Samuel Jackson', ] ], 'read' => ['user:'.$this->getUser()['$id']], @@ -131,27 +177,12 @@ trait DatabaseBase 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'data' => [ - 'name' => 'Spider-Man: Far From Home', + 'title' => 'Spider-Man: Far From Home', 'releaseYear' => 2019, 'actors' => [ - [ - '$collection' => $data['actorsId'], - '$permissions' => ['read' => [], 'write' => []], - 'firstName' => 'Tom', - 'lastName' => 'Holland', - ], - [ - '$collection' => $data['actorsId'], - '$permissions' => ['read' => [], 'write' => []], - 'firstName' => 'Zendaya', - 'lastName' => 'Maree Stoermer', - ], - [ - '$collection' => $data['actorsId'], - '$permissions' => ['read' => [], 'write' => []], - 'firstName' => 'Samuel', - 'lastName' => 'Jackson', - ], + 'Tom Holland', + 'Zendaya Maree Stoermer', + 'Samuel Jackson', ] ], 'read' => ['user:'.$this->getUser()['$id']], @@ -163,21 +194,11 @@ trait DatabaseBase 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'data' => [ - 'name' => 'Spider-Man: Homecoming', + 'title' => 'Spider-Man: Homecoming', 'releaseYear' => 2017, 'actors' => [ - [ - '$collection' => $data['actorsId'], - '$permissions' => ['read' => [], 'write' => []], - 'firstName' => 'Tom', - 'lastName' => 'Holland', - ], - [ - '$collection' => $data['actorsId'], - '$permissions' => ['read' => [], 'write' => []], - 'firstName' => 'Zendaya', - 'lastName' => 'Maree Stoermer', - ], + 'Tom Holland', + 'Zendaya Maree Stoermer', ], ], 'read' => ['user:'.$this->getUser()['$id']], @@ -197,46 +218,40 @@ trait DatabaseBase $this->assertEquals($document1['headers']['status-code'], 201); $this->assertEquals($document1['body']['$collection'], $data['moviesId']); - $this->assertEquals($document1['body']['name'], 'Captain America'); + $this->assertEquals($document1['body']['title'], 'Captain America'); $this->assertEquals($document1['body']['releaseYear'], 1944); - $this->assertIsArray($document1['body']['$permissions']); - $this->assertIsArray($document1['body']['$permissions']['read']); - $this->assertIsArray($document1['body']['$permissions']['write']); - $this->assertCount(1, $document1['body']['$permissions']['read']); - $this->assertCount(1, $document1['body']['$permissions']['write']); + $this->assertIsArray($document1['body']['$read']); + $this->assertIsArray($document1['body']['$write']); + $this->assertCount(1, $document1['body']['$read']); + $this->assertCount(1, $document1['body']['$write']); $this->assertCount(2, $document1['body']['actors']); + $this->assertEquals($document1['body']['actors'][0], 'Chris Evans'); + $this->assertEquals($document1['body']['actors'][1], 'Samuel Jackson'); $this->assertEquals($document2['headers']['status-code'], 201); $this->assertEquals($document2['body']['$collection'], $data['moviesId']); - $this->assertEquals($document2['body']['name'], 'Spider-Man: Far From Home'); + $this->assertEquals($document2['body']['title'], 'Spider-Man: Far From Home'); $this->assertEquals($document2['body']['releaseYear'], 2019); - $this->assertIsArray($document2['body']['$permissions']); - $this->assertIsArray($document2['body']['$permissions']['read']); - $this->assertIsArray($document2['body']['$permissions']['write']); - $this->assertCount(1, $document2['body']['$permissions']['read']); - $this->assertCount(1, $document2['body']['$permissions']['write']); + $this->assertIsArray($document2['body']['$read']); + $this->assertIsArray($document2['body']['$write']); + $this->assertCount(1, $document2['body']['$read']); + $this->assertCount(1, $document2['body']['$write']); $this->assertCount(3, $document2['body']['actors']); - $this->assertEquals($document2['body']['actors'][0]['firstName'], 'Tom'); - $this->assertEquals($document2['body']['actors'][0]['lastName'], 'Holland'); - $this->assertEquals($document2['body']['actors'][1]['firstName'], 'Zendaya'); - $this->assertEquals($document2['body']['actors'][1]['lastName'], 'Maree Stoermer'); - $this->assertEquals($document2['body']['actors'][2]['firstName'], 'Samuel'); - $this->assertEquals($document2['body']['actors'][2]['lastName'], 'Jackson'); + $this->assertEquals($document2['body']['actors'][0], 'Tom Holland'); + $this->assertEquals($document2['body']['actors'][1], 'Zendaya Maree Stoermer'); + $this->assertEquals($document2['body']['actors'][2], 'Samuel Jackson'); $this->assertEquals($document3['headers']['status-code'], 201); $this->assertEquals($document3['body']['$collection'], $data['moviesId']); - $this->assertEquals($document3['body']['name'], 'Spider-Man: Homecoming'); + $this->assertEquals($document3['body']['title'], 'Spider-Man: Homecoming'); $this->assertEquals($document3['body']['releaseYear'], 2017); - $this->assertIsArray($document3['body']['$permissions']); - $this->assertIsArray($document3['body']['$permissions']['read']); - $this->assertIsArray($document3['body']['$permissions']['write']); - $this->assertCount(1, $document3['body']['$permissions']['read']); - $this->assertCount(1, $document3['body']['$permissions']['write']); + $this->assertIsArray($document3['body']['$read']); + $this->assertIsArray($document3['body']['$write']); + $this->assertCount(1, $document3['body']['$read']); + $this->assertCount(1, $document3['body']['$write']); $this->assertCount(2, $document3['body']['actors']); - $this->assertEquals($document3['body']['actors'][0]['firstName'], 'Tom'); - $this->assertEquals($document3['body']['actors'][0]['lastName'], 'Holland'); - $this->assertEquals($document3['body']['actors'][1]['firstName'], 'Zendaya'); - $this->assertEquals($document3['body']['actors'][1]['lastName'], 'Maree Stoermer'); + $this->assertEquals($document2['body']['actors'][0], 'Tom Holland'); + $this->assertEquals($document2['body']['actors'][1], 'Zendaya Maree Stoermer'); $this->assertEquals($document4['headers']['status-code'], 400); @@ -252,9 +267,8 @@ trait DatabaseBase 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ - 'orderField' => 'releaseYear', - 'orderType' => 'ASC', - 'orderCast' => 'int', + 'orderAttributes' => ['releaseYear'], + 'orderTypes' => ['ASC'], ]); $this->assertEquals(1944, $documents['body']['documents'][0]['releaseYear']); @@ -266,9 +280,8 @@ trait DatabaseBase 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ - 'orderField' => 'releaseYear', - 'orderType' => 'DESC', - 'orderCast' => 'int', + 'orderAttributes' => ['releaseYear'], + 'orderTypes' => ['DESC'], ]); $this->assertEquals(1944, $documents['body']['documents'][2]['releaseYear']); @@ -289,9 +302,8 @@ trait DatabaseBase 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'limit' => 1, - 'orderField' => 'releaseYear', - 'orderType' => 'ASC', - 'orderCast' => 'int', + 'orderAttributes' => ['releaseYear'], + 'orderTypes' => ['ASC'], ]); $this->assertEquals(1944, $documents['body']['documents'][0]['releaseYear']); @@ -303,9 +315,8 @@ trait DatabaseBase ], $this->getHeaders()), [ 'limit' => 2, 'offset' => 1, - 'orderField' => 'releaseYear', - 'orderType' => 'ASC', - 'orderCast' => 'int', + 'orderAttributes' => ['releaseYear'], + 'orderTypes' => ['ASC'], ]); $this->assertEquals(2017, $documents['body']['documents'][0]['releaseYear']); @@ -324,7 +335,7 @@ trait DatabaseBase 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ - 'search' => 'Captain America', + 'queries' => ['title.search("Captain America")'], ]); $this->assertEquals(1944, $documents['body']['documents'][0]['releaseYear']); @@ -334,7 +345,7 @@ trait DatabaseBase 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ - 'search' => 'Homecoming', + 'queries' => ['title.search("Homecoming")'], ]); $this->assertEquals(2017, $documents['body']['documents'][0]['releaseYear']); @@ -344,7 +355,7 @@ trait DatabaseBase 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ - 'search' => 'spider', + 'queries' => ['title.search("spider")'], ]); $this->assertEquals(2019, $documents['body']['documents'][0]['releaseYear']); @@ -353,52 +364,53 @@ trait DatabaseBase return []; } + // TODO@kodumbeats test for empty searches and misformatted queries /** * @depends testCreateDocument */ - public function testListDocumentsFilters(array $data):array - { - $documents = $this->client->call(Client::METHOD_GET, '/database/collections/' . $data['moviesId'] . '/documents', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'filters' => [ - 'actors.firstName=Tom' - ], - ]); + // public function testListDocumentsFilters(array $data):array + // { + // $documents = $this->client->call(Client::METHOD_GET, '/database/collections/' . $data['moviesId'] . '/documents', array_merge([ + // 'content-type' => 'application/json', + // 'x-appwrite-project' => $this->getProject()['$id'], + // ], $this->getHeaders()), [ + // 'filters' => [ + // 'actors.firstName=Tom' + // ], + // ]); - $this->assertCount(2, $documents['body']['documents']); - $this->assertEquals('Spider-Man: Far From Home', $documents['body']['documents'][0]['name']); - $this->assertEquals('Spider-Man: Homecoming', $documents['body']['documents'][1]['name']); + // $this->assertCount(2, $documents['body']['documents']); + // $this->assertEquals('Spider-Man: Far From Home', $documents['body']['documents'][0]['name']); + // $this->assertEquals('Spider-Man: Homecoming', $documents['body']['documents'][1]['name']); - $documents = $this->client->call(Client::METHOD_GET, '/database/collections/' . $data['moviesId'] . '/documents', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'filters' => [ - 'releaseYear=1944' - ], - ]); + // $documents = $this->client->call(Client::METHOD_GET, '/database/collections/' . $data['moviesId'] . '/documents', array_merge([ + // 'content-type' => 'application/json', + // 'x-appwrite-project' => $this->getProject()['$id'], + // ], $this->getHeaders()), [ + // 'filters' => [ + // 'releaseYear=1944' + // ], + // ]); - $this->assertCount(1, $documents['body']['documents']); - $this->assertEquals('Captain America', $documents['body']['documents'][0]['name']); + // $this->assertCount(1, $documents['body']['documents']); + // $this->assertEquals('Captain America', $documents['body']['documents'][0]['name']); - $documents = $this->client->call(Client::METHOD_GET, '/database/collections/' . $data['moviesId'] . '/documents', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'filters' => [ - 'releaseYear!=1944' - ], - ]); + // $documents = $this->client->call(Client::METHOD_GET, '/database/collections/' . $data['moviesId'] . '/documents', array_merge([ + // 'content-type' => 'application/json', + // 'x-appwrite-project' => $this->getProject()['$id'], + // ], $this->getHeaders()), [ + // 'filters' => [ + // 'releaseYear!=1944' + // ], + // ]); - $this->assertCount(2, $documents['body']['documents']); - $this->assertEquals('Spider-Man: Far From Home', $documents['body']['documents'][0]['name']); - $this->assertEquals('Spider-Man: Homecoming', $documents['body']['documents'][1]['name']); + // $this->assertCount(2, $documents['body']['documents']); + // $this->assertEquals('Spider-Man: Far From Home', $documents['body']['documents'][0]['name']); + // $this->assertEquals('Spider-Man: Homecoming', $documents['body']['documents'][1]['name']); - return []; - } + // return []; + // } /** * @depends testCreateDocument @@ -410,35 +422,34 @@ trait DatabaseBase 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'data' => [ - 'name' => 'Thor: Ragnaroc', + 'title' => 'Thor: Ragnaroc', 'releaseYear' => 2017, + 'actors' => [], ], - 'read' => ['user:'.$this->getUser()['$id'], 'testx'], - 'write' => ['user:'.$this->getUser()['$id'], 'testy'], + 'read' => ['user:'.$this->getUser()['$id'], 'user:testx'], + 'write' => ['user:'.$this->getUser()['$id'], 'user:testy'], ]); $id = $document['body']['$id']; $collection = $document['body']['$collection']; $this->assertEquals($document['headers']['status-code'], 201); - $this->assertEquals($document['body']['name'], 'Thor: Ragnaroc'); + $this->assertEquals($document['body']['title'], 'Thor: Ragnaroc'); $this->assertEquals($document['body']['releaseYear'], 2017); - $this->assertEquals($document['body']['$permissions']['read'][1], 'testx'); - $this->assertEquals($document['body']['$permissions']['write'][1], 'testy'); + $this->assertEquals($document['body']['$read'][1], 'user:testx'); + $this->assertEquals($document['body']['$write'][1], 'user:testy'); $document = $this->client->call(Client::METHOD_PATCH, '/database/collections/' . $collection . '/documents/' . $id, array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'data' => [ - 'name' => 'Thor: Ragnarok' + 'title' => 'Thor: Ragnarok', ], - 'read' => ['user:'.$this->getUser()['$id']], - 'write' => ['user:'.$this->getUser()['$id']], ]); $this->assertEquals($document['headers']['status-code'], 200); - $this->assertEquals($document['body']['name'], 'Thor: Ragnarok'); + $this->assertEquals($document['body']['title'], 'Thor: Ragnarok'); $this->assertEquals($document['body']['releaseYear'], 2017); $document = $this->client->call(Client::METHOD_GET, '/database/collections/' . $collection . '/documents/' . $id, array_merge([ @@ -450,7 +461,7 @@ trait DatabaseBase $collection = $document['body']['$collection']; $this->assertEquals($document['headers']['status-code'], 200); - $this->assertEquals($document['body']['name'], 'Thor: Ragnarok'); + $this->assertEquals($document['body']['title'], 'Thor: Ragnarok'); $this->assertEquals($document['body']['releaseYear'], 2017); return []; @@ -466,8 +477,9 @@ trait DatabaseBase 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'data' => [ - 'name' => 'Thor: Ragnarok', + 'title' => 'Thor: Ragnarok', 'releaseYear' => 2017, + 'actors' => [], ], 'read' => ['user:'.$this->getUser()['$id']], 'write' => ['user:'.$this->getUser()['$id']], @@ -512,7 +524,7 @@ trait DatabaseBase 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'data' => [ - 'name' => 'Captain America', + 'title' => 'Captain America', 'releaseYear' => 1944, 'actors' => [], ], @@ -522,24 +534,23 @@ trait DatabaseBase $this->assertEquals($document['headers']['status-code'], 201); $this->assertEquals($document['body']['$collection'], $data['moviesId']); - $this->assertEquals($document['body']['name'], 'Captain America'); + $this->assertEquals($document['body']['title'], 'Captain America'); $this->assertEquals($document['body']['releaseYear'], 1944); - $this->assertIsArray($document['body']['$permissions']); - $this->assertIsArray($document['body']['$permissions']['read']); - $this->assertIsArray($document['body']['$permissions']['write']); + $this->assertIsArray($document['body']['$read']); + $this->assertIsArray($document['body']['$write']); if($this->getSide() == 'client') { - $this->assertCount(1, $document['body']['$permissions']['read']); - $this->assertCount(1, $document['body']['$permissions']['write']); - $this->assertEquals(['user:'.$this->getUser()['$id']], $document['body']['$permissions']['read']); - $this->assertEquals(['user:'.$this->getUser()['$id']], $document['body']['$permissions']['write']); + $this->assertCount(1, $document['body']['$read']); + $this->assertCount(1, $document['body']['$write']); + $this->assertEquals(['user:'.$this->getUser()['$id']], $document['body']['$read']); + $this->assertEquals(['user:'.$this->getUser()['$id']], $document['body']['$write']); } if($this->getSide() == 'server') { - $this->assertCount(0, $document['body']['$permissions']['read']); - $this->assertCount(0, $document['body']['$permissions']['write']); - $this->assertEquals([], $document['body']['$permissions']['read']); - $this->assertEquals([], $document['body']['$permissions']['write']); + $this->assertCount(0, $document['body']['$read']); + $this->assertCount(0, $document['body']['$write']); + $this->assertEquals([], $document['body']['$read']); + $this->assertEquals([], $document['body']['$write']); } // Updated and Inherit Permissions @@ -549,7 +560,7 @@ trait DatabaseBase 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'data' => [ - 'name' => 'Captain America 2', + 'title' => 'Captain America 2', 'releaseYear' => 1945, 'actors' => [], ], @@ -557,21 +568,21 @@ trait DatabaseBase ]); $this->assertEquals($document['headers']['status-code'], 200); - $this->assertEquals($document['body']['name'], 'Captain America 2'); + $this->assertEquals($document['body']['title'], 'Captain America 2'); $this->assertEquals($document['body']['releaseYear'], 1945); if($this->getSide() == 'client') { - $this->assertCount(1, $document['body']['$permissions']['read']); - $this->assertCount(1, $document['body']['$permissions']['write']); - $this->assertEquals(['role:all'], $document['body']['$permissions']['read']); - $this->assertEquals(['user:'.$this->getUser()['$id']], $document['body']['$permissions']['write']); + $this->assertCount(1, $document['body']['$read']); + $this->assertCount(1, $document['body']['$write']); + $this->assertEquals(['role:all'], $document['body']['$read']); + $this->assertEquals(['user:'.$this->getUser()['$id']], $document['body']['$write']); } if($this->getSide() == 'server') { - $this->assertCount(1, $document['body']['$permissions']['read']); - $this->assertCount(0, $document['body']['$permissions']['write']); - $this->assertEquals(['role:all'], $document['body']['$permissions']['read']); - $this->assertEquals([], $document['body']['$permissions']['write']); + $this->assertCount(1, $document['body']['$read']); + $this->assertCount(0, $document['body']['$write']); + $this->assertEquals(['role:all'], $document['body']['$read']); + $this->assertEquals([], $document['body']['$write']); } $document = $this->client->call(Client::METHOD_GET, '/database/collections/' . $data['moviesId'] . '/documents/' . $id, array_merge([ @@ -580,21 +591,21 @@ trait DatabaseBase ], $this->getHeaders())); $this->assertEquals($document['headers']['status-code'], 200); - $this->assertEquals($document['body']['name'], 'Captain America 2'); + $this->assertEquals($document['body']['title'], 'Captain America 2'); $this->assertEquals($document['body']['releaseYear'], 1945); if($this->getSide() == 'client') { - $this->assertCount(1, $document['body']['$permissions']['read']); - $this->assertCount(1, $document['body']['$permissions']['write']); - $this->assertEquals(['role:all'], $document['body']['$permissions']['read']); - $this->assertEquals(['user:'.$this->getUser()['$id']], $document['body']['$permissions']['write']); + $this->assertCount(1, $document['body']['$read']); + $this->assertCount(1, $document['body']['$write']); + $this->assertEquals(['role:all'], $document['body']['$read']); + $this->assertEquals(['user:'.$this->getUser()['$id']], $document['body']['$write']); } if($this->getSide() == 'server') { - $this->assertCount(1, $document['body']['$permissions']['read']); - $this->assertCount(0, $document['body']['$permissions']['write']); - $this->assertEquals(['role:all'], $document['body']['$permissions']['read']); - $this->assertEquals([], $document['body']['$permissions']['write']); + $this->assertCount(1, $document['body']['$read']); + $this->assertCount(0, $document['body']['$write']); + $this->assertEquals(['role:all'], $document['body']['$read']); + $this->assertEquals([], $document['body']['$write']); } // Reset Permissions @@ -604,7 +615,7 @@ trait DatabaseBase 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'data' => [ - 'name' => 'Captain America 3', + 'title' => 'Captain America 3', 'releaseYear' => 1946, 'actors' => [], ], @@ -618,12 +629,12 @@ trait DatabaseBase if($this->getSide() == 'server') { $this->assertEquals($document['headers']['status-code'], 200); - $this->assertEquals($document['body']['name'], 'Captain America 3'); + $this->assertEquals($document['body']['title'], 'Captain America 3'); $this->assertEquals($document['body']['releaseYear'], 1946); - $this->assertCount(0, $document['body']['$permissions']['read']); - $this->assertCount(0, $document['body']['$permissions']['write']); - $this->assertEquals([], $document['body']['$permissions']['read']); - $this->assertEquals([], $document['body']['$permissions']['write']); + $this->assertCount(0, $document['body']['$read']); + $this->assertCount(0, $document['body']['$write']); + $this->assertEquals([], $document['body']['$read']); + $this->assertEquals([], $document['body']['$write']); } return $data; diff --git a/tests/e2e/Services/Database/DatabaseCustomServerTest.php b/tests/e2e/Services/Database/DatabaseCustomServerTest.php index 5fdc87689..9be5bcbf3 100644 --- a/tests/e2e/Services/Database/DatabaseCustomServerTest.php +++ b/tests/e2e/Services/Database/DatabaseCustomServerTest.php @@ -27,34 +27,49 @@ class DatabaseCustomServerTest extends Scope ]), [ 'name' => 'Actors', 'read' => ['role:all'], - 'write' => ['role:member', 'role:admin'], - 'rules' => [ - [ - 'label' => 'First Name', - 'key' => 'firstName', - 'type' => 'text', - 'default' => '', - 'required' => true, - 'array' => false - ], - [ - 'label' => 'Last Name', - 'key' => 'lastName', - 'type' => 'text', - 'default' => '', - 'required' => true, - 'array' => false - ], - ], + 'write' => ['role:all'], ]); $this->assertEquals($actors['headers']['status-code'], 201); $this->assertEquals($actors['body']['name'], 'Actors'); - $this->assertIsArray($actors['body']['$permissions']); - $this->assertIsArray($actors['body']['$permissions']['read']); - $this->assertIsArray($actors['body']['$permissions']['write']); - $this->assertCount(1, $actors['body']['$permissions']['read']); - $this->assertCount(2, $actors['body']['$permissions']['write']); + + $firstName = $this->client->call(Client::METHOD_POST, '/database/collections/' . $actors['body']['$id'] . '/attributes', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'id' => 'firstName', + 'type' => 'string', + 'size' => 256, + 'required' => true, + ]); + + $lastName = $this->client->call(Client::METHOD_POST, '/database/collections/' . $actors['body']['$id'] . '/attributes', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'id' => 'lastName', + 'type' => 'string', + 'size' => 256, + 'required' => true, + ]); + + // wait for database worker to finish creating attributes + sleep(5); + + $collection = $this->client->call(Client::METHOD_GET, '/database/collections/' . $actors['body']['$id'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), []); + + $this->assertEquals($collection['body']['$id'], $firstName['body']['$collection']); + $this->assertEquals($collection['body']['$id'], $lastName['body']['$collection']); + $this->assertIsArray($collection['body']['attributes']); + $this->assertCount(2, $collection['body']['attributes']); + $this->assertEquals($collection['body']['attributes'][0]['$id'], $firstName['body']['$id']); + $this->assertEquals($collection['body']['attributes'][1]['$id'], $lastName['body']['$id']); // Add Documents to the collection $document1 = $this->client->call(Client::METHOD_POST, '/database/collections/' . $actors['body']['$id'] . '/documents', array_merge([ @@ -80,24 +95,22 @@ class DatabaseCustomServerTest extends Scope 'read' => ['user:'.$this->getUser()['$id']], 'write' => ['user:'.$this->getUser()['$id']], ]); - + $this->assertEquals($document1['headers']['status-code'], 201); $this->assertEquals($document1['body']['$collection'], $actors['body']['$id']); - $this->assertIsArray($document1['body']['$permissions']); - $this->assertIsArray($document1['body']['$permissions']['read']); - $this->assertIsArray($document1['body']['$permissions']['write']); - $this->assertCount(1, $document1['body']['$permissions']['read']); - $this->assertCount(1, $document1['body']['$permissions']['write']); + $this->assertIsArray($document1['body']['$read']); + $this->assertIsArray($document1['body']['$write']); + $this->assertCount(1, $document1['body']['$read']); + $this->assertCount(1, $document1['body']['$write']); $this->assertEquals($document1['body']['firstName'], 'Tom'); $this->assertEquals($document1['body']['lastName'], 'Holland'); $this->assertEquals($document2['headers']['status-code'], 201); $this->assertEquals($document2['body']['$collection'], $actors['body']['$id']); - $this->assertIsArray($document2['body']['$permissions']); - $this->assertIsArray($document2['body']['$permissions']['read']); - $this->assertIsArray($document2['body']['$permissions']['write']); - $this->assertCount(1, $document2['body']['$permissions']['read']); - $this->assertCount(1, $document2['body']['$permissions']['write']); + $this->assertIsArray($document2['body']['$read']); + $this->assertIsArray($document2['body']['$write']); + $this->assertCount(1, $document2['body']['$read']); + $this->assertCount(1, $document2['body']['$write']); $this->assertEquals($document2['body']['firstName'], 'Samuel'); $this->assertEquals($document2['body']['lastName'], 'Jackson'); diff --git a/tests/e2e/Services/Webhooks/WebhooksBase.php b/tests/e2e/Services/Webhooks/WebhooksBase.php index dfbdc524c..bda83bb06 100644 --- a/tests/e2e/Services/Webhooks/WebhooksBase.php +++ b/tests/e2e/Services/Webhooks/WebhooksBase.php @@ -20,24 +20,6 @@ trait WebhooksBase 'name' => 'Actors', 'read' => ['role:all'], 'write' => ['role:all'], - 'rules' => [ - [ - 'label' => 'First Name', - 'key' => 'firstName', - 'type' => 'text', - 'default' => '', - 'required' => true, - 'array' => false - ], - [ - 'label' => 'Last Name', - 'key' => 'lastName', - 'type' => 'text', - 'default' => '', - 'required' => true, - 'array' => false - ], - ], ]); $this->assertEquals($actors['headers']['status-code'], 201); @@ -55,12 +37,10 @@ trait WebhooksBase $this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), true); $this->assertNotEmpty($webhook['data']['$id']); $this->assertEquals($webhook['data']['name'], 'Actors'); - $this->assertIsArray($webhook['data']['$permissions']); $this->assertIsArray($webhook['data']['$read']); $this->assertIsArray($webhook['data']['$write']); $this->assertCount(1, $webhook['data']['$read']); $this->assertCount(1, $webhook['data']['$write']); - $this->assertCount(2, $webhook['data']['rules']); return array_merge(['actorsId' => $actors['body']['$id']]); } @@ -68,6 +48,60 @@ trait WebhooksBase /** * @depends testCreateCollection */ + public function testCreateAttributes(array $data): array + { + $firstName = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['actorsId'] . '/attributes', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'id' => 'firstName', + 'type' => 'string', + 'size' => 256, + 'required' => true, + ]); + + $lastName = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['actorsId'] . '/attributes', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'id' => 'lastName', + 'type' => 'string', + 'size' => 256, + 'required' => true, + ]); + + $this->assertEquals($firstName['headers']['status-code'], 201); + $this->assertEquals($firstName['body']['$collection'], $data['actorsId']); + $this->assertEquals($firstName['body']['$id'], 'firstName'); + $this->assertEquals($lastName['headers']['status-code'], 201); + $this->assertEquals($lastName['body']['$collection'], $data['actorsId']); + $this->assertEquals($lastName['body']['$id'], 'lastName'); + + // wait for database worker to kick in + sleep(10); + + $webhook = $this->getLastRequest(); + + $this->assertEquals($webhook['method'], 'POST'); + $this->assertEquals($webhook['headers']['Content-Type'], 'application/json'); + $this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io'); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Event'], 'database.attributes.create'); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented'); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']); + $this->assertNotEmpty($webhook['data']['$id']); + $this->assertEquals($webhook['data']['$id'], 'lastName'); + + // TODO@kodumbeats test webhook for removing attribute + + return $data; + } + + /** + * @depends testCreateAttributes + */ public function testCreateDocument(array $data): array { $document = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['actorsId'] . '/documents', array_merge([ diff --git a/tests/e2e/Services/Webhooks/WebhooksCustomServerTest.php b/tests/e2e/Services/Webhooks/WebhooksCustomServerTest.php index dbb44e0de..4e4c768e6 100644 --- a/tests/e2e/Services/Webhooks/WebhooksCustomServerTest.php +++ b/tests/e2e/Services/Webhooks/WebhooksCustomServerTest.php @@ -15,7 +15,7 @@ class WebhooksCustomServerTest extends Scope use SideServer; /** - * @depends testCreateCollection + * @depends testCreateAttributes */ public function testUpdateCollection($data): array { @@ -28,26 +28,6 @@ class WebhooksCustomServerTest extends Scope 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'name' => 'Actors1', - 'read' => ['role:all'], - 'write' => ['role:all'], - 'rules' => [ - [ - 'label' => 'First Name', - 'key' => 'firstName', - 'type' => 'text', - 'default' => '', - 'required' => true, - 'array' => false - ], - [ - 'label' => 'Last Name', - 'key' => 'lastName', - 'type' => 'text', - 'default' => '', - 'required' => true, - 'array' => false - ], - ], ]); $this->assertEquals($actors['headers']['status-code'], 200); @@ -65,16 +45,73 @@ class WebhooksCustomServerTest extends Scope $this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), true); $this->assertNotEmpty($webhook['data']['$id']); $this->assertEquals($webhook['data']['name'], 'Actors1'); - $this->assertIsArray($webhook['data']['$permissions']); - $this->assertIsArray($webhook['data']['$permissions']['read']); - $this->assertIsArray($webhook['data']['$permissions']['write']); - $this->assertCount(1, $webhook['data']['$permissions']['read']); - $this->assertCount(1, $webhook['data']['$permissions']['write']); - $this->assertCount(2, $webhook['data']['rules']); + $this->assertIsArray($webhook['data']['$read']); + $this->assertIsArray($webhook['data']['$write']); + $this->assertCount(1, $webhook['data']['$read']); + $this->assertCount(1, $webhook['data']['$write']); return array_merge(['actorsId' => $actors['body']['$id']]); } + /** + * @depends testCreateAttributes + */ + public function testCreateDeleteIndexes($data): array + { + $index = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['actorsId'] . '/indexes', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'id' => 'fullname', + 'type' => 'text', + 'attributes' => ['lastName', 'firstName'], + 'orders' => ['ASC', 'ASC'], + ]); + + $this->assertEquals($index['headers']['status-code'], 201); + $this->assertEquals($index['body']['$collection'], $data['actorsId']); + $this->assertEquals($index['body']['$id'], 'fullname'); + + // wait for database worker to create index + sleep(5); + + $webhook = $this->getLastRequest(); + + $this->assertEquals($webhook['method'], 'POST'); + $this->assertEquals($webhook['headers']['Content-Type'], 'application/json'); + $this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io'); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Event'], 'database.indexes.create'); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented'); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']); + $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']); + $this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), true); + + // TODO@kodumbeats test for indexes.delete + // Remove index + // $index = $this->client->call(Client::METHOD_DELETE, '/database/collections/' . $data['actorsId'] . '/indexes/' . $index['body']['$id'], array_merge([ + // 'content-type' => 'application/json', + // 'x-appwrite-project' => $this->getProject()['$id'], + // 'x-appwrite-key' => $this->getProject()['apiKey'] + // ])); + + // // wait for database worker to remove index + // sleep(5); + + // $webhook = $this->getLastRequest(); + + // // $this->assertEquals($webhook['method'], 'DELETE'); + // $this->assertEquals($webhook['headers']['Content-Type'], 'application/json'); + // $this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io'); + // $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Event'], 'database.indexes.delete'); + // $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], 'not-yet-implemented'); + // $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']); + // $this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']); + // $this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), true); + + return $data; + } + public function testDeleteCollection(): array { /** @@ -88,24 +125,6 @@ class WebhooksCustomServerTest extends Scope 'name' => 'Demo', 'read' => ['role:all'], 'write' => ['role:all'], - 'rules' => [ - [ - 'label' => 'First Name', - 'key' => 'firstName', - 'type' => 'text', - 'default' => '', - 'required' => true, - 'array' => false - ], - [ - 'label' => 'Last Name', - 'key' => 'lastName', - 'type' => 'text', - 'default' => '', - 'required' => true, - 'array' => false - ], - ], ]); $this->assertEquals($actors['headers']['status-code'], 201); @@ -131,12 +150,10 @@ class WebhooksCustomServerTest extends Scope $this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), true); $this->assertNotEmpty($webhook['data']['$id']); $this->assertEquals($webhook['data']['name'], 'Demo'); - $this->assertIsArray($webhook['data']['$permissions']); - $this->assertIsArray($webhook['data']['$permissions']['read']); - $this->assertIsArray($webhook['data']['$permissions']['write']); - $this->assertCount(1, $webhook['data']['$permissions']['read']); - $this->assertCount(1, $webhook['data']['$permissions']['write']); - $this->assertCount(2, $webhook['data']['rules']); + $this->assertIsArray($webhook['data']['$read']); + $this->assertIsArray($webhook['data']['$write']); + $this->assertCount(1, $webhook['data']['$read']); + $this->assertCount(1, $webhook['data']['$write']); return []; } @@ -306,8 +323,8 @@ class WebhooksCustomServerTest extends Scope 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'name' => 'Test', - 'env' => 'php-8.0', - 'runtime' => ['role:all'], + 'execute' => ['role:all'], + 'runtime' => 'php-8.0', 'timeout' => 10, ]);