diff --git a/composer.json b/composer.json index 66b0fad6d..71c11264f 100755 --- a/composer.json +++ b/composer.json @@ -55,5 +55,11 @@ "ext-redis": "Needed to support Redis Cache Adapter", "ext-pdo": "Needed to support MariaDB, MySQL or SQLite Database Adapter", "mongodb/mongodb": "Needed to support MongoDB Database Adapter" + }, + "config": { + "allow-plugins": { + "php-http/discovery": false, + "tbachert/spi": false + } } } diff --git a/src/Database/Database.php b/src/Database/Database.php index 95a499df2..551e318e2 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -4084,6 +4084,17 @@ public function updateDocuments(string $collection, Document $updates, array $qu $this->silent(fn () => $this->updateDocumentRelationships($collection, $document, $newDocument)); $documents[] = $newDocument; } + + // Check if document was updated after the request timestamp + try { + $oldUpdatedAt = new \DateTime($document->getUpdatedAt()); + } catch (Exception $e) { + throw new DatabaseException($e->getMessage(), $e->getCode(), $e); + } + + if (!is_null($this->timestamp) && $oldUpdatedAt > $this->timestamp) { + throw new ConflictException('Document was updated after the request timestamp'); + } } $getResults = fn () => $this->adapter->updateDocuments( @@ -5236,6 +5247,17 @@ public function deleteDocuments(string $collection, array $queries = [], int $ba $document = $this->silent(fn () => $this->deleteDocumentRelationships($collection, $document)); } + // Check if document was updated after the request timestamp + try { + $oldUpdatedAt = new \DateTime($document->getUpdatedAt()); + } catch (Exception $e) { + throw new DatabaseException($e->getMessage(), $e->getCode(), $e); + } + + if (!\is_null($this->timestamp) && $oldUpdatedAt > $this->timestamp) { + throw new ConflictException('Document was updated after the request timestamp'); + } + $this->purgeRelatedDocuments($collection, $document->getId()); $this->purgeCachedDocument($collection->getId(), $document->getId()); } diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 943eb458e..602241299 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -15917,6 +15917,18 @@ public function testDeleteBulkDocuments(): void $docs = static::getDatabase()->find('bulk_delete'); $this->assertCount(5, $docs); + // TEST (FAIL): Can't delete documents in the past + $oneHourAgo = (new \DateTime())->sub(new \DateInterval('PT1H')); + + try { + $this->getDatabase()->withRequestTimestamp($oneHourAgo, function () { + return $this->getDatabase()->deleteDocuments('bulk_delete'); + }); + $this->fail('Failed to throw exception'); + } catch (ConflictException $e) { + $this->assertEquals('Document was updated after the request timestamp', $e->getMessage()); + } + // TEST (FAIL): Bulk delete all documents with invalid collection permission static::getDatabase()->updateCollection('bulk_delete', [], false); try { @@ -16650,6 +16662,20 @@ public function testUpdateDocuments(): void $this->assertEquals('text📝 updated all', $document->getAttribute('string')); } + // TEST: Can't delete documents in the past + $oneHourAgo = (new \DateTime())->sub(new \DateInterval('PT1H')); + + try { + $this->getDatabase()->withRequestTimestamp($oneHourAgo, function () use ($collection) { + return static::getDatabase()->updateDocuments($collection, new Document([ + 'string' => 'text📝 updated all', + ])); + }); + $this->fail('Failed to throw exception'); + } catch (ConflictException $e) { + $this->assertEquals('Document was updated after the request timestamp', $e->getMessage()); + } + // Check collection level permissions static::getDatabase()->updateCollection($collection, permissions: [ Permission::read(Role::user('asd')),