Skip to content

Commit 012af26

Browse files
Merge pull request #9 from swift-sprinter/feature/optimistic_lock
Add optimistic locking support
2 parents 861b2da + 72f25fe commit 012af26

File tree

18 files changed

+193
-48
lines changed

18 files changed

+193
-48
lines changed

Example/BreezeItemAPI/Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ let package = Package(
1212
.executable(name: "ItemAPI", targets: ["ItemAPI"]),
1313
],
1414
dependencies: [
15-
.package(url: "https://github.com/swift-sprinter/Breeze.git", from: "0.1.0")
15+
.package(url: "https://github.com/swift-sprinter/Breeze.git", from: "0.2.0")
1616
],
1717
targets: [
1818
.executableTarget(

Package.resolved

Lines changed: 20 additions & 20 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ let package = Package(
2525
dependencies: [
2626
.package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "1.0.0-alpha.1"),
2727
.package(url: "https://github.com/swift-server/swift-aws-lambda-events.git", from: "0.1.0"),
28-
.package(url: "https://github.com/soto-project/soto.git", from: "6.0.0"),
28+
.package(url: "https://github.com/soto-project/soto.git", from: "6.7.0"),
2929
.package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"),
3030
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.0.0"),
3131
.package(url: "https://github.com/stencilproject/Stencil.git", from: "0.15.1"),

README.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@ All you need to do is to decide the struct conforming `BreezeCodable` to persist
6969

7070
Each lambda will be initialized with a specific `_HANDLER` and it will run the code to implement the required logic needed by one of the CRUD functions. The code needs to be packaged and deployed using the referenced architecture.
7171

72+
### Optimistic locking
73+
74+
Optimistic locking is a strategy to ensure that the BreezeCodable Item is not updated by another request before updating or deleting it.
75+
The fields `updatedAt` and `createdAt` are used to implement optimistic locking.
76+
Refer to the [DynamoDB documentation](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBMapper.OptimisticLocking.html) for more details.
77+
7278
## Lambda package with Swift Package Manager
7379

7480
To package the Lambda is required to create a Swift Package using the following `Package.swift` file.
@@ -88,7 +94,7 @@ let package = Package(
8894
.executable(name: "ItemAPI", targets: ["ItemAPI"]),
8995
],
9096
dependencies: [
91-
.package(url: "https://github.com/swift-sprinter/Breeze.git", from: "0.1.0"),
97+
.package(url: "https://github.com/swift-sprinter/Breeze.git", from: "0.2.0"),
9298
],
9399
targets: [
94100
.executableTarget(
@@ -146,7 +152,7 @@ OPTIONS:
146152
Define a configuration file with the following format:
147153
```yml
148154
service: swift-breeze-rest-item-api
149-
awsRegion: us_east_1
155+
awsRegion: us-east-1
150156
swiftVersion: 5.7.3
151157
swiftConfiguration: release
152158
packageName: BreezeItemAPI
@@ -199,7 +205,7 @@ output:
199205
/Users/andreascuderi/Documents/workspace/Breeze/Sources/BreezeCommand/Resources/breeze.yml
200206
201207
service: swift-breeze-rest-item-api
202-
awsRegion: us_east_1
208+
awsRegion: us-east-1
203209
swiftVersion: 5.7.3
204210
swiftConfiguration: release
205211
packageName: BreezeItemAPI
@@ -303,12 +309,12 @@ Returns the updated `BreezeCodable`.
303309

304310
- `delete`
305311

306-
Gets the value of the `BreezeCodable.key` from the `APIGatewayV2Request.pathParameters` dictionary and calls `deleteItem` on `BreezeDynamoDBService`.
312+
Gets the value of the `BreezeCodable.key` from the `APIGatewayV2Request.pathParameters` dictionary, the value of `updatedAt` and `createdAt` from `APIGatewayV2Request.queryStringParameters` dictionary and calls `deleteItem` on `BreezeDynamoDBService`.
307313
Returns the `BreezeCodable` if persisted on DynamoDB.
308314

309315
- `list`
310316

311-
Gets the value of the `exclusiveStartKey` and `limit` from the `APIGatewayV2Request.pathParameters` dictionary and calls `listItems` on `BreezeDynamoDBService`.
317+
Gets the value of the `exclusiveStartKey` and `limit` from the `APIGatewayV2Request.queryStringParameters` dictionary and calls `listItems` on `BreezeDynamoDBService`.
312318
Returns the `ListResponse` containing the items if persisted on DynamoDB.
313319

314320
```swift

Sources/BreezeCommand/Resources/Template/SwiftPackage/Package.swift.stencil

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ let package = Package(
1212
.executable(name: "{{ params.targetName }}", targets: ["{{ params.targetName }}"]),
1313
],
1414
dependencies: [
15-
.package(url: "https://github.com/swift-sprinter/Breeze.git", from: "0.1.0")
15+
.package(url: "https://github.com/swift-sprinter/Breeze.git", from: "0.2.0")
1616
],
1717
targets: [
1818
.executableTarget(

Sources/BreezeCommand/Resources/breeze-sign-in-with-apple.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
service: swift-breeze-rest-item-api
2-
awsRegion: us_east_1
2+
awsRegion: us-east-1
33
swiftVersion: 5.7.3
44
swiftConfiguration: release
55
packageName: BreezeItemAPI

Sources/BreezeCommand/Resources/breeze.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
service: swift-breeze-rest-item-api
2-
awsRegion: us_east_1
2+
awsRegion: us-east-1
33
swiftVersion: 5.7.3
44
swiftConfiguration: release
55
packageName: BreezeItemAPI

Sources/BreezeDynamoDBService/BreezeDynamoDBService.swift

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import SotoDynamoDB
1919
public class BreezeDynamoDBService: BreezeDynamoDBServing {
2020
enum ServiceError: Error {
2121
case notFound
22+
case missingParameters
2223
}
2324

2425
let db: DynamoDB
@@ -39,6 +40,8 @@ public extension BreezeDynamoDBService {
3940
item.createdAt = date.iso8601
4041
item.updatedAt = date.iso8601
4142
let input = DynamoDB.PutItemCodableInput(
43+
conditionExpression: "attribute_not_exists(#keyName)",
44+
expressionAttributeNames: ["#keyName": keyName],
4245
item: item,
4346
tableName: tableName
4447
)
@@ -58,12 +61,19 @@ public extension BreezeDynamoDBService {
5861
return item
5962
}
6063

64+
private struct AdditionalAttributes: Encodable {
65+
let oldUpdatedAt: String
66+
}
67+
6168
func updateItem<T: BreezeCodable>(item: T) async throws -> T {
6269
var item = item
70+
let oldUpdatedAt = item.updatedAt ?? ""
6371
let date = Date()
6472
item.updatedAt = date.iso8601
65-
let input = DynamoDB.UpdateItemCodableInput(
66-
conditionExpression: "attribute_exists(createdAt)",
73+
let attributes = AdditionalAttributes(oldUpdatedAt: oldUpdatedAt)
74+
let input = try DynamoDB.UpdateItemCodableInput(
75+
additionalAttributes: attributes,
76+
conditionExpression: "attribute_exists(#\(keyName)) AND #updatedAt = :oldUpdatedAt AND #createdAt = :createdAt",
6777
key: [keyName],
6878
tableName: tableName,
6979
updateItem: item
@@ -72,9 +82,19 @@ public extension BreezeDynamoDBService {
7282
return try await readItem(key: item.key)
7383
}
7484

75-
func deleteItem(key: String) async throws {
85+
func deleteItem<T: BreezeCodable>(item: T) async throws {
86+
guard let updatedAt = item.updatedAt,
87+
let createdAt = item.createdAt else {
88+
throw ServiceError.missingParameters
89+
}
90+
7691
let input = DynamoDB.DeleteItemInput(
77-
key: [keyName: DynamoDB.AttributeValue.s(key)],
92+
conditionExpression: "#updatedAt = :updatedAt AND #createdAt = :createdAt",
93+
expressionAttributeNames: ["#updatedAt": "updatedAt",
94+
"#createdAt" : "createdAt"],
95+
expressionAttributeValues: [":updatedAt": .s(updatedAt),
96+
":createdAt" : .s(createdAt)],
97+
key: [keyName: DynamoDB.AttributeValue.s(item.key)],
7898
tableName: tableName
7999
)
80100
let _ = try await db.deleteItem(input)

Sources/BreezeDynamoDBService/BreezeDynamoDBServing.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,6 @@ public protocol BreezeDynamoDBServing {
2020
func createItem<Item: BreezeCodable>(item: Item) async throws -> Item
2121
func readItem<Item: BreezeCodable>(key: String) async throws -> Item
2222
func updateItem<Item: BreezeCodable>(item: Item) async throws -> Item
23-
func deleteItem(key: String) async throws
23+
func deleteItem<Item: BreezeCodable>(item: Item) async throws
2424
func listItems<Item: BreezeCodable>(key: String?, limit: Int?) async throws -> ListResponse<Item>
2525
}

Sources/BreezeLambdaAPI/BreezeLambdaHandler.swift

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,14 +81,23 @@ struct BreezeLambdaHandler<T: BreezeCodable> {
8181
return APIGatewayV2Response(with: error, statusCode: .notFound)
8282
}
8383
}
84+
85+
struct SimpleItem: BreezeCodable {
86+
var key: String
87+
var createdAt: String?
88+
var updatedAt: String?
89+
}
8490

8591
func deleteLambdaHandler(context: AWSLambdaRuntimeCore.LambdaContext, event: APIGatewayV2Request) async -> APIGatewayV2Response {
86-
guard let key = event.pathParameters?[keyName] else {
92+
guard let key = event.pathParameters?[keyName],
93+
let createdAt = event.queryStringParameters?["createdAt"],
94+
let updatedAt = event.queryStringParameters?["updatedAt"] else {
8795
let error = BreezeLambdaAPIError.invalidRequest
8896
return APIGatewayV2Response(with: error, statusCode: .forbidden)
8997
}
9098
do {
91-
try await self.service.deleteItem(key: key)
99+
let simpleItem = SimpleItem(key: key, createdAt: createdAt, updatedAt: updatedAt)
100+
try await self.service.deleteItem(item: simpleItem)
92101
return APIGatewayV2Response(with: BreezeEmptyResponse(), statusCode: .ok)
93102
} catch {
94103
return APIGatewayV2Response(with: error, statusCode: .notFound)

0 commit comments

Comments
 (0)