diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 599da878e..64e9681a2 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -980,155 +980,55 @@ public function updateDocument(Document $collection, string $id, Document $docum $attributes['_createdAt'] = $document->getCreatedAt(); $attributes['_updatedAt'] = $document->getUpdatedAt(); $attributes['_permissions'] = json_encode($document->getPermissions()); + $attributes['_uid'] = $document->getId(); $name = $this->filter($collection); $columns = ''; if (!$skipPermissions) { + $newUid = $document->offsetExists('$id') ? $document->getId() : $id; + $sql = " - SELECT _type, _permission - FROM {$this->getSQLTable($name . '_perms')} + DELETE FROM {$this->getSQLTable($name . '_perms')} WHERE _document = :_uid {$this->getTenantQuery($collection)} - "; + "; - $sql = $this->trigger(Database::EVENT_PERMISSIONS_READ, $sql); - - /** - * Get current permissions from the database - */ - $sqlPermissions = $this->getPDO()->prepare($sql); - $sqlPermissions->bindValue(':_uid', $document->getId()); + $sql = $this->trigger(Database::EVENT_PERMISSIONS_DELETE, $sql); + $stmtRemovePermissions = $this->getPDO()->prepare($sql); + $stmtRemovePermissions->bindValue(':_uid', $id); if ($this->sharedTables) { - $sqlPermissions->bindValue(':_tenant', $this->tenant); + $stmtRemovePermissions->bindValue(':_tenant', $this->tenant); } - $sqlPermissions->execute(); - $permissions = $sqlPermissions->fetchAll(); - $sqlPermissions->closeCursor(); - - $initial = []; - foreach (Database::PERMISSIONS as $type) { - $initial[$type] = []; - } - - $permissions = array_reduce($permissions, function (array $carry, array $item) { - $carry[$item['_type']][] = $item['_permission']; - - return $carry; - }, $initial); - - /** - * Get removed Permissions - */ - $removals = []; + $values = []; + $binds = []; foreach (Database::PERMISSIONS as $type) { - $diff = \array_diff($permissions[$type], $document->getPermissionsByType($type)); - if (!empty($diff)) { - $removals[$type] = $diff; - } - } - - /** - * Get added Permissions - */ - $additions = []; - foreach (Database::PERMISSIONS as $type) { - $diff = \array_diff($document->getPermissionsByType($type), $permissions[$type]); - if (!empty($diff)) { - $additions[$type] = $diff; - } - } - - /** - * Query to remove permissions - */ - $removeQuery = ''; - if (!empty($removals)) { - $removeQuery = ' AND ('; - foreach ($removals as $type => $permissions) { - $removeQuery .= "( - _type = '{$type}' - AND _permission IN (" . implode(', ', \array_map(fn (string $i) => ":_remove_{$type}_{$i}", \array_keys($permissions))) . ") - )"; - if ($type !== \array_key_last($removals)) { - $removeQuery .= ' OR '; - } + foreach ($document->getPermissionsByType($type) as $i => $permission) { + $tenantPlaceholder = $this->sharedTables ? ', :_tenant' : ''; + $values[] = "( :_uid, '{$type}', :_add_{$type}_{$i} {$tenantPlaceholder})"; + $binds[":_add_{$type}_{$i}"] = $permission; } } - if (!empty($removeQuery)) { - $removeQuery .= ')'; - $sql = " - DELETE - FROM {$this->getSQLTable($name . '_perms')} - WHERE _document = :_uid - {$this->getTenantQuery($collection)} - "; - - $removeQuery = $sql . $removeQuery; - $removeQuery = $this->trigger(Database::EVENT_PERMISSIONS_DELETE, $removeQuery); - - $stmtRemovePermissions = $this->getPDO()->prepare($removeQuery); - $stmtRemovePermissions->bindValue(':_uid', $document->getId()); - - if ($this->sharedTables) { - $stmtRemovePermissions->bindValue(':_tenant', $this->tenant); - } - - foreach ($removals as $type => $permissions) { - foreach ($permissions as $i => $permission) { - $stmtRemovePermissions->bindValue(":_remove_{$type}_{$i}", $permission); - } - } - } - - /** - * Query to add permissions - */ - if (!empty($additions)) { - $values = []; - foreach ($additions as $type => $permissions) { - foreach ($permissions as $i => $_) { - $value = "( :_uid, '{$type}', :_add_{$type}_{$i}"; - - if ($this->sharedTables) { - $value .= ", :_tenant)"; - } else { - $value .= ")"; - } - - $values[] = $value; - } - } + if (!empty($values)) { + $tenantColumn = $this->sharedTables ? ', _tenant' : ''; $sql = " - INSERT INTO {$this->getSQLTable($name . '_perms')} (_document, _type, _permission - "; - - if ($this->sharedTables) { - $sql .= ', _tenant)'; - } else { - $sql .= ')'; - } - - $sql .= " VALUES " . \implode(', ', $values); + INSERT INTO {$this->getSQLTable($name . '_perms')} (_document, _type, _permission {$tenantColumn}) + VALUES " . \implode(', ', $values); $sql = $this->trigger(Database::EVENT_PERMISSIONS_CREATE, $sql); $stmtAddPermissions = $this->getPDO()->prepare($sql); - - $stmtAddPermissions->bindValue(":_uid", $document->getId()); - + $stmtAddPermissions->bindValue(":_uid", $newUid); if ($this->sharedTables) { $stmtAddPermissions->bindValue(":_tenant", $this->tenant); } - foreach ($additions as $type => $permissions) { - foreach ($permissions as $i => $permission) { - $stmtAddPermissions->bindValue(":_add_{$type}_{$i}", $permission); - } + foreach ($binds as $key => $permission) { + $stmtAddPermissions->bindValue($key, $permission); } } } @@ -1168,7 +1068,7 @@ public function updateDocument(Document $collection, string $id, Document $docum $sql = " UPDATE {$this->getSQLTable($name)} - SET {$columns} _uid = :_newUid + SET " . \rtrim($columns, ',') . " WHERE _id=:_sequence {$this->getTenantQuery($collection)} "; @@ -1178,7 +1078,6 @@ public function updateDocument(Document $collection, string $id, Document $docum $stmt = $this->getPDO()->prepare($sql); $stmt->bindValue(':_sequence', $document->getSequence()); - $stmt->bindValue(':_newUid', $document->getId()); if ($this->sharedTables) { $stmt->bindValue(':_tenant', $this->tenant); diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index d81cdec0b..211761089 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -1139,140 +1139,55 @@ public function updateDocument(Document $collection, string $id, Document $docum $attributes['_createdAt'] = $document->getCreatedAt(); $attributes['_updatedAt'] = $document->getUpdatedAt(); $attributes['_permissions'] = json_encode($document->getPermissions()); + $attributes['_uid'] = $document->getId(); $name = $this->filter($collection); $columns = ''; if (!$skipPermissions) { + $newUid = $document->offsetExists('$id') ? $document->getId() : $id; + $sql = " - SELECT _type, _permission - FROM {$this->getSQLTable($name . '_perms')} + DELETE FROM {$this->getSQLTable($name . '_perms')} WHERE _document = :_uid {$this->getTenantQuery($collection)} "; - $sql = $this->trigger(Database::EVENT_PERMISSIONS_READ, $sql); - - /** - * Get current permissions from the database - */ - $permissionsStmt = $this->getPDO()->prepare($sql); - $permissionsStmt->bindValue(':_uid', $document->getId()); + $sql = $this->trigger(Database::EVENT_PERMISSIONS_DELETE, $sql); + $stmtRemovePermissions = $this->getPDO()->prepare($sql); + $stmtRemovePermissions->bindValue(':_uid', $id); if ($this->sharedTables) { - $permissionsStmt->bindValue(':_tenant', $this->tenant); + $stmtRemovePermissions->bindValue(':_tenant', $this->tenant); } - $this->execute($permissionsStmt); - $permissions = $permissionsStmt->fetchAll(); - $permissionsStmt->closeCursor(); - - $initial = []; + $values = []; + $binds = []; foreach (Database::PERMISSIONS as $type) { - $initial[$type] = []; - } - - $permissions = array_reduce($permissions, function (array $carry, array $item) { - $carry[$item['_type']][] = $item['_permission']; - - return $carry; - }, $initial); - - /** - * Get removed Permissions - */ - $removals = []; - foreach (Database::PERMISSIONS as $type) { - $diff = \array_diff($permissions[$type], $document->getPermissionsByType($type)); - if (!empty($diff)) { - $removals[$type] = $diff; - } - } - - /** - * Get added Permissions - */ - $additions = []; - foreach (Database::PERMISSIONS as $type) { - $diff = \array_diff($document->getPermissionsByType($type), $permissions[$type]); - if (!empty($diff)) { - $additions[$type] = $diff; - } - } - - /** - * Query to remove permissions - */ - $removeQuery = ''; - if (!empty($removals)) { - $removeQuery = ' AND ('; - foreach ($removals as $type => $permissions) { - $removeQuery .= "( - _type = '{$type}' - AND _permission IN (" . implode(', ', \array_map(fn (string $i) => ":_remove_{$type}_{$i}", \array_keys($permissions))) . ") - )"; - if ($type !== \array_key_last($removals)) { - $removeQuery .= ' OR '; - } + foreach ($document->getPermissionsByType($type) as $i => $permission) { + $sqlTenant = $this->sharedTables ? ', :_tenant' : ''; + $values[] = "( :_uid, '{$type}', :_add_{$type}_{$i} {$sqlTenant})"; + $binds[":_add_{$type}_{$i}"] = $permission; } } - if (!empty($removeQuery)) { - $removeQuery .= ')'; - - $sql = " - DELETE - FROM {$this->getSQLTable($name . '_perms')} - WHERE _document = :_uid - {$this->getTenantQuery($collection)} - "; - - $removeQuery = $sql . $removeQuery; - - $removeQuery = $this->trigger(Database::EVENT_PERMISSIONS_DELETE, $removeQuery); - $stmtRemovePermissions = $this->getPDO()->prepare($removeQuery); - $stmtRemovePermissions->bindValue(':_uid', $document->getId()); - - if ($this->sharedTables) { - $stmtRemovePermissions->bindValue(':_tenant', $this->tenant); - } - - foreach ($removals as $type => $permissions) { - foreach ($permissions as $i => $permission) { - $stmtRemovePermissions->bindValue(":_remove_{$type}_{$i}", $permission); - } - } - } - - /** - * Query to add permissions - */ - if (!empty($additions)) { - $values = []; - foreach ($additions as $type => $permissions) { - foreach ($permissions as $i => $_) { - $sqlTenant = $this->sharedTables ? ', :_tenant' : ''; - $values[] = "( :_uid, '{$type}', :_add_{$type}_{$i} {$sqlTenant})"; - } - } + if (!empty($values)) { $sqlTenant = $this->sharedTables ? ', _tenant' : ''; $sql = " INSERT INTO {$this->getSQLTable($name . '_perms')} (_document, _type, _permission {$sqlTenant}) - VALUES" . \implode(', ', $values); + VALUES " . \implode(', ', $values); $sql = $this->trigger(Database::EVENT_PERMISSIONS_CREATE, $sql); $stmtAddPermissions = $this->getPDO()->prepare($sql); - $stmtAddPermissions->bindValue(":_uid", $document->getId()); + $stmtAddPermissions->bindValue(":_uid", $newUid); if ($this->sharedTables) { $stmtAddPermissions->bindValue(':_tenant', $this->tenant); } - foreach ($additions as $type => $permissions) { - foreach ($permissions as $i => $permission) { - $stmtAddPermissions->bindValue(":_add_{$type}_{$i}", $permission); - } + foreach ($binds as $key => $permission) { + $stmtAddPermissions->bindValue($key, $permission); } } } @@ -1312,7 +1227,7 @@ public function updateDocument(Document $collection, string $id, Document $docum $sql = " UPDATE {$this->getSQLTable($name)} - SET {$columns} _uid = :_newUid + SET " . \rtrim($columns, ',') . " WHERE _id=:_sequence {$this->getTenantQuery($collection)} "; @@ -1322,7 +1237,6 @@ public function updateDocument(Document $collection, string $id, Document $docum $stmt = $this->getPDO()->prepare($sql); $stmt->bindValue(':_sequence', $document->getSequence()); - $stmt->bindValue(':_newUid', $document->getId()); if ($this->sharedTables) { $stmt->bindValue(':_tenant', $this->tenant); diff --git a/src/Database/Adapter/SQLite.php b/src/Database/Adapter/SQLite.php index 5b07ce3b7..eb7e92b5b 100644 --- a/src/Database/Adapter/SQLite.php +++ b/src/Database/Adapter/SQLite.php @@ -1275,6 +1275,7 @@ public function updateDocument(Document $collection, string $id, Document $docum $attributes['_createdAt'] = $document->getCreatedAt(); $attributes['_updatedAt'] = $document->getUpdatedAt(); $attributes['_permissions'] = json_encode($document->getPermissions()); + $attributes['_uid'] = $document->getId(); if ($this->sharedTables) { $attributes['_tenant'] = $this->tenant; @@ -1284,116 +1285,33 @@ public function updateDocument(Document $collection, string $id, Document $docum $columns = ''; if (!$skipPermissions) { + $newUid = $document->offsetExists('$id') ? $document->getId() : $id; + $sql = " - SELECT _type, _permission - FROM `{$this->getNamespace()}_{$name}_perms` + DELETE FROM `{$this->getNamespace()}_{$name}_perms` WHERE _document = :_uid {$this->getTenantQuery($collection)} "; - $sql = $this->trigger(Database::EVENT_PERMISSIONS_READ, $sql); - - /** - * Get current permissions from the database - */ - $permissionsStmt = $this->getPDO()->prepare($sql); - $permissionsStmt->bindValue(':_uid', $document->getId()); + $sql = $this->trigger(Database::EVENT_PERMISSIONS_DELETE, $sql); + $stmtRemovePermissions = $this->getPDO()->prepare($sql); + $stmtRemovePermissions->bindValue(':_uid', $id); if ($this->sharedTables) { - $permissionsStmt->bindValue(':_tenant', $this->tenant); + $stmtRemovePermissions->bindValue(':_tenant', $this->tenant); } - $permissionsStmt->execute(); - $permissions = $permissionsStmt->fetchAll(); - $permissionsStmt->closeCursor(); - - $initial = []; + $values = []; + $binds = []; foreach (Database::PERMISSIONS as $type) { - $initial[$type] = []; - } - - $permissions = array_reduce($permissions, function (array $carry, array $item) { - $carry[$item['_type']][] = $item['_permission']; - - return $carry; - }, $initial); - - /** - * Get removed Permissions - */ - $removals = []; - foreach (Database::PERMISSIONS as $type) { - $diff = \array_diff($permissions[$type], $document->getPermissionsByType($type)); - if (!empty($diff)) { - $removals[$type] = $diff; + foreach ($document->getPermissionsByType($type) as $i => $permission) { + $tenantQuery = $this->sharedTables ? ', :_tenant' : ''; + $values[] = "(:_uid, '{$type}', :_add_{$type}_{$i} {$tenantQuery})"; + $binds[":_add_{$type}_{$i}"] = $permission; } } - /** - * Get added Permissions - */ - $additions = []; - foreach (Database::PERMISSIONS as $type) { - $diff = \array_diff($document->getPermissionsByType($type), $permissions[$type]); - if (!empty($diff)) { - $additions[$type] = $diff; - } - } - - /** - * Query to remove permissions - */ - $removeQuery = ''; - if (!empty($removals)) { - $removeQuery = ' AND ('; - foreach ($removals as $type => $permissions) { - $removeQuery .= "( - _type = '{$type}' - AND _permission IN (" . implode(', ', \array_map(fn (string $i) => ":_remove_{$type}_{$i}", \array_keys($permissions))) . ") - )"; - if ($type !== \array_key_last($removals)) { - $removeQuery .= ' OR '; - } - } - } - if (!empty($removeQuery)) { - $removeQuery .= ')'; - $sql = " - DELETE - FROM `{$this->getNamespace()}_{$name}_perms` - WHERE _document = :_uid - {$this->getTenantQuery($collection)} - "; - - $removeQuery = $sql . $removeQuery; - $removeQuery = $this->trigger(Database::EVENT_PERMISSIONS_DELETE, $removeQuery); - - $stmtRemovePermissions = $this->getPDO()->prepare($removeQuery); - $stmtRemovePermissions->bindValue(':_uid', $document->getId()); - - if ($this->sharedTables) { - $stmtRemovePermissions->bindValue(':_tenant', $this->tenant); - } - - foreach ($removals as $type => $permissions) { - foreach ($permissions as $i => $permission) { - $stmtRemovePermissions->bindValue(":_remove_{$type}_{$i}", $permission); - } - } - } - - /** - * Query to add permissions - */ - if (!empty($additions)) { - $values = []; - foreach ($additions as $type => $permissions) { - foreach ($permissions as $i => $_) { - $tenantQuery = $this->sharedTables ? ', :_tenant' : ''; - $values[] = "(:_uid, '{$type}', :_add_{$type}_{$i} {$tenantQuery})"; - } - } - + if (!empty($values)) { $tenantQuery = $this->sharedTables ? ', _tenant' : ''; $sql = " @@ -1403,16 +1321,13 @@ public function updateDocument(Document $collection, string $id, Document $docum $sql = $this->trigger(Database::EVENT_PERMISSIONS_CREATE, $sql); $stmtAddPermissions = $this->getPDO()->prepare($sql); - - $stmtAddPermissions->bindValue(":_uid", $document->getId()); + $stmtAddPermissions->bindValue(":_uid", $newUid); if ($this->sharedTables) { $stmtAddPermissions->bindValue(":_tenant", $this->tenant); } - foreach ($additions as $type => $permissions) { - foreach ($permissions as $i => $permission) { - $stmtAddPermissions->bindValue(":_add_{$type}_{$i}", $permission); - } + foreach ($binds as $key => $permission) { + $stmtAddPermissions->bindValue($key, $permission); } } } @@ -1456,7 +1371,7 @@ public function updateDocument(Document $collection, string $id, Document $docum $sql = " UPDATE `{$this->getNamespace()}_{$name}` - SET {$columns}, _uid = :_newUid + SET {$columns} WHERE _uid = :_existingUid {$this->getTenantQuery($collection)} "; @@ -1466,7 +1381,6 @@ public function updateDocument(Document $collection, string $id, Document $docum $stmt = $this->getPDO()->prepare($sql); $stmt->bindValue(':_existingUid', $id); - $stmt->bindValue(':_newUid', $document->getId()); if ($this->sharedTables) { $stmt->bindValue(':_tenant', $this->tenant); @@ -2930,14 +2844,15 @@ public function getSchemaIndexes(string $collection): array * Schema-index entries for FTS5 fulltext tables on `$collection`. * Maps each back to a metadata index id when possible. * - * @return array, - * lengths: array, - * }> + * Each entry has keys: + * - `$id`: string + * - `indexName`: string + * - `indexType`: string + * - `nonUnique`: int + * - `columns`: array + * - `lengths`: array + * + * @return array> */ protected function getFulltextSchemaIndexes(string $collection): array { diff --git a/src/Database/Database.php b/src/Database/Database.php index 37b903d13..5a6cd97d7 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -6170,10 +6170,17 @@ public function updateDocument(string $collection, string $id, Document $documen $skipPermissionsUpdate = ($originalPermissions === $currentPermissions); } + + // UID change + if ($document->offsetExists('$id') && $document->getId() !== $id) { + $skipPermissionsUpdate = false; + } + $createdAt = $document->getCreatedAt(); $document = \array_merge($old->getArrayCopy(), $document->getArrayCopy()); $document['$collection'] = $old->getAttribute('$collection'); // Make sure user doesn't switch collection ID + $document['$sequence'] = $old->getSequence(); // Sequence is immutable $document['$createdAt'] = ($createdAt === null || !$this->preserveDates) ? $old->getCreatedAt() : $createdAt; if ($this->adapter->getSharedTables()) { diff --git a/tests/e2e/Adapter/Scopes/DocumentTests.php b/tests/e2e/Adapter/Scopes/DocumentTests.php index 23cc3b623..4f998372d 100644 --- a/tests/e2e/Adapter/Scopes/DocumentTests.php +++ b/tests/e2e/Adapter/Scopes/DocumentTests.php @@ -1572,6 +1572,113 @@ public function testCreateDocumentDefaults(): void $database->deleteCollection('defaults'); } + /** + * When a document's UID changes on update, its permission rows in the + * collection's _perms table must follow the new UID. Otherwise the old + * rows are orphaned and the renamed document is left with no permissions, + * even when the permission set itself was not changed. + */ + public function testUpdateDocumentChangeIdMigratesPermissions(): void + { + /** @var Database $database */ + $database = $this->getDatabase(); + + $auth = $database->getAuthorization(); + + $collection = 'update_change_id_perms'; + + try { + // documentSecurity with no collection-level permissions: reads are + // governed purely by the document's rows in the _perms table. + $database->createCollection($collection, permissions: [], documentSecurity: true); + $this->assertEquals(true, $database->createAttribute($collection, 'name', Database::VAR_STRING, 128, false)); + + // Create a document whose read permission is scoped to a single role, + // so that find() must consult the _perms table to return it. + $document = $auth->skip(fn () => $database->createDocument($collection, new Document([ + '$id' => 'old_id', + 'name' => 'test', + '$permissions' => [ + Permission::read(Role::user('alice')), + Permission::update(Role::user('alice')), + Permission::delete(Role::user('alice')), + ], + ]))); + $this->assertEquals('old_id', $document->getId()); + + // Sanity: as alice the document is visible via the _perms table. + $auth->addRole(Role::user('alice')->toString()); + $this->assertCount(1, $database->find($collection)); + + // Rename the document WITHOUT changing its permission set. + $renamed = $auth->skip(fn () => $database->updateDocument($collection, 'old_id', new Document(\array_merge( + $document->getArrayCopy(), + ['$id' => 'new_id'], + )))); + $this->assertEquals('new_id', $renamed->getId()); + + // The old UID must no longer resolve to a document. + $this->assertTrue($auth->skip(fn () => $database->getDocument($collection, 'old_id'))->isEmpty()); + + // The new UID must exist and keep its permissions on the main row. + $newDoc = $auth->skip(fn () => $database->getDocument($collection, 'new_id')); + $this->assertFalse($newDoc->isEmpty()); + $this->assertContains(Permission::read(Role::user('alice')), $newDoc->getPermissions()); + + // The crucial check: the permission rows must have migrated to the new + // UID in the _perms table. As alice, find() (which joins _perms) must + // still return exactly the renamed document. With orphaned rows under + // the old UID this returns 0. + $found = $database->find($collection); + $this->assertCount(1, $found); + $this->assertEquals('new_id', $found[0]->getId()); + + /** + * Second scenario: change the UID AND the permission set in the same + * update. Drop alice's access and grant bob instead. The removed rows + * must be gone, the added rows must land under the new UID, and nothing + * may be left orphaned under the old UID. + */ + $rekeyed = $auth->skip(fn () => $database->updateDocument($collection, 'new_id', new Document(\array_merge( + $newDoc->getArrayCopy(), + [ + '$id' => 'final_id', + '$permissions' => [ + Permission::read(Role::user('bob')), + Permission::read(Role::user('bob')), // Duplication check + Permission::update(Role::user('bob')), + Permission::delete(Role::user('bob')), + ], + ], + )))); + $this->assertEquals('final_id', $rekeyed->getId()); + + // The old UID must no longer resolve to a document. + $this->assertTrue($auth->skip(fn () => $database->getDocument($collection, 'new_id'))->isEmpty()); + + // The main row must reflect the new permission set. + $finalDoc = $auth->skip(fn () => $database->getDocument($collection, 'final_id')); + $this->assertFalse($finalDoc->isEmpty()); + $this->assertContains(Permission::read(Role::user('bob')), $finalDoc->getPermissions()); + $this->assertNotContains(Permission::read(Role::user('alice')), $finalDoc->getPermissions()); + + // alice's permission rows were removed: as alice nothing is returned. + $this->assertCount(0, $database->find($collection)); + + // bob's permission rows landed under the new UID: as bob the renamed + // document is returned via the _perms join. + $auth->addRole(Role::user('bob')->toString()); + $foundAsBob = $database->find($collection); + $this->assertCount(1, $foundAsBob); + $this->assertEquals('final_id', $foundAsBob[0]->getId()); + } finally { + $auth->removeRole(Role::user('alice')->toString()); + $auth->removeRole(Role::user('bob')->toString()); + + $auth->skip(fn () => $database->deleteCollection($collection)); + } + } + public function testIncreaseDecrease(): Document { /** @var Database $database */