From c2157110f569799a7532514a8cac49bf3a8945fd Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Tue, 20 Jul 2021 16:44:13 -0400 Subject: [PATCH 01/21] Use multi-arch swagger-validator build --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 32ec332314..c7dbc308d4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -509,7 +509,7 @@ services: - appwrite swagger-validator: - image: 'swaggerapi/swagger-validator-v2:v2.0.5' + image: 'kodumbeats/swagger-validator-v2:v2.0.5' container_name: appwrite-swagger-validator ports: - '9506:8080' From 9ac2a4f94cd91dd977406d02433a5b17b95be73d Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Wed, 21 Jul 2021 09:22:34 -0400 Subject: [PATCH 02/21] Remove outdated comments --- app/controllers/api/database.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/controllers/api/database.php b/app/controllers/api/database.php index 92a4f802f7..b8c2f6ab8d 100644 --- a/app/controllers/api/database.php +++ b/app/controllers/api/database.php @@ -456,9 +456,7 @@ App::post('/v1/database/collections/:collectionId/indexes') ->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') From 6fc5bf0a71cf63755cebd5e1b31edeb78a8b9267 Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Wed, 21 Jul 2021 09:23:12 -0400 Subject: [PATCH 03/21] Add createAttribute routes for each primitive type --- app/controllers/api/database.php | 254 +++++++++++++++++++++++++++++-- 1 file changed, 239 insertions(+), 15 deletions(-) diff --git a/app/controllers/api/database.php b/app/controllers/api/database.php index b8c2f6ab8d..db715f5234 100644 --- a/app/controllers/api/database.php +++ b/app/controllers/api/database.php @@ -3,11 +3,11 @@ use Utopia\App; use Utopia\Exception; use Utopia\Validator\Boolean; +use Utopia\Validator\FloatValidator as Float; 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; @@ -224,38 +224,115 @@ App::delete('/v1/database/collections/:collectionId') $response->noContent(); }); -App::post('/v1/database/collections/:collectionId/attributes') - ->desc('Create Attribute') +App::post('/v1/database/collections/:collectionId/attributes/string') + ->desc('Create String 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.method', 'createStringAttribute') + ->label('sdk.description', '/docs/references/database/create-attribute-string.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('attributeId', '', new Key(), 'Attribute ID.') + ->param('size', null, new Integer(), 'Attribute size for text attributes, in number of characters.') ->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('default', null, new Text(0), '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) { + ->action(function ($collectionId, $attributeId, $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 */ + $type = Database::VAR_STRING; + + $collection = $dbForExternal->getCollection($collectionId); + + if ($collection->isEmpty()) { + throw new Exception('Collection not found', 404); + } + + // TODO@kodumbeats how to depend on $size for Text validator length + // Ensure attribute default is within required size + $validator = new Text($size); + if (!$validator->isValid($default)) { + throw new Exception('Length of default attribute exceeds attribute size', 400); + } + + // integers are signed by default, and filters are hidden from the endpoint. + $signed = true; + $filters = []; + + $success = $dbForExternal->addAttributeInQueue($collectionId, $attributeId, $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' => $attributeId, + '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::post('/v1/database/collections/:collectionId/attributes/integer') + ->desc('Create Integer 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', 'createIntegerAttribute') + ->label('sdk.description', '/docs/references/database/create-attribute-integer.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).') + ->param('attributeId', '', new Key(), 'Attribute ID.') + ->param('required', null, new Boolean(), 'Is attribute required?') + ->param('default', null, new Integer(), '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, $attributeId, $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 */ + + $type = Database::VAR_INTEGER; + $collection = $dbForExternal->getCollection($collectionId); if ($collection->isEmpty()) { @@ -264,9 +341,10 @@ App::post('/v1/database/collections/:collectionId/attributes') // integers are signed by default, and filters are hidden from the endpoint. $signed = true; + $size = 0; $filters = []; - $success = $dbForExternal->addAttributeInQueue($collectionId, $id, $type, $size, $required, $default, $signed, $array, $filters); + $success = $dbForExternal->addAttributeInQueue($collectionId, $attributeId, $type, $size, $required, $default, $signed, $array, $filters); // Database->addAttributeInQueue() does not return a document // So we need to create one for the response @@ -274,7 +352,153 @@ App::post('/v1/database/collections/:collectionId/attributes') // TODO@kodumbeats should $signed and $filters be part of the response model? $attribute = new Document([ '$collection' => $collectionId, - '$id' => $id, + '$id' => $attributeId, + '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::post('/v1/database/collections/:collectionId/attributes/float') + ->desc('Create Float 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', 'createFloatAttribute') + ->label('sdk.description', '/docs/references/database/create-attribute-integer.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).') + ->param('attributeId', '', new Key(), 'Attribute ID.') + ->param('required', null, new Boolean(), 'Is attribute required?') + ->param('default', null, new Float(), '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, $attributeId, $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 */ + + $type = Database::VAR_FLOAT; + + $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; + $size = 0; + $filters = []; + + $success = $dbForExternal->addAttributeInQueue($collectionId, $attributeId, $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' => $attributeId, + '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::post('/v1/database/collections/:collectionId/attributes/boolean') + ->desc('Create Boolean 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', 'createBooleanAttribute') + ->label('sdk.description', '/docs/references/database/create-attribute-boolean.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).') + ->param('attributeId', '', new Key(), 'Attribute ID.') + ->param('required', null, new Boolean(), 'Is attribute required?') + ->param('default', null, new Boolean(), '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, $attributeId, $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 */ + + $type = Database::VAR_BOOLEAN; + + $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; + $size = 0; + $filters = []; + + $success = $dbForExternal->addAttributeInQueue($collectionId, $attributeId, $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' => $attributeId, 'type' => $type, 'size' => $size, 'required' => $required, From b64202043de87da12b6d84a454723bbd047b898b Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Wed, 21 Jul 2021 10:26:08 -0400 Subject: [PATCH 04/21] Renamed import due to reserved keyword conflicts --- app/controllers/api/database.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/api/database.php b/app/controllers/api/database.php index db715f5234..279bd2ae71 100644 --- a/app/controllers/api/database.php +++ b/app/controllers/api/database.php @@ -3,7 +3,7 @@ use Utopia\App; use Utopia\Exception; use Utopia\Validator\Boolean; -use Utopia\Validator\FloatValidator as Float; +use Utopia\Validator\FloatValidator; use Utopia\Validator\Integer; use Utopia\Validator\Numeric; use Utopia\Validator\Range; @@ -392,7 +392,7 @@ App::post('/v1/database/collections/:collectionId/attributes/float') ->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.') ->param('required', null, new Boolean(), 'Is attribute required?') - ->param('default', null, new Float(), 'Default value for attribute when not provided. Cannot be set when attribute is required.', true) + ->param('default', null, new FloatValidator(), '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') From 97b7c863a8180132da0f40ca9581ca21e95ad07b Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Wed, 21 Jul 2021 10:26:50 -0400 Subject: [PATCH 05/21] Validate default value only when not null --- app/controllers/api/database.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api/database.php b/app/controllers/api/database.php index 279bd2ae71..13db4dc5a4 100644 --- a/app/controllers/api/database.php +++ b/app/controllers/api/database.php @@ -263,7 +263,7 @@ App::post('/v1/database/collections/:collectionId/attributes/string') // TODO@kodumbeats how to depend on $size for Text validator length // Ensure attribute default is within required size $validator = new Text($size); - if (!$validator->isValid($default)) { + if (!\is_null($default) && !$validator->isValid($default)) { throw new Exception('Length of default attribute exceeds attribute size', 400); } From 92175c5e3244b2e916efb8066174c8b48b636363 Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Wed, 21 Jul 2021 11:05:18 -0400 Subject: [PATCH 06/21] Adjust tests for new attribute routes --- tests/e2e/Services/Database/DatabaseBase.php | 15 ++++++--------- .../Database/DatabaseCustomServerTest.php | 10 ++++------ tests/e2e/Services/Webhooks/WebhooksBase.php | 10 ++++------ 3 files changed, 14 insertions(+), 21 deletions(-) diff --git a/tests/e2e/Services/Database/DatabaseBase.php b/tests/e2e/Services/Database/DatabaseBase.php index 21f22ba7a0..9bcfb6102f 100644 --- a/tests/e2e/Services/Database/DatabaseBase.php +++ b/tests/e2e/Services/Database/DatabaseBase.php @@ -32,35 +32,32 @@ trait DatabaseBase */ public function testCreateAttributes(array $data): array { - $title = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['moviesId'] . '/attributes', array_merge([ + $title = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['moviesId'] . '/attributes/string', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ - 'id' => 'title', - 'type' => 'string', + 'attributeId' => 'title', 'size' => 256, 'required' => true, ]); - $releaseYear = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['moviesId'] . '/attributes', array_merge([ + $releaseYear = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['moviesId'] . '/attributes/integer', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ - 'id' => 'releaseYear', - 'type' => 'integer', + 'attributeId' => 'releaseYear', 'size' => 0, 'required' => true, ]); - $actors = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['moviesId'] . '/attributes', array_merge([ + $actors = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['moviesId'] . '/attributes/string', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ - 'id' => 'actors', - 'type' => 'string', + 'attributeId' => 'actors', 'size' => 256, 'required' => false, 'default' => null, diff --git a/tests/e2e/Services/Database/DatabaseCustomServerTest.php b/tests/e2e/Services/Database/DatabaseCustomServerTest.php index 9be5bcbf3b..4b63a03df6 100644 --- a/tests/e2e/Services/Database/DatabaseCustomServerTest.php +++ b/tests/e2e/Services/Database/DatabaseCustomServerTest.php @@ -33,24 +33,22 @@ class DatabaseCustomServerTest extends Scope $this->assertEquals($actors['headers']['status-code'], 201); $this->assertEquals($actors['body']['name'], 'Actors'); - $firstName = $this->client->call(Client::METHOD_POST, '/database/collections/' . $actors['body']['$id'] . '/attributes', array_merge([ + $firstName = $this->client->call(Client::METHOD_POST, '/database/collections/' . $actors['body']['$id'] . '/attributes/string', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ - 'id' => 'firstName', - 'type' => 'string', + 'attributeId' => 'firstName', 'size' => 256, 'required' => true, ]); - $lastName = $this->client->call(Client::METHOD_POST, '/database/collections/' . $actors['body']['$id'] . '/attributes', array_merge([ + $lastName = $this->client->call(Client::METHOD_POST, '/database/collections/' . $actors['body']['$id'] . '/attributes/string', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ - 'id' => 'lastName', - 'type' => 'string', + 'attributeId' => 'lastName', 'size' => 256, 'required' => true, ]); diff --git a/tests/e2e/Services/Webhooks/WebhooksBase.php b/tests/e2e/Services/Webhooks/WebhooksBase.php index bda83bb06c..9a2db5d9f4 100644 --- a/tests/e2e/Services/Webhooks/WebhooksBase.php +++ b/tests/e2e/Services/Webhooks/WebhooksBase.php @@ -50,24 +50,22 @@ trait WebhooksBase */ public function testCreateAttributes(array $data): array { - $firstName = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['actorsId'] . '/attributes', array_merge([ + $firstName = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['actorsId'] . '/attributes/string', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ - 'id' => 'firstName', - 'type' => 'string', + 'attributeId' => 'firstName', 'size' => 256, 'required' => true, ]); - $lastName = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['actorsId'] . '/attributes', array_merge([ + $lastName = $this->client->call(Client::METHOD_POST, '/database/collections/' . $data['actorsId'] . '/attributes/string', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ - 'id' => 'lastName', - 'type' => 'string', + 'attributeId' => 'lastName', 'size' => 256, 'required' => true, ]); From 2f13542d085a995245ff4319260ec376ab41f5b4 Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Wed, 21 Jul 2021 11:50:37 -0400 Subject: [PATCH 07/21] fix(deps) composer update --- composer.lock | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/composer.lock b/composer.lock index 4a34e6accb..d81ddb33ba 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": "ea49a8cc41d837be81393c1457051bcb", + "content-hash": "17c1c46d283e4745df9915d082c22078", "packages": [ { "name": "adhocore/jwt", @@ -2079,16 +2079,16 @@ }, { "name": "utopia-php/framework", - "version": "0.16.1", + "version": "0.16.2", "source": { "type": "git", "url": "https://github.com/utopia-php/framework.git", - "reference": "e7440b91077ccf2f4f3908fde4f31d177f8ea692" + "reference": "df02354a670df366b92e2e927fbf128be9a8e64e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/framework/zipball/e7440b91077ccf2f4f3908fde4f31d177f8ea692", - "reference": "e7440b91077ccf2f4f3908fde4f31d177f8ea692", + "url": "https://api.github.com/repos/utopia-php/framework/zipball/df02354a670df366b92e2e927fbf128be9a8e64e", + "reference": "df02354a670df366b92e2e927fbf128be9a8e64e", "shasum": "" }, "require": { @@ -2122,9 +2122,9 @@ ], "support": { "issues": "https://github.com/utopia-php/framework/issues", - "source": "https://github.com/utopia-php/framework/tree/0.16.1" + "source": "https://github.com/utopia-php/framework/tree/0.16.2" }, - "time": "2021-07-18T10:59:00+00:00" + "time": "2021-07-20T10:24:56+00:00" }, { "name": "utopia-php/image", @@ -3367,16 +3367,16 @@ }, { "name": "nikic/php-parser", - "version": "v4.11.0", + "version": "v4.12.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "fe14cf3672a149364fb66dfe11bf6549af899f94" + "reference": "6608f01670c3cc5079e18c1dab1104e002579143" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/fe14cf3672a149364fb66dfe11bf6549af899f94", - "reference": "fe14cf3672a149364fb66dfe11bf6549af899f94", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/6608f01670c3cc5079e18c1dab1104e002579143", + "reference": "6608f01670c3cc5079e18c1dab1104e002579143", "shasum": "" }, "require": { @@ -3417,9 +3417,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.11.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.12.0" }, - "time": "2021-07-03T13:36:55+00:00" + "time": "2021-07-21T10:44:31+00:00" }, { "name": "openlss/lib-array2xml", @@ -3476,16 +3476,16 @@ }, { "name": "phar-io/manifest", - "version": "2.0.1", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/phar-io/manifest.git", - "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133" + "reference": "97803eca37d319dfa7826cc2437fc020857acb53" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/85265efd3af7ba3ca4b2a2c34dbfc5788dd29133", - "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", + "reference": "97803eca37d319dfa7826cc2437fc020857acb53", "shasum": "" }, "require": { @@ -3530,9 +3530,9 @@ "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", "support": { "issues": "https://github.com/phar-io/manifest/issues", - "source": "https://github.com/phar-io/manifest/tree/master" + "source": "https://github.com/phar-io/manifest/tree/2.0.3" }, - "time": "2020-06-27T14:33:11+00:00" + "time": "2021-07-20T11:28:43+00:00" }, { "name": "phar-io/version", @@ -6258,5 +6258,5 @@ "platform-overrides": { "php": "8.0" }, - "plugin-api-version": "2.0.0" + "plugin-api-version": "2.1.0" } From aea2d2ffa58fd93bd310a44217002675792e2a15 Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Thu, 22 Jul 2021 16:38:42 -0400 Subject: [PATCH 08/21] Accept format on createAttribute for document structure validation --- app/controllers/api/database.php | 79 +++++++++++++++++++++++++++++--- app/workers/database.php | 3 +- 2 files changed, 74 insertions(+), 8 deletions(-) diff --git a/app/controllers/api/database.php b/app/controllers/api/database.php index 13db4dc5a4..07f9b1fb95 100644 --- a/app/controllers/api/database.php +++ b/app/controllers/api/database.php @@ -18,6 +18,9 @@ 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\Network\Validator\Email; +use Appwrite\Network\Validator\IP; +use Appwrite\Network\Validator\URL; use Appwrite\Utopia\Response; use Utopia\Database\Database; use Utopia\Database\Document; @@ -241,12 +244,13 @@ App::post('/v1/database/collections/:collectionId/attributes/string') ->param('size', null, new Integer(), 'Attribute size for text attributes, in number of characters.') ->param('required', null, new Boolean(), 'Is attribute required?') ->param('default', null, new Text(0), 'Default value for attribute when not provided. Cannot be set when attribute is required.', true) + ->param('format', null, new Whitelist(['email', 'ip', 'url']), 'Optional format validation of attribute. Must be one of (email, ip, url).', true) ->param('array', false, new Boolean(), 'Is attribute an array?', true) ->inject('response') ->inject('dbForExternal') ->inject('database') ->inject('audits') - ->action(function ($collectionId, $attributeId, $size, $required, $default, $array, $response, $dbForExternal, $database, $audits) { + ->action(function ($collectionId, $attributeId, $size, $required, $default, $format, $array, $response, $dbForExternal, $database, $audits) { /** @var Appwrite\Utopia\Response $response */ /** @var Utopia\Database\Database $dbForExternal*/ /** @var Appwrite\Event\Event $database */ @@ -271,7 +275,7 @@ App::post('/v1/database/collections/:collectionId/attributes/string') $signed = true; $filters = []; - $success = $dbForExternal->addAttributeInQueue($collectionId, $attributeId, $type, $size, $required, $default, $signed, $array, $filters); + $success = $dbForExternal->addAttributeInQueue($collectionId, $attributeId, $type, $size, $required, $default, $signed, $array, $format, $filters); // Database->addAttributeInQueue() does not return a document // So we need to create one for the response @@ -286,6 +290,7 @@ App::post('/v1/database/collections/:collectionId/attributes/string') 'default' => $default, 'signed' => $signed, 'array' => $array, + 'format' => $format, 'filters' => $filters ]); @@ -344,7 +349,7 @@ App::post('/v1/database/collections/:collectionId/attributes/integer') $size = 0; $filters = []; - $success = $dbForExternal->addAttributeInQueue($collectionId, $attributeId, $type, $size, $required, $default, $signed, $array, $filters); + $success = $dbForExternal->addAttributeInQueue($collectionId, $attributeId, $type, $size, $required, $default, $signed, $array, /*format*/ null, $filters); // Database->addAttributeInQueue() does not return a document // So we need to create one for the response @@ -417,7 +422,7 @@ App::post('/v1/database/collections/:collectionId/attributes/float') $size = 0; $filters = []; - $success = $dbForExternal->addAttributeInQueue($collectionId, $attributeId, $type, $size, $required, $default, $signed, $array, $filters); + $success = $dbForExternal->addAttributeInQueue($collectionId, $attributeId, $type, $size, $required, $default, $signed, $array, /*$format*/ null, $filters); // Database->addAttributeInQueue() does not return a document // So we need to create one for the response @@ -490,7 +495,7 @@ App::post('/v1/database/collections/:collectionId/attributes/boolean') $size = 0; $filters = []; - $success = $dbForExternal->addAttributeInQueue($collectionId, $attributeId, $type, $size, $required, $default, $signed, $array, $filters); + $success = $dbForExternal->addAttributeInQueue($collectionId, $attributeId, $type, $size, $required, $default, $signed, $array, /*$format*/ null, $filters); // Database->addAttributeInQueue() does not return a document // So we need to create one for the response @@ -940,8 +945,38 @@ App::post('/v1/database/collections/:collectionId/documents') $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 + /** @var string[] $formats */ + $formats = []; + \array_walk($collection->getAttributes()['attributes'], function ($attribute) use (&$formats) { + switch ($attribute['format']) { + case 'email': + $formats[] = [ + 'name' => 'email', + 'validator' => new Email(), + 'type' => Database::VAR_STRING, + ]; + break; + case 'ip': + $formats[] = [ + 'name' => 'ip', + 'validator' => new IP(), + 'type' => Database::VAR_STRING, + ]; + break; + case 'url': + $formats[] = [ + 'name' => 'url', + 'validator' => new URL(), + 'type' => Database::VAR_STRING, + ]; + break; + default: + break; + } + }); + try { - $document = $dbForExternal->createDocument($collectionId, new Document($data)); + $document = $dbForExternal->createDocument($collectionId, new Document($data), $formats); } catch (StructureException $exception) { throw new Exception($exception->getMessage(), 400); } @@ -1098,8 +1133,38 @@ App::patch('/v1/database/collections/:collectionId/documents/:documentId') $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 + /** @var string[] $formats */ + $formats = []; + \array_walk($collection->getAttribute('attributes', []), function (Document $attribute) { + switch ($attribute->getAttribute('format', '')) { + case 'email': + $formats[] = [ + 'name' => 'email', + 'validator' => new Email(), + 'type' => Database::VAR_STRING, + ]; + break; + case 'ip': + $formats[] = [ + 'name' => 'ip', + 'validator' => new IP(), + 'type' => Database::VAR_STRING, + ]; + break; + case 'url': + $formats[] = [ + 'name' => 'url', + 'validator' => new URL(), + 'type' => Database::VAR_STRING, + ]; + break; + default: + break; + } + }); + try { - $document = $dbForExternal->updateDocument($collection->getId(), $document->getId(), new Document($data)); + $document = $dbForExternal->updateDocument($collection->getId(), $document->getId(), new Document($data), $formats); } catch (AuthorizationException $exception) { throw new Exception('Unauthorized permissions', 401); } catch (StructureException $exception) { diff --git a/app/workers/database.php b/app/workers/database.php index b024871045..17968d27d2 100644 --- a/app/workers/database.php +++ b/app/workers/database.php @@ -75,9 +75,10 @@ class DatabaseV1 extends Worker $default = $attribute->getAttribute('default', null); $signed = $attribute->getAttribute('signed', true); $array = $attribute->getAttribute('array', false); + $format = $attribute->getAttribute('format', ''); $filters = $attribute->getAttribute('filters', []); - $success = $dbForExternal->createAttribute($collectionId, $id, $type, $size, $required, $default, $signed, $array, $filters); + $success = $dbForExternal->createAttribute($collectionId, $id, $type, $size, $required, $default, $signed, $array, $format, $filters); if ($success) { $removed = $dbForExternal->removeAttributeInQueue($collectionId, $id); } From 7b7e2ad17f0f395e87aa21bbb4bbb12cedc46ccd Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Thu, 22 Jul 2021 16:39:52 -0400 Subject: [PATCH 09/21] Test for invalid document structure with custom attributes --- tests/e2e/Services/Database/DatabaseBase.php | 98 ++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/tests/e2e/Services/Database/DatabaseBase.php b/tests/e2e/Services/Database/DatabaseBase.php index 9bcfb6102f..aa6371a493 100644 --- a/tests/e2e/Services/Database/DatabaseBase.php +++ b/tests/e2e/Services/Database/DatabaseBase.php @@ -511,6 +511,104 @@ trait DatabaseBase return $data; } + public function testInvalidDocumentStructure() + { + $collection = $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' => 'invalidDocumentStructure', + 'read' => ['role:all'], + 'write' => ['role:all'], + ]); + + $this->assertEquals($collection['headers']['status-code'], 201); + $this->assertEquals($collection['body']['name'], 'invalidDocumentStructure'); + + $collectionId = $collection['body']['$id']; + + $email = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'attributeId' => 'email', + 'size' => 256, + 'required' => false, + 'format' => 'email', + ]); + + $ip = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'attributeId' => 'ip', + 'size' => 64, + 'required' => false, + 'format' => 'ip', + ]); + + $url = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'attributeId' => 'url', + 'size' => 256, + 'required' => false, + 'format' => 'url', + ]); + + $this->assertEquals($email['headers']['status-code'], 201); + $this->assertEquals($ip['headers']['status-code'], 201); + $this->assertEquals($url['headers']['status-code'], 201); + + // wait for worker to add attributes + sleep(10); + + $badEmail = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'email' => 'user@@example.com', + ], + 'read' => ['user:'.$this->getUser()['$id']], + 'write' => ['user:'.$this->getUser()['$id']], + ]); + + $badIp = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'ip' => '1.1.1.1.1', + ], + 'read' => ['user:'.$this->getUser()['$id']], + 'write' => ['user:'.$this->getUser()['$id']], + ]); + + $badUrl = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'url' => 'example...com', + ], + 'read' => ['user:'.$this->getUser()['$id']], + 'write' => ['user:'.$this->getUser()['$id']], + ]); + + $this->assertEquals(400, $badEmail['headers']['status-code']); + $this->assertEquals(400, $badIp['headers']['status-code']); + $this->assertEquals(400, $badUrl['headers']['status-code']); + $this->assertEquals('Invalid document structure: Attribute "email" has invalid format. Value must be a valid email address', $badEmail['body']['message']); + $this->assertEquals('Invalid document structure: Attribute "ip" has invalid format. Value must be a valid IP address', $badIp['body']['message']); + $this->assertEquals('Invalid document structure: Attribute "url" has invalid format. Value must be a valid URL', $badUrl['body']['message']); + } + /** * @depends testDeleteDocument */ From ff2fd8a3817ee7416339d6e9840eec604ee6923e Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Fri, 23 Jul 2021 10:59:55 -0400 Subject: [PATCH 10/21] Set format structure validators on app init --- app/controllers/api/database.php | 74 ++++---------------------------- app/init.php | 8 ++++ 2 files changed, 16 insertions(+), 66 deletions(-) diff --git a/app/controllers/api/database.php b/app/controllers/api/database.php index 07f9b1fb95..c4404dcff8 100644 --- a/app/controllers/api/database.php +++ b/app/controllers/api/database.php @@ -15,12 +15,10 @@ 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\Structure; use Utopia\Database\Validator\UID; use Utopia\Database\Exception\Authorization as AuthorizationException; use Utopia\Database\Exception\Structure as StructureException; -use Appwrite\Network\Validator\Email; -use Appwrite\Network\Validator\IP; -use Appwrite\Network\Validator\URL; use Appwrite\Utopia\Response; use Utopia\Database\Database; use Utopia\Database\Document; @@ -244,7 +242,7 @@ App::post('/v1/database/collections/:collectionId/attributes/string') ->param('size', null, new Integer(), 'Attribute size for text attributes, in number of characters.') ->param('required', null, new Boolean(), 'Is attribute required?') ->param('default', null, new Text(0), 'Default value for attribute when not provided. Cannot be set when attribute is required.', true) - ->param('format', null, new Whitelist(['email', 'ip', 'url']), 'Optional format validation of attribute. Must be one of (email, ip, url).', true) + ->param('format', null, new Whitelist(['email', 'ip', 'url']), 'Optional format validation of attribute. Must be one of: email, ip, url', true) ->param('array', false, new Boolean(), 'Is attribute an array?', true) ->inject('response') ->inject('dbForExternal') @@ -271,6 +269,10 @@ App::post('/v1/database/collections/:collectionId/attributes/string') throw new Exception('Length of default attribute exceeds attribute size', 400); } + if (!Structure::hasFormat($format, $type)) { + throw new Exception("Format {$format} not available for {$type} attributes.", 400); + } + // integers are signed by default, and filters are hidden from the endpoint. $signed = true; $filters = []; @@ -945,38 +947,8 @@ App::post('/v1/database/collections/:collectionId/documents') $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 - /** @var string[] $formats */ - $formats = []; - \array_walk($collection->getAttributes()['attributes'], function ($attribute) use (&$formats) { - switch ($attribute['format']) { - case 'email': - $formats[] = [ - 'name' => 'email', - 'validator' => new Email(), - 'type' => Database::VAR_STRING, - ]; - break; - case 'ip': - $formats[] = [ - 'name' => 'ip', - 'validator' => new IP(), - 'type' => Database::VAR_STRING, - ]; - break; - case 'url': - $formats[] = [ - 'name' => 'url', - 'validator' => new URL(), - 'type' => Database::VAR_STRING, - ]; - break; - default: - break; - } - }); - try { - $document = $dbForExternal->createDocument($collectionId, new Document($data), $formats); + $document = $dbForExternal->createDocument($collectionId, new Document($data)); } catch (StructureException $exception) { throw new Exception($exception->getMessage(), 400); } @@ -1133,38 +1105,8 @@ App::patch('/v1/database/collections/:collectionId/documents/:documentId') $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 - /** @var string[] $formats */ - $formats = []; - \array_walk($collection->getAttribute('attributes', []), function (Document $attribute) { - switch ($attribute->getAttribute('format', '')) { - case 'email': - $formats[] = [ - 'name' => 'email', - 'validator' => new Email(), - 'type' => Database::VAR_STRING, - ]; - break; - case 'ip': - $formats[] = [ - 'name' => 'ip', - 'validator' => new IP(), - 'type' => Database::VAR_STRING, - ]; - break; - case 'url': - $formats[] = [ - 'name' => 'url', - 'validator' => new URL(), - 'type' => Database::VAR_STRING, - ]; - break; - default: - break; - } - }); - try { - $document = $dbForExternal->updateDocument($collection->getId(), $document->getId(), new Document($data), $formats); + $document = $dbForExternal->updateDocument($collection->getId(), $document->getId(), new Document($data)); } catch (AuthorizationException $exception) { throw new Exception('Unauthorized permissions', 401); } catch (StructureException $exception) { diff --git a/app/init.php b/app/init.php index 854a938dc0..fc87454826 100644 --- a/app/init.php +++ b/app/init.php @@ -26,6 +26,9 @@ use Appwrite\Database\Adapter\Redis as RedisAdapter; use Appwrite\Database\Document; use Appwrite\Database\Validator\Authorization; use Appwrite\Event\Event; +use Appwrite\Network\Validator\Email; +use Appwrite\Network\Validator\IP; +use Appwrite\Network\Validator\URL; use Appwrite\OpenSSL\OpenSSL; use Utopia\App; use Utopia\View; @@ -40,6 +43,7 @@ use Utopia\Database\Adapter\MariaDB; use Utopia\Database\Document as Document2; use Utopia\Database\Database as Database2; use Utopia\Database\Validator\Authorization as Authorization2; +use Utopia\Database\Validator\Structure; use Swoole\Database\PDOConfig; use Swoole\Database\PDOPool; use Swoole\Database\RedisConfig; @@ -185,6 +189,10 @@ Database2::addFilter('encrypt', } ); +Structure::addFormat('email', new Email(), Database2::VAR_STRING); +Structure::addFormat('ip', new IP(), Database2::VAR_STRING); +Structure::addFormat('url', new URL(), Database2::VAR_STRING); + /* * Registry */ From 04a69edf34eb225591a30d62ba46975af6cbf534 Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Fri, 23 Jul 2021 11:04:31 -0400 Subject: [PATCH 11/21] Test for unsupported formats --- tests/e2e/Services/Database/DatabaseBase.php | 22 +++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/tests/e2e/Services/Database/DatabaseBase.php b/tests/e2e/Services/Database/DatabaseBase.php index aa6371a493..182376133f 100644 --- a/tests/e2e/Services/Database/DatabaseBase.php +++ b/tests/e2e/Services/Database/DatabaseBase.php @@ -523,8 +523,8 @@ trait DatabaseBase 'write' => ['role:all'], ]); - $this->assertEquals($collection['headers']['status-code'], 201); - $this->assertEquals($collection['body']['name'], 'invalidDocumentStructure'); + $this->assertEquals(201, $collection['headers']['status-code']); + $this->assertEquals('invalidDocumentStructure', $collection['body']['name']); $collectionId = $collection['body']['$id']; @@ -561,9 +561,21 @@ trait DatabaseBase 'format' => 'url', ]); - $this->assertEquals($email['headers']['status-code'], 201); - $this->assertEquals($ip['headers']['status-code'], 201); - $this->assertEquals($url['headers']['status-code'], 201); + $unsupported = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/string', array_merge([ + 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'attributeId' => 'unsupported', + 'size' => 256, + 'required' => false, + 'format' => 'unsupported', + ]); + + $this->assertEquals(201, $email['headers']['status-code']); + $this->assertEquals(201, $ip['headers']['status-code']); + $this->assertEquals(201, $url['headers']['status-code']); + $this->assertEquals(400, $unsupported['headers']['status-code']); + $this->assertEquals('Invalid format: Value must be one of (email, ip, url)', $unsupported['body']['message']); // wait for worker to add attributes sleep(10); From e22d4e1db908653fc6b8517e124fae68e4849851 Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Fri, 23 Jul 2021 17:38:14 -0400 Subject: [PATCH 12/21] Add support for min and max for numeric attributes --- app/controllers/api/database.php | 127 +++++++++++++++++++++++++++++-- composer.json | 12 ++- 2 files changed, 132 insertions(+), 7 deletions(-) diff --git a/app/controllers/api/database.php b/app/controllers/api/database.php index c4404dcff8..a5b386de5f 100644 --- a/app/controllers/api/database.php +++ b/app/controllers/api/database.php @@ -212,6 +212,20 @@ App::delete('/v1/database/collections/:collectionId') $dbForExternal->deleteCollection($collectionId); + // Remove any float validators from Structure object + $attributes = $collection->getAttribute('attributes', []); + array_walk($attributes, function (Document $attribute) { + $type = $attribute->getAttribute('type', ''); + $format = $attribute->getAttribute('format', ''); + + // Remove the range filter from ints/floats + if ($type === Database::VAR_INTEGER || $type === Database::VAR_FLOAT) { + if ($format) { + Structure::removeFormat($format); + } + } + }); + $events ->setParam('eventData', $response->output2($collection, Response::MODEL_COLLECTION)) ; @@ -269,7 +283,7 @@ App::post('/v1/database/collections/:collectionId/attributes/string') throw new Exception('Length of default attribute exceeds attribute size', 400); } - if (!Structure::hasFormat($format, $type)) { + if (!\is_null($format) && !Structure::hasFormat($format, $type)) { throw new Exception("Format {$format} not available for {$type} attributes.", 400); } @@ -326,13 +340,15 @@ App::post('/v1/database/collections/:collectionId/attributes/integer') ->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.') ->param('required', null, new Boolean(), 'Is attribute required?') + ->param('min', null, new Integer(), 'Minimum value to enforce on new documents', true) + ->param('max', null, new Integer(), 'Maximum value to enforce on new documents', true) ->param('default', null, new Integer(), '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, $attributeId, $required, $default, $array, $response, $dbForExternal, $database, $audits) { + ->action(function ($collectionId, $attributeId, $required, $min, $max, $default, $array, $response, $dbForExternal, $database, $audits) { /** @var Appwrite\Utopia\Response $response */ /** @var Utopia\Database\Database $dbForExternal*/ /** @var Appwrite\Event\Event $database */ @@ -346,12 +362,28 @@ App::post('/v1/database/collections/:collectionId/attributes/integer') throw new Exception('Collection not found', 404); } + /** @var string $format Name for custom range validation */ + $format = null; + + if (!is_null($min) || !is_null($max)) { + $min = (is_null($min)) ? -INF : \intval($min); + $max = (is_null($max)) ? INF : \intval($max); + + // TODO@kodumbeats troubleshoot + // if ($min > $max) { + // throw new Exception('Minimum value must be lesser than maximum value', 400); + // } + + $format = strval($min) . '_to_' . strval($max); + Structure::addFormat($format, new Range($min, $max, $type), $type); + } + // integers are signed by default, and filters are hidden from the endpoint. $signed = true; $size = 0; $filters = []; - $success = $dbForExternal->addAttributeInQueue($collectionId, $attributeId, $type, $size, $required, $default, $signed, $array, /*format*/ null, $filters); + $success = $dbForExternal->addAttributeInQueue($collectionId, $attributeId, $type, $size, $required, $default, $signed, $array, $format, $filters); // Database->addAttributeInQueue() does not return a document // So we need to create one for the response @@ -365,6 +397,7 @@ App::post('/v1/database/collections/:collectionId/attributes/integer') 'required' => $required, 'default' => $default, 'signed' => $signed, + 'format' => $format, 'array' => $array, 'filters' => $filters ]); @@ -399,13 +432,15 @@ App::post('/v1/database/collections/:collectionId/attributes/float') ->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.') ->param('required', null, new Boolean(), 'Is attribute required?') + ->param('min', null, new FloatValidator(), 'Minimum value to enforce on new documents', true) + ->param('max', null, new FloatValidator(), 'Maximum value to enforce on new documents', true) ->param('default', null, new FloatValidator(), '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, $attributeId, $required, $default, $array, $response, $dbForExternal, $database, $audits) { + ->action(function ($collectionId, $attributeId, $required, $min, $max, $default, $array, $response, $dbForExternal, $database, $audits) { /** @var Appwrite\Utopia\Response $response */ /** @var Utopia\Database\Database $dbForExternal*/ /** @var Appwrite\Event\Event $database */ @@ -419,12 +454,27 @@ App::post('/v1/database/collections/:collectionId/attributes/float') throw new Exception('Collection not found', 404); } + /** @var string $format Name for custom range validation */ + $format = null; + + if (!is_null($min) || !is_null($max)) { + $min = (is_null($min)) ? -INF : \floatval($min); + $max = (is_null($max)) ? INF : \floatval($max); + + if ($min > $max) { + throw new Exception('Minimum value must be lesser than maximum value', 400); + } + + $format = strval($min) . '_to_' . strval($max); + Structure::addFormat($format, new Range($min, $max, $type), $type); + } + // integers are signed by default, and filters are hidden from the endpoint. $signed = true; $size = 0; $filters = []; - $success = $dbForExternal->addAttributeInQueue($collectionId, $attributeId, $type, $size, $required, $default, $signed, $array, /*$format*/ null, $filters); + $success = $dbForExternal->addAttributeInQueue($collectionId, $attributeId, $type, $size, $required, $default, $signed, $array, $format, $filters); // Database->addAttributeInQueue() does not return a document // So we need to create one for the response @@ -439,6 +489,7 @@ App::post('/v1/database/collections/:collectionId/attributes/float') 'default' => $default, 'signed' => $signed, 'array' => $array, + 'format' => $format, 'filters' => $filters ]); @@ -653,6 +704,16 @@ App::delete('/v1/database/collections/:collectionId/attributes/:attributeId') 'collectionId' => $collectionId, ])]); + $type = $attribute->getAttribute('type', ''); + $format = $attribute->getAttribute('format', ''); + + // Remove the range filter from ints/floats, if given + if ($type === Database::VAR_INTEGER || $type === Database::VAR_FLOAT) { + if ($format) { + Structure::removeFormat($format); + } + } + $database ->setParam('type', DELETE_TYPE_ATTRIBUTE) ->setParam('document', $attribute) @@ -947,6 +1008,34 @@ App::post('/v1/database/collections/:collectionId/documents') $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 + // Add any missing range validators not present on numeric attributes + $attributes = $collection->getAttribute('attributes', []); + array_walk($attributes, function (Document $attribute) { + $type = $attribute->getAttribute('type', ''); + $format = $attribute->getAttribute('format', ''); + + if ($format || !Structure::hasFormat($format, $type)) { + switch ($type) { + case Database::VAR_INTEGER: + // format is stored as "{$min}_to_{$max}" + [$min, /*_to_*/, $max] = \explode('_', $format); + $min = ($min === "-INF") ? -INF : \intval($min); + $max = ($max === "INF") ? INF : \intval($max); + Structure::addFormat($format, new Range($min, $max, $type), $type); + break; + case Database::VAR_FLOAT: + // format is stored as "{$min}_to_{$max}" + [$min, /*_to_*/, $max] = \explode('_', $format); + $min = ($min === "-INF") ? -INF :\floatval($min); + $max = ($max === "INF") ? INF :\floatval($max); + Structure::addFormat($format, new Range($min, $max, $type), $type); + break; + default: + break; + } + } + }); + try { $document = $dbForExternal->createDocument($collectionId, new Document($data)); } catch (StructureException $exception) { @@ -1105,6 +1194,34 @@ App::patch('/v1/database/collections/:collectionId/documents/:documentId') $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 + // Add any missing range validators not present on numeric attributes + $attributes = $collection->getAttribute('attributes', []); + array_walk($attributes, function (Document $attribute) { + $type = $attribute->getAttribute('type', ''); + $format = $attribute->getAttribute('format', ''); + + if ($format || !Structure::hasFormat($format, $type)) { + switch ($type) { + case Database::VAR_INTEGER: + // format is stored as "{$min}_to_{$max}" + [$min, /*_to_*/, $max] = \explode('_', $format); + $min = ($min === "-INF") ? -INF :\intval($min); + $max = ($max === "INF") ? INF :\intval($max); + Structure::addFormat($format, new Range($min, $max, $type), $type); + break; + case Database::VAR_FLOAT: + // format is stored as "{$min}_to_{$max}" + [$min, /*_to_*/, $max] = \explode('_', $format); + $min = ($min === "-INF") ? -INF :\floatval($min); + $max = ($max === "INF") ? INF :\floatval($max); + Structure::addFormat($format, new Range($min, $max, $type), $type); + break; + default: + break; + } + } + }); + try { $document = $dbForExternal->updateDocument($collection->getId(), $document->getId(), new Document($data)); } catch (AuthorizationException $exception) { diff --git a/composer.json b/composer.json index bb8003ffb6..f31f364fca 100644 --- a/composer.json +++ b/composer.json @@ -38,14 +38,14 @@ "appwrite/php-clamav": "1.1.*", "appwrite/php-runtimes": "0.4.*", - "utopia-php/framework": "0.16.*", + "utopia-php/framework": "dev-feat-range-validator-infinity as 0.16.2", "utopia-php/abuse": "dev-feat-utopia-db-integration", "utopia-php/analytics": "0.2.*", "utopia-php/audit": "dev-feat-utopia-db-integration", "utopia-php/cache": "0.4.*", "utopia-php/cli": "0.11.*", "utopia-php/config": "0.2.*", - "utopia-php/database": "0.5.*", + "utopia-php/database": "dev-feat-attribute-format-validators as 0.5.1", "utopia-php/locale": "0.3.*", "utopia-php/registry": "0.5.*", "utopia-php/preloader": "0.2.*", @@ -70,6 +70,14 @@ { "type": "git", "url": "https://github.com/lohanidamodar/audit" + }, + { + "type": "git", + "url": "https://github.com/utopia-php/database" + }, + { + "type": "git", + "url": "https://github.com/utopia-php/framework" } ], "require-dev": { From 3aae021cfb42eb91667cd53e693178c3cf23fed0 Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Fri, 23 Jul 2021 17:38:31 -0400 Subject: [PATCH 13/21] Add tests for document structure validation --- tests/e2e/Services/Database/DatabaseBase.php | 219 +++++++++++++++++++ 1 file changed, 219 insertions(+) diff --git a/tests/e2e/Services/Database/DatabaseBase.php b/tests/e2e/Services/Database/DatabaseBase.php index 182376133f..d201dd9095 100644 --- a/tests/e2e/Services/Database/DatabaseBase.php +++ b/tests/e2e/Services/Database/DatabaseBase.php @@ -561,6 +561,49 @@ trait DatabaseBase 'format' => 'url', ]); + $range = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/integer', array_merge([ + 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'attributeId' => 'range', + 'required' => false, + 'min' => 1, + 'max' => 10, + ]); + + // TODO@kodumbeats float validator rejects 0.0 and 1.0 as floats + $floatRange = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/float', array_merge([ + 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'attributeId' => 'floatRange', + 'required' => false, + 'min' => 0.5, + 'max' => 1.5, + ]); + + $upperBound = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/integer', array_merge([ + 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'attributeId' => 'upperBound', + 'required' => false, + 'max' => 10, + ]); + + $lowerBound = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/integer', array_merge([ + 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'attributeId' => 'lowerBound', + 'required' => false, + 'min' => 5, + ]); + + /** + * Test for failure + */ + $unsupported = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/string', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -571,15 +614,138 @@ trait DatabaseBase 'format' => 'unsupported', ]); + // TODO@kodumbeats troubleshoot + // $invalidRange = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/integer', array_merge([ + // 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + // 'x-appwrite-key' => $this->getProject()['apiKey'] + // ]), [ + // 'attributeId' => 'invalidRange', + // 'required' => false, + // 'min' => 4, + // 'max' => 3, + // ]); + $this->assertEquals(201, $email['headers']['status-code']); $this->assertEquals(201, $ip['headers']['status-code']); $this->assertEquals(201, $url['headers']['status-code']); + $this->assertEquals(201, $range['headers']['status-code']); + $this->assertEquals(201, $floatRange['headers']['status-code']); + $this->assertEquals(201, $upperBound['headers']['status-code']); + $this->assertEquals(201, $lowerBound['headers']['status-code']); $this->assertEquals(400, $unsupported['headers']['status-code']); + // $this->assertEquals(400, $invalidRange['headers']['status-code']); $this->assertEquals('Invalid format: Value must be one of (email, ip, url)', $unsupported['body']['message']); + // $this->assertEquals('Minimum value must be lesser than maximum value', $invalidRange['body']['message']); // wait for worker to add attributes sleep(10); + $collection = $this->client->call(Client::METHOD_GET, '/database/collections/' . $collectionId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]), []); + + // var_dump($collection); + + $this->assertCount(7, $collection['body']['attributes']); + $this->assertCount(0, $collection['body']['attributesInQueue']); + + /** + * Test for successful validation + */ + + $goodEmail = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'email' => 'user@example.com', + ], + 'read' => ['user:'.$this->getUser()['$id']], + 'write' => ['user:'.$this->getUser()['$id']], + ]); + + $goodIp = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'ip' => '1.1.1.1', + ], + 'read' => ['user:'.$this->getUser()['$id']], + 'write' => ['user:'.$this->getUser()['$id']], + ]); + + $goodUrl = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'url' => 'http://www.example.com', + ], + 'read' => ['user:'.$this->getUser()['$id']], + 'write' => ['user:'.$this->getUser()['$id']], + ]); + + $goodRange = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'range' => 3, + ], + 'read' => ['user:'.$this->getUser()['$id']], + 'write' => ['user:'.$this->getUser()['$id']], + ]); + + $goodFloatRange = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'floatRange' => 0.8, + ], + 'read' => ['user:'.$this->getUser()['$id']], + 'write' => ['user:'.$this->getUser()['$id']], + ]); + + $notTooHigh = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'upperBound' => 8, + ], + 'read' => ['user:'.$this->getUser()['$id']], + 'write' => ['user:'.$this->getUser()['$id']], + ]); + + $notTooLow = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'lowerBound' => 8, + ], + 'read' => ['user:'.$this->getUser()['$id']], + 'write' => ['user:'.$this->getUser()['$id']], + ]); + + // var_dump($notTooLow); + + $this->assertEquals(201, $goodEmail['headers']['status-code']); + $this->assertEquals(201, $goodIp['headers']['status-code']); + $this->assertEquals(201, $goodUrl['headers']['status-code']); + $this->assertEquals(201, $goodRange['headers']['status-code']); + $this->assertEquals(201, $goodFloatRange['headers']['status-code']); + $this->assertEquals(201, $notTooHigh['headers']['status-code']); + $this->assertEquals(201, $notTooLow['headers']['status-code']); + + /* + * Test that custom validators reject documents + */ + $badEmail = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/documents', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -613,12 +779,65 @@ trait DatabaseBase 'write' => ['user:'.$this->getUser()['$id']], ]); + $badRange = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'range' => 11, + ], + 'read' => ['user:'.$this->getUser()['$id']], + 'write' => ['user:'.$this->getUser()['$id']], + ]); + + $badProbability = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'floatRange' => 2.5, + ], + 'read' => ['user:'.$this->getUser()['$id']], + 'write' => ['user:'.$this->getUser()['$id']], + ]); + + $tooHigh = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'upperBound' => 11, + ], + 'read' => ['user:'.$this->getUser()['$id']], + 'write' => ['user:'.$this->getUser()['$id']], + ]); + + $tooLow = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'lowerBound' => 3, + ], + 'read' => ['user:'.$this->getUser()['$id']], + 'write' => ['user:'.$this->getUser()['$id']], + ]); + + $this->assertEquals(400, $badEmail['headers']['status-code']); $this->assertEquals(400, $badIp['headers']['status-code']); $this->assertEquals(400, $badUrl['headers']['status-code']); + $this->assertEquals(400, $badRange['headers']['status-code']); + $this->assertEquals(400, $badProbability['headers']['status-code']); + $this->assertEquals(400, $tooHigh['headers']['status-code']); + $this->assertEquals(400, $tooLow['headers']['status-code']); $this->assertEquals('Invalid document structure: Attribute "email" has invalid format. Value must be a valid email address', $badEmail['body']['message']); $this->assertEquals('Invalid document structure: Attribute "ip" has invalid format. Value must be a valid IP address', $badIp['body']['message']); $this->assertEquals('Invalid document structure: Attribute "url" has invalid format. Value must be a valid URL', $badUrl['body']['message']); + $this->assertEquals('Invalid document structure: Attribute "range" has invalid format. Value must be a valid range between 1 and 10', $badRange['body']['message']); + $this->assertEquals('Invalid document structure: Attribute "floatRange" has invalid format. Value must be a valid range between 1 and 2', $badProbability['body']['message']); + $this->assertEquals('Invalid document structure: Attribute "upperBound" has invalid format. Value must be a valid range between inf and 10', $tooHigh['body']['message']); + $this->assertEquals('Invalid document structure: Attribute "lowerBound" has invalid format. Value must be a valid range between 5 and inf', $tooLow['body']['message']); } /** From df5703022b4bbdb81a43985027486dff876faf44 Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Mon, 26 Jul 2021 19:10:39 -0400 Subject: [PATCH 14/21] Create custom formats via callback in structure validator --- app/controllers/api/database.php | 101 ++++--------------------------- app/init.php | 23 ++++++- 2 files changed, 31 insertions(+), 93 deletions(-) diff --git a/app/controllers/api/database.php b/app/controllers/api/database.php index 24ad7a01c6..033bdeef10 100644 --- a/app/controllers/api/database.php +++ b/app/controllers/api/database.php @@ -212,20 +212,6 @@ App::delete('/v1/database/collections/:collectionId') $dbForExternal->deleteCollection($collectionId); - // Remove any float validators from Structure object - $attributes = $collection->getAttribute('attributes', []); - array_walk($attributes, function (Document $attribute) { - $type = $attribute->getAttribute('type', ''); - $format = $attribute->getAttribute('format', ''); - - // Remove the range filter from ints/floats - if ($type === Database::VAR_INTEGER || $type === Database::VAR_FLOAT) { - if ($format) { - Structure::removeFormat($format); - } - } - }); - $events ->setParam('eventData', $response->output($collection, Response::MODEL_COLLECTION)) ; @@ -362,20 +348,14 @@ App::post('/v1/database/collections/:collectionId/attributes/integer') throw new Exception('Collection not found', 404); } - /** @var string $format Name for custom range validation */ + /** @var string $format */ $format = null; + // Add range validator if either $min or $max is provided if (!is_null($min) || !is_null($max)) { $min = (is_null($min)) ? -INF : \intval($min); $max = (is_null($max)) ? INF : \intval($max); - - // TODO@kodumbeats troubleshoot - // if ($min > $max) { - // throw new Exception('Minimum value must be lesser than maximum value', 400); - // } - - $format = strval($min) . '_to_' . strval($max); - Structure::addFormat($format, new Range($min, $max, $type), $type); + $format = 'int-range'; } // integers are signed by default, and filters are hidden from the endpoint. @@ -397,7 +377,8 @@ App::post('/v1/database/collections/:collectionId/attributes/integer') 'required' => $required, 'default' => $default, 'signed' => $signed, - 'format' => $format, + 'min' => $min, + 'max' => $max, 'array' => $array, 'filters' => $filters ]); @@ -457,16 +438,11 @@ App::post('/v1/database/collections/:collectionId/attributes/float') /** @var string $format Name for custom range validation */ $format = null; + // Add range validator if either $min or $max is provided if (!is_null($min) || !is_null($max)) { - $min = (is_null($min)) ? -INF : \floatval($min); - $max = (is_null($max)) ? INF : \floatval($max); - - if ($min > $max) { - throw new Exception('Minimum value must be lesser than maximum value', 400); - } - - $format = strval($min) . '_to_' . strval($max); - Structure::addFormat($format, new Range($min, $max, $type), $type); + $min = (is_null($min)) ? -INF : \intval($min); + $max = (is_null($max)) ? INF : \intval($max); + $format = 'int-range'; } // integers are signed by default, and filters are hidden from the endpoint. @@ -489,7 +465,8 @@ App::post('/v1/database/collections/:collectionId/attributes/float') 'default' => $default, 'signed' => $signed, 'array' => $array, - 'format' => $format, + 'min' => $min, + 'max' => $max, 'filters' => $filters ]); @@ -1008,34 +985,6 @@ App::post('/v1/database/collections/:collectionId/documents') $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 - // Add any missing range validators not present on numeric attributes - $attributes = $collection->getAttribute('attributes', []); - array_walk($attributes, function (Document $attribute) { - $type = $attribute->getAttribute('type', ''); - $format = $attribute->getAttribute('format', ''); - - if ($format || !Structure::hasFormat($format, $type)) { - switch ($type) { - case Database::VAR_INTEGER: - // format is stored as "{$min}_to_{$max}" - [$min, /*_to_*/, $max] = \explode('_', $format); - $min = ($min === "-INF") ? -INF : \intval($min); - $max = ($max === "INF") ? INF : \intval($max); - Structure::addFormat($format, new Range($min, $max, $type), $type); - break; - case Database::VAR_FLOAT: - // format is stored as "{$min}_to_{$max}" - [$min, /*_to_*/, $max] = \explode('_', $format); - $min = ($min === "-INF") ? -INF :\floatval($min); - $max = ($max === "INF") ? INF :\floatval($max); - Structure::addFormat($format, new Range($min, $max, $type), $type); - break; - default: - break; - } - } - }); - try { $document = $dbForExternal->createDocument($collectionId, new Document($data)); } catch (StructureException $exception) { @@ -1194,34 +1143,6 @@ App::patch('/v1/database/collections/:collectionId/documents/:documentId') $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 - // Add any missing range validators not present on numeric attributes - $attributes = $collection->getAttribute('attributes', []); - array_walk($attributes, function (Document $attribute) { - $type = $attribute->getAttribute('type', ''); - $format = $attribute->getAttribute('format', ''); - - if ($format || !Structure::hasFormat($format, $type)) { - switch ($type) { - case Database::VAR_INTEGER: - // format is stored as "{$min}_to_{$max}" - [$min, /*_to_*/, $max] = \explode('_', $format); - $min = ($min === "-INF") ? -INF :\intval($min); - $max = ($max === "INF") ? INF :\intval($max); - Structure::addFormat($format, new Range($min, $max, $type), $type); - break; - case Database::VAR_FLOAT: - // format is stored as "{$min}_to_{$max}" - [$min, /*_to_*/, $max] = \explode('_', $format); - $min = ($min === "-INF") ? -INF :\floatval($min); - $max = ($max === "INF") ? INF :\floatval($max); - Structure::addFormat($format, new Range($min, $max, $type), $type); - break; - default: - break; - } - } - }); - try { $document = $dbForExternal->updateDocument($collection->getId(), $document->getId(), new Document($data)); } catch (AuthorizationException $exception) { diff --git a/app/init.php b/app/init.php index 40b28deb8e..cbb8b792d3 100644 --- a/app/init.php +++ b/app/init.php @@ -43,6 +43,7 @@ use Utopia\Database\Document as Document2; use Utopia\Database\Database as Database2; use Utopia\Database\Validator\Structure; use Utopia\Database\Validator\Authorization; +use Utopia\Validator\Range; use Swoole\Database\PDOConfig; use Swoole\Database\PDOPool; use Swoole\Database\RedisConfig; @@ -188,9 +189,25 @@ Database2::addFilter('encrypt', } ); -Structure::addFormat('email', new Email(), Database2::VAR_STRING); -Structure::addFormat('ip', new IP(), Database2::VAR_STRING); -Structure::addFormat('url', new URL(), Database2::VAR_STRING); +Structure::addFormat('email', function() { + return new Email(); +}, Database2::VAR_STRING); + +Structure::addFormat('ip', function() { + return new IP(); +}, Database2::VAR_STRING); + +Structure::addFormat('url', function() { + return new URL(); +}, Database2::VAR_STRING); + +Structure::addFormat('int-range', function($min, $max, $type) { + return new Range($min, $max, $type); +}, Database2::VAR_INTEGER, ['min', 'max', 'type']); + +Structure::addFormat('float-range', function($min, $max, $type) { + return new Range($min, $max, $type); +}, Database2::VAR_FLOAT, ['min', 'max', 'type']); /* * Registry From c0b48ec247fa1208e39da14bee982f96e2690f3c Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Mon, 26 Jul 2021 19:11:25 -0400 Subject: [PATCH 15/21] Remove references to old db responses --- app/controllers/api/database.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/controllers/api/database.php b/app/controllers/api/database.php index 033bdeef10..db6a98a5a6 100644 --- a/app/controllers/api/database.php +++ b/app/controllers/api/database.php @@ -308,7 +308,7 @@ App::post('/v1/database/collections/:collectionId/attributes/string') ; $response->setStatusCode(Response::STATUS_CODE_CREATED); - $response->dynamic2($attribute, Response::MODEL_ATTRIBUTE); + $response->dynamic($attribute, Response::MODEL_ATTRIBUTE); }); App::post('/v1/database/collections/:collectionId/attributes/integer') @@ -342,6 +342,7 @@ App::post('/v1/database/collections/:collectionId/attributes/integer') $type = Database::VAR_INTEGER; + $collection = $dbForExternal->getCollection($collectionId); if ($collection->isEmpty()) { @@ -395,7 +396,7 @@ App::post('/v1/database/collections/:collectionId/attributes/integer') ; $response->setStatusCode(Response::STATUS_CODE_CREATED); - $response->dynamic2($attribute, Response::MODEL_ATTRIBUTE); + $response->dynamic($attribute, Response::MODEL_ATTRIBUTE); }); App::post('/v1/database/collections/:collectionId/attributes/float') @@ -482,7 +483,7 @@ App::post('/v1/database/collections/:collectionId/attributes/float') ; $response->setStatusCode(Response::STATUS_CODE_CREATED); - $response->dynamic2($attribute, Response::MODEL_ATTRIBUTE); + $response->dynamic($attribute, Response::MODEL_ATTRIBUTE); }); App::post('/v1/database/collections/:collectionId/attributes/boolean') From b9fea89e7338459af73f7366bd4209beaf96a1ba Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Mon, 26 Jul 2021 21:00:36 -0400 Subject: [PATCH 16/21] Use refactored attribute function --- app/controllers/api/database.php | 416 ++++++++++--------- tests/e2e/Services/Database/DatabaseBase.php | 39 +- 2 files changed, 239 insertions(+), 216 deletions(-) diff --git a/app/controllers/api/database.php b/app/controllers/api/database.php index db6a98a5a6..071429b2dd 100644 --- a/app/controllers/api/database.php +++ b/app/controllers/api/database.php @@ -24,6 +24,96 @@ use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Query; +$attributesCallback = function ($attribute, $response, $dbForExternal, $database, $audits) { + /** @var Utopia\Database\Document $document*/ + /** @var Appwrite\Utopia\Response $response */ + /** @var Utopia\Database\Database $dbForExternal*/ + /** @var Appwrite\Event\Event $database */ + /** @var Appwrite\Event\Event $audits */ + + $collectionId = $attribute->getCollection(); + $attributeId = $attribute->getId(); + $type = $attribute->getAttribute('type', ''); + $size = $attribute->getAttribute('size', 0); + $required = $attribute->getAttribute('required', true); + $default = $attribute->getAttribute('default', null); + $min = $attribute->getAttribute('min', null); + $max = $attribute->getAttribute('max', null); + $signed = $attribute->getAttribute('signed', true); // integers are signed by default + $array = $attribute->getAttribute('array', false); + $format = $attribute->getAttribute('format', null); + $filters = $attribute->getAttribute('filters', []); // filters are hidden from the endpoint + + $collection = $dbForExternal->getCollection($collectionId); + + if ($collection->isEmpty()) { + throw new Exception('Collection not found', 404); + } + + // TODO@kodumbeats how to depend on $size for Text validator length + // Ensure attribute default is within required size + if ($size > 0 && !\is_null($default)) { + $validator = new Text($size); + if (!$validator->isValid($default)) { + throw new Exception('Length of default attribute exceeds attribute size', 400); + } + } + + if (!\is_null($format) && !Structure::hasFormat($format, $type)) { + throw new Exception("Format {$format} not available for {$type} attributes.", 400); + } + + if (!is_null($min) || !is_null($max)) { // Add range validator if either $min or $max is provided + $min = (is_null($min)) ? -INF : \intval($min); + $max = (is_null($max)) ? INF : \intval($max); + switch ($type) { + case Database::VAR_INTEGER: + $format = 'int-range'; + break; + case Database::VAR_FLOAT: + $format = 'float-range'; + break; + default: + throw new Exception("Format range not available for {$type} attributes.", 400); + } + } + + $success = $dbForExternal->addAttributeInQueue($collectionId, $attributeId, $type, $size, $required, $default, $signed, $array, $format, $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' => $attributeId, + 'type' => $type, + 'size' => $size, + 'required' => $required, + 'default' => $default, + 'min' => $min, + 'max' => $max, + 'signed' => $signed, + 'array' => $array, + 'format' => $format, + '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->dynamic($attribute, Response::MODEL_ATTRIBUTE); +}; + App::post('/v1/database/collections') ->desc('Create Collection') ->groups(['api', 'database']) @@ -242,73 +332,144 @@ App::post('/v1/database/collections/:collectionId/attributes/string') ->param('size', null, new Integer(), 'Attribute size for text attributes, in number of characters.') ->param('required', null, new Boolean(), 'Is attribute required?') ->param('default', null, new Text(0), 'Default value for attribute when not provided. Cannot be set when attribute is required.', true) - ->param('format', null, new Whitelist(['email', 'ip', 'url']), 'Optional format validation of attribute. Must be one of: email, ip, url', true) ->param('array', false, new Boolean(), 'Is attribute an array?', true) ->inject('response') ->inject('dbForExternal') ->inject('database') ->inject('audits') - ->action(function ($collectionId, $attributeId, $size, $required, $default, $format, $array, $response, $dbForExternal, $database, $audits) { + ->action(function ($collectionId, $attributeId, $size, $required, $default, $array, $response, $dbForExternal, $database, $audits) use ($attributesCallback) { /** @var Appwrite\Utopia\Response $response */ /** @var Utopia\Database\Database $dbForExternal*/ /** @var Appwrite\Event\Event $database */ /** @var Appwrite\Event\Event $audits */ - $type = Database::VAR_STRING; - - $collection = $dbForExternal->getCollection($collectionId); - - if ($collection->isEmpty()) { - throw new Exception('Collection not found', 404); - } - - // TODO@kodumbeats how to depend on $size for Text validator length - // Ensure attribute default is within required size - $validator = new Text($size); - if (!\is_null($default) && !$validator->isValid($default)) { - throw new Exception('Length of default attribute exceeds attribute size', 400); - } - - if (!\is_null($format) && !Structure::hasFormat($format, $type)) { - throw new Exception("Format {$format} not available for {$type} attributes.", 400); - } - - // integers are signed by default, and filters are hidden from the endpoint. - $signed = true; - $filters = []; - - $success = $dbForExternal->addAttributeInQueue($collectionId, $attributeId, $type, $size, $required, $default, $signed, $array, $format, $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([ + return $attributesCallback(new Document([ '$collection' => $collectionId, '$id' => $attributeId, - 'type' => $type, + 'type' => Database::VAR_STRING, 'size' => $size, 'required' => $required, 'default' => $default, - 'signed' => $signed, 'array' => $array, - 'format' => $format, - 'filters' => $filters - ]); + ]), $response, $dbForExternal, $database, $audits); + }); - $database - ->setParam('type', CREATE_TYPE_ATTRIBUTE) - ->setParam('document', $attribute) - ; +App::post('/v1/database/collections/:collectionId/attributes/email') + ->desc('Create Email 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', 'createEmailAttribute') + ->label('sdk.description', '/docs/references/database/create-attribute-email.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).') + ->param('attributeId', '', new Key(), 'Attribute ID.') + ->param('required', null, new Boolean(), 'Is attribute required?') + ->param('default', null, new Text(0), '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, $attributeId, $required, $default, $array, $response, $dbForExternal, $database, $audits) use ($attributesCallback) { + /** @var Appwrite\Utopia\Response $response */ + /** @var Utopia\Database\Database $dbForExternal*/ + /** @var Appwrite\Event\Event $database */ + /** @var Appwrite\Event\Event $audits */ - $audits - ->setParam('event', 'database.attributes.create') - ->setParam('resource', 'database/attributes/'.$attribute->getId()) - ->setParam('data', $attribute) - ; + return $attributesCallback(new Document([ + '$collection' => $collectionId, + '$id' => $attributeId, + 'type' => Database::VAR_STRING, + 'size' => 254, + 'required' => $required, + 'default' => $default, + 'array' => $array, + 'format' => 'email', + ]), $response, $dbForExternal, $database, $audits); + }); - $response->setStatusCode(Response::STATUS_CODE_CREATED); - $response->dynamic($attribute, Response::MODEL_ATTRIBUTE); +App::post('/v1/database/collections/:collectionId/attributes/ip') + ->desc('Create IP Address 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', 'createIpAttribute') + ->label('sdk.description', '/docs/references/database/create-attribute-ip.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).') + ->param('attributeId', '', new Key(), 'Attribute ID.') + ->param('required', null, new Boolean(), 'Is attribute required?') + ->param('default', null, new Text(0), '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, $attributeId, $required, $default, $array, $response, $dbForExternal, $database, $audits) use ($attributesCallback) { + /** @var Appwrite\Utopia\Response $response */ + /** @var Utopia\Database\Database $dbForExternal*/ + /** @var Appwrite\Event\Event $database */ + /** @var Appwrite\Event\Event $audits */ + + return $attributesCallback(new Document([ + '$collection' => $collectionId, + '$id' => $attributeId, + 'type' => Database::VAR_STRING, + 'size' => 39, + 'required' => $required, + 'default' => $default, + 'array' => $array, + 'format' => 'ip', + ]), $response, $dbForExternal, $database, $audits); + }); + +App::post('/v1/database/collections/:collectionId/attributes/url') + ->desc('Create IP Address 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', 'createUrlAttribute') + ->label('sdk.description', '/docs/references/database/create-attribute-url.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).') + ->param('attributeId', '', new Key(), 'Attribute ID.') + ->param('size', null, new Integer(), 'Attribute size for text attributes, in number of characters.') + ->param('required', null, new Boolean(), 'Is attribute required?') + ->param('default', null, new Text(0), '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, $attributeId, $size, $required, $default, $array, $response, $dbForExternal, $database, $audits) use ($attributesCallback) { + /** @var Appwrite\Utopia\Response $response */ + /** @var Utopia\Database\Database $dbForExternal*/ + /** @var Appwrite\Event\Event $database */ + /** @var Appwrite\Event\Event $audits */ + + return $attributesCallback(new Document([ + '$collection' => $collectionId, + '$id' => $attributeId, + 'type' => Database::VAR_STRING, + 'size' => $size, + 'required' => $required, + 'default' => $default, + 'array' => $array, + 'format' => 'url', + ]), $response, $dbForExternal, $database, $audits); }); App::post('/v1/database/collections/:collectionId/attributes/integer') @@ -334,69 +495,24 @@ App::post('/v1/database/collections/:collectionId/attributes/integer') ->inject('dbForExternal') ->inject('database') ->inject('audits') - ->action(function ($collectionId, $attributeId, $required, $min, $max, $default, $array, $response, $dbForExternal, $database, $audits) { + ->action(function ($collectionId, $attributeId, $required, $min, $max, $default, $array, $response, $dbForExternal, $database, $audits) use ($attributesCallback) { /** @var Appwrite\Utopia\Response $response */ /** @var Utopia\Database\Database $dbForExternal*/ /** @var Appwrite\Event\Event $database */ /** @var Appwrite\Event\Event $audits */ - $type = Database::VAR_INTEGER; - - - $collection = $dbForExternal->getCollection($collectionId); - - if ($collection->isEmpty()) { - throw new Exception('Collection not found', 404); - } - - /** @var string $format */ - $format = null; - - // Add range validator if either $min or $max is provided - if (!is_null($min) || !is_null($max)) { - $min = (is_null($min)) ? -INF : \intval($min); - $max = (is_null($max)) ? INF : \intval($max); - $format = 'int-range'; - } - - // integers are signed by default, and filters are hidden from the endpoint. - $signed = true; - $size = 0; - $filters = []; - - $success = $dbForExternal->addAttributeInQueue($collectionId, $attributeId, $type, $size, $required, $default, $signed, $array, $format, $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([ + return $attributesCallback(new Document([ '$collection' => $collectionId, '$id' => $attributeId, - 'type' => $type, - 'size' => $size, + 'type' => Database::VAR_INTEGER, + 'size' => 0, 'required' => $required, 'default' => $default, - 'signed' => $signed, 'min' => $min, 'max' => $max, 'array' => $array, - 'filters' => $filters - ]); + ]), $response, $dbForExternal, $database, $audits); - $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->dynamic($attribute, Response::MODEL_ATTRIBUTE); }); App::post('/v1/database/collections/:collectionId/attributes/float') @@ -407,7 +523,7 @@ App::post('/v1/database/collections/:collectionId/attributes/float') ->label('sdk.namespace', 'database') ->label('sdk.platform', [APP_PLATFORM_SERVER]) ->label('sdk.method', 'createFloatAttribute') - ->label('sdk.description', '/docs/references/database/create-attribute-integer.md') + ->label('sdk.description', '/docs/references/database/create-attribute-float.md') ->label('sdk.response.code', Response::STATUS_CODE_CREATED) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_ATTRIBUTE) @@ -422,68 +538,23 @@ App::post('/v1/database/collections/:collectionId/attributes/float') ->inject('dbForExternal') ->inject('database') ->inject('audits') - ->action(function ($collectionId, $attributeId, $required, $min, $max, $default, $array, $response, $dbForExternal, $database, $audits) { + ->action(function ($collectionId, $attributeId, $required, $min, $max, $default, $array, $response, $dbForExternal, $database, $audits) use ($attributesCallback) { /** @var Appwrite\Utopia\Response $response */ /** @var Utopia\Database\Database $dbForExternal*/ /** @var Appwrite\Event\Event $database */ /** @var Appwrite\Event\Event $audits */ - $type = Database::VAR_FLOAT; - - $collection = $dbForExternal->getCollection($collectionId); - - if ($collection->isEmpty()) { - throw new Exception('Collection not found', 404); - } - - /** @var string $format Name for custom range validation */ - $format = null; - - // Add range validator if either $min or $max is provided - if (!is_null($min) || !is_null($max)) { - $min = (is_null($min)) ? -INF : \intval($min); - $max = (is_null($max)) ? INF : \intval($max); - $format = 'int-range'; - } - - // integers are signed by default, and filters are hidden from the endpoint. - $signed = true; - $size = 0; - $filters = []; - - $success = $dbForExternal->addAttributeInQueue($collectionId, $attributeId, $type, $size, $required, $default, $signed, $array, $format, $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([ + return $attributesCallback(new Document([ '$collection' => $collectionId, '$id' => $attributeId, - 'type' => $type, - 'size' => $size, + 'type' => Database::VAR_FLOAT, 'required' => $required, + 'size' => 0, 'default' => $default, - 'signed' => $signed, - 'array' => $array, 'min' => $min, 'max' => $max, - '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->dynamic($attribute, Response::MODEL_ATTRIBUTE); + 'array' => $array, + ]), $response, $dbForExternal, $database, $audits); }); App::post('/v1/database/collections/:collectionId/attributes/boolean') @@ -507,56 +578,21 @@ App::post('/v1/database/collections/:collectionId/attributes/boolean') ->inject('dbForExternal') ->inject('database') ->inject('audits') - ->action(function ($collectionId, $attributeId, $required, $default, $array, $response, $dbForExternal, $database, $audits) { + ->action(function ($collectionId, $attributeId, $required, $default, $array, $response, $dbForExternal, $database, $audits) use ($attributesCallback) { /** @var Appwrite\Utopia\Response $response */ /** @var Utopia\Database\Database $dbForExternal*/ /** @var Appwrite\Event\Event $database */ /** @var Appwrite\Event\Event $audits */ - $type = Database::VAR_BOOLEAN; - - $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; - $size = 0; - $filters = []; - - $success = $dbForExternal->addAttributeInQueue($collectionId, $attributeId, $type, $size, $required, $default, $signed, $array, /*$format*/ null, $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([ + return $attributesCallback(new Document([ '$collection' => $collectionId, '$id' => $attributeId, - 'type' => $type, - 'size' => $size, + 'type' => Database::VAR_BOOLEAN, + 'size' => 0, '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->dynamic($attribute, Response::MODEL_ATTRIBUTE); + ]), $response, $dbForExternal, $database, $audits); }); App::get('/v1/database/collections/:collectionId/attributes') diff --git a/tests/e2e/Services/Database/DatabaseBase.php b/tests/e2e/Services/Database/DatabaseBase.php index d201dd9095..6fce95bf86 100644 --- a/tests/e2e/Services/Database/DatabaseBase.php +++ b/tests/e2e/Services/Database/DatabaseBase.php @@ -528,29 +528,25 @@ trait DatabaseBase $collectionId = $collection['body']['$id']; - $email = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/string', array_merge([ + $email = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/email', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'attributeId' => 'email', - 'size' => 256, 'required' => false, - 'format' => 'email', ]); - $ip = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/string', array_merge([ + $ip = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/ip', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'attributeId' => 'ip', - 'size' => 64, 'required' => false, - 'format' => 'ip', ]); - $url = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/string', array_merge([ + $url = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/url', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] @@ -558,11 +554,11 @@ trait DatabaseBase 'attributeId' => 'url', 'size' => 256, 'required' => false, - 'format' => 'url', ]); $range = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/integer', array_merge([ - 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'attributeId' => 'range', @@ -573,7 +569,8 @@ trait DatabaseBase // TODO@kodumbeats float validator rejects 0.0 and 1.0 as floats $floatRange = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/float', array_merge([ - 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'attributeId' => 'floatRange', @@ -583,7 +580,8 @@ trait DatabaseBase ]); $upperBound = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/integer', array_merge([ - 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'attributeId' => 'upperBound', @@ -592,7 +590,8 @@ trait DatabaseBase ]); $lowerBound = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/integer', array_merge([ - 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'attributeId' => 'lowerBound', @@ -604,16 +603,6 @@ trait DatabaseBase * Test for failure */ - $unsupported = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/string', array_merge([ - 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], - 'x-appwrite-key' => $this->getProject()['apiKey'] - ]), [ - 'attributeId' => 'unsupported', - 'size' => 256, - 'required' => false, - 'format' => 'unsupported', - ]); - // TODO@kodumbeats troubleshoot // $invalidRange = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/integer', array_merge([ // 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -632,13 +621,11 @@ trait DatabaseBase $this->assertEquals(201, $floatRange['headers']['status-code']); $this->assertEquals(201, $upperBound['headers']['status-code']); $this->assertEquals(201, $lowerBound['headers']['status-code']); - $this->assertEquals(400, $unsupported['headers']['status-code']); // $this->assertEquals(400, $invalidRange['headers']['status-code']); - $this->assertEquals('Invalid format: Value must be one of (email, ip, url)', $unsupported['body']['message']); // $this->assertEquals('Minimum value must be lesser than maximum value', $invalidRange['body']['message']); // wait for worker to add attributes - sleep(10); + sleep(15); $collection = $this->client->call(Client::METHOD_GET, '/database/collections/' . $collectionId, array_merge([ 'content-type' => 'application/json', @@ -646,7 +633,7 @@ trait DatabaseBase 'x-appwrite-key' => $this->getProject()['apiKey'], ]), []); - // var_dump($collection); + var_dump($collection); $this->assertCount(7, $collection['body']['attributes']); $this->assertCount(0, $collection['body']['attributesInQueue']); From 1eae5490df73a96df0f3c7e5205da90ed7a1d4ee Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Tue, 27 Jul 2021 14:19:37 -0400 Subject: [PATCH 17/21] Encode format as json string --- app/controllers/api/database.php | 28 +++++++++++++------- app/init.php | 20 +++++++++++--- tests/e2e/Services/Database/DatabaseBase.php | 1 + 3 files changed, 35 insertions(+), 14 deletions(-) diff --git a/app/controllers/api/database.php b/app/controllers/api/database.php index 071429b2dd..365c08455f 100644 --- a/app/controllers/api/database.php +++ b/app/controllers/api/database.php @@ -59,8 +59,11 @@ $attributesCallback = function ($attribute, $response, $dbForExternal, $database } } - if (!\is_null($format) && !Structure::hasFormat($format, $type)) { - throw new Exception("Format {$format} not available for {$type} attributes.", 400); + if (!\is_null($format)) { + $name = \json_decode($format, true)['name']; + if (!Structure::hasFormat($name, $type)) { + throw new Exception("Format {$name} not available for {$type} attributes.", 400); + } } if (!is_null($min) || !is_null($max)) { // Add range validator if either $min or $max is provided @@ -389,7 +392,7 @@ App::post('/v1/database/collections/:collectionId/attributes/email') 'required' => $required, 'default' => $default, 'array' => $array, - 'format' => 'email', + 'format' => \json_encode(['name'=>'email']), ]), $response, $dbForExternal, $database, $audits); }); @@ -428,7 +431,7 @@ App::post('/v1/database/collections/:collectionId/attributes/ip') 'required' => $required, 'default' => $default, 'array' => $array, - 'format' => 'ip', + 'format' => \json_encode(['name'=>'ip']), ]), $response, $dbForExternal, $database, $audits); }); @@ -468,7 +471,7 @@ App::post('/v1/database/collections/:collectionId/attributes/url') 'required' => $required, 'default' => $default, 'array' => $array, - 'format' => 'url', + 'format' => \json_encode(['name'=>'url']), ]), $response, $dbForExternal, $database, $audits); }); @@ -508,11 +511,13 @@ App::post('/v1/database/collections/:collectionId/attributes/integer') 'size' => 0, 'required' => $required, 'default' => $default, - 'min' => $min, - 'max' => $max, 'array' => $array, + 'format' => \json_encode([ + 'name'=>'int-range', + 'min' => $min, + 'max' => $max, + ]), ]), $response, $dbForExternal, $database, $audits); - }); App::post('/v1/database/collections/:collectionId/attributes/float') @@ -551,9 +556,12 @@ App::post('/v1/database/collections/:collectionId/attributes/float') 'required' => $required, 'size' => 0, 'default' => $default, - 'min' => $min, - 'max' => $max, 'array' => $array, + 'format' => \json_encode([ + 'name'=>'float-range', + 'min' => $min, + 'max' => $max, + ]), ]), $response, $dbForExternal, $database, $audits); }); diff --git a/app/init.php b/app/init.php index cbb8b792d3..8101ce810a 100644 --- a/app/init.php +++ b/app/init.php @@ -201,13 +201,25 @@ Structure::addFormat('url', function() { return new URL(); }, Database2::VAR_STRING); -Structure::addFormat('int-range', function($min, $max, $type) { +Structure::addFormat('int-range', function($attribute) { + // Format encoded as json string containing name and relevant options + // E.g. Range: $format = json_encode(['name'=>$name, 'min'=>$min, 'max'=>$max]); + $format = json_decode($attribute['format'], true); + $min = $format['min'] ?? -INF; + $max = $format['max'] ?? INF; + $type = $attribute['type']; return new Range($min, $max, $type); -}, Database2::VAR_INTEGER, ['min', 'max', 'type']); +}, Database2::VAR_INTEGER); -Structure::addFormat('float-range', function($min, $max, $type) { +Structure::addFormat('float-range', function($attribute) { + // Format encoded as json string containing name and relevant options + // E.g. Range: $format = json_encode(['name'=>$name, 'min'=>$min, 'max'=>$max]); + $format = json_decode($attribute['format'], true); + $min = $format['min'] ?? -INF; + $max = $format['max'] ?? INF; + $type = $attribute['type'] ?? ''; return new Range($min, $max, $type); -}, Database2::VAR_FLOAT, ['min', 'max', 'type']); +}, Database2::VAR_FLOAT); /* * Registry diff --git a/tests/e2e/Services/Database/DatabaseBase.php b/tests/e2e/Services/Database/DatabaseBase.php index 6fce95bf86..090d1978fc 100644 --- a/tests/e2e/Services/Database/DatabaseBase.php +++ b/tests/e2e/Services/Database/DatabaseBase.php @@ -568,6 +568,7 @@ trait DatabaseBase ]); // TODO@kodumbeats float validator rejects 0.0 and 1.0 as floats + // TODO@kodumbeats min and max are rounded in error message $floatRange = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/float', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], From 432a1a7f5c8d0b7a9d184ad24c4cc173389c1da7 Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Tue, 27 Jul 2021 15:12:24 -0400 Subject: [PATCH 18/21] Parse min and max as proper types --- app/controllers/api/database.php | 29 ++++++++------------ tests/e2e/Services/Database/DatabaseBase.php | 29 +++++++++++++------- 2 files changed, 31 insertions(+), 27 deletions(-) diff --git a/app/controllers/api/database.php b/app/controllers/api/database.php index 365c08455f..e2b6ccec72 100644 --- a/app/controllers/api/database.php +++ b/app/controllers/api/database.php @@ -67,17 +67,19 @@ $attributesCallback = function ($attribute, $response, $dbForExternal, $database } if (!is_null($min) || !is_null($max)) { // Add range validator if either $min or $max is provided - $min = (is_null($min)) ? -INF : \intval($min); - $max = (is_null($max)) ? INF : \intval($max); switch ($type) { - case Database::VAR_INTEGER: - $format = 'int-range'; - break; - case Database::VAR_FLOAT: - $format = 'float-range'; - break; - default: - throw new Exception("Format range not available for {$type} attributes.", 400); + case Database::VAR_INTEGER: + $min = (is_null($min)) ? -INF : \intval($min); + $max = (is_null($max)) ? INF : \intval($max); + $format = 'int-range'; + break; + case Database::VAR_FLOAT: + $min = (is_null($min)) ? -INF : \floatval($min); + $max = (is_null($max)) ? INF : \floatval($max); + $format = 'float-range'; + break; + default: + throw new Exception("Format range not available for {$type} attributes.", 400); } } @@ -729,13 +731,6 @@ App::delete('/v1/database/collections/:collectionId/attributes/:attributeId') $type = $attribute->getAttribute('type', ''); $format = $attribute->getAttribute('format', ''); - // Remove the range filter from ints/floats, if given - if ($type === Database::VAR_INTEGER || $type === Database::VAR_FLOAT) { - if ($format) { - Structure::removeFormat($format); - } - } - $database ->setParam('type', DELETE_TYPE_ATTRIBUTE) ->setParam('document', $attribute) diff --git a/tests/e2e/Services/Database/DatabaseBase.php b/tests/e2e/Services/Database/DatabaseBase.php index 090d1978fc..755321ed73 100644 --- a/tests/e2e/Services/Database/DatabaseBase.php +++ b/tests/e2e/Services/Database/DatabaseBase.php @@ -567,7 +567,6 @@ trait DatabaseBase 'max' => 10, ]); - // TODO@kodumbeats float validator rejects 0.0 and 1.0 as floats // TODO@kodumbeats min and max are rounded in error message $floatRange = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/float', array_merge([ 'content-type' => 'application/json', @@ -576,10 +575,22 @@ trait DatabaseBase ]), [ 'attributeId' => 'floatRange', 'required' => false, - 'min' => 0.5, - 'max' => 1.5, + 'min' => 1.1, + 'max' => 1.4, ]); + // TODO@kodumbeats float validator rejects 0.0 and 1.0 as floats + // $probability = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/float', array_merge([ + // 'content-type' => 'application/json', + // 'x-appwrite-project' => $this->getProject()['$id'], + // 'x-appwrite-key' => $this->getProject()['apiKey'] + // ]), [ + // 'attributeId' => 'probability', + // 'required' => false, + // 'min' => \floatval(0.0), + // 'max' => \floatval(1.0), + // ]); + $upperBound = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/integer', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -626,7 +637,7 @@ trait DatabaseBase // $this->assertEquals('Minimum value must be lesser than maximum value', $invalidRange['body']['message']); // wait for worker to add attributes - sleep(15); + sleep(10); $collection = $this->client->call(Client::METHOD_GET, '/database/collections/' . $collectionId, array_merge([ 'content-type' => 'application/json', @@ -634,8 +645,6 @@ trait DatabaseBase 'x-appwrite-key' => $this->getProject()['apiKey'], ]), []); - var_dump($collection); - $this->assertCount(7, $collection['body']['attributes']); $this->assertCount(0, $collection['body']['attributesInQueue']); @@ -692,7 +701,7 @@ trait DatabaseBase 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'data' => [ - 'floatRange' => 0.8, + 'floatRange' => 1.4, ], 'read' => ['user:'.$this->getUser()['$id']], 'write' => ['user:'.$this->getUser()['$id']], @@ -778,7 +787,7 @@ trait DatabaseBase 'write' => ['user:'.$this->getUser()['$id']], ]); - $badProbability = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/documents', array_merge([ + $badFloatRange = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/documents', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ @@ -816,14 +825,14 @@ trait DatabaseBase $this->assertEquals(400, $badIp['headers']['status-code']); $this->assertEquals(400, $badUrl['headers']['status-code']); $this->assertEquals(400, $badRange['headers']['status-code']); - $this->assertEquals(400, $badProbability['headers']['status-code']); + $this->assertEquals(400, $badFloatRange['headers']['status-code']); $this->assertEquals(400, $tooHigh['headers']['status-code']); $this->assertEquals(400, $tooLow['headers']['status-code']); $this->assertEquals('Invalid document structure: Attribute "email" has invalid format. Value must be a valid email address', $badEmail['body']['message']); $this->assertEquals('Invalid document structure: Attribute "ip" has invalid format. Value must be a valid IP address', $badIp['body']['message']); $this->assertEquals('Invalid document structure: Attribute "url" has invalid format. Value must be a valid URL', $badUrl['body']['message']); $this->assertEquals('Invalid document structure: Attribute "range" has invalid format. Value must be a valid range between 1 and 10', $badRange['body']['message']); - $this->assertEquals('Invalid document structure: Attribute "floatRange" has invalid format. Value must be a valid range between 1 and 2', $badProbability['body']['message']); + $this->assertEquals('Invalid document structure: Attribute "floatRange" has invalid format. Value must be a valid range between 1 and 1', $badFloatRange['body']['message']); $this->assertEquals('Invalid document structure: Attribute "upperBound" has invalid format. Value must be a valid range between inf and 10', $tooHigh['body']['message']); $this->assertEquals('Invalid document structure: Attribute "lowerBound" has invalid format. Value must be a valid range between 5 and inf', $tooLow['body']['message']); } From d7126e6d8bafe108ed4d7b4d93d7a9801c1eff70 Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Mon, 2 Aug 2021 16:11:10 -0400 Subject: [PATCH 19/21] Fix tests --- composer.lock | 65 +++++++++---------- .../Webhooks/WebhooksCustomServerTest.php | 2 +- 2 files changed, 33 insertions(+), 34 deletions(-) diff --git a/composer.lock b/composer.lock index 3b7c1c3591..dbc8430840 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": "58102c809e5d82ce2c41dfca515a18dc", + "content-hash": "e7dd41744fe6d89355623d5425d6ccd4", "packages": [ { "name": "adhocore/jwt", @@ -1984,17 +1984,11 @@ }, { "name": "utopia-php/database", - "version": "0.5.0", + "version": "dev-feat-attribute-format-validators", "source": { "type": "git", - "url": "https://github.com/utopia-php/database.git", - "reference": "e050e51060df72eff3af9fc24fc95a41ca9a2096" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/e050e51060df72eff3af9fc24fc95a41ca9a2096", - "reference": "e050e51060df72eff3af9fc24fc95a41ca9a2096", - "shasum": "" + "url": "https://github.com/utopia-php/database", + "reference": "d54d9c3de3a11796c0454548cb62a5296f601259" }, "require": { "ext-mongodb": "*", @@ -2017,7 +2011,11 @@ "Utopia\\Database\\": "src/Database" } }, - "notification-url": "https://packagist.org/downloads/", + "autoload-dev": { + "psr-4": { + "Utopia\\Tests\\": "tests/Database" + } + }, "license": [ "MIT" ], @@ -2039,11 +2037,7 @@ "upf", "utopia" ], - "support": { - "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/0.5.0" - }, - "time": "2021-07-03T16:49:44+00:00" + "time": "2021-07-27T18:20:30+00:00" }, { "name": "utopia-php/domains", @@ -2101,17 +2095,11 @@ }, { "name": "utopia-php/framework", - "version": "0.16.2", + "version": "dev-feat-range-validator-infinity", "source": { "type": "git", - "url": "https://github.com/utopia-php/framework.git", - "reference": "df02354a670df366b92e2e927fbf128be9a8e64e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/utopia-php/framework/zipball/df02354a670df366b92e2e927fbf128be9a8e64e", - "reference": "df02354a670df366b92e2e927fbf128be9a8e64e", - "shasum": "" + "url": "https://github.com/utopia-php/framework", + "reference": "c42cce848c6c20d0c4b4b75105dd15b5d8234e28" }, "require": { "php": ">=7.3.0" @@ -2126,7 +2114,6 @@ "Utopia\\": "src/" } }, - "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], @@ -2142,11 +2129,7 @@ "php", "upf" ], - "support": { - "issues": "https://github.com/utopia-php/framework/issues", - "source": "https://github.com/utopia-php/framework/tree/0.16.2" - }, - "time": "2021-07-20T10:24:56+00:00" + "time": "2021-08-02T16:53:46+00:00" }, { "name": "utopia-php/image", @@ -6255,9 +6238,25 @@ "time": "2015-12-17T08:42:14+00:00" } ], - "aliases": [], + "aliases": [ + { + "package": "utopia-php/database", + "version": "dev-feat-attribute-format-validators", + "alias": "0.5.1", + "alias_normalized": "0.5.1.0" + }, + { + "package": "utopia-php/framework", + "version": "dev-feat-range-validator-infinity", + "alias": "0.16.2", + "alias_normalized": "0.16.2.0" + } + ], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": { + "utopia-php/framework": 20, + "utopia-php/database": 20 + }, "prefer-stable": false, "prefer-lowest": false, "platform": { diff --git a/tests/e2e/Services/Webhooks/WebhooksCustomServerTest.php b/tests/e2e/Services/Webhooks/WebhooksCustomServerTest.php index 2f254b0e6d..4de19f3876 100644 --- a/tests/e2e/Services/Webhooks/WebhooksCustomServerTest.php +++ b/tests/e2e/Services/Webhooks/WebhooksCustomServerTest.php @@ -64,7 +64,7 @@ class WebhooksCustomServerTest extends Scope 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'id' => 'fullname', - 'type' => 'text', + 'type' => 'key', 'attributes' => ['lastName', 'firstName'], 'orders' => ['ASC', 'ASC'], ]); From 3846d66868b77da6d1c603e489c23f6072ece462 Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Mon, 2 Aug 2021 16:11:24 -0400 Subject: [PATCH 20/21] Use sdk.auth instead of sdk.platform --- app/controllers/api/database.php | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/app/controllers/api/database.php b/app/controllers/api/database.php index 1458fd6975..649c346452 100644 --- a/app/controllers/api/database.php +++ b/app/controllers/api/database.php @@ -327,7 +327,6 @@ App::post('/v1/database/collections/:collectionId/attributes/string') ->label('scope', 'attributes.write') ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.namespace', 'database') - ->label('sdk.platform', [APP_PLATFORM_SERVER]) ->label('sdk.method', 'createStringAttribute') ->label('sdk.description', '/docs/references/database/create-attribute-string.md') ->label('sdk.response.code', Response::STATUS_CODE_CREATED) @@ -366,7 +365,7 @@ App::post('/v1/database/collections/:collectionId/attributes/email') ->label('event', 'database.attributes.create') ->label('scope', 'attributes.write') ->label('sdk.namespace', 'database') - ->label('sdk.platform', [APP_PLATFORM_SERVER]) + ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.method', 'createEmailAttribute') ->label('sdk.description', '/docs/references/database/create-attribute-email.md') ->label('sdk.response.code', Response::STATUS_CODE_CREATED) @@ -405,7 +404,7 @@ App::post('/v1/database/collections/:collectionId/attributes/ip') ->label('event', 'database.attributes.create') ->label('scope', 'attributes.write') ->label('sdk.namespace', 'database') - ->label('sdk.platform', [APP_PLATFORM_SERVER]) + ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.method', 'createIpAttribute') ->label('sdk.description', '/docs/references/database/create-attribute-ip.md') ->label('sdk.response.code', Response::STATUS_CODE_CREATED) @@ -444,7 +443,7 @@ App::post('/v1/database/collections/:collectionId/attributes/url') ->label('event', 'database.attributes.create') ->label('scope', 'attributes.write') ->label('sdk.namespace', 'database') - ->label('sdk.platform', [APP_PLATFORM_SERVER]) + ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.method', 'createUrlAttribute') ->label('sdk.description', '/docs/references/database/create-attribute-url.md') ->label('sdk.response.code', Response::STATUS_CODE_CREATED) @@ -484,7 +483,7 @@ App::post('/v1/database/collections/:collectionId/attributes/integer') ->label('event', 'database.attributes.create') ->label('scope', 'attributes.write') ->label('sdk.namespace', 'database') - ->label('sdk.platform', [APP_PLATFORM_SERVER]) + ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.method', 'createIntegerAttribute') ->label('sdk.description', '/docs/references/database/create-attribute-integer.md') ->label('sdk.response.code', Response::STATUS_CODE_CREATED) @@ -529,7 +528,7 @@ App::post('/v1/database/collections/:collectionId/attributes/float') ->label('event', 'database.attributes.create') ->label('scope', 'attributes.write') ->label('sdk.namespace', 'database') - ->label('sdk.platform', [APP_PLATFORM_SERVER]) + ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.method', 'createFloatAttribute') ->label('sdk.description', '/docs/references/database/create-attribute-float.md') ->label('sdk.response.code', Response::STATUS_CODE_CREATED) @@ -574,7 +573,7 @@ App::post('/v1/database/collections/:collectionId/attributes/boolean') ->label('event', 'database.attributes.create') ->label('scope', 'attributes.write') ->label('sdk.namespace', 'database') - ->label('sdk.platform', [APP_PLATFORM_SERVER]) + ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) ->label('sdk.method', 'createBooleanAttribute') ->label('sdk.description', '/docs/references/database/create-attribute-boolean.md') ->label('sdk.response.code', Response::STATUS_CODE_CREATED) From 94eeb2d8164b1dabd0d87ffdcf5cc37239456460 Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Tue, 3 Aug 2021 10:38:04 -0400 Subject: [PATCH 21/21] Update utopia-php/framework to 0.17 --- composer.json | 6 +----- composer.lock | 29 ++++++++++++++++------------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/composer.json b/composer.json index 2e32462894..943c73f72c 100644 --- a/composer.json +++ b/composer.json @@ -38,7 +38,7 @@ "appwrite/php-clamav": "1.1.*", "appwrite/php-runtimes": "0.4.*", - "utopia-php/framework": "dev-feat-range-validator-infinity as 0.16.2", + "utopia-php/framework": "0.17.*", "utopia-php/abuse": "0.6.*", "utopia-php/analytics": "0.2.*", "utopia-php/audit": "0.6.*", @@ -66,10 +66,6 @@ { "type": "git", "url": "https://github.com/utopia-php/database" - }, - { - "type": "git", - "url": "https://github.com/utopia-php/framework" } ], "require-dev": { diff --git a/composer.lock b/composer.lock index dbc8430840..ca9b5de9fb 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": "e7dd41744fe6d89355623d5425d6ccd4", + "content-hash": "6c240e73a06b648ec38d71e56ffbb67c", "packages": [ { "name": "adhocore/jwt", @@ -2095,11 +2095,17 @@ }, { "name": "utopia-php/framework", - "version": "dev-feat-range-validator-infinity", + "version": "0.17.3", "source": { "type": "git", - "url": "https://github.com/utopia-php/framework", - "reference": "c42cce848c6c20d0c4b4b75105dd15b5d8234e28" + "url": "https://github.com/utopia-php/framework.git", + "reference": "0274f6b3e49db2af0d702edf278ec7504dc99878" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/framework/zipball/0274f6b3e49db2af0d702edf278ec7504dc99878", + "reference": "0274f6b3e49db2af0d702edf278ec7504dc99878", + "shasum": "" }, "require": { "php": ">=7.3.0" @@ -2114,6 +2120,7 @@ "Utopia\\": "src/" } }, + "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], @@ -2129,7 +2136,11 @@ "php", "upf" ], - "time": "2021-08-02T16:53:46+00:00" + "support": { + "issues": "https://github.com/utopia-php/framework/issues", + "source": "https://github.com/utopia-php/framework/tree/0.17.3" + }, + "time": "2021-08-03T13:57:01+00:00" }, { "name": "utopia-php/image", @@ -5137,7 +5148,6 @@ "type": "github" } ], - "abandoned": true, "time": "2020-09-28T06:45:17+00:00" }, { @@ -6244,17 +6254,10 @@ "version": "dev-feat-attribute-format-validators", "alias": "0.5.1", "alias_normalized": "0.5.1.0" - }, - { - "package": "utopia-php/framework", - "version": "dev-feat-range-validator-infinity", - "alias": "0.16.2", - "alias_normalized": "0.16.2.0" } ], "minimum-stability": "stable", "stability-flags": { - "utopia-php/framework": 20, "utopia-php/database": 20 }, "prefer-stable": false,