├── .gitignore ├── NOTICE ├── .swiftlint.yml ├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── codeql.yml │ └── swift.yml ├── CODE_OF_CONDUCT.md ├── Sources └── SmokeDynamoDB │ ├── CustomRowTypeIdentifier.swift │ ├── InternalDynamoDBCodingKey.swift │ ├── DynamoDBDecoder.swift │ ├── RowWithItemVersionProtocol.swift │ ├── DynamoDBEncoder.swift │ ├── TimeToLive.swift │ ├── TypedDatabaseItemWithTimeToLive+RowWithItemVersionProtocol.swift │ ├── String+DynamoDBKey.swift │ ├── RowWithItemVersion.swift │ ├── RowWithIndex.swift │ ├── CompositePrimaryKey.swift │ ├── InMemoryDynamoDBCompositePrimaryKeysProjection.swift │ ├── PolymorphicOperationReturnType.swift │ ├── PolymorphicWriteEntry.swift │ ├── QueryInput+forSortKeyCondition.swift │ ├── InternalUnkeyedEncodingContainer.swift │ ├── InternalKeyedEncodingContainer.swift │ ├── DynamoDBCompositePrimaryKeysProjection.swift │ ├── TypedDatabaseItemWithTimeToLive.swift │ ├── AWSDynamoDBTableOperationsClient.swift │ ├── _DynamoDBClient │ └── _AWSDynamoDBClientGenerator.swift │ ├── InternalUnkeyedDecodingContainer.swift │ ├── InternalKeyedDecodingContainer.swift │ ├── AWSDynamoDBCompositePrimaryKeysProjectionGenerator.swift │ ├── InternalSingleValueDecodingContainer.swift │ ├── DynamoDBCompositePrimaryKeyGSILogic.swift │ ├── InMemoryDynamoDBCompositePrimaryKeysProjectionStore.swift │ ├── AWSDynamoDBCompositePrimaryKeyTableGenerator.swift │ ├── DynamoDBCompositePrimaryKeyTable+clobberVersionedItemWithHistoricalRow.swift │ ├── DynamoDBCompositePrimaryKeyTable+consistentReadQuery.swift │ ├── InMemoryDynamoDBCompositePrimaryKeyTableStore+execute.swift │ ├── AWSDynamoDBCompositePrimaryKeyTable+deleteItems.swift │ ├── InternalSingleValueEncodingContainer.swift │ └── InMemoryDynamoDBCompositePrimaryKeyTableStore+monomorphicQuery.swift ├── Package.swift ├── Tests └── SmokeDynamoDBTests │ ├── String+DynamoDBKeyTests.swift │ ├── TestConfiguration.swift │ ├── DynamoDBEncoderDecoderTests.swift │ ├── DynamoDBCompositePrimaryKeyTableClobberVersionedItemWithHistoricalRowTests.swift │ └── SmokeDynamoDBTestInput.swift ├── CONTRIBUTING.md └── Package.resolved /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | .DS_Store 3 | .build/ 4 | .swiftpm/ 5 | *.xcodeproj 6 | *~ 7 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Smoke Dynamodb 2 | Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - trailing_whitespace 3 | - void_return 4 | included: 5 | - Sources 6 | line_length: 150 7 | function_body_length: 8 | warning: 50 9 | error: 75 10 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. 7 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "smoke-dynamodb-3.x" ] 6 | pull_request: 7 | branches: [ "smoke-dynamodb-3.x" ] 8 | 9 | jobs: 10 | run-codeql-linux: 11 | name: Run CodeQL on Linux 12 | runs-on: ubuntu-latest 13 | permissions: 14 | security-events: write 15 | 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v3 19 | 20 | - name: Initialize CodeQL 21 | uses: github/codeql-action/init@v2 22 | with: 23 | languages: swift 24 | 25 | - name: Build 26 | run: swift build 27 | 28 | - name: Perform CodeQL Analysis 29 | uses: github/codeql-action/analyze@v2 30 | -------------------------------------------------------------------------------- /Sources/SmokeDynamoDB/CustomRowTypeIdentifier.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // CustomRowTypeIdentifier.swift 15 | // SmokeDynamoDB 16 | // 17 | 18 | import Foundation 19 | 20 | public protocol CustomRowTypeIdentifier { 21 | static var rowTypeIdentifier: String? { get } 22 | } 23 | 24 | func getTypeRowIdentifier(type: Any.Type) -> String { 25 | let typeRowIdentifier: String 26 | // if this type has a custom row identity 27 | if let customAttributesTypeType = type as? CustomRowTypeIdentifier.Type, 28 | let identifier = customAttributesTypeType.rowTypeIdentifier { 29 | typeRowIdentifier = identifier 30 | } else { 31 | typeRowIdentifier = String(describing: type) 32 | } 33 | 34 | return typeRowIdentifier 35 | } 36 | -------------------------------------------------------------------------------- /Sources/SmokeDynamoDB/InternalDynamoDBCodingKey.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // InternalDynamoDBCodingKey.swift 15 | // SmokeDynamoDB 16 | // 17 | 18 | import Foundation 19 | 20 | struct InternalDynamoDBCodingKey: CodingKey { 21 | var stringValue: String 22 | var intValue: Int? 23 | 24 | init?(stringValue: String) { 25 | self.stringValue = stringValue 26 | self.intValue = nil 27 | } 28 | 29 | init?(intValue: Int) { 30 | self.stringValue = "\(intValue)" 31 | self.intValue = intValue 32 | } 33 | 34 | init(index: Int) { 35 | self.stringValue = "Index \(index)" 36 | self.intValue = index 37 | } 38 | 39 | static let `super` = InternalDynamoDBCodingKey(stringValue: "super")! 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [ smoke-dynamodb-3.x ] 6 | pull_request: 7 | branches: [ smoke-dynamodb-3.x ] 8 | 9 | jobs: 10 | LatestVersionBuild: 11 | name: Swift ${{ matrix.swift }} on ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | os: [ubuntu-22.04, ubuntu-20.04] 15 | swift: ["5.9"] 16 | runs-on: ${{ matrix.os }} 17 | steps: 18 | - uses: swift-actions/setup-swift@v1.25.0 19 | with: 20 | swift-version: ${{ matrix.swift }} 21 | - uses: actions/checkout@v2 22 | - name: Build 23 | run: swift build -c release 24 | - name: Run tests 25 | run: swift test 26 | OlderVersionBuild: 27 | name: Swift ${{ matrix.swift }} on ${{ matrix.os }} 28 | strategy: 29 | matrix: 30 | os: [ubuntu-20.04] 31 | swift: ["5.8.1", "5.7.3"] 32 | runs-on: ${{ matrix.os }} 33 | steps: 34 | - uses: swift-actions/setup-swift@v1.25.0 35 | with: 36 | swift-version: ${{ matrix.swift }} 37 | - uses: actions/checkout@v2 38 | - name: Build 39 | run: swift build -c release 40 | - name: Run tests 41 | run: swift test 42 | SwiftLint: 43 | name: SwiftLint version 3.2.1 44 | runs-on: ubuntu-latest 45 | steps: 46 | - uses: actions/checkout@v1 47 | - name: GitHub Action for SwiftLint 48 | uses: norio-nomura/action-swiftlint@3.2.1 49 | -------------------------------------------------------------------------------- /Sources/SmokeDynamoDB/DynamoDBDecoder.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // DynamoDBDecoder.swift 15 | // SmokeDynamoDB 16 | // 17 | 18 | import Foundation 19 | import DynamoDBModel 20 | 21 | public class DynamoDBDecoder { 22 | internal let attributeNameTransform: ((String) -> String)? 23 | 24 | public init(attributeNameTransform: ((String) -> String)? = nil) { 25 | self.attributeNameTransform = attributeNameTransform 26 | } 27 | 28 | public func decode(_ value: DynamoDBModel.AttributeValue, userInfo: [CodingUserInfoKey: Any] = [:]) throws -> T { 29 | let container = InternalSingleValueDecodingContainer(attributeValue: value, 30 | codingPath: [], 31 | userInfo: userInfo, 32 | attributeNameTransform: attributeNameTransform) 33 | 34 | return try T(from: container) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/SmokeDynamoDB/RowWithItemVersionProtocol.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // RowWithItemVersionProtocol.swift 15 | // SmokeDynamoDB 16 | // 17 | 18 | import Foundation 19 | 20 | /** 21 | Protocol for a item payload wrapper that declares an item version. 22 | Primarily required to allow the constrained extension below. 23 | */ 24 | public protocol RowWithItemVersionProtocol { 25 | associatedtype RowType: Codable 26 | 27 | /// The item version number 28 | var itemVersion: Int { get } 29 | /// The item payload 30 | var rowValue: RowType { get } 31 | 32 | /// Function that accepts a version and an updated row version and returns 33 | /// an instance of the implementing type 34 | func createUpdatedItem(withVersion itemVersion: Int?, 35 | withValue newRowValue: RowType) -> Self 36 | 37 | /// Function that accepts an updated row version and returns 38 | /// an instance of the implementing type 39 | func createUpdatedItem(withValue newRowValue: RowType) -> Self 40 | } 41 | 42 | public extension RowWithItemVersionProtocol { 43 | /// Default implementation that delegates to createUpdatedItem(withVersion:withValue:) 44 | func createUpdatedItem(withValue newRowValue: RowType) -> Self { 45 | return createUpdatedItem(withVersion: nil, withValue: newRowValue) 46 | } 47 | } 48 | 49 | /// Declare conformance of RowWithItemVersion to RowWithItemVersionProtocol 50 | extension RowWithItemVersion: RowWithItemVersionProtocol { 51 | 52 | } 53 | -------------------------------------------------------------------------------- /Sources/SmokeDynamoDB/DynamoDBEncoder.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // DynamoDBEncoder.swift 15 | // SmokeDynamoDB 16 | // 17 | 18 | import Foundation 19 | import DynamoDBModel 20 | 21 | public class DynamoDBEncoder { 22 | private let attributeNameTransform: ((String) -> String)? 23 | 24 | public init(attributeNameTransform: ((String) -> String)? = nil) { 25 | self.attributeNameTransform = attributeNameTransform 26 | } 27 | 28 | public func encode(_ value: T, userInfo: [CodingUserInfoKey: Any] = [:]) throws -> DynamoDBModel.AttributeValue { 29 | let container = InternalSingleValueEncodingContainer(userInfo: userInfo, 30 | codingPath: [], 31 | attributeNameTransform: attributeNameTransform, 32 | defaultValue: nil) 33 | try value.encode(to: container) 34 | 35 | return container.attributeValue 36 | } 37 | } 38 | 39 | internal protocol AttributeValueConvertable { 40 | var attributeValue: DynamoDBModel.AttributeValue { get } 41 | } 42 | 43 | extension DynamoDBModel.AttributeValue: AttributeValueConvertable { 44 | var attributeValue: DynamoDBModel.AttributeValue { 45 | return self 46 | } 47 | } 48 | 49 | internal enum ContainerValueType { 50 | case singleValue(AttributeValueConvertable) 51 | case unkeyedContainer([AttributeValueConvertable]) 52 | case keyedContainer([String: AttributeValueConvertable]) 53 | } 54 | -------------------------------------------------------------------------------- /Sources/SmokeDynamoDB/TimeToLive.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // TimeToLive.swift 15 | // SmokeDynamoDB 16 | // 17 | 18 | import Foundation 19 | 20 | public protocol TimeToLiveAttributes { 21 | static var timeToLiveAttributeName: String { get } 22 | } 23 | 24 | public struct StandardTimeToLiveAttributes: TimeToLiveAttributes { 25 | public static var timeToLiveAttributeName: String { 26 | return "ExpireDate" 27 | } 28 | } 29 | 30 | public typealias StandardTimeToLive = TimeToLive 31 | 32 | public struct TimeToLive: Codable, CustomStringConvertible, Hashable { 33 | public var description: String { 34 | return "TimeToLive(timeToLiveTimestamp: \(timeToLiveTimestamp)" 35 | } 36 | 37 | public let timeToLiveTimestamp: Int64 38 | 39 | public init(timeToLiveTimestamp: Int64) { 40 | self.timeToLiveTimestamp = timeToLiveTimestamp 41 | } 42 | 43 | public init(from decoder: Decoder) throws { 44 | let values = try decoder.container(keyedBy: DynamoDBAttributesTypeCodingKey.self) 45 | self.timeToLiveTimestamp = try values.decode(Int64.self, forKey: DynamoDBAttributesTypeCodingKey(stringValue: AttributesType.timeToLiveAttributeName)!) 46 | } 47 | 48 | public func encode(to encoder: Encoder) throws { 49 | var container = encoder.container(keyedBy: DynamoDBAttributesTypeCodingKey.self) 50 | try container.encode(self.timeToLiveTimestamp, forKey: DynamoDBAttributesTypeCodingKey(stringValue: AttributesType.timeToLiveAttributeName)!) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.7 2 | // 3 | // Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"). 6 | // You may not use this file except in compliance with the License. 7 | // A copy of the License is located at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // or in the "license" file accompanying this file. This file is distributed 12 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 13 | // express or implied. See the License for the specific language governing 14 | // permissions and limitations under the License. 15 | 16 | import PackageDescription 17 | 18 | let package = Package( 19 | name: "smoke-dynamodb", 20 | platforms: [ 21 | .macOS(.v10_15), .iOS(.v13), .watchOS(.v6), .tvOS(.v13) 22 | ], 23 | products: [ 24 | .library( 25 | name: "SmokeDynamoDB", 26 | targets: ["SmokeDynamoDB"]), 27 | ], 28 | dependencies: [ 29 | .package(url: "https://github.com/amzn/smoke-aws.git", from: "3.0.0"), 30 | .package(url: "https://github.com/amzn/smoke-http.git", from: "3.0.0"), 31 | .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"), 32 | .package(url: "https://github.com/apple/swift-metrics.git", "1.0.0"..<"3.0.0"), 33 | .package(url: "https://github.com/JohnSundell/CollectionConcurrencyKit", from :"0.2.0") 34 | ], 35 | targets: [ 36 | .target( 37 | name: "SmokeDynamoDB", dependencies: [ 38 | .product(name: "Logging", package: "swift-log"), 39 | .product(name: "Metrics", package: "swift-metrics"), 40 | .product(name: "DynamoDBClient", package: "smoke-aws"), 41 | .product(name: "SmokeHTTPClient", package: "smoke-http"), 42 | .product(name: "_SmokeAWSHttpConcurrency", package: "smoke-aws"), 43 | .product(name: "CollectionConcurrencyKit", package: "CollectionConcurrencyKit"), 44 | ]), 45 | .testTarget( 46 | name: "SmokeDynamoDBTests", dependencies: [ 47 | .target(name: "SmokeDynamoDB"), 48 | .product(name: "SmokeHTTPClient", package: "smoke-http"), 49 | ]), 50 | ], 51 | swiftLanguageVersions: [.v5] 52 | ) 53 | -------------------------------------------------------------------------------- /Sources/SmokeDynamoDB/TypedDatabaseItemWithTimeToLive+RowWithItemVersionProtocol.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // TypedDatabaseItemWithTimeToLive+RowWithItemVersionProtocol.swift 15 | // SmokeDynamoDB 16 | // 17 | 18 | import Foundation 19 | 20 | /// An extension for TypedDatabaseItem that is constrained by the RowType conforming 21 | /// to RowWithItemVersionProtocol 22 | extension TypedDatabaseItemWithTimeToLive where RowType: RowWithItemVersionProtocol { 23 | /// Helper function wrapping createUpdatedItem that will verify if 24 | /// conditionalStatusVersion is provided that it matches the version 25 | /// of the current item 26 | public func createUpdatedRowWithItemVersion(withValue value: RowType.RowType, 27 | conditionalStatusVersion: Int?, 28 | andTimeToLive timeToLive: TimeToLive? = nil) throws 29 | -> TypedDatabaseItemWithTimeToLive { 30 | // if we can only update a particular version 31 | if let overwriteVersion = conditionalStatusVersion, 32 | rowValue.itemVersion != overwriteVersion { 33 | throw SmokeDynamoDBError.concurrencyError(partitionKey: compositePrimaryKey.partitionKey, 34 | sortKey: compositePrimaryKey.sortKey, 35 | message: "Current row did not have the required version '\(overwriteVersion)'") 36 | } 37 | 38 | let updatedPayloadWithVersion: RowType = rowValue.createUpdatedItem(withValue: value) 39 | return createUpdatedItem(withValue: updatedPayloadWithVersion, andTimeToLive: timeToLive) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/SmokeDynamoDB/String+DynamoDBKey.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // String+DynamoDBKey.swift 15 | // SmokeDynamoDB 16 | // 17 | 18 | import Foundation 19 | 20 | /// Extension for Arrays of Strings 21 | public extension Array where Element == String { 22 | // Transforms the Array into a Dynamo key - putting dots between each element. 23 | var dynamodbKey: String { 24 | // return all elements joined with dots 25 | return self.joined(separator: ".") 26 | } 27 | 28 | // Transforms an Array into a DynamoDB key prefix - a DynamoDB key with a dot on the end. 29 | var dynamodbKeyPrefix: String { 30 | 31 | let dynamodbKey = self.dynamodbKey 32 | if dynamodbKey.count == 0 { 33 | return "" 34 | } 35 | return dynamodbKey + "." 36 | } 37 | 38 | /** 39 | Returns the provided string with the DynamoDB key (with the trailing 40 | dot) corresponding to this array dropped as a prefix. Returns nil 41 | if the provided string doesn't have the prefix. 42 | */ 43 | func dropAsDynamoDBKeyPrefix(from string: String) -> String? { 44 | let prefix = self.dynamodbKeyPrefix 45 | 46 | guard string.hasPrefix(prefix) else { 47 | return nil 48 | } 49 | 50 | return String(string.dropFirst(prefix.count)) 51 | } 52 | 53 | /** 54 | Transforms the Array into a DynamoDB key - putting dots between each element - with a prefix 55 | element specifying the version. 56 | 57 | - Parameters: 58 | - versionNumber: The version number to prefix. 59 | - minimumFieldWidth: the minimum field width of the version field. Leading 60 | zeros will be padded if required. 61 | */ 62 | func dynamodbKeyWithPrefixedVersion(_ versionNumber: Int, minimumFieldWidth: Int) -> String { 63 | let versionAsString = String(format: "%0\(minimumFieldWidth)d", versionNumber) 64 | return (["v\(versionAsString)"] + self).dynamodbKey 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/SmokeDynamoDB/RowWithItemVersion.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // RowWithItemVersion.swift 15 | // SmokeDynamoDB 16 | // 17 | 18 | import Foundation 19 | 20 | public struct RowWithItemVersion: Codable, CustomRowTypeIdentifier { 21 | 22 | public static var rowTypeIdentifier: String? { 23 | let rowTypeIdentity = getTypeRowIdentifier(type: RowType.self) 24 | 25 | return "\(rowTypeIdentity)WithItemVersion" 26 | } 27 | 28 | enum CodingKeys: String, CodingKey { 29 | case itemVersion = "ItemVersion" 30 | } 31 | 32 | public let itemVersion: Int 33 | public let rowValue: RowType 34 | 35 | public static func newItem(withVersion itemVersion: Int = 1, 36 | withValue rowValue: RowType) -> RowWithItemVersion { 37 | return RowWithItemVersion(itemVersion: itemVersion, 38 | rowValue: rowValue) 39 | } 40 | 41 | public func createUpdatedItem(withVersion itemVersion: Int? = nil, 42 | withValue newRowValue: RowType) -> RowWithItemVersion { 43 | return RowWithItemVersion(itemVersion: itemVersion != nil ? itemVersion! : self.itemVersion + 1, 44 | rowValue: newRowValue) 45 | } 46 | 47 | init(itemVersion: Int, 48 | rowValue: RowType) { 49 | self.itemVersion = itemVersion 50 | self.rowValue = rowValue 51 | } 52 | 53 | public init(from decoder: Decoder) throws { 54 | let values = try decoder.container(keyedBy: CodingKeys.self) 55 | self.itemVersion = try values.decode(Int.self, forKey: .itemVersion) 56 | 57 | self.rowValue = try RowType(from: decoder) 58 | } 59 | 60 | public func encode(to encoder: Encoder) throws { 61 | var container = encoder.container(keyedBy: CodingKeys.self) 62 | try container.encode(itemVersion, forKey: .itemVersion) 63 | 64 | try rowValue.encode(to: encoder) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Tests/SmokeDynamoDBTests/String+DynamoDBKeyTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // String+DynamoDBKeyTests.swift 15 | // SmokeDynamoDBTests 16 | // 17 | 18 | import XCTest 19 | @testable import SmokeDynamoDB 20 | 21 | class StringDynamoDBKeyTests: XCTestCase { 22 | 23 | func testDynamoDBKeyTests() { 24 | XCTAssertEqual([].dynamodbKey, "") 25 | XCTAssertEqual(["one"].dynamodbKey, "one") 26 | XCTAssertEqual(["one", "two"].dynamodbKey, "one.two") 27 | XCTAssertEqual(["one", "two", "three", "four", "five", "six"].dynamodbKey, "one.two.three.four.five.six") 28 | } 29 | 30 | func testDropAsDynamoDBKeyPrefix() { 31 | XCTAssertEqual(["one", "two"].dropAsDynamoDBKeyPrefix(from: "one.two.three.four.five.six")!, 32 | "three.four.five.six") 33 | XCTAssertEqual([].dropAsDynamoDBKeyPrefix(from: "one.two.three.four.five.six")!, 34 | "one.two.three.four.five.six") 35 | XCTAssertEqual(["four", "two"].dropAsDynamoDBKeyPrefix(from: "one.two.three.four.five.six"), nil) 36 | } 37 | 38 | func testDynamoDBKeyPrefixTests() { 39 | XCTAssertEqual([].dynamodbKeyPrefix, "") 40 | XCTAssertEqual(["one"].dynamodbKeyPrefix, "one.") 41 | XCTAssertEqual(["one", "two"].dynamodbKeyPrefix, "one.two.") 42 | XCTAssertEqual(["one", "two", "three", "four", "five", "six"].dynamodbKeyPrefix, "one.two.three.four.five.six.") 43 | } 44 | 45 | func testDynamoDBKeyWithPrefixedVersionTests() { 46 | XCTAssertEqual([].dynamodbKeyWithPrefixedVersion(8, minimumFieldWidth: 5), "v00008") 47 | XCTAssertEqual(["one"].dynamodbKeyWithPrefixedVersion(8, minimumFieldWidth: 5), "v00008.one") 48 | XCTAssertEqual(["one", "two"].dynamodbKeyWithPrefixedVersion(8, minimumFieldWidth: 5), "v00008.one.two") 49 | XCTAssertEqual(["one", "two", "three", "four", "five", "six"].dynamodbKeyWithPrefixedVersion(8, minimumFieldWidth: 5), 50 | "v00008.one.two.three.four.five.six") 51 | 52 | XCTAssertEqual(["one", "two"].dynamodbKeyWithPrefixedVersion(8, minimumFieldWidth: 2), "v08.one.two") 53 | XCTAssertEqual(["one", "two"].dynamodbKeyWithPrefixedVersion(4888, minimumFieldWidth: 2), "v4888.one.two") 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/SmokeDynamoDB/RowWithIndex.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // RowWithIndex.swift 15 | // SmokeDynamoDB 16 | // 17 | 18 | import Foundation 19 | 20 | public protocol IndexIdentity { 21 | static var codingKey: RowWithIndexCodingKey { get } 22 | static var identity: String { get } 23 | } 24 | 25 | public struct RowWithIndexCodingKey: CodingKey { 26 | public var intValue: Int? 27 | public var stringValue: String 28 | 29 | public init?(stringValue: String) { 30 | self.stringValue = stringValue 31 | self.intValue = nil 32 | } 33 | 34 | public init?(intValue: Int) { 35 | return nil 36 | } 37 | 38 | static let index = InternalDynamoDBCodingKey(stringValue: "super")! 39 | } 40 | 41 | public func createRowWithIndexCodingKey(stringValue: String) -> RowWithIndexCodingKey { 42 | return RowWithIndexCodingKey.init(stringValue: stringValue)! 43 | } 44 | 45 | public struct RowWithIndex: Codable, CustomRowTypeIdentifier { 46 | 47 | public static var rowTypeIdentifier: String? { 48 | let rowTypeIdentity = getTypeRowIdentifier(type: RowType.self) 49 | let indexIdentity = Identity.identity 50 | 51 | return "\(rowTypeIdentity)With\(indexIdentity)Index" 52 | } 53 | 54 | public let indexValue: String 55 | public let rowValue: RowType 56 | 57 | public static func newItem(withIndex indexValue: String, 58 | andValue rowValue: RowType) -> RowWithIndex { 59 | return RowWithIndex(indexValue: indexValue, 60 | rowValue: rowValue) 61 | } 62 | 63 | public func createUpdatedItem(withValue newRowValue: RowType) -> RowWithIndex { 64 | return RowWithIndex(indexValue: indexValue, 65 | rowValue: newRowValue) 66 | } 67 | 68 | init(indexValue: String, 69 | rowValue: RowType) { 70 | self.indexValue = indexValue 71 | self.rowValue = rowValue 72 | } 73 | 74 | public init(from decoder: Decoder) throws { 75 | let values = try decoder.container(keyedBy: RowWithIndexCodingKey.self) 76 | self.indexValue = try values.decode(String.self, forKey: Identity.codingKey) 77 | 78 | self.rowValue = try RowType(from: decoder) 79 | } 80 | 81 | public func encode(to encoder: Encoder) throws { 82 | var container = encoder.container(keyedBy: RowWithIndexCodingKey.self) 83 | try container.encode(indexValue, forKey: Identity.codingKey) 84 | 85 | try rowValue.encode(to: encoder) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Sources/SmokeDynamoDB/CompositePrimaryKey.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // CompositePrimaryKey.swift 15 | // SmokeDynamoDB 16 | // 17 | 18 | import Foundation 19 | 20 | public protocol PrimaryKeyAttributes { 21 | static var partitionKeyAttributeName: String { get } 22 | static var sortKeyAttributeName: String { get } 23 | static var indexName: String? { get } 24 | } 25 | 26 | public extension PrimaryKeyAttributes { 27 | static var indexName: String? { 28 | return nil 29 | } 30 | } 31 | 32 | public struct StandardPrimaryKeyAttributes: PrimaryKeyAttributes { 33 | public static var partitionKeyAttributeName: String { 34 | return "PK" 35 | } 36 | public static var sortKeyAttributeName: String { 37 | return "SK" 38 | } 39 | } 40 | 41 | public typealias StandardTypedDatabaseItem = TypedDatabaseItem 42 | public typealias StandardCompositePrimaryKey = CompositePrimaryKey 43 | 44 | struct DynamoDBAttributesTypeCodingKey: CodingKey { 45 | var stringValue: String 46 | var intValue: Int? 47 | 48 | init?(stringValue: String) { 49 | self.stringValue = stringValue 50 | self.intValue = nil 51 | } 52 | 53 | init?(intValue: Int) { 54 | self.stringValue = "\(intValue)" 55 | self.intValue = intValue 56 | } 57 | } 58 | 59 | public struct CompositePrimaryKey: Codable, CustomStringConvertible, Hashable { 60 | public var description: String { 61 | return "CompositePrimaryKey(partitionKey: \(partitionKey), sortKey: \(sortKey))" 62 | } 63 | 64 | public let partitionKey: String 65 | public let sortKey: String 66 | 67 | public init(partitionKey: String, sortKey: String) { 68 | self.partitionKey = partitionKey 69 | self.sortKey = sortKey 70 | } 71 | 72 | public init(from decoder: Decoder) throws { 73 | let values = try decoder.container(keyedBy: DynamoDBAttributesTypeCodingKey.self) 74 | partitionKey = try values.decode(String.self, forKey: DynamoDBAttributesTypeCodingKey(stringValue: AttributesType.partitionKeyAttributeName)!) 75 | sortKey = try values.decode(String.self, forKey: DynamoDBAttributesTypeCodingKey(stringValue: AttributesType.sortKeyAttributeName)!) 76 | } 77 | 78 | public func encode(to encoder: Encoder) throws { 79 | var container = encoder.container(keyedBy: DynamoDBAttributesTypeCodingKey.self) 80 | try container.encode(partitionKey, forKey: DynamoDBAttributesTypeCodingKey(stringValue: AttributesType.partitionKeyAttributeName)!) 81 | try container.encode(sortKey, forKey: DynamoDBAttributesTypeCodingKey(stringValue: AttributesType.sortKeyAttributeName)!) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check [existing open](https://github.com/amzn/smoke-dynamodb/issues), or [recently closed](https://github.com/amzn/smoke-dynamodb/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/amzn/smoke-dynamodb/labels/help%20wanted) issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](https://github.com/amzn/smoke-dynamodb/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /Sources/SmokeDynamoDB/InMemoryDynamoDBCompositePrimaryKeysProjection.swift: -------------------------------------------------------------------------------- 1 | // swiftlint:disable cyclomatic_complexity 2 | // Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"). 5 | // You may not use this file except in compliance with the License. 6 | // A copy of the License is located at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // or in the "license" file accompanying this file. This file is distributed 11 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | // express or implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | // 15 | // InMemoryDynamoDBCompositePrimaryKeysProjection.swift 16 | // SmokeDynamoDB 17 | // 18 | 19 | import Foundation 20 | import SmokeHTTPClient 21 | import DynamoDBModel 22 | import NIO 23 | 24 | public class InMemoryDynamoDBCompositePrimaryKeysProjection: DynamoDBCompositePrimaryKeysProjection { 25 | public var eventLoop: EventLoop 26 | 27 | internal let keysWrapper: InMemoryDynamoDBCompositePrimaryKeysProjectionStore 28 | 29 | public var keys: [Any] { 30 | do { 31 | return try keysWrapper.getKeys(eventLoop: self.eventLoop).wait() 32 | } catch { 33 | fatalError("Unable to retrieve InMemoryDynamoDBCompositePrimaryKeysProjection keys.") 34 | } 35 | } 36 | 37 | public init(keys: [Any] = [], eventLoop: EventLoop) { 38 | self.keysWrapper = InMemoryDynamoDBCompositePrimaryKeysProjectionStore(keys: keys) 39 | self.eventLoop = eventLoop 40 | } 41 | 42 | internal init(eventLoop: EventLoop, 43 | keysWrapper: InMemoryDynamoDBCompositePrimaryKeysProjectionStore) { 44 | self.eventLoop = eventLoop 45 | self.keysWrapper = keysWrapper 46 | } 47 | 48 | public func on(eventLoop: EventLoop) -> InMemoryDynamoDBCompositePrimaryKeysProjection { 49 | return InMemoryDynamoDBCompositePrimaryKeysProjection(eventLoop: eventLoop, 50 | keysWrapper: self.keysWrapper) 51 | } 52 | 53 | public func query(forPartitionKey partitionKey: String, 54 | sortKeyCondition: AttributeCondition?) 55 | -> EventLoopFuture<[CompositePrimaryKey]> { 56 | return keysWrapper.query(forPartitionKey: partitionKey, sortKeyCondition: sortKeyCondition, eventLoop: self.eventLoop) 57 | } 58 | 59 | public func query(forPartitionKey partitionKey: String, 60 | sortKeyCondition: AttributeCondition?, 61 | limit: Int?, 62 | exclusiveStartKey: String?) 63 | -> EventLoopFuture<([CompositePrimaryKey], String?)> 64 | where AttributesType: PrimaryKeyAttributes { 65 | return keysWrapper.query(forPartitionKey: partitionKey, sortKeyCondition: sortKeyCondition, 66 | limit: limit, exclusiveStartKey: exclusiveStartKey, eventLoop: self.eventLoop) 67 | } 68 | 69 | public func query(forPartitionKey partitionKey: String, 70 | sortKeyCondition: AttributeCondition?, 71 | limit: Int?, 72 | scanIndexForward: Bool, 73 | exclusiveStartKey: String?) 74 | -> EventLoopFuture<([CompositePrimaryKey], String?)> 75 | where AttributesType: PrimaryKeyAttributes { 76 | return keysWrapper.query(forPartitionKey: partitionKey, sortKeyCondition: sortKeyCondition, 77 | limit: limit, scanIndexForward: scanIndexForward, 78 | exclusiveStartKey: exclusiveStartKey, eventLoop: self.eventLoop) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Tests/SmokeDynamoDBTests/TestConfiguration.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // TestConfiguration.swift 15 | // SmokeDynamoDBTests 16 | // 17 | 18 | import Foundation 19 | @testable import SmokeDynamoDB 20 | 21 | struct TestTypeA: Codable, Equatable { 22 | let firstly: String 23 | let secondly: String 24 | } 25 | 26 | struct TestTypeB: Codable, Equatable, CustomRowTypeIdentifier { 27 | static var rowTypeIdentifier: String? = "TypeBCustom" 28 | 29 | let thirdly: String 30 | let fourthly: String 31 | } 32 | 33 | struct TestTypeC: Codable { 34 | let theString: String? 35 | let theNumber: Int? 36 | let theStruct: TestTypeA? 37 | let theList: [String]? 38 | 39 | init(theString: String?, theNumber: Int?, theStruct: TestTypeA?, theList: [String]?) { 40 | self.theString = theString 41 | self.theNumber = theNumber 42 | self.theStruct = theStruct 43 | self.theList = theList 44 | } 45 | } 46 | 47 | enum TestQueryableTypes: PolymorphicOperationReturnType { 48 | typealias AttributesType = StandardPrimaryKeyAttributes 49 | 50 | static var types: [(Codable.Type, PolymorphicOperationReturnOption)] = [ 51 | (TestTypeA.self, .init( {.testTypeA($0)} )), 52 | (TestTypeB.self, .init( {.testTypeB($0)} )), 53 | ] 54 | 55 | case testTypeA(StandardTypedDatabaseItem) 56 | case testTypeB(StandardTypedDatabaseItem) 57 | } 58 | 59 | extension TestQueryableTypes: BatchCapableReturnType { 60 | func getItemKey() -> CompositePrimaryKey { 61 | switch self { 62 | case .testTypeA(let databaseItem): 63 | return databaseItem.compositePrimaryKey 64 | case .testTypeB(let databaseItem): 65 | return databaseItem.compositePrimaryKey 66 | } 67 | } 68 | } 69 | 70 | typealias TestTypeAWriteEntry = StandardWriteEntry 71 | typealias TestTypeBWriteEntry = StandardWriteEntry 72 | typealias TestTypeAStandardTransactionConstraintEntry = StandardTransactionConstraintEntry 73 | typealias TestTypeBStandardTransactionConstraintEntry = StandardTransactionConstraintEntry 74 | 75 | enum TestPolymorphicWriteEntry: PolymorphicWriteEntry { 76 | case testTypeA(TestTypeAWriteEntry) 77 | case testTypeB(TestTypeBWriteEntry) 78 | 79 | func handle(context: Context) throws -> Context.WriteEntryTransformType { 80 | switch self { 81 | case .testTypeA(let writeEntry): 82 | return try context.transform(writeEntry) 83 | case .testTypeB(let writeEntry): 84 | return try context.transform(writeEntry) 85 | } 86 | } 87 | } 88 | 89 | enum TestPolymorphicTransactionConstraintEntry: PolymorphicTransactionConstraintEntry { 90 | case testTypeA(TestTypeAStandardTransactionConstraintEntry) 91 | case testTypeB(TestTypeBStandardTransactionConstraintEntry) 92 | 93 | func handle(context: Context) throws -> Context.WriteTransactionConstraintType { 94 | switch self { 95 | case .testTypeA(let writeEntry): 96 | return try context.transform(writeEntry) 97 | case .testTypeB(let writeEntry): 98 | return try context.transform(writeEntry) 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Tests/SmokeDynamoDBTests/DynamoDBEncoderDecoderTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // DynamoDBEncoderDecoderTests.swift 15 | // SmokeDynamoDBTests 16 | // 17 | 18 | import XCTest 19 | @testable import SmokeDynamoDB 20 | 21 | private let dynamodbEncoder = DynamoDBEncoder() 22 | private let dynamodbDecoder = DynamoDBDecoder() 23 | 24 | struct CoreAccountAttributes: Codable { 25 | var description: String 26 | var mappedValues: [String: String] 27 | var notificationTargets: NotificationTargets 28 | } 29 | 30 | extension CoreAccountAttributes: Equatable { 31 | static func ==(lhs: CoreAccountAttributes, rhs: CoreAccountAttributes) -> Bool { 32 | return lhs.description == rhs.description && lhs.notificationTargets == rhs.notificationTargets 33 | && lhs.mappedValues == rhs.mappedValues 34 | } 35 | } 36 | 37 | struct NotificationTargets: Codable { 38 | var currentIDs: [String] 39 | var maximum: Int 40 | } 41 | 42 | extension NotificationTargets: Equatable { 43 | static func ==(lhs: NotificationTargets, rhs: NotificationTargets) -> Bool { 44 | return lhs.currentIDs == rhs.currentIDs && lhs.maximum == rhs.maximum 45 | } 46 | } 47 | 48 | typealias DatabaseItemType = StandardTypedDatabaseItem 49 | 50 | class DynamoDBEncoderDecoderTests: XCTestCase { 51 | let partitionKey = "partitionKey" 52 | let sortKey = "sortKey" 53 | let attributes = CoreAccountAttributes( 54 | description: "Description", 55 | mappedValues: ["A": "one", "B": "two"], 56 | notificationTargets: NotificationTargets(currentIDs: [], maximum: 20)) 57 | 58 | func testEncoderDecoder() { 59 | // create key and database item to create 60 | let key = StandardCompositePrimaryKey(partitionKey: partitionKey, sortKey: sortKey) 61 | let newDatabaseItem: DatabaseItemType = StandardTypedDatabaseItem.newItem(withKey: key, andValue: attributes) 62 | 63 | let encodedAttributeValue = try! dynamodbEncoder.encode(newDatabaseItem) 64 | 65 | let output: DatabaseItemType = try! dynamodbDecoder.decode(encodedAttributeValue) 66 | 67 | XCTAssertEqual(newDatabaseItem.rowValue, output.rowValue) 68 | XCTAssertEqual(partitionKey, output.compositePrimaryKey.partitionKey) 69 | XCTAssertEqual(sortKey, output.compositePrimaryKey.sortKey) 70 | XCTAssertEqual(attributes, output.rowValue) 71 | XCTAssertNil(output.timeToLive) 72 | } 73 | 74 | func testEncoderDecoderWithTimeToLive() { 75 | let timeToLiveTimestamp: Int64 = 123456789 76 | let timeToLive = StandardTimeToLive(timeToLiveTimestamp: timeToLiveTimestamp) 77 | 78 | // create key and database item to create 79 | let key = StandardCompositePrimaryKey(partitionKey: partitionKey, sortKey: sortKey) 80 | let newDatabaseItem: DatabaseItemType = StandardTypedDatabaseItem.newItem( 81 | withKey: key, 82 | andValue: attributes, 83 | andTimeToLive: timeToLive) 84 | 85 | let encodedAttributeValue = try! dynamodbEncoder.encode(newDatabaseItem) 86 | 87 | let output: DatabaseItemType = try! dynamodbDecoder.decode(encodedAttributeValue) 88 | 89 | XCTAssertEqual(newDatabaseItem.rowValue, output.rowValue) 90 | XCTAssertEqual(partitionKey, output.compositePrimaryKey.partitionKey) 91 | XCTAssertEqual(sortKey, output.compositePrimaryKey.sortKey) 92 | XCTAssertEqual(attributes, output.rowValue) 93 | XCTAssertEqual(timeToLiveTimestamp, output.timeToLive?.timeToLiveTimestamp) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Sources/SmokeDynamoDB/PolymorphicOperationReturnType.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // PolymorphicOperationReturnType.swift 15 | // SmokeDynamoDB 16 | // 17 | import Foundation 18 | import SmokeHTTPClient 19 | import DynamoDBModel 20 | 21 | public protocol BatchCapableReturnType { 22 | associatedtype AttributesType: PrimaryKeyAttributes 23 | 24 | func getItemKey() -> CompositePrimaryKey 25 | } 26 | 27 | public protocol PolymorphicOperationReturnType { 28 | associatedtype AttributesType: PrimaryKeyAttributes 29 | 30 | static var types: [(Codable.Type, PolymorphicOperationReturnOption)] { get } 31 | } 32 | 33 | public struct PolymorphicOperationReturnOption { 34 | private let decodingPayloadHandler: (Decoder) throws -> ReturnType 35 | private let typeConvertingPayloadHander: (Any) throws -> ReturnType 36 | 37 | public init( 38 | _ payloadHandler: @escaping (TypedDatabaseItem) -> ReturnType) { 39 | func newDecodingPayloadHandler(decoder: Decoder) throws -> ReturnType { 40 | let typedDatabaseItem: TypedDatabaseItem = try TypedDatabaseItem(from: decoder) 41 | 42 | return payloadHandler(typedDatabaseItem) 43 | } 44 | 45 | func newTypeConvertingPayloadHandler(input: Any) throws -> ReturnType { 46 | guard let typedDatabaseItem = input as? TypedDatabaseItem else { 47 | let description = "Expected to use item type \(TypedDatabaseItem.self)." 48 | let context = DecodingError.Context(codingPath: [], debugDescription: description) 49 | throw DecodingError.typeMismatch(TypedDatabaseItem.self, context) 50 | } 51 | 52 | return payloadHandler(typedDatabaseItem) 53 | } 54 | 55 | self.decodingPayloadHandler = newDecodingPayloadHandler 56 | self.typeConvertingPayloadHander = newTypeConvertingPayloadHandler 57 | } 58 | 59 | internal func getReturnType(from decoder: Decoder) throws -> ReturnType { 60 | return try self.decodingPayloadHandler(decoder) 61 | } 62 | 63 | internal func getReturnType(input: Any) throws -> ReturnType { 64 | return try self.typeConvertingPayloadHander(input) 65 | } 66 | } 67 | 68 | internal struct ReturnTypeDecodable: Decodable { 69 | public let decodedValue: ReturnType 70 | 71 | enum CodingKeys: String, CodingKey { 72 | case rowType = "RowType" 73 | } 74 | 75 | init(decodedValue: ReturnType) { 76 | self.decodedValue = decodedValue 77 | } 78 | 79 | public init(from decoder: Decoder) throws { 80 | let values = try decoder.container(keyedBy: CodingKeys.self) 81 | let storedRowTypeName = try values.decode(String.self, forKey: .rowType) 82 | 83 | var queryableTypeProviders: [String: PolymorphicOperationReturnOption] = [:] 84 | ReturnType.types.forEach { (type, provider) in 85 | queryableTypeProviders[getTypeRowIdentifier(type: type)] = provider 86 | } 87 | 88 | if let provider = queryableTypeProviders[storedRowTypeName] { 89 | self.decodedValue = try provider.getReturnType(from: decoder) 90 | } else { 91 | // throw an exception, we don't know what this type is 92 | throw SmokeDynamoDBError.unexpectedType(provided: storedRowTypeName) 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Sources/SmokeDynamoDB/PolymorphicWriteEntry.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // PolymorphicWriteEntry.swift 15 | // SmokeDynamoDB 16 | // 17 | 18 | import DynamoDBModel 19 | 20 | // Conforming types are provided by the Table implementation to convert a `WriteEntry` into 21 | // something the table can use to perform the write. 22 | public protocol PolymorphicWriteEntryTransform { 23 | associatedtype TableType 24 | 25 | init(_ entry: WriteEntry, table: TableType) throws 26 | } 27 | 28 | // Conforming types are provided by the Table implementation to convert a `WriteEntry` into 29 | // something the table can use to achieve the constraint. 30 | public protocol PolymorphicTransactionConstraintTransform { 31 | associatedtype TableType 32 | 33 | init(_ entry: TransactionConstraintEntry, table: TableType) throws 34 | } 35 | 36 | // Conforming types are provided by the application to express the different possible write entries 37 | // and how they can be converted to the table-provided transform type. 38 | public protocol PolymorphicWriteEntry { 39 | 40 | func handle(context: Context) throws -> Context.WriteEntryTransformType 41 | 42 | var compositePrimaryKey: StandardCompositePrimaryKey? { get } 43 | } 44 | 45 | public extension PolymorphicWriteEntry { 46 | var compositePrimaryKey: StandardCompositePrimaryKey? { 47 | return nil 48 | } 49 | } 50 | 51 | public typealias StandardTransactionConstraintEntry = TransactionConstraintEntry 52 | 53 | public enum TransactionConstraintEntry { 54 | case required(existing: TypedDatabaseItem) 55 | } 56 | 57 | // Conforming types are provided by the application to express the different possible constraint entries 58 | // and how they can be converted to the table-provided transform type. 59 | public protocol PolymorphicTransactionConstraintEntry { 60 | 61 | func handle(context: Context) throws -> Context.WriteTransactionConstraintType 62 | 63 | var compositePrimaryKey: StandardCompositePrimaryKey? { get } 64 | } 65 | 66 | public extension PolymorphicTransactionConstraintEntry { 67 | var compositePrimaryKey: StandardCompositePrimaryKey? { 68 | return nil 69 | } 70 | } 71 | 72 | public struct EmptyPolymorphicTransactionConstraintEntry: PolymorphicTransactionConstraintEntry { 73 | public func handle(context: Context) throws -> Context.WriteTransactionConstraintType { 74 | fatalError("There are no items to transform") 75 | } 76 | } 77 | 78 | // Helper Context type that enables transforming Write Entries into the table-provided transform type. 79 | public protocol PolymorphicWriteEntryContext { 80 | associatedtype WriteEntryTransformType: PolymorphicWriteEntryTransform 81 | associatedtype WriteTransactionConstraintType: PolymorphicTransactionConstraintTransform 82 | 83 | func transform(_ entry: WriteEntry) throws 84 | -> WriteEntryTransformType 85 | 86 | func transform(_ entry: TransactionConstraintEntry) throws 87 | -> WriteTransactionConstraintType 88 | } 89 | 90 | public struct StandardPolymorphicWriteEntryContext: PolymorphicWriteEntryContext 92 | where WriteEntryTransformType.TableType == WriteTransactionConstraintType.TableType { 93 | public typealias TableType = WriteEntryTransformType.TableType 94 | 95 | private let table: TableType 96 | 97 | public init(table: TableType) { 98 | self.table = table 99 | } 100 | 101 | public func transform(_ entry: WriteEntry) throws 102 | -> WriteEntryTransformType { 103 | return try .init(entry, table: self.table) 104 | } 105 | 106 | public func transform(_ entry: TransactionConstraintEntry) throws 107 | -> WriteTransactionConstraintType { 108 | return try .init(entry, table: self.table) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Sources/SmokeDynamoDB/QueryInput+forSortKeyCondition.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // QueryInput+forSortKeyCondition.swift 15 | // SmokeDynamoDB 16 | // 17 | 18 | import Foundation 19 | import DynamoDBModel 20 | 21 | extension QueryInput { 22 | internal static func forSortKeyCondition(partitionKey: String, 23 | targetTableName: String, 24 | primaryKeyType: AttributesType.Type, 25 | sortKeyCondition: AttributeCondition?, 26 | limit: Int?, 27 | scanIndexForward: Bool, 28 | exclusiveStartKey: String?, 29 | consistentRead: Bool?) throws 30 | -> DynamoDBModel.QueryInput where AttributesType: PrimaryKeyAttributes { 31 | let expressionAttributeValues: [String: DynamoDBModel.AttributeValue] 32 | let expressionAttributeNames: [String: String] 33 | let keyConditionExpression: String 34 | if let currentSortKeyCondition = sortKeyCondition { 35 | var withSortConditionAttributeValues: [String: DynamoDBModel.AttributeValue] = [ 36 | ":pk": DynamoDBModel.AttributeValue(S: partitionKey)] 37 | 38 | let sortKeyExpression: String 39 | switch currentSortKeyCondition { 40 | case .equals(let value): 41 | withSortConditionAttributeValues[":sortkeyval"] = DynamoDBModel.AttributeValue(S: value) 42 | sortKeyExpression = "#sk = :sortkeyval" 43 | case .lessThan(let value): 44 | withSortConditionAttributeValues[":sortkeyval"] = DynamoDBModel.AttributeValue(S: value) 45 | sortKeyExpression = "#sk < :sortkeyval" 46 | case .lessThanOrEqual(let value): 47 | withSortConditionAttributeValues[":sortkeyval"] = DynamoDBModel.AttributeValue(S: value) 48 | sortKeyExpression = "#sk <= :sortkeyval" 49 | case .greaterThan(let value): 50 | withSortConditionAttributeValues[":sortkeyval"] = DynamoDBModel.AttributeValue(S: value) 51 | sortKeyExpression = "#sk > :sortkeyval" 52 | case .greaterThanOrEqual(let value): 53 | withSortConditionAttributeValues[":sortkeyval"] = DynamoDBModel.AttributeValue(S: value) 54 | sortKeyExpression = "#sk >= :sortkeyval" 55 | case .between(let value1, let value2): 56 | withSortConditionAttributeValues[":sortkeyval1"] = DynamoDBModel.AttributeValue(S: value1) 57 | withSortConditionAttributeValues[":sortkeyval2"] = DynamoDBModel.AttributeValue(S: value2) 58 | sortKeyExpression = "#sk BETWEEN :sortkeyval1 AND :sortkeyval2" 59 | case .beginsWith(let value): 60 | withSortConditionAttributeValues[":sortkeyval"] = DynamoDBModel.AttributeValue(S: value) 61 | sortKeyExpression = "begins_with ( #sk, :sortkeyval )" 62 | } 63 | 64 | keyConditionExpression = "#pk= :pk AND \(sortKeyExpression)" 65 | 66 | expressionAttributeNames = ["#pk": AttributesType.partitionKeyAttributeName, 67 | "#sk": AttributesType.sortKeyAttributeName] 68 | expressionAttributeValues = withSortConditionAttributeValues 69 | } else { 70 | keyConditionExpression = "#pk= :pk" 71 | 72 | expressionAttributeNames = ["#pk": AttributesType.partitionKeyAttributeName] 73 | expressionAttributeValues = [":pk": DynamoDBModel.AttributeValue(S: partitionKey)] 74 | } 75 | 76 | let inputExclusiveStartKey: [String: DynamoDBModel.AttributeValue]? 77 | if let exclusiveStartKey = exclusiveStartKey?.data(using: .utf8) { 78 | inputExclusiveStartKey = try JSONDecoder().decode([String: DynamoDBModel.AttributeValue].self, 79 | from: exclusiveStartKey) 80 | } else { 81 | inputExclusiveStartKey = nil 82 | } 83 | 84 | return DynamoDBModel.QueryInput(consistentRead: consistentRead, 85 | exclusiveStartKey: inputExclusiveStartKey, 86 | expressionAttributeNames: expressionAttributeNames, 87 | expressionAttributeValues: expressionAttributeValues, 88 | indexName: primaryKeyType.indexName, 89 | keyConditionExpression: keyConditionExpression, 90 | limit: limit, 91 | scanIndexForward: scanIndexForward, 92 | tableName: targetTableName) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Tests/SmokeDynamoDBTests/DynamoDBCompositePrimaryKeyTableClobberVersionedItemWithHistoricalRowTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // DynamoDBCompositePrimaryKeyTableClobberVersionedItemWithHistoricalRowTests.swift 15 | // SmokeDynamoDBTests 16 | // 17 | 18 | import Foundation 19 | import XCTest 20 | @testable import SmokeDynamoDB 21 | import NIO 22 | 23 | class DynamoDBCompositePrimaryKeyTableClobberVersionedItemWithHistoricalRowTests: XCTestCase { 24 | var eventLoopGroup: EventLoopGroup? 25 | var eventLoop: EventLoop! 26 | 27 | override func setUp() { 28 | super.setUp() 29 | 30 | let newEventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) 31 | eventLoop = newEventLoopGroup.next() 32 | eventLoopGroup = newEventLoopGroup 33 | } 34 | 35 | override func tearDown() { 36 | super.tearDown() 37 | 38 | try? eventLoopGroup?.syncShutdownGracefully() 39 | eventLoop = nil 40 | } 41 | 42 | func testClobberVersionedItemWithHistoricalRow() throws { 43 | let payload1 = TestTypeA(firstly: "firstly", secondly: "secondly") 44 | let partitionKey = "partitionId" 45 | let historicalPartitionPrefix = "historical" 46 | let historicalPartitionKey = "\(historicalPartitionPrefix).\(partitionKey)" 47 | 48 | func generateSortKey(withVersion version: Int) -> String { 49 | let prefix = String(format: "v%05d", version) 50 | return [prefix, "sortId"].dynamodbKey 51 | } 52 | 53 | let table = InMemoryDynamoDBCompositePrimaryKeyTable(eventLoop: eventLoop) 54 | 55 | try table.clobberVersionedItemWithHistoricalRow(forPrimaryKey: partitionKey, 56 | andHistoricalKey: historicalPartitionKey, 57 | item: payload1, 58 | primaryKeyType: StandardPrimaryKeyAttributes.self, 59 | generateSortKey: generateSortKey).wait() 60 | 61 | // the v0 row, copy of version 1 62 | let key1 = StandardCompositePrimaryKey(partitionKey: partitionKey, sortKey: generateSortKey(withVersion: 0)) 63 | let item1: StandardTypedDatabaseItem> = try table.getItem(forKey: key1).wait()! 64 | XCTAssertEqual(1, item1.rowValue.itemVersion) 65 | XCTAssertEqual(1, item1.rowStatus.rowVersion) 66 | XCTAssertEqual(payload1, item1.rowValue.rowValue) 67 | 68 | // the v1 row, has version 1 69 | let key2 = StandardCompositePrimaryKey(partitionKey: historicalPartitionKey, sortKey: generateSortKey(withVersion: 1)) 70 | let item2: StandardTypedDatabaseItem> = try table.getItem(forKey: key2).wait()! 71 | XCTAssertEqual(1, item2.rowValue.itemVersion) 72 | XCTAssertEqual(1, item2.rowStatus.rowVersion) 73 | XCTAssertEqual(payload1, item2.rowValue.rowValue) 74 | 75 | let payload2 = TestTypeA(firstly: "thirdly", secondly: "fourthly") 76 | 77 | try table.clobberVersionedItemWithHistoricalRow(forPrimaryKey: partitionKey, 78 | andHistoricalKey: historicalPartitionKey, 79 | item: payload2, 80 | primaryKeyType: StandardPrimaryKeyAttributes.self, 81 | generateSortKey: generateSortKey).wait() 82 | 83 | // the v0 row, copy of version 2 84 | let key3 = StandardCompositePrimaryKey(partitionKey: partitionKey, sortKey: generateSortKey(withVersion: 0)) 85 | let item3: StandardTypedDatabaseItem> = try table.getItem(forKey: key3).wait()! 86 | XCTAssertEqual(2, item3.rowValue.itemVersion) 87 | XCTAssertEqual(2, item3.rowStatus.rowVersion) 88 | XCTAssertEqual(payload2, item3.rowValue.rowValue) 89 | 90 | // the v1 row, still has version 1 91 | let key4 = StandardCompositePrimaryKey(partitionKey: historicalPartitionKey, sortKey: generateSortKey(withVersion: 1)) 92 | let item4: StandardTypedDatabaseItem> = try table.getItem(forKey: key4).wait()! 93 | XCTAssertEqual(1, item4.rowValue.itemVersion) 94 | XCTAssertEqual(1, item4.rowStatus.rowVersion) 95 | XCTAssertEqual(payload1, item4.rowValue.rowValue) 96 | 97 | // the v2 row, has version 2 98 | let key5 = StandardCompositePrimaryKey(partitionKey: historicalPartitionKey, sortKey: generateSortKey(withVersion: 2)) 99 | let item5: StandardTypedDatabaseItem> = try table.getItem(forKey: key5).wait()! 100 | XCTAssertEqual(2, item5.rowValue.itemVersion) 101 | XCTAssertEqual(1, item5.rowStatus.rowVersion) 102 | XCTAssertEqual(payload2, item5.rowValue.rowValue) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Sources/SmokeDynamoDB/InternalUnkeyedEncodingContainer.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // InternalUnkeyedEncodingContainer.swift 15 | // SmokeDynamoDB 16 | // 17 | 18 | import Foundation 19 | import DynamoDBModel 20 | 21 | internal struct InternalUnkeyedEncodingContainer: UnkeyedEncodingContainer { 22 | private let enclosingContainer: InternalSingleValueEncodingContainer 23 | 24 | init(enclosingContainer: InternalSingleValueEncodingContainer) { 25 | self.enclosingContainer = enclosingContainer 26 | } 27 | 28 | // MARK: - Swift.UnkeyedEncodingContainer Methods 29 | 30 | var codingPath: [CodingKey] { 31 | return enclosingContainer.codingPath 32 | } 33 | 34 | var count: Int { return enclosingContainer.unkeyedContainerCount } 35 | 36 | func encodeNil() throws { 37 | enclosingContainer.addToUnkeyedContainer(value: DynamoDBModel.AttributeValue(NULL: true)) } 38 | 39 | func encode(_ value: Bool) throws { 40 | enclosingContainer.addToUnkeyedContainer(value: DynamoDBModel.AttributeValue(BOOL: value)) } 41 | 42 | func encode(_ value: Int) throws { 43 | enclosingContainer.addToUnkeyedContainer(value: DynamoDBModel.AttributeValue(N: String(value))) 44 | } 45 | 46 | func encode(_ value: Int8) throws { 47 | enclosingContainer.addToUnkeyedContainer(value: DynamoDBModel.AttributeValue(N: String(value))) 48 | } 49 | 50 | func encode(_ value: Int16) throws { 51 | enclosingContainer.addToUnkeyedContainer(value: DynamoDBModel.AttributeValue(N: String(value))) 52 | } 53 | 54 | func encode(_ value: Int32) throws { 55 | enclosingContainer.addToUnkeyedContainer(value: DynamoDBModel.AttributeValue(N: String(value))) 56 | } 57 | 58 | func encode(_ value: Int64) throws { 59 | enclosingContainer.addToUnkeyedContainer(value: DynamoDBModel.AttributeValue(N: String(value))) 60 | } 61 | 62 | func encode(_ value: UInt) throws { 63 | enclosingContainer.addToUnkeyedContainer(value: DynamoDBModel.AttributeValue(N: String(value))) 64 | } 65 | 66 | func encode(_ value: UInt8) throws { 67 | enclosingContainer.addToUnkeyedContainer(value: DynamoDBModel.AttributeValue(N: String(value))) 68 | } 69 | 70 | func encode(_ value: UInt16) throws { 71 | enclosingContainer.addToUnkeyedContainer(value: DynamoDBModel.AttributeValue(N: String(value))) 72 | } 73 | 74 | func encode(_ value: UInt32) throws { 75 | enclosingContainer.addToUnkeyedContainer(value: DynamoDBModel.AttributeValue(N: String(value))) 76 | } 77 | 78 | func encode(_ value: UInt64) throws { 79 | enclosingContainer.addToUnkeyedContainer(value: DynamoDBModel.AttributeValue(N: String(value))) 80 | } 81 | 82 | func encode(_ value: Float) throws { 83 | enclosingContainer.addToUnkeyedContainer(value: DynamoDBModel.AttributeValue(N: String(value))) 84 | } 85 | 86 | func encode(_ value: Double) throws { 87 | enclosingContainer.addToUnkeyedContainer(value: DynamoDBModel.AttributeValue(N: String(value))) 88 | } 89 | 90 | func encode(_ value: String) throws { 91 | enclosingContainer.addToUnkeyedContainer(value: DynamoDBModel.AttributeValue(S: value)) 92 | } 93 | 94 | func encode(_ value: T) throws where T: Encodable { 95 | try createNestedContainer().encode(value) 96 | } 97 | 98 | func nestedContainer(keyedBy type: NestedKey.Type) -> KeyedEncodingContainer { 99 | let nestedContainer = createNestedContainer(defaultValue: .keyedContainer([:])) 100 | 101 | let nestedKeyContainer = InternalKeyedEncodingContainer(enclosingContainer: nestedContainer) 102 | 103 | return KeyedEncodingContainer(nestedKeyContainer) 104 | } 105 | 106 | func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { 107 | let nestedContainer = createNestedContainer(defaultValue: .unkeyedContainer([])) 108 | 109 | let nestedKeyContainer = InternalUnkeyedEncodingContainer(enclosingContainer: nestedContainer) 110 | 111 | return nestedKeyContainer 112 | } 113 | 114 | func superEncoder() -> Encoder { return createNestedContainer() } 115 | 116 | // MARK: - 117 | 118 | private func createNestedContainer(defaultValue: ContainerValueType? = nil) 119 | -> InternalSingleValueEncodingContainer { 120 | let index = enclosingContainer.unkeyedContainerCount 121 | 122 | let nestedContainer = InternalSingleValueEncodingContainer(userInfo: enclosingContainer.userInfo, 123 | codingPath: enclosingContainer.codingPath + [InternalDynamoDBCodingKey(index: index)], 124 | attributeNameTransform: enclosingContainer.attributeNameTransform, 125 | defaultValue: defaultValue) 126 | enclosingContainer.addToUnkeyedContainer(value: nestedContainer) 127 | 128 | return nestedContainer 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "async-http-client", 6 | "repositoryURL": "https://github.com/swift-server/async-http-client.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "7f05a8da46cc2a4ab43218722298b81ac7a08031", 10 | "version": "1.13.2" 11 | } 12 | }, 13 | { 14 | "package": "CollectionConcurrencyKit", 15 | "repositoryURL": "https://github.com/JohnSundell/CollectionConcurrencyKit", 16 | "state": { 17 | "branch": null, 18 | "revision": "b4f23e24b5a1bff301efc5e70871083ca029ff95", 19 | "version": "0.2.0" 20 | } 21 | }, 22 | { 23 | "package": "smoke-aws", 24 | "repositoryURL": "https://github.com/amzn/smoke-aws.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "2ab12570ff4e45b41f3c1adf9204baf57d9c0147", 28 | "version": "3.0.0" 29 | } 30 | }, 31 | { 32 | "package": "smoke-aws-support", 33 | "repositoryURL": "https://github.com/amzn/smoke-aws-support.git", 34 | "state": { 35 | "branch": null, 36 | "revision": "b858670eac000b3a957d85672b915c03275c0d9b", 37 | "version": "2.0.0" 38 | } 39 | }, 40 | { 41 | "package": "smoke-http", 42 | "repositoryURL": "https://github.com/amzn/smoke-http.git", 43 | "state": { 44 | "branch": null, 45 | "revision": "a6f386fc42d4758719b01a4e45f20c68bed88d50", 46 | "version": "3.0.0" 47 | } 48 | }, 49 | { 50 | "package": "swift-atomics", 51 | "repositoryURL": "https://github.com/apple/swift-atomics.git", 52 | "state": { 53 | "branch": null, 54 | "revision": "ff3d2212b6b093db7f177d0855adbc4ef9c5f036", 55 | "version": "1.0.3" 56 | } 57 | }, 58 | { 59 | "package": "swift-collections", 60 | "repositoryURL": "https://github.com/apple/swift-collections.git", 61 | "state": { 62 | "branch": null, 63 | "revision": "937e904258d22af6e447a0b72c0bc67583ef64a2", 64 | "version": "1.0.4" 65 | } 66 | }, 67 | { 68 | "package": "swift-crypto", 69 | "repositoryURL": "https://github.com/apple/swift-crypto.git", 70 | "state": { 71 | "branch": null, 72 | "revision": "ddb07e896a2a8af79512543b1c7eb9797f8898a5", 73 | "version": "1.1.7" 74 | } 75 | }, 76 | { 77 | "package": "swift-distributed-tracing", 78 | "repositoryURL": "https://github.com/apple/swift-distributed-tracing.git", 79 | "state": { 80 | "branch": null, 81 | "revision": "11c756c5c4d7de0eeed8595695cadd7fa107aa19", 82 | "version": "1.1.1" 83 | } 84 | }, 85 | { 86 | "package": "swift-log", 87 | "repositoryURL": "https://github.com/apple/swift-log.git", 88 | "state": { 89 | "branch": null, 90 | "revision": "32e8d724467f8fe623624570367e3d50c5638e46", 91 | "version": "1.5.2" 92 | } 93 | }, 94 | { 95 | "package": "swift-metrics", 96 | "repositoryURL": "https://github.com/apple/swift-metrics.git", 97 | "state": { 98 | "branch": null, 99 | "revision": "e8bced74bc6d747745935e469f45d03f048d6cbd", 100 | "version": "2.3.4" 101 | } 102 | }, 103 | { 104 | "package": "swift-nio", 105 | "repositoryURL": "https://github.com/apple/swift-nio.git", 106 | "state": { 107 | "branch": null, 108 | "revision": "45167b8006448c79dda4b7bd604e07a034c15c49", 109 | "version": "2.48.0" 110 | } 111 | }, 112 | { 113 | "package": "swift-nio-extras", 114 | "repositoryURL": "https://github.com/apple/swift-nio-extras.git", 115 | "state": { 116 | "branch": null, 117 | "revision": "98378d1fe56527761c180f70b2d66a7b2307fc39", 118 | "version": "1.16.0" 119 | } 120 | }, 121 | { 122 | "package": "swift-nio-http2", 123 | "repositoryURL": "https://github.com/apple/swift-nio-http2.git", 124 | "state": { 125 | "branch": null, 126 | "revision": "22757ac305f3d44d2b99ba541193ff1d64e77d00", 127 | "version": "1.24.1" 128 | } 129 | }, 130 | { 131 | "package": "swift-nio-ssl", 132 | "repositoryURL": "https://github.com/apple/swift-nio-ssl.git", 133 | "state": { 134 | "branch": null, 135 | "revision": "4fb7ead803e38949eb1d6fabb849206a72c580f3", 136 | "version": "2.23.0" 137 | } 138 | }, 139 | { 140 | "package": "swift-nio-transport-services", 141 | "repositoryURL": "https://github.com/apple/swift-nio-transport-services.git", 142 | "state": { 143 | "branch": null, 144 | "revision": "c0d9a144cfaec8d3d596aadde3039286a266c15c", 145 | "version": "1.15.0" 146 | } 147 | }, 148 | { 149 | "package": "swift-service-context", 150 | "repositoryURL": "https://github.com/apple/swift-service-context.git", 151 | "state": { 152 | "branch": null, 153 | "revision": "ce0141c8f123132dbd02fd45fea448018762df1b", 154 | "version": "1.0.0" 155 | } 156 | }, 157 | { 158 | "package": "XMLCoding", 159 | "repositoryURL": "https://github.com/LiveUI/XMLCoding.git", 160 | "state": { 161 | "branch": null, 162 | "revision": "f0fbfe17e73f329e13a6133ff5437f7b174049fd", 163 | "version": "0.4.1" 164 | } 165 | } 166 | ] 167 | }, 168 | "version": 1 169 | } 170 | -------------------------------------------------------------------------------- /Tests/SmokeDynamoDBTests/SmokeDynamoDBTestInput.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // TestInput.swift 15 | // SmokeDynamoDBTests 16 | // 17 | import Foundation 18 | @testable import SmokeDynamoDB 19 | 20 | enum AllQueryableTypes: PolymorphicOperationReturnType { 21 | typealias AttributesType = StandardPrimaryKeyAttributes 22 | 23 | static var types: [(Codable.Type, PolymorphicOperationReturnOption)] = [ 24 | (TypeA.self, .init( {.typeA($0)} )), 25 | (TypeB.self, .init( {.typeB($0)} )), 26 | ] 27 | 28 | case typeA(StandardTypedDatabaseItem) 29 | case typeB(StandardTypedDatabaseItem) 30 | } 31 | 32 | enum SomeQueryableTypes: PolymorphicOperationReturnType { 33 | typealias AttributesType = StandardPrimaryKeyAttributes 34 | 35 | static var types: [(Codable.Type, PolymorphicOperationReturnOption)] = [ 36 | (TypeA.self, .init( {.typeA($0)} )), 37 | ] 38 | 39 | case typeA(StandardTypedDatabaseItem) 40 | } 41 | 42 | struct GSI1PKIndexIdentity : IndexIdentity { 43 | static var codingKey = createRowWithIndexCodingKey(stringValue: "GSI-1-PK") 44 | static var identity = "GSI1PK" 45 | } 46 | 47 | enum AllQueryableTypesWithIndex: PolymorphicOperationReturnType { 48 | typealias AttributesType = StandardPrimaryKeyAttributes 49 | 50 | static var types: [(Codable.Type, PolymorphicOperationReturnOption)] = [ 51 | (RowWithIndex.self, .init( {.typeAWithIndex($0)} )), 52 | (TypeB.self, .init( {.typeB($0)} )), 53 | ] 54 | 55 | case typeAWithIndex(StandardTypedDatabaseItem>) 56 | case typeB(StandardTypedDatabaseItem) 57 | } 58 | 59 | struct TypeA: Codable { 60 | let firstly: String 61 | let secondly: String 62 | 63 | init(firstly: String, secondly: String) { 64 | self.firstly = firstly 65 | self.secondly = secondly 66 | } 67 | } 68 | 69 | struct TypeB: Codable, CustomRowTypeIdentifier { 70 | static var rowTypeIdentifier: String? = "TypeBCustom" 71 | 72 | let thirdly: String 73 | let fourthly: String 74 | } 75 | 76 | let serializedTypeADatabaseItem = """ 77 | { 78 | "M" : { 79 | "PK" : { "S": "partitionKey" }, 80 | "SK" : { "S": "sortKey" }, 81 | "CreateDate" : { "S" : "2018-01-06T23:36:20.355Z" }, 82 | "RowVersion" : { "N": "5" }, 83 | "LastUpdatedDate" : { "S" : "2018-01-06T23:36:20.355Z" }, 84 | "RowType": { "S": "TypeA" }, 85 | "firstly" : { "S": "aaa" }, 86 | "secondly": { "S": "bbb" } 87 | } 88 | } 89 | """ 90 | 91 | let serializedTypeADatabaseItemWithTimeToLive = """ 92 | { 93 | "M" : { 94 | "PK" : { "S": "partitionKey" }, 95 | "SK" : { "S": "sortKey" }, 96 | "CreateDate" : { "S" : "2018-01-06T23:36:20.355Z" }, 97 | "RowVersion" : { "N": "5" }, 98 | "LastUpdatedDate" : { "S" : "2018-01-06T23:36:20.355Z" }, 99 | "RowType": { "S": "TypeA" }, 100 | "firstly" : { "S": "aaa" }, 101 | "secondly": { "S": "bbb" }, 102 | "ExpireDate": { "N": "123456789" } 103 | } 104 | } 105 | """ 106 | 107 | let serializedPolymorphicDatabaseItemList = """ 108 | [ 109 | { 110 | "M" : { 111 | "PK" : { "S": "partitionKey1" }, 112 | "SK" : { "S": "sortKey1" }, 113 | "CreateDate" : { "S" : "2018-01-06T23:36:20.355Z" }, 114 | "RowVersion" : { "N": "5" }, 115 | "LastUpdatedDate" : { "S" : "2018-01-06T23:36:20.355Z" }, 116 | "RowType": { "S": "TypeA" }, 117 | "firstly" : { "S": "aaa" }, 118 | "secondly": { "S": "bbb" } 119 | } 120 | }, 121 | { 122 | "M" : { 123 | "PK" : { "S": "partitionKey2" }, 124 | "SK" : { "S": "sortKey2" }, 125 | "CreateDate" : { "S" : "2018-01-06T23:36:20.355Z" }, 126 | "RowVersion" : { "N": "12" }, 127 | "LastUpdatedDate" : { "S" : "2018-01-06T23:36:20.355Z" }, 128 | "RowType": { "S": "TypeBCustom" }, 129 | "thirdly" : { "S": "ccc" }, 130 | "fourthly": { "S": "ddd" } 131 | } 132 | } 133 | ] 134 | """ 135 | 136 | let serializedPolymorphicDatabaseItemListWithIndex = """ 137 | [ 138 | { 139 | "M" : { 140 | "PK" : { "S": "partitionKey1" }, 141 | "SK" : { "S": "sortKey1" }, 142 | "GSI-1-PK" : { "S": "gsi-index" }, 143 | "CreateDate" : { "S" : "2018-01-06T23:36:20.355Z" }, 144 | "RowVersion" : { "N": "5" }, 145 | "LastUpdatedDate" : { "S" : "2018-01-06T23:36:20.355Z" }, 146 | "RowType": { "S": "TypeAWithGSI1PKIndex" }, 147 | "firstly" : { "S": "aaa" }, 148 | "secondly": { "S": "bbb" } 149 | } 150 | }, 151 | { 152 | "M" : { 153 | "PK" : { "S": "partitionKey2" }, 154 | "SK" : { "S": "sortKey2" }, 155 | "CreateDate" : { "S" : "2018-01-06T23:36:20.355Z" }, 156 | "RowVersion" : { "N": "12" }, 157 | "LastUpdatedDate" : { "S" : "2018-01-06T23:36:20.355Z" }, 158 | "RowType": { "S": "TypeBCustom" }, 159 | "thirdly" : { "S": "ccc" }, 160 | "fourthly": { "S": "ddd" } 161 | } 162 | } 163 | ] 164 | """ 165 | -------------------------------------------------------------------------------- /Sources/SmokeDynamoDB/InternalKeyedEncodingContainer.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // InternalKeyedEncodingContainer.swift 15 | // SmokeDynamoDB 16 | // 17 | 18 | import Foundation 19 | import DynamoDBModel 20 | 21 | internal struct InternalKeyedEncodingContainer: KeyedEncodingContainerProtocol { 22 | typealias Key = K 23 | 24 | private let enclosingContainer: InternalSingleValueEncodingContainer 25 | 26 | init(enclosingContainer: InternalSingleValueEncodingContainer) { 27 | self.enclosingContainer = enclosingContainer 28 | } 29 | 30 | // MARK: - Swift.KeyedEncodingContainerProtocol Methods 31 | 32 | var codingPath: [CodingKey] { 33 | return enclosingContainer.codingPath 34 | } 35 | 36 | func encodeNil(forKey key: Key) throws { 37 | enclosingContainer.addToKeyedContainer(key: key, value: DynamoDBModel.AttributeValue(NULL: true)) 38 | } 39 | 40 | func encode(_ value: Bool, forKey key: Key) throws { 41 | enclosingContainer.addToKeyedContainer(key: key, value: DynamoDBModel.AttributeValue(BOOL: value)) 42 | } 43 | 44 | func encode(_ value: Int, forKey key: Key) throws { 45 | enclosingContainer.addToKeyedContainer(key: key, value: DynamoDBModel.AttributeValue(N: String(value))) 46 | } 47 | 48 | func encode(_ value: Int8, forKey key: Key) throws { 49 | enclosingContainer.addToKeyedContainer(key: key, value: DynamoDBModel.AttributeValue(N: String(value))) 50 | } 51 | 52 | func encode(_ value: Int16, forKey key: Key) throws { 53 | enclosingContainer.addToKeyedContainer(key: key, value: DynamoDBModel.AttributeValue(N: String(value))) 54 | } 55 | 56 | func encode(_ value: Int32, forKey key: Key) throws { 57 | enclosingContainer.addToKeyedContainer(key: key, value: DynamoDBModel.AttributeValue(N: String(value))) 58 | } 59 | 60 | func encode(_ value: Int64, forKey key: Key) throws { 61 | enclosingContainer.addToKeyedContainer(key: key, value: DynamoDBModel.AttributeValue(N: String(value))) 62 | } 63 | 64 | func encode(_ value: UInt, forKey key: Key) throws { 65 | enclosingContainer.addToKeyedContainer(key: key, value: DynamoDBModel.AttributeValue(N: String(value))) 66 | } 67 | 68 | func encode(_ value: UInt8, forKey key: Key) throws { 69 | enclosingContainer.addToKeyedContainer(key: key, value: DynamoDBModel.AttributeValue(N: String(value))) 70 | } 71 | 72 | func encode(_ value: UInt16, forKey key: Key) throws { 73 | enclosingContainer.addToKeyedContainer(key: key, value: DynamoDBModel.AttributeValue(N: String(value))) 74 | } 75 | 76 | func encode(_ value: UInt32, forKey key: Key) throws { 77 | enclosingContainer.addToKeyedContainer(key: key, value: DynamoDBModel.AttributeValue(N: String(value))) 78 | } 79 | 80 | func encode(_ value: UInt64, forKey key: Key) throws { 81 | enclosingContainer.addToKeyedContainer(key: key, value: DynamoDBModel.AttributeValue(N: String(value))) 82 | } 83 | 84 | func encode(_ value: Float, forKey key: Key) throws { 85 | enclosingContainer.addToKeyedContainer(key: key, value: DynamoDBModel.AttributeValue(N: String(value))) 86 | } 87 | 88 | func encode(_ value: Double, forKey key: Key) throws { 89 | enclosingContainer.addToKeyedContainer(key: key, value: DynamoDBModel.AttributeValue(N: String(value))) 90 | } 91 | 92 | func encode(_ value: String, forKey key: Key) throws { 93 | enclosingContainer.addToKeyedContainer(key: key, value: DynamoDBModel.AttributeValue(S: value)) } 94 | 95 | func encode(_ value: T, forKey key: Key) throws where T: Encodable { 96 | let nestedContainer = createNestedContainer(for: key) 97 | 98 | try nestedContainer.encode(value) 99 | } 100 | 101 | func nestedContainer(keyedBy type: NestedKey.Type, 102 | forKey key: Key) -> KeyedEncodingContainer { 103 | let nestedContainer = createNestedContainer(for: key, defaultValue: .keyedContainer([:])) 104 | 105 | let nestedKeyContainer = InternalKeyedEncodingContainer(enclosingContainer: nestedContainer) 106 | 107 | return KeyedEncodingContainer(nestedKeyContainer) 108 | } 109 | 110 | func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { 111 | let nestedContainer = createNestedContainer(for: key, defaultValue: .unkeyedContainer([])) 112 | 113 | let nestedKeyContainer = InternalUnkeyedEncodingContainer(enclosingContainer: nestedContainer) 114 | 115 | return nestedKeyContainer 116 | } 117 | 118 | func superEncoder() -> Encoder { return createNestedContainer(for: InternalDynamoDBCodingKey.super) } 119 | func superEncoder(forKey key: Key) -> Encoder { return createNestedContainer(for: key) } 120 | 121 | // MARK: - 122 | 123 | private func createNestedContainer(for key: NestedKey, 124 | defaultValue: ContainerValueType? = nil) 125 | -> InternalSingleValueEncodingContainer { 126 | let nestedContainer = InternalSingleValueEncodingContainer(userInfo: enclosingContainer.userInfo, 127 | codingPath: enclosingContainer.codingPath + [key], 128 | attributeNameTransform: enclosingContainer.attributeNameTransform, 129 | defaultValue: defaultValue) 130 | enclosingContainer.addToKeyedContainer(key: key, value: nestedContainer) 131 | 132 | return nestedContainer 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /Sources/SmokeDynamoDB/DynamoDBCompositePrimaryKeysProjection.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // DynamoDBCompositePrimaryKeysProjection.swift 15 | // SmokeDynamoDB 16 | // 17 | 18 | import Foundation 19 | import SmokeHTTPClient 20 | import DynamoDBModel 21 | import NIO 22 | 23 | /** 24 | Protocol presenting a Keys Only projection of a DynamoDB table such as a Keys Only GSI projection. 25 | Provides the ability to query the projection to get the list of keys without attempting to decode the row into a particular data type. 26 | */ 27 | public protocol DynamoDBCompositePrimaryKeysProjection { 28 | var eventLoop: EventLoop { get } 29 | 30 | /** 31 | * Queries a partition in the database table and optionally a sort key condition. If the 32 | partition doesn't exist, this operation will return an empty list as a response. This 33 | function will potentially make multiple calls to DynamoDB to retrieve all results for 34 | the query. 35 | */ 36 | func query(forPartitionKey partitionKey: String, 37 | sortKeyCondition: AttributeCondition?) 38 | -> EventLoopFuture<[CompositePrimaryKey]> 39 | 40 | /** 41 | * Queries a partition in the database table and optionally a sort key condition. If the 42 | partition doesn't exist, this operation will return an empty list as a response. This 43 | function will return paginated results based on the limit and exclusiveStartKey provided. 44 | */ 45 | func query(forPartitionKey partitionKey: String, 46 | sortKeyCondition: AttributeCondition?, 47 | limit: Int?, 48 | exclusiveStartKey: String?) 49 | -> EventLoopFuture<([CompositePrimaryKey], String?)> 50 | 51 | func query(forPartitionKey partitionKey: String, 52 | sortKeyCondition: AttributeCondition?, 53 | limit: Int?, 54 | scanIndexForward: Bool, 55 | exclusiveStartKey: String?) 56 | -> EventLoopFuture<([CompositePrimaryKey], String?)> 57 | 58 | #if (os(Linux) && compiler(>=5.5)) || (!os(Linux) && compiler(>=5.5.2)) && canImport(_Concurrency) 59 | /** 60 | * Queries a partition in the database table and optionally a sort key condition. If the 61 | partition doesn't exist, this operation will return an empty list as a response. This 62 | function will potentially make multiple calls to DynamoDB to retrieve all results for 63 | the query. 64 | */ 65 | func query(forPartitionKey partitionKey: String, 66 | sortKeyCondition: AttributeCondition?) async throws 67 | -> [CompositePrimaryKey] 68 | 69 | /** 70 | * Queries a partition in the database table and optionally a sort key condition. If the 71 | partition doesn't exist, this operation will return an empty list as a response. This 72 | function will return paginated results based on the limit and exclusiveStartKey provided. 73 | */ 74 | func query(forPartitionKey partitionKey: String, 75 | sortKeyCondition: AttributeCondition?, 76 | limit: Int?, 77 | exclusiveStartKey: String?) async throws 78 | -> ([CompositePrimaryKey], String?) 79 | 80 | func query(forPartitionKey partitionKey: String, 81 | sortKeyCondition: AttributeCondition?, 82 | limit: Int?, 83 | scanIndexForward: Bool, 84 | exclusiveStartKey: String?) async throws 85 | -> ([CompositePrimaryKey], String?) 86 | #endif 87 | } 88 | 89 | // For async/await APIs, simply delegate to the EventLoopFuture implementation until support is dropped for Swift <5.5 90 | public extension DynamoDBCompositePrimaryKeysProjection { 91 | #if (os(Linux) && compiler(>=5.5)) || (!os(Linux) && compiler(>=5.5.2)) && canImport(_Concurrency) 92 | func query(forPartitionKey partitionKey: String, 93 | sortKeyCondition: AttributeCondition?) async throws 94 | -> [CompositePrimaryKey] { 95 | return try await query(forPartitionKey: partitionKey, 96 | sortKeyCondition: sortKeyCondition).get() 97 | } 98 | 99 | func query(forPartitionKey partitionKey: String, 100 | sortKeyCondition: AttributeCondition?, 101 | limit: Int?, 102 | exclusiveStartKey: String?) async throws 103 | -> ([CompositePrimaryKey], String?) { 104 | return try await query(forPartitionKey: partitionKey, 105 | sortKeyCondition: sortKeyCondition, 106 | limit: limit, 107 | exclusiveStartKey: exclusiveStartKey).get() 108 | } 109 | 110 | func query(forPartitionKey partitionKey: String, 111 | sortKeyCondition: AttributeCondition?, 112 | limit: Int?, 113 | scanIndexForward: Bool, 114 | exclusiveStartKey: String?) async throws 115 | -> ([CompositePrimaryKey], String?) { 116 | return try await query(forPartitionKey: partitionKey, 117 | sortKeyCondition: sortKeyCondition, 118 | limit: limit, 119 | scanIndexForward: scanIndexForward, 120 | exclusiveStartKey: exclusiveStartKey).get() 121 | } 122 | #endif 123 | } 124 | -------------------------------------------------------------------------------- /Sources/SmokeDynamoDB/TypedDatabaseItemWithTimeToLive.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // TypedDatabaseItemWithTimeToLive.swift 15 | // SmokeDynamoDB 16 | // 17 | 18 | import Foundation 19 | 20 | public struct RowStatus: Codable { 21 | public let rowVersion: Int 22 | public let lastUpdatedDate: Date 23 | 24 | public init(rowVersion: Int, lastUpdatedDate: Date) { 25 | self.rowVersion = rowVersion 26 | self.lastUpdatedDate = lastUpdatedDate 27 | } 28 | 29 | enum CodingKeys: String, CodingKey { 30 | case rowVersion = "RowVersion" 31 | case lastUpdatedDate = "LastUpdatedDate" 32 | } 33 | } 34 | 35 | public protocol DatabaseItem { 36 | associatedtype AttributesType: PrimaryKeyAttributes 37 | // Default to StandardTimeToLiveAttributes for backwards compatibility 38 | associatedtype TimeToLiveAttributesType: TimeToLiveAttributes = StandardTimeToLiveAttributes 39 | 40 | var compositePrimaryKey: CompositePrimaryKey { get } 41 | var createDate: Date { get } 42 | var rowStatus: RowStatus { get } 43 | var timeToLive: TimeToLive? { get } 44 | } 45 | 46 | public extension DatabaseItem { 47 | var timeToLive: TimeToLive? { 48 | return nil 49 | } 50 | } 51 | 52 | public protocol StandardDatabaseItem: DatabaseItem where AttributesType == StandardPrimaryKeyAttributes { 53 | 54 | } 55 | 56 | // Default to StandardTimeToLiveAttributes for backwards compatibility 57 | public typealias TypedDatabaseItem = TypedDatabaseItemWithTimeToLive 58 | 59 | public struct TypedDatabaseItemWithTimeToLive: DatabaseItem, Codable { 62 | public let compositePrimaryKey: CompositePrimaryKey 63 | public let createDate: Date 64 | public let rowStatus: RowStatus 65 | public let timeToLive: TimeToLive? 66 | public let rowValue: RowType 67 | 68 | enum CodingKeys: String, CodingKey { 69 | case rowType = "RowType" 70 | case createDate = "CreateDate" 71 | } 72 | 73 | public static func newItem(withKey key: CompositePrimaryKey, 74 | andValue value: RowType, 75 | andTimeToLive timeToLive: TimeToLive? = nil) 76 | -> TypedDatabaseItemWithTimeToLive { 77 | return TypedDatabaseItemWithTimeToLive( 78 | compositePrimaryKey: key, 79 | createDate: Date(), 80 | rowStatus: RowStatus(rowVersion: 1, lastUpdatedDate: Date()), 81 | rowValue: value, 82 | timeToLive: timeToLive) 83 | } 84 | 85 | public func createUpdatedItem(withValue value: RowType, 86 | andTimeToLive timeToLive: TimeToLive? = nil) 87 | -> TypedDatabaseItemWithTimeToLive { 88 | return TypedDatabaseItemWithTimeToLive( 89 | compositePrimaryKey: compositePrimaryKey, 90 | createDate: createDate, 91 | rowStatus: RowStatus(rowVersion: rowStatus.rowVersion + 1, 92 | lastUpdatedDate: Date()), 93 | rowValue: value, 94 | timeToLive: timeToLive) 95 | } 96 | 97 | init(compositePrimaryKey: CompositePrimaryKey, 98 | createDate: Date, 99 | rowStatus: RowStatus, 100 | rowValue: RowType, 101 | timeToLive: TimeToLive? = nil) { 102 | self.compositePrimaryKey = compositePrimaryKey 103 | self.createDate = createDate 104 | self.rowStatus = rowStatus 105 | self.rowValue = rowValue 106 | self.timeToLive = timeToLive 107 | } 108 | 109 | public init(from decoder: Decoder) throws { 110 | let values = try decoder.container(keyedBy: CodingKeys.self) 111 | let storedRowTypeName = try values.decode(String.self, forKey: .rowType) 112 | self.createDate = try values.decode(Date.self, forKey: .createDate) 113 | 114 | // get the type that is being requested to be decoded into 115 | let requestedRowTypeName = getTypeRowIdentifier(type: RowType.self) 116 | 117 | // if the stored rowType is not what we should attempt to decode into 118 | guard storedRowTypeName == requestedRowTypeName else { 119 | // throw an exception to avoid accidentally decoding into the incorrect type 120 | throw SmokeDynamoDBError.typeMismatch(expected: storedRowTypeName, provided: requestedRowTypeName) 121 | } 122 | 123 | self.compositePrimaryKey = try CompositePrimaryKey(from: decoder) 124 | self.rowStatus = try RowStatus(from: decoder) 125 | 126 | do { 127 | self.timeToLive = try TimeToLive(from: decoder) 128 | } catch DecodingError.keyNotFound { 129 | self.timeToLive = nil 130 | } 131 | 132 | self.rowValue = try RowType(from: decoder) 133 | } 134 | 135 | public func encode(to encoder: Encoder) throws { 136 | var container = encoder.container(keyedBy: CodingKeys.self) 137 | try container.encode(getTypeRowIdentifier(type: RowType.self), forKey: .rowType) 138 | try container.encode(createDate, forKey: .createDate) 139 | 140 | try compositePrimaryKey.encode(to: encoder) 141 | try rowStatus.encode(to: encoder) 142 | try timeToLive?.encode(to: encoder) 143 | try rowValue.encode(to: encoder) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /Sources/SmokeDynamoDB/AWSDynamoDBTableOperationsClient.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // AWSDynamoDBTableOperationsClient.swift 15 | // DynamoDBClient 16 | // 17 | 18 | import DynamoDBModel 19 | import SmokeAWSCore 20 | import SmokeHTTPClient 21 | import SmokeAWSHttp 22 | import AsyncHTTPClient 23 | 24 | public typealias AWSDynamoDBTableOperationsClient = 25 | AWSGenericDynamoDBTableOperationsClient> 26 | 27 | public struct AWSGenericDynamoDBTableOperationsClient { 28 | public let config: AWSGenericDynamoDBClientConfiguration 29 | public let tableName: String 30 | public let httpClient: HTTPOperationsClient 31 | 32 | public init( 33 | tableName: String, 34 | credentialsProvider: CredentialsProvider, 35 | awsRegion: AWSRegion, 36 | endpointHostName endpointHostNameOptional: String? = nil, 37 | endpointPort: Int = 443, 38 | requiresTLS: Bool? = nil, 39 | service: String = "dynamodb", 40 | contentType: String = "application/x-amz-json-1.0", 41 | target: String? = "DynamoDB_20120810", 42 | ignoreInvocationEventLoop: Bool = false, 43 | traceContext: TraceContextType, 44 | timeoutConfiguration: HTTPClient.Configuration.Timeout = .init(), 45 | retryConfiguration: HTTPClientRetryConfiguration = .default, 46 | eventLoopProvider: HTTPClient.EventLoopGroupProvider = .createNew, 47 | reportingConfiguration: SmokeAWSClientReportingConfiguration 48 | = SmokeAWSClientReportingConfiguration(), 49 | connectionPoolConfiguration: HTTPClient.Configuration.ConnectionPool? = nil, 50 | enableAHCLogging: Bool = false) 51 | where InvocationReportingType == StandardHTTPClientCoreInvocationReporting { 52 | self.config = AWSGenericDynamoDBClientConfiguration( 53 | credentialsProvider: credentialsProvider, 54 | awsRegion: awsRegion, 55 | endpointHostName: endpointHostNameOptional, 56 | endpointPort: endpointPort, 57 | requiresTLS: requiresTLS, 58 | service: service, 59 | contentType: contentType, 60 | target: target, 61 | ignoreInvocationEventLoop: ignoreInvocationEventLoop, 62 | traceContext: traceContext, 63 | timeoutConfiguration: timeoutConfiguration, 64 | retryConfiguration: retryConfiguration, 65 | eventLoopProvider: eventLoopProvider, 66 | reportingConfiguration: reportingConfiguration, 67 | connectionPoolConfiguration: connectionPoolConfiguration, 68 | enableAHCLogging: enableAHCLogging) 69 | self.httpClient = self.config.createHTTPOperationsClient() 70 | self.tableName = tableName 71 | } 72 | 73 | public init( 74 | tableName: String, 75 | credentialsProvider: CredentialsProvider, 76 | awsRegion: AWSRegion, 77 | endpointHostName endpointHostNameOptional: String? = nil, 78 | endpointPort: Int = 443, 79 | requiresTLS: Bool? = nil, 80 | service: String = "dynamodb", 81 | contentType: String = "application/x-amz-json-1.0", 82 | target: String? = "DynamoDB_20120810", 83 | ignoreInvocationEventLoop: Bool = false, 84 | timeoutConfiguration: HTTPClient.Configuration.Timeout = .init(), 85 | retryConfiguration: HTTPClientRetryConfiguration = .default, 86 | eventLoopProvider: HTTPClient.EventLoopGroupProvider = .createNew, 87 | reportingConfiguration: SmokeAWSClientReportingConfiguration 88 | = SmokeAWSClientReportingConfiguration(), 89 | connectionPoolConfiguration: HTTPClient.Configuration.ConnectionPool? = nil, 90 | enableAHCLogging: Bool = false) 91 | where InvocationReportingType == StandardHTTPClientCoreInvocationReporting { 92 | self.init(tableName: tableName, 93 | credentialsProvider: credentialsProvider, 94 | awsRegion: awsRegion, 95 | endpointHostName: endpointHostNameOptional, 96 | endpointPort: endpointPort, 97 | requiresTLS: requiresTLS, 98 | service: service, 99 | contentType: contentType, 100 | target: target, 101 | ignoreInvocationEventLoop: ignoreInvocationEventLoop, 102 | traceContext: AWSClientInvocationTraceContext(), 103 | timeoutConfiguration: timeoutConfiguration, 104 | retryConfiguration: retryConfiguration, 105 | eventLoopProvider: eventLoopProvider, 106 | reportingConfiguration: reportingConfiguration, 107 | connectionPoolConfiguration: connectionPoolConfiguration, 108 | enableAHCLogging: enableAHCLogging) 109 | } 110 | 111 | internal init(config: AWSGenericDynamoDBClientConfiguration, 112 | tableName: String) { 113 | self.config = config 114 | self.tableName = tableName 115 | self.httpClient = self.config.createHTTPOperationsClient() 116 | } 117 | 118 | /** 119 | Gracefully shuts down the eventloop if owned by this client. 120 | This function is idempotent and will handle being called multiple 121 | times. Will block until shutdown is complete. 122 | */ 123 | public func syncShutdown() throws { 124 | try httpClient.syncShutdown() 125 | } 126 | 127 | /** 128 | Gracefully shuts down the eventloop if owned by this client. 129 | This function is idempotent and will handle being called multiple 130 | times. Will return when shutdown is complete. 131 | */ 132 | #if (os(Linux) && compiler(>=5.5)) || (!os(Linux) && compiler(>=5.5.2)) && canImport(_Concurrency) 133 | public func shutdown() async throws { 134 | try await httpClient.shutdown() 135 | } 136 | #endif 137 | } 138 | -------------------------------------------------------------------------------- /Sources/SmokeDynamoDB/_DynamoDBClient/_AWSDynamoDBClientGenerator.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // swiftlint:disable superfluous_disable_command 15 | // swiftlint:disable file_length line_length identifier_name type_name vertical_parameter_alignment 16 | // swiftlint:disable type_body_length function_body_length generic_type_name cyclomatic_complexity 17 | // -- Generated Code; do not edit -- 18 | // 19 | // AWSDynamoDBClientGenerator.swift 20 | // DynamoDBClient 21 | // 22 | 23 | import Foundation 24 | import DynamoDBModel 25 | import DynamoDBClient 26 | import SmokeAWSCore 27 | import SmokeHTTPClient 28 | import SmokeAWSHttp 29 | import NIO 30 | import NIOHTTP1 31 | import AsyncHTTPClient 32 | import Logging 33 | 34 | /** 35 | AWS Client Generator for the DynamoDB service. 36 | */ 37 | struct _AWSDynamoDBClientGenerator { 38 | let httpClient: HTTPOperationsClient 39 | let awsRegion: AWSRegion 40 | let service: String 41 | let target: String? 42 | let retryConfiguration: HTTPClientRetryConfiguration 43 | let retryOnErrorProvider: (SmokeHTTPClient.HTTPClientError) -> Bool 44 | let credentialsProvider: CredentialsProvider 45 | 46 | public let eventLoopGroup: EventLoopGroup 47 | 48 | let operationsReporting: DynamoDBOperationsReporting 49 | 50 | public init(credentialsProvider: CredentialsProvider, awsRegion: AWSRegion, 51 | endpointHostName: String, 52 | endpointPort: Int = 443, 53 | requiresTLS: Bool? = nil, 54 | service: String = "dynamodb", 55 | contentType: String = "application/x-amz-json-1.0", 56 | target: String? = "DynamoDB_20120810", 57 | connectionTimeoutSeconds: Int64 = 10, 58 | retryConfiguration: HTTPClientRetryConfiguration = .default, 59 | eventLoopProvider: HTTPClient.EventLoopGroupProvider = .createNew, 60 | reportingConfiguration: SmokeAWSClientReportingConfiguration 61 | = SmokeAWSClientReportingConfiguration() ) { 62 | self.eventLoopGroup = AWSClientHelper.getEventLoop(eventLoopGroupProvider: eventLoopProvider) 63 | let useTLS = requiresTLS ?? AWSHTTPClientDelegate.requiresTLS(forEndpointPort: endpointPort) 64 | let clientDelegate = JSONAWSHttpClientDelegate(requiresTLS: useTLS) 65 | 66 | self.httpClient = HTTPOperationsClient( 67 | endpointHostName: endpointHostName, 68 | endpointPort: endpointPort, 69 | contentType: contentType, 70 | clientDelegate: clientDelegate, 71 | connectionTimeoutSeconds: connectionTimeoutSeconds, 72 | eventLoopProvider: .shared(self.eventLoopGroup)) 73 | self.awsRegion = awsRegion 74 | self.service = service 75 | self.target = target 76 | self.credentialsProvider = credentialsProvider 77 | self.retryConfiguration = retryConfiguration 78 | self.retryOnErrorProvider = { error in error.isRetriable() } 79 | self.operationsReporting = DynamoDBOperationsReporting(clientName: "AWSDynamoDBClient", reportingConfiguration: reportingConfiguration) 80 | } 81 | 82 | /** 83 | Gracefully shuts down this client. This function is idempotent and 84 | will handle being called multiple times. Will block until shutdown is complete. 85 | */ 86 | public func syncShutdown() throws { 87 | try self.httpClient.syncShutdown() 88 | } 89 | 90 | // renamed `syncShutdown` to make it clearer this version of shutdown will block. 91 | @available(*, deprecated, renamed: "syncShutdown") 92 | public func close() throws { 93 | try self.httpClient.close() 94 | } 95 | 96 | /** 97 | Gracefully shuts down this client. This function is idempotent and 98 | will handle being called multiple times. Will return when shutdown is complete. 99 | */ 100 | #if (os(Linux) && compiler(>=5.5)) || (!os(Linux) && compiler(>=5.5.2)) && canImport(_Concurrency) 101 | public func shutdown() async throws { 102 | try await self.httpClient.shutdown() 103 | } 104 | #endif 105 | 106 | public func with( 107 | reporting: NewInvocationReportingType) -> _AWSDynamoDBClient { 108 | return _AWSDynamoDBClient( 109 | credentialsProvider: self.credentialsProvider, 110 | awsRegion: self.awsRegion, 111 | reporting: reporting, 112 | httpClient: self.httpClient, 113 | service: self.service, 114 | target: self.target, 115 | eventLoopGroup: self.eventLoopGroup, 116 | retryOnErrorProvider: self.retryOnErrorProvider, 117 | retryConfiguration: self.retryConfiguration, 118 | operationsReporting: self.operationsReporting) 119 | } 120 | 121 | public func with( 122 | logger: Logging.Logger, 123 | internalRequestId: String = "none", 124 | traceContext: NewTraceContextType, 125 | eventLoop: EventLoop? = nil) -> _AWSDynamoDBClient> { 126 | let reporting = StandardHTTPClientCoreInvocationReporting( 127 | logger: logger, 128 | internalRequestId: internalRequestId, 129 | traceContext: traceContext, 130 | eventLoop: eventLoop) 131 | 132 | return with(reporting: reporting) 133 | } 134 | 135 | public func with( 136 | logger: Logging.Logger, 137 | internalRequestId: String = "none", 138 | eventLoop: EventLoop? = nil) -> _AWSDynamoDBClient> { 139 | let reporting = StandardHTTPClientCoreInvocationReporting( 140 | logger: logger, 141 | internalRequestId: internalRequestId, 142 | traceContext: AWSClientInvocationTraceContext(), 143 | eventLoop: eventLoop) 144 | 145 | return with(reporting: reporting) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /Sources/SmokeDynamoDB/InternalUnkeyedDecodingContainer.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // InternalUnkeyedDecodingContainer.swift 15 | // SmokeDynamoDB 16 | // 17 | 18 | import Foundation 19 | 20 | internal struct InternalUnkeyedDecodingContainer: UnkeyedDecodingContainer { 21 | private let decodingContainer: InternalSingleValueDecodingContainer 22 | internal private(set) var currentIndex: Int 23 | 24 | init(decodingContainer: InternalSingleValueDecodingContainer) { 25 | self.decodingContainer = decodingContainer 26 | self.currentIndex = 0 27 | } 28 | 29 | // MARK: - Swift.UnkeyedEncodingContainer Methods 30 | 31 | var codingPath: [CodingKey] { 32 | return decodingContainer.codingPath 33 | } 34 | 35 | mutating func decodeNil() throws -> Bool { 36 | return try createNestedContainer().decodeNil() 37 | } 38 | 39 | mutating func decode(_ type: Bool.Type) throws -> Bool { 40 | return try createNestedContainer().decode(Bool.self) 41 | } 42 | 43 | mutating func decode(_ type: Int.Type) throws -> Int { 44 | return try createNestedContainer().decode(Int.self) 45 | } 46 | 47 | mutating func decode(_ type: Int8.Type) throws -> Int8 { 48 | return try createNestedContainer().decode(Int8.self) 49 | } 50 | 51 | mutating func decode(_ type: Int16.Type) throws -> Int16 { 52 | return try createNestedContainer().decode(Int16.self) 53 | } 54 | 55 | mutating func decode(_ type: Int32.Type) throws -> Int32 { 56 | return try createNestedContainer().decode(Int32.self) 57 | } 58 | 59 | mutating func decode(_ type: Int64.Type) throws -> Int64 { 60 | return try createNestedContainer().decode(Int64.self) 61 | } 62 | 63 | mutating func decode(_ type: UInt.Type) throws -> UInt { 64 | return try createNestedContainer().decode(UInt.self) 65 | } 66 | 67 | mutating func decode(_ type: UInt8.Type) throws -> UInt8 { 68 | return try createNestedContainer().decode(UInt8.self) 69 | } 70 | 71 | mutating func decode(_ type: UInt16.Type) throws -> UInt16 { 72 | return try createNestedContainer().decode(UInt16.self) 73 | } 74 | 75 | mutating func decode(_ type: UInt32.Type) throws -> UInt32 { 76 | return try createNestedContainer().decode(UInt32.self) 77 | } 78 | 79 | mutating func decode(_ type: UInt64.Type) throws -> UInt64 { 80 | return try createNestedContainer().decode(UInt64.self) 81 | } 82 | 83 | mutating func decode(_ type: Float.Type) throws -> Float { 84 | return try createNestedContainer().decode(Float.self) 85 | } 86 | 87 | mutating func decode(_ type: Double.Type) throws -> Double { 88 | return try createNestedContainer().decode(Double.self) 89 | } 90 | 91 | mutating func decode(_ type: String.Type) throws -> String { 92 | return try createNestedContainer().decode(String.self) 93 | } 94 | 95 | mutating func decode(_ type: T.Type) throws -> T where T: Decodable { 96 | return try createNestedContainer().decode(type) 97 | } 98 | 99 | var count: Int? { 100 | guard let values = decodingContainer.attributeValue.L else { 101 | return nil 102 | } 103 | 104 | return values.count 105 | } 106 | 107 | var isAtEnd: Bool { 108 | guard let values = decodingContainer.attributeValue.L else { 109 | return true 110 | } 111 | 112 | return currentIndex >= values.count 113 | } 114 | 115 | mutating func nestedContainer(keyedBy type: NestedKey.Type) throws 116 | -> KeyedDecodingContainer where NestedKey: CodingKey { 117 | return try createNestedContainer().container(keyedBy: type) 118 | } 119 | 120 | mutating func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer { 121 | return try createNestedContainer().unkeyedContainer() 122 | } 123 | 124 | mutating func superDecoder() throws -> Decoder { 125 | return try createNestedContainer() 126 | } 127 | 128 | // MARK: - 129 | 130 | private mutating func createNestedContainer() throws -> InternalSingleValueDecodingContainer { 131 | let index = currentIndex 132 | currentIndex += 1 133 | 134 | guard let values = decodingContainer.attributeValue.L else { 135 | let description = "Expected to decode a list." 136 | let context = DecodingError.Context(codingPath: codingPath, debugDescription: description) 137 | throw DecodingError.dataCorrupted(context) 138 | } 139 | 140 | guard index < values.count else { 141 | let description = "Could not find key for index \(index)." 142 | let context = DecodingError.Context(codingPath: codingPath, debugDescription: description) 143 | throw DecodingError.valueNotFound(Any.self, context) 144 | } 145 | 146 | let value = values[index] 147 | 148 | return InternalSingleValueDecodingContainer(attributeValue: value, 149 | codingPath: decodingContainer.codingPath 150 | + [InternalDynamoDBCodingKey(index: index)], 151 | userInfo: decodingContainer.userInfo, 152 | attributeNameTransform: decodingContainer.attributeNameTransform) 153 | } 154 | } 155 | 156 | private let iso8601DateFormatter: DateFormatter = { 157 | let formatter = DateFormatter() 158 | formatter.calendar = Calendar(identifier: .iso8601) 159 | formatter.locale = Locale(identifier: "en_US_POSIX") 160 | formatter.timeZone = TimeZone(secondsFromGMT: 0) 161 | formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX" 162 | return formatter 163 | }() 164 | 165 | extension Date { 166 | var iso8601: String { 167 | return iso8601DateFormatter.string(from: self) 168 | } 169 | } 170 | 171 | extension String { 172 | var dateFromISO8601: Date? { 173 | return iso8601DateFormatter.date(from: self) // "Mar 22, 2017, 10:22 AM" 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /Sources/SmokeDynamoDB/InternalKeyedDecodingContainer.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // InternalKeyedDecodingContainer.swift 15 | // SmokeDynamoDB 16 | // 17 | 18 | import Foundation 19 | import DynamoDBModel 20 | 21 | internal struct InternalKeyedDecodingContainer: KeyedDecodingContainerProtocol { 22 | typealias Key = K 23 | 24 | private let decodingContainer: InternalSingleValueDecodingContainer 25 | 26 | init(decodingContainer: InternalSingleValueDecodingContainer) { 27 | self.decodingContainer = decodingContainer 28 | } 29 | 30 | // MARK: - Swift.KeyedEncodingContainerProtocol Methods 31 | 32 | var codingPath: [CodingKey] { 33 | return decodingContainer.codingPath 34 | } 35 | 36 | func decodeNil(forKey key: Key) throws -> Bool { 37 | return try createNestedContainer(for: key).decodeNil() 38 | } 39 | 40 | func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool { 41 | return try createNestedContainer(for: key).decode(Bool.self) 42 | } 43 | func decode(_ type: Int.Type, forKey key: Key) throws -> Int { 44 | return try createNestedContainer(for: key).decode(Int.self) 45 | } 46 | 47 | func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 { 48 | return try createNestedContainer(for: key).decode(Int8.self) 49 | } 50 | 51 | func decode(_ type: Int16.Type, forKey key: Key) throws -> Int16 { 52 | return try createNestedContainer(for: key).decode(Int16.self) 53 | } 54 | 55 | func decode(_ type: Int32.Type, forKey key: Key) throws -> Int32 { 56 | return try createNestedContainer(for: key).decode(Int32.self) 57 | } 58 | 59 | func decode(_ type: Int64.Type, forKey key: Key) throws -> Int64 { 60 | return try createNestedContainer(for: key).decode(Int64.self) 61 | } 62 | 63 | func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt { 64 | return try createNestedContainer(for: key).decode(UInt.self) 65 | } 66 | 67 | func decode(_ type: UInt8.Type, forKey key: Key) throws -> UInt8 { 68 | return try createNestedContainer(for: key).decode(UInt8.self) 69 | } 70 | 71 | func decode(_ type: UInt16.Type, forKey key: Key) throws -> UInt16 { 72 | return try createNestedContainer(for: key).decode(UInt16.self) 73 | } 74 | 75 | func decode(_ type: UInt32.Type, forKey key: Key) throws -> UInt32 { 76 | return try createNestedContainer(for: key).decode(UInt32.self) 77 | } 78 | 79 | func decode(_ type: UInt64.Type, forKey key: Key) throws -> UInt64 { 80 | return try createNestedContainer(for: key).decode(UInt64.self) 81 | } 82 | 83 | func decode(_ type: Float.Type, forKey key: Key) throws -> Float { 84 | return try createNestedContainer(for: key).decode(Float.self) 85 | } 86 | 87 | func decode(_ type: Double.Type, forKey key: Key) throws -> Double { 88 | return try createNestedContainer(for: key).decode(Double.self) 89 | } 90 | 91 | func decode(_ type: String.Type, forKey key: Key) throws -> String { 92 | return try createNestedContainer(for: key).decode(String.self) 93 | } 94 | 95 | func decode(_ type: T.Type, forKey key: Key) throws -> T where T: Decodable { 96 | return try createNestedContainer(for: key).decode(type) 97 | } 98 | 99 | func nestedContainer(keyedBy type: NestedKey.Type, forKey key: K) throws 100 | -> KeyedDecodingContainer where NestedKey: CodingKey { 101 | return try createNestedContainer(for: key).container(keyedBy: type) 102 | } 103 | 104 | func nestedUnkeyedContainer(forKey key: K) throws -> UnkeyedDecodingContainer { 105 | return try createNestedContainer(for: key).unkeyedContainer() 106 | } 107 | 108 | private func getValues() -> [String: DynamoDBModel.AttributeValue] { 109 | guard let values = decodingContainer.attributeValue.M else { 110 | fatalError("Expected keyed container and there wasn't one.") 111 | } 112 | 113 | return values 114 | } 115 | 116 | var allKeys: [K] { 117 | return getValues().keys.map { key in K(stringValue: key) } 118 | .filter { key in key != nil } 119 | .map { key in key! } 120 | } 121 | 122 | func contains(_ key: K) -> Bool { 123 | let attributeName = getAttributeName(key: key) 124 | 125 | return getValues()[attributeName] != nil 126 | } 127 | 128 | func superDecoder() throws -> Decoder { 129 | return try createNestedContainer(for: InternalDynamoDBCodingKey.super) 130 | } 131 | 132 | func superDecoder(forKey key: K) throws -> Decoder { 133 | return try createNestedContainer(for: key) 134 | } 135 | 136 | // MARK: - 137 | 138 | private func createNestedContainer(for key: CodingKey) throws -> InternalSingleValueDecodingContainer { 139 | guard let values = decodingContainer.attributeValue.M else { 140 | let description = "Expected to decode a map." 141 | let context = DecodingError.Context(codingPath: codingPath, debugDescription: description) 142 | throw DecodingError.dataCorrupted(context) 143 | } 144 | 145 | let attributeName = getAttributeName(key: key) 146 | 147 | guard let value = values[attributeName] else { 148 | let description = "Could not find value for \(key) and attributeName '\(attributeName)'." 149 | let context = DecodingError.Context(codingPath: codingPath, debugDescription: description) 150 | throw DecodingError.keyNotFound(key, context) 151 | } 152 | 153 | return InternalSingleValueDecodingContainer(attributeValue: value, codingPath: decodingContainer.codingPath + [key], 154 | userInfo: decodingContainer.userInfo, 155 | attributeNameTransform: decodingContainer.attributeNameTransform) 156 | } 157 | 158 | private func getAttributeName(key: CodingKey) -> String { 159 | let attributeName: String 160 | if let attributeNameTransform = decodingContainer.attributeNameTransform { 161 | attributeName = attributeNameTransform(key.stringValue) 162 | } else { 163 | attributeName = key.stringValue 164 | } 165 | 166 | return attributeName 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /Sources/SmokeDynamoDB/AWSDynamoDBCompositePrimaryKeysProjectionGenerator.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // AWSDynamoDBCompositePrimaryKeysProjectionGenerator.swift 15 | // SmokeDynamoDB 16 | // 17 | 18 | import Foundation 19 | import Logging 20 | import DynamoDBClient 21 | import DynamoDBModel 22 | import SmokeAWSCore 23 | import SmokeAWSHttp 24 | import SmokeHTTPClient 25 | import AsyncHTTPClient 26 | import NIO 27 | 28 | public class AWSDynamoDBCompositePrimaryKeysProjectionGenerator { 29 | internal let dynamodbGenerator: _AWSDynamoDBClientGenerator 30 | internal let targetTableName: String 31 | 32 | public init(accessKeyId: String, secretAccessKey: String, 33 | region: AWSRegion, 34 | endpointHostName: String, endpointPort: Int = 443, 35 | requiresTLS: Bool? = nil, tableName: String, 36 | connectionTimeoutSeconds: Int64 = 10, 37 | retryConfiguration: HTTPClientRetryConfiguration = .default, 38 | eventLoopProvider: HTTPClient.EventLoopGroupProvider = .createNew, 39 | reportingConfiguration: SmokeAWSCore.SmokeAWSClientReportingConfiguration 40 | = SmokeAWSClientReportingConfiguration()) { 41 | let staticCredentials = StaticCredentials(accessKeyId: accessKeyId, 42 | secretAccessKey: secretAccessKey, 43 | sessionToken: nil) 44 | 45 | self.dynamodbGenerator = _AWSDynamoDBClientGenerator(credentialsProvider: staticCredentials, 46 | awsRegion: region, 47 | endpointHostName: endpointHostName, 48 | endpointPort: endpointPort, requiresTLS: requiresTLS, 49 | connectionTimeoutSeconds: connectionTimeoutSeconds, 50 | retryConfiguration: retryConfiguration, 51 | eventLoopProvider: eventLoopProvider, 52 | reportingConfiguration: reportingConfiguration) 53 | self.targetTableName = tableName 54 | } 55 | 56 | public init(credentialsProvider: CredentialsProvider, 57 | region: AWSRegion, 58 | endpointHostName: String, endpointPort: Int = 443, 59 | requiresTLS: Bool? = nil, tableName: String, 60 | connectionTimeoutSeconds: Int64 = 10, 61 | retryConfiguration: HTTPClientRetryConfiguration = .default, 62 | eventLoopProvider: HTTPClient.EventLoopGroupProvider = .createNew, 63 | reportingConfiguration: SmokeAWSCore.SmokeAWSClientReportingConfiguration 64 | = SmokeAWSClientReportingConfiguration()) { 65 | self.dynamodbGenerator = _AWSDynamoDBClientGenerator(credentialsProvider: credentialsProvider, 66 | awsRegion: region, 67 | endpointHostName: endpointHostName, 68 | endpointPort: endpointPort, requiresTLS: requiresTLS, 69 | connectionTimeoutSeconds: connectionTimeoutSeconds, 70 | retryConfiguration: retryConfiguration, 71 | eventLoopProvider: eventLoopProvider, 72 | reportingConfiguration: reportingConfiguration) 73 | self.targetTableName = tableName 74 | } 75 | 76 | /** 77 | Gracefully shuts down the client behind this table. This function is idempotent and 78 | will handle being called multiple times. Will block until shutdown is complete. 79 | */ 80 | public func syncShutdown() throws { 81 | try self.dynamodbGenerator.syncShutdown() 82 | } 83 | 84 | // renamed `syncShutdown` to make it clearer this version of shutdown will block. 85 | @available(*, deprecated, renamed: "syncShutdown") 86 | public func close() throws { 87 | try self.dynamodbGenerator.close() 88 | } 89 | 90 | /** 91 | Gracefully shuts down the client behind this table. This function is idempotent and 92 | will handle being called multiple times. Will return when shutdown is complete. 93 | */ 94 | #if (os(Linux) && compiler(>=5.5)) || (!os(Linux) && compiler(>=5.5.2)) && canImport(_Concurrency) 95 | public func shutdown() async throws { 96 | try await self.dynamodbGenerator.shutdown() 97 | } 98 | #endif 99 | 100 | public func with( 101 | reporting: NewInvocationReportingType) -> AWSDynamoDBCompositePrimaryKeysProjection { 102 | return AWSDynamoDBCompositePrimaryKeysProjection( 103 | dynamodb: self.dynamodbGenerator.with(reporting: reporting), 104 | targetTableName: self.targetTableName, 105 | logger: reporting.logger) 106 | } 107 | 108 | public func with( 109 | logger: Logging.Logger, 110 | internalRequestId: String = "none", 111 | traceContext: NewTraceContextType, 112 | eventLoop: EventLoop? = nil) -> AWSDynamoDBCompositePrimaryKeysProjection> { 113 | let reporting = StandardHTTPClientCoreInvocationReporting( 114 | logger: logger, 115 | internalRequestId: internalRequestId, 116 | traceContext: traceContext, 117 | eventLoop: eventLoop) 118 | 119 | return with(reporting: reporting) 120 | } 121 | 122 | public func with( 123 | logger: Logging.Logger, 124 | internalRequestId: String = "none", 125 | eventLoop: EventLoop? = nil) -> AWSDynamoDBCompositePrimaryKeysProjection> { 126 | let reporting = StandardHTTPClientCoreInvocationReporting( 127 | logger: logger, 128 | internalRequestId: internalRequestId, 129 | traceContext: AWSClientInvocationTraceContext(), 130 | eventLoop: eventLoop) 131 | 132 | return with(reporting: reporting) 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /Sources/SmokeDynamoDB/InternalSingleValueDecodingContainer.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // InternalSingleValueDecodingContainer.swift 15 | // SmokeDynamoDB 16 | // 17 | 18 | import Foundation 19 | import DynamoDBModel 20 | 21 | internal struct InternalSingleValueDecodingContainer { 22 | internal let codingPath: [CodingKey] 23 | internal let userInfo: [CodingUserInfoKey: Any] 24 | internal let attributeValue: DynamoDBModel.AttributeValue 25 | internal let attributeNameTransform: ((String) -> String)? 26 | 27 | init(attributeValue: DynamoDBModel.AttributeValue, 28 | codingPath: [CodingKey], 29 | userInfo: [CodingUserInfoKey: Any], 30 | attributeNameTransform: ((String) -> String)?) { 31 | self.attributeValue = attributeValue 32 | self.codingPath = codingPath 33 | self.userInfo = userInfo 34 | self.attributeNameTransform = attributeNameTransform 35 | } 36 | } 37 | 38 | extension InternalSingleValueDecodingContainer: SingleValueDecodingContainer { 39 | func decodeNil() -> Bool { 40 | return attributeValue.NULL ?? false 41 | } 42 | 43 | func decode(_ type: Bool.Type) throws -> Bool { 44 | guard let value = attributeValue.BOOL else { 45 | throw getTypeMismatchError(expectation: Bool.self) 46 | } 47 | 48 | return value 49 | } 50 | 51 | func decode(_ type: Int.Type) throws -> Int { 52 | guard let valueAsString = attributeValue.N, 53 | let value = Int(valueAsString) else { 54 | throw getTypeMismatchError(expectation: Int.self) 55 | } 56 | 57 | return value 58 | } 59 | 60 | func decode(_ type: Int8.Type) throws -> Int8 { 61 | guard let valueAsString = attributeValue.N, 62 | let value = Int8(valueAsString) else { 63 | throw getTypeMismatchError(expectation: Int8.self) 64 | } 65 | 66 | return value 67 | } 68 | 69 | func decode(_ type: Int16.Type) throws -> Int16 { 70 | guard let valueAsString = attributeValue.N, 71 | let value = Int16(valueAsString) else { 72 | throw getTypeMismatchError(expectation: Int16.self) 73 | } 74 | 75 | return value 76 | } 77 | 78 | func decode(_ type: Int32.Type) throws -> Int32 { 79 | guard let valueAsString = attributeValue.N, 80 | let value = Int32(valueAsString) else { 81 | throw getTypeMismatchError(expectation: Int32.self) 82 | } 83 | 84 | return value 85 | } 86 | 87 | func decode(_ type: Int64.Type) throws -> Int64 { 88 | guard let valueAsString = attributeValue.N, 89 | let value = Int64(valueAsString) else { 90 | throw getTypeMismatchError(expectation: Int64.self) 91 | } 92 | 93 | return value 94 | } 95 | 96 | func decode(_ type: UInt.Type) throws -> UInt { 97 | guard let valueAsString = attributeValue.N, 98 | let value = UInt(valueAsString) else { 99 | throw getTypeMismatchError(expectation: UInt.self) 100 | } 101 | 102 | return value 103 | } 104 | 105 | func decode(_ type: UInt8.Type) throws -> UInt8 { 106 | guard let valueAsString = attributeValue.N, 107 | let value = UInt8(valueAsString) else { 108 | throw getTypeMismatchError(expectation: UInt8.self) 109 | } 110 | 111 | return value 112 | } 113 | 114 | func decode(_ type: UInt16.Type) throws -> UInt16 { 115 | guard let valueAsString = attributeValue.N, 116 | let value = UInt16(valueAsString) else { 117 | throw getTypeMismatchError(expectation: UInt16.self) 118 | } 119 | 120 | return value 121 | } 122 | 123 | func decode(_ type: UInt32.Type) throws -> UInt32 { 124 | guard let valueAsString = attributeValue.N, 125 | let value = UInt32(valueAsString) else { 126 | throw getTypeMismatchError(expectation: UInt32.self) 127 | } 128 | 129 | return value 130 | } 131 | 132 | func decode(_ type: UInt64.Type) throws -> UInt64 { 133 | guard let valueAsString = attributeValue.N, 134 | let value = UInt64(valueAsString) else { 135 | throw getTypeMismatchError(expectation: UInt64.self) 136 | } 137 | 138 | return value 139 | } 140 | 141 | func decode(_ type: Float.Type) throws -> Float { 142 | guard let valueAsString = attributeValue.N, 143 | let value = Float(valueAsString) else { 144 | throw getTypeMismatchError(expectation: Float.self) 145 | } 146 | 147 | return value 148 | } 149 | 150 | func decode(_ type: Double.Type) throws -> Double { 151 | guard let valueAsString = attributeValue.N, 152 | let value = Double(valueAsString) else { 153 | throw getTypeMismatchError(expectation: Double.self) 154 | } 155 | 156 | return value 157 | } 158 | 159 | func decode(_ type: String.Type) throws -> String { 160 | guard let value = attributeValue.S else { 161 | throw getTypeMismatchError(expectation: String.self) 162 | } 163 | 164 | return value 165 | } 166 | 167 | func decode(_ type: T.Type) throws -> T where T: Decodable { 168 | if type == Date.self { 169 | let dateAsString = try String(from: self) 170 | 171 | guard let date = dateAsString.dateFromISO8601 as? T else { 172 | throw getTypeMismatchError(expectation: Date.self) 173 | } 174 | 175 | return date 176 | } 177 | 178 | return try T(from: self) 179 | } 180 | 181 | private func getTypeMismatchError(expectation: Any.Type) -> DecodingError { 182 | let description = "Expected to decode \(expectation)." 183 | let context = DecodingError.Context(codingPath: codingPath, debugDescription: description) 184 | 185 | return DecodingError.typeMismatch(expectation, context) 186 | } 187 | } 188 | 189 | extension InternalSingleValueDecodingContainer: Swift.Decoder { 190 | func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer where Key: CodingKey { 191 | let container = InternalKeyedDecodingContainer(decodingContainer: self) 192 | 193 | return KeyedDecodingContainer(container) 194 | } 195 | 196 | func unkeyedContainer() throws -> UnkeyedDecodingContainer { 197 | let container = InternalUnkeyedDecodingContainer(decodingContainer: self) 198 | 199 | return container 200 | } 201 | 202 | func singleValueContainer() throws -> SingleValueDecodingContainer { 203 | return self 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /Sources/SmokeDynamoDB/DynamoDBCompositePrimaryKeyGSILogic.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // DynamoDBCompositePrimaryKeyGSILogic.swift 15 | // SmokeDynamoDB 16 | // 17 | 18 | import Foundation 19 | import SmokeHTTPClient 20 | import DynamoDBModel 21 | import NIO 22 | 23 | // Provide a default `PolymorphicWriteEntry` for the `DynamoDBCompositePrimaryKeyGSILogic` for backwards compatibility 24 | public struct NoOpPolymorphicWriteEntry: PolymorphicWriteEntry { 25 | public func handle(context: Context) throws -> Context.WriteEntryTransformType where Context : PolymorphicWriteEntryContext { 26 | fatalError("Unimplemented") 27 | } 28 | } 29 | 30 | /** 31 | A protocol that simulates the logic of a GSI reacting to events on the main table. 32 | */ 33 | public protocol DynamoDBCompositePrimaryKeyGSILogic { 34 | associatedtype GSIAttributesType: PrimaryKeyAttributes 35 | associatedtype WriteEntryType: PolymorphicWriteEntry = NoOpPolymorphicWriteEntry 36 | /** 37 | * Called when an item is inserted on the main table. Can be used to transform the provided item to the item that would be made available on the GSI. 38 | */ 39 | func onInsertItem(_ item: TypedDatabaseItem, 40 | gsiDataStore: InMemoryDynamoDBCompositePrimaryKeyTable) -> EventLoopFuture 41 | 42 | /** 43 | * Called when an item is clobbered on the main table. Can be used to transform the provided item to the item that would be made available on the GSI. 44 | */ 45 | func onClobberItem(_ item: TypedDatabaseItem, 46 | gsiDataStore: InMemoryDynamoDBCompositePrimaryKeyTable) -> EventLoopFuture 47 | 48 | /** 49 | * Called when an item is updated on the main table. Can be used to transform the provided item to the item that would be made available on the GSI. 50 | */ 51 | func onUpdateItem(newItem: TypedDatabaseItem, 52 | existingItem: TypedDatabaseItem, 53 | gsiDataStore: InMemoryDynamoDBCompositePrimaryKeyTable) -> EventLoopFuture 54 | 55 | /** 56 | * Called when an item is delete on the main table. Can be used to also delete the corresponding item on the GSI. 57 | 58 | */ 59 | func onDeleteItem(forKey key: CompositePrimaryKey, 60 | gsiDataStore: InMemoryDynamoDBCompositePrimaryKeyTable) -> EventLoopFuture 61 | 62 | #if (os(Linux) && compiler(>=5.5)) || (!os(Linux) && compiler(>=5.5.2)) && canImport(_Concurrency) 63 | /** 64 | * Called when an item is inserted on the main table. Can be used to transform the provided item to the item that would be made available on the GSI. 65 | */ 66 | func onInsertItem(_ item: TypedDatabaseItem, 67 | gsiDataStore: InMemoryDynamoDBCompositePrimaryKeyTable) async throws 68 | 69 | /** 70 | * Called when an item is clobbered on the main table. Can be used to transform the provided item to the item that would be made available on the GSI. 71 | */ 72 | func onClobberItem(_ item: TypedDatabaseItem, 73 | gsiDataStore: InMemoryDynamoDBCompositePrimaryKeyTable) async throws 74 | 75 | /** 76 | * Called when an item is updated on the main table. Can be used to transform the provided item to the item that would be made available on the GSI. 77 | */ 78 | func onUpdateItem(newItem: TypedDatabaseItem, 79 | existingItem: TypedDatabaseItem, 80 | gsiDataStore: InMemoryDynamoDBCompositePrimaryKeyTable) async throws 81 | 82 | /** 83 | * Called when an item is delete on the main table. Can be used to also delete the corresponding item on the GSI. 84 | 85 | */ 86 | func onDeleteItem(forKey key: CompositePrimaryKey, 87 | gsiDataStore: InMemoryDynamoDBCompositePrimaryKeyTable) async throws 88 | 89 | /** 90 | * Called when an transact write in the main table. Can be used to also transact write the corresponding item on the GSI. 91 | 92 | */ 93 | func onTransactWrite(_ entries: [WriteEntryType], 94 | gsiDataStore: InMemoryDynamoDBCompositePrimaryKeyTable) async throws 95 | #endif 96 | } 97 | 98 | // For async/await APIs, simply delegate to the EventLoopFuture implementation until support is dropped for Swift <5.5 99 | public extension DynamoDBCompositePrimaryKeyGSILogic { 100 | #if (os(Linux) && compiler(>=5.5)) || (!os(Linux) && compiler(>=5.5.2)) && canImport(_Concurrency) 101 | func onInsertItem(_ item: TypedDatabaseItem, 102 | gsiDataStore: InMemoryDynamoDBCompositePrimaryKeyTable) async throws { 103 | try await onInsertItem(item, gsiDataStore: gsiDataStore).get() 104 | } 105 | 106 | /** 107 | * Called when an item is clobbered on the main table. Can be used to transform the provided item to the item that would be made available on the GSI. 108 | */ 109 | func onClobberItem(_ item: TypedDatabaseItem, 110 | gsiDataStore: InMemoryDynamoDBCompositePrimaryKeyTable) async throws { 111 | try await onClobberItem(item, gsiDataStore: gsiDataStore).get() 112 | } 113 | 114 | /** 115 | * Called when an item is updated on the main table. Can be used to transform the provided item to the item that would be made available on the GSI. 116 | */ 117 | func onUpdateItem(newItem: TypedDatabaseItem, 118 | existingItem: TypedDatabaseItem, 119 | gsiDataStore: InMemoryDynamoDBCompositePrimaryKeyTable) async throws { 120 | try await onUpdateItem(newItem: newItem, existingItem: existingItem, gsiDataStore: gsiDataStore).get() 121 | } 122 | 123 | /** 124 | * Called when an item is delete on the main table. Can be used to also delete the corresponding item on the GSI. 125 | 126 | */ 127 | func onDeleteItem(forKey key: CompositePrimaryKey, 128 | gsiDataStore: InMemoryDynamoDBCompositePrimaryKeyTable) async throws { 129 | try await onDeleteItem(forKey: key, gsiDataStore: gsiDataStore).get() 130 | } 131 | 132 | // provide default for backwards compatibility 133 | func onTransactWrite(_ entries: [WriteEntryType], 134 | gsiDataStore: InMemoryDynamoDBCompositePrimaryKeyTable) async throws { 135 | // do nothing 136 | } 137 | #endif 138 | } 139 | -------------------------------------------------------------------------------- /Sources/SmokeDynamoDB/InMemoryDynamoDBCompositePrimaryKeysProjectionStore.swift: -------------------------------------------------------------------------------- 1 | // swiftlint:disable cyclomatic_complexity 2 | // Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"). 5 | // You may not use this file except in compliance with the License. 6 | // A copy of the License is located at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // or in the "license" file accompanying this file. This file is distributed 11 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | // express or implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | // 15 | // InMemoryDynamoDBCompositePrimaryKeysProjectionStore.swift 16 | // SmokeDynamoDB 17 | // 18 | 19 | import Foundation 20 | import SmokeHTTPClient 21 | import DynamoDBModel 22 | import NIO 23 | 24 | public class InMemoryDynamoDBCompositePrimaryKeysProjectionStore { 25 | public var keys: [Any] = [] 26 | private let accessQueue = DispatchQueue( 27 | label: "com.amazon.SmokeDynamoDB.InMemoryDynamoDBCompositePrimaryKeysProjection.accessQueue", 28 | target: DispatchQueue.global()) 29 | 30 | public init(keys: [Any] = []) { 31 | self.keys = keys 32 | } 33 | 34 | func getKeys(eventLoop: EventLoop) -> EventLoopFuture<[Any]> { 35 | let promise = eventLoop.makePromise(of: [Any].self) 36 | 37 | accessQueue.async { 38 | promise.succeed(self.keys) 39 | } 40 | 41 | return promise.futureResult 42 | } 43 | 44 | public func query(forPartitionKey partitionKey: String, 45 | sortKeyCondition: AttributeCondition?, 46 | eventLoop: EventLoop) 47 | -> EventLoopFuture<[CompositePrimaryKey]> { 48 | let promise = eventLoop.makePromise(of: [CompositePrimaryKey].self) 49 | 50 | accessQueue.async { 51 | var items: [CompositePrimaryKey] = [] 52 | 53 | let sortedKeys = self.keys.compactMap { $0 as? CompositePrimaryKey }.sorted(by: { (left, right) -> Bool in 54 | return left.sortKey < right.sortKey 55 | }) 56 | 57 | sortKeyIteration: for key in sortedKeys { 58 | if key.partitionKey != partitionKey { 59 | // don't include this in the results 60 | continue sortKeyIteration 61 | } 62 | 63 | let sortKey = key.sortKey 64 | 65 | if let currentSortKeyCondition = sortKeyCondition { 66 | switch currentSortKeyCondition { 67 | case .equals(let value): 68 | if !(value == sortKey) { 69 | // don't include this in the results 70 | continue sortKeyIteration 71 | } 72 | case .lessThan(let value): 73 | if !(sortKey < value) { 74 | // don't include this in the results 75 | continue sortKeyIteration 76 | } 77 | case .lessThanOrEqual(let value): 78 | if !(sortKey <= value) { 79 | // don't include this in the results 80 | continue sortKeyIteration 81 | } 82 | case .greaterThan(let value): 83 | if !(sortKey > value) { 84 | // don't include this in the results 85 | continue sortKeyIteration 86 | } 87 | case .greaterThanOrEqual(let value): 88 | if !(sortKey >= value) { 89 | // don't include this in the results 90 | continue sortKeyIteration 91 | } 92 | case .between(let value1, let value2): 93 | if !(sortKey > value1 && sortKey < value2) { 94 | // don't include this in the results 95 | continue sortKeyIteration 96 | } 97 | case .beginsWith(let value): 98 | if !(sortKey.hasPrefix(value)) { 99 | // don't include this in the results 100 | continue sortKeyIteration 101 | } 102 | } 103 | } 104 | 105 | items.append(key) 106 | } 107 | 108 | promise.succeed(items) 109 | } 110 | 111 | return promise.futureResult 112 | } 113 | 114 | public func query(forPartitionKey partitionKey: String, 115 | sortKeyCondition: AttributeCondition?, 116 | limit: Int?, 117 | exclusiveStartKey: String?, 118 | eventLoop: EventLoop) 119 | -> EventLoopFuture<([CompositePrimaryKey], String?)> 120 | where AttributesType: PrimaryKeyAttributes { 121 | return query(forPartitionKey: partitionKey, 122 | sortKeyCondition: sortKeyCondition, 123 | limit: limit, 124 | scanIndexForward: true, 125 | exclusiveStartKey: exclusiveStartKey, 126 | eventLoop: eventLoop) 127 | } 128 | 129 | public func query(forPartitionKey partitionKey: String, 130 | sortKeyCondition: AttributeCondition?, 131 | limit: Int?, 132 | scanIndexForward: Bool, 133 | exclusiveStartKey: String?, 134 | eventLoop: EventLoop) 135 | -> EventLoopFuture<([CompositePrimaryKey], String?)> 136 | where AttributesType: PrimaryKeyAttributes { 137 | // get all the results 138 | return query(forPartitionKey: partitionKey, 139 | sortKeyCondition: sortKeyCondition, 140 | eventLoop: eventLoop) 141 | .map { (rawItems: [CompositePrimaryKey]) in 142 | let items: [CompositePrimaryKey] 143 | if !scanIndexForward { 144 | items = rawItems.reversed() 145 | } else { 146 | items = rawItems 147 | } 148 | 149 | let startIndex: Int 150 | // if there is an exclusiveStartKey 151 | if let exclusiveStartKey = exclusiveStartKey { 152 | guard let storedStartIndex = Int(exclusiveStartKey) else { 153 | fatalError("Unexpectedly encoded exclusiveStartKey '\(exclusiveStartKey)'") 154 | } 155 | 156 | startIndex = storedStartIndex 157 | } else { 158 | startIndex = 0 159 | } 160 | 161 | let endIndex: Int 162 | let lastEvaluatedKey: String? 163 | if let limit = limit, startIndex + limit < items.count { 164 | endIndex = startIndex + limit 165 | lastEvaluatedKey = String(endIndex) 166 | } else { 167 | endIndex = items.count 168 | lastEvaluatedKey = nil 169 | } 170 | 171 | return (Array(items[startIndex.. 41 | = SmokeAWSClientReportingConfiguration(), 42 | escapeSingleQuoteInPartiQL: Bool = false) { 43 | let staticCredentials = StaticCredentials(accessKeyId: accessKeyId, 44 | secretAccessKey: secretAccessKey, 45 | sessionToken: nil) 46 | 47 | self.dynamodbGenerator = _AWSDynamoDBClientGenerator(credentialsProvider: staticCredentials, 48 | awsRegion: region, 49 | endpointHostName: endpointHostName, 50 | endpointPort: endpointPort, requiresTLS: requiresTLS, 51 | connectionTimeoutSeconds: connectionTimeoutSeconds, 52 | retryConfiguration: retryConfiguration, 53 | eventLoopProvider: eventLoopProvider, 54 | reportingConfiguration: reportingConfiguration) 55 | self.targetTableName = tableName 56 | self.escapeSingleQuoteInPartiQL = escapeSingleQuoteInPartiQL 57 | } 58 | 59 | public init(credentialsProvider: CredentialsProvider, 60 | region: AWSRegion, 61 | endpointHostName: String, endpointPort: Int = 443, 62 | requiresTLS: Bool? = nil, tableName: String, 63 | connectionTimeoutSeconds: Int64 = 10, 64 | retryConfiguration: HTTPClientRetryConfiguration = .default, 65 | eventLoopProvider: HTTPClient.EventLoopGroupProvider = .createNew, 66 | reportingConfiguration: SmokeAWSCore.SmokeAWSClientReportingConfiguration 67 | = SmokeAWSClientReportingConfiguration(), 68 | escapeSingleQuoteInPartiQL: Bool = false) { 69 | self.dynamodbGenerator = _AWSDynamoDBClientGenerator(credentialsProvider: credentialsProvider, 70 | awsRegion: region, 71 | endpointHostName: endpointHostName, 72 | endpointPort: endpointPort, requiresTLS: requiresTLS, 73 | connectionTimeoutSeconds: connectionTimeoutSeconds, 74 | retryConfiguration: retryConfiguration, 75 | eventLoopProvider: eventLoopProvider, 76 | reportingConfiguration: reportingConfiguration) 77 | self.targetTableName = tableName 78 | self.escapeSingleQuoteInPartiQL = escapeSingleQuoteInPartiQL 79 | } 80 | 81 | /** 82 | Gracefully shuts down the client behind this table. This function is idempotent and 83 | will handle being called multiple times. Will block until shutdown is complete. 84 | */ 85 | public func syncShutdown() throws { 86 | try self.dynamodbGenerator.syncShutdown() 87 | } 88 | 89 | // renamed `syncShutdown` to make it clearer this version of shutdown will block. 90 | @available(*, deprecated, renamed: "syncShutdown") 91 | public func close() throws { 92 | try self.dynamodbGenerator.close() 93 | } 94 | 95 | /** 96 | Gracefully shuts down the client behind this table. This function is idempotent and 97 | will handle being called multiple times. Will return when shutdown is complete. 98 | */ 99 | #if (os(Linux) && compiler(>=5.5)) || (!os(Linux) && compiler(>=5.5.2)) && canImport(_Concurrency) 100 | public func shutdown() async throws { 101 | try await self.dynamodbGenerator.shutdown() 102 | } 103 | #endif 104 | 105 | public func with( 106 | reporting: NewInvocationReportingType, 107 | tableMetrics: AWSDynamoDBTableMetrics = .init()) 108 | -> AWSDynamoDBCompositePrimaryKeyTable { 109 | return AWSDynamoDBCompositePrimaryKeyTable( 110 | dynamodb: self.dynamodbGenerator.with(reporting: reporting), 111 | targetTableName: self.targetTableName, 112 | escapeSingleQuoteInPartiQL: self.escapeSingleQuoteInPartiQL, 113 | logger: reporting.logger, 114 | tableMetrics: tableMetrics) 115 | } 116 | 117 | public func with( 118 | logger: Logging.Logger, 119 | internalRequestId: String = "none", 120 | traceContext: NewTraceContextType, 121 | eventLoop: EventLoop? = nil, 122 | tableMetrics: AWSDynamoDBTableMetrics = .init()) 123 | -> AWSDynamoDBCompositePrimaryKeyTable> { 124 | let reporting = StandardHTTPClientCoreInvocationReporting( 125 | logger: logger, 126 | internalRequestId: internalRequestId, 127 | traceContext: traceContext, 128 | eventLoop: eventLoop) 129 | 130 | return with(reporting: reporting, tableMetrics: tableMetrics) 131 | } 132 | 133 | public func with( 134 | logger: Logging.Logger, 135 | internalRequestId: String = "none", 136 | eventLoop: EventLoop? = nil, 137 | tableMetrics: AWSDynamoDBTableMetrics = .init()) 138 | -> AWSDynamoDBCompositePrimaryKeyTable> { 139 | let reporting = StandardHTTPClientCoreInvocationReporting( 140 | logger: logger, 141 | internalRequestId: internalRequestId, 142 | traceContext: AWSClientInvocationTraceContext(), 143 | eventLoop: eventLoop) 144 | 145 | return with(reporting: reporting, tableMetrics: tableMetrics) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /Sources/SmokeDynamoDB/DynamoDBCompositePrimaryKeyTable+clobberVersionedItemWithHistoricalRow.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // DynamoDBCompositePrimaryKeyTable+clobberVersionedItemWithHistoricalRow.swift 15 | // SmokeDynamoDB 16 | // 17 | 18 | import Foundation 19 | import NIO 20 | 21 | public extension DynamoDBCompositePrimaryKeyTable { 22 | /** 23 | * This operation provide a mechanism for managing mutable database rows 24 | * and storing all previous versions of that row in a historical partition. 25 | * This operation store the primary item under a "version zero" sort key 26 | * with a payload that replicates the current version of the row. This 27 | * historical partition contains rows for each version, including the 28 | * current version under a sort key for that version. 29 | 30 | - Parameters: 31 | - partitionKey: the partition key to use for the primary (v0) item 32 | - historicalKey: the partition key to use for the historical items 33 | - item: the payload for the new version of the primary item row 34 | - AttributesType: the row identity type 35 | - generateSortKey: generator to provide a sort key for a provided 36 | version number. 37 | - completion: completion handler providing an error that was thrown or nil 38 | */ 39 | func clobberVersionedItemWithHistoricalRow( 40 | forPrimaryKey partitionKey: String, 41 | andHistoricalKey historicalKey: String, 42 | item: ItemType, 43 | primaryKeyType: AttributesType.Type, 44 | generateSortKey: @escaping (Int) -> String) -> EventLoopFuture { 45 | func primaryItemProvider(_ existingItem: TypedDatabaseItem>?) 46 | -> TypedDatabaseItem> { 47 | if let existingItem = existingItem { 48 | // If an item already exists, the inserted item should be created 49 | // from that item (to get an accurate version number) 50 | // with the payload from the default item. 51 | let overWrittenItemRowValue = existingItem.rowValue.createUpdatedItem( 52 | withVersion: existingItem.rowValue.itemVersion + 1, 53 | withValue: item) 54 | return existingItem.createUpdatedItem(withValue: overWrittenItemRowValue) 55 | } 56 | 57 | // If there is no existing item to be overwritten, a new item should be constructed. 58 | let newItemRowValue = RowWithItemVersion.newItem(withValue: item) 59 | let defaultKey = CompositePrimaryKey(partitionKey: partitionKey, sortKey: generateSortKey(0)) 60 | return TypedDatabaseItem.newItem(withKey: defaultKey, andValue: newItemRowValue) 61 | } 62 | 63 | func historicalItemProvider(_ primaryItem: TypedDatabaseItem>) 64 | -> TypedDatabaseItem> { 65 | let sortKey = generateSortKey(primaryItem.rowValue.itemVersion) 66 | let key = CompositePrimaryKey(partitionKey: historicalKey, 67 | sortKey: sortKey) 68 | return TypedDatabaseItem.newItem(withKey: key, andValue: primaryItem.rowValue) 69 | } 70 | 71 | return clobberItemWithHistoricalRow(primaryItemProvider: primaryItemProvider, 72 | historicalItemProvider: historicalItemProvider) 73 | } 74 | 75 | #if (os(Linux) && compiler(>=5.5)) || (!os(Linux) && compiler(>=5.5.2)) && canImport(_Concurrency) 76 | /** 77 | * This operation provide a mechanism for managing mutable database rows 78 | * and storing all previous versions of that row in a historical partition. 79 | * This operation store the primary item under a "version zero" sort key 80 | * with a payload that replicates the current version of the row. This 81 | * historical partition contains rows for each version, including the 82 | * current version under a sort key for that version. 83 | 84 | - Parameters: 85 | - partitionKey: the partition key to use for the primary (v0) item 86 | - historicalKey: the partition key to use for the historical items 87 | - item: the payload for the new version of the primary item row 88 | - AttributesType: the row identity type 89 | - generateSortKey: generator to provide a sort key for a provided 90 | version number. 91 | - completion: completion handler providing an error that was thrown or nil 92 | */ 93 | func clobberVersionedItemWithHistoricalRow( 94 | forPrimaryKey partitionKey: String, 95 | andHistoricalKey historicalKey: String, 96 | item: ItemType, 97 | primaryKeyType: AttributesType.Type, 98 | generateSortKey: @escaping (Int) -> String) async throws { 99 | func primaryItemProvider(_ existingItem: TypedDatabaseItem>?) 100 | -> TypedDatabaseItem> { 101 | if let existingItem = existingItem { 102 | // If an item already exists, the inserted item should be created 103 | // from that item (to get an accurate version number) 104 | // with the payload from the default item. 105 | let overWrittenItemRowValue = existingItem.rowValue.createUpdatedItem( 106 | withVersion: existingItem.rowValue.itemVersion + 1, 107 | withValue: item) 108 | return existingItem.createUpdatedItem(withValue: overWrittenItemRowValue) 109 | } 110 | 111 | // If there is no existing item to be overwritten, a new item should be constructed. 112 | let newItemRowValue = RowWithItemVersion.newItem(withValue: item) 113 | let defaultKey = CompositePrimaryKey(partitionKey: partitionKey, sortKey: generateSortKey(0)) 114 | return TypedDatabaseItem.newItem(withKey: defaultKey, andValue: newItemRowValue) 115 | } 116 | 117 | func historicalItemProvider(_ primaryItem: TypedDatabaseItem>) 118 | -> TypedDatabaseItem> { 119 | let sortKey = generateSortKey(primaryItem.rowValue.itemVersion) 120 | let key = CompositePrimaryKey(partitionKey: historicalKey, 121 | sortKey: sortKey) 122 | return TypedDatabaseItem.newItem(withKey: key, andValue: primaryItem.rowValue) 123 | } 124 | 125 | return try await clobberItemWithHistoricalRow(primaryItemProvider: primaryItemProvider, 126 | historicalItemProvider: historicalItemProvider) 127 | } 128 | #endif 129 | } 130 | -------------------------------------------------------------------------------- /Sources/SmokeDynamoDB/DynamoDBCompositePrimaryKeyTable+consistentReadQuery.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // DynamoDBCompositePrimaryKeyTable+consistentReadQuery.swift 15 | // SmokeDynamoDB 16 | // 17 | 18 | import Foundation 19 | import SmokeHTTPClient 20 | import DynamoDBModel 21 | import NIO 22 | 23 | public extension DynamoDBCompositePrimaryKeyTable { 24 | 25 | func query(forPartitionKey partitionKey: String, 26 | sortKeyCondition: AttributeCondition?) 27 | -> EventLoopFuture<[ReturnedType]> { 28 | return query(forPartitionKey: partitionKey, 29 | sortKeyCondition: sortKeyCondition, 30 | consistentRead: self.consistentRead) 31 | } 32 | 33 | func query(forPartitionKey partitionKey: String, 34 | sortKeyCondition: AttributeCondition?, 35 | limit: Int?, 36 | exclusiveStartKey: String?) 37 | -> EventLoopFuture<([ReturnedType], String?)> { 38 | return query(forPartitionKey: partitionKey, 39 | sortKeyCondition: sortKeyCondition, 40 | limit: limit, 41 | exclusiveStartKey: exclusiveStartKey, 42 | consistentRead: self.consistentRead) 43 | } 44 | 45 | func query(forPartitionKey partitionKey: String, 46 | sortKeyCondition: AttributeCondition?, 47 | limit: Int?, 48 | scanIndexForward: Bool, 49 | exclusiveStartKey: String?) 50 | -> EventLoopFuture<([ReturnedType], String?)> { 51 | return query(forPartitionKey: partitionKey, 52 | sortKeyCondition: sortKeyCondition, 53 | limit: limit, 54 | scanIndexForward: scanIndexForward, 55 | exclusiveStartKey: exclusiveStartKey, 56 | consistentRead: self.consistentRead) 57 | } 58 | 59 | func monomorphicQuery(forPartitionKey partitionKey: String, 60 | sortKeyCondition: AttributeCondition?) 61 | -> EventLoopFuture<[TypedDatabaseItem]> { 62 | return monomorphicQuery(forPartitionKey: partitionKey, 63 | sortKeyCondition: sortKeyCondition, 64 | consistentRead: self.consistentRead) 65 | } 66 | 67 | func monomorphicQuery(forPartitionKey partitionKey: String, 68 | sortKeyCondition: AttributeCondition?, 69 | limit: Int?, 70 | scanIndexForward: Bool, 71 | exclusiveStartKey: String?) 72 | -> EventLoopFuture<([TypedDatabaseItem], String?)> { 73 | return monomorphicQuery(forPartitionKey: partitionKey, 74 | sortKeyCondition: sortKeyCondition, 75 | limit: limit, 76 | scanIndexForward: scanIndexForward, 77 | exclusiveStartKey: exclusiveStartKey, 78 | consistentRead: self.consistentRead) 79 | } 80 | 81 | #if (os(Linux) && compiler(>=5.5)) || (!os(Linux) && compiler(>=5.5.2)) && canImport(_Concurrency) 82 | func query(forPartitionKey partitionKey: String, 83 | sortKeyCondition: AttributeCondition?) async throws 84 | -> [ReturnedType] { 85 | return try await query(forPartitionKey: partitionKey, 86 | sortKeyCondition: sortKeyCondition, 87 | consistentRead: self.consistentRead) 88 | } 89 | 90 | func query(forPartitionKey partitionKey: String, 91 | sortKeyCondition: AttributeCondition?, 92 | limit: Int?, 93 | exclusiveStartKey: String?) async throws 94 | -> ([ReturnedType], String?) { 95 | return try await query(forPartitionKey: partitionKey, 96 | sortKeyCondition: sortKeyCondition, 97 | limit: limit, 98 | exclusiveStartKey: exclusiveStartKey, 99 | consistentRead: self.consistentRead) 100 | } 101 | 102 | func query(forPartitionKey partitionKey: String, 103 | sortKeyCondition: AttributeCondition?, 104 | limit: Int?, 105 | scanIndexForward: Bool, 106 | exclusiveStartKey: String?) async throws 107 | -> ([ReturnedType], String?) { 108 | return try await query(forPartitionKey: partitionKey, 109 | sortKeyCondition: sortKeyCondition, 110 | limit: limit, 111 | scanIndexForward: scanIndexForward, 112 | exclusiveStartKey: exclusiveStartKey, 113 | consistentRead: self.consistentRead) 114 | } 115 | 116 | func monomorphicQuery(forPartitionKey partitionKey: String, 117 | sortKeyCondition: AttributeCondition?) async throws 118 | -> [TypedDatabaseItem] { 119 | return try await monomorphicQuery(forPartitionKey: partitionKey, 120 | sortKeyCondition: sortKeyCondition, 121 | consistentRead: self.consistentRead) 122 | } 123 | 124 | func monomorphicQuery(forPartitionKey partitionKey: String, 125 | sortKeyCondition: AttributeCondition?, 126 | limit: Int?, 127 | scanIndexForward: Bool, 128 | exclusiveStartKey: String?) async throws 129 | -> ([TypedDatabaseItem], String?) { 130 | return try await monomorphicQuery(forPartitionKey: partitionKey, 131 | sortKeyCondition: sortKeyCondition, 132 | limit: limit, 133 | scanIndexForward: scanIndexForward, 134 | exclusiveStartKey: exclusiveStartKey, 135 | consistentRead: self.consistentRead) 136 | } 137 | #endif 138 | } 139 | -------------------------------------------------------------------------------- /Sources/SmokeDynamoDB/InMemoryDynamoDBCompositePrimaryKeyTableStore+execute.swift: -------------------------------------------------------------------------------- 1 | // swiftlint:disable cyclomatic_complexity 2 | // Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"). 5 | // You may not use this file except in compliance with the License. 6 | // A copy of the License is located at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // or in the "license" file accompanying this file. This file is distributed 11 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | // express or implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | // 15 | // InMemoryDynamoDBCompositePrimaryKeyTableStore+execute.swift 16 | // SmokeDynamoDB 17 | // 18 | 19 | import Foundation 20 | import SmokeHTTPClient 21 | import DynamoDBModel 22 | import NIO 23 | 24 | extension InMemoryDynamoDBCompositePrimaryKeyTableStore { 25 | 26 | func execute( 27 | partitionKeys: [String], 28 | attributesFilter: [String]?, 29 | additionalWhereClause: String?, 30 | eventLoop: EventLoop) -> EventLoopFuture<[ReturnedType]> { 31 | let promise = eventLoop.makePromise(of: [ReturnedType].self) 32 | 33 | accessQueue.async { 34 | let items = self.getExecuteItems(partitionKeys: partitionKeys, additionalWhereClause: additionalWhereClause) 35 | 36 | let returnedItems: [ReturnedType] 37 | do { 38 | returnedItems = try items.map { item in 39 | return try self.convertToQueryableType(input: item) 40 | } 41 | } catch { 42 | promise.fail(error) 43 | return 44 | } 45 | 46 | promise.succeed(returnedItems) 47 | } 48 | 49 | return promise.futureResult 50 | } 51 | 52 | func execute( 53 | partitionKeys: [String], 54 | attributesFilter: [String]?, 55 | additionalWhereClause: String?, 56 | nextToken: String?, 57 | eventLoop: EventLoop) -> EventLoopFuture<([ReturnedType], String?)> { 58 | let promise = eventLoop.makePromise(of: ([ReturnedType], String?).self) 59 | 60 | accessQueue.async { 61 | let items = self.getExecuteItems(partitionKeys: partitionKeys, additionalWhereClause: additionalWhereClause) 62 | 63 | let returnedItems: [ReturnedType] 64 | do { 65 | returnedItems = try items.map { item in 66 | return try self.convertToQueryableType(input: item) 67 | } 68 | } catch { 69 | promise.fail(error) 70 | return 71 | } 72 | 73 | promise.succeed((returnedItems, nil)) 74 | } 75 | 76 | return promise.futureResult 77 | } 78 | 79 | func monomorphicExecute( 80 | partitionKeys: [String], 81 | attributesFilter: [String]?, 82 | additionalWhereClause: String?, 83 | eventLoop: EventLoop) 84 | -> EventLoopFuture<[TypedDatabaseItem]> { 85 | let promise = eventLoop.makePromise(of: [TypedDatabaseItem].self) 86 | 87 | accessQueue.async { 88 | let items = self.getExecuteItems(partitionKeys: partitionKeys, additionalWhereClause: additionalWhereClause) 89 | 90 | let returnedItems: [TypedDatabaseItem] 91 | do { 92 | returnedItems = try items.map { item in 93 | guard let typedItem = item as? TypedDatabaseItem else { 94 | let foundType = type(of: item) 95 | let description = "Expected to decode \(TypedDatabaseItem.self). Instead found \(foundType)." 96 | let context = DecodingError.Context(codingPath: [], debugDescription: description) 97 | let error = DecodingError.typeMismatch(TypedDatabaseItem.self, context) 98 | 99 | throw error 100 | } 101 | 102 | return typedItem 103 | } 104 | } catch { 105 | promise.fail(error) 106 | return 107 | } 108 | 109 | promise.succeed(returnedItems) 110 | } 111 | 112 | return promise.futureResult 113 | } 114 | 115 | func monomorphicExecute( 116 | partitionKeys: [String], 117 | attributesFilter: [String]?, 118 | additionalWhereClause: String?, 119 | nextToken: String?, 120 | eventLoop: EventLoop) 121 | -> EventLoopFuture<([TypedDatabaseItem], String?)> { 122 | let promise = eventLoop.makePromise(of: ([TypedDatabaseItem], String?).self) 123 | 124 | accessQueue.async { 125 | let items = self.getExecuteItems(partitionKeys: partitionKeys, additionalWhereClause: additionalWhereClause) 126 | 127 | let returnedItems: [TypedDatabaseItem] 128 | do { 129 | returnedItems = try items.map { item in 130 | guard let typedItem = item as? TypedDatabaseItem else { 131 | let foundType = type(of: item) 132 | let description = "Expected to decode \(TypedDatabaseItem.self). Instead found \(foundType)." 133 | let context = DecodingError.Context(codingPath: [], debugDescription: description) 134 | let error = DecodingError.typeMismatch(TypedDatabaseItem.self, context) 135 | 136 | throw error 137 | } 138 | 139 | return typedItem 140 | } 141 | } catch { 142 | promise.fail(error) 143 | return 144 | } 145 | 146 | promise.succeed((returnedItems, nil)) 147 | } 148 | 149 | return promise.futureResult 150 | } 151 | 152 | func getExecuteItems(partitionKeys: [String], 153 | additionalWhereClause: String?) -> [PolymorphicOperationReturnTypeConvertable] { 154 | var items: [PolymorphicOperationReturnTypeConvertable] = [] 155 | partitionKeys.forEach { partitionKey in 156 | guard let partition = self.store[partitionKey] else { 157 | // no such partition, continue 158 | return 159 | } 160 | 161 | partition.forEach { (sortKey, databaseItem) in 162 | // if there is an additional where clause 163 | if let additionalWhereClause = additionalWhereClause { 164 | // there must be an executeItemFilter 165 | if let executeItemFilter = self.executeItemFilter { 166 | if executeItemFilter(partitionKey, sortKey, additionalWhereClause, databaseItem) { 167 | // add if the filter says yes 168 | items.append(databaseItem) 169 | } 170 | } else { 171 | fatalError("An executeItemFilter must be provided when an excute call includes an additionalWhereClause") 172 | } 173 | } else { 174 | // otherwise just add the item 175 | items.append(databaseItem) 176 | } 177 | } 178 | } 179 | 180 | return items 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /Sources/SmokeDynamoDB/AWSDynamoDBCompositePrimaryKeyTable+deleteItems.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // AWSDynamoDBCompositePrimaryKeyTable+deleteItems.swift 15 | // SmokeDynamoDB 16 | // 17 | 18 | import Foundation 19 | import SmokeAWSCore 20 | import DynamoDBModel 21 | import SmokeHTTPClient 22 | import Logging 23 | import NIO 24 | 25 | // BatchExecuteStatement has a maximum of 25 statements 26 | private let maximumUpdatesPerExecuteStatement = 25 27 | 28 | /// DynamoDBTable conformance updateItems function 29 | public extension AWSDynamoDBCompositePrimaryKeyTable { 30 | private func deleteChunkedItems(_ keys: [CompositePrimaryKey]) 31 | -> EventLoopFuture { 32 | // if there are no keys, there is nothing to update 33 | guard keys.count > 0 else { 34 | let promise = self.eventLoop.makePromise(of: Void.self) 35 | promise.succeed(()) 36 | return promise.futureResult 37 | } 38 | 39 | let statements: [BatchStatementRequest] 40 | do { 41 | statements = try keys.map { existingKey -> BatchStatementRequest in 42 | let statement = try getDeleteExpression(tableName: self.targetTableName, 43 | existingKey: existingKey) 44 | 45 | // doesn't require read consistency as no items are being read 46 | return BatchStatementRequest(consistentRead: false, statement: statement) 47 | } 48 | } catch { 49 | let promise = self.eventLoop.makePromise(of: Void.self) 50 | promise.fail(error) 51 | return promise.futureResult 52 | } 53 | 54 | let executeInput = BatchExecuteStatementInput(statements: statements) 55 | 56 | return dynamodb.batchExecuteStatement(input: executeInput).flatMapThrowing { response in 57 | try self.throwOnBatchExecuteStatementErrors(response: response) 58 | } 59 | } 60 | 61 | private func deleteChunkedItems(_ existingItems: [ItemType]) 62 | -> EventLoopFuture { 63 | // if there are no items, there is nothing to update 64 | guard existingItems.count > 0 else { 65 | let promise = self.eventLoop.makePromise(of: Void.self) 66 | promise.succeed(()) 67 | return promise.futureResult 68 | } 69 | 70 | let statements: [BatchStatementRequest] 71 | do { 72 | statements = try existingItems.map { existingItem -> BatchStatementRequest in 73 | let statement = try getDeleteExpression(tableName: self.targetTableName, 74 | existingItem: existingItem) 75 | 76 | // doesn't require read consistency as no items are being read 77 | return BatchStatementRequest(consistentRead: false, statement: statement) 78 | } 79 | } catch { 80 | let promise = self.eventLoop.makePromise(of: Void.self) 81 | promise.fail(error) 82 | return promise.futureResult 83 | } 84 | 85 | let executeInput = BatchExecuteStatementInput(statements: statements) 86 | 87 | return dynamodb.batchExecuteStatement(input: executeInput).flatMapThrowing { response in 88 | try self.throwOnBatchExecuteStatementErrors(response: response) 89 | } 90 | } 91 | 92 | func deleteItems(forKeys keys: [CompositePrimaryKey]) -> EventLoopFuture { 93 | // BatchExecuteStatement has a maximum of 25 statements 94 | // This function handles pagination internally. 95 | let chunkedKeys = keys.chunked(by: maximumUpdatesPerExecuteStatement) 96 | let futures = chunkedKeys.map { chunk -> EventLoopFuture in 97 | return deleteChunkedItems(chunk) 98 | } 99 | 100 | return EventLoopFuture.andAllSucceed(futures, on: self.eventLoop) 101 | } 102 | 103 | func deleteItems(existingItems: [ItemType]) -> EventLoopFuture { 104 | // BatchExecuteStatement has a maximum of 25 statements 105 | // This function handles pagination internally. 106 | let chunkedItems = existingItems.chunked(by: maximumUpdatesPerExecuteStatement) 107 | let futures = chunkedItems.map { chunk -> EventLoopFuture in 108 | return deleteChunkedItems(chunk) 109 | } 110 | 111 | return EventLoopFuture.andAllSucceed(futures, on: self.eventLoop) 112 | } 113 | 114 | #if (os(Linux) && compiler(>=5.5)) || (!os(Linux) && compiler(>=5.5.2)) && canImport(_Concurrency) 115 | private func deleteChunkedItems(_ keys: [CompositePrimaryKey]) async throws { 116 | // if there are no keys, there is nothing to update 117 | guard keys.count > 0 else { 118 | return 119 | } 120 | 121 | let statements: [BatchStatementRequest] = try keys.map { existingKey -> BatchStatementRequest in 122 | let statement = try getDeleteExpression(tableName: self.targetTableName, 123 | existingKey: existingKey) 124 | 125 | return BatchStatementRequest(consistentRead: true, statement: statement) 126 | } 127 | 128 | let executeInput = BatchExecuteStatementInput(statements: statements) 129 | 130 | let response = try await self.dynamodb.batchExecuteStatement(input: executeInput) 131 | try throwOnBatchExecuteStatementErrors(response: response) 132 | } 133 | 134 | private func deleteChunkedItems(_ existingItems: [ItemType]) async throws { 135 | // if there are no items, there is nothing to update 136 | guard existingItems.count > 0 else { 137 | return 138 | } 139 | 140 | let statements: [BatchStatementRequest] = try existingItems.map { existingItem -> BatchStatementRequest in 141 | let statement = try getDeleteExpression(tableName: self.targetTableName, 142 | existingItem: existingItem) 143 | 144 | return BatchStatementRequest(consistentRead: true, statement: statement) 145 | } 146 | 147 | let executeInput = BatchExecuteStatementInput(statements: statements) 148 | 149 | let response = try await self.dynamodb.batchExecuteStatement(input: executeInput) 150 | try throwOnBatchExecuteStatementErrors(response: response) 151 | } 152 | 153 | func deleteItems(forKeys keys: [CompositePrimaryKey]) async throws { 154 | // BatchExecuteStatement has a maximum of 25 statements 155 | // This function handles pagination internally. 156 | let chunkedKeys = keys.chunked(by: maximumUpdatesPerExecuteStatement) 157 | try await chunkedKeys.concurrentForEach { chunk in 158 | try await self.deleteChunkedItems(chunk) 159 | } 160 | } 161 | 162 | func deleteItems(existingItems: [ItemType]) async throws { 163 | // BatchExecuteStatement has a maximum of 25 statements 164 | // This function handles pagination internally. 165 | let chunkedItems = existingItems.chunked(by: maximumUpdatesPerExecuteStatement) 166 | try await chunkedItems.concurrentForEach { chunk in 167 | try await self.deleteChunkedItems(chunk) 168 | } 169 | } 170 | #endif 171 | } 172 | -------------------------------------------------------------------------------- /Sources/SmokeDynamoDB/InternalSingleValueEncodingContainer.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). 4 | // You may not use this file except in compliance with the License. 5 | // A copy of the License is located at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | // 14 | // InternalSingleValueEncodingContainer.swift 15 | // SmokeDynamoDB 16 | // 17 | 18 | import Foundation 19 | import DynamoDBModel 20 | import Logging 21 | 22 | internal class InternalSingleValueEncodingContainer: SingleValueEncodingContainer { 23 | internal private(set) var containerValue: ContainerValueType? 24 | internal let attributeNameTransform: ((String) -> String)? 25 | 26 | let codingPath: [CodingKey] 27 | let userInfo: [CodingUserInfoKey: Any] 28 | 29 | init(userInfo: [CodingUserInfoKey: Any], 30 | codingPath: [CodingKey], 31 | attributeNameTransform: ((String) -> String)?, 32 | defaultValue: ContainerValueType?) { 33 | self.containerValue = defaultValue 34 | self.userInfo = userInfo 35 | self.codingPath = codingPath 36 | self.attributeNameTransform = attributeNameTransform 37 | } 38 | 39 | func encodeNil() throws { 40 | containerValue = .singleValue(DynamoDBModel.AttributeValue(NULL: true)) 41 | } 42 | 43 | func encode(_ value: Bool) throws { 44 | containerValue = .singleValue(DynamoDBModel.AttributeValue(BOOL: value)) 45 | } 46 | 47 | func encode(_ value: Int) throws { 48 | containerValue = .singleValue(DynamoDBModel.AttributeValue(N: String(value))) 49 | } 50 | 51 | func encode(_ value: Int8) throws { 52 | containerValue = .singleValue(DynamoDBModel.AttributeValue(N: String(value))) 53 | } 54 | 55 | func encode(_ value: Int16) throws { 56 | containerValue = .singleValue(DynamoDBModel.AttributeValue(N: String(value))) 57 | } 58 | 59 | func encode(_ value: Int32) throws { 60 | containerValue = .singleValue(DynamoDBModel.AttributeValue(N: String(value))) 61 | } 62 | 63 | func encode(_ value: Int64) throws { 64 | containerValue = .singleValue(DynamoDBModel.AttributeValue(N: String(value))) 65 | } 66 | 67 | func encode(_ value: UInt) throws { 68 | containerValue = .singleValue(DynamoDBModel.AttributeValue(N: String(value))) 69 | } 70 | 71 | func encode(_ value: UInt8) throws { 72 | containerValue = .singleValue(DynamoDBModel.AttributeValue(N: String(value))) 73 | } 74 | 75 | func encode(_ value: UInt16) throws { 76 | containerValue = .singleValue(DynamoDBModel.AttributeValue(N: String(value))) 77 | } 78 | 79 | func encode(_ value: UInt32) throws { 80 | containerValue = .singleValue(DynamoDBModel.AttributeValue(N: String(value))) 81 | } 82 | 83 | func encode(_ value: UInt64) throws { 84 | containerValue = .singleValue(DynamoDBModel.AttributeValue(N: String(value))) 85 | } 86 | 87 | func encode(_ value: Float) throws { 88 | containerValue = .singleValue(DynamoDBModel.AttributeValue(N: String(value))) 89 | } 90 | 91 | func encode(_ value: Double) throws { 92 | containerValue = .singleValue(DynamoDBModel.AttributeValue(N: String(value))) 93 | } 94 | 95 | func encode(_ value: String) throws { 96 | containerValue = .singleValue(DynamoDBModel.AttributeValue(S: value)) 97 | } 98 | 99 | func encode(_ value: T) throws where T: Encodable { 100 | if let date = value as? Foundation.Date { 101 | let dateAsString = date.iso8601 102 | 103 | containerValue = .singleValue(DynamoDBModel.AttributeValue(S: dateAsString)) 104 | return 105 | } 106 | 107 | try value.encode(to: self) 108 | } 109 | 110 | func addToKeyedContainer(key: KeyType, value: AttributeValueConvertable) { 111 | guard let currentContainerValue = containerValue else { 112 | fatalError("Attempted to add a keyed item to an unitinialized container.") 113 | } 114 | 115 | guard case .keyedContainer(var values) = currentContainerValue else { 116 | fatalError("Expected keyed container and there wasn't one.") 117 | } 118 | 119 | let attributeName = getAttributeName(key: key) 120 | 121 | values[attributeName] = value 122 | 123 | containerValue = .keyedContainer(values) 124 | } 125 | 126 | func addToUnkeyedContainer(value: AttributeValueConvertable) { 127 | guard let currentContainerValue = containerValue else { 128 | fatalError("Attempted to ad an unkeyed item to an uninitialized container.") 129 | } 130 | 131 | guard case .unkeyedContainer(var values) = currentContainerValue else { 132 | fatalError("Expected unkeyed container and there wasn't one.") 133 | } 134 | 135 | values.append(value) 136 | 137 | containerValue = .unkeyedContainer(values) 138 | } 139 | 140 | private func getAttributeName(key: CodingKey) -> String { 141 | let attributeName: String 142 | if let attributeNameTransform = attributeNameTransform { 143 | attributeName = attributeNameTransform(key.stringValue) 144 | } else { 145 | attributeName = key.stringValue 146 | } 147 | 148 | return attributeName 149 | } 150 | } 151 | 152 | extension InternalSingleValueEncodingContainer: AttributeValueConvertable { 153 | var attributeValue: DynamoDBModel.AttributeValue { 154 | guard let containerValue = containerValue else { 155 | fatalError("Attempted to access uninitialized container.") 156 | } 157 | 158 | switch containerValue { 159 | case .singleValue(let value): 160 | return value.attributeValue 161 | case .unkeyedContainer(let values): 162 | let mappedValues = values.map { value in value.attributeValue } 163 | 164 | return DynamoDBModel.AttributeValue(L: mappedValues) 165 | case .keyedContainer(let values): 166 | let mappedValues = values.mapValues { value in value.attributeValue } 167 | 168 | return DynamoDBModel.AttributeValue(M: mappedValues) 169 | } 170 | } 171 | } 172 | 173 | extension InternalSingleValueEncodingContainer: Swift.Encoder { 174 | var unkeyedContainerCount: Int { 175 | guard let containerValue = containerValue else { 176 | fatalError("Attempted to access unitialized container.") 177 | } 178 | 179 | guard case .unkeyedContainer(let values) = containerValue else { 180 | fatalError("Expected unkeyed container and there wasn't one.") 181 | } 182 | 183 | return values.count 184 | } 185 | 186 | func container(keyedBy type: Key.Type) -> KeyedEncodingContainer where Key: CodingKey { 187 | 188 | // if there container is already initialized 189 | if let currentContainerValue = containerValue { 190 | guard case .keyedContainer = currentContainerValue else { 191 | fatalError("Trying to use an already initialized container as a keyed container.") 192 | } 193 | } else { 194 | containerValue = .keyedContainer([:]) 195 | } 196 | 197 | let container = InternalKeyedEncodingContainer(enclosingContainer: self) 198 | 199 | return KeyedEncodingContainer(container) 200 | } 201 | 202 | func unkeyedContainer() -> UnkeyedEncodingContainer { 203 | 204 | // if there container is already initialized 205 | if let currentContainerValue = containerValue { 206 | guard case .unkeyedContainer = currentContainerValue else { 207 | fatalError("Trying to use an already initialized container as an unkeyed container.") 208 | } 209 | } else { 210 | containerValue = .unkeyedContainer([]) 211 | } 212 | 213 | let container = InternalUnkeyedEncodingContainer(enclosingContainer: self) 214 | 215 | return container 216 | } 217 | 218 | func singleValueContainer() -> SingleValueEncodingContainer { 219 | return self 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /Sources/SmokeDynamoDB/InMemoryDynamoDBCompositePrimaryKeyTableStore+monomorphicQuery.swift: -------------------------------------------------------------------------------- 1 | // swiftlint:disable cyclomatic_complexity 2 | // Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"). 5 | // You may not use this file except in compliance with the License. 6 | // A copy of the License is located at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // or in the "license" file accompanying this file. This file is distributed 11 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | // express or implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | // 15 | // InMemoryDynamoDBCompositePrimaryKeyTableStore+monomorphicQuery.swift 16 | // SmokeDynamoDB 17 | // 18 | 19 | import Foundation 20 | import SmokeHTTPClient 21 | import DynamoDBModel 22 | import NIO 23 | 24 | extension InMemoryDynamoDBCompositePrimaryKeyTableStore { 25 | 26 | func monomorphicGetItems( 27 | forKeys keys: [CompositePrimaryKey], 28 | eventLoop: EventLoop) 29 | -> EventLoopFuture<[CompositePrimaryKey: TypedDatabaseItem]> { 30 | let promise = eventLoop.makePromise(of: [CompositePrimaryKey: TypedDatabaseItem].self) 31 | 32 | accessQueue.async { 33 | var map: [CompositePrimaryKey: TypedDatabaseItem] = [:] 34 | 35 | keys.forEach { key in 36 | if let partition = self.store[key.partitionKey] { 37 | 38 | guard let value = partition[key.sortKey] else { 39 | return 40 | } 41 | 42 | guard let item = value as? TypedDatabaseItem else { 43 | let foundType = type(of: value) 44 | let description = "Expected to decode \(TypedDatabaseItem.self). Instead found \(foundType)." 45 | let context = DecodingError.Context(codingPath: [], debugDescription: description) 46 | let error = DecodingError.typeMismatch(TypedDatabaseItem.self, context) 47 | 48 | promise.fail(error) 49 | return 50 | } 51 | 52 | map[key] = item 53 | } 54 | } 55 | 56 | promise.succeed(map) 57 | } 58 | 59 | return promise.futureResult 60 | } 61 | 62 | func monomorphicQuery(forPartitionKey partitionKey: String, 63 | sortKeyCondition: AttributeCondition?, 64 | eventLoop: EventLoop) 65 | -> EventLoopFuture<[TypedDatabaseItem]> 66 | where AttributesType : PrimaryKeyAttributes, ItemType : Decodable, ItemType : Encodable { 67 | let promise = eventLoop.makePromise(of: [TypedDatabaseItem].self) 68 | 69 | accessQueue.async { 70 | var items: [TypedDatabaseItem] = [] 71 | 72 | if let partition = self.store[partitionKey] { 73 | let sortedPartition = partition.sorted(by: { (left, right) -> Bool in 74 | return left.key < right.key 75 | }) 76 | 77 | sortKeyIteration: for (sortKey, value) in sortedPartition { 78 | if let currentSortKeyCondition = sortKeyCondition { 79 | switch currentSortKeyCondition { 80 | case .equals(let value): 81 | if !(value == sortKey) { 82 | // don't include this in the results 83 | continue sortKeyIteration 84 | } 85 | case .lessThan(let value): 86 | if !(sortKey < value) { 87 | // don't include this in the results 88 | continue sortKeyIteration 89 | } 90 | case .lessThanOrEqual(let value): 91 | if !(sortKey <= value) { 92 | // don't include this in the results 93 | continue sortKeyIteration 94 | } 95 | case .greaterThan(let value): 96 | if !(sortKey > value) { 97 | // don't include this in the results 98 | continue sortKeyIteration 99 | } 100 | case .greaterThanOrEqual(let value): 101 | if !(sortKey >= value) { 102 | // don't include this in the results 103 | continue sortKeyIteration 104 | } 105 | case .between(let value1, let value2): 106 | if !(sortKey > value1 && sortKey < value2) { 107 | // don't include this in the results 108 | continue sortKeyIteration 109 | } 110 | case .beginsWith(let value): 111 | if !(sortKey.hasPrefix(value)) { 112 | // don't include this in the results 113 | continue sortKeyIteration 114 | } 115 | } 116 | } 117 | 118 | if let typedValue = value as? TypedDatabaseItem { 119 | items.append(typedValue) 120 | } else { 121 | let description = "Expected type \(TypedDatabaseItem.self), " 122 | + " was \(type(of: value))." 123 | let context = DecodingError.Context(codingPath: [], debugDescription: description) 124 | let error = DecodingError.typeMismatch(TypedDatabaseItem.self, context) 125 | 126 | promise.fail(error) 127 | return 128 | } 129 | } 130 | } 131 | 132 | promise.succeed(items) 133 | } 134 | 135 | return promise.futureResult 136 | } 137 | 138 | func monomorphicQuery( 139 | forPartitionKey partitionKey: String, 140 | sortKeyCondition: AttributeCondition?, 141 | limit: Int?, 142 | scanIndexForward: Bool, 143 | exclusiveStartKey: String?, 144 | eventLoop: EventLoop) 145 | -> EventLoopFuture<([TypedDatabaseItem], String?)> 146 | where AttributesType : PrimaryKeyAttributes, ItemType : Decodable, ItemType : Encodable { 147 | // get all the results 148 | return monomorphicQuery(forPartitionKey: partitionKey, 149 | sortKeyCondition: sortKeyCondition, 150 | eventLoop: eventLoop) 151 | .map { (rawItems: [TypedDatabaseItem]) in 152 | let items: [TypedDatabaseItem] 153 | if !scanIndexForward { 154 | items = rawItems.reversed() 155 | } else { 156 | items = rawItems 157 | } 158 | 159 | let startIndex: Int 160 | // if there is an exclusiveStartKey 161 | if let exclusiveStartKey = exclusiveStartKey { 162 | guard let storedStartIndex = Int(exclusiveStartKey) else { 163 | fatalError("Unexpectedly encoded exclusiveStartKey '\(exclusiveStartKey)'") 164 | } 165 | 166 | startIndex = storedStartIndex 167 | } else { 168 | startIndex = 0 169 | } 170 | 171 | let endIndex: Int 172 | let lastEvaluatedKey: String? 173 | if let limit = limit, startIndex + limit < items.count { 174 | endIndex = startIndex + limit 175 | lastEvaluatedKey = String(endIndex) 176 | } else { 177 | endIndex = items.count 178 | lastEvaluatedKey = nil 179 | } 180 | 181 | return (Array(items[startIndex..