Skip to content

Commit 7f6293d

Browse files
Implement DynamoDB TransactWriteItem (#1262)
1 parent 2de24b2 commit 7f6293d

18 files changed

+1705
-52
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
"php": "^7.2.5 || ^8.0",
1515
"ext-filter": "*",
1616
"ext-json": "*",
17-
"async-aws/core": "^1.9"
17+
"async-aws/core": "^1.9",
18+
"symfony/polyfill-uuid": "^1.0"
1819
},
1920
"conflict": {
2021
"symfony/http-client": "<4.4.16,<5.1.7"

src/DynamoDbClient.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,17 @@
1616
use AsyncAws\DynamoDb\Enum\TableClass;
1717
use AsyncAws\DynamoDb\Exception\ConditionalCheckFailedException;
1818
use AsyncAws\DynamoDb\Exception\DuplicateItemException;
19+
use AsyncAws\DynamoDb\Exception\IdempotentParameterMismatchException;
1920
use AsyncAws\DynamoDb\Exception\InternalServerErrorException;
2021
use AsyncAws\DynamoDb\Exception\ItemCollectionSizeLimitExceededException;
2122
use AsyncAws\DynamoDb\Exception\LimitExceededException;
2223
use AsyncAws\DynamoDb\Exception\ProvisionedThroughputExceededException;
2324
use AsyncAws\DynamoDb\Exception\RequestLimitExceededException;
2425
use AsyncAws\DynamoDb\Exception\ResourceInUseException;
2526
use AsyncAws\DynamoDb\Exception\ResourceNotFoundException;
27+
use AsyncAws\DynamoDb\Exception\TransactionCanceledException;
2628
use AsyncAws\DynamoDb\Exception\TransactionConflictException;
29+
use AsyncAws\DynamoDb\Exception\TransactionInProgressException;
2730
use AsyncAws\DynamoDb\Input\BatchGetItemInput;
2831
use AsyncAws\DynamoDb\Input\BatchWriteItemInput;
2932
use AsyncAws\DynamoDb\Input\CreateTableInput;
@@ -36,6 +39,7 @@
3639
use AsyncAws\DynamoDb\Input\PutItemInput;
3740
use AsyncAws\DynamoDb\Input\QueryInput;
3841
use AsyncAws\DynamoDb\Input\ScanInput;
42+
use AsyncAws\DynamoDb\Input\TransactWriteItemsInput;
3943
use AsyncAws\DynamoDb\Input\UpdateItemInput;
4044
use AsyncAws\DynamoDb\Input\UpdateTableInput;
4145
use AsyncAws\DynamoDb\Input\UpdateTimeToLiveInput;
@@ -53,6 +57,7 @@
5357
use AsyncAws\DynamoDb\Result\ScanOutput;
5458
use AsyncAws\DynamoDb\Result\TableExistsWaiter;
5559
use AsyncAws\DynamoDb\Result\TableNotExistsWaiter;
60+
use AsyncAws\DynamoDb\Result\TransactWriteItemsOutput;
5661
use AsyncAws\DynamoDb\Result\UpdateItemOutput;
5762
use AsyncAws\DynamoDb\Result\UpdateTableOutput;
5863
use AsyncAws\DynamoDb\Result\UpdateTimeToLiveOutput;
@@ -72,6 +77,7 @@
7277
use AsyncAws\DynamoDb\ValueObject\StreamSpecification;
7378
use AsyncAws\DynamoDb\ValueObject\Tag;
7479
use AsyncAws\DynamoDb\ValueObject\TimeToLiveSpecification;
80+
use AsyncAws\DynamoDb\ValueObject\TransactWriteItem;
7581

7682
class DynamoDbClient extends AbstractApi
7783
{
@@ -568,6 +574,47 @@ public function tableNotExists($input): TableNotExistsWaiter
568574
return new TableNotExistsWaiter($response, $this, $input);
569575
}
570576

577+
/**
578+
* `TransactWriteItems` is a synchronous write operation that groups up to 25 action requests. These actions can target
579+
* items in different tables, but not in different Amazon Web Services accounts or Regions, and no two actions can
580+
* target the same item. For example, you cannot both `ConditionCheck` and `Update` the same item. The aggregate size of
581+
* the items in the transaction cannot exceed 4 MB.
582+
*
583+
* @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TransactWriteItems.html
584+
* @see https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-dynamodb-2012-08-10.html#transactwriteitems
585+
*
586+
* @param array{
587+
* TransactItems: TransactWriteItem[],
588+
* ReturnConsumedCapacity?: ReturnConsumedCapacity::*,
589+
* ReturnItemCollectionMetrics?: ReturnItemCollectionMetrics::*,
590+
* ClientRequestToken?: string,
591+
* @region?: string,
592+
* }|TransactWriteItemsInput $input
593+
*
594+
* @throws ResourceNotFoundException
595+
* @throws TransactionCanceledException
596+
* @throws TransactionInProgressException
597+
* @throws IdempotentParameterMismatchException
598+
* @throws ProvisionedThroughputExceededException
599+
* @throws RequestLimitExceededException
600+
* @throws InternalServerErrorException
601+
*/
602+
public function transactWriteItems($input): TransactWriteItemsOutput
603+
{
604+
$input = TransactWriteItemsInput::create($input);
605+
$response = $this->getResponse($input->request(), new RequestContext(['operation' => 'TransactWriteItems', 'region' => $input->getRegion(), 'exceptionMapping' => [
606+
'ResourceNotFoundException' => ResourceNotFoundException::class,
607+
'TransactionCanceledException' => TransactionCanceledException::class,
608+
'TransactionInProgressException' => TransactionInProgressException::class,
609+
'IdempotentParameterMismatchException' => IdempotentParameterMismatchException::class,
610+
'ProvisionedThroughputExceededException' => ProvisionedThroughputExceededException::class,
611+
'RequestLimitExceeded' => RequestLimitExceededException::class,
612+
'InternalServerError' => InternalServerErrorException::class,
613+
]]));
614+
615+
return new TransactWriteItemsOutput($response);
616+
}
617+
571618
/**
572619
* Edits an existing item's attributes, or adds a new item to the table if it does not already exist. You can put,
573620
* delete, or add attribute values. You can also perform a conditional update on an existing item (insert a new
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace AsyncAws\DynamoDb\Enum;
4+
5+
/**
6+
* Use `ReturnValuesOnConditionCheckFailure` to get the item attributes if the `ConditionCheck` condition fails. For
7+
* `ReturnValuesOnConditionCheckFailure`, the valid values are: NONE and ALL_OLD.
8+
*/
9+
final class ReturnValuesOnConditionCheckFailure
10+
{
11+
public const ALL_OLD = 'ALL_OLD';
12+
public const NONE = 'NONE';
13+
14+
public static function exists(string $value): bool
15+
{
16+
return isset([
17+
self::ALL_OLD => true,
18+
self::NONE => true,
19+
][$value]);
20+
}
21+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
namespace AsyncAws\DynamoDb\Exception;
4+
5+
use AsyncAws\Core\Exception\Http\ClientException;
6+
use Symfony\Contracts\HttpClient\ResponseInterface;
7+
8+
/**
9+
* DynamoDB rejected the request because you retried a request with a different payload but with an idempotent token
10+
* that was already used.
11+
*/
12+
final class IdempotentParameterMismatchException extends ClientException
13+
{
14+
protected function populateResult(ResponseInterface $response): void
15+
{
16+
$data = $response->toArray(false);
17+
18+
if (null !== $v = (isset($data['message']) ? (string) $data['message'] : null)) {
19+
$this->message = $v;
20+
}
21+
}
22+
}
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
<?php
2+
3+
namespace AsyncAws\DynamoDb\Exception;
4+
5+
use AsyncAws\Core\Exception\Http\ClientException;
6+
use AsyncAws\DynamoDb\ValueObject\AttributeValue;
7+
use AsyncAws\DynamoDb\ValueObject\CancellationReason;
8+
use Symfony\Contracts\HttpClient\ResponseInterface;
9+
10+
/**
11+
* The entire transaction request was canceled.
12+
* DynamoDB cancels a `TransactWriteItems` request under the following circumstances:.
13+
*
14+
* - A condition in one of the condition expressions is not met.
15+
* - A table in the `TransactWriteItems` request is in a different account or region.
16+
* - More than one action in the `TransactWriteItems` operation targets the same item.
17+
* - There is insufficient provisioned capacity for the transaction to be completed.
18+
* - An item size becomes too large (larger than 400 KB), or a local secondary index (LSI) becomes too large, or a
19+
* similar validation error occurs because of changes made by the transaction.
20+
* - There is a user error, such as an invalid data format.
21+
*
22+
* DynamoDB cancels a `TransactGetItems` request under the following circumstances:
23+
*
24+
* - There is an ongoing `TransactGetItems` operation that conflicts with a concurrent `PutItem`, `UpdateItem`,
25+
* `DeleteItem` or `TransactWriteItems` request. In this case the `TransactGetItems` operation fails with a
26+
* `TransactionCanceledException`.
27+
* - A table in the `TransactGetItems` request is in a different account or region.
28+
* - There is insufficient provisioned capacity for the transaction to be completed.
29+
* - There is a user error, such as an invalid data format.
30+
*
31+
* > If using Java, DynamoDB lists the cancellation reasons on the `CancellationReasons` property. This property is not
32+
* > set for other languages. Transaction cancellation reasons are ordered in the order of requested items, if an item
33+
* > has no error it will have `None` code and `Null` message.
34+
*
35+
* Cancellation reason codes and possible error messages:
36+
*
37+
* - No Errors:
38+
*
39+
* - Code: `None`
40+
* - Message: `null`
41+
*
42+
* - Conditional Check Failed:
43+
*
44+
* - Code: `ConditionalCheckFailed`
45+
* - Message: The conditional request failed.
46+
*
47+
* - Item Collection Size Limit Exceeded:
48+
*
49+
* - Code: `ItemCollectionSizeLimitExceeded`
50+
* - Message: Collection size exceeded.
51+
*
52+
* - Transaction Conflict:
53+
*
54+
* - Code: `TransactionConflict`
55+
* - Message: Transaction is ongoing for the item.
56+
*
57+
* - Provisioned Throughput Exceeded:
58+
*
59+
* - Code: `ProvisionedThroughputExceeded`
60+
* - Messages:
61+
*
62+
* - The level of configured provisioned throughput for the table was exceeded. Consider increasing your
63+
* provisioning level with the UpdateTable API.
64+
*
65+
* > This Message is received when provisioned throughput is exceeded is on a provisioned DynamoDB table.
66+
*
67+
* - The level of configured provisioned throughput for one or more global secondary indexes of the table was
68+
* exceeded. Consider increasing your provisioning level for the under-provisioned global secondary indexes with
69+
* the UpdateTable API.
70+
*
71+
* > This message is returned when provisioned throughput is exceeded is on a provisioned GSI.
72+
*
73+
*
74+
*
75+
* - Throttling Error:
76+
*
77+
* - Code: `ThrottlingError`
78+
* - Messages:
79+
*
80+
* - Throughput exceeds the current capacity of your table or index. DynamoDB is automatically scaling your table or
81+
* index so please try again shortly. If exceptions persist, check if you have a hot key:
82+
* https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-partition-key-design.html.
83+
*
84+
* > This message is returned when writes get throttled on an On-Demand table as DynamoDB is automatically scaling
85+
* > the table.
86+
*
87+
* - Throughput exceeds the current capacity for one or more global secondary indexes. DynamoDB is automatically
88+
* scaling your index so please try again shortly.
89+
*
90+
* > This message is returned when when writes get throttled on an On-Demand GSI as DynamoDB is automatically
91+
* > scaling the GSI.
92+
*
93+
*
94+
*
95+
* - Validation Error:
96+
*
97+
* - Code: `ValidationError`
98+
* - Messages:
99+
*
100+
* - One or more parameter values were invalid.
101+
* - The update expression attempted to update the secondary index key beyond allowed size limits.
102+
* - The update expression attempted to update the secondary index key to unsupported type.
103+
* - An operand in the update expression has an incorrect data type.
104+
* - Item size to update has exceeded the maximum allowed size.
105+
* - Number overflow. Attempting to store a number with magnitude larger than supported range.
106+
* - Type mismatch for attribute to update.
107+
* - Nesting Levels have exceeded supported limits.
108+
* - The document path provided in the update expression is invalid for update.
109+
* - The provided expression refers to an attribute that does not exist in the item.
110+
*/
111+
final class TransactionCanceledException extends ClientException
112+
{
113+
/**
114+
* A list of cancellation reasons.
115+
*/
116+
private $cancellationReasons;
117+
118+
/**
119+
* @return CancellationReason[]
120+
*/
121+
public function getCancellationReasons(): array
122+
{
123+
return $this->cancellationReasons;
124+
}
125+
126+
protected function populateResult(ResponseInterface $response): void
127+
{
128+
$data = $response->toArray(false);
129+
130+
if (null !== $v = (isset($data['message']) ? (string) $data['message'] : null)) {
131+
$this->message = $v;
132+
}
133+
$this->cancellationReasons = empty($data['CancellationReasons']) ? [] : $this->populateResultCancellationReasonList($data['CancellationReasons']);
134+
}
135+
136+
/**
137+
* @return array<string, AttributeValue>
138+
*/
139+
private function populateResultAttributeMap(array $json): array
140+
{
141+
$items = [];
142+
foreach ($json as $name => $value) {
143+
$items[(string) $name] = AttributeValue::create($value);
144+
}
145+
146+
return $items;
147+
}
148+
149+
private function populateResultCancellationReason(array $json): CancellationReason
150+
{
151+
return new CancellationReason([
152+
'Item' => !isset($json['Item']) ? null : $this->populateResultAttributeMap($json['Item']),
153+
'Code' => isset($json['Code']) ? (string) $json['Code'] : null,
154+
'Message' => isset($json['Message']) ? (string) $json['Message'] : null,
155+
]);
156+
}
157+
158+
/**
159+
* @return CancellationReason[]
160+
*/
161+
private function populateResultCancellationReasonList(array $json): array
162+
{
163+
$items = [];
164+
foreach ($json as $item) {
165+
$items[] = $this->populateResultCancellationReason($item);
166+
}
167+
168+
return $items;
169+
}
170+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace AsyncAws\DynamoDb\Exception;
4+
5+
use AsyncAws\Core\Exception\Http\ClientException;
6+
use Symfony\Contracts\HttpClient\ResponseInterface;
7+
8+
/**
9+
* The transaction with the given request token is already in progress.
10+
*/
11+
final class TransactionInProgressException extends ClientException
12+
{
13+
protected function populateResult(ResponseInterface $response): void
14+
{
15+
$data = $response->toArray(false);
16+
17+
if (null !== $v = (isset($data['message']) ? (string) $data['message'] : null)) {
18+
$this->message = $v;
19+
}
20+
}
21+
}

0 commit comments

Comments
 (0)