Skip to content

Commit f0c28b7

Browse files
authored
Merge pull request #604 from utopia-php/feat-type-except
Return new value atomically
2 parents d4b2048 + b4ab39b commit f0c28b7

File tree

5 files changed

+149
-109
lines changed

5 files changed

+149
-109
lines changed

src/Database/Adapter.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1153,7 +1153,15 @@ protected function escapeWildcards(string $value): string
11531153
* @return bool
11541154
* @throws Exception
11551155
*/
1156-
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;
1156+
abstract public function increaseDocumentAttribute(
1157+
string $collection,
1158+
string $id,
1159+
string $attribute,
1160+
int|float $value,
1161+
string $updatedAt,
1162+
int|float|null $min = null,
1163+
int|float|null $max = null
1164+
): bool;
11571165

11581166
/**
11591167
* Returns the connection ID identifier

src/Database/Adapter/MariaDB.php

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1382,8 +1382,15 @@ public function createOrUpdateDocuments(
13821382
* @return bool
13831383
* @throws DatabaseException
13841384
*/
1385-
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
1386-
{
1385+
public function increaseDocumentAttribute(
1386+
string $collection,
1387+
string $id,
1388+
string $attribute,
1389+
int|float $value,
1390+
string $updatedAt,
1391+
int|float|null $min = null,
1392+
int|float|null $max = null
1393+
): bool {
13871394
$name = $this->filter($collection);
13881395
$attribute = $this->filter($attribute);
13891396

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

1415-
$stmt->execute() || throw new DatabaseException('Failed to update attribute');
1422+
try {
1423+
$stmt->execute();
1424+
} catch (PDOException $e) {
1425+
throw $this->processException($e);
1426+
}
1427+
14161428
return true;
14171429
}
14181430

src/Database/Database.php

Lines changed: 114 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -5091,44 +5091,32 @@ public function createOrUpdateDocumentsWithIncrease(
50915091
/**
50925092
* Increase a document attribute by a value
50935093
*
5094-
* @param string $collection
5095-
* @param string $id
5096-
* @param string $attribute
5097-
* @param int|float $value
5098-
* @param int|float|null $max
5099-
* @return bool
5100-
*
5094+
* @param string $collection The collection ID
5095+
* @param string $id The document ID
5096+
* @param string $attribute The attribute to increase
5097+
* @param int|float $value The value to increase the attribute by, can be a float
5098+
* @param int|float|null $max The maximum value the attribute can reach after the increase, null means no limit
5099+
* @return Document
51015100
* @throws AuthorizationException
51025101
* @throws DatabaseException
5103-
* @throws Exception
5102+
* @throws LimitException
5103+
* @throws NotFoundException
5104+
* @throws TypeException
5105+
* @throws \Throwable
51045106
*/
5105-
public function increaseDocumentAttribute(string $collection, string $id, string $attribute, int|float $value = 1, int|float|null $max = null): bool
5106-
{
5107+
public function increaseDocumentAttribute(
5108+
string $collection,
5109+
string $id,
5110+
string $attribute,
5111+
int|float $value = 1,
5112+
int|float|null $max = null
5113+
): Document {
51075114
if ($value <= 0) { // Can be a float
51085115
throw new DatabaseException('Value must be numeric and greater than 0');
51095116
}
51105117

5111-
$validator = new Authorization(self::PERMISSION_UPDATE);
5112-
5113-
/* @var $document Document */
5114-
$document = Authorization::skip(fn () => $this->silent(fn () => $this->getDocument($collection, $id))); // Skip ensures user does not need read permission for this
5115-
5116-
if ($document->isEmpty()) {
5117-
return false;
5118-
}
5119-
51205118
$collection = $this->silent(fn () => $this->getCollection($collection));
51215119

5122-
if ($collection->getId() !== self::METADATA) {
5123-
$documentSecurity = $collection->getAttribute('documentSecurity', false);
5124-
if (!$validator->isValid([
5125-
...$collection->getUpdate(),
5126-
...($documentSecurity ? $document->getUpdate() : [])
5127-
])) {
5128-
throw new AuthorizationException($validator->getDescription());
5129-
}
5130-
}
5131-
51325120
$attr = \array_filter($collection->getAttribute('attributes', []), function ($a) use ($attribute) {
51335121
return $a['$id'] === $attribute;
51345122
});
@@ -5137,46 +5125,66 @@ public function increaseDocumentAttribute(string $collection, string $id, string
51375125
throw new NotFoundException('Attribute not found');
51385126
}
51395127

5140-
$whiteList = [self::VAR_INTEGER, self::VAR_FLOAT];
5128+
$whiteList = [
5129+
self::VAR_INTEGER,
5130+
self::VAR_FLOAT
5131+
];
51415132

5142-
/**
5143-
* @var Document $attr
5144-
*/
5133+
/** @var Document $attr */
51455134
$attr = \end($attr);
51465135
if (!in_array($attr->getAttribute('type'), $whiteList)) {
51475136
throw new TypeException('Attribute type must be one of: ' . implode(',', $whiteList));
51485137
}
51495138

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

5154-
$time = DateTime::now();
5155-
$updatedAt = $document->getUpdatedAt();
5156-
$updatedAt = (empty($updatedAt) || !$this->preserveDates) ? $time : $updatedAt;
5143+
if ($document->isEmpty()) {
5144+
throw new NotFoundException('Document not found');
5145+
}
51575146

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

5164-
$max = $max ? $max - $value : null;
5149+
if ($collection->getId() !== self::METADATA) {
5150+
$documentSecurity = $collection->getAttribute('documentSecurity', false);
5151+
if (!$validator->isValid([
5152+
...$collection->getUpdate(),
5153+
...($documentSecurity ? $document->getUpdate() : [])
5154+
])) {
5155+
throw new AuthorizationException($validator->getDescription());
5156+
}
5157+
}
51655158

5166-
$result = $this->adapter->increaseDocumentAttribute(
5167-
$collection->getId(),
5168-
$id,
5169-
$attribute,
5170-
$value,
5171-
$updatedAt,
5172-
max: $max
5173-
);
5159+
if ($max && ($document->getAttribute($attribute) + $value > $max)) {
5160+
throw new LimitException('Attribute value exceeds maximum limit: ' . $max);
5161+
}
5162+
5163+
$time = DateTime::now();
5164+
$updatedAt = $document->getUpdatedAt();
5165+
$updatedAt = (empty($updatedAt) || !$this->preserveDates) ? $time : $updatedAt;
5166+
$max = $max ? $max - $value : null;
5167+
5168+
$this->adapter->increaseDocumentAttribute(
5169+
$collection->getId(),
5170+
$id,
5171+
$attribute,
5172+
$value,
5173+
$updatedAt,
5174+
max: $max
5175+
);
5176+
5177+
return $document->setAttribute(
5178+
$attribute,
5179+
$document->getAttribute($attribute) + $value
5180+
);
5181+
});
51745182

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

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

5179-
return $result;
5187+
return $document;
51805188
}
51815189

51825190

@@ -5188,38 +5196,24 @@ public function increaseDocumentAttribute(string $collection, string $id, string
51885196
* @param string $attribute
51895197
* @param int|float $value
51905198
* @param int|float|null $min
5191-
* @return bool
5199+
* @return Document
51925200
*
51935201
* @throws AuthorizationException
51945202
* @throws DatabaseException
51955203
*/
5196-
public function decreaseDocumentAttribute(string $collection, string $id, string $attribute, int|float $value = 1, int|float|null $min = null): bool
5197-
{
5204+
public function decreaseDocumentAttribute(
5205+
string $collection,
5206+
string $id,
5207+
string $attribute,
5208+
int|float $value = 1,
5209+
int|float|null $min = null
5210+
): Document {
51985211
if ($value <= 0) { // Can be a float
51995212
throw new DatabaseException('Value must be numeric and greater than 0');
52005213
}
52015214

5202-
$validator = new Authorization(self::PERMISSION_UPDATE);
5203-
5204-
/* @var $document Document */
5205-
$document = Authorization::skip(fn () => $this->silent(fn () => $this->getDocument($collection, $id))); // Skip ensures user does not need read permission for this
5206-
5207-
if ($document->isEmpty()) {
5208-
return false;
5209-
}
5210-
52115215
$collection = $this->silent(fn () => $this->getCollection($collection));
52125216

5213-
if ($collection->getId() !== self::METADATA) {
5214-
$documentSecurity = $collection->getAttribute('documentSecurity', false);
5215-
if (!$validator->isValid([
5216-
...$collection->getUpdate(),
5217-
...($documentSecurity ? $document->getUpdate() : [])
5218-
])) {
5219-
throw new AuthorizationException($validator->getDescription());
5220-
}
5221-
}
5222-
52235217
$attr = \array_filter($collection->getAttribute('attributes', []), function ($a) use ($attribute) {
52245218
return $a['$id'] === $attribute;
52255219
});
@@ -5228,7 +5222,10 @@ public function decreaseDocumentAttribute(string $collection, string $id, string
52285222
throw new NotFoundException('Attribute not found');
52295223
}
52305224

5231-
$whiteList = [self::VAR_INTEGER, self::VAR_FLOAT];
5225+
$whiteList = [
5226+
self::VAR_INTEGER,
5227+
self::VAR_FLOAT
5228+
];
52325229

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

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

5245-
$time = DateTime::now();
5246-
$updatedAt = $document->getUpdatedAt();
5247-
$updatedAt = (empty($updatedAt) || !$this->preserveDates) ? $time : $updatedAt;
5242+
if ($document->isEmpty()) {
5243+
throw new NotFoundException('Document not found');
5244+
}
52485245

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

5255-
$min = $min ? $min + $value : null;
5248+
if ($collection->getId() !== self::METADATA) {
5249+
$documentSecurity = $collection->getAttribute('documentSecurity', false);
5250+
if (!$validator->isValid([
5251+
...$collection->getUpdate(),
5252+
...($documentSecurity ? $document->getUpdate() : [])
5253+
])) {
5254+
throw new AuthorizationException($validator->getDescription());
5255+
}
5256+
}
52565257

5257-
$result = $this->adapter->increaseDocumentAttribute(
5258-
$collection->getId(),
5259-
$id,
5260-
$attribute,
5261-
$value * -1,
5262-
$updatedAt,
5263-
min: $min
5264-
);
5258+
if ($min && ($document->getAttribute($attribute) - $value < $min)) {
5259+
throw new LimitException('Attribute value exceeds minimum limit: ' . $min);
5260+
}
5261+
5262+
$time = DateTime::now();
5263+
$updatedAt = $document->getUpdatedAt();
5264+
$updatedAt = (empty($updatedAt) || !$this->preserveDates) ? $time : $updatedAt;
5265+
$min = $min ? $min + $value : null;
5266+
5267+
$this->adapter->increaseDocumentAttribute(
5268+
$collection->getId(),
5269+
$id,
5270+
$attribute,
5271+
$value * -1,
5272+
$updatedAt,
5273+
min: $min
5274+
);
5275+
5276+
return $document->setAttribute(
5277+
$attribute,
5278+
$document->getAttribute($attribute) - $value
5279+
);
5280+
});
52655281

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

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

5270-
return $result;
5286+
return $document;
52715287
}
52725288

52735289
/**

src/Database/Mirror.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -948,12 +948,12 @@ public function renameIndex(string $collection, string $old, string $new): bool
948948
return $this->delegate(__FUNCTION__, \func_get_args());
949949
}
950950

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

956-
public function decreaseDocumentAttribute(string $collection, string $id, string $attribute, int|float $value = 1, int|float|null $min = null): bool
956+
public function decreaseDocumentAttribute(string $collection, string $id, string $attribute, int|float $value = 1, int|float|null $min = null): Document
957957
{
958958
return $this->delegate(__FUNCTION__, \func_get_args());
959959
}

tests/e2e/Adapter/Scopes/DocumentTests.php

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -981,7 +981,7 @@ public function testIncreaseDecrease(): Document
981981
'increase' => 100,
982982
'decrease' => 100,
983983
'increase_float' => 100,
984-
'increase_text' => "some text",
984+
'increase_text' => 'some text',
985985
'$permissions' => [
986986
Permission::read(Role::any()),
987987
Permission::create(Role::any()),
@@ -992,21 +992,25 @@ public function testIncreaseDecrease(): Document
992992

993993
$updatedAt = $document->getUpdatedAt();
994994

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

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

1001-
$this->assertEquals(true, $database->decreaseDocumentAttribute($collection, $document->getId(), 'decrease', 1, 98));
1002+
$doc = $database->decreaseDocumentAttribute($collection, $document->getId(), 'decrease', 1, 98);
1003+
$this->assertEquals(99, $doc->getAttribute('decrease'));
10021004
$document = $database->getDocument($collection, $document->getId());
10031005
$this->assertEquals(99, $document->getAttribute('decrease'));
10041006

1005-
$this->assertEquals(true, $database->increaseDocumentAttribute($collection, $document->getId(), 'increase_float', 5.5, 110));
1007+
$doc = $database->increaseDocumentAttribute($collection, $document->getId(), 'increase_float', 5.5, 110);
1008+
$this->assertEquals(105.5, $doc->getAttribute('increase_float'));
10061009
$document = $database->getDocument($collection, $document->getId());
10071010
$this->assertEquals(105.5, $document->getAttribute('increase_float'));
10081011

1009-
$this->assertEquals(true, $database->decreaseDocumentAttribute($collection, $document->getId(), 'increase_float', 1.1, 100));
1012+
$doc = $database->decreaseDocumentAttribute($collection, $document->getId(), 'increase_float', 1.1, 100);
1013+
$this->assertEquals(104.4, $doc->getAttribute('increase_float'));
10101014
$document = $database->getDocument($collection, $document->getId());
10111015
$this->assertEquals(104.4, $document->getAttribute('increase_float'));
10121016

0 commit comments

Comments
 (0)