diff --git a/src/Database/Adapter/MongoDB.php b/src/Database/Adapter/MongoDB.php index 9ee6398ff..c283bc0fe 100644 --- a/src/Database/Adapter/MongoDB.php +++ b/src/Database/Adapter/MongoDB.php @@ -107,10 +107,19 @@ public function createCollection(string $name, array $attributes = [], array $in $collection = $database->$id; - // Mongo creates an index for _id; _uid and _read index by default + // Mongo creates an index for _id; _uid (unique, case insensitive) and _read index by default // Returns the name of the created index as a string. - // Update $this->getIndexCount when adding another default index - $uid = $collection->createIndex(['_uid' => $this->getOrder(Database::ORDER_DESC)], ['name' => '_uid', 'unique' => true]); + $uid = $collection->createIndex([ + '_uid' => $this->getOrder(Database::ORDER_DESC)], + [ + 'name' => '_uid', + 'unique' => true, + 'collation' => [ // https://docs.mongodb.com/manual/core/index-case-insensitive/#create-a-case-insensitive-index + 'locale' => 'en', + 'strength' => 1 + ], + ] + ); $read = $collection->createIndex(['_read' => $this->getOrder(Database::ORDER_DESC)], ['name' => '_read_permissions']); diff --git a/src/Database/Database.php b/src/Database/Database.php index e1ad29ccb..f930623b6 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -6,6 +6,7 @@ use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Structure; use Utopia\Database\Exception\Authorization as AuthorizationException; +use Utopia\Database\Exception\Duplicate as DuplicateException; use Utopia\Database\Exception\Limit as LimitException; use Utopia\Database\Exception\Structure as StructureException; use Utopia\Cache\Cache; @@ -394,6 +395,14 @@ public function createAttribute(string $collection, string $id, string $type, in { $collection = $this->getCollection($collection); + // attribute IDs are case insensitive + $attributes = $collection->getAttribute('attributes', []); /** @var Document[] $attributes */ + foreach ($attributes as $attribute) { + if (\strtolower($attribute->getId()) === \strtolower($id)) { + throw new DuplicateException('Attribute already exists'); + } + } + if ($this->adapter->getAttributeLimit() > 0 && $this->adapter->getAttributeCount($collection, true) >= $this->adapter->getAttributeLimit()) { @@ -531,6 +540,23 @@ public function addAttributeInQueue(string $collection, string $id, string $type { $collection = $this->getCollection($collection); + /** @var Document[] $attributes */ + /** @var Document[] $attributesInQueue */ + $attributes = $collection->getAttribute('attributes'); + $attributesInQueue = $collection->getAttribute('attributesInQueue'); + + foreach ($attributes as $attribute) { + if (\strtolower($attribute->getId()) === \strtolower($id)) { + throw new DuplicateException('Attribute already exists'); + } + } + + foreach ($attributesInQueue as $attribute) { + if (\strtolower($attribute->getId()) === \strtolower($id)) { + throw new DuplicateException('Attribute already exists in queue'); + } + } + if ($format) { $name = \json_decode($format, true)['name']; if (!Structure::hasFormat(json_decode($format, true)['name'], $type)) { @@ -618,6 +644,14 @@ public function createIndex(string $collection, string $id, string $type, array $collection = $this->getCollection($collection); + // index IDs are case insensitive + $indexes = $collection->getAttribute('indexes', []); /** @var Document[] $indexes */ + foreach ($indexes as $index) { + if (\strtolower($index->getId()) === \strtolower($id)) { + throw new DuplicateException('Index already exists'); + } + } + if ($this->adapter->getIndexCount($collection, true) >= $this->adapter->getIndexLimit()) { throw new LimitException('Index limit reached. Cannot create new index.'); } @@ -710,6 +744,22 @@ public function addIndexInQueue(string $collection, string $id, string $type, ar $collection = $this->getCollection($collection); + // index IDs are case insensitive + $indexes = $collection->getAttribute('indexes', []); /** @var Document[] $indexes */ + $indexesInQueue = $collection->getAttribute('indexesInQueue', []); /** @var Document[] $indexesInQueue */ + + foreach ($indexes as $index) { + if (\strtolower($index->getId()) === \strtolower($id)) { + throw new DuplicateException('Index already exists'); + } + } + + foreach ($indexesInQueue as $index) { + if (\strtolower($index->getId()) === \strtolower($id)) { + throw new DuplicateException('Index already exists in queue'); + } + } + $collection->setAttribute('indexesInQueue', new Document([ '$id' => $id, 'type' => $type, diff --git a/tests/Database/Base.php b/tests/Database/Base.php index a9bb80a9a..7966f191f 100644 --- a/tests/Database/Base.php +++ b/tests/Database/Base.php @@ -8,7 +8,7 @@ use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Exception\Authorization as ExceptionAuthorization; -use Utopia\Database\Exception\Duplicate; +use Utopia\Database\Exception\Duplicate as DuplicateException; use Utopia\Database\Exception\Limit as LimitException; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; @@ -190,12 +190,54 @@ public function testInvalidDefaultValues($type, $default) $this->assertEquals(false, static::getDatabase()->createAttribute('attributes', 'bad_default', $type, 256, true, $default)); } + /** + * @depends testInvalidDefaultValues + */ + public function testAttributeCaseInsensitivity() + { + $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'caseSensitive', Database::VAR_STRING, 128, true)); + $this->expectException(DuplicateException::class); + $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'CaseSensitive', Database::VAR_STRING, 128, true)); + } + + /** + * @depends testAttributeCaseInsensitivity + */ + public function testAttributeQueueCaseInsensitivity() + { + $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'caseSensitiveInQueue', Database::VAR_STRING, 128, true)); + $this->expectException(DuplicateException::class); + $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'CaseSensitiveInQueue', Database::VAR_STRING, 128, true)); + } + + /** + * @depends testAttributeQueueCaseInsensitivity + */ + + public function testIndexCaseInsensitivity() + { + $this->assertEquals(true, static::getDatabase()->createIndex('attributes', 'key_caseSensitive', Database::INDEX_KEY, ['caseSensitive'], [128])); + $this->expectException(DuplicateException::class); + $this->assertEquals(true, static::getDatabase()->createIndex('attributes', 'key_CaseSensitive', Database::INDEX_KEY, ['caseSensitive'], [128])); + } + + /** + * @depends testIndexCaseInsensitivity + */ + + public function testIndexQueueCaseInsensitivity() + { + $this->assertEquals(true, static::getDatabase()->createIndex('attributes', 'key_caseSensitiveInQueue', Database::INDEX_KEY, ['caseSensitive'], [128])); + $this->expectException(DuplicateException::class); + $this->assertEquals(true, static::getDatabase()->createIndex('attributes', 'key_CaseSensitiveInQueue', Database::INDEX_KEY, ['caseSensitive'], [128])); + } + /** * Ensure the collection is removed after use * - * @depends testInvalidDefaultValues + * @depends testIndexQueueCaseInsensitivity */ - public function testCleanupInvalidDefaultValues() + public function testCleanupAttributeTests() { static::getDatabase()->deleteCollection('attributes'); $this->assertEquals(1,1); @@ -1466,7 +1508,7 @@ public function testExceptionIndexLimitInQueue() */ public function testExceptionDuplicate(Document $document) { - $this->expectException(Duplicate::class); + $this->expectException(DuplicateException::class); $document->setAttribute('$id', 'duplicated'); @@ -1481,7 +1523,7 @@ public function testExceptionDuplicate(Document $document) */ public function testUniqueIndexDuplicate() { - $this->expectException(Duplicate::class); + $this->expectException(DuplicateException::class); $this->assertEquals(true, static::getDatabase()->createIndex('movies', 'uniqueIndex', Database::INDEX_UNIQUE, ['name'], [128], [Database::ORDER_ASC]));