diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f2a2f9e5..b4b539e13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,15 @@ # Changelog All notable changes to this project will be documented in this file. +## [4.7.0] - 2024-07-19 + +* Add `Query\Builder::upsert()` method by @GromNaN in [#3052](https://github.com/mongodb/laravel-mongodb/pull/3052) +* Add `Connection::getServerVersion()` by @GromNaN in [#3043](https://github.com/mongodb/laravel-mongodb/pull/3043) +* Add `Schema\Builder::getTables()` and `getTableListing()` by @GromNaN in [#3044](https://github.com/mongodb/laravel-mongodb/pull/3044) +* Add `Schema\Builder::getColumns()` and `getIndexes()` by @GromNaN in [#3045](https://github.com/mongodb/laravel-mongodb/pull/3045) +* Add `Schema\Builder::hasColumn()` and `hasColumns()` method by @Alex-Belyi in [#3001](https://github.com/mongodb/laravel-mongodb/pull/3001) +* Fix unsetting a field in an embedded model by @GromNaN in [#3052](https://github.com/mongodb/laravel-mongodb/pull/3052) + ## [4.6.0] - 2024-07-09 * Add `DocumentModel` trait to use any 3rd party model with MongoDB @GromNaN in [#2580](https://github.com/mongodb/laravel-mongodb/pull/2580) diff --git a/docs/includes/auth/AuthController.php b/docs/includes/auth/AuthController.php new file mode 100644 index 000000000..c76552cbe --- /dev/null +++ b/docs/includes/auth/AuthController.php @@ -0,0 +1,38 @@ +validate([ + 'email' => 'required|email', + 'password' => 'required', + ]); + + if (Auth::attempt($request->only('email', 'password'))) { + return response()->json([ + 'user' => Auth::user(), + 'message' => 'Successfully logged in', + ]); + } + + throw ValidationException::withMessages([ + 'email' => ['The provided credentials are incorrect.'], + ]); + } + + public function logout() + { + Auth::logout(); + + return response()->json(['message' => 'Successfully logged out']); + } +} diff --git a/docs/includes/auth/AuthUser.php b/docs/includes/auth/AuthUser.php new file mode 100644 index 000000000..8b6a0f173 --- /dev/null +++ b/docs/includes/auth/AuthUser.php @@ -0,0 +1,22 @@ +` +in the {+server-docs-name+}. + +This guide contains the following sections: - :ref:`laravel-transaction-requirements` - :ref:`laravel-transaction-callback` - :ref:`laravel-transaction-commit` - :ref:`laravel-transaction-rollback` -.. tip:: +.. tip:: Transactions Learning Byte - To learn more about transactions in MongoDB, see :manual:`Transactions ` - in the {+server-docs-name+}. + Practice using {+odm-short+} to perform transactions + in the `Laravel Transactions Learning Byte + `__. .. _laravel-transaction-requirements: @@ -156,4 +160,3 @@ transaction is rolled back, and none of the models are updated: :emphasize-lines: 1,18,20 :start-after: begin rollback transaction :end-before: end rollback transaction - diff --git a/docs/user-authentication.txt b/docs/user-authentication.txt index 8755c7c6a..b86a8659f 100644 --- a/docs/user-authentication.txt +++ b/docs/user-authentication.txt @@ -1,7 +1,7 @@ .. _laravel-user-authentication: =================== -User authentication +User Authentication =================== .. facet:: @@ -11,14 +11,124 @@ User authentication .. meta:: :keywords: php framework, odm, code example -If you want to use Laravel's native Auth functionality, register this included -service provider: +.. contents:: On this page + :local: + :backlinks: none + :depth: 1 + :class: singlecol + +Overview +-------- + +In this guide, you can learn how to authenticate MongoDB users +by using Laravel's native authentication functionality. + +Laravel provides a native ``Auth`` module that includes authentication services, +such as guards that define how users are authenticated and providers that define +how users are retrieved. To learn more about these services, see `Authentication +`__ in the +Laravel documentation. + +Modify the User Model +--------------------- + +By default, Laravel generates the ``User`` Eloquent model in your ``App/Models`` +directory. To enable authentication for MongoDB users, your ``User`` model +must extend the ``MongoDB\Laravel\Auth\User`` class. + +To extend this class, navigate to your ``app/Models/User.php`` file and replace the +``use Illuminate\Foundation\Auth\User as Authenticatable`` statement with the following +code: + +.. code-block:: php + + use MongoDB\Laravel\Auth\User as Authenticatable; + +Next, ensure that your ``User`` class extends ``Authenticatable``, as shown in the following +code: + +.. code-block:: php + + class User extends Authenticatable + { + ... + } + +After configuring your ``User`` model, create a corresponding controller. To learn how to +create a controller, see the :ref:`laravel-auth-controller` section on this page. + +Example +~~~~~~~ + +The following code shows a ``User.php`` file that extends the ``MongoDB\Laravel\Auth\User`` +class: + +.. literalinclude:: /includes/auth/AuthUser.php + :language: php + :dedent: + +.. _laravel-auth-controller: + +Create the User Controller +-------------------------- + +To store functions that manage authentication, create an authentication controller for +your ``User`` model. + +Run the following command from your project root to create a controller: + +.. code-block:: php + + php artisan make:controller + +Example +~~~~~~~ + +The following command creates a controller file called ``AuthController.php``: + +.. code-block:: php + + php artisan make:controller AuthController + +The ``AuthController.php`` file can store ``login()`` and ``logout()`` functions to +manage user authentication, as shown in the following code: + +.. literalinclude:: /includes/auth/AuthController.php + :language: php + :dedent: + +Enable Password Reminders +------------------------- + +To add support for MongoDB-based password reminders, register the following service +provider in your application: + +.. code-block:: php + + MongoDB\Laravel\Auth\PasswordResetServiceProvider::class + +This service provider modifies the internal ``DatabaseReminderRepository`` +to enable password reminders. + +Example +~~~~~~~ + +The following code updates the ``providers.php`` file in the ``bootstrap`` directory +of a Laravel application to register the ``PasswordResetServiceProvider`` provider: .. code-block:: php + :emphasize-lines: 4 + + return [ + App\Providers\AppServiceProvider::class, + MongoDB\Laravel\MongoDBServiceProvider::class, + MongoDB\Laravel\Auth\PasswordResetServiceProvider::class + ]; - MongoDB\Laravel\Auth\PasswordResetServiceProvider::class, +Additional Information +---------------------- -This service provider will slightly modify the internal ``DatabaseReminderRepository`` -to add support for MongoDB based password reminders. +To learn more about user authentication, see `Authentication `__ +in the Laravel documentation. -If you don't use password reminders, you can omit this service provider. +To learn more about Eloquent models, see the :ref:`laravel-eloquent-model-class` guide. \ No newline at end of file diff --git a/src/Connection.php b/src/Connection.php index 2ce5324ee..685e509b6 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -327,6 +327,17 @@ public function __call($method, $parameters) return $this->db->$method(...$parameters); } + /** + * Return the server version of one of the MongoDB servers: primary for + * replica sets and standalone, and the selected server for sharded clusters. + * + * @internal + */ + public function getServerVersion(): string + { + return $this->db->command(['buildInfo' => 1])->toArray()[0]['version']; + } + private static function getVersion(): string { return self::$version ?? self::lookupVersion(); diff --git a/src/Query/Builder.php b/src/Query/Builder.php index 89faa4b17..1d4dcf153 100644 --- a/src/Query/Builder.php +++ b/src/Query/Builder.php @@ -725,6 +725,47 @@ public function update(array $values, array $options = []) return $this->performUpdate($values, $options); } + /** @inheritdoc */ + public function upsert(array $values, $uniqueBy, $update = null): int + { + if ($values === []) { + return 0; + } + + $this->applyBeforeQueryCallbacks(); + + $options = $this->inheritConnectionOptions(); + $uniqueBy = array_fill_keys((array) $uniqueBy, 1); + + // If no update fields are specified, all fields are updated + if ($update !== null) { + $update = array_fill_keys((array) $update, 1); + } + + $bulk = []; + + foreach ($values as $value) { + $filter = $operation = []; + foreach ($value as $key => $val) { + if (isset($uniqueBy[$key])) { + $filter[$key] = $val; + } + + if ($update === null || array_key_exists($key, $update)) { + $operation['$set'][$key] = $val; + } else { + $operation['$setOnInsert'][$key] = $val; + } + } + + $bulk[] = ['updateOne' => [$filter, $operation, ['upsert' => true]]]; + } + + $result = $this->collection->bulkWrite($bulk, $options); + + return $result->getInsertedCount() + $result->getUpsertedCount() + $result->getModifiedCount(); + } + /** @inheritdoc */ public function increment($column, $amount = 1, array $extra = [], array $options = []) { diff --git a/src/Relations/EmbedsOneOrMany.php b/src/Relations/EmbedsOneOrMany.php index 9c83aa299..f18d3d526 100644 --- a/src/Relations/EmbedsOneOrMany.php +++ b/src/Relations/EmbedsOneOrMany.php @@ -15,8 +15,10 @@ use Throwable; use function array_merge; +use function assert; use function count; use function is_array; +use function str_starts_with; use function throw_if; abstract class EmbedsOneOrMany extends Relation @@ -392,7 +394,12 @@ public static function getUpdateValues($array, $prepend = '') $results = []; foreach ($array as $key => $value) { - $results[$prepend . $key] = $value; + if (str_starts_with($key, '$')) { + assert(is_array($value), 'Update operator value must be an array.'); + $results[$key] = static::getUpdateValues($value, $prepend); + } else { + $results[$prepend . $key] = $value; + } } return $results; diff --git a/src/Schema/Builder.php b/src/Schema/Builder.php index bfa0e4715..29f089d7d 100644 --- a/src/Schema/Builder.php +++ b/src/Schema/Builder.php @@ -6,23 +6,46 @@ use Closure; use MongoDB\Model\CollectionInfo; +use MongoDB\Model\IndexInfo; +use function array_fill_keys; +use function array_keys; +use function assert; use function count; use function current; +use function implode; use function iterator_to_array; +use function sort; +use function sprintf; +use function usort; class Builder extends \Illuminate\Database\Schema\Builder { - /** @inheritdoc */ - public function hasColumn($table, $column) + /** + * Check if column exists in the collection schema. + * + * @param string $table + * @param string $column + */ + public function hasColumn($table, $column): bool { - return true; + return $this->hasColumns($table, [$column]); } - /** @inheritdoc */ - public function hasColumns($table, array $columns) + /** + * Check if columns exists in the collection schema. + * + * @param string $table + * @param string[] $columns + */ + public function hasColumns($table, array $columns): bool { - return true; + $collection = $this->connection->table($table); + + return $collection + ->where(array_fill_keys($columns, ['$exists' => true])) + ->project(['_id' => 1]) + ->exists(); } /** @@ -107,6 +130,120 @@ public function dropAllTables() } } + public function getTables() + { + $db = $this->connection->getMongoDB(); + $collections = []; + + foreach ($db->listCollectionNames() as $collectionName) { + $stats = $db->selectCollection($collectionName)->aggregate([ + ['$collStats' => ['storageStats' => ['scale' => 1]]], + ['$project' => ['storageStats.totalSize' => 1]], + ])->toArray(); + + $collections[] = [ + 'name' => $collectionName, + 'schema' => null, + 'size' => $stats[0]?->storageStats?->totalSize ?? null, + 'comment' => null, + 'collation' => null, + 'engine' => null, + ]; + } + + usort($collections, function ($a, $b) { + return $a['name'] <=> $b['name']; + }); + + return $collections; + } + + public function getTableListing() + { + $collections = iterator_to_array($this->connection->getMongoDB()->listCollectionNames()); + + sort($collections); + + return $collections; + } + + public function getColumns($table) + { + $stats = $this->connection->getMongoDB()->selectCollection($table)->aggregate([ + // Sample 1,000 documents to get a representative sample of the collection + ['$sample' => ['size' => 1_000]], + // Convert each document to an array of fields + ['$project' => ['fields' => ['$objectToArray' => '$$ROOT']]], + // Unwind to get one document per field + ['$unwind' => '$fields'], + // Group by field name, count the number of occurrences and get the types + [ + '$group' => [ + '_id' => '$fields.k', + 'total' => ['$sum' => 1], + 'types' => ['$addToSet' => ['$type' => '$fields.v']], + ], + ], + // Get the most seen field names + ['$sort' => ['total' => -1]], + // Limit to 1,000 fields + ['$limit' => 1_000], + // Sort by field name + ['$sort' => ['_id' => 1]], + ], [ + 'typeMap' => ['array' => 'array'], + 'allowDiskUse' => true, + ])->toArray(); + + $columns = []; + foreach ($stats as $stat) { + sort($stat->types); + $type = implode(', ', $stat->types); + $columns[] = [ + 'name' => $stat->_id, + 'type_name' => $type, + 'type' => $type, + 'collation' => null, + 'nullable' => $stat->_id !== '_id', + 'default' => null, + 'auto_increment' => false, + 'comment' => sprintf('%d occurrences', $stat->total), + 'generation' => $stat->_id === '_id' ? ['type' => 'objectId', 'expression' => null] : null, + ]; + } + + return $columns; + } + + public function getIndexes($table) + { + $indexes = $this->connection->getMongoDB()->selectCollection($table)->listIndexes(); + + $indexList = []; + foreach ($indexes as $index) { + assert($index instanceof IndexInfo); + $indexList[] = [ + 'name' => $index->getName(), + 'columns' => array_keys($index->getKey()), + 'primary' => $index->getKey() === ['_id' => 1], + 'type' => match (true) { + $index->isText() => 'text', + $index->is2dSphere() => '2dsphere', + $index->isTtl() => 'ttl', + default => 'default', + }, + 'unique' => $index->isUnique(), + ]; + } + + return $indexList; + } + + public function getForeignKeys($table) + { + return []; + } + /** @inheritdoc */ protected function createBlueprint($table, ?Closure $callback = null) { diff --git a/tests/ConnectionTest.php b/tests/ConnectionTest.php index 586452109..ef0b746c3 100644 --- a/tests/ConnectionTest.php +++ b/tests/ConnectionTest.php @@ -299,4 +299,10 @@ public function testPingMethod() $instance = new Connection($config); $instance->ping(); } + + public function testServerVersion() + { + $version = DB::connection('mongodb')->getServerVersion(); + $this->assertIsString($version); + } } diff --git a/tests/EmbeddedRelationsTest.php b/tests/EmbeddedRelationsTest.php index 00a84360c..22e6e8d08 100644 --- a/tests/EmbeddedRelationsTest.php +++ b/tests/EmbeddedRelationsTest.php @@ -10,12 +10,6 @@ use Mockery; use MongoDB\BSON\ObjectId; use MongoDB\Laravel\Tests\Models\Address; -use MongoDB\Laravel\Tests\Models\Book; -use MongoDB\Laravel\Tests\Models\Client; -use MongoDB\Laravel\Tests\Models\Group; -use MongoDB\Laravel\Tests\Models\Item; -use MongoDB\Laravel\Tests\Models\Photo; -use MongoDB\Laravel\Tests\Models\Role; use MongoDB\Laravel\Tests\Models\User; use function array_merge; @@ -25,14 +19,7 @@ class EmbeddedRelationsTest extends TestCase public function tearDown(): void { Mockery::close(); - User::truncate(); - Book::truncate(); - Item::truncate(); - Role::truncate(); - Client::truncate(); - Group::truncate(); - Photo::truncate(); } public function testEmbedsManySave() @@ -951,4 +938,36 @@ public function testGetQueueableRelationsEmbedsOne() $this->assertEquals(['father'], $user->getQueueableRelations()); $this->assertEquals([], $user->father->getQueueableRelations()); } + + public function testUnsetPropertyOnEmbed() + { + $user = User::create(['name' => 'John Doe']); + $user->addresses()->save(new Address(['city' => 'New York'])); + $user->addresses()->save(new Address(['city' => 'Tokyo'])); + + // Set property + $user->addresses->first()->city = 'Paris'; + $user->addresses->first()->save(); + + $user = User::where('name', 'John Doe')->first(); + $this->assertSame('Paris', $user->addresses->get(0)->city); + $this->assertSame('Tokyo', $user->addresses->get(1)->city); + + // Unset property + unset($user->addresses->first()->city); + $user->addresses->first()->save(); + + $user = User::where('name', 'John Doe')->first(); + $this->assertNull($user->addresses->get(0)->city); + $this->assertSame('Tokyo', $user->addresses->get(1)->city); + + // Unset and reset property + unset($user->addresses->get(1)->city); + $user->addresses->get(1)->city = 'Kyoto'; + $user->addresses->get(1)->save(); + + $user = User::where('name', 'John Doe')->first(); + $this->assertNull($user->addresses->get(0)->city); + $this->assertSame('Kyoto', $user->addresses->get(1)->city); + } } diff --git a/tests/ModelTest.php b/tests/ModelTest.php index 3c4cbd8df..57e49574f 100644 --- a/tests/ModelTest.php +++ b/tests/ModelTest.php @@ -143,6 +143,40 @@ public function testUpdate(): void $this->assertEquals('Hans Thomas', $check->fullname); } + public function testUpsert() + { + $result = User::upsert([ + ['email' => 'foo', 'name' => 'bar'], + ['name' => 'bar2', 'email' => 'foo2'], + ], ['email']); + + $this->assertSame(2, $result); + + $this->assertSame(2, $result); + $this->assertSame(2, User::count()); + $this->assertSame('bar', User::where('email', 'foo')->first()->name); + + // Update 1 document + $result = User::upsert([ + ['email' => 'foo', 'name' => 'bar2'], + ['name' => 'bar2', 'email' => 'foo2'], + ], 'email', ['name']); + + // Even if the same value is set for the 2nd document, the "updated_at" field is updated + $this->assertSame(2, $result); + $this->assertSame(2, User::count()); + $this->assertSame('bar2', User::where('email', 'foo')->first()->name); + + // If no update fields are specified, all fields are updated + $result = User::upsert([ + ['email' => 'foo', 'name' => 'bar3'], + ], 'email'); + + $this->assertSame(1, $result); + $this->assertSame(2, User::count()); + $this->assertSame('bar3', User::where('email', 'foo')->first()->name); + } + public function testManualStringId(): void { $user = new User(); diff --git a/tests/QueryBuilderTest.php b/tests/QueryBuilderTest.php index 4320e6a54..7924e02f3 100644 --- a/tests/QueryBuilderTest.php +++ b/tests/QueryBuilderTest.php @@ -11,6 +11,7 @@ use Illuminate\Support\LazyCollection; use Illuminate\Support\Str; use Illuminate\Testing\Assert; +use Illuminate\Tests\Database\DatabaseQueryBuilderTest; use InvalidArgumentException; use MongoDB\BSON\ObjectId; use MongoDB\BSON\Regex; @@ -588,7 +589,7 @@ public function testSubdocumentArrayAggregate() $this->assertEquals(12, DB::collection('items')->avg('amount.*.hidden')); } - public function testUpsert() + public function testUpdateWithUpsert() { DB::collection('items')->where('name', 'knife') ->update( @@ -607,6 +608,39 @@ public function testUpsert() $this->assertEquals(2, DB::collection('items')->count()); } + public function testUpsert() + { + /** @see DatabaseQueryBuilderTest::testUpsertMethod() */ + // Insert 2 documents + $result = DB::collection('users')->upsert([ + ['email' => 'foo', 'name' => 'bar'], + ['name' => 'bar2', 'email' => 'foo2'], + ], 'email', 'name'); + + $this->assertSame(2, $result); + $this->assertSame(2, DB::collection('users')->count()); + $this->assertSame('bar', DB::collection('users')->where('email', 'foo')->first()['name']); + + // Update 1 document + $result = DB::collection('users')->upsert([ + ['email' => 'foo', 'name' => 'bar2'], + ['name' => 'bar2', 'email' => 'foo2'], + ], 'email', 'name'); + + $this->assertSame(1, $result); + $this->assertSame(2, DB::collection('users')->count()); + $this->assertSame('bar2', DB::collection('users')->where('email', 'foo')->first()['name']); + + // If no update fields are specified, all fields are updated + $result = DB::collection('users')->upsert([ + ['email' => 'foo', 'name' => 'bar3'], + ], 'email'); + + $this->assertSame(1, $result); + $this->assertSame(2, DB::collection('users')->count()); + $this->assertSame('bar3', DB::collection('users')->where('email', 'foo')->first()['name']); + } + public function testUnset() { $id1 = DB::collection('users')->insertGetId(['name' => 'John Doe', 'note1' => 'ABC', 'note2' => 'DEF']); diff --git a/tests/SchemaTest.php b/tests/SchemaTest.php index 6e6248beb..e9d039fa7 100644 --- a/tests/SchemaTest.php +++ b/tests/SchemaTest.php @@ -6,8 +6,13 @@ use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; +use MongoDB\BSON\Binary; +use MongoDB\BSON\UTCDateTime; use MongoDB\Laravel\Schema\Blueprint; +use function collect; +use function count; + class SchemaTest extends TestCase { public function tearDown(): void @@ -377,6 +382,127 @@ public function testRenameColumn(): void $this->assertSame($check[2]['column'], $check2[2]['column']); } + public function testHasColumn(): void + { + DB::connection()->collection('newcollection')->insert(['column1' => 'value']); + + $this->assertTrue(Schema::hasColumn('newcollection', 'column1')); + $this->assertFalse(Schema::hasColumn('newcollection', 'column2')); + } + + public function testHasColumns(): void + { + // Insert documents with both column1 and column2 + DB::connection()->collection('newcollection')->insert([ + ['column1' => 'value1', 'column2' => 'value2'], + ['column1' => 'value3'], + ]); + + $this->assertTrue(Schema::hasColumns('newcollection', ['column1', 'column2'])); + $this->assertFalse(Schema::hasColumns('newcollection', ['column1', 'column3'])); + } + + public function testGetTables() + { + DB::connection('mongodb')->collection('newcollection')->insert(['test' => 'value']); + DB::connection('mongodb')->collection('newcollection_two')->insert(['test' => 'value']); + + $tables = Schema::getTables(); + $this->assertIsArray($tables); + $this->assertGreaterThanOrEqual(2, count($tables)); + $found = false; + foreach ($tables as $table) { + $this->assertArrayHasKey('name', $table); + $this->assertArrayHasKey('size', $table); + + if ($table['name'] === 'newcollection') { + $this->assertEquals(8192, $table['size']); + $found = true; + } + } + + if (! $found) { + $this->fail('Collection "newcollection" not found'); + } + } + + public function testGetTableListing() + { + DB::connection('mongodb')->collection('newcollection')->insert(['test' => 'value']); + DB::connection('mongodb')->collection('newcollection_two')->insert(['test' => 'value']); + + $tables = Schema::getTableListing(); + + $this->assertIsArray($tables); + $this->assertGreaterThanOrEqual(2, count($tables)); + $this->assertContains('newcollection', $tables); + $this->assertContains('newcollection_two', $tables); + } + + public function testGetColumns() + { + $collection = DB::connection('mongodb')->collection('newcollection'); + $collection->insert(['text' => 'value', 'mixed' => ['key' => 'value']]); + $collection->insert(['date' => new UTCDateTime(), 'binary' => new Binary('binary'), 'mixed' => true]); + + $columns = Schema::getColumns('newcollection'); + $this->assertIsArray($columns); + $this->assertCount(5, $columns); + + $columns = collect($columns)->keyBy('name'); + + $columns->each(function ($column) { + $this->assertIsString($column['name']); + $this->assertEquals($column['type'], $column['type_name']); + $this->assertNull($column['collation']); + $this->assertIsBool($column['nullable']); + $this->assertNull($column['default']); + $this->assertFalse($column['auto_increment']); + $this->assertIsString($column['comment']); + }); + + $this->assertEquals('objectId', $columns->get('_id')['type']); + $this->assertEquals('objectId', $columns->get('_id')['generation']['type']); + $this->assertNull($columns->get('text')['generation']); + $this->assertEquals('string', $columns->get('text')['type']); + $this->assertEquals('date', $columns->get('date')['type']); + $this->assertEquals('binData', $columns->get('binary')['type']); + $this->assertEquals('bool, object', $columns->get('mixed')['type']); + $this->assertEquals('2 occurrences', $columns->get('mixed')['comment']); + + // Non-existent collection + $columns = Schema::getColumns('missing'); + $this->assertSame([], $columns); + } + + public function testGetIndexes() + { + Schema::create('newcollection', function (Blueprint $collection) { + $collection->index('mykey1'); + $collection->string('mykey2')->unique('unique_index'); + $collection->string('mykey3')->index(); + }); + $indexes = Schema::getIndexes('newcollection'); + $this->assertIsArray($indexes); + $this->assertCount(4, $indexes); + + $indexes = collect($indexes)->keyBy('name'); + + $indexes->each(function ($index) { + $this->assertIsString($index['name']); + $this->assertIsString($index['type']); + $this->assertIsArray($index['columns']); + $this->assertIsBool($index['unique']); + $this->assertIsBool($index['primary']); + }); + $this->assertTrue($indexes->get('_id_')['primary']); + $this->assertTrue($indexes->get('unique_index_1')['unique']); + + // Non-existent collection + $indexes = Schema::getIndexes('missing'); + $this->assertSame([], $indexes); + } + protected function getIndex(string $collection, string $name) { $collection = DB::getCollection($collection);