diff --git a/src/Database/Hook/Relationships.php b/src/Database/Hook/Relationships.php index 63c0fa9a5..e037b0de0 100644 --- a/src/Database/Hook/Relationships.php +++ b/src/Database/Hook/Relationships.php @@ -592,7 +592,10 @@ private function collectCreated(PreparedCreate $prepared, Document $collection, continue; } - $relatedCollection = $this->collection($prepared, $relationship->relatedCollection); + $relatedCollection = $this->relatedCollection($relationship, $prepared); + if ($relatedCollection === null) { + continue; + } if ($depth >= Database::RELATION_MAX_DEPTH - 1 && $writeStack[$depth - 1] !== $relatedCollection->getId()) { continue; @@ -701,6 +704,57 @@ private function collection(PreparedCreate $prepared, string $id): Document return $prepared->collections[$id] ??= $this->database->getCollection($id); } + /** + * The collection a relationship relates to, or null when it was deleted while the relationship was left behind, + * as 7.x could leave it: such a relationship is not related, populated or cascaded, and keeps its stored value. + */ + private function relatedCollection(Relationship $relationship, ?PreparedCreate $prepared = null): ?Document + { + $id = $relationship->relatedCollection; + if ($prepared !== null && isset($prepared->collections[$id])) { + return $prepared->collections[$id]; + } + + $collection = $this->database->findCollection($id); + if ($prepared !== null && $collection !== null) { + $prepared->collections[$id] = $collection; + } + + return $collection; + } + + /** + * Keep the value written to a relationship whose related collection was deleted as the side holding its key + * stores it, by ID, and drop it from a side that stores nothing. + */ + private static function detach(Attribute $attribute, Document $document): void + { + $key = $attribute->key; + $relationship = $attribute->relationship; + if ($relationship === null) { + return; + } + + $parent = $attribute->side === RelationshipSide::Parent; + $storesKey = match ($relationship->type) { + RelationshipType::OneToOne => $parent || $relationship->twoWay, + RelationshipType::OneToMany => ! $parent, + RelationshipType::ManyToOne => $parent, + RelationshipType::ManyToMany => false, + }; + + if (! $storesKey) { + $document->removeAttribute($key); + + return; + } + + $value = $document->getAttribute($key); + if ($value instanceof Document) { + $document->setAttribute($key, $value->getId()); + } + } + /** * The attributes $collection declares, hydrated at most once per collection instance. * @@ -865,9 +919,12 @@ private function relateAttributes(Document $collection, Document $document, int if ($copies !== null && $stackCount === 0 && (\is_array($value) || $value instanceof Document)) { $this->copy($document, $copies); } - $relatedCollection = $prepared === null - ? $this->database->getCollection($relationship->relatedCollection) - : $this->collection($prepared, $relationship->relatedCollection); + $relatedCollection = $this->relatedCollection($relationship, $prepared); + if ($relatedCollection === null) { + self::detach($attribute, $document); + + continue; + } $relationType = $relationship->type; $twoWay = $relationship->twoWay; $twoWayKey = $relationship->twoWayKey ?? ''; @@ -1032,7 +1089,6 @@ public function afterDocumentUpdate(Document $collection, Document $old, Documen $oldValue = $old->getAttribute($key); $oldId = $oldValue instanceof Document ? $oldValue->getId() : (\is_string($oldValue) ? $oldValue : null); - $relatedCollection = $this->database->getCollection($relationship->relatedCollection); $relationType = $relationship->type; $twoWay = $relationship->twoWay; $twoWayKey = $relationship->twoWayKey ?? ''; @@ -1067,6 +1123,13 @@ public function afterDocumentUpdate(Document $collection, Document $old, Documen continue; } + $relatedCollection = $this->relatedCollection($relationship); + if ($relatedCollection === null) { + self::detach($attribute, $document); + + continue; + } + if ($stackCount >= Database::RELATION_MAX_DEPTH - 1 && $writeStack[$stackCount - 1] !== $relatedCollection->getId()) { $document->removeAttribute($key); @@ -1539,9 +1602,13 @@ public function beforeDocumentDelete(Document $collection, Document $document, b continue; } + $relatedCollection = $this->relatedCollection($relationship); + if ($relatedCollection === null) { + continue; + } + $key = $attribute->key; $value = $document->getAttribute($key); - $relatedCollection = $this->database->getCollection($relationship->relatedCollection); $relationType = $relationship->type; $twoWay = $relationship->twoWay; $twoWayKey = $relationship->twoWayKey ?? ''; @@ -1730,9 +1797,15 @@ public function populateDocuments(array $documents, Document $collection, int $f continue; } + $relatedCollection = $this->relatedCollection($relationship); + if ($relatedCollection === null) { + continue; + } + $relatedDocuments = $this->populateSingleRelationshipBatch( $batchDocuments, $batchCollection, + $relatedCollection, $key, $relationship, $side, @@ -1747,25 +1820,20 @@ public function populateDocuments(array $documents, Document $collection, int $f ($hasNestedSelects || ! $parentHasExplicitSelects); if ($shouldQueue) { - $relatedCollectionId = $relationship->relatedCollection; - $relatedCollection = $this->database->silent(fn () => $this->database->findCollection($relatedCollectionId)); - - if ($relatedCollection !== null) { - $relationshipQueries = $hasNestedSelects ? $batchSelects[$key] : []; + $relationshipQueries = $hasNestedSelects ? $batchSelects[$key] : []; - $nextSelects = $this->processQueries(self::relationships($relatedCollection), $relationshipQueries); + $nextSelects = $this->processQueries(self::relationships($relatedCollection), $relationshipQueries); - $childHasExplicitSelects = $parentHasExplicitSelects || self::selects($relationshipQueries); + $childHasExplicitSelects = $parentHasExplicitSelects || self::selects($relationshipQueries); - $nextQueue[] = [ - 'documents' => $relatedDocuments, - 'collection' => $relatedCollection, - 'depth' => $currentDepth + 1, - 'selects' => $nextSelects, - 'skipKey' => $twoWay ? $twoWayKey : null, - 'hasExplicitSelects' => $childHasExplicitSelects, - ]; - } + $nextQueue[] = [ + 'documents' => $relatedDocuments, + 'collection' => $relatedCollection, + 'depth' => $currentDepth + 1, + 'selects' => $nextSelects, + 'skipKey' => $twoWay ? $twoWayKey : null, + 'hasExplicitSelects' => $childHasExplicitSelects, + ]; } if ($twoWay && ! empty($relatedDocuments)) { @@ -2413,13 +2481,13 @@ private function applyRelationshipOperator(Operator $operator, array $existingId * @param array $queries * @return array */ - private function populateSingleRelationshipBatch(array $documents, Document $collection, string $key, Relationship $relationship, RelationshipSide $side, array $queries): array + private function populateSingleRelationshipBatch(array $documents, Document $collection, Document $relatedCollection, string $key, Relationship $relationship, RelationshipSide $side, array $queries): array { return match ($relationship->type) { - RelationshipType::OneToOne => $this->populateOneToOneRelationshipsBatch($documents, $key, $relationship, $queries), - RelationshipType::OneToMany => $this->populateOneToManyRelationshipsBatch($documents, $key, $relationship, $side, $queries), - RelationshipType::ManyToOne => $this->populateManyToOneRelationshipsBatch($documents, $key, $relationship, $side, $queries), - RelationshipType::ManyToMany => $this->populateManyToManyRelationshipsBatch($documents, $collection, $key, $relationship, $side, $queries), + RelationshipType::OneToOne => $this->populateOneToOneRelationshipsBatch($documents, $relatedCollection, $key, $relationship, $queries), + RelationshipType::OneToMany => $this->populateOneToManyRelationshipsBatch($documents, $relatedCollection, $key, $relationship, $side, $queries), + RelationshipType::ManyToOne => $this->populateManyToOneRelationshipsBatch($documents, $relatedCollection, $key, $relationship, $side, $queries), + RelationshipType::ManyToMany => $this->populateManyToManyRelationshipsBatch($documents, $collection, $relatedCollection, $key, $relationship, $side, $queries), }; } @@ -2428,10 +2496,8 @@ private function populateSingleRelationshipBatch(array $documents, Document $col * @param array $queries * @return array */ - private function populateOneToOneRelationshipsBatch(array $documents, string $key, Relationship $relationship, array $queries): array + private function populateOneToOneRelationshipsBatch(array $documents, Document $relatedCollection, string $key, Relationship $relationship, array $queries): array { - $relatedCollection = $this->database->getCollection($relationship->relatedCollection); - $relatedIds = []; $documentsByRelatedId = []; @@ -2505,10 +2571,9 @@ private function populateOneToOneRelationshipsBatch(array $documents, string $ke * @param array $queries * @return array */ - private function populateOneToManyRelationshipsBatch(array $documents, string $key, Relationship $relationship, RelationshipSide $side, array $queries): array + private function populateOneToManyRelationshipsBatch(array $documents, Document $relatedCollection, string $key, Relationship $relationship, RelationshipSide $side, array $queries): array { $twoWayKey = $relationship->twoWayKey ?? ''; - $relatedCollection = $this->database->getCollection($relationship->relatedCollection); if ($side === RelationshipSide::Child) { if (! $relationship->twoWay) { @@ -2519,7 +2584,7 @@ private function populateOneToManyRelationshipsBatch(array $documents, string $k return []; } - return $this->populateOneToOneRelationshipsBatch($documents, $key, $relationship, $queries); + return $this->populateOneToOneRelationshipsBatch($documents, $relatedCollection, $key, $relationship, $queries); } $parentIds = []; @@ -2583,13 +2648,12 @@ private function populateOneToManyRelationshipsBatch(array $documents, string $k * @param array $queries * @return array */ - private function populateManyToOneRelationshipsBatch(array $documents, string $key, Relationship $relationship, RelationshipSide $side, array $queries): array + private function populateManyToOneRelationshipsBatch(array $documents, Document $relatedCollection, string $key, Relationship $relationship, RelationshipSide $side, array $queries): array { $twoWayKey = $relationship->twoWayKey ?? ''; - $relatedCollection = $this->database->getCollection($relationship->relatedCollection); if ($side === RelationshipSide::Parent) { - return $this->populateOneToOneRelationshipsBatch($documents, $key, $relationship, $queries); + return $this->populateOneToOneRelationshipsBatch($documents, $relatedCollection, $key, $relationship, $queries); } if (! $relationship->twoWay) { @@ -2661,10 +2725,9 @@ private function populateManyToOneRelationshipsBatch(array $documents, string $k * @param array $queries * @return array */ - private function populateManyToManyRelationshipsBatch(array $documents, Document $collection, string $key, Relationship $relationship, RelationshipSide $side, array $queries): array + private function populateManyToManyRelationshipsBatch(array $documents, Document $collection, Document $relatedCollection, string $key, Relationship $relationship, RelationshipSide $side, array $queries): array { $twoWayKey = $relationship->twoWayKey ?? ''; - $relatedCollection = $this->database->getCollection($relationship->relatedCollection); if (! $relationship->twoWay && $side === RelationshipSide::Child) { return []; @@ -3389,7 +3452,10 @@ private function prepareNestedValues(Document $collection, Document $document, a continue; } - $relatedCollection = $this->database->getCollection($relationship->relatedCollection); + $relatedCollection = $this->relatedCollection($relationship); + if ($relatedCollection === null) { + continue; + } if ($depth >= Database::RELATION_MAX_DEPTH - 1 && $writeStack[$depth - 1] !== $relatedCollection->getId()) { continue; } @@ -3537,7 +3603,10 @@ private function authorizeRelated(Document $collection, Document $document, ?Doc continue; } - $relatedCollection = $this->database->getCollection($relationship->relatedCollection); + $relatedCollection = $this->relatedCollection($relationship); + if ($relatedCollection === null) { + continue; + } if ($depth >= Database::RELATION_MAX_DEPTH - 1 && $writeStack[$depth - 1] !== $relatedCollection->getId()) { continue; } diff --git a/src/Database/Trait/Documents.php b/src/Database/Trait/Documents.php index 9d01213fe..6793dcc9a 100644 --- a/src/Database/Trait/Documents.php +++ b/src/Database/Trait/Documents.php @@ -5517,7 +5517,11 @@ private function validateRelationshipSelects(Collection $collection, array $quer continue; } - $related = $this->silent(fn () => $this->getCollection($relatedCollectionId)); + $related = $this->silent(fn () => $this->findCollection($relatedCollectionId)); + if ($related === null) { + continue; + } + $validator = new SelectValidator( [...$related->attributes(), ...DocumentsValidator::internalAttributes()], $this->adapter->supports(Capability::DefinedAttributes), diff --git a/tests/e2e/Adapter/Scopes/RelationshipTests.php b/tests/e2e/Adapter/Scopes/RelationshipTests.php index ad30ee21d..1030a234b 100644 --- a/tests/e2e/Adapter/Scopes/RelationshipTests.php +++ b/tests/e2e/Adapter/Scopes/RelationshipTests.php @@ -12,6 +12,7 @@ use Tests\E2E\Adapter\Scopes\Relationships\NestedPreparationTests; use Tests\E2E\Adapter\Scopes\Relationships\OneToManyTests; use Tests\E2E\Adapter\Scopes\Relationships\OneToOneTests; +use Tests\E2E\Adapter\Scopes\Relationships\OrphanedTests; use Tests\E2E\Adapter\Support\EventRecorder; use Throwable; use Utopia\Database\Adapter\Feature; @@ -46,6 +47,7 @@ trait RelationshipTests use NestedPreparationTests; use OneToManyTests; use OneToOneTests; + use OrphanedTests; /** * @return array diff --git a/tests/e2e/Adapter/Scopes/Relationships/OrphanedTests.php b/tests/e2e/Adapter/Scopes/Relationships/OrphanedTests.php new file mode 100644 index 000000000..b8d991446 --- /dev/null +++ b/tests/e2e/Adapter/Scopes/Relationships/OrphanedTests.php @@ -0,0 +1,272 @@ +getDatabase(); + $articles = $this->orphanedArticles($database); + if ($articles === null) { + return; + } + + $updated = $database->updateDocument($articles, 'article1', new Document(['name' => 'updated'])); + $this->assertSame('updated', $updated->getAttribute('name'), 'updateDocument must succeed past relationships to deleted collections'); + $article = $database->getDocument($articles, 'article1'); + $this->assertSame('updated', $article->getAttribute('name')); + $this->assertSame('author1', $article->getAttribute('author'), 'updateDocument must leave an untouched orphaned relationship as stored'); + $this->assertSame('editor1', $article->getAttribute('editor')); + + $updated = $database->updateDocument($articles, 'article1', $article->setAttribute('name', 'whole')); + $this->assertSame('whole', $updated->getAttribute('name'), 'updateDocument must take back the whole document it read'); + $this->assertSame('author1', $database->getDocument($articles, 'article1')->getAttribute('author')); + + $updated = $database->updateDocument($articles, 'article1', new Document([ + 'name' => 'linked', + 'comments' => ['comment2'], + 'tags' => ['tag2'], + 'topics' => ['topic2'], + ])); + $this->assertSame('linked', $updated->getAttribute('name'), 'updateDocument must drop a write to an orphaned relationship that stores nothing'); + + $updated = $database->updateDocument($articles, 'article1', new Document(['author' => 'author2'])); + $this->assertSame('author2', $updated->getAttribute('author'), 'A write to an orphaned relationship stores the raw ID'); + $this->assertSame('author2', $database->getDocument($articles, 'article1')->getAttribute('author')); + + $updated = $database->updateDocument($articles, 'article1', new Document(['author' => null])); + $this->assertNull($updated->getAttribute('author')); + $this->assertNull($database->getDocument($articles, 'article1')->getAttribute('author')); + } + + public function testOrphanedRelationshipCreateDocument(): void + { + $database = $this->getDatabase(); + $articles = $this->orphanedArticles($database); + if ($articles === null) { + return; + } + + $created = $database->createDocument($articles, new Document([ + '$id' => 'article3', + '$permissions' => self::orphanedPermissions(), + 'name' => 'third', + ])); + $this->assertSame('third', $created->getAttribute('name'), 'createDocument must succeed past relationships to deleted collections'); + + $created = $database->createDocument($articles, new Document([ + '$id' => 'article4', + '$permissions' => self::orphanedPermissions(), + 'name' => 'fourth', + 'author' => 'author1', + 'comments' => ['comment2'], + 'tags' => ['tag2'], + 'topics' => ['topic2'], + ])); + $this->assertSame('author1', $created->getAttribute('author'), 'createDocument stores the raw ID of an orphaned relationship'); + $this->assertSame('author1', $database->getDocument($articles, 'article4')->getAttribute('author')); + } + + public function testOrphanedRelationshipGetDocument(): void + { + $database = $this->getDatabase(); + $articles = $this->orphanedArticles($database); + if ($articles === null) { + return; + } + + $article = $database->getDocument($articles, 'article1'); + $this->assertSame('first', $article->getAttribute('name'), 'getDocument must read a document whose relationships point at deleted collections'); + $this->assertSame('author1', $article->getAttribute('author'), 'An orphaned relationship keeps its stored value unpopulated'); + $this->assertSame('editor1', $article->getAttribute('editor'), 'An orphaned relationship keeps its stored value unpopulated'); + + $article = $database->getDocument($articles, 'article1', [Query::select(['name', 'author.*'])]); + $this->assertSame('first', $article->getAttribute('name'), 'getDocument selecting an orphaned relationship must read the document'); + $this->assertSame('author1', $article->getAttribute('author')); + } + + public function testOrphanedRelationshipFind(): void + { + $database = $this->getDatabase(); + $articles = $this->orphanedArticles($database); + if ($articles === null) { + return; + } + + $ids = static fn (array $documents): array => \array_map(static fn (mixed $document): string => $document instanceof Document ? $document->getId() : '', $documents); + + $found = $database->find($articles, [Query::orderAsc('$id')]); + $this->assertSame(['article1', 'article2'], $ids($found), 'find must list documents whose relationships point at deleted collections'); + $this->assertSame('author1', $found[0]->getAttribute('author')); + + $found = $database->find($articles, [Query::select(['name']), Query::orderAsc('$id')]); + $this->assertSame(['article1', 'article2'], $ids($found), 'find with a select must list documents whose relationships point at deleted collections'); + $this->assertSame('first', $found[0]->getAttribute('name')); + + $found = $database->find($articles, [Query::select(['name', 'author.*']), Query::orderAsc('$id')]); + $this->assertSame(['article1', 'article2'], $ids($found), 'find selecting an orphaned relationship must list its documents'); + $this->assertSame('author1', $found[0]->getAttribute('author'), 'A selected orphaned relationship returns its stored value'); + + $found = $database->find($articles, [Query::select(['*', 'author.name', 'comments.name', 'tags.name', 'topics.name']), Query::orderAsc('$id')]); + $this->assertSame(['article1', 'article2'], $ids($found), 'find selecting through orphaned relationships must list their documents'); + $this->assertSame('author1', $found[0]->getAttribute('author')); + + $found = $database->find($articles, [Query::equal('author', ['author1']), Query::orderAsc('$id')]); + $this->assertSame(['article1', 'article2'], $ids($found), 'find must filter on the stored value of an orphaned relationship'); + } + + public function testOrphanedRelationshipDeleteDocument(): void + { + $database = $this->getDatabase(); + $articles = $this->orphanedArticles($database); + if ($articles === null) { + return; + } + + $this->assertTrue($database->deleteDocument($articles, 'article1'), 'deleteDocument must ignore onDelete actions of relationships to deleted collections'); + $this->assertTrue($database->getDocument($articles, 'article1')->isEmpty()); + + $this->assertSame(1, $database->deleteDocuments($articles), 'deleteDocuments must ignore onDelete actions of relationships to deleted collections'); + $this->assertSame([], $database->find($articles)); + } + + public function testOrphanedRelationshipUpsertDocument(): void + { + $database = $this->getDatabase(); + $articles = $this->orphanedArticles($database); + if ($articles === null) { + return; + } + + if (! $database->getAdapter()->hasFeature(Feature\Upserts::class)) { + $this->expectNotToPerformAssertions(); + + return; + } + + $upserted = $database->upsertDocument($articles, new Document([ + '$id' => 'article2', + '$permissions' => self::orphanedPermissions(), + 'name' => 'upserted', + 'author' => 'author1', + 'editor' => 'editor1', + ])); + $this->assertSame('upserted', $upserted->getAttribute('name'), 'upsertDocument must update a document whose relationships point at deleted collections'); + $this->assertSame('upserted', $database->getDocument($articles, 'article2')->getAttribute('name')); + + $upserted = $database->upsertDocument($articles, new Document([ + '$id' => 'article5', + '$permissions' => self::orphanedPermissions(), + 'name' => 'fifth', + 'author' => 'author2', + ])); + $this->assertSame('fifth', $upserted->getAttribute('name'), 'upsertDocument must create a document whose relationships point at deleted collections'); + $this->assertSame('author2', $database->getDocument($articles, 'article5')->getAttribute('author')); + } + + public function testOrphanedRelationshipUpdateDocuments(): void + { + $database = $this->getDatabase(); + $articles = $this->orphanedArticles($database); + if ($articles === null) { + return; + } + + $count = $database->updateDocuments($articles, new Document(['name' => 'bulk'])); + $this->assertSame(2, $count, 'updateDocuments must update documents whose relationships point at deleted collections'); + foreach ($database->find($articles) as $document) { + $this->assertSame('bulk', $document->getAttribute('name')); + $this->assertSame('author1', $document->getAttribute('author')); + } + } + + /** + * A collection of articles relating to collections since deleted, as 7.x could leave them: one-way many-to-one + * relationships it holds (onDelete setNull and restrict), the child sides of one-way many-to-one relationships + * other collections held on it, a one-to-many and a many-to-many. Null when the adapter has no relationships. + */ + private function orphanedArticles(Database $database): ?string + { + if (! ($database->getAdapter()->hasFeature(Feature\Relationships::class))) { + $this->expectNotToPerformAssertions(); + + return null; + } + + $suffix = Id::unique(); + $articles = 'orphanArticles'.$suffix; + $authors = 'orphanAuthors'.$suffix; + $editors = 'orphanEditors'.$suffix; + $comments = 'orphanComments'.$suffix; + $likes = 'orphanLikes'.$suffix; + $tags = 'orphanTags'.$suffix; + $topics = 'orphanTopics'.$suffix; + $permissions = self::orphanedPermissions(); + + foreach ([$articles, $authors, $editors, $comments, $likes, $tags, $topics] as $collection) { + $database->createCollection(Collection::create( + id: $collection, + attributes: [Attribute::string(key: 'name', size: 64)], + permissions: $permissions, + )); + } + + $database->createRelationship($articles, Relationship::manyToOne(relatedCollection: $authors, key: 'author', twoWayKey: 'articles', onDelete: RelationshipDeleteAction::SetNull)); + $database->createRelationship($articles, Relationship::manyToOne(relatedCollection: $editors, key: 'editor', twoWayKey: 'articles', onDelete: RelationshipDeleteAction::Restrict)); + $database->createRelationship($comments, Relationship::manyToOne(relatedCollection: $articles, key: 'article', twoWayKey: 'comments', onDelete: RelationshipDeleteAction::SetNull)); + $database->createRelationship($likes, Relationship::manyToOne(relatedCollection: $articles, key: 'article', twoWayKey: 'likes', onDelete: RelationshipDeleteAction::Restrict)); + $database->createRelationship($articles, Relationship::oneToMany(relatedCollection: $tags, key: 'tags', twoWayKey: 'article', onDelete: RelationshipDeleteAction::Cascade)); + $database->createRelationship($articles, Relationship::manyToMany(relatedCollection: $topics, key: 'topics', twoWayKey: 'articles', onDelete: RelationshipDeleteAction::Restrict)); + + $database->createDocument($authors, new Document(['$id' => 'author1', '$permissions' => $permissions, 'name' => 'author'])); + $database->createDocument($editors, new Document(['$id' => 'editor1', '$permissions' => $permissions, 'name' => 'editor'])); + $database->createDocument($topics, new Document(['$id' => 'topic1', '$permissions' => $permissions, 'name' => 'topic'])); + foreach (['article1' => 'first', 'article2' => 'second'] as $id => $name) { + $database->createDocument($articles, new Document([ + '$id' => $id, + '$permissions' => $permissions, + 'name' => $name, + 'author' => 'author1', + 'editor' => 'editor1', + ])); + } + $database->createDocument($comments, new Document(['$id' => 'comment1', '$permissions' => $permissions, 'name' => 'comment', 'article' => 'article1'])); + $database->createDocument($likes, new Document(['$id' => 'like1', '$permissions' => $permissions, 'name' => 'like', 'article' => 'article1'])); + $database->createDocument($tags, new Document(['$id' => 'tag1', '$permissions' => $permissions, 'name' => 'tag', 'article' => 'article1'])); + $database->updateDocument($articles, 'article1', new Document(['topics' => ['topic1']])); + + foreach ([$authors, $editors, $comments, $likes, $tags, $topics] as $collection) { + $database->getAdapter()->deleteCollection($collection); + $database->getAuthorization()->skip(fn (): bool => $database->silent(fn (): bool => $database->deleteDocument(Database::METADATA, $collection))); + $database->purgeCachedCollection($collection); + } + + return $articles; + } + + /** + * @return list + */ + private static function orphanedPermissions(): array + { + return [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ]; + } +}