diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index c90f4d720..936e32fb1 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -422,6 +422,10 @@ public function updateDocuments(string $collection, Document $updates, array $do $attributes['_updatedAt'] = $updates->getUpdatedAt(); } + if (!empty($updates->getCreatedAt())) { + $attributes['_createdAt'] = $updates->getCreatedAt(); + } + if (!empty($updates->getPermissions())) { $attributes['_permissions'] = json_encode($updates->getPermissions()); } diff --git a/src/Database/Database.php b/src/Database/Database.php index cc1336625..ad7210cdf 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -3604,8 +3604,8 @@ public function createDocument(string $collection, Document $document): Document $document ->setAttribute('$id', empty($document->getId()) ? ID::unique() : $document->getId()) ->setAttribute('$collection', $collection->getId()) - ->setAttribute('$createdAt', empty($createdAt) || !$this->preserveDates ? $time : $createdAt) - ->setAttribute('$updatedAt', empty($updatedAt) || !$this->preserveDates ? $time : $updatedAt); + ->setAttribute('$createdAt', ($createdAt === null || !$this->preserveDates) ? $time : $createdAt) + ->setAttribute('$updatedAt', ($updatedAt === null || !$this->preserveDates) ? $time : $updatedAt); if ($this->adapter->getSharedTables()) { if ($this->adapter->getTenantPerDocument()) { @@ -3703,8 +3703,8 @@ public function createDocuments( $document ->setAttribute('$id', empty($document->getId()) ? ID::unique() : $document->getId()) ->setAttribute('$collection', $collection->getId()) - ->setAttribute('$createdAt', empty($createdAt) || !$this->preserveDates ? $time : $createdAt) - ->setAttribute('$updatedAt', empty($updatedAt) || !$this->preserveDates ? $time : $updatedAt); + ->setAttribute('$createdAt', ($createdAt === null || !$this->preserveDates) ? $time : $createdAt) + ->setAttribute('$updatedAt', ($updatedAt === null || !$this->preserveDates) ? $time : $updatedAt); if ($this->adapter->getSharedTables()) { if ($this->adapter->getTenantPerDocument()) { @@ -4119,10 +4119,11 @@ public function updateDocument(string $collection, string $id, Document $documen $skipPermissionsUpdate = ($originalPermissions === $currentPermissions); } + $createdAt = $document->getCreatedAt(); $document = \array_merge($old->getArrayCopy(), $document->getArrayCopy()); $document['$collection'] = $old->getAttribute('$collection'); // Make sure user doesn't switch collection ID - $document['$createdAt'] = $old->getCreatedAt(); // Make sure user doesn't switch createdAt + $document['$createdAt'] = ($createdAt === null || !$this->preserveDates) ? $old->getCreatedAt() : $createdAt; if ($this->adapter->getSharedTables()) { $document['$tenant'] = $old->getTenant(); // Make sure user doesn't switch tenant @@ -4251,7 +4252,7 @@ public function updateDocument(string $collection, string $id, Document $documen if ($shouldUpdate) { $updatedAt = $document->getUpdatedAt(); - $document->setAttribute('$updatedAt', empty($updatedAt) || !$this->preserveDates ? $time : $updatedAt); + $document->setAttribute('$updatedAt', ($updatedAt === null || !$this->preserveDates) ? $time : $updatedAt); } // Check if document was updated after the request timestamp @@ -4365,21 +4366,21 @@ public function updateDocuments( if (!empty($cursor) && $cursor->getCollection() !== $collection->getId()) { throw new DatabaseException("Cursor document must be from the same Collection."); } - unset($updates['$id']); - unset($updates['$createdAt']); unset($updates['$tenant']); - + if (($updates->getCreatedAt() === null || !$this->preserveDates)) { + unset($updates['$createdAt']); + } else { + $updates['$createdAt'] = $updates->getCreatedAt(); + } if ($this->adapter->getSharedTables()) { $updates['$tenant'] = $this->adapter->getTenant(); } - if (!$this->preserveDates) { - $updates['$updatedAt'] = DateTime::now(); - } + $updatedAt = $updates->getUpdatedAt(); + $updates['$updatedAt'] = ($updatedAt === null || !$this->preserveDates) ? DateTime::now() : $updatedAt; $updates = $this->encode($collection, $updates); - // Check new document structure $validator = new PartialStructure( $collection, @@ -4990,14 +4991,14 @@ public function createOrUpdateDocumentsWithIncrease( $document ->setAttribute('$id', empty($document->getId()) ? ID::unique() : $document->getId()) ->setAttribute('$collection', $collection->getId()) - ->setAttribute('$updatedAt', empty($updatedAt) || !$this->preserveDates ? $time : $updatedAt) + ->setAttribute('$updatedAt', ($updatedAt === null || !$this->preserveDates) ? $time : $updatedAt) ->removeAttribute('$sequence'); - if ($old->isEmpty()) { - $createdAt = $document->getCreatedAt(); - $document->setAttribute('$createdAt', empty($createdAt) || !$this->preserveDates ? $time : $createdAt); + $createdAt = $document->getCreatedAt(); + if ($createdAt === null || !$this->preserveDates) { + $document->setAttribute('$createdAt', $old->isEmpty() ? $time : $old->getCreatedAt()); } else { - $document['$createdAt'] = $old->getCreatedAt(); + $document->setAttribute('$createdAt', $createdAt); } // Force matching optional parameter sets @@ -6312,7 +6313,7 @@ public static function addFilter(string $name, callable $encode, callable $decod public function encode(Document $collection, Document $document): Document { $attributes = $collection->getAttribute('attributes', []); - + $internalDateAttributes = ['$createdAt','$updatedAt']; foreach ($this->getInternalAttributes() as $attribute) { $attributes[] = $attribute; } @@ -6324,6 +6325,11 @@ public function encode(Document $collection, Document $document): Document $filters = $attribute['filters'] ?? []; $value = $document->getAttribute($key); + if (in_array($key, $internalDateAttributes) && is_string($value) && empty($value)) { + $document->setAttribute($key, null); + continue; + } + if ($key === '$permissions') { if (empty($value)) { $document->setAttribute('$permissions', []); // set default value @@ -6356,7 +6362,6 @@ public function encode(Document $collection, Document $document): Document if (!$array) { $value = $value[0]; } - $document->setAttribute($key, $value); } diff --git a/src/Database/Validator/PartialStructure.php b/src/Database/Validator/PartialStructure.php index 1f52a451c..fd8f5a989 100644 --- a/src/Database/Validator/PartialStructure.php +++ b/src/Database/Validator/PartialStructure.php @@ -36,7 +36,19 @@ public function isValid($document): bool $name = $attribute['$id'] ?? ''; $keys[$name] = $attribute; } + /** + * @var array $requiredAttributes + */ + $requiredAttributes = []; + foreach ($this->attributes as $attribute) { + if ($attribute['required'] === true && $document->offsetExists($attribute['$id'])) { + $requiredAttributes[] = $attribute; + } + } + if (!$this->checkForAllRequiredValues($structure, $requiredAttributes, $keys)) { + return false; + } if (!$this->checkForUnknownAttributes($structure, $keys)) { return false; } diff --git a/src/Database/Validator/Structure.php b/src/Database/Validator/Structure.php index 478557697..dbf777c2b 100644 --- a/src/Database/Validator/Structure.php +++ b/src/Database/Validator/Structure.php @@ -71,7 +71,7 @@ class Structure extends Validator '$id' => '$createdAt', 'type' => Database::VAR_DATETIME, 'size' => 0, - 'required' => false, + 'required' => true, 'signed' => false, 'array' => false, 'filters' => [], @@ -80,7 +80,7 @@ class Structure extends Validator '$id' => '$updatedAt', 'type' => Database::VAR_DATETIME, 'size' => 0, - 'required' => false, + 'required' => true, 'signed' => false, 'array' => false, 'filters' => [], diff --git a/tests/e2e/Adapter/Scopes/DocumentTests.php b/tests/e2e/Adapter/Scopes/DocumentTests.php index 8a5133aef..1d5208df9 100644 --- a/tests/e2e/Adapter/Scopes/DocumentTests.php +++ b/tests/e2e/Adapter/Scopes/DocumentTests.php @@ -4634,4 +4634,604 @@ public function testEmptyOperatorValues(): void $this->assertEquals('Invalid query: Contains queries require at least one value.', $e->getMessage()); } } + + public function testDateTimeDocument(): void + { + /** + * @var Database $database + */ + $database = static::getDatabase(); + $collection = 'create_modify_dates'; + $database->createCollection($collection); + $this->assertEquals(true, $database->createAttribute($collection, 'string', Database::VAR_STRING, 128, false)); + $this->assertEquals(true, $database->createAttribute($collection, 'datetime', Database::VAR_DATETIME, 0, false, null, true, false, null, [], ['datetime'])); + + $date = '2000-01-01T10:00:00.000+00:00'; + // test - default behaviour of external datetime attribute not changed + $doc = $database->createDocument($collection, new Document([ + '$id' => 'doc1', + '$permissions' => [Permission::read(Role::any()),Permission::write(Role::any()),Permission::update(Role::any())], + 'datetime' => '' + ])); + $this->assertNotEmpty($doc->getAttribute('datetime')); + $this->assertNotEmpty($doc->getAttribute('$createdAt')); + $this->assertNotEmpty($doc->getAttribute('$updatedAt')); + + $doc = $database->getDocument($collection, 'doc1'); + $this->assertNotEmpty($doc->getAttribute('datetime')); + $this->assertNotEmpty($doc->getAttribute('$createdAt')); + $this->assertNotEmpty($doc->getAttribute('$updatedAt')); + + $database->setPreserveDates(true); + // test - modifying $createdAt and $updatedAt + $doc = $database->createDocument($collection, new Document([ + '$id' => 'doc2', + '$permissions' => [Permission::read(Role::any()),Permission::write(Role::any()),Permission::update(Role::any())], + '$createdAt' => $date + ])); + + $this->assertEquals($doc->getAttribute('$createdAt'), $date); + $this->assertNotEmpty($doc->getAttribute('$updatedAt')); + $this->assertNotEquals($doc->getAttribute('$updatedAt'), $date); + + $doc = $database->getDocument($collection, 'doc2'); + + $this->assertEquals($doc->getAttribute('$createdAt'), $date); + $this->assertNotEmpty($doc->getAttribute('$updatedAt')); + $this->assertNotEquals($doc->getAttribute('$updatedAt'), $date); + + $database->setPreserveDates(false); + $database->deleteCollection($collection); + } + + public function testSingleDocumentDateOperations(): void + { + /** @var Database $database */ + $database = static::getDatabase(); + $collection = 'normal_date_operations'; + $database->createCollection($collection); + $this->assertEquals(true, $database->createAttribute($collection, 'string', Database::VAR_STRING, 128, false)); + + $database->setPreserveDates(true); + + $createDate = '2000-01-01T10:00:00.000+00:00'; + $updateDate = '2000-02-01T15:30:00.000+00:00'; + $date1 = '2000-01-01T10:00:00.000+00:00'; + $date2 = '2000-02-01T15:30:00.000+00:00'; + $date3 = '2000-03-01T20:45:00.000+00:00'; + // Test 1: Create with custom createdAt, then update with custom updatedAt + $doc = $database->createDocument($collection, new Document([ + '$id' => 'doc1', + '$permissions' => [Permission::read(Role::any()), Permission::write(Role::any()),Permission::update(Role::any())], + 'string' => 'initial', + '$createdAt' => $createDate + ])); + + $this->assertEquals($createDate, $doc->getAttribute('$createdAt')); + $this->assertNotEquals($createDate, $doc->getAttribute('$updatedAt')); + + // Update with custom updatedAt + $doc->setAttribute('string', 'updated'); + $doc->setAttribute('$updatedAt', $updateDate); + $updatedDoc = $database->updateDocument($collection, 'doc1', $doc); + + $this->assertEquals($createDate, $updatedDoc->getAttribute('$createdAt')); + $this->assertEquals($updateDate, $updatedDoc->getAttribute('$updatedAt')); + + // Test 2: Create with both custom dates + $doc2 = $database->createDocument($collection, new Document([ + '$id' => 'doc2', + '$permissions' => [Permission::read(Role::any()), Permission::write(Role::any()),Permission::update(Role::any())], + 'string' => 'both_dates', + '$createdAt' => $createDate, + '$updatedAt' => $updateDate + ])); + + $this->assertEquals($createDate, $doc2->getAttribute('$createdAt')); + $this->assertEquals($updateDate, $doc2->getAttribute('$updatedAt')); + + // Test 3: Create without dates, then update with custom dates + $doc3 = $database->createDocument($collection, new Document([ + '$id' => 'doc3', + '$permissions' => [Permission::read(Role::any()), Permission::write(Role::any()),Permission::update(Role::any())], + 'string' => 'no_dates' + ])); + + + $doc3->setAttribute('string', 'updated_no_dates'); + $doc3->setAttribute('$createdAt', $createDate); + $doc3->setAttribute('$updatedAt', $updateDate); + $updatedDoc3 = $database->updateDocument($collection, 'doc3', $doc3); + + $this->assertEquals($createDate, $updatedDoc3->getAttribute('$createdAt')); + $this->assertEquals($updateDate, $updatedDoc3->getAttribute('$updatedAt')); + + // Test 4: Update only createdAt + $doc4 = $database->createDocument($collection, new Document([ + '$id' => 'doc4', + '$permissions' => [Permission::read(Role::any()), Permission::write(Role::any()),Permission::update(Role::any())], + 'string' => 'initial' + ])); + + $originalCreatedAt4 = $doc4->getAttribute('$createdAt'); + $originalUpdatedAt4 = $doc4->getAttribute('$updatedAt'); + + $doc4->setAttribute('$updatedAt', null); + $doc4->setAttribute('$createdAt', null); + $updatedDoc4 = $database->updateDocument($collection, 'doc4', document: $doc4); + + $this->assertEquals($originalCreatedAt4, $updatedDoc4->getAttribute('$createdAt')); + $this->assertNotEquals($originalUpdatedAt4, $updatedDoc4->getAttribute('$updatedAt')); + + // Test 5: Update only updatedAt + $updatedDoc4->setAttribute('$updatedAt', $updateDate); + $updatedDoc4->setAttribute('$createdAt', $createDate); + $finalDoc4 = $database->updateDocument($collection, 'doc4', $updatedDoc4); + + $this->assertEquals($createDate, $finalDoc4->getAttribute('$createdAt')); + $this->assertEquals($updateDate, $finalDoc4->getAttribute('$updatedAt')); + + // Test 6: Create with updatedAt, update with createdAt + $doc5 = $database->createDocument($collection, new Document([ + '$id' => 'doc5', + '$permissions' => [Permission::read(Role::any()), Permission::write(Role::any()),Permission::update(Role::any())], + 'string' => 'doc5', + '$updatedAt' => $date2 + ])); + + $this->assertNotEquals($date2, $doc5->getAttribute('$createdAt')); + $this->assertEquals($date2, $doc5->getAttribute('$updatedAt')); + + $doc5->setAttribute('string', 'doc5_updated'); + $doc5->setAttribute('$createdAt', $date1); + $updatedDoc5 = $database->updateDocument($collection, 'doc5', $doc5); + + $this->assertEquals($date1, $updatedDoc5->getAttribute('$createdAt')); + $this->assertEquals($date2, $updatedDoc5->getAttribute('$updatedAt')); + + // Test 7: Create with both dates, update with different dates + $doc6 = $database->createDocument($collection, new Document([ + '$id' => 'doc6', + '$permissions' => [Permission::read(Role::any()), Permission::write(Role::any()),Permission::update(Role::any())], + 'string' => 'doc6', + '$createdAt' => $date1, + '$updatedAt' => $date2 + ])); + + $this->assertEquals($date1, $doc6->getAttribute('$createdAt')); + $this->assertEquals($date2, $doc6->getAttribute('$updatedAt')); + + $doc6->setAttribute('string', 'doc6_updated'); + $doc6->setAttribute('$createdAt', $date3); + $doc6->setAttribute('$updatedAt', $date3); + $updatedDoc6 = $database->updateDocument($collection, 'doc6', $doc6); + + $this->assertEquals($date3, $updatedDoc6->getAttribute('$createdAt')); + $this->assertEquals($date3, $updatedDoc6->getAttribute('$updatedAt')); + + // Test 8: Preserve dates disabled + $database->setPreserveDates(false); + + $customDate = '2000-01-01T10:00:00.000+00:00'; + + $doc7 = $database->createDocument($collection, new Document([ + '$id' => 'doc7', + '$permissions' => [Permission::read(Role::any()), Permission::write(Role::any()),Permission::update(Role::any())], + 'string' => 'doc7', + '$createdAt' => $customDate, + '$updatedAt' => $customDate + ])); + + $this->assertNotEquals($customDate, $doc7->getAttribute('$createdAt')); + $this->assertNotEquals($customDate, $doc7->getAttribute('$updatedAt')); + + // Update with custom dates should also be ignored + $doc7->setAttribute('string', 'updated'); + $doc7->setAttribute('$createdAt', $customDate); + $doc7->setAttribute('$updatedAt', $customDate); + $updatedDoc7 = $database->updateDocument($collection, 'doc7', $doc7); + + $this->assertNotEquals($customDate, $updatedDoc7->getAttribute('$createdAt')); + $this->assertNotEquals($customDate, $updatedDoc7->getAttribute('$updatedAt')); + + $database->setPreserveDates(false); + $database->deleteCollection($collection); + } + + public function testBulkDocumentDateOperations(): void + { + /** @var Database $database */ + $database = static::getDatabase(); + $collection = 'bulk_date_operations'; + $database->createCollection($collection); + $this->assertEquals(true, $database->createAttribute($collection, 'string', Database::VAR_STRING, 128, false)); + + $database->setPreserveDates(true); + + $createDate = '2000-01-01T10:00:00.000+00:00'; + $updateDate = '2000-02-01T15:30:00.000+00:00'; + $permissions = [Permission::read(Role::any()), Permission::write(Role::any()),Permission::update(Role::any())]; + + // Test 1: Bulk create with different date configurations + $documents = [ + new Document([ + '$id' => 'doc1', + '$permissions' => $permissions, + 'string' => 'doc1', + '$createdAt' => $createDate + ]), + new Document([ + '$id' => 'doc2', + '$permissions' => $permissions, + 'string' => 'doc2', + '$updatedAt' => $updateDate + ]), + new Document([ + '$id' => 'doc3', + '$permissions' => $permissions, + 'string' => 'doc3', + '$createdAt' => $createDate, + '$updatedAt' => $updateDate + ]), + new Document([ + '$id' => 'doc4', + '$permissions' => $permissions, + 'string' => 'doc4' + ]), + new Document([ + '$id' => 'doc5', + '$permissions' => $permissions, + 'string' => 'doc5', + '$createdAt' => null + ]), + new Document([ + '$id' => 'doc6', + '$permissions' => $permissions, + 'string' => 'doc6', + '$updatedAt' => null + ]) + ]; + + $database->createDocuments($collection, $documents); + + // Verify initial state + foreach (['doc1', 'doc3'] as $id) { + $doc = $database->getDocument($collection, $id); + $this->assertEquals($createDate, $doc->getAttribute('$createdAt'), "createdAt mismatch for $id"); + } + + foreach (['doc2', 'doc3'] as $id) { + $doc = $database->getDocument($collection, $id); + $this->assertEquals($updateDate, $doc->getAttribute('$updatedAt'), "updatedAt mismatch for $id"); + } + + foreach (['doc4', 'doc5', 'doc6'] as $id) { + $doc = $database->getDocument($collection, $id); + $this->assertNotEmpty($doc->getAttribute('$createdAt'), "createdAt missing for $id"); + $this->assertNotEmpty($doc->getAttribute('$updatedAt'), "updatedAt missing for $id"); + } + + // Test 2: Bulk update with custom dates + $updateDoc = new Document([ + 'string' => 'updated', + '$createdAt' => $createDate, + '$updatedAt' => $updateDate + ]); + $ids = []; + foreach ($documents as $doc) { + $ids[] = $doc->getId(); + } + $count = $database->updateDocuments($collection, $updateDoc, [ + Query::equal('$id', $ids) + ]); + $this->assertEquals(6, $count); + + foreach (['doc1', 'doc3'] as $id) { + $doc = $database->getDocument($collection, $id); + $this->assertEquals($createDate, $doc->getAttribute('$createdAt'), "createdAt mismatch for $id"); + $this->assertEquals($updateDate, $doc->getAttribute('$updatedAt'), "updatedAt mismatch for $id"); + $this->assertEquals('updated', $doc->getAttribute('string'), "string mismatch for $id"); + } + + foreach (['doc2', 'doc4','doc5','doc6'] as $id) { + $doc = $database->getDocument($collection, $id); + $this->assertEquals($updateDate, $doc->getAttribute('$updatedAt'), "updatedAt mismatch for $id"); + $this->assertEquals('updated', $doc->getAttribute('string'), "string mismatch for $id"); + } + + // Test 3: Bulk update with preserve dates disabled + $database->setPreserveDates(false); + + $customDate = 'should be ignored anyways so no error'; + $updateDocDisabled = new Document([ + 'string' => 'disabled_update', + '$createdAt' => $customDate, + '$updatedAt' => $customDate + ]); + + $countDisabled = $database->updateDocuments($collection, $updateDocDisabled); + $this->assertEquals(6, $countDisabled); + + // Test 4: Bulk update with preserve dates re-enabled + $database->setPreserveDates(true); + + $newDate = '2000-03-01T20:45:00.000+00:00'; + $updateDocEnabled = new Document([ + 'string' => 'enabled_update', + '$createdAt' => $newDate, + '$updatedAt' => $newDate + ]); + + $countEnabled = $database->updateDocuments($collection, $updateDocEnabled); + $this->assertEquals(6, $countEnabled); + + $database->setPreserveDates(false); + $database->deleteCollection($collection); + } + + public function testUpsertDateOperations(): void + { + /** @var Database $database */ + $database = static::getDatabase(); + + if (!$database->getAdapter()->getSupportForUpserts()) { + return; + } + + $collection = 'upsert_date_operations'; + $database->createCollection($collection); + $this->assertEquals(true, $database->createAttribute($collection, 'string', Database::VAR_STRING, 128, false)); + + $database->setPreserveDates(true); + + $createDate = '2000-01-01T10:00:00.000+00:00'; + $updateDate = '2000-02-01T15:30:00.000+00:00'; + $date1 = '2000-01-01T10:00:00.000+00:00'; + $date2 = '2000-02-01T15:30:00.000+00:00'; + $date3 = '2000-03-01T20:45:00.000+00:00'; + $permissions = [Permission::read(Role::any()), Permission::write(Role::any()),Permission::update(Role::any())]; + + // Test 1: Upsert new document with custom createdAt + $upsertResults = []; + $database->createOrUpdateDocuments($collection, [ + new Document([ + '$id' => 'upsert1', + '$permissions' => $permissions, + 'string' => 'upsert1_initial', + '$createdAt' => $createDate + ]) + ], onNext: function ($doc) use (&$upsertResults) { + $upsertResults[] = $doc; + }); + $upsertDoc1 = $upsertResults[0]; + + $this->assertEquals($createDate, $upsertDoc1->getAttribute('$createdAt')); + $this->assertNotEquals($createDate, $upsertDoc1->getAttribute('$updatedAt')); + + // Test 2: Upsert existing document with custom updatedAt + $upsertDoc1->setAttribute('string', 'upsert1_updated'); + $upsertDoc1->setAttribute('$updatedAt', $updateDate); + $updatedUpsertResults = []; + $database->createOrUpdateDocuments($collection, [$upsertDoc1], onNext: function ($doc) use (&$updatedUpsertResults) { + $updatedUpsertResults[] = $doc; + }); + $updatedUpsertDoc1 = $updatedUpsertResults[0]; + + $this->assertEquals($createDate, $updatedUpsertDoc1->getAttribute('$createdAt')); + $this->assertEquals($updateDate, $updatedUpsertDoc1->getAttribute('$updatedAt')); + + // Test 3: Upsert new document with both custom dates + $upsertResults2 = []; + $database->createOrUpdateDocuments($collection, [ + new Document([ + '$id' => 'upsert2', + '$permissions' => $permissions, + 'string' => 'upsert2_both_dates', + '$createdAt' => $createDate, + '$updatedAt' => $updateDate + ]) + ], onNext: function ($doc) use (&$upsertResults2) { + $upsertResults2[] = $doc; + }); + $upsertDoc2 = $upsertResults2[0]; + + $this->assertEquals($createDate, $upsertDoc2->getAttribute('$createdAt')); + $this->assertEquals($updateDate, $upsertDoc2->getAttribute('$updatedAt')); + + // Test 4: Upsert existing document with different dates + $upsertDoc2->setAttribute('string', 'upsert2_updated'); + $upsertDoc2->setAttribute('$createdAt', $date3); + $upsertDoc2->setAttribute('$updatedAt', $date3); + $updatedUpsertResults2 = []; + $database->createOrUpdateDocuments($collection, [$upsertDoc2], onNext: function ($doc) use (&$updatedUpsertResults2) { + $updatedUpsertResults2[] = $doc; + }); + $updatedUpsertDoc2 = $updatedUpsertResults2[0]; + + $this->assertEquals($date3, $updatedUpsertDoc2->getAttribute('$createdAt')); + $this->assertEquals($date3, $updatedUpsertDoc2->getAttribute('$updatedAt')); + + // Test 5: Upsert with preserve dates disabled + $database->setPreserveDates(false); + + $customDate = '2000-01-01T10:00:00.000+00:00'; + $upsertResults3 = []; + $database->createOrUpdateDocuments($collection, [ + new Document([ + '$id' => 'upsert3', + '$permissions' => $permissions, + 'string' => 'upsert3_disabled', + '$createdAt' => $customDate, + '$updatedAt' => $customDate + ]) + ], onNext: function ($doc) use (&$upsertResults3) { + $upsertResults3[] = $doc; + }); + $upsertDoc3 = $upsertResults3[0]; + + $this->assertNotEquals($customDate, $upsertDoc3->getAttribute('$createdAt')); + $this->assertNotEquals($customDate, $upsertDoc3->getAttribute('$updatedAt')); + + // Update with custom dates should also be ignored + $upsertDoc3->setAttribute('string', 'upsert3_updated'); + $upsertDoc3->setAttribute('$createdAt', $customDate); + $upsertDoc3->setAttribute('$updatedAt', $customDate); + $updatedUpsertResults3 = []; + $database->createOrUpdateDocuments($collection, [$upsertDoc3], onNext: function ($doc) use (&$updatedUpsertResults3) { + $updatedUpsertResults3[] = $doc; + }); + $updatedUpsertDoc3 = $updatedUpsertResults3[0]; + + $this->assertNotEquals($customDate, $updatedUpsertDoc3->getAttribute('$createdAt')); + $this->assertNotEquals($customDate, $updatedUpsertDoc3->getAttribute('$updatedAt')); + + // Test 6: Bulk upsert operations with custom dates + $database->setPreserveDates(true); + + // Test 7: Bulk upsert with different date configurations + $upsertDocuments = [ + new Document([ + '$id' => 'bulk_upsert1', + '$permissions' => $permissions, + 'string' => 'bulk_upsert1_initial', + '$createdAt' => $createDate + ]), + new Document([ + '$id' => 'bulk_upsert2', + '$permissions' => $permissions, + 'string' => 'bulk_upsert2_initial', + '$updatedAt' => $updateDate + ]), + new Document([ + '$id' => 'bulk_upsert3', + '$permissions' => $permissions, + 'string' => 'bulk_upsert3_initial', + '$createdAt' => $createDate, + '$updatedAt' => $updateDate + ]), + new Document([ + '$id' => 'bulk_upsert4', + '$permissions' => $permissions, + 'string' => 'bulk_upsert4_initial' + ]) + ]; + + $bulkUpsertResults = []; + $database->createOrUpdateDocuments($collection, $upsertDocuments, onNext: function ($doc) use (&$bulkUpsertResults) { + $bulkUpsertResults[] = $doc; + }); + + // Test 8: Verify initial bulk upsert state + foreach (['bulk_upsert1', 'bulk_upsert3'] as $id) { + $doc = $database->getDocument($collection, $id); + $this->assertEquals($createDate, $doc->getAttribute('$createdAt'), "createdAt mismatch for $id"); + } + + foreach (['bulk_upsert2', 'bulk_upsert3'] as $id) { + $doc = $database->getDocument($collection, $id); + $this->assertEquals($updateDate, $doc->getAttribute('$updatedAt'), "updatedAt mismatch for $id"); + } + + foreach (['bulk_upsert4'] as $id) { + $doc = $database->getDocument($collection, $id); + $this->assertNotEmpty($doc->getAttribute('$createdAt'), "createdAt missing for $id"); + $this->assertNotEmpty($doc->getAttribute('$updatedAt'), "updatedAt missing for $id"); + } + + // Test 9: Bulk upsert update with custom dates using updateDocuments + $newDate = '2000-04-01T12:00:00.000+00:00'; + $updateUpsertDoc = new Document([ + 'string' => 'bulk_upsert_updated', + '$createdAt' => $newDate, + '$updatedAt' => $newDate + ]); + + $upsertIds = []; + foreach ($upsertDocuments as $doc) { + $upsertIds[] = $doc->getId(); + } + + $database->updateDocuments($collection, $updateUpsertDoc, [ + Query::equal('$id', $upsertIds) + ]); + + foreach ($upsertIds as $id) { + $doc = $database->getDocument($collection, $id); + $this->assertEquals($newDate, $doc->getAttribute('$createdAt'), "createdAt mismatch for $id"); + $this->assertEquals($newDate, $doc->getAttribute('$updatedAt'), "updatedAt mismatch for $id"); + $this->assertEquals('bulk_upsert_updated', $doc->getAttribute('string'), "string mismatch for $id"); + } + + // Test 10: checking by passing null to each + $updateUpsertDoc = new Document([ + 'string' => 'bulk_upsert_updated', + '$createdAt' => null, + '$updatedAt' => null + ]); + + $upsertIds = []; + foreach ($upsertDocuments as $doc) { + $upsertIds[] = $doc->getId(); + } + + $database->updateDocuments($collection, $updateUpsertDoc, [ + Query::equal('$id', $upsertIds) + ]); + + foreach ($upsertIds as $id) { + $doc = $database->getDocument($collection, $id); + $this->assertNotEmpty($doc->getAttribute('$createdAt'), "createdAt mismatch for $id"); + $this->assertNotEmpty($doc->getAttribute('$updatedAt'), "updatedAt mismatch for $id"); + } + + // Test 11: Bulk upsert operations with createOrUpdateDocuments + $upsertUpdateDocuments = []; + foreach ($upsertDocuments as $doc) { + $updatedDoc = clone $doc; + $updatedDoc->setAttribute('string', 'bulk_upsert_updated_via_upsert'); + $updatedDoc->setAttribute('$createdAt', $newDate); + $updatedDoc->setAttribute('$updatedAt', $newDate); + $upsertUpdateDocuments[] = $updatedDoc; + } + + $upsertUpdateResults = []; + $countUpsertUpdate = $database->createOrUpdateDocuments($collection, $upsertUpdateDocuments, onNext: function ($doc) use (&$upsertUpdateResults) { + $upsertUpdateResults[] = $doc; + }); + $this->assertEquals(4, $countUpsertUpdate); + + foreach ($upsertUpdateResults as $doc) { + $this->assertEquals($newDate, $doc->getAttribute('$createdAt'), "createdAt mismatch for upsert update"); + $this->assertEquals($newDate, $doc->getAttribute('$updatedAt'), "updatedAt mismatch for upsert update"); + $this->assertEquals('bulk_upsert_updated_via_upsert', $doc->getAttribute('string'), "string mismatch for upsert update"); + } + + // Test 12: Bulk upsert with preserve dates disabled + $database->setPreserveDates(false); + + $customDate = 'should be ignored anyways so no error'; + $upsertDisabledDocuments = []; + foreach ($upsertDocuments as $doc) { + $disabledDoc = clone $doc; + $disabledDoc->setAttribute('string', 'bulk_upsert_disabled'); + $disabledDoc->setAttribute('$createdAt', $customDate); + $disabledDoc->setAttribute('$updatedAt', $customDate); + $upsertDisabledDocuments[] = $disabledDoc; + } + + $upsertDisabledResults = []; + $countUpsertDisabled = $database->createOrUpdateDocuments($collection, $upsertDisabledDocuments, onNext: function ($doc) use (&$upsertDisabledResults) { + $upsertDisabledResults[] = $doc; + }); + $this->assertEquals(4, $countUpsertDisabled); + + foreach ($upsertDisabledResults as $doc) { + $this->assertNotEquals($customDate, $doc->getAttribute('$createdAt'), "createdAt should not be custom date when disabled"); + $this->assertNotEquals($customDate, $doc->getAttribute('$updatedAt'), "updatedAt should not be custom date when disabled"); + $this->assertEquals('bulk_upsert_disabled', $doc->getAttribute('string'), "string mismatch for disabled upsert"); + } + + $database->setPreserveDates(false); + $database->deleteCollection($collection); + } } diff --git a/tests/e2e/Adapter/Scopes/GeneralTests.php b/tests/e2e/Adapter/Scopes/GeneralTests.php index a5fc8f200..795f4d096 100644 --- a/tests/e2e/Adapter/Scopes/GeneralTests.php +++ b/tests/e2e/Adapter/Scopes/GeneralTests.php @@ -119,7 +119,38 @@ public function testPreserveDatesUpdate(): void '$permissions' => [], 'attr1' => 'value3', ])); + // updating with empty dates + try { + $doc1->setAttribute('$updatedAt', ''); + $doc1 = $database->updateDocument('preserve_update_dates', 'doc1', $doc1); + $this->fail('Failed to throw structure exception'); + + } catch (Exception $e) { + $this->assertInstanceOf(StructureException::class, $e); + $this->assertEquals('Invalid document structure: Missing required attribute "$updatedAt"', $e->getMessage()); + } + try { + $this->getDatabase()->updateDocuments( + 'preserve_update_dates', + new Document([ + '$updatedAt' => '' + ]), + [ + Query::equal('$id', [ + $doc2->getId(), + $doc3->getId() + ]) + ] + ); + $this->fail('Failed to throw structure exception'); + + } catch (Exception $e) { + $this->assertInstanceOf(StructureException::class, $e); + $this->assertEquals('Invalid document structure: Missing required attribute "$updatedAt"', $e->getMessage()); + } + + // non empty dates $newDate = '2000-01-01T10:00:00.000+00:00'; $doc1->setAttribute('$updatedAt', $newDate); @@ -166,6 +197,43 @@ public function testPreserveDatesCreate(): void $database->createAttribute('preserve_create_dates', 'attr1', Database::VAR_STRING, 10, false); + // empty string for $createdAt should throw Structure exception + try { + $date = ''; + $database->createDocument('preserve_create_dates', new Document([ + '$id' => 'doc1', + '$permissions' => [], + 'attr1' => 'value1', + '$createdAt' => $date + ])); + $this->fail('Failed to throw structure exception'); + } catch (Exception $e) { + $this->assertInstanceOf(StructureException::class, $e); + $this->assertEquals('Invalid document structure: Missing required attribute "$createdAt"', $e->getMessage()); + } + + try { + $database->createDocuments('preserve_create_dates', [ + new Document([ + '$id' => 'doc2', + '$permissions' => [], + 'attr1' => 'value2', + '$createdAt' => $date + ]), + new Document([ + '$id' => 'doc3', + '$permissions' => [], + 'attr1' => 'value3', + '$createdAt' => $date + ]), + ], batchSize: 2); + $this->fail('Failed to throw structure exception'); + } catch (Exception $e) { + $this->assertInstanceOf(StructureException::class, $e); + $this->assertEquals('Invalid document structure: Missing required attribute "$createdAt"', $e->getMessage()); + } + + // non empty date $date = '2000-01-01T10:00:00.000+00:00'; $database->createDocument('preserve_create_dates', new Document([ @@ -186,16 +254,33 @@ public function testPreserveDatesCreate(): void '$id' => 'doc3', '$permissions' => [], 'attr1' => 'value3', - '$createdAt' => $date + '$createdAt' => $date, + ]), + new Document([ + '$id' => 'doc4', + '$permissions' => [], + 'attr1' => 'value3', + '$createdAt' => null, + ]), + new Document([ + '$id' => 'doc5', + '$permissions' => [], + 'attr1' => 'value3', ]), ], batchSize: 2); $doc1 = $database->getDocument('preserve_create_dates', 'doc1'); $doc2 = $database->getDocument('preserve_create_dates', 'doc2'); $doc3 = $database->getDocument('preserve_create_dates', 'doc3'); + $doc4 = $database->getDocument('preserve_create_dates', 'doc4'); + $doc5 = $database->getDocument('preserve_create_dates', 'doc5'); $this->assertEquals($date, $doc1->getAttribute('$createdAt')); $this->assertEquals($date, $doc2->getAttribute('$createdAt')); $this->assertEquals($date, $doc3->getAttribute('$createdAt')); + $this->assertNotEmpty($date, $doc4->getAttribute('$createdAt')); + $this->assertNotEquals($date, $doc4->getAttribute('$createdAt')); + $this->assertNotEmpty($date, $doc5->getAttribute('$createdAt')); + $this->assertNotEquals($date, $doc5->getAttribute('$createdAt')); $database->deleteCollection('preserve_create_dates'); diff --git a/tests/unit/Validator/StructureTest.php b/tests/unit/Validator/StructureTest.php index a9f641038..603256bfa 100644 --- a/tests/unit/Validator/StructureTest.php +++ b/tests/unit/Validator/StructureTest.php @@ -153,6 +153,8 @@ public function testCollection(): void 'published' => true, 'tags' => ['dog', 'cat', 'mouse'], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); $this->assertEquals('Invalid document structure: Collection not found', $validator->getDescription()); @@ -170,6 +172,8 @@ public function testRequiredKeys(): void 'published' => true, 'tags' => ['dog', 'cat', 'mouse'], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); $this->assertEquals('Invalid document structure: Missing required attribute "title"', $validator->getDescription()); @@ -188,6 +192,8 @@ public function testNullValues(): void 'published' => true, 'tags' => ['dog', 'cat', 'mouse'], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); $this->assertEquals(true, $validator->isValid(new Document([ @@ -199,6 +205,8 @@ public function testNullValues(): void 'published' => true, 'tags' => ['dog', null, 'mouse'], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); } @@ -216,6 +224,8 @@ public function testUnknownKeys(): void 'published' => true, 'tags' => ['dog', 'cat', 'mouse'], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); $this->assertEquals('Invalid document structure: Unknown attribute: "titlex"', $validator->getDescription()); @@ -234,6 +244,8 @@ public function testIntegerAsString(): void 'published' => true, 'tags' => ['dog', 'cat', 'mouse'], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); $this->assertEquals('Invalid document structure: Attribute "rating" has invalid type. Value must be a valid integer', $validator->getDescription()); @@ -252,6 +264,8 @@ public function testValidDocument(): void 'published' => true, 'tags' => ['dog', 'cat', 'mouse'], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); } @@ -268,6 +282,8 @@ public function testStringValidation(): void 'published' => true, 'tags' => ['dog', 'cat', 'mouse'], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); $this->assertEquals('Invalid document structure: Attribute "title" has invalid type. Value must be a valid string and no longer than 256 chars', $validator->getDescription()); @@ -286,6 +302,8 @@ public function testArrayOfStringsValidation(): void 'published' => true, 'tags' => [1, 'cat', 'mouse'], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); $this->assertEquals('Invalid document structure: Attribute "tags[\'0\']" has invalid type. Value must be a valid string and no longer than 55 chars', $validator->getDescription()); @@ -299,6 +317,8 @@ public function testArrayOfStringsValidation(): void 'published' => true, 'tags' => [true], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); $this->assertEquals('Invalid document structure: Attribute "tags[\'0\']" has invalid type. Value must be a valid string and no longer than 55 chars', $validator->getDescription()); @@ -312,6 +332,8 @@ public function testArrayOfStringsValidation(): void 'published' => true, 'tags' => [], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); $this->assertEquals(false, $validator->isValid(new Document([ @@ -323,6 +345,8 @@ public function testArrayOfStringsValidation(): void 'published' => true, 'tags' => ['too-long-tag-name-to-make-sure-the-length-validator-inside-string-attribute-type-fails-properly'], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); $this->assertEquals('Invalid document structure: Attribute "tags[\'0\']" has invalid type. Value must be a valid string and no longer than 55 chars', $validator->getDescription()); @@ -343,7 +367,9 @@ public function testArrayAsObjectValidation(): void 'price' => 1.99, 'published' => true, 'tags' => ['name' => 'dog'], - 'feedback' => 'team@appwrite.io' + 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); } @@ -359,7 +385,9 @@ public function testArrayOfObjectsValidation(): void 'price' => 1.99, 'published' => true, 'tags' => [['name' => 'dog']], - 'feedback' => 'team@appwrite.io' + 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); } @@ -376,6 +404,8 @@ public function testIntegerValidation(): void 'published' => false, 'tags' => ['dog', 'cat', 'mouse'], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); $this->assertEquals('Invalid document structure: Attribute "rating" has invalid type. Value must be a valid integer', $validator->getDescription()); @@ -389,6 +419,8 @@ public function testIntegerValidation(): void 'published' => false, 'tags' => ['dog', 'cat', 'mouse'], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); $this->assertEquals('Invalid document structure: Attribute "rating" has invalid type. Value must be a valid integer', $validator->getDescription()); @@ -408,6 +440,8 @@ public function testArrayOfIntegersValidation(): void 'published' => true, 'tags' => ['dog', 'cat', 'mouse'], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); $this->assertEquals(true, $validator->isValid(new Document([ @@ -420,6 +454,8 @@ public function testArrayOfIntegersValidation(): void 'published' => true, 'tags' => ['dog', 'cat', 'mouse'], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); $this->assertEquals(true, $validator->isValid(new Document([ @@ -432,6 +468,8 @@ public function testArrayOfIntegersValidation(): void 'published' => true, 'tags' => ['dog', 'cat', 'mouse'], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); $this->assertEquals(false, $validator->isValid(new Document([ @@ -444,6 +482,8 @@ public function testArrayOfIntegersValidation(): void 'published' => true, 'tags' => ['dog', 'cat', 'mouse'], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); $this->assertEquals('Invalid document structure: Attribute "reviews[\'0\']" has invalid type. Value must be a valid integer', $validator->getDescription()); @@ -462,6 +502,8 @@ public function testFloatValidation(): void 'published' => false, 'tags' => ['dog', 'cat', 'mouse'], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); $this->assertEquals('Invalid document structure: Attribute "price" has invalid type. Value must be a valid float', $validator->getDescription()); @@ -475,6 +517,8 @@ public function testFloatValidation(): void 'published' => false, 'tags' => ['dog', 'cat', 'mouse'], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); $this->assertEquals('Invalid document structure: Attribute "price" has invalid type. Value must be a valid float', $validator->getDescription()); @@ -493,6 +537,8 @@ public function testBooleanValidation(): void 'published' => 1, 'tags' => ['dog', 'cat', 'mouse'], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); $this->assertEquals('Invalid document structure: Attribute "published" has invalid type. Value must be a valid boolean', $validator->getDescription()); @@ -506,6 +552,8 @@ public function testBooleanValidation(): void 'published' => '', 'tags' => ['dog', 'cat', 'mouse'], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); $this->assertEquals('Invalid document structure: Attribute "published" has invalid type. Value must be a valid boolean', $validator->getDescription()); @@ -524,6 +572,8 @@ public function testFormatValidation(): void 'published' => true, 'tags' => ['dog', 'cat', 'mouse'], 'feedback' => 'team_appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); $this->assertEquals('Invalid document structure: Attribute "feedback" has invalid format. Value must be a valid email address', $validator->getDescription()); @@ -542,6 +592,8 @@ public function testIntegerMaxRange(): void 'published' => true, 'tags' => ['dog', 'cat', 'mouse'], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); $this->assertEquals('Invalid document structure: Attribute "rating" has invalid type. Value must be a valid range between -2,147,483,647 and 2,147,483,647', $validator->getDescription()); @@ -560,6 +612,8 @@ public function testDoubleUnsigned(): void 'published' => true, 'tags' => ['dog', 'cat', 'mouse'], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); $this->assertStringContainsString('Invalid document structure: Attribute "price" has invalid type. Value must be a valid range between 0 and ', $validator->getDescription()); @@ -578,6 +632,8 @@ public function testDoubleMaxRange(): void 'published' => true, 'tags' => ['dog', 'cat', 'mouse'], 'feedback' => 'team@appwrite.io', + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); }