diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index 12cd7a9fa..f79a5a14a 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -815,8 +815,6 @@ public function updateDocuments(string $collection, array $documents, int $batch } $this->client->update($name, $filters, $document); - - $documents[$index] = new Document($document); } return $documents; diff --git a/src/Database/Database.php b/src/Database/Database.php index 0a849e5cc..72864e2cc 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -135,6 +135,8 @@ class Database protected Cache $cache; + protected string $cacheName = 'default'; + /** * @var array */ @@ -311,9 +313,13 @@ class Database protected bool $resolveRelationships = true; + protected int $relationshipFetchDepth = 1; + + protected bool $filter = true; + protected bool $validate = true; - protected int $relationshipFetchDepth = 1; + protected bool $preserveDates = false; /** * Stack of collection IDs when creating or updating related documents @@ -599,6 +605,52 @@ public function getDatabase(): string return $this->adapter->getDatabase(); } + /** + * Set the cache instance + * + * @param Cache $cache + * + * @return $this + */ + public function setCache(Cache $cache): self + { + $this->cache = $cache; + return $this; + } + + /** + * Get the cache instance + * + * @return Cache + */ + public function getCache(): Cache + { + return $this->cache; + } + + /** + * Set the name to use for cache + * + * @param string $name + * @return $this + */ + public function setCacheName(string $name): self + { + $this->cacheName = $name; + + return $this; + } + + /** + * Get the cache name + * + * @return string + */ + public function getCacheName(): string + { + return $this->cacheName; + } + /** * Set a metadata value to be printed in the query comments * @@ -659,6 +711,40 @@ public function clearTimeout(string $event = Database::EVENT_ALL): void $this->adapter->clearTimeout($event); } + /** + * Enable filters + * + * @return $this + */ + public function enableFilters(): self + { + $this->filter = true; + + return $this; + } + + /** + * Disable filters + * + * @return $this + */ + public function disableFilters(): self + { + $this->filter = false; + + return $this; + } + + /** + * Get instance filters + * + * @return array + */ + public function getInstanceFilters(): array + { + return $this->instanceFilters; + } + /** * Enable validation * @@ -713,6 +799,33 @@ public function setTenant(?int $tenant): self return $this; } + public function setPreserveDates(bool $preserve): self + { + $this->preserveDates = $preserve; + + return $this; + } + + /** + * Get list of keywords that cannot be used + * + * @return string[] + */ + public function getKeywords(): array + { + return $this->adapter->getKeywords(); + } + + /** + * Get Database Adapter + * + * @return Adapter + */ + public function getAdapter(): Adapter + { + return $this->adapter; + } + /** * Ping Database * @@ -1804,7 +1917,8 @@ public function createRelationship( throw new DuplicateException('Attribute already exists'); } - if ($attribute->getAttribute('type') === self::VAR_RELATIONSHIP + if ( + $attribute->getAttribute('type') === self::VAR_RELATIONSHIP && \strtolower($attribute->getAttribute('options')['twoWayKey']) === \strtolower($twoWayKey) && $attribute->getAttribute('options')['relatedCollection'] === $relatedCollection->getId() ) { @@ -2559,7 +2673,7 @@ public function getDocument(string $collection, string $id, array $queries = []) $validator = new Authorization(self::PERMISSION_READ); $documentSecurity = $collection->getAttribute('documentSecurity', false); - $cacheKey = 'cache-' . $this->getNamespace() . ':' . $this->adapter->getTenant() . ':' . $collection->getId() . ':' . $id; + $cacheKey = $this->cacheName . '-cache-' . $this->getNamespace() . ':' . $this->adapter->getTenant() . ':' . $collection->getId() . ':' . $id; if (!empty($selections)) { $cacheKey .= ':' . \md5(\implode($selections)); @@ -2630,7 +2744,7 @@ public function getDocument(string $collection, string $id, array $queries = []) */ foreach ($this->map as $key => $value) { [$k, $v] = \explode('=>', $key); - $ck = 'cache-' . $this->getNamespace() . ':map:' . $k; + $ck = $this->cacheName . '-cache-' . $this->getNamespace() . ':' . $this->adapter->getTenant() . ':map:' . $k; $cache = $this->cache->load($ck, self::TTL); if (empty($cache)) { $cache = []; @@ -2927,11 +3041,14 @@ public function createDocument(string $collection, Document $document): Document $time = DateTime::now(); + $createdAt = $document->getCreatedAt(); + $updatedAt = $document->getUpdatedAt(); + $document ->setAttribute('$id', empty($document->getId()) ? ID::unique() : $document->getId()) ->setAttribute('$collection', $collection->getId()) - ->setAttribute('$createdAt', $time) - ->setAttribute('$updatedAt', $time); + ->setAttribute('$createdAt', empty($createdAt) || !$this->preserveDates ? $time : $createdAt) + ->setAttribute('$updatedAt', empty($updatedAt) || !$this->preserveDates ? $time : $updatedAt); $document = $this->encode($collection, $document); @@ -2998,11 +3115,14 @@ public function createDocuments(string $collection, array $documents, int $batch } } + $createdAt = $document->getCreatedAt(); + $updatedAt = $document->getUpdatedAt(); + $document ->setAttribute('$id', empty($document->getId()) ? ID::unique() : $document->getId()) ->setAttribute('$collection', $collection->getId()) - ->setAttribute('$createdAt', $time) - ->setAttribute('$updatedAt', $time); + ->setAttribute('$createdAt', empty($createdAt) || !$this->preserveDates ? $time : $createdAt) + ->setAttribute('$updatedAt', empty($updatedAt) || !$this->preserveDates ? $time : $updatedAt); $document = $this->encode($collection, $document); @@ -3359,15 +3479,16 @@ public function updateDocument(string $collection, string $id, Document $documen $relationType = (string) $relationships[$key]['options']['relationType']; $side = (string) $relationships[$key]['options']['side']; - switch($relationType) { + switch ($relationType) { case Database::RELATION_ONE_TO_ONE: $oldValue = $old->getAttribute($key) instanceof Document ? $old->getAttribute($key)->getId() : $old->getAttribute($key); if ((\is_null($value) !== \is_null($oldValue)) - || (\is_string($value) && $value !== $oldValue) - || ($value instanceof Document && $value->getId() !== $oldValue)) { + || (\is_string($value) && $value !== $oldValue) + || ($value instanceof Document && $value->getId() !== $oldValue) + ) { $shouldUpdate = true; } break; @@ -3383,15 +3504,17 @@ public function updateDocument(string $collection, string $id, Document $documen : $old->getAttribute($key); if ((\is_null($value) !== \is_null($oldValue)) - || (\is_string($value) && $value !== $oldValue) - || ($value instanceof Document && $value->getId() !== $oldValue)) { + || (\is_string($value) && $value !== $oldValue) + || ($value instanceof Document && $value->getId() !== $oldValue) + ) { $shouldUpdate = true; } break; } if ((\is_null($old->getAttribute($key)) !== \is_null($value)) - || \count($old->getAttribute($key)) !== \count($value)) { + || \count($old->getAttribute($key)) !== \count($value) + ) { $shouldUpdate = true; break; } @@ -3401,7 +3524,8 @@ public function updateDocument(string $collection, string $id, Document $documen : $old->getAttribute($key)[$index]; if ((\is_string($relation) && $relation !== $oldValue) - || ($relation instanceof Document && $relation->getId() !== $oldValue)) { + || ($relation instanceof Document && $relation->getId() !== $oldValue) + ) { $shouldUpdate = true; break; } @@ -3434,7 +3558,8 @@ public function updateDocument(string $collection, string $id, Document $documen } if ($shouldUpdate) { - $document->setAttribute('$updatedAt', $time); + $updatedAt = $document->getUpdatedAt(); + $document->setAttribute('$updatedAt', empty($updatedAt) || !$this->preserveDates ? $time : $updatedAt); } // Check if document was updated after the request timestamp @@ -3507,6 +3632,10 @@ public function updateDocuments(string $collection, array $documents, int $batch throw new DatabaseException('Must define $id attribute for each document'); } + $updatedAt = $document->getUpdatedAt(); + $document->setAttribute('$updatedAt', empty($updatedAt) || !$this->preserveDates ? $time : $updatedAt); + $document = $this->encode($collection, $document); + $old = Authorization::skip(fn () => $this->silent( fn () => $this->getDocument( $collection->getId(), @@ -3514,12 +3643,11 @@ public function updateDocuments(string $collection, array $documents, int $batch ) )); - $document->setAttribute('$updatedAt', $time); - $document = $this->encode($collection, $document); - $validator = new Authorization(self::PERMISSION_UPDATE); - if ($collection->getId() !== self::METADATA - && !$validator->isValid($old->getUpdate())) { + if ( + $collection->getId() !== self::METADATA + && !$validator->isValid($old->getUpdate()) + ) { throw new AuthorizationException($validator->getDescription()); } @@ -3578,7 +3706,7 @@ private function updateDocumentRelationships(Document $collection, Document $old if ($oldValue == $value) { if ( ($relationType === Database::RELATION_ONE_TO_ONE || - ($relationType === Database::RELATION_MANY_TO_ONE && $side === Database::RELATION_SIDE_PARENT)) && + ($relationType === Database::RELATION_MANY_TO_ONE && $side === Database::RELATION_SIDE_PARENT)) && $value instanceof Document ) { $document->setAttribute($key, $value->getId()); @@ -4059,8 +4187,11 @@ public function decreaseDocumentAttribute(string $collection, string $id, string } $min = $min ? $min + $value : null; + $result = $this->adapter->increaseDocumentAttribute($collection->getId(), $id, $attribute, $value * -1, $min); + $this->purgeCachedDocument($collection->getId(), $id); + $this->trigger(self::EVENT_DOCUMENT_DECREASE, $document); return $result; @@ -4192,8 +4323,8 @@ private function deleteDocumentRelationships(Document $collection, Document $doc // collection as an existing relationship, but a different two-way key (the third condition), // or the same two-way key as an existing relationship, but a different key (the fourth condition). $transitive = (($existingKey === $twoWayKey - && $existingCollection === $relatedCollection->getId() - && $existingSide !== $side) + && $existingCollection === $relatedCollection->getId() + && $existingSide !== $side) || ($existingTwoWayKey === $key && $existingRelatedCollection === $collection->getId() && $existingSide !== $side) @@ -4321,7 +4452,7 @@ private function deleteSetNull(Document $collection, Document $relatedCollection Query::equal($twoWayKey, [$document->getId()]) ]); } else { - if(empty($value)) { + if (empty($value)) { return; } $related = $this->getDocument($relatedCollection->getId(), $value->getId()); @@ -4506,7 +4637,7 @@ private function deleteCascade(Document $collection, Document $relatedCollection */ public function purgeCachedCollection(string $collection): bool { - return $this->cache->purge('cache-' . $this->getNamespace() . ':' . $this->adapter->getTenant() . ':' . $collection . ':*'); + return $this->cache->purge($this->cacheName . '-cache-' . $this->getNamespace() . ':' . $this->adapter->getTenant() . ':' . $collection . ':*'); } /** @@ -4520,7 +4651,7 @@ public function purgeCachedCollection(string $collection): bool */ public function purgeCachedDocument(string $collection, string $id): bool { - return $this->cache->purge('cache-' . $this->getNamespace() . ':' . $this->adapter->getTenant() . ':' . $collection . ':' . $id . ':*'); + return $this->cache->purge($this->cacheName . '-cache-' . $this->getNamespace() . ':' . $this->adapter->getTenant() . ':' . $collection . ':' . $id . ':*'); } /** @@ -5046,6 +5177,10 @@ protected function encodeAttribute(string $name, $value, Document $document): mi */ protected function decodeAttribute(string $name, mixed $value, Document $document): mixed { + if (!$this->filter) { + return $value; + } + if (!array_key_exists($name, self::$filters) && !array_key_exists($name, $this->instanceFilters)) { throw new DatabaseException('Filter not found'); } @@ -5141,46 +5276,6 @@ public function getLimitForIndexes(): int return $this->adapter->getLimitForIndexes() - $this->adapter->getCountOfDefaultIndexes(); } - /** - * Get list of keywords that cannot be used - * - * @return string[] - */ - public function getKeywords(): array - { - return $this->adapter->getKeywords(); - } - - /** - * Get Database Adapter - * - * @return Adapter - */ - public function getAdapter(): Adapter - { - return $this->adapter; - } - - /** - * Get Cache - * - * @return Cache - */ - public function getCache(): Cache - { - return $this->cache; - } - - /** - * Get instance filters - * - * @return array - */ - public function getInstanceFilters(): array - { - return $this->instanceFilters; - } - /** * @param Document $collection * @param array $queries @@ -5236,7 +5331,7 @@ private function purgeRelatedDocuments(Document $collection, string $id): void return; } - $key = 'cache-' . $this->getNamespace() . ':map:' . $collection->getId() . ':' . $id; + $key = $this->cacheName . '-cache-' . $this->getNamespace() . ':map:' . $collection->getId() . ':' . $id; $cache = $this->cache->load($key, self::TTL); if (!empty($cache)) { foreach ($cache as $v) { diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 74fc8d9b6..801c8b089 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -241,6 +241,105 @@ public function testDeleteRelatedCollection(): void $this->assertCount(0, $collection->getAttribute('indexes')); } + public function testPreserveDatesUpdate(): void + { + Authorization::disable(); + + static::getDatabase()->setPreserveDates(true); + + static::getDatabase()->createCollection('preserve_update_dates'); + + static::getDatabase()->createAttribute('preserve_update_dates', 'attr1', Database::VAR_STRING, 10, false); + + $doc1 = static::getDatabase()->createDocument('preserve_update_dates', new Document([ + '$id' => 'doc1', + '$permissions' => [], + 'attr1' => 'value1', + ])); + + $doc2 = static::getDatabase()->createDocument('preserve_update_dates', new Document([ + '$id' => 'doc2', + '$permissions' => [], + 'attr1' => 'value2', + ])); + + $doc3 = static::getDatabase()->createDocument('preserve_update_dates', new Document([ + '$id' => 'doc3', + '$permissions' => [], + 'attr1' => 'value3', + ])); + + $newDate = '2000-01-01T10:00:00.000+00:00'; + + $doc1->setAttribute('$updatedAt', $newDate); + static::getDatabase()->updateDocument('preserve_update_dates', 'doc1', $doc1); + $doc1 = static::getDatabase()->getDocument('preserve_update_dates', 'doc1'); + $this->assertEquals($newDate, $doc1->getAttribute('$updatedAt')); + + $doc2->setAttribute('$updatedAt', $newDate); + $doc3->setAttribute('$updatedAt', $newDate); + static::getDatabase()->updateDocuments('preserve_update_dates', [$doc2, $doc3], 2); + + $doc2 = static::getDatabase()->getDocument('preserve_update_dates', 'doc2'); + $doc3 = static::getDatabase()->getDocument('preserve_update_dates', 'doc3'); + $this->assertEquals($newDate, $doc2->getAttribute('$updatedAt')); + $this->assertEquals($newDate, $doc3->getAttribute('$updatedAt')); + + static::getDatabase()->deleteCollection('preserve_update_dates'); + + static::getDatabase()->setPreserveDates(false); + + Authorization::reset(); + } + + public function testPreserveDatesCreate(): void + { + Authorization::disable(); + + static::getDatabase()->setPreserveDates(true); + + static::getDatabase()->createCollection('preserve_create_dates'); + + static::getDatabase()->createAttribute('preserve_create_dates', 'attr1', Database::VAR_STRING, 10, false); + + $date = '2000-01-01T10:00:00.000+00:00'; + + static::getDatabase()->createDocument('preserve_create_dates', new Document([ + '$id' => 'doc1', + '$permissions' => [], + 'attr1' => 'value1', + '$createdAt' => $date + ])); + + static::getDatabase()->createDocuments('preserve_create_dates', [ + new Document([ + '$id' => 'doc2', + '$permissions' => [], + 'attr1' => 'value2', + '$createdAt' => $date + ]), + new Document([ + '$id' => 'doc3', + '$permissions' => [], + 'attr1' => 'value3', + '$createdAt' => $date + ]), + ], 2); + + $doc1 = static::getDatabase()->getDocument('preserve_create_dates', 'doc1'); + $doc2 = static::getDatabase()->getDocument('preserve_create_dates', 'doc2'); + $doc3 = static::getDatabase()->getDocument('preserve_create_dates', 'doc3'); + $this->assertEquals($date, $doc1->getAttribute('$createdAt')); + $this->assertEquals($date, $doc2->getAttribute('$createdAt')); + $this->assertEquals($date, $doc3->getAttribute('$createdAt')); + + static::getDatabase()->deleteCollection('preserve_create_dates'); + + static::getDatabase()->setPreserveDates(false); + + Authorization::reset(); + } + /** * @throws Exception|Throwable */