Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/Database/Adapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -1153,7 +1153,15 @@ protected function escapeWildcards(string $value): string
* @return bool
* @throws Exception
*/
abstract public function increaseDocumentAttribute(string $collection, string $id, string $attribute, int|float $value, string $updatedAt, int|float|null $min = null, int|float|null $max = null): bool;
abstract public function increaseDocumentAttribute(
string $collection,
string $id,
string $attribute,
int|float $value,
string $updatedAt,
int|float|null $min = null,
int|float|null $max = null
): bool;

/**
* Returns the connection ID identifier
Expand Down
18 changes: 15 additions & 3 deletions src/Database/Adapter/MariaDB.php
Original file line number Diff line number Diff line change
Expand Up @@ -1382,8 +1382,15 @@ public function createOrUpdateDocuments(
* @return bool
* @throws DatabaseException
*/
public function increaseDocumentAttribute(string $collection, string $id, string $attribute, int|float $value, string $updatedAt, int|float|null $min = null, int|float|null $max = null): bool
{
public function increaseDocumentAttribute(
string $collection,
string $id,
string $attribute,
int|float $value,
string $updatedAt,
int|float|null $min = null,
int|float|null $max = null
): bool {
$name = $this->filter($collection);
$attribute = $this->filter($attribute);

Expand Down Expand Up @@ -1412,7 +1419,12 @@ public function increaseDocumentAttribute(string $collection, string $id, string
$stmt->bindValue(':_tenant', $this->tenant);
}

$stmt->execute() || throw new DatabaseException('Failed to update attribute');
try {
$stmt->execute();
} catch (PDOException $e) {
throw $this->processException($e);
}

return true;
}

Expand Down
212 changes: 114 additions & 98 deletions src/Database/Database.php
Original file line number Diff line number Diff line change
Expand Up @@ -5091,44 +5091,32 @@ public function createOrUpdateDocumentsWithIncrease(
/**
* Increase a document attribute by a value
*
* @param string $collection
* @param string $id
* @param string $attribute
* @param int|float $value
* @param int|float|null $max
* @return bool
*
* @param string $collection The collection ID
* @param string $id The document ID
* @param string $attribute The attribute to increase
* @param int|float $value The value to increase the attribute by, can be a float
* @param int|float|null $max The maximum value the attribute can reach after the increase, null means no limit
* @return Document
* @throws AuthorizationException
* @throws DatabaseException
* @throws Exception
* @throws LimitException
* @throws NotFoundException
* @throws TypeException
* @throws \Throwable
*/
public function increaseDocumentAttribute(string $collection, string $id, string $attribute, int|float $value = 1, int|float|null $max = null): bool
{
public function increaseDocumentAttribute(
string $collection,
string $id,
string $attribute,
int|float $value = 1,
int|float|null $max = null
): Document {
if ($value <= 0) { // Can be a float
throw new DatabaseException('Value must be numeric and greater than 0');
}

$validator = new Authorization(self::PERMISSION_UPDATE);

/* @var $document Document */
$document = Authorization::skip(fn () => $this->silent(fn () => $this->getDocument($collection, $id))); // Skip ensures user does not need read permission for this

if ($document->isEmpty()) {
return false;
}

$collection = $this->silent(fn () => $this->getCollection($collection));

if ($collection->getId() !== self::METADATA) {
$documentSecurity = $collection->getAttribute('documentSecurity', false);
if (!$validator->isValid([
...$collection->getUpdate(),
...($documentSecurity ? $document->getUpdate() : [])
])) {
throw new AuthorizationException($validator->getDescription());
}
}

$attr = \array_filter($collection->getAttribute('attributes', []), function ($a) use ($attribute) {
return $a['$id'] === $attribute;
});
Expand All @@ -5137,46 +5125,66 @@ public function increaseDocumentAttribute(string $collection, string $id, string
throw new NotFoundException('Attribute not found');
}

$whiteList = [self::VAR_INTEGER, self::VAR_FLOAT];
$whiteList = [
self::VAR_INTEGER,
self::VAR_FLOAT
];

/**
* @var Document $attr
*/
/** @var Document $attr */
$attr = \end($attr);
if (!in_array($attr->getAttribute('type'), $whiteList)) {
throw new TypeException('Attribute type must be one of: ' . implode(',', $whiteList));
}

if ($max && ($document->getAttribute($attribute) + $value > $max)) {
throw new LimitException('Attribute value exceeds maximum limit: ' . $max);
}
$document = $this->withTransaction(function () use ($collection, $id, $attribute, $value, $max) {
/* @var $document Document */
$document = Authorization::skip(fn () => $this->silent(fn () => $this->getDocument($collection->getId(), $id, forUpdate: true))); // Skip ensures user does not need read permission for this

$time = DateTime::now();
$updatedAt = $document->getUpdatedAt();
$updatedAt = (empty($updatedAt) || !$this->preserveDates) ? $time : $updatedAt;
if ($document->isEmpty()) {
throw new NotFoundException('Document not found');
}

// Check if document was updated after the request timestamp
$oldUpdatedAt = new \DateTime($document->getUpdatedAt());
if (!is_null($this->timestamp) && $oldUpdatedAt > $this->timestamp) {
throw new ConflictException('Document was updated after the request timestamp');
}
$validator = new Authorization(self::PERMISSION_UPDATE);

$max = $max ? $max - $value : null;
if ($collection->getId() !== self::METADATA) {
$documentSecurity = $collection->getAttribute('documentSecurity', false);
if (!$validator->isValid([
...$collection->getUpdate(),
...($documentSecurity ? $document->getUpdate() : [])
])) {
throw new AuthorizationException($validator->getDescription());
}
}

$result = $this->adapter->increaseDocumentAttribute(
$collection->getId(),
$id,
$attribute,
$value,
$updatedAt,
max: $max
);
if ($max && ($document->getAttribute($attribute) + $value > $max)) {
throw new LimitException('Attribute value exceeds maximum limit: ' . $max);
}

$time = DateTime::now();
$updatedAt = $document->getUpdatedAt();
$updatedAt = (empty($updatedAt) || !$this->preserveDates) ? $time : $updatedAt;
$max = $max ? $max - $value : null;

$this->adapter->increaseDocumentAttribute(
$collection->getId(),
$id,
$attribute,
$value,
$updatedAt,
max: $max
);

return $document->setAttribute(
$attribute,
$document->getAttribute($attribute) + $value
);
});

$this->purgeCachedDocument($collection->getId(), $id);

$this->trigger(self::EVENT_DOCUMENT_INCREASE, $document);

return $result;
return $document;
}


Expand All @@ -5188,38 +5196,24 @@ public function increaseDocumentAttribute(string $collection, string $id, string
* @param string $attribute
* @param int|float $value
* @param int|float|null $min
* @return bool
* @return Document
*
* @throws AuthorizationException
* @throws DatabaseException
*/
public function decreaseDocumentAttribute(string $collection, string $id, string $attribute, int|float $value = 1, int|float|null $min = null): bool
{
public function decreaseDocumentAttribute(
string $collection,
string $id,
string $attribute,
int|float $value = 1,
int|float|null $min = null
): Document {
if ($value <= 0) { // Can be a float
throw new DatabaseException('Value must be numeric and greater than 0');
}

$validator = new Authorization(self::PERMISSION_UPDATE);

/* @var $document Document */
$document = Authorization::skip(fn () => $this->silent(fn () => $this->getDocument($collection, $id))); // Skip ensures user does not need read permission for this

if ($document->isEmpty()) {
return false;
}

$collection = $this->silent(fn () => $this->getCollection($collection));

if ($collection->getId() !== self::METADATA) {
$documentSecurity = $collection->getAttribute('documentSecurity', false);
if (!$validator->isValid([
...$collection->getUpdate(),
...($documentSecurity ? $document->getUpdate() : [])
])) {
throw new AuthorizationException($validator->getDescription());
}
}

$attr = \array_filter($collection->getAttribute('attributes', []), function ($a) use ($attribute) {
return $a['$id'] === $attribute;
});
Expand All @@ -5228,7 +5222,10 @@ public function decreaseDocumentAttribute(string $collection, string $id, string
throw new NotFoundException('Attribute not found');
}

$whiteList = [self::VAR_INTEGER, self::VAR_FLOAT];
$whiteList = [
self::VAR_INTEGER,
self::VAR_FLOAT
];

/**
* @var Document $attr
Expand All @@ -5238,36 +5235,55 @@ public function decreaseDocumentAttribute(string $collection, string $id, string
throw new TypeException('Attribute type must be one of: ' . \implode(',', $whiteList));
}

if ($min && ($document->getAttribute($attribute) - $value < $min)) {
throw new LimitException('Attribute value exceeds minimum limit: ' . $min);
}
$document = $this->withTransaction(function () use ($collection, $id, $attribute, $value, $min) {
/* @var $document Document */
$document = Authorization::skip(fn () => $this->silent(fn () => $this->getDocument($collection->getId(), $id, forUpdate: true))); // Skip ensures user does not need read permission for this

$time = DateTime::now();
$updatedAt = $document->getUpdatedAt();
$updatedAt = (empty($updatedAt) || !$this->preserveDates) ? $time : $updatedAt;
if ($document->isEmpty()) {
throw new NotFoundException('Document not found');
}

// Check if document was updated after the request timestamp
$oldUpdatedAt = new \DateTime($document->getUpdatedAt());
if (!is_null($this->timestamp) && $oldUpdatedAt > $this->timestamp) {
throw new ConflictException('Document was updated after the request timestamp');
}
$validator = new Authorization(self::PERMISSION_UPDATE);

$min = $min ? $min + $value : null;
if ($collection->getId() !== self::METADATA) {
$documentSecurity = $collection->getAttribute('documentSecurity', false);
if (!$validator->isValid([
...$collection->getUpdate(),
...($documentSecurity ? $document->getUpdate() : [])
])) {
throw new AuthorizationException($validator->getDescription());
}
}

$result = $this->adapter->increaseDocumentAttribute(
$collection->getId(),
$id,
$attribute,
$value * -1,
$updatedAt,
min: $min
);
if ($min && ($document->getAttribute($attribute) - $value < $min)) {
throw new LimitException('Attribute value exceeds minimum limit: ' . $min);
}

$time = DateTime::now();
$updatedAt = $document->getUpdatedAt();
$updatedAt = (empty($updatedAt) || !$this->preserveDates) ? $time : $updatedAt;
$min = $min ? $min + $value : null;

$this->adapter->increaseDocumentAttribute(
$collection->getId(),
$id,
$attribute,
$value * -1,
$updatedAt,
min: $min
);

return $document->setAttribute(
$attribute,
$document->getAttribute($attribute) - $value
);
});

$this->purgeCachedDocument($collection->getId(), $id);

$this->trigger(self::EVENT_DOCUMENT_DECREASE, $document);

return $result;
return $document;
}

/**
Expand Down
4 changes: 2 additions & 2 deletions src/Database/Mirror.php
Original file line number Diff line number Diff line change
Expand Up @@ -948,12 +948,12 @@ public function renameIndex(string $collection, string $old, string $new): bool
return $this->delegate(__FUNCTION__, \func_get_args());
}

public function increaseDocumentAttribute(string $collection, string $id, string $attribute, int|float $value = 1, int|float|null $max = null): bool
public function increaseDocumentAttribute(string $collection, string $id, string $attribute, int|float $value = 1, int|float|null $max = null): Document
{
return $this->delegate(__FUNCTION__, \func_get_args());
}

public function decreaseDocumentAttribute(string $collection, string $id, string $attribute, int|float $value = 1, int|float|null $min = null): bool
public function decreaseDocumentAttribute(string $collection, string $id, string $attribute, int|float $value = 1, int|float|null $min = null): Document
{
return $this->delegate(__FUNCTION__, \func_get_args());
}
Expand Down
14 changes: 9 additions & 5 deletions tests/e2e/Adapter/Scopes/DocumentTests.php
Original file line number Diff line number Diff line change
Expand Up @@ -981,7 +981,7 @@ public function testIncreaseDecrease(): Document
'increase' => 100,
'decrease' => 100,
'increase_float' => 100,
'increase_text' => "some text",
'increase_text' => 'some text',
'$permissions' => [
Permission::read(Role::any()),
Permission::create(Role::any()),
Expand All @@ -992,21 +992,25 @@ public function testIncreaseDecrease(): Document

$updatedAt = $document->getUpdatedAt();

$this->assertEquals(true, $database->increaseDocumentAttribute($collection, $document->getId(), 'increase', 1, 101));
$doc = $database->increaseDocumentAttribute($collection, $document->getId(), 'increase', 1, 101);
$this->assertEquals(101, $doc->getAttribute('increase'));

$document = $database->getDocument($collection, $document->getId());
$this->assertEquals(101, $document->getAttribute('increase'));
$this->assertNotEquals($updatedAt, $document->getUpdatedAt());

$this->assertEquals(true, $database->decreaseDocumentAttribute($collection, $document->getId(), 'decrease', 1, 98));
$doc = $database->decreaseDocumentAttribute($collection, $document->getId(), 'decrease', 1, 98);
$this->assertEquals(99, $doc->getAttribute('decrease'));
$document = $database->getDocument($collection, $document->getId());
$this->assertEquals(99, $document->getAttribute('decrease'));

$this->assertEquals(true, $database->increaseDocumentAttribute($collection, $document->getId(), 'increase_float', 5.5, 110));
$doc = $database->increaseDocumentAttribute($collection, $document->getId(), 'increase_float', 5.5, 110);
$this->assertEquals(105.5, $doc->getAttribute('increase_float'));
$document = $database->getDocument($collection, $document->getId());
$this->assertEquals(105.5, $document->getAttribute('increase_float'));

$this->assertEquals(true, $database->decreaseDocumentAttribute($collection, $document->getId(), 'increase_float', 1.1, 100));
$doc = $database->decreaseDocumentAttribute($collection, $document->getId(), 'increase_float', 1.1, 100);
$this->assertEquals(104.4, $doc->getAttribute('increase_float'));
$document = $database->getDocument($collection, $document->getId());
$this->assertEquals(104.4, $document->getAttribute('increase_float'));

Expand Down