Cleanup on create failures

This commit is contained in:
Jake Barnby
2025-12-03 21:17:38 +13:00
parent c09c2848af
commit 8b4657ff8a
2 changed files with 120 additions and 9 deletions
@@ -119,36 +119,55 @@ class Create extends Action
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collectionKey = 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence();
$databaseKey = 'database_' . $database->getSequence();
$collectionAttributes = [];
$attributeDocuments = [];
foreach ($attributes as $attributeDef) {
$attrDoc = $this->buildAttributeDocument($database, $collection, $attributeDef, $dbForProject);
$collectionAttributes[] = $attrDoc['collection'];
$attributeDocuments[] = $attrDoc['document'];
try {
foreach ($attributes as $attributeDef) {
$attrDoc = $this->buildAttributeDocument($database, $collection, $attributeDef, $dbForProject);
$collectionAttributes[] = $attrDoc['collection'];
$attributeDocuments[] = $attrDoc['document'];
}
} catch (\Throwable $e) {
$dbForProject->deleteDocument($databaseKey, $collection->getId());
throw $e;
}
$collectionIndexes = [];
$indexDocuments = [];
foreach ($indexes as $indexDef) {
$idxDoc = $this->buildIndexDocument($database, $collection, $indexDef, $collectionAttributes);
$collectionIndexes[] = $idxDoc['collection'];
$indexDocuments[] = $idxDoc['document'];
try {
foreach ($indexes as $indexDef) {
$idxDoc = $this->buildIndexDocument($database, $collection, $indexDef, $collectionAttributes);
$collectionIndexes[] = $idxDoc['collection'];
$indexDocuments[] = $idxDoc['document'];
}
} catch (\Throwable $e) {
$dbForProject->deleteDocument($databaseKey, $collection->getId());
throw $e;
}
try {
$dbForProject->createCollection(
id: 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
id: $collectionKey,
attributes: $collectionAttributes,
indexes: $collectionIndexes,
permissions: $permissions,
documentSecurity: $documentSecurity
);
} catch (DuplicateException) {
$dbForProject->deleteDocument($databaseKey, $collection->getId());
throw new Exception($this->getDuplicateException());
} catch (IndexException $e) {
$dbForProject->deleteDocument($databaseKey, $collection->getId());
throw new Exception($this->getInvalidIndexException(), $e->getMessage());
} catch (LimitException) {
$dbForProject->deleteDocument($databaseKey, $collection->getId());
throw new Exception($this->getLimitException());
} catch (\Throwable $e) {
$dbForProject->deleteDocument($databaseKey, $collection->getId());
throw $e;
}
// Create documents in attributes and indexes collections
@@ -160,7 +179,11 @@ class Create extends Action
$dbForProject->createDocuments('indexes', $indexDocuments);
}
} catch (DuplicateException) {
$this->cleanup($dbForProject, $databaseKey, $collectionKey, $collection->getId());
throw new Exception($this->getDuplicateException());
} catch (\Throwable $e) {
$this->cleanup($dbForProject, $databaseKey, $collectionKey, $collection->getId());
throw $e;
}
$dbForProject->purgeCachedDocument('database_' . $database->getSequence(), $collection->getId());
@@ -362,4 +385,22 @@ class Create extends Action
'document' => $document,
];
}
/**
* Cleanup on failure: delete the collection document and the underlying DB collection
*/
protected function cleanup(Database $dbForProject, string $databaseKey, string $collectionKey, string $collectionId): void
{
try {
$dbForProject->deleteCollection($collectionKey);
} catch (\Throwable) {
// Ignore cleanup errors for collection deletion
}
try {
$dbForProject->deleteDocument($databaseKey, $collectionId);
} catch (\Throwable) {
// Ignore cleanup errors for document deletion
}
}
}
@@ -7110,6 +7110,76 @@ class DatabasesCustomServerTest extends Scope
]));
}
public function testCreateCollectionCleanupOnFailure(): void
{
// Create database
$database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'databaseId' => ID::unique(),
'name' => 'Test Cleanup',
]);
$this->assertEquals(201, $database['headers']['status-code']);
$databaseId = $database['body']['$id'];
$collectionId = ID::unique();
// Test: Create collection with invalid index referencing non-existent attribute (should fail)
$collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'collectionId' => $collectionId,
'name' => 'Should Fail',
'attributes' => [
[
'key' => 'title',
'type' => Database::VAR_STRING,
'size' => 256,
],
],
'indexes' => [
[
'key' => 'idx_invalid',
'type' => Database::INDEX_KEY,
'attributes' => ['nonexistent'],
],
],
]);
$this->assertEquals(400, $collection['headers']['status-code']);
// Verify collection was cleaned up - creating with same ID should succeed
$collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'collectionId' => $collectionId,
'name' => 'Should Succeed',
'attributes' => [
[
'key' => 'title',
'type' => Database::VAR_STRING,
'size' => 256,
],
],
]);
$this->assertEquals(201, $collection['headers']['status-code']);
// Cleanup
$this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
}
public function testCreateCollectionWithEnumAttribute(): void
{
// Create database