diff --git a/src/Database/Database.php b/src/Database/Database.php index e9bd5b1b6..cdb6badb6 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -2791,17 +2791,29 @@ public function updateDocument(string $collection, string $id, Document $documen } $time = DateTime::now(); - $document->setAttribute('$updatedAt', $time); - $old = Authorization::skip(fn () => $this->silent(fn () => $this->getDocument($collection, $id))); // Skip ensures user does not need read permission for this + $collection = $this->silent(fn () => $this->getCollection($collection)); $validator = new Authorization(self::PERMISSION_UPDATE); + $shouldUpdate = false; if ($collection->getId() !== self::METADATA) { $documentSecurity = $collection->getAttribute('documentSecurity', false); - if (!$validator->isValid([ + // Compare if the document has any changes + foreach ($document as $key=>$value) { + // Skip the nested documents as they will be checked later in recursions. + $oldAttributeValue = $old->getAttribute($key); + if ($oldAttributeValue instanceof Document) { + continue; + } + if ($oldAttributeValue !== $value) { + $shouldUpdate = true; + break; + } + } + if ($shouldUpdate && !$validator->isValid([ ...$collection->getUpdate(), ...($documentSecurity ? $old->getUpdate() : []) ])) { @@ -2809,6 +2821,10 @@ public function updateDocument(string $collection, string $id, Document $documen } } + if ($shouldUpdate) { + $document->setAttribute('$updatedAt', $time); + } + // Check if document was updated after the request timestamp $oldUpdatedAt = new \DateTime($old->getUpdatedAt()); if (!is_null($this->timestamp) && $oldUpdatedAt > $this->timestamp) { diff --git a/tests/Database/Base.php b/tests/Database/Base.php index 83f8b3027..1631b82c3 100644 --- a/tests/Database/Base.php +++ b/tests/Database/Base.php @@ -78,9 +78,10 @@ public function testCreateExistsDelete(): void public function testCreatedAtUpdatedAt(): void { $this->assertInstanceOf('Utopia\Database\Document', static::getDatabase()->createCollection('created_at')); - + static::getDatabase()->createAttribute('created_at', 'title', Database::VAR_STRING, 100, false); $document = static::getDatabase()->createDocument('created_at', new Document([ '$id' => ID::custom('uid123'), + '$permissions' => [ Permission::read(Role::any()), Permission::create(Role::any()), @@ -2728,6 +2729,26 @@ public function testWritePermissionsUpdateFailure(Document $document): Document Permission::update(Role::any()), Permission::delete(Role::any()), ], + 'string' => 'text📝', + 'integer' => 6, + 'bigint' => 8589934592, // 2^33 + 'float' => 5.55, + 'boolean' => true, + 'colors' => ['pink', 'green', 'blue'], + ])); + + return $document; + } + + + /** + * @depends testCreateDocument + */ + public function testNoChangeUpdateDocumentWithoutPermission(Document $document): Document + { + Authorization::setRole(Role::any()->toString()); + + $document = static::getDatabase()->createDocument('documents', new Document([ 'string' => 'text📝', 'integer' => 5, 'bigint' => 8589934592, // 2^33 @@ -2736,6 +2757,12 @@ public function testWritePermissionsUpdateFailure(Document $document): Document 'colors' => ['pink', 'green', 'blue'], ])); + Authorization::cleanRoles(); + $updatedDocument = static::getDatabase()->updateDocument('documents', $document->getId(), $document); + + // Document should not be updated as there is no change. It should also not throw any authorization exception without any permission because of no change. + $this->assertEquals($updatedDocument->getUpdatedAt(), $document->getUpdatedAt()); + return $document; } @@ -3485,6 +3512,7 @@ public function testCreatedAtUpdatedAtAssert(): void $document = static::getDatabase()->getDocument('created_at', 'uid123'); $this->assertEquals(true, !$document->isEmpty()); sleep(1); + $document->setAttribute('title', 'new title'); static::getDatabase()->updateDocument('created_at', 'uid123', $document); $document = static::getDatabase()->getDocument('created_at', 'uid123'); @@ -10597,7 +10625,6 @@ public function testCollectionPermissionsUpdateWorks(array $data): array public function testCollectionPermissionsUpdateThrowsException(array $data): void { [$collection, $document] = $data; - Authorization::cleanRoles(); Authorization::setRole(Role::any()->toString()); @@ -10605,7 +10632,7 @@ public function testCollectionPermissionsUpdateThrowsException(array $data): voi $document = static::getDatabase()->updateDocument( $collection->getId(), $document->getId(), - $document->setAttribute('test', 'ipsum') + $document->setAttribute('test', 'lorem') ); }