From 9e4343e2baac70a809433e85c9e587567f388aca Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Fri, 10 Sep 2021 16:09:11 -0400 Subject: [PATCH 1/9] Create route for enum attribute --- app/controllers/api/database.php | 49 ++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/app/controllers/api/database.php b/app/controllers/api/database.php index 44817cc0a1..78df069ede 100644 --- a/app/controllers/api/database.php +++ b/app/controllers/api/database.php @@ -535,6 +535,55 @@ App::post('/v1/database/collections/:collectionId/attributes/email') $response->dynamic($attribute, Response::MODEL_ATTRIBUTE_EMAIL); }); +App::post('/v1/database/collections/:collectionId/attributes/enum') + ->desc('Create Enum Attribute') + ->groups(['api', 'database']) + ->label('event', 'database.attributes.create') + ->label('scope', 'collections.write') + ->label('sdk.namespace', 'database') + ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) + ->label('sdk.method', 'createEnumAttribute') + ->label('sdk.description', '/docs/references/database/create-attribute-enum.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('elements', [], new ArrayList(new Text(0)), 'Array of elements in enumerated type. Uses length of longest element to determine size.') + ->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('dbForInternal') + ->inject('database') + ->inject('audits') + ->action(function ($collectionId, $attributeId, $elements, $required, $default, $array, $response, $dbForInternal, $database, $audits) { + /** @var Appwrite\Utopia\Response $response */ + /** @var Utopia\Database\Database $dbForInternal*/ + /** @var Appwrite\Event\Event $database */ + /** @var Appwrite\Event\Event $audits */ + + // use length of longest string as attribute size + $size = 0; + foreach ($elements as $element) { + $length = \strlen($element); + $size = ($length > $size) ? $length : $size; + } + + $attribute = createAttribute($collectionId, new Document([ + '$id' => $attributeId, + 'type' => Database::VAR_STRING, + 'size' => $size, + 'required' => $required, + 'default' => $default, + 'array' => $array, + 'format' => APP_DATABASE_ATTRIBUTE_ENUM, + 'formatOptions' => ['elements' => $elements], + ]), $response, $dbForInternal, $database, $audits); + + $response->dynamic($attribute, Response::MODEL_ATTRIBUTE_ENUM); + }); + App::post('/v1/database/collections/:collectionId/attributes/ip') ->desc('Create IP Address Attribute') ->groups(['api', 'database']) From 42ff17d163207044aa00df83d0f8691107368161 Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Fri, 10 Sep 2021 16:12:22 -0400 Subject: [PATCH 2/9] Use whitelist validator to enforce enum --- app/init.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/init.php b/app/init.php index cdaf8cfeb8..d1cc3fda11 100644 --- a/app/init.php +++ b/app/init.php @@ -44,6 +44,7 @@ use Utopia\Database\Database; use Utopia\Database\Validator\Structure; use Utopia\Database\Validator\Authorization; use Utopia\Validator\Range; +use Utopia\Validator\WhiteList; use Swoole\Database\PDOConfig; use Swoole\Database\PDOPool; use Swoole\Database\RedisConfig; @@ -63,6 +64,7 @@ const APP_LIMIT_USERS = 10000; const APP_CACHE_BUSTER = 151; const APP_VERSION_STABLE = '0.9.4'; const APP_DATABASE_ATTRIBUTE_EMAIL = 'email'; +const APP_DATABASE_ATTRIBUTE_ENUM = 'enum'; const APP_DATABASE_ATTRIBUTE_IP = 'ip'; const APP_DATABASE_ATTRIBUTE_URL = 'url'; const APP_DATABASE_ATTRIBUTE_INT_RANGE = 'intRange'; @@ -270,6 +272,11 @@ Structure::addFormat(APP_DATABASE_ATTRIBUTE_EMAIL, function() { return new Email(); }, Database::VAR_STRING); +Structure::addFormat(APP_DATABASE_ATTRIBUTE_ENUM, function($attribute) { + $elements = $attribute['formatOptions']['elements']; + return new WhiteList($elements); +}, Database::VAR_STRING); + Structure::addFormat(APP_DATABASE_ATTRIBUTE_IP, function() { return new IP(); }, Database::VAR_STRING); From 23611857a1b47aacd5518ffed0b81f82b9ac3289 Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Fri, 10 Sep 2021 16:12:55 -0400 Subject: [PATCH 3/9] Add response model for enum attribute --- .../Utopia/Response/Model/AttributeEnum.php | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 src/Appwrite/Utopia/Response/Model/AttributeEnum.php diff --git a/src/Appwrite/Utopia/Response/Model/AttributeEnum.php b/src/Appwrite/Utopia/Response/Model/AttributeEnum.php new file mode 100644 index 0000000000..6db73fce6b --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/AttributeEnum.php @@ -0,0 +1,61 @@ +addRule('elements', [ + 'type' => self::TYPE_STRING, + 'description' => 'Array of elements in enumerated type.', + 'default' => null, + 'example' => 'element', + 'array' => true, + 'require' => true, + ]) + ->addRule('format', [ + 'type' => self::TYPE_STRING, + 'description' => 'String format.', + 'default' => APP_DATABASE_ATTRIBUTE_ENUM, + 'example' => APP_DATABASE_ATTRIBUTE_ENUM, + 'array' => false, + 'require' => true, + ]) + ->addRule('default', [ + 'type' => self::TYPE_STRING, + 'description' => 'Default value for attribute when not provided. Cannot be set when attribute is required.', + 'default' => null, + 'example' => 'element', + 'array' => false, + 'require' => false, + ]) + ; + } + + /** + * Get Name + * + * @return string + */ + public function getName():string + { + return 'AttributeEnum'; + } + + /** + * Get Collection + * + * @return string + */ + public function getType():string + { + return Response::MODEL_ATTRIBUTE_ENUM; + } +} \ No newline at end of file From 7fbe95de300c7cf77078cc00301f75afe3615e40 Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Fri, 10 Sep 2021 16:14:12 -0400 Subject: [PATCH 4/9] Use enum filter to respond with proper enum model --- app/config/collections2.php | 2 +- app/controllers/api/database.php | 1 + app/init.php | 16 ++++++++++++++++ src/Appwrite/Utopia/Response.php | 13 ++++++++----- .../Utopia/Response/Model/AttributeList.php | 1 + .../Utopia/Response/Model/Collection.php | 1 + 6 files changed, 28 insertions(+), 6 deletions(-) diff --git a/app/config/collections2.php b/app/config/collections2.php index 6e13f19f27..e4f5c0147b 100644 --- a/app/config/collections2.php +++ b/app/config/collections2.php @@ -214,7 +214,7 @@ $collections = [ 'required' => false, 'default' => new stdClass, 'array' => false, - 'filters' => ['json', 'range'], + 'filters' => ['json', 'range', 'enum'], ], [ '$id' => 'filters', diff --git a/app/controllers/api/database.php b/app/controllers/api/database.php index 78df069ede..6c284e207f 100644 --- a/app/controllers/api/database.php +++ b/app/controllers/api/database.php @@ -900,6 +900,7 @@ App::get('/v1/database/collections/:collectionId/attributes/:attributeId') Database::VAR_FLOAT => Response::MODEL_ATTRIBUTE_FLOAT, Database::VAR_STRING => match($format) { APP_DATABASE_ATTRIBUTE_EMAIL => Response::MODEL_ATTRIBUTE_EMAIL, + APP_DATABASE_ATTRIBUTE_ENUM => Response::MODEL_ATTRIBUTE_ENUM, APP_DATABASE_ATTRIBUTE_IP => Response::MODEL_ATTRIBUTE_IP, APP_DATABASE_ATTRIBUTE_URL => Response::MODEL_ATTRIBUTE_URL, default => Response::MODEL_ATTRIBUTE_STRING, diff --git a/app/init.php b/app/init.php index d1cc3fda11..54a6428cd7 100644 --- a/app/init.php +++ b/app/init.php @@ -198,6 +198,22 @@ Database::addFilter('casting', } ); +Database::addFilter('enum', + function($value, Document $attribute) { + if ($attribute->isSet('elements')) { + $attribute->removeAttribute('elements'); + } + return $value; + }, + function($value, Document $attribute) { + $formatOptions = json_decode($attribute->getAttribute('formatOptions', []), true); + if (isset($formatOptions['elements'])) { + $attribute->setAttribute('elements', $formatOptions['elements']); + } + return $value; + } +); + Database::addFilter('range', function($value, Document $attribute) { if ($attribute->isSet('min')) { diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index a653ddbb0f..6a1ff23078 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -17,6 +17,7 @@ use Appwrite\Utopia\Response\Model\AttributeInteger; use Appwrite\Utopia\Response\Model\AttributeFloat; use Appwrite\Utopia\Response\Model\AttributeBoolean; use Appwrite\Utopia\Response\Model\AttributeEmail; +use Appwrite\Utopia\Response\Model\AttributeEnum; use Appwrite\Utopia\Response\Model\AttributeIP; use Appwrite\Utopia\Response\Model\AttributeURL; use Appwrite\Utopia\Response\Model\BaseList; @@ -79,11 +80,12 @@ class Response extends SwooleResponse const MODEL_ATTRIBUTE = 'attribute'; const MODEL_ATTRIBUTE_LIST = 'attributeList'; const MODEL_ATTRIBUTE_STRING = 'attributeString'; - const MODEL_ATTRIBUTE_INTEGER= 'attributeInteger'; - const MODEL_ATTRIBUTE_FLOAT= 'attributeFloat'; - const MODEL_ATTRIBUTE_BOOLEAN= 'attributeBoolean'; - const MODEL_ATTRIBUTE_EMAIL= 'attributeEmail'; - const MODEL_ATTRIBUTE_IP= 'attributeIp'; + const MODEL_ATTRIBUTE_INTEGER = 'attributeInteger'; + const MODEL_ATTRIBUTE_FLOAT = 'attributeFloat'; + const MODEL_ATTRIBUTE_BOOLEAN = 'attributeBoolean'; + const MODEL_ATTRIBUTE_EMAIL = 'attributeEmail'; + const MODEL_ATTRIBUTE_ENUM = 'attributeEnum'; + const MODEL_ATTRIBUTE_IP = 'attributeIp'; const MODEL_ATTRIBUTE_URL= 'attributeUrl'; // Users @@ -201,6 +203,7 @@ class Response extends SwooleResponse ->setModel(new AttributeFloat()) ->setModel(new AttributeBoolean()) ->setModel(new AttributeEmail()) + ->setModel(new AttributeEnum()) ->setModel(new AttributeIP()) ->setModel(new AttributeURL()) ->setModel(new Index()) diff --git a/src/Appwrite/Utopia/Response/Model/AttributeList.php b/src/Appwrite/Utopia/Response/Model/AttributeList.php index 26ea146ec8..69e083cede 100644 --- a/src/Appwrite/Utopia/Response/Model/AttributeList.php +++ b/src/Appwrite/Utopia/Response/Model/AttributeList.php @@ -29,6 +29,7 @@ class AttributeList extends Model self::TYPE_FLOAT => Response::MODEL_ATTRIBUTE_FLOAT, self::TYPE_STRING => match($attribute->getAttribute('format')) { APP_DATABASE_ATTRIBUTE_EMAIL => Response::MODEL_ATTRIBUTE_EMAIL, + APP_DATABASE_ATTRIBUTE_ENUM => Response::MODEL_ATTRIBUTE_ENUM, APP_DATABASE_ATTRIBUTE_IP => Response::MODEL_ATTRIBUTE_IP, APP_DATABASE_ATTRIBUTE_URL => Response::MODEL_ATTRIBUTE_URL, default => Response::MODEL_ATTRIBUTE_STRING, diff --git a/src/Appwrite/Utopia/Response/Model/Collection.php b/src/Appwrite/Utopia/Response/Model/Collection.php index f023be261f..f59674bbf2 100644 --- a/src/Appwrite/Utopia/Response/Model/Collection.php +++ b/src/Appwrite/Utopia/Response/Model/Collection.php @@ -57,6 +57,7 @@ class Collection extends Model self::TYPE_FLOAT => Response::MODEL_ATTRIBUTE_FLOAT, self::TYPE_STRING => match($attribute->getAttribute('format')) { APP_DATABASE_ATTRIBUTE_EMAIL => Response::MODEL_ATTRIBUTE_EMAIL, + APP_DATABASE_ATTRIBUTE_ENUM => Response::MODEL_ATTRIBUTE_ENUM, APP_DATABASE_ATTRIBUTE_IP => Response::MODEL_ATTRIBUTE_IP, APP_DATABASE_ATTRIBUTE_URL => Response::MODEL_ATTRIBUTE_URL, default => Response::MODEL_ATTRIBUTE_STRING, From d8ba1095cd30d1ba63a5756f51c7804a7571ce1c Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Fri, 10 Sep 2021 16:14:31 -0400 Subject: [PATCH 5/9] Test enum attribute creation and response model --- tests/e2e/Services/Database/DatabaseBase.php | 207 ++++++++++++------- 1 file changed, 132 insertions(+), 75 deletions(-) diff --git a/tests/e2e/Services/Database/DatabaseBase.php b/tests/e2e/Services/Database/DatabaseBase.php index f0c3d4e7b3..041935f11a 100644 --- a/tests/e2e/Services/Database/DatabaseBase.php +++ b/tests/e2e/Services/Database/DatabaseBase.php @@ -143,6 +143,17 @@ trait DatabaseBase 'default' => 'default@example.com', ]); + $enum = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/enum', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'attributeId' => 'enum', + 'elements' => ['yes', 'no', 'maybe'], + 'required' => false, + 'default' => 'maybe', + ]); + $ip = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/ip', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -210,11 +221,23 @@ trait DatabaseBase $this->assertEquals('email', $email['body']['key']); $this->assertEquals('string', $email['body']['type']); $this->assertEquals('processing', $email['body']['status']); + $this->assertEquals('processing', $email['body']['status']); $this->assertEquals(false, $email['body']['required']); $this->assertEquals(false, $email['body']['array']); $this->assertEquals('email', $email['body']['format']); $this->assertEquals('default@example.com', $email['body']['default']); + $this->assertEquals(201, $enum['headers']['status-code']); + $this->assertEquals('enum', $enum['body']['key']); + $this->assertEquals('string', $enum['body']['type']); + $this->assertEquals('processing', $enum['body']['status']); + $this->assertEquals(false, $enum['body']['required']); + $this->assertEquals(false, $enum['body']['array']); + $this->assertEquals('enum', $enum['body']['format']); + $this->assertEquals('maybe', $enum['body']['default']); + $this->assertIsArray($enum['body']['elements']); + $this->assertEquals(['yes', 'no', 'maybe'], $enum['body']['elements']); + $this->assertEquals(201, $ip['headers']['status-code']); $this->assertEquals('ip', $ip['body']['key']); $this->assertEquals('string', $ip['body']['type']); @@ -276,6 +299,12 @@ trait DatabaseBase 'x-appwrite-key' => $this->getProject()['apiKey'] ])); + $enumResponse = $this->client->call(Client::METHOD_GET, "/database/collections/{$collectionId}/attributes/{$collectionId}_{$enum['body']['key']}",array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ])); + $ipResponse = $this->client->call(Client::METHOD_GET, "/database/collections/{$collectionId}/attributes/{$collectionId}_{$ip['body']['key']}",array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -324,6 +353,16 @@ trait DatabaseBase $this->assertEquals($email['body']['format'], $emailResponse['body']['format']); $this->assertEquals($email['body']['default'], $emailResponse['body']['default']); + $this->assertEquals(200, $enumResponse['headers']['status-code']); + $this->assertEquals($enum['body']['key'], $enumResponse['body']['key']); + $this->assertEquals($enum['body']['type'], $enumResponse['body']['type']); + $this->assertEquals('available', $enumResponse['body']['status']); + $this->assertEquals($enum['body']['required'], $enumResponse['body']['required']); + $this->assertEquals($enum['body']['array'], $enumResponse['body']['array']); + $this->assertEquals($enum['body']['format'], $enumResponse['body']['format']); + $this->assertEquals($enum['body']['default'], $enumResponse['body']['default']); + $this->assertEquals($enum['body']['elements'], $enumResponse['body']['elements']); + $this->assertEquals(200, $ipResponse['headers']['status-code']); $this->assertEquals($ip['body']['key'], $ipResponse['body']['key']); $this->assertEquals($ip['body']['type'], $ipResponse['body']['type']); @@ -377,12 +416,12 @@ trait DatabaseBase ])); $this->assertEquals(200, $attributes['headers']['status-code']); - $this->assertEquals(7, $attributes['body']['sum']); + $this->assertEquals(8, $attributes['body']['sum']); $attributes = $attributes['body']['attributes']; $this->assertIsArray($attributes); - $this->assertCount(7, $attributes); + $this->assertCount(8, $attributes); $this->assertEquals($stringResponse['body']['key'], $attributes[0]['key']); $this->assertEquals($stringResponse['body']['type'], $attributes[0]['type']); @@ -400,46 +439,55 @@ trait DatabaseBase $this->assertEquals($emailResponse['body']['default'], $attributes[1]['default']); $this->assertEquals($emailResponse['body']['format'], $attributes[1]['format']); - $this->assertEquals($ipResponse['body']['key'], $attributes[2]['key']); - $this->assertEquals($ipResponse['body']['type'], $attributes[2]['type']); - $this->assertEquals($ipResponse['body']['status'], $attributes[2]['status']); - $this->assertEquals($ipResponse['body']['required'], $attributes[2]['required']); - $this->assertEquals($ipResponse['body']['array'], $attributes[2]['array']); - $this->assertEquals($ipResponse['body']['default'], $attributes[2]['default']); - $this->assertEquals($ipResponse['body']['format'], $attributes[2]['format']); + $this->assertEquals($enumResponse['body']['key'], $attributes[2]['key']); + $this->assertEquals($enumResponse['body']['type'], $attributes[2]['type']); + $this->assertEquals($enumResponse['body']['status'], $attributes[2]['status']); + $this->assertEquals($enumResponse['body']['required'], $attributes[2]['required']); + $this->assertEquals($enumResponse['body']['array'], $attributes[2]['array']); + $this->assertEquals($enumResponse['body']['default'], $attributes[2]['default']); + $this->assertEquals($enumResponse['body']['format'], $attributes[2]['format']); + $this->assertEquals($enumResponse['body']['elements'], $attributes[2]['elements']); - $this->assertEquals($urlResponse['body']['key'], $attributes[3]['key']); - $this->assertEquals($urlResponse['body']['type'], $attributes[3]['type']); - $this->assertEquals($urlResponse['body']['status'], $attributes[3]['status']); - $this->assertEquals($urlResponse['body']['required'], $attributes[3]['required']); - $this->assertEquals($urlResponse['body']['array'], $attributes[3]['array']); - $this->assertEquals($urlResponse['body']['default'], $attributes[3]['default']); - $this->assertEquals($urlResponse['body']['format'], $attributes[3]['format']); + $this->assertEquals($ipResponse['body']['key'], $attributes[3]['key']); + $this->assertEquals($ipResponse['body']['type'], $attributes[3]['type']); + $this->assertEquals($ipResponse['body']['status'], $attributes[3]['status']); + $this->assertEquals($ipResponse['body']['required'], $attributes[3]['required']); + $this->assertEquals($ipResponse['body']['array'], $attributes[3]['array']); + $this->assertEquals($ipResponse['body']['default'], $attributes[3]['default']); + $this->assertEquals($ipResponse['body']['format'], $attributes[3]['format']); - $this->assertEquals($integerResponse['body']['key'], $attributes[4]['key']); - $this->assertEquals($integerResponse['body']['type'], $attributes[4]['type']); - $this->assertEquals($integerResponse['body']['status'], $attributes[4]['status']); - $this->assertEquals($integerResponse['body']['required'], $attributes[4]['required']); - $this->assertEquals($integerResponse['body']['array'], $attributes[4]['array']); - $this->assertEquals($integerResponse['body']['default'], $attributes[4]['default']); - $this->assertEquals($integerResponse['body']['min'], $attributes[4]['min']); - $this->assertEquals($integerResponse['body']['max'], $attributes[4]['max']); + $this->assertEquals($urlResponse['body']['key'], $attributes[4]['key']); + $this->assertEquals($urlResponse['body']['type'], $attributes[4]['type']); + $this->assertEquals($urlResponse['body']['status'], $attributes[4]['status']); + $this->assertEquals($urlResponse['body']['required'], $attributes[4]['required']); + $this->assertEquals($urlResponse['body']['array'], $attributes[4]['array']); + $this->assertEquals($urlResponse['body']['default'], $attributes[4]['default']); + $this->assertEquals($urlResponse['body']['format'], $attributes[4]['format']); - $this->assertEquals($floatResponse['body']['key'], $attributes[5]['key']); - $this->assertEquals($floatResponse['body']['type'], $attributes[5]['type']); - $this->assertEquals($floatResponse['body']['status'], $attributes[5]['status']); - $this->assertEquals($floatResponse['body']['required'], $attributes[5]['required']); - $this->assertEquals($floatResponse['body']['array'], $attributes[5]['array']); - $this->assertEquals($floatResponse['body']['default'], $attributes[5]['default']); - $this->assertEquals($floatResponse['body']['min'], $attributes[5]['min']); - $this->assertEquals($floatResponse['body']['max'], $attributes[5]['max']); + $this->assertEquals($integerResponse['body']['key'], $attributes[5]['key']); + $this->assertEquals($integerResponse['body']['type'], $attributes[5]['type']); + $this->assertEquals($integerResponse['body']['status'], $attributes[5]['status']); + $this->assertEquals($integerResponse['body']['required'], $attributes[5]['required']); + $this->assertEquals($integerResponse['body']['array'], $attributes[5]['array']); + $this->assertEquals($integerResponse['body']['default'], $attributes[5]['default']); + $this->assertEquals($integerResponse['body']['min'], $attributes[5]['min']); + $this->assertEquals($integerResponse['body']['max'], $attributes[5]['max']); - $this->assertEquals($booleanResponse['body']['key'], $attributes[6]['key']); - $this->assertEquals($booleanResponse['body']['type'], $attributes[6]['type']); - $this->assertEquals($booleanResponse['body']['status'], $attributes[6]['status']); - $this->assertEquals($booleanResponse['body']['required'], $attributes[6]['required']); - $this->assertEquals($booleanResponse['body']['array'], $attributes[6]['array']); - $this->assertEquals($booleanResponse['body']['default'], $attributes[6]['default']); + $this->assertEquals($floatResponse['body']['key'], $attributes[6]['key']); + $this->assertEquals($floatResponse['body']['type'], $attributes[6]['type']); + $this->assertEquals($floatResponse['body']['status'], $attributes[6]['status']); + $this->assertEquals($floatResponse['body']['required'], $attributes[6]['required']); + $this->assertEquals($floatResponse['body']['array'], $attributes[6]['array']); + $this->assertEquals($floatResponse['body']['default'], $attributes[6]['default']); + $this->assertEquals($floatResponse['body']['min'], $attributes[6]['min']); + $this->assertEquals($floatResponse['body']['max'], $attributes[6]['max']); + + $this->assertEquals($booleanResponse['body']['key'], $attributes[7]['key']); + $this->assertEquals($booleanResponse['body']['type'], $attributes[7]['type']); + $this->assertEquals($booleanResponse['body']['status'], $attributes[7]['status']); + $this->assertEquals($booleanResponse['body']['required'], $attributes[7]['required']); + $this->assertEquals($booleanResponse['body']['array'], $attributes[7]['array']); + $this->assertEquals($booleanResponse['body']['default'], $attributes[7]['default']); $collection = $this->client->call(Client::METHOD_GET, '/database/collections/' . $collectionId, array_merge([ 'content-type' => 'application/json', @@ -452,7 +500,7 @@ trait DatabaseBase $attributes = $collection['body']['attributes']; $this->assertIsArray($attributes); - $this->assertCount(7, $attributes); + $this->assertCount(8, $attributes); $this->assertEquals($stringResponse['body']['key'], $attributes[0]['key']); $this->assertEquals($stringResponse['body']['type'], $attributes[0]['type']); @@ -470,46 +518,55 @@ trait DatabaseBase $this->assertEquals($emailResponse['body']['default'], $attributes[1]['default']); $this->assertEquals($emailResponse['body']['format'], $attributes[1]['format']); - $this->assertEquals($ipResponse['body']['key'], $attributes[2]['key']); - $this->assertEquals($ipResponse['body']['type'], $attributes[2]['type']); - $this->assertEquals($ipResponse['body']['status'], $attributes[2]['status']); - $this->assertEquals($ipResponse['body']['required'], $attributes[2]['required']); - $this->assertEquals($ipResponse['body']['array'], $attributes[2]['array']); - $this->assertEquals($ipResponse['body']['default'], $attributes[2]['default']); - $this->assertEquals($ipResponse['body']['format'], $attributes[2]['format']); + $this->assertEquals($enumResponse['body']['key'], $attributes[2]['key']); + $this->assertEquals($enumResponse['body']['type'], $attributes[2]['type']); + $this->assertEquals($enumResponse['body']['status'], $attributes[2]['status']); + $this->assertEquals($enumResponse['body']['required'], $attributes[2]['required']); + $this->assertEquals($enumResponse['body']['array'], $attributes[2]['array']); + $this->assertEquals($enumResponse['body']['default'], $attributes[2]['default']); + $this->assertEquals($enumResponse['body']['format'], $attributes[2]['format']); + $this->assertEquals($enumResponse['body']['elements'], $attributes[2]['elements']); - $this->assertEquals($urlResponse['body']['key'], $attributes[3]['key']); - $this->assertEquals($urlResponse['body']['type'], $attributes[3]['type']); - $this->assertEquals($urlResponse['body']['status'], $attributes[3]['status']); - $this->assertEquals($urlResponse['body']['required'], $attributes[3]['required']); - $this->assertEquals($urlResponse['body']['array'], $attributes[3]['array']); - $this->assertEquals($urlResponse['body']['default'], $attributes[3]['default']); - $this->assertEquals($urlResponse['body']['format'], $attributes[3]['format']); + $this->assertEquals($ipResponse['body']['key'], $attributes[3]['key']); + $this->assertEquals($ipResponse['body']['type'], $attributes[3]['type']); + $this->assertEquals($ipResponse['body']['status'], $attributes[3]['status']); + $this->assertEquals($ipResponse['body']['required'], $attributes[3]['required']); + $this->assertEquals($ipResponse['body']['array'], $attributes[3]['array']); + $this->assertEquals($ipResponse['body']['default'], $attributes[3]['default']); + $this->assertEquals($ipResponse['body']['format'], $attributes[3]['format']); - $this->assertEquals($integerResponse['body']['key'], $attributes[4]['key']); - $this->assertEquals($integerResponse['body']['type'], $attributes[4]['type']); - $this->assertEquals($integerResponse['body']['status'], $attributes[4]['status']); - $this->assertEquals($integerResponse['body']['required'], $attributes[4]['required']); - $this->assertEquals($integerResponse['body']['array'], $attributes[4]['array']); - $this->assertEquals($integerResponse['body']['default'], $attributes[4]['default']); - $this->assertEquals($integerResponse['body']['min'], $attributes[4]['min']); - $this->assertEquals($integerResponse['body']['max'], $attributes[4]['max']); + $this->assertEquals($urlResponse['body']['key'], $attributes[4]['key']); + $this->assertEquals($urlResponse['body']['type'], $attributes[4]['type']); + $this->assertEquals($urlResponse['body']['status'], $attributes[4]['status']); + $this->assertEquals($urlResponse['body']['required'], $attributes[4]['required']); + $this->assertEquals($urlResponse['body']['array'], $attributes[4]['array']); + $this->assertEquals($urlResponse['body']['default'], $attributes[4]['default']); + $this->assertEquals($urlResponse['body']['format'], $attributes[4]['format']); - $this->assertEquals($floatResponse['body']['key'], $attributes[5]['key']); - $this->assertEquals($floatResponse['body']['type'], $attributes[5]['type']); - $this->assertEquals($floatResponse['body']['status'], $attributes[5]['status']); - $this->assertEquals($floatResponse['body']['required'], $attributes[5]['required']); - $this->assertEquals($floatResponse['body']['array'], $attributes[5]['array']); - $this->assertEquals($floatResponse['body']['default'], $attributes[5]['default']); - $this->assertEquals($floatResponse['body']['min'], $attributes[5]['min']); - $this->assertEquals($floatResponse['body']['max'], $attributes[5]['max']); + $this->assertEquals($integerResponse['body']['key'], $attributes[5]['key']); + $this->assertEquals($integerResponse['body']['type'], $attributes[5]['type']); + $this->assertEquals($integerResponse['body']['status'], $attributes[5]['status']); + $this->assertEquals($integerResponse['body']['required'], $attributes[5]['required']); + $this->assertEquals($integerResponse['body']['array'], $attributes[5]['array']); + $this->assertEquals($integerResponse['body']['default'], $attributes[5]['default']); + $this->assertEquals($integerResponse['body']['min'], $attributes[5]['min']); + $this->assertEquals($integerResponse['body']['max'], $attributes[5]['max']); - $this->assertEquals($booleanResponse['body']['key'], $attributes[6]['key']); - $this->assertEquals($booleanResponse['body']['type'], $attributes[6]['type']); - $this->assertEquals($booleanResponse['body']['status'], $attributes[6]['status']); - $this->assertEquals($booleanResponse['body']['required'], $attributes[6]['required']); - $this->assertEquals($booleanResponse['body']['array'], $attributes[6]['array']); - $this->assertEquals($booleanResponse['body']['default'], $attributes[6]['default']); + $this->assertEquals($floatResponse['body']['key'], $attributes[6]['key']); + $this->assertEquals($floatResponse['body']['type'], $attributes[6]['type']); + $this->assertEquals($floatResponse['body']['status'], $attributes[6]['status']); + $this->assertEquals($floatResponse['body']['required'], $attributes[6]['required']); + $this->assertEquals($floatResponse['body']['array'], $attributes[6]['array']); + $this->assertEquals($floatResponse['body']['default'], $attributes[6]['default']); + $this->assertEquals($floatResponse['body']['min'], $attributes[6]['min']); + $this->assertEquals($floatResponse['body']['max'], $attributes[6]['max']); + + $this->assertEquals($booleanResponse['body']['key'], $attributes[7]['key']); + $this->assertEquals($booleanResponse['body']['type'], $attributes[7]['type']); + $this->assertEquals($booleanResponse['body']['status'], $attributes[7]['status']); + $this->assertEquals($booleanResponse['body']['required'], $attributes[7]['required']); + $this->assertEquals($booleanResponse['body']['array'], $attributes[7]['array']); + $this->assertEquals($booleanResponse['body']['default'], $attributes[7]['default']); return $data; } From ca335c9be556f832b4b39dcf6aa3f90177d0ab9d Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Fri, 10 Sep 2021 16:25:16 -0400 Subject: [PATCH 6/9] Test enum enforcement when creating documents --- tests/e2e/Services/Database/DatabaseBase.php | 39 +++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/tests/e2e/Services/Database/DatabaseBase.php b/tests/e2e/Services/Database/DatabaseBase.php index 041935f11a..f77538ed69 100644 --- a/tests/e2e/Services/Database/DatabaseBase.php +++ b/tests/e2e/Services/Database/DatabaseBase.php @@ -1124,6 +1124,16 @@ trait DatabaseBase 'required' => false, ]); + $enum = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/enum', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'attributeId' => 'enum', + 'elements' => ['yes', 'no', 'maybe'], + 'required' => false, + ]); + $ip = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/ip', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], @@ -1232,7 +1242,7 @@ trait DatabaseBase 'x-appwrite-key' => $this->getProject()['apiKey'], ]), []); - $this->assertCount(7, $collection['body']['attributes']); + $this->assertCount(8, $collection['body']['attributes']); /** * Test for successful validation @@ -1250,6 +1260,18 @@ trait DatabaseBase 'write' => ['user:'.$this->getUser()['$id']], ]); + $goodEnum = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'documentId' => 'unique()', + 'data' => [ + 'enum' => 'yes', + ], + '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'], @@ -1323,6 +1345,7 @@ trait DatabaseBase ]); $this->assertEquals(201, $goodEmail['headers']['status-code']); + $this->assertEquals(201, $goodEnum['headers']['status-code']); $this->assertEquals(201, $goodIp['headers']['status-code']); $this->assertEquals(201, $goodUrl['headers']['status-code']); $this->assertEquals(201, $goodRange['headers']['status-code']); @@ -1346,6 +1369,18 @@ trait DatabaseBase 'write' => ['user:'.$this->getUser()['$id']], ]); + $badEnum = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'documentId' => 'unique()', + 'data' => [ + 'enum' => 'badEnum', + ], + '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'], @@ -1419,6 +1454,7 @@ trait DatabaseBase ]); $this->assertEquals(400, $badEmail['headers']['status-code']); + $this->assertEquals(400, $badEnum['headers']['status-code']); $this->assertEquals(400, $badIp['headers']['status-code']); $this->assertEquals(400, $badUrl['headers']['status-code']); $this->assertEquals(400, $badRange['headers']['status-code']); @@ -1426,6 +1462,7 @@ trait DatabaseBase $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 "enum" has invalid format. Value must be one of (yes, no, maybe)', $badEnum['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']); From 1b4beb5ea29ff88d3741d256e794329886ac30e4 Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Wed, 6 Oct 2021 22:25:03 -0400 Subject: [PATCH 7/9] Ensure enum attribute has proper response model --- app/controllers/api/database.php | 10 +++++++--- src/Appwrite/Utopia/Response/Model/AttributeEnum.php | 5 +++++ src/Appwrite/Utopia/Response/Model/AttributeList.php | 1 + src/Appwrite/Utopia/Response/Model/Collection.php | 1 + 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/app/controllers/api/database.php b/app/controllers/api/database.php index d26d960d42..eb7636e448 100644 --- a/app/controllers/api/database.php +++ b/app/controllers/api/database.php @@ -759,7 +759,7 @@ App::post('/v1/database/collections/:collectionId/attributes/enum') ->label('sdk.description', '/docs/references/database/create-attribute-enum.md') ->label('sdk.response.code', Response::STATUS_CODE_CREATED) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) - ->label('sdk.response.model', Response::MODEL_ATTRIBUTE) + ->label('sdk.response.model', Response::MODEL_ATTRIBUTE_ENUM) ->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('elements', [], new ArrayList(new Text(0)), 'Array of elements in enumerated type. Uses length of longest element to determine size.') @@ -770,11 +770,13 @@ App::post('/v1/database/collections/:collectionId/attributes/enum') ->inject('dbForInternal') ->inject('database') ->inject('audits') - ->action(function ($collectionId, $attributeId, $elements, $required, $default, $array, $response, $dbForInternal, $database, $audits) { + ->inject('usage') + ->action(function ($collectionId, $attributeId, $elements, $required, $default, $array, $response, $dbForInternal, $database, $audits, $usage) { /** @var Appwrite\Utopia\Response $response */ /** @var Utopia\Database\Database $dbForInternal*/ /** @var Appwrite\Event\Event $database */ /** @var Appwrite\Event\Event $audits */ + /** @var Appwrite\Stats\Stats $usage */ // use length of longest string as attribute size $size = 0; @@ -792,7 +794,7 @@ App::post('/v1/database/collections/:collectionId/attributes/enum') 'array' => $array, 'format' => APP_DATABASE_ATTRIBUTE_ENUM, 'formatOptions' => ['elements' => $elements], - ]), $response, $dbForInternal, $database, $audits); + ]), $response, $dbForInternal, $database, $audits, $usage); $response->dynamic($attribute, Response::MODEL_ATTRIBUTE_ENUM); }); @@ -1100,6 +1102,7 @@ App::get('/v1/database/collections/:collectionId/attributes/:attributeId') Response::MODEL_ATTRIBUTE_INTEGER, Response::MODEL_ATTRIBUTE_FLOAT, Response::MODEL_ATTRIBUTE_EMAIL, + Response::MODEL_ATTRIBUTE_ENUM, Response::MODEL_ATTRIBUTE_URL, Response::MODEL_ATTRIBUTE_IP, Response::MODEL_ATTRIBUTE_STRING,])// needs to be last, since its condition would dominate any other string attribute @@ -1607,6 +1610,7 @@ App::get('/v1/database/collections/:collectionId/documents') throw new Exception($validator->getDescription(), 400); } + $afterDocument = null; if (!empty($after)) { $afterDocument = $dbForExternal->getDocument($collectionId, $after); diff --git a/src/Appwrite/Utopia/Response/Model/AttributeEnum.php b/src/Appwrite/Utopia/Response/Model/AttributeEnum.php index 6db73fce6b..53297ff689 100644 --- a/src/Appwrite/Utopia/Response/Model/AttributeEnum.php +++ b/src/Appwrite/Utopia/Response/Model/AttributeEnum.php @@ -39,6 +39,11 @@ class AttributeEnum extends Attribute ; } + public array $conditions = [ + 'type' => self::TYPE_STRING, + 'format' => \APP_DATABASE_ATTRIBUTE_ENUM + ]; + /** * Get Name * diff --git a/src/Appwrite/Utopia/Response/Model/AttributeList.php b/src/Appwrite/Utopia/Response/Model/AttributeList.php index 0cf67b3bd2..bbb5d8001a 100644 --- a/src/Appwrite/Utopia/Response/Model/AttributeList.php +++ b/src/Appwrite/Utopia/Response/Model/AttributeList.php @@ -23,6 +23,7 @@ class AttributeList extends Model Response::MODEL_ATTRIBUTE_INTEGER, Response::MODEL_ATTRIBUTE_FLOAT, Response::MODEL_ATTRIBUTE_EMAIL, + Response::MODEL_ATTRIBUTE_ENUM, Response::MODEL_ATTRIBUTE_URL, Response::MODEL_ATTRIBUTE_IP, Response::MODEL_ATTRIBUTE_STRING // needs to be last, since its condition would dominate any other string attribute diff --git a/src/Appwrite/Utopia/Response/Model/Collection.php b/src/Appwrite/Utopia/Response/Model/Collection.php index b2a05846c5..2ed6522211 100644 --- a/src/Appwrite/Utopia/Response/Model/Collection.php +++ b/src/Appwrite/Utopia/Response/Model/Collection.php @@ -48,6 +48,7 @@ class Collection extends Model Response::MODEL_ATTRIBUTE_INTEGER, Response::MODEL_ATTRIBUTE_FLOAT, Response::MODEL_ATTRIBUTE_EMAIL, + Response::MODEL_ATTRIBUTE_ENUM, Response::MODEL_ATTRIBUTE_URL, Response::MODEL_ATTRIBUTE_IP, Response::MODEL_ATTRIBUTE_STRING, // needs to be last, since its condition would dominate any other string attribute From b3d5ba41231bb41f4b06609013b9c404039591a6 Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Thu, 7 Oct 2021 14:27:20 -0400 Subject: [PATCH 8/9] Address review comments --- src/Appwrite/Utopia/Response/Model/AttributeEnum.php | 2 +- tests/e2e/Services/Database/DatabaseBase.php | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Appwrite/Utopia/Response/Model/AttributeEnum.php b/src/Appwrite/Utopia/Response/Model/AttributeEnum.php index 53297ff689..d04ac7491a 100644 --- a/src/Appwrite/Utopia/Response/Model/AttributeEnum.php +++ b/src/Appwrite/Utopia/Response/Model/AttributeEnum.php @@ -55,7 +55,7 @@ class AttributeEnum extends Attribute } /** - * Get Collection + * Get Type * * @return string */ diff --git a/tests/e2e/Services/Database/DatabaseBase.php b/tests/e2e/Services/Database/DatabaseBase.php index 3e0004710c..3af0b80a2a 100644 --- a/tests/e2e/Services/Database/DatabaseBase.php +++ b/tests/e2e/Services/Database/DatabaseBase.php @@ -222,7 +222,6 @@ trait DatabaseBase $this->assertEquals('email', $email['body']['key']); $this->assertEquals('string', $email['body']['type']); $this->assertEquals('processing', $email['body']['status']); - $this->assertEquals('processing', $email['body']['status']); $this->assertEquals(false, $email['body']['required']); $this->assertEquals(false, $email['body']['array']); $this->assertEquals('email', $email['body']['format']); From f19769952319f48c1736e90a1c4a44e8e5bc8d25 Mon Sep 17 00:00:00 2001 From: kodumbeats Date: Thu, 7 Oct 2021 14:30:52 -0400 Subject: [PATCH 9/9] Throw exception when any enum element is empty --- app/controllers/api/database.php | 4 ++++ tests/e2e/Services/Database/DatabaseBase.php | 17 +++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/app/controllers/api/database.php b/app/controllers/api/database.php index ee18694e67..7187b75110 100644 --- a/app/controllers/api/database.php +++ b/app/controllers/api/database.php @@ -783,6 +783,10 @@ App::post('/v1/database/collections/:collectionId/attributes/enum') $size = 0; foreach ($elements as $element) { $length = \strlen($element); + if ($length === 0) { + throw new Exception('Each enum element must not be empty', 400); + + } $size = ($length > $size) ? $length : $size; } diff --git a/tests/e2e/Services/Database/DatabaseBase.php b/tests/e2e/Services/Database/DatabaseBase.php index 3af0b80a2a..216b8c0831 100644 --- a/tests/e2e/Services/Database/DatabaseBase.php +++ b/tests/e2e/Services/Database/DatabaseBase.php @@ -568,6 +568,23 @@ trait DatabaseBase $this->assertEquals($booleanResponse['body']['array'], $attributes[7]['array']); $this->assertEquals($booleanResponse['body']['default'], $attributes[7]['default']); + /** + * Test for FAILURE + */ + $badEnum = $this->client->call(Client::METHOD_POST, '/database/collections/' . $collectionId . '/attributes/enum', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'attributeId' => 'enum', + 'elements' => ['yes', 'no', ''], + 'required' => false, + 'default' => 'maybe', + ]); + + $this->assertEquals(400, $badEnum['headers']['status-code']); + $this->assertEquals('Each enum element must not be empty', $badEnum['body']['message']); + return $data; }