diff --git a/Examples/runtimes/go/main.go b/Examples/runtimes/go/main.go index ed6f52ff2..318a24bb3 100644 --- a/Examples/runtimes/go/main.go +++ b/Examples/runtimes/go/main.go @@ -9,6 +9,7 @@ import ( "github.com/aws/aws-database-encryption-sdk-dynamodb/examples/keyring" "github.com/aws/aws-database-encryption-sdk-dynamodb/examples/misc" "github.com/aws/aws-database-encryption-sdk-dynamodb/examples/searchableencryption" + "github.com/aws/aws-database-encryption-sdk-dynamodb/examples/searchableencryption/complexexample" "github.com/aws/aws-database-encryption-sdk-dynamodb/examples/utils" ) @@ -94,4 +95,9 @@ func main() { branchKey2, utils.TestKeystoreKmsKeyId(), utils.TestKeystoreName()) + complexexample.ComplexSearchableEncryptionExample( + utils.TestComplexDdbTableName(), + branchKey1, + utils.TestKeystoreKmsKeyId(), + utils.TestKeystoreName()) } diff --git a/Examples/runtimes/go/searchableencryption/complexexample/README.md b/Examples/runtimes/go/searchableencryption/complexexample/README.md new file mode 100644 index 000000000..375f2fa66 --- /dev/null +++ b/Examples/runtimes/go/searchableencryption/complexexample/README.md @@ -0,0 +1,23 @@ +# ComplexSearchableEncryptionExample + +This example demonstrates complex queries +you can perform using beacons. +The example data used is for demonstrative purposes only, +and might not meet the distribution and correlation uniqueness +recommendations for beacons. +See our documentation for whether beacons are +right for your particular data set: +https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/searchable-encryption.html#are-beacons-right-for-me + +This example uses a more complex searchable encryption configuration than other examples. +This is intended to demonstrate how to set up a more complicated searchable encryption configuration. +This also walks through some example query expressions one can use to search their encrypted data. + +``` +. +├── complexsearchableencryptionexample.go // Main entry point for example +├── beaconconfig.go // Sets up beacons and searchable encryption configuration +├── putrequests.go // PUT requests added to the DDB table +├── queryrequests.go // QUERY requests executed on the DDB table +└── README.md +``` diff --git a/Examples/runtimes/go/searchableencryption/complexexample/beaconconfig.go b/Examples/runtimes/go/searchableencryption/complexexample/beaconconfig.go new file mode 100644 index 000000000..c909d719c --- /dev/null +++ b/Examples/runtimes/go/searchableencryption/complexexample/beaconconfig.go @@ -0,0 +1,357 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package complexexample + +import ( + "context" + + keystoreclient "github.com/aws/aws-cryptographic-material-providers-library/releases/go/mpl/awscryptographykeystoresmithygenerated" + keystoretypes "github.com/aws/aws-cryptographic-material-providers-library/releases/go/mpl/awscryptographykeystoresmithygeneratedtypes" + mpl "github.com/aws/aws-cryptographic-material-providers-library/releases/go/mpl/awscryptographymaterialproviderssmithygenerated" + mpltypes "github.com/aws/aws-cryptographic-material-providers-library/releases/go/mpl/awscryptographymaterialproviderssmithygeneratedtypes" + dbesdkdynamodbencryptiontypes "github.com/aws/aws-database-encryption-sdk-dynamodb/awscryptographydbencryptionsdkdynamodbsmithygeneratedtypes" + dbesdkstructuredencryptiontypes "github.com/aws/aws-database-encryption-sdk-dynamodb/awscryptographydbencryptionsdkstructuredencryptionsmithygeneratedtypes" + "github.com/aws/aws-database-encryption-sdk-dynamodb/dbesdkmiddleware" + "github.com/aws/aws-database-encryption-sdk-dynamodb/examples/utils" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/dynamodb" + "github.com/aws/aws-sdk-go-v2/service/kms" +) + +/* + * This file is used in an example to demonstrate complex queries + * you can perform using beacons. + * The example data used is for demonstrative purposes only, + * and might not meet the distribution and correlation uniqueness + * recommendations for beacons. + * See our documentation for whether beacons are + * right for your particular data set: + * https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/searchable-encryption.html#are-beacons-right-for-me + * + * This file sets up all the searchable encryption configuration required to execute the examples from + * our workshop using the encryption client. + */ + +func SetupBeaconConfig( + ctx context.Context, + ddbTableName, + branchKeyId, + branchKeyWrappingKmsKeyArn, + branchKeyDdbTableName string) (*dynamodb.Client, error) { + // 1. Create Keystore and branch key. + // These are the same constructions as in the Basic examples, which describe this in more detail. + cfg, err := config.LoadDefaultConfig(ctx) + if err != nil { + return nil, err + } + + kmsClient := kms.NewFromConfig(cfg) + ddbClient := dynamodb.NewFromConfig(cfg) + + kmsConfig := keystoretypes.KMSConfigurationMemberkmsKeyArn{ + Value: branchKeyWrappingKmsKeyArn, + } + keyStoreConfig := keystoretypes.KeyStoreConfig{ + KmsClient: kmsClient, + DdbClient: ddbClient, + DdbTableName: branchKeyDdbTableName, + LogicalKeyStoreName: branchKeyDdbTableName, + KmsConfiguration: &kmsConfig, + } + + keyStore, err := keystoreclient.NewClient(keyStoreConfig) + if err != nil { + return nil, err + } + + // 2. Create standard beacons + // For this example, we use a standard beacon length of 4. + // The BasicSearchableEncryptionExample gives a more thorough consideration of beacon length. + // For production applications, one should always exercise rigor when deciding beacon length, including + // examining population size and considering performance. + standardBeaconList := []dbesdkdynamodbencryptiontypes.StandardBeacon{ + {Name: "EmployeeID", Length: 4}, + {Name: "TicketNumber", Length: 4}, + {Name: "ProjectName", Length: 4}, + {Name: "EmployeeEmail", Length: 4}, + {Name: "CreatorEmail", Length: 4}, + {Name: "ProjectStatus", Length: 4}, + {Name: "OrganizerEmail", Length: 4}, + {Name: "ManagerEmail", Length: 4}, + {Name: "AssigneeEmail", Length: 4}, + {Name: "City", Loc: StringPtr("Location.City"), Length: 4}, + {Name: "Severity", Length: 4}, + {Name: "Building", Loc: StringPtr("Location.Building"), Length: 4}, + {Name: "Floor", Loc: StringPtr("Location.Floor"), Length: 4}, + {Name: "Room", Loc: StringPtr("Location.Room"), Length: 4}, + {Name: "Desk", Loc: StringPtr("Location.Desk"), Length: 4}, + } + + // 3. Define encrypted parts + // Note that some of the prefixes are modified from the suggested prefixes in Demo.md. + // This is because all prefixes must be unique in a configuration. + // Encrypted parts are described in more detail in the CompoundBeaconSearchableEncryptionExample. + encryptedPartList := []dbesdkdynamodbencryptiontypes.EncryptedPart{ + {Name: "EmployeeID", Prefix: "E-"}, + {Name: "TicketNumber", Prefix: "T-"}, + {Name: "ProjectName", Prefix: "P-"}, + {Name: "EmployeeEmail", Prefix: "EE-"}, + {Name: "CreatorEmail", Prefix: "CE-"}, + {Name: "ProjectStatus", Prefix: "PSts-"}, + {Name: "OrganizerEmail", Prefix: "OE-"}, + {Name: "ManagerEmail", Prefix: "ME-"}, + {Name: "AssigneeEmail", Prefix: "AE-"}, + {Name: "City", Prefix: "C-"}, + {Name: "Severity", Prefix: "S-"}, + {Name: "Building", Prefix: "B-"}, + {Name: "Floor", Prefix: "F-"}, + {Name: "Room", Prefix: "R-"}, + {Name: "Desk", Prefix: "D-"}, + } + + // 4. Define signed parts. + // These are unencrypted attributes we would like to use in beacon queries. + // In this example, all of these represent dates or times. + // Keeping these attributes unencrypted allows us to use them in comparison-based queries. If a signed + // part is the first part in a compound beacon, then that part can be used in comparison for sorting. + signedPartList := []dbesdkdynamodbencryptiontypes.SignedPart{ + {Name: "TicketModTime", Prefix: "M-"}, + {Name: "MeetingStart", Prefix: "MS-"}, + {Name: "TimeCardStart", Prefix: "TC-"}, + {Name: "ProjectStart", Prefix: "PS-"}, + } + + // 5. Create constructor parts. + // Constructor parts are used to assemble constructors (constructors described more in next step). + // For each attribute that will be used in a constructor, there must be a corresponding constructor part. + // A constructor part must receive: + // - name: Name of a standard beacon + // - required: Whether this attribute must be present in the item to match a constructor + // In this example, we will define each constructor part once and re-use it across multiple constructors. + // The parts below are defined by working backwards from the constructors in "PK Constructors", + // "SK constructors", etc. sections in Demo.md. + employeeIdConstructorPart := &dbesdkdynamodbencryptiontypes.ConstructorPart{Name: "EmployeeID", Required: true} + ticketNumberConstructorPart := &dbesdkdynamodbencryptiontypes.ConstructorPart{Name: "TicketNumber", Required: true} + projectNameConstructorPart := &dbesdkdynamodbencryptiontypes.ConstructorPart{Name: "ProjectName", Required: true} + ticketModTimeConstructorPart := &dbesdkdynamodbencryptiontypes.ConstructorPart{Name: "TicketModTime", Required: true} + meetingStartConstructorPart := &dbesdkdynamodbencryptiontypes.ConstructorPart{Name: "MeetingStart", Required: true} + timeCardStartConstructorPart := &dbesdkdynamodbencryptiontypes.ConstructorPart{Name: "TimeCardStart", Required: true} + employeeEmailConstructorPart := &dbesdkdynamodbencryptiontypes.ConstructorPart{Name: "EmployeeEmail", Required: true} + creatorEmailConstructorPart := &dbesdkdynamodbencryptiontypes.ConstructorPart{Name: "CreatorEmail", Required: true} + projectStatusConstructorPart := &dbesdkdynamodbencryptiontypes.ConstructorPart{Name: "ProjectStatus", Required: true} + organizerEmailConstructorPart := &dbesdkdynamodbencryptiontypes.ConstructorPart{Name: "OrganizerEmail", Required: true} + projectStartConstructorPart := &dbesdkdynamodbencryptiontypes.ConstructorPart{Name: "ProjectStart", Required: true} + managerEmailConstructorPart := &dbesdkdynamodbencryptiontypes.ConstructorPart{Name: "ManagerEmail", Required: true} + assigneeEmailConstructorPart := &dbesdkdynamodbencryptiontypes.ConstructorPart{Name: "AssigneeEmail", Required: true} + cityConstructorPart := &dbesdkdynamodbencryptiontypes.ConstructorPart{Name: "City", Required: true} + severityConstructorPart := &dbesdkdynamodbencryptiontypes.ConstructorPart{Name: "Severity", Required: true} + buildingConstructorPart := &dbesdkdynamodbencryptiontypes.ConstructorPart{Name: "Building", Required: true} + floorConstructorPart := &dbesdkdynamodbencryptiontypes.ConstructorPart{Name: "Floor", Required: true} + roomConstructorPart := &dbesdkdynamodbencryptiontypes.ConstructorPart{Name: "Room", Required: true} + deskConstructorPart := &dbesdkdynamodbencryptiontypes.ConstructorPart{Name: "Desk", Required: true} + + // 6. Define constructors + // Constructors define how encrypted and signed parts are assembled into compound beacons. + // The constructors below are based off of the "PK Constructors", "SK constructors", etc. sections in Demo.md. + + // The employee ID constructor only requires an employee ID. + // If an item has an attribute with name "EmployeeID", it will match this constructor. + // If this is the first matching constructor in the constructor list (constructor list described more below), + // the compound beacon will use this constructor, and the compound beacon will be written as `E-X`. + employeeIdConstructor := dbesdkdynamodbencryptiontypes.Constructor{Parts: []dbesdkdynamodbencryptiontypes.ConstructorPart{*employeeIdConstructorPart}} + ticketNumberConstructor := dbesdkdynamodbencryptiontypes.Constructor{Parts: []dbesdkdynamodbencryptiontypes.ConstructorPart{*ticketNumberConstructorPart}} + projectNameConstructor := dbesdkdynamodbencryptiontypes.Constructor{Parts: []dbesdkdynamodbencryptiontypes.ConstructorPart{*projectNameConstructorPart}} + ticketModTimeConstructor := dbesdkdynamodbencryptiontypes.Constructor{Parts: []dbesdkdynamodbencryptiontypes.ConstructorPart{*ticketModTimeConstructorPart}} + buildingConstructor := dbesdkdynamodbencryptiontypes.Constructor{Parts: []dbesdkdynamodbencryptiontypes.ConstructorPart{*buildingConstructorPart}} + + // This constructor requires all of "MeetingStart", "Location.Floor", and "Location.Room" attributes. + // If an item has all of these attributes, it will match this constructor. + // If this is the first matching constructor in the constructor list (constructor list described more below), + // the compound beacon will use this constructor, and the compound beacon will be written as `MS-X~F-Y~R-Z`. + // In a constructor with multiple constructor parts, the order the constructor parts are added to + // the constructor part list defines how the compound beacon is written. + // We can rearrange the beacon parts by changing the order the constructors were added to the list. + meetingStartFloorRoomConstructor := dbesdkdynamodbencryptiontypes.Constructor{ + Parts: []dbesdkdynamodbencryptiontypes.ConstructorPart{*meetingStartConstructorPart, *floorConstructorPart, *roomConstructorPart}, + } + + timeCardStartEmployeeEmailConstructor := dbesdkdynamodbencryptiontypes.Constructor{ + Parts: []dbesdkdynamodbencryptiontypes.ConstructorPart{*timeCardStartConstructorPart, *employeeEmailConstructorPart}, + } + timeCardStartConstructor := dbesdkdynamodbencryptiontypes.Constructor{Parts: []dbesdkdynamodbencryptiontypes.ConstructorPart{*timeCardStartConstructorPart}} + creatorEmailConstructor := dbesdkdynamodbencryptiontypes.Constructor{Parts: []dbesdkdynamodbencryptiontypes.ConstructorPart{*creatorEmailConstructorPart}} + projectStatusConstructor := dbesdkdynamodbencryptiontypes.Constructor{Parts: []dbesdkdynamodbencryptiontypes.ConstructorPart{*projectStatusConstructorPart}} + employeeEmailConstructor := dbesdkdynamodbencryptiontypes.Constructor{Parts: []dbesdkdynamodbencryptiontypes.ConstructorPart{*employeeEmailConstructorPart}} + organizerEmailConstructor := dbesdkdynamodbencryptiontypes.Constructor{Parts: []dbesdkdynamodbencryptiontypes.ConstructorPart{*organizerEmailConstructorPart}} + projectStartConstructor := dbesdkdynamodbencryptiontypes.Constructor{Parts: []dbesdkdynamodbencryptiontypes.ConstructorPart{*projectStartConstructorPart}} + managerEmailConstructor := dbesdkdynamodbencryptiontypes.Constructor{Parts: []dbesdkdynamodbencryptiontypes.ConstructorPart{*managerEmailConstructorPart}} + assigneeEmailConstructor := dbesdkdynamodbencryptiontypes.Constructor{Parts: []dbesdkdynamodbencryptiontypes.ConstructorPart{*assigneeEmailConstructorPart}} + cityConstructor := dbesdkdynamodbencryptiontypes.Constructor{Parts: []dbesdkdynamodbencryptiontypes.ConstructorPart{*cityConstructorPart}} + severityConstructor := dbesdkdynamodbencryptiontypes.Constructor{Parts: []dbesdkdynamodbencryptiontypes.ConstructorPart{*severityConstructorPart}} + buildingFloorDeskConstructor := dbesdkdynamodbencryptiontypes.Constructor{ + Parts: []dbesdkdynamodbencryptiontypes.ConstructorPart{*buildingConstructorPart, *floorConstructorPart, *deskConstructorPart}, + } + + // 7. Add constructors to the compound beacon constructor list in desired construction order. + // In a compound beacon with multiple constructors, the order the constructors are added to + // the constructor list determines their priority. + // The first constructor added to a constructor list will be the first constructor that is executed. + // The client will evaluate constructors until one matches, and will use the first one that matches. + // If no constructors match, an attribute value is not written for that beacon. + // A general strategy is to add constructors with unique conditions at the beginning of the list, + // and add constructors with general conditions at the end of the list. This would allow a given + // item would trigger the constructor most specific to its attributes. + pk0ConstructorList := []dbesdkdynamodbencryptiontypes.Constructor{ + employeeIdConstructor, + buildingConstructor, + ticketNumberConstructor, + projectNameConstructor, + } + + sk0ConstructorList := []dbesdkdynamodbencryptiontypes.Constructor{ + ticketModTimeConstructor, + meetingStartFloorRoomConstructor, + timeCardStartEmployeeEmailConstructor, + projectNameConstructor, + employeeIdConstructor, + } + + pk1ConstructorList := []dbesdkdynamodbencryptiontypes.Constructor{ + creatorEmailConstructor, + employeeEmailConstructor, + projectStatusConstructor, + organizerEmailConstructor, + } + + sk1ConstructorList := []dbesdkdynamodbencryptiontypes.Constructor{ + meetingStartFloorRoomConstructor, + timeCardStartConstructor, + ticketModTimeConstructor, + projectStartConstructor, + employeeIdConstructor, + } + + pk2ConstructorList := []dbesdkdynamodbencryptiontypes.Constructor{ + managerEmailConstructor, + assigneeEmailConstructor, + } + + pk3ConstructorList := []dbesdkdynamodbencryptiontypes.Constructor{ + cityConstructor, + severityConstructor, + } + + sk3ConstructorList := []dbesdkdynamodbencryptiontypes.Constructor{ + buildingFloorDeskConstructor, + ticketModTimeConstructor, + } + + // 8. Define compound beacons + // Compound beacon construction is defined in more detail in CompoundBeaconSearchableEncryptionExample. + // Note that the split character must be a character that is not used in any attribute value. + compoundBeaconList := []dbesdkdynamodbencryptiontypes.CompoundBeacon{ + {Name: "PK", Split: "~", Constructors: pk0ConstructorList}, + {Name: "SK", Split: "~", Constructors: sk0ConstructorList}, + {Name: "PK1", Split: "~", Constructors: pk1ConstructorList}, + {Name: "SK1", Split: "~", Constructors: sk1ConstructorList}, + {Name: "PK2", Split: "~", Constructors: pk2ConstructorList}, + {Name: "PK3", Split: "~", Constructors: pk3ConstructorList}, + {Name: "SK3", Split: "~", Constructors: sk3ConstructorList}, + } + + // 9. Create BeaconVersion. + beaconVersion := &dbesdkdynamodbencryptiontypes.BeaconVersion{ + StandardBeacons: standardBeaconList, + CompoundBeacons: compoundBeaconList, + EncryptedParts: encryptedPartList, + SignedParts: signedPartList, + Version: 1, // MUST be 1 + KeyStore: keyStore, + KeySource: &dbesdkdynamodbencryptiontypes.BeaconKeySourceMembersingle{ + Value: dbesdkdynamodbencryptiontypes.SingleKeyStore{ + KeyId: branchKeyId, + CacheTTL: 6000, + }, + }, + } + + beaconVersions := []dbesdkdynamodbencryptiontypes.BeaconVersion{*beaconVersion} + + // 10. Create a Hierarchical Keyring + matProv, err := mpl.NewClient(mpltypes.MaterialProvidersConfig{}) + utils.HandleError(err) + + hierarchicalKeyringInput := mpltypes.CreateAwsKmsHierarchicalKeyringInput{ + KeyStore: keyStore, + BranchKeyId: &branchKeyId, + TtlSeconds: 6000, // This dictates how often we call back to KMS to authorize use of the branch keys + } + + kmsKeyring, err := matProv.CreateAwsKmsHierarchicalKeyring(context.Background(), hierarchicalKeyringInput) + utils.HandleError(err) + + // 11. Define crypto actions + attributeActionsOnEncrypt := map[string]dbesdkstructuredencryptiontypes.CryptoAction{ + // Our partition key must be configured as SIGN_ONLY + "partition_key": dbesdkstructuredencryptiontypes.CryptoActionSignOnly, + // Attributes used in beacons must be configured as ENCRYPT_AND_SIGN + "EmployeeID": dbesdkstructuredencryptiontypes.CryptoActionEncryptAndSign, + "TicketNumber": dbesdkstructuredencryptiontypes.CryptoActionEncryptAndSign, + "ProjectName": dbesdkstructuredencryptiontypes.CryptoActionEncryptAndSign, + "EmployeeName": dbesdkstructuredencryptiontypes.CryptoActionEncryptAndSign, + "EmployeeEmail": dbesdkstructuredencryptiontypes.CryptoActionEncryptAndSign, + "CreatorEmail": dbesdkstructuredencryptiontypes.CryptoActionEncryptAndSign, + "ProjectStatus": dbesdkstructuredencryptiontypes.CryptoActionEncryptAndSign, + "OrganizerEmail": dbesdkstructuredencryptiontypes.CryptoActionEncryptAndSign, + "ManagerEmail": dbesdkstructuredencryptiontypes.CryptoActionEncryptAndSign, + "AssigneeEmail": dbesdkstructuredencryptiontypes.CryptoActionEncryptAndSign, + "City": dbesdkstructuredencryptiontypes.CryptoActionEncryptAndSign, + "Severity": dbesdkstructuredencryptiontypes.CryptoActionEncryptAndSign, + "Location": dbesdkstructuredencryptiontypes.CryptoActionEncryptAndSign, + // These are not beaconized attributes, but are sensitive data that must be encrypted + "Attendees": dbesdkstructuredencryptiontypes.CryptoActionEncryptAndSign, + "Subject": dbesdkstructuredencryptiontypes.CryptoActionEncryptAndSign, + // signed parts and unencrypted attributes can be configured as SIGN_ONLY or DO_NOTHING + // For this example, we will set these to SIGN_ONLY to ensure authenticity + "TicketModTime": dbesdkstructuredencryptiontypes.CryptoActionSignOnly, + "MeetingStart": dbesdkstructuredencryptiontypes.CryptoActionSignOnly, + "TimeCardStart": dbesdkstructuredencryptiontypes.CryptoActionSignOnly, + "EmployeeTitle": dbesdkstructuredencryptiontypes.CryptoActionSignOnly, + "Description": dbesdkstructuredencryptiontypes.CryptoActionSignOnly, + "ProjectTarget": dbesdkstructuredencryptiontypes.CryptoActionSignOnly, + "Hours": dbesdkstructuredencryptiontypes.CryptoActionSignOnly, + "Role": dbesdkstructuredencryptiontypes.CryptoActionSignOnly, + "Message": dbesdkstructuredencryptiontypes.CryptoActionSignOnly, + "ProjectStart": dbesdkstructuredencryptiontypes.CryptoActionSignOnly, + "Duration": dbesdkstructuredencryptiontypes.CryptoActionSignOnly, + } + + // 12. Set up table config + tableConfig := &dbesdkdynamodbencryptiontypes.DynamoDbTableEncryptionConfig{ + LogicalTableName: ddbTableName, + PartitionKeyName: "partition_key", + AttributeActionsOnEncrypt: attributeActionsOnEncrypt, + Keyring: kmsKeyring, + Search: &dbesdkdynamodbencryptiontypes.SearchConfig{ + WriteVersion: 1, // MUST be 1 + Versions: beaconVersions, + }, + } + + tableConfigs := map[string]dbesdkdynamodbencryptiontypes.DynamoDbTableEncryptionConfig{ + ddbTableName: *tableConfig, + } + + // 13. Create the DynamoDb Encryption Interceptor + encryptionConfig := dbesdkdynamodbencryptiontypes.DynamoDbTablesEncryptionConfig{ + TableEncryptionConfigs: tableConfigs, + } + + // 14. Create a new AWS SDK DynamoDb client using the DynamoDb Encryption Interceptor above + dbEsdkMiddleware, err := dbesdkmiddleware.NewDBEsdkMiddleware(encryptionConfig) + utils.HandleError(err) + ddb := dynamodb.NewFromConfig(cfg, dbEsdkMiddleware.CreateMiddleware()) + + return ddb, nil +} diff --git a/Examples/runtimes/go/searchableencryption/complexexample/complexsearchableencryptionexample.go b/Examples/runtimes/go/searchableencryption/complexexample/complexsearchableencryptionexample.go new file mode 100644 index 000000000..0c0f3c8c5 --- /dev/null +++ b/Examples/runtimes/go/searchableencryption/complexexample/complexsearchableencryptionexample.go @@ -0,0 +1,41 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package complexexample + +import ( + "context" + "fmt" + + "github.com/aws/aws-database-encryption-sdk-dynamodb/examples/utils" +) + +/* + * This file is used in an example to demonstrate complex queries + * you can perform using beacons. + * The example data used is for demonstrative purposes only, + * and might not meet the distribution and correlation uniqueness + * recommendations for beacons. + * See our documentation for whether beacons are + * right for your particular data set: + * https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/searchable-encryption.html#are-beacons-right-for-me + */ +func ComplexSearchableEncryptionExample( + ddbTableName, + branchKeyId, + branchKeyWrappingKmsKeyArn, + branchKeyDdbTableName string, +) { + ddb, err := SetupBeaconConfig( + context.TODO(), + ddbTableName, + branchKeyId, + branchKeyWrappingKmsKeyArn, + branchKeyDdbTableName, + ) + utils.HandleError(err) + PutAllItemsToTable(context.TODO(), ddbTableName, ddb) + RunQueries(context.TODO(), ddbTableName, ddb) + + fmt.Println("Complex Searchable Encryption Example Completed") +} diff --git a/Examples/runtimes/go/searchableencryption/complexexample/putrequests.go b/Examples/runtimes/go/searchableencryption/complexexample/putrequests.go new file mode 100644 index 000000000..625f6b0cd --- /dev/null +++ b/Examples/runtimes/go/searchableencryption/complexexample/putrequests.go @@ -0,0 +1,646 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package complexexample + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/dynamodb" + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" +) + +/* + * This file is used in an example to demonstrate complex queries + * you can perform using beacons. + * The example data used is for demonstrative purposes only, + * and might not meet the distribution and correlation uniqueness + * recommendations for beacons. + * See our documentation for whether beacons are + * right for your particular data set: + * https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/searchable-encryption.html#are-beacons-right-for-me + * + * This file implements PutItem calls to populate a DDB table with items from our workshop. + * By providing a DynamoDbClient that is configured to use searchable encryption, + * this file will encrypt the data client side and add beacon attributes to the items. + * This only implements a single item of each of the 6 record types from Demo.md. Adding the remaining + * items would extend the test and example coverage. + */ + +// PutAllItemsToTable puts all sample items to the specified DynamoDB table +func PutAllItemsToTable(ctx context.Context, ddbTableName string, ddb *dynamodb.Client) error { + if err := PutAllMeetingItemsToTable(ctx, ddbTableName, ddb); err != nil { + return err + } + if err := PutAllEmployeeItemsToTable(ctx, ddbTableName, ddb); err != nil { + return err + } + if err := PutAllProjectItemsToTable(ctx, ddbTableName, ddb); err != nil { + return err + } + if err := PutAllReservationItemsToTable(ctx, ddbTableName, ddb); err != nil { + return err + } + if err := PutAllTicketItemsToTable(ctx, ddbTableName, ddb); err != nil { + return err + } + if err := PutAllTimecardItemsToTable(ctx, ddbTableName, ddb); err != nil { + return err + } + return nil +} + +// Helper function to create string attribute value +func stringAttr(s string) types.AttributeValue { + return &types.AttributeValueMemberS{Value: s} +} + +// Helper function to create map attribute value +func mapAttr(m map[string]types.AttributeValue) types.AttributeValue { + return &types.AttributeValueMemberM{Value: m} +} + +// Helper function to create list attribute value +func listAttr(l []types.AttributeValue) types.AttributeValue { + return &types.AttributeValueMemberL{Value: l} +} + +// meeting.json +// PutAllMeetingItemsToTable puts all meeting items to the table +func PutAllMeetingItemsToTable(ctx context.Context, ddbTableName string, ddb *dynamodb.Client) error { + // Meeting 1 + meeting1AttendeeList := []types.AttributeValue{ + stringAttr("able@gmail.com"), + stringAttr("zorro@gmail.com"), + } + + meeting1Location := map[string]types.AttributeValue{ + "Floor": stringAttr("12"), + "Room": stringAttr("403"), + } + + meeting1 := map[string]types.AttributeValue{ + "partition_key": stringAttr("meeting1"), + "EmployeeID": stringAttr("emp_001"), + "EmployeeEmail": stringAttr("able@gmail.com"), + "MeetingStart": stringAttr("2022-07-04T13:00"), + "Location": mapAttr(meeting1Location), + "Duration": stringAttr("30"), + "Attendees": listAttr(meeting1AttendeeList), + "Subject": stringAttr("Scan Beacons"), + } + + _, err := ddb.PutItem(ctx, &dynamodb.PutItemInput{ + TableName: &ddbTableName, + Item: meeting1, + }) + if err != nil { + return err + } + + // Meeting 2 + meeting2AttendeeList := []types.AttributeValue{ + stringAttr("barney@gmail.com"), + stringAttr("zorro@gmail.com"), + } + + meeting2Location := map[string]types.AttributeValue{ + "Floor": stringAttr("12"), + "Room": stringAttr("403"), + } + + meeting2 := map[string]types.AttributeValue{ + "partition_key": stringAttr("meeting2"), + "EmployeeID": stringAttr("emp_002"), + "EmployeeEmail": stringAttr("barney@gmail.com"), + "MeetingStart": stringAttr("2022-07-04T13:00"), + "Location": mapAttr(meeting2Location), + "Duration": stringAttr("30"), + "Attendees": listAttr(meeting2AttendeeList), + "Subject": stringAttr("Scan Beacons"), + } + + _, err = ddb.PutItem(ctx, &dynamodb.PutItemInput{ + TableName: &ddbTableName, + Item: meeting2, + }) + if err != nil { + return err + } + + // Meeting 3 + meeting3AttendeeList := []types.AttributeValue{ + stringAttr("charlie@gmail.com"), + stringAttr("zorro@gmail.com"), + } + + meeting3Location := map[string]types.AttributeValue{ + "Floor": stringAttr("12"), + "Room": stringAttr("403"), + } + + meeting3 := map[string]types.AttributeValue{ + "partition_key": stringAttr("meeting3"), + "EmployeeID": stringAttr("emp_003"), + "EmployeeEmail": stringAttr("charlie@gmail.com"), + "MeetingStart": stringAttr("2022-07-04T13:00"), + "Location": mapAttr(meeting3Location), + "Duration": stringAttr("30"), + "Attendees": listAttr(meeting3AttendeeList), + "Subject": stringAttr("Scan Beacons"), + } + + _, err = ddb.PutItem(ctx, &dynamodb.PutItemInput{ + TableName: &ddbTableName, + Item: meeting3, + }) + if err != nil { + return err + } + + // Meeting 4 + meeting4AttendeeList := []types.AttributeValue{ + stringAttr("david@gmail.com"), + stringAttr("zorro@gmail.com"), + } + + meeting4Location := map[string]types.AttributeValue{ + "Floor": stringAttr("12"), + "Room": stringAttr("403"), + } + + meeting4 := map[string]types.AttributeValue{ + "partition_key": stringAttr("meeting4"), + "EmployeeID": stringAttr("emp_004"), + "EmployeeEmail": stringAttr("david@gmail.com"), + "MeetingStart": stringAttr("2022-07-04T13:00"), + "Location": mapAttr(meeting4Location), + "Duration": stringAttr("30"), + "Attendees": listAttr(meeting4AttendeeList), + "Subject": stringAttr("Scan Beacons"), + } + + _, err = ddb.PutItem(ctx, &dynamodb.PutItemInput{ + TableName: &ddbTableName, + Item: meeting4, + }) + if err != nil { + return err + } + + // Meeting 5 + meeting5AttendeeList := []types.AttributeValue{ + stringAttr("barney@gmail.com"), + stringAttr("zorro@gmail.com"), + } + + meeting5Location := map[string]types.AttributeValue{ + "Floor": stringAttr("12"), + "Room": stringAttr("407"), + } + + meeting5 := map[string]types.AttributeValue{ + "partition_key": stringAttr("meeting5"), + "EmployeeID": stringAttr("emp_002"), + "EmployeeEmail": stringAttr("barney@gmail.com"), + "MeetingStart": stringAttr("2022-07-04T14:00"), + "Location": mapAttr(meeting5Location), + "Duration": stringAttr("30"), + "Attendees": listAttr(meeting5AttendeeList), + "Subject": stringAttr("DB ESDK"), + } + + _, err = ddb.PutItem(ctx, &dynamodb.PutItemInput{ + TableName: &ddbTableName, + Item: meeting5, + }) + if err != nil { + return err + } + + // Meeting 6 + meeting6AttendeeList := []types.AttributeValue{ + stringAttr("charlie@gmail.com"), + stringAttr("zorro@gmail.com"), + } + + meeting6Location := map[string]types.AttributeValue{ + "Floor": stringAttr("12"), + "Room": stringAttr("407"), + } + + meeting6 := map[string]types.AttributeValue{ + "partition_key": stringAttr("meeting6"), + "EmployeeID": stringAttr("emp_003"), + "EmployeeEmail": stringAttr("charlie@gmail.com"), + "MeetingStart": stringAttr("2022-07-04T14:00"), + "Location": mapAttr(meeting6Location), + "Duration": stringAttr("30"), + "Attendees": listAttr(meeting6AttendeeList), + "Subject": stringAttr("DB ESDK"), + } + + _, err = ddb.PutItem(ctx, &dynamodb.PutItemInput{ + TableName: &ddbTableName, + Item: meeting6, + }) + return err +} + +// employee.json +// PutAllEmployeeItemsToTable puts all employee items to the table +func PutAllEmployeeItemsToTable(ctx context.Context, ddbTableName string, ddb *dynamodb.Client) error { + // Employee 1 + employee1Location := map[string]types.AttributeValue{ + "Building": stringAttr("44"), + "Floor": stringAttr("12"), + "Desk": stringAttr("3"), + "City": stringAttr("Seattle"), + } + + employee1 := map[string]types.AttributeValue{ + "partition_key": stringAttr("employee1"), + "EmployeeID": stringAttr("emp_001"), + "EmployeeEmail": stringAttr("able@gmail.com"), + "ManagerEmail": stringAttr("zorro@gmail.com"), + "EmployeeName": stringAttr("Able Jones"), + "EmployeeTitle": stringAttr("SDE9"), + "Location": mapAttr(employee1Location), + } + + _, err := ddb.PutItem(ctx, &dynamodb.PutItemInput{ + TableName: &ddbTableName, + Item: employee1, + }) + if err != nil { + return err + } + + // Employee 2 + employee2Location := map[string]types.AttributeValue{ + "Building": stringAttr("44"), + "Floor": stringAttr("12"), + "Desk": stringAttr("4"), + "City": stringAttr("Seattle"), + } + + employee2 := map[string]types.AttributeValue{ + "partition_key": stringAttr("employee2"), + "EmployeeID": stringAttr("emp_002"), + "EmployeeEmail": stringAttr("barney@gmail.com"), + "ManagerEmail": stringAttr("zorro@gmail.com"), + "EmployeeName": stringAttr("Barney Jones"), + "EmployeeTitle": stringAttr("SDE8"), + "Location": mapAttr(employee2Location), + } + + _, err = ddb.PutItem(ctx, &dynamodb.PutItemInput{ + TableName: &ddbTableName, + Item: employee2, + }) + if err != nil { + return err + } + + // Employee 3 + employee3Location := map[string]types.AttributeValue{ + "Building": stringAttr("44"), + "Floor": stringAttr("4"), + "Desk": stringAttr("5"), + "City": stringAttr("Seattle"), + } + + employee3 := map[string]types.AttributeValue{ + "partition_key": stringAttr("employee3"), + "EmployeeID": stringAttr("emp_003"), + "EmployeeEmail": stringAttr("charlie@gmail.com"), + "ManagerEmail": stringAttr("zorro@gmail.com"), + "EmployeeName": stringAttr("Charlie Jones"), + "EmployeeTitle": stringAttr("SDE7"), + "Location": mapAttr(employee3Location), + } + + _, err = ddb.PutItem(ctx, &dynamodb.PutItemInput{ + TableName: &ddbTableName, + Item: employee3, + }) + if err != nil { + return err + } + + // Employee 4 + employee4Location := map[string]types.AttributeValue{ + "Building": stringAttr("22"), + "Floor": stringAttr("1"), + "Desk": stringAttr("3"), + "City": stringAttr("NYC"), + } + + employee4 := map[string]types.AttributeValue{ + "partition_key": stringAttr("employee4"), + "EmployeeID": stringAttr("emp_004"), + "EmployeeEmail": stringAttr("david@gmail.com"), + "ManagerEmail": stringAttr("zorro@gmail.com"), + "EmployeeName": stringAttr("David Jones"), + "EmployeeTitle": stringAttr("SDE6"), + "Location": mapAttr(employee4Location), + } + + _, err = ddb.PutItem(ctx, &dynamodb.PutItemInput{ + TableName: &ddbTableName, + Item: employee4, + }) + return err +} + +// project.json +// PutAllProjectItemsToTable puts all project items to the table +func PutAllProjectItemsToTable(ctx context.Context, ddbTableName string, ddb *dynamodb.Client) error { + // Project 1 + project1 := map[string]types.AttributeValue{ + "partition_key": stringAttr("project1"), + "ProjectName": stringAttr("project_001"), + "ProjectStatus": stringAttr("Pending"), + "ProjectStart": stringAttr("2022-11-01"), + "Description": stringAttr("Turbo Crypto"), + "ProjectTarget": stringAttr("2024-01-01"), + } + + _, err := ddb.PutItem(ctx, &dynamodb.PutItemInput{ + TableName: &ddbTableName, + Item: project1, + }) + if err != nil { + return err + } + + // Project 2 + project2 := map[string]types.AttributeValue{ + "partition_key": stringAttr("project2"), + "ProjectName": stringAttr("project_002"), + "ProjectStatus": stringAttr("Active"), + "ProjectStart": stringAttr("2022-07-04"), + "Description": stringAttr("Scan Beacons"), + "ProjectTarget": stringAttr("2024-01-01"), + } + + _, err = ddb.PutItem(ctx, &dynamodb.PutItemInput{ + TableName: &ddbTableName, + Item: project2, + }) + if err != nil { + return err + } + + // Project 3 + project3 := map[string]types.AttributeValue{ + "partition_key": stringAttr("project3"), + "ProjectName": stringAttr("project_003"), + "ProjectStatus": stringAttr("Active"), + "ProjectStart": stringAttr("2022-08-05"), + "Description": stringAttr("DB ESDK"), + "ProjectTarget": stringAttr("2023-02-27"), + } + + _, err = ddb.PutItem(ctx, &dynamodb.PutItemInput{ + TableName: &ddbTableName, + Item: project3, + }) + if err != nil { + return err + } + + // Project 4 + project4 := map[string]types.AttributeValue{ + "partition_key": stringAttr("project4"), + "ProjectName": stringAttr("project_004"), + "ProjectStatus": stringAttr("Done"), + "ProjectStart": stringAttr("2020-03-03"), + "Description": stringAttr("S3EC"), + "ProjectTarget": stringAttr("2021-09-05"), + } + + _, err = ddb.PutItem(ctx, &dynamodb.PutItemInput{ + TableName: &ddbTableName, + Item: project4, + }) + return err +} + +// reservation.json +// PutAllReservationItemsToTable puts all reservation items to the table +func PutAllReservationItemsToTable(ctx context.Context, ddbTableName string, ddb *dynamodb.Client) error { + // Reservation 1 + reservation1AttendeeList := []types.AttributeValue{ + stringAttr("able@gmail.com"), + stringAttr("barney@gmail.com"), + } + + reservation1Location := map[string]types.AttributeValue{ + "Building": stringAttr("SEA33"), + "Floor": stringAttr("12"), + "Room": stringAttr("403"), + } + + reservation1 := map[string]types.AttributeValue{ + "partition_key": stringAttr("reservation1"), + "Location": mapAttr(reservation1Location), + "MeetingStart": stringAttr("2022-07-04T13:00"), + "OrganizerEmail": stringAttr("able@gmail.com"), + "Duration": stringAttr("30"), + "Attendees": listAttr(reservation1AttendeeList), + "Subject": stringAttr("Scan beacons"), + } + + _, err := ddb.PutItem(ctx, &dynamodb.PutItemInput{ + TableName: &ddbTableName, + Item: reservation1, + }) + if err != nil { + return err + } + + // Reservation 2 + reservation2AttendeeList := []types.AttributeValue{ + stringAttr("able@gmail.com"), + stringAttr("barney@gmail.com"), + } + + reservation2Location := map[string]types.AttributeValue{ + "Building": stringAttr("SEA33"), + "Floor": stringAttr("12"), + "Room": stringAttr("407"), + } + + reservation2 := map[string]types.AttributeValue{ + "partition_key": stringAttr("reservation2"), + "Location": mapAttr(reservation2Location), + "MeetingStart": stringAttr("2022-07-04T14:00"), + "OrganizerEmail": stringAttr("barney@gmail.com"), + "Duration": stringAttr("30"), + "Attendees": listAttr(reservation2AttendeeList), + "Subject": stringAttr("DB ESDK"), + } + + _, err = ddb.PutItem(ctx, &dynamodb.PutItemInput{ + TableName: &ddbTableName, + Item: reservation2, + }) + return err +} + +// ticket.json +// PutAllTicketItemsToTable puts all ticket items to the table +func PutAllTicketItemsToTable(ctx context.Context, ddbTableName string, ddb *dynamodb.Client) error { + // Ticket 1 + ticket1 := map[string]types.AttributeValue{ + "partition_key": stringAttr("ticket1"), + "TicketNumber": stringAttr("ticket_001"), + "TicketModTime": stringAttr("2022-10-07T14:32:25"), + "CreatorEmail": stringAttr("zorro@gmail.com"), + "AssigneeEmail": stringAttr("able@gmail.com"), + "Severity": stringAttr("3"), + "Subject": stringAttr("Bad bug"), + "Message": stringAttr("This bug looks pretty bad"), + } + + _, err := ddb.PutItem(ctx, &dynamodb.PutItemInput{ + TableName: &ddbTableName, + Item: ticket1, + }) + if err != nil { + return err + } + + // Ticket 2 + ticket2 := map[string]types.AttributeValue{ + "partition_key": stringAttr("ticket2"), + "TicketNumber": stringAttr("ticket_001"), + "TicketModTime": stringAttr("2022-10-07T14:32:25"), + "CreatorEmail": stringAttr("able@gmail.com"), + "AssigneeEmail": stringAttr("charlie@gmail.com"), + "Severity": stringAttr("3"), + "Subject": stringAttr("Bad bug"), + "Message": stringAttr("Charlie should handle this"), + } + + _, err = ddb.PutItem(ctx, &dynamodb.PutItemInput{ + TableName: &ddbTableName, + Item: ticket2, + }) + if err != nil { + return err + } + + // Ticket 3 + ticket3 := map[string]types.AttributeValue{ + "partition_key": stringAttr("ticket3"), + "TicketNumber": stringAttr("ticket_002"), + "TicketModTime": stringAttr("2022-10-06T14:32:25"), + "CreatorEmail": stringAttr("zorro@gmail.com"), + "AssigneeEmail": stringAttr("charlie@gmail.com"), + "Severity": stringAttr("3"), + "Subject": stringAttr("Easy Bug"), + "Message": stringAttr("This seems simple enough"), + } + + _, err = ddb.PutItem(ctx, &dynamodb.PutItemInput{ + TableName: &ddbTableName, + Item: ticket3, + }) + if err != nil { + return err + } + + // Ticket 4 + ticket4 := map[string]types.AttributeValue{ + "partition_key": stringAttr("ticket4"), + "TicketNumber": stringAttr("ticket_002"), + "TicketModTime": stringAttr("2022-10-08T14:32:25"), + "CreatorEmail": stringAttr("charlie@gmail.com"), + "AssigneeEmail": stringAttr("able@gmail.com"), + "Severity": stringAttr("3"), + "Subject": stringAttr("Easy Bug"), + "Message": stringAttr("that's in able's code"), + } + + _, err = ddb.PutItem(ctx, &dynamodb.PutItemInput{ + TableName: &ddbTableName, + Item: ticket4, + }) + return err +} + +// timecard.json +// PutAllTimecardItemsToTable puts all timecard items to the table +func PutAllTimecardItemsToTable(ctx context.Context, ddbTableName string, ddb *dynamodb.Client) error { + // Timecard 1 + timecard1 := map[string]types.AttributeValue{ + "partition_key": stringAttr("timecard1"), + "ProjectName": stringAttr("project_002"), + "TimeCardStart": stringAttr("2022-09-12"), + "EmployeeEmail": stringAttr("able@gmail.com"), + "Hours": stringAttr("40"), + "Role": stringAttr("SDE3"), + } + + _, err := ddb.PutItem(ctx, &dynamodb.PutItemInput{ + TableName: &ddbTableName, + Item: timecard1, + }) + if err != nil { + return err + } + + // Timecard 2 + timecard2 := map[string]types.AttributeValue{ + "partition_key": stringAttr("timecard2"), + "ProjectName": stringAttr("project_002"), + "TimeCardStart": stringAttr("2022-09-12"), + "EmployeeEmail": stringAttr("barney@gmail.com"), + "Hours": stringAttr("20"), + "Role": stringAttr("PM"), + } + + _, err = ddb.PutItem(ctx, &dynamodb.PutItemInput{ + TableName: &ddbTableName, + Item: timecard2, + }) + if err != nil { + return err + } + + // Timecard 3 + timecard3 := map[string]types.AttributeValue{ + "partition_key": stringAttr("timecard3"), + "ProjectName": stringAttr("project_003"), + "TimeCardStart": stringAttr("2022-09-12"), + "EmployeeEmail": stringAttr("charlie@gmail.com"), + "Hours": stringAttr("40"), + "Role": stringAttr("SDE3"), + } + + _, err = ddb.PutItem(ctx, &dynamodb.PutItemInput{ + TableName: &ddbTableName, + Item: timecard3, + }) + if err != nil { + return err + } + + // Timecard 4 + timecard4 := map[string]types.AttributeValue{ + "partition_key": stringAttr("timecard4"), + "ProjectName": stringAttr("project_003"), + "TimeCardStart": stringAttr("2022-09-12"), + "EmployeeEmail": stringAttr("barney@gmail.com"), + "Hours": stringAttr("20"), + "Role": stringAttr("PM"), + } + + _, err = ddb.PutItem(ctx, &dynamodb.PutItemInput{ + TableName: &ddbTableName, + Item: timecard4, + }) + return err +} diff --git a/Examples/runtimes/go/searchableencryption/complexexample/queryrequests.go b/Examples/runtimes/go/searchableencryption/complexexample/queryrequests.go new file mode 100644 index 000000000..ce1e85936 --- /dev/null +++ b/Examples/runtimes/go/searchableencryption/complexexample/queryrequests.go @@ -0,0 +1,1426 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package complexexample + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go-v2/service/dynamodb" + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" +) + +/* + * This file is used in an example to demonstrate complex queries + * you can perform using beacons. + * The example data used is for demonstrative purposes only, + * and might not meet the distribution and correlation uniqueness + * recommendations for beacons. + * See our documentation for whether beacons are + * right for your particular data set: + * https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/searchable-encryption.html#are-beacons-right-for-me + * + * This class implements query access patterns from our workshop. + * The queries in this file are more complicated than in other searchable encryption examples, + * and should demonstrate how one can structure queries on beacons in a broader variety of applications. + */ + +// RunQueries executes all 23 complex query examples +func RunQueries(ctx context.Context, ddbTableName string, ddb *dynamodb.Client) error { + if err := RunQuery1(ctx, ddbTableName, ddb); err != nil { + return err + } + if err := RunQuery2(ctx, ddbTableName, ddb); err != nil { + return err + } + if err := RunQuery3(ctx, ddbTableName, ddb); err != nil { + return err + } + if err := RunQuery4(ctx, ddbTableName, ddb); err != nil { + return err + } + if err := RunQuery5(ctx, ddbTableName, ddb); err != nil { + return err + } + if err := RunQuery6(ctx, ddbTableName, ddb); err != nil { + return err + } + if err := RunQuery7(ctx, ddbTableName, ddb); err != nil { + return err + } + if err := RunQuery8(ctx, ddbTableName, ddb); err != nil { + return err + } + if err := RunQuery9(ctx, ddbTableName, ddb); err != nil { + return err + } + if err := RunQuery10(ctx, ddbTableName, ddb); err != nil { + return err + } + if err := RunQuery11(ctx, ddbTableName, ddb); err != nil { + return err + } + if err := RunQuery12(ctx, ddbTableName, ddb); err != nil { + return err + } + if err := RunQuery13(ctx, ddbTableName, ddb); err != nil { + return err + } + if err := RunQuery14(ctx, ddbTableName, ddb); err != nil { + return err + } + if err := RunQuery15(ctx, ddbTableName, ddb); err != nil { + return err + } + if err := RunQuery16(ctx, ddbTableName, ddb); err != nil { + return err + } + if err := RunQuery17(ctx, ddbTableName, ddb); err != nil { + return err + } + if err := RunQuery18(ctx, ddbTableName, ddb); err != nil { + return err + } + if err := RunQuery19(ctx, ddbTableName, ddb); err != nil { + return err + } + if err := RunQuery20(ctx, ddbTableName, ddb); err != nil { + return err + } + if err := RunQuery21(ctx, ddbTableName, ddb); err != nil { + return err + } + if err := RunQuery22(ctx, ddbTableName, ddb); err != nil { + return err + } + if err := RunQuery23(ctx, ddbTableName, ddb); err != nil { + return err + } + return nil +} + +// Helper function to check if a list contains a specific string value +func listContainsString(list []types.AttributeValue, value string) bool { + for _, item := range list { + if s, ok := item.(*types.AttributeValueMemberS); ok && s.Value == value { + return true + } + } + return false +} + +// RunQuery1 executes Query 1: Get meetings by date and email +// Key condition: PK1=email SK1 between(date1, date2) +// Filter condition: duration > 0 +func RunQuery1(ctx context.Context, ddbTableName string, ddb *dynamodb.Client) error { + query1AttributeNames := map[string]string{ + "#pk1": "PK1", + "#sk1": "SK1", + "#duration": "Duration", + } + + query1AttributeValues := map[string]types.AttributeValue{ + ":e": stringAttr("EE-able@gmail.com"), + ":date1": stringAttr("MS-2022-07-02"), + ":date2": stringAttr("MS-2022-07-08"), + ":zero": stringAttr("0"), + } + + query1Input := &dynamodb.QueryInput{ + TableName: &ddbTableName, + IndexName: StringPtr("GSI-1"), + KeyConditionExpression: StringPtr("#pk1 = :e AND #sk1 BETWEEN :date1 AND :date2"), + FilterExpression: StringPtr("#duration > :zero"), + ExpressionAttributeNames: query1AttributeNames, + ExpressionAttributeValues: query1AttributeValues, + } + + query1Response, err := ddb.Query(ctx, query1Input) + if err != nil { + return fmt.Errorf("query1 failed: %w", err) + } + + // Validate query was returned successfully + if query1Response == nil { + return fmt.Errorf("query1: no response") + } + + // Assert 1 item was returned: `meeting1` + if len(query1Response.Items) != 1 { + return fmt.Errorf("query1: expected 1 item, got %d", len(query1Response.Items)) + } + + // Known value test: Assert some properties on one of the items + foundKnownValueItemQuery1 := false + for _, item := range query1Response.Items { + if partitionKey, ok := item["partition_key"].(*types.AttributeValueMemberS); ok && partitionKey.Value == "meeting1" { + foundKnownValueItemQuery1 = true + + if subject, ok := item["Subject"].(*types.AttributeValueMemberS); !ok || subject.Value != "Scan Beacons" { + return fmt.Errorf("query1: expected Subject 'Scan Beacons'") + } + + if location, ok := item["Location"].(*types.AttributeValueMemberM); ok { + if floor, ok := location.Value["Floor"].(*types.AttributeValueMemberS); !ok || floor.Value != "12" { + return fmt.Errorf("query1: expected Floor '12'") + } + } else { + return fmt.Errorf("query1: expected Location map") + } + + if attendees, ok := item["Attendees"].(*types.AttributeValueMemberL); ok { + if !listContainsString(attendees.Value, "zorro@gmail.com") { + return fmt.Errorf("query1: expected attendee 'zorro@gmail.com'") + } + } else { + return fmt.Errorf("query1: expected Attendees list") + } + } + } + + if !foundKnownValueItemQuery1 { + return fmt.Errorf("query1: did not find expected item 'meeting1'") + } + + return nil +} + +// RunQuery2 executes Query 2: Get meetings by date and employeeID +// Key condition: PK=employeeID SK between(date1, date2) +// Filter condition: duration > 0 +func RunQuery2(ctx context.Context, ddbTableName string, ddb *dynamodb.Client) error { + query2AttributeNames := map[string]string{ + "#pk": "PK", + "#sk": "SK", + "#duration": "Duration", + } + + query2AttributeValues := map[string]types.AttributeValue{ + ":employee": stringAttr("E-emp_001"), + ":date1": stringAttr("MS-2022-07-02"), + ":date2": stringAttr("MS-2022-07-08"), + ":zero": stringAttr("0"), + } + + query2Input := &dynamodb.QueryInput{ + TableName: &ddbTableName, + IndexName: StringPtr("GSI-0"), + KeyConditionExpression: StringPtr("#pk = :employee AND #sk BETWEEN :date1 AND :date2"), + FilterExpression: StringPtr("#duration > :zero"), + ExpressionAttributeNames: query2AttributeNames, + ExpressionAttributeValues: query2AttributeValues, + } + + query2Response, err := ddb.Query(ctx, query2Input) + if err != nil { + return fmt.Errorf("query2 failed: %w", err) + } + + // Assert 1 item was returned: `meeting1` + if len(query2Response.Items) != 1 { + return fmt.Errorf("query2: expected 1 item, got %d", len(query2Response.Items)) + } + + // Known value test: Assert some properties on one of the items + foundKnownValueItemQuery2 := false + for _, item := range query2Response.Items { + if partitionKey, ok := item["partition_key"].(*types.AttributeValueMemberS); ok && partitionKey.Value == "meeting1" { + foundKnownValueItemQuery2 = true + + if subject, ok := item["Subject"].(*types.AttributeValueMemberS); !ok || subject.Value != "Scan Beacons" { + return fmt.Errorf("query2: expected Subject 'Scan Beacons'") + } + + if location, ok := item["Location"].(*types.AttributeValueMemberM); ok { + if floor, ok := location.Value["Floor"].(*types.AttributeValueMemberS); !ok || floor.Value != "12" { + return fmt.Errorf("query2: expected Floor '12'") + } + } else { + return fmt.Errorf("query2: expected Location map") + } + + if attendees, ok := item["Attendees"].(*types.AttributeValueMemberL); ok { + if !listContainsString(attendees.Value, "zorro@gmail.com") { + return fmt.Errorf("query2: expected attendee 'zorro@gmail.com'") + } + } else { + return fmt.Errorf("query2: expected Attendees list") + } + } + } + + if !foundKnownValueItemQuery2 { + return fmt.Errorf("query2: did not find expected item 'meeting1'") + } + + return nil +} + +// RunQuery3 executes Query 3: Get meetings by date and building/floor/room +// Key condition: PK=employeeID SK between(date1, date2) +// Filter condition: SK contains building.floor.room (see NOTE) +// NOTE: This query is modified from Demo.md. +// +// Demo.md calls for a filter condition "SK contains building.floor.room" +// However, one cannot use primary keys (partition nor sort) in a filter expression. +// Instead, this query filters on the individual beacon attributes: building, floor, and room. +func RunQuery3(ctx context.Context, ddbTableName string, ddb *dynamodb.Client) error { + query3AttributeNames := map[string]string{ + "#pk": "PK", + "#sk": "SK", + "#building": "Building", + "#floor": "Floor", + "#room": "Room", + } + + query3AttributeValues := map[string]types.AttributeValue{ + ":buildingbeacon": stringAttr("B-SEA33"), + ":building": stringAttr("SEA33"), + ":floor": stringAttr("12"), + ":room": stringAttr("403"), + ":date1": stringAttr("MS-2022-07-02"), + ":date2": stringAttr("MS-2022-07-08"), + } + + query3Input := &dynamodb.QueryInput{ + TableName: &ddbTableName, + IndexName: StringPtr("GSI-0"), + KeyConditionExpression: StringPtr("#pk = :buildingbeacon AND #sk BETWEEN :date1 AND :date2"), + FilterExpression: StringPtr("#building = :building AND #floor = :floor AND #room = :room"), + ExpressionAttributeNames: query3AttributeNames, + ExpressionAttributeValues: query3AttributeValues, + } + + query3Response, err := ddb.Query(ctx, query3Input) + if err != nil { + return fmt.Errorf("query3 failed: %w", err) + } + + // Assert 1 item was returned: `reservation1` + if len(query3Response.Items) != 1 { + return fmt.Errorf("query3: expected 1 item, got %d", len(query3Response.Items)) + } + + // Known value test: Assert some properties on one of the items + foundKnownValueItemQuery3 := false + for _, item := range query3Response.Items { + if partitionKey, ok := item["partition_key"].(*types.AttributeValueMemberS); ok && partitionKey.Value == "reservation1" { + foundKnownValueItemQuery3 = true + + if subject, ok := item["Subject"].(*types.AttributeValueMemberS); !ok || subject.Value != "Scan beacons" { + return fmt.Errorf("query3: expected Subject 'Scan beacons'") + } + + if location, ok := item["Location"].(*types.AttributeValueMemberM); ok { + if building, ok := location.Value["Building"].(*types.AttributeValueMemberS); !ok || building.Value != "SEA33" { + return fmt.Errorf("query3: expected Building 'SEA33'") + } + } else { + return fmt.Errorf("query3: expected Location map") + } + + if attendees, ok := item["Attendees"].(*types.AttributeValueMemberL); ok { + if !listContainsString(attendees.Value, "barney@gmail.com") { + return fmt.Errorf("query3: expected attendee 'barney@gmail.com'") + } + } else { + return fmt.Errorf("query3: expected Attendees list") + } + } + } + + if !foundKnownValueItemQuery3 { + return fmt.Errorf("query3: did not find expected item 'reservation1'") + } + + return nil +} + +// RunQuery4 executes Query 4: Get employee data by email +// Key condition: PK1=email SK1=employee ID +func RunQuery4(ctx context.Context, ddbTableName string, ddb *dynamodb.Client) error { + query4AttributeNames := map[string]string{ + "#pk1": "PK1", + "#sk1": "SK1", + } + + query4AttributeValues := map[string]types.AttributeValue{ + ":email": stringAttr("EE-able@gmail.com"), + ":employee": stringAttr("E-emp_001"), + } + + query4Input := &dynamodb.QueryInput{ + TableName: &ddbTableName, + IndexName: StringPtr("GSI-1"), + KeyConditionExpression: StringPtr("#pk1 = :email AND #sk1 = :employee"), + ExpressionAttributeNames: query4AttributeNames, + ExpressionAttributeValues: query4AttributeValues, + } + + query4Response, err := ddb.Query(ctx, query4Input) + if err != nil { + return fmt.Errorf("query4 failed: %w", err) + } + + // Assert 1 item was returned: `employee1` + if len(query4Response.Items) != 1 { + return fmt.Errorf("query4: expected 1 item, got %d", len(query4Response.Items)) + } + + // Known value test: Assert some properties on one of the items + foundKnownValueItemQuery4 := false + for _, item := range query4Response.Items { + if partitionKey, ok := item["partition_key"].(*types.AttributeValueMemberS); ok && partitionKey.Value == "employee1" { + foundKnownValueItemQuery4 = true + + if employeeID, ok := item["EmployeeID"].(*types.AttributeValueMemberS); !ok || employeeID.Value != "emp_001" { + return fmt.Errorf("query4: expected EmployeeID 'emp_001'") + } + + if location, ok := item["Location"].(*types.AttributeValueMemberM); ok { + if desk, ok := location.Value["Desk"].(*types.AttributeValueMemberS); !ok || desk.Value != "3" { + return fmt.Errorf("query4: expected Desk '3'") + } + } else { + return fmt.Errorf("query4: expected Location map") + } + } + } + + if !foundKnownValueItemQuery4 { + return fmt.Errorf("query4: did not find expected item 'employee1'") + } + + return nil +} + +// RunQuery5 executes Query 5: Get meetings by email +// Key condition: PK1=email SK1 > 30 days ago +func RunQuery5(ctx context.Context, ddbTableName string, ddb *dynamodb.Client) error { + query5AttributeNames := map[string]string{ + "#pk1": "PK1", + "#sk1": "SK1", + } + + query5AttributeValues := map[string]types.AttributeValue{ + ":email": stringAttr("EE-able@gmail.com"), + ":thirtydaysago": stringAttr("MS-2023-03-20"), + ":prefix": stringAttr("MS-"), + } + + query5Input := &dynamodb.QueryInput{ + TableName: &ddbTableName, + IndexName: StringPtr("GSI-1"), + KeyConditionExpression: StringPtr("#pk1 = :email AND #sk1 BETWEEN :prefix AND :thirtydaysago"), + ExpressionAttributeNames: query5AttributeNames, + ExpressionAttributeValues: query5AttributeValues, + } + + query5Response, err := ddb.Query(ctx, query5Input) + if err != nil { + return fmt.Errorf("query5 failed: %w", err) + } + + // Assert 1 item was returned: `meeting1` + if len(query5Response.Items) != 1 { + return fmt.Errorf("query5: expected 1 item, got %d", len(query5Response.Items)) + } + + // Known value test: Assert some properties on one of the items + foundKnownValueItemQuery5 := false + for _, item := range query5Response.Items { + if partitionKey, ok := item["partition_key"].(*types.AttributeValueMemberS); ok && partitionKey.Value == "meeting1" { + foundKnownValueItemQuery5 = true + + if subject, ok := item["Subject"].(*types.AttributeValueMemberS); !ok || subject.Value != "Scan Beacons" { + return fmt.Errorf("query5: expected Subject 'Scan Beacons'") + } + + if location, ok := item["Location"].(*types.AttributeValueMemberM); ok { + if floor, ok := location.Value["Floor"].(*types.AttributeValueMemberS); !ok || floor.Value != "12" { + return fmt.Errorf("query5: expected Floor '12'") + } + } else { + return fmt.Errorf("query5: expected Location map") + } + + if attendees, ok := item["Attendees"].(*types.AttributeValueMemberL); ok { + if !listContainsString(attendees.Value, "zorro@gmail.com") { + return fmt.Errorf("query5: expected attendee 'zorro@gmail.com'") + } + } else { + return fmt.Errorf("query5: expected Attendees list") + } + } + } + + if !foundKnownValueItemQuery5 { + return fmt.Errorf("query5: did not find expected item 'meeting1'") + } + + return nil +} + +// RunQuery6 executes Query 6: Get tickets by email +// Key condition: PK1=email SK1 > 30 days ago +func RunQuery6(ctx context.Context, ddbTableName string, ddb *dynamodb.Client) error { + query6AttributeNames := map[string]string{ + "#pk1": "PK1", + "#sk1": "SK1", + } + + query6AttributeValues := map[string]types.AttributeValue{ + ":creatoremail": stringAttr("CE-zorro@gmail.com"), + ":thirtydaysago": stringAttr("MS-2023-03-20"), + } + + query6Input := &dynamodb.QueryInput{ + TableName: &ddbTableName, + IndexName: StringPtr("GSI-1"), + KeyConditionExpression: StringPtr("#pk1 = :creatoremail AND #sk1 < :thirtydaysago"), + ExpressionAttributeNames: query6AttributeNames, + ExpressionAttributeValues: query6AttributeValues, + } + + query6Response, err := ddb.Query(ctx, query6Input) + if err != nil { + return fmt.Errorf("query6 failed: %w", err) + } + + // Assert 2 items returned: Expected to be `ticket1` and `ticket3` + if len(query6Response.Items) != 2 { + return fmt.Errorf("query6: expected 2 items, got %d", len(query6Response.Items)) + } + + // Known value test: Assert some properties on one of the items + foundKnownValueItemQuery6 := false + for _, item := range query6Response.Items { + if partitionKey, ok := item["partition_key"].(*types.AttributeValueMemberS); ok && partitionKey.Value == "ticket1" { + foundKnownValueItemQuery6 = true + + if ticketNumber, ok := item["TicketNumber"].(*types.AttributeValueMemberS); !ok || ticketNumber.Value != "ticket_001" { + return fmt.Errorf("query6: expected TicketNumber 'ticket_001'") + } + } + } + + if !foundKnownValueItemQuery6 { + return fmt.Errorf("query6: did not find expected item 'ticket1'") + } + + return nil +} + +// RunQuery7 executes Query 7: Get reservations by email +// Key condition: PK1=organizeremail SK1 > 30 days ago +func RunQuery7(ctx context.Context, ddbTableName string, ddb *dynamodb.Client) error { + query7AttributeNames := map[string]string{ + "#pk1": "PK1", + "#sk1": "SK1", + } + + query7AttributeValues := map[string]types.AttributeValue{ + ":organizeremail": stringAttr("OE-able@gmail.com"), + ":thirtydaysago": stringAttr("MS-2023-03-20"), + } + + query7Input := &dynamodb.QueryInput{ + TableName: &ddbTableName, + IndexName: StringPtr("GSI-1"), + KeyConditionExpression: StringPtr("#pk1 = :organizeremail AND #sk1 < :thirtydaysago"), + ExpressionAttributeNames: query7AttributeNames, + ExpressionAttributeValues: query7AttributeValues, + } + + query7Response, err := ddb.Query(ctx, query7Input) + if err != nil { + return fmt.Errorf("query7 failed: %w", err) + } + + // Assert 1 item was returned: `reservation1` + if len(query7Response.Items) != 1 { + return fmt.Errorf("query7: expected 1 item, got %d", len(query7Response.Items)) + } + + // Known value test: Assert some properties on one of the items + foundKnownValueItemQuery7 := false + for _, item := range query7Response.Items { + if partitionKey, ok := item["partition_key"].(*types.AttributeValueMemberS); ok && partitionKey.Value == "reservation1" { + foundKnownValueItemQuery7 = true + + if subject, ok := item["Subject"].(*types.AttributeValueMemberS); !ok || subject.Value != "Scan beacons" { + return fmt.Errorf("query7: expected Subject 'Scan beacons'") + } + + if location, ok := item["Location"].(*types.AttributeValueMemberM); ok { + if floor, ok := location.Value["Floor"].(*types.AttributeValueMemberS); !ok || floor.Value != "12" { + return fmt.Errorf("query7: expected Floor '12'") + } + } else { + return fmt.Errorf("query7: expected Location map") + } + + if attendees, ok := item["Attendees"].(*types.AttributeValueMemberL); ok { + if !listContainsString(attendees.Value, "barney@gmail.com") { + return fmt.Errorf("query7: expected attendee 'barney@gmail.com'") + } + } else { + return fmt.Errorf("query7: expected Attendees list") + } + } + } + + if !foundKnownValueItemQuery7 { + return fmt.Errorf("query7: did not find expected item 'reservation1'") + } + + return nil +} + +// RunQuery8 executes Query 8: Get time cards by email +// Key condition: PK1=employeeemail SK1 > 30 days ago +func RunQuery8(ctx context.Context, ddbTableName string, ddb *dynamodb.Client) error { + query8AttributeNames := map[string]string{ + "#pk1": "PK1", + "#sk1": "SK1", + } + + query8AttributeValues := map[string]types.AttributeValue{ + ":email": stringAttr("EE-able@gmail.com"), + ":prefix": stringAttr("TC-"), + ":thirtydaysago": stringAttr("TC-2023-03-20"), + } + + query8Input := &dynamodb.QueryInput{ + TableName: &ddbTableName, + IndexName: StringPtr("GSI-1"), + KeyConditionExpression: StringPtr("#pk1 = :email AND #sk1 BETWEEN :prefix AND :thirtydaysago"), + ExpressionAttributeNames: query8AttributeNames, + ExpressionAttributeValues: query8AttributeValues, + } + + query8Response, err := ddb.Query(ctx, query8Input) + if err != nil { + return fmt.Errorf("query8 failed: %w", err) + } + + // Assert 1 item was returned: `timecard1` + if len(query8Response.Items) != 1 { + return fmt.Errorf("query8: expected 1 item, got %d", len(query8Response.Items)) + } + + // Known value test: Assert some properties on one of the items + foundKnownValueItemQuery8 := false + for _, item := range query8Response.Items { + if partitionKey, ok := item["partition_key"].(*types.AttributeValueMemberS); ok && partitionKey.Value == "timecard1" { + foundKnownValueItemQuery8 = true + + if projectName, ok := item["ProjectName"].(*types.AttributeValueMemberS); !ok || projectName.Value != "project_002" { + return fmt.Errorf("query8: expected ProjectName 'project_002'") + } + } + } + + if !foundKnownValueItemQuery8 { + return fmt.Errorf("query8: did not find expected item 'timecard1'") + } + + return nil +} + +// RunQuery9 executes Query 9: Get employee info by employee ID +// Key condition: PK1=employeeID SK starts with "E-" +func RunQuery9(ctx context.Context, ddbTableName string, ddb *dynamodb.Client) error { + query9AttributeNames := map[string]string{ + "#pk": "PK", + "#sk": "SK", + } + + query9AttributeValues := map[string]types.AttributeValue{ + ":employee": stringAttr("E-emp_001"), + ":prefix": stringAttr("E-"), + } + + query9Input := &dynamodb.QueryInput{ + TableName: &ddbTableName, + IndexName: StringPtr("GSI-0"), + KeyConditionExpression: StringPtr("#pk = :employee AND begins_with(#sk, :prefix)"), + ExpressionAttributeNames: query9AttributeNames, + ExpressionAttributeValues: query9AttributeValues, + } + + query9Response, err := ddb.Query(ctx, query9Input) + if err != nil { + return fmt.Errorf("query9 failed: %w", err) + } + + // Assert 1 item was returned: `employee1` + if len(query9Response.Items) != 1 { + return fmt.Errorf("query9: expected 1 item, got %d", len(query9Response.Items)) + } + + // Known value test: Assert some properties on one of the items + foundKnownValueItemQuery9 := false + for _, item := range query9Response.Items { + if partitionKey, ok := item["partition_key"].(*types.AttributeValueMemberS); ok && partitionKey.Value == "employee1" { + foundKnownValueItemQuery9 = true + + if employeeID, ok := item["EmployeeID"].(*types.AttributeValueMemberS); !ok || employeeID.Value != "emp_001" { + return fmt.Errorf("query9: expected EmployeeID 'emp_001'") + } + } + } + + if !foundKnownValueItemQuery9 { + return fmt.Errorf("query9: did not find expected item 'employee1'") + } + + return nil +} + +// RunQuery10 executes Query 10: Get employee info by email +// Key condition: PK1=email +// Filter condition: SK starts with "E-" +func RunQuery10(ctx context.Context, ddbTableName string, ddb *dynamodb.Client) error { + query10AttributeNames := map[string]string{ + "#pk1": "PK1", + "#sk1": "SK1", + } + + query10AttributeValues := map[string]types.AttributeValue{ + ":email": stringAttr("EE-able@gmail.com"), + ":prefix": stringAttr("E-"), + } + + query10Input := &dynamodb.QueryInput{ + TableName: &ddbTableName, + IndexName: StringPtr("GSI-1"), + KeyConditionExpression: StringPtr("#pk1 = :email AND begins_with(#sk1, :prefix)"), + ExpressionAttributeNames: query10AttributeNames, + ExpressionAttributeValues: query10AttributeValues, + } + + query10Response, err := ddb.Query(ctx, query10Input) + if err != nil { + return fmt.Errorf("query10 failed: %w", err) + } + + // Assert 1 item was returned: `employee1` + if len(query10Response.Items) != 1 { + return fmt.Errorf("query10: expected 1 item, got %d", len(query10Response.Items)) + } + + // Known value test: Assert some properties on one of the items + foundKnownValueItemQuery10 := false + for _, item := range query10Response.Items { + if partitionKey, ok := item["partition_key"].(*types.AttributeValueMemberS); ok && partitionKey.Value == "employee1" { + foundKnownValueItemQuery10 = true + + if employeeID, ok := item["EmployeeID"].(*types.AttributeValueMemberS); !ok || employeeID.Value != "emp_001" { + return fmt.Errorf("query10: expected EmployeeID 'emp_001'") + } + } + } + + if !foundKnownValueItemQuery10 { + return fmt.Errorf("query10: did not find expected item 'employee1'") + } + + return nil +} + +// RunQuery11 executes Query 11: Get ticket history by ticket number +// Key condition: PK=TicketNumber +func RunQuery11(ctx context.Context, ddbTableName string, ddb *dynamodb.Client) error { + query11AttributeNames := map[string]string{ + "#pk": "PK", + } + + query11AttributeValues := map[string]types.AttributeValue{ + ":ticket": stringAttr("T-ticket_001"), + } + + query11Input := &dynamodb.QueryInput{ + TableName: &ddbTableName, + IndexName: StringPtr("GSI-0"), + KeyConditionExpression: StringPtr("#pk = :ticket"), + ExpressionAttributeNames: query11AttributeNames, + ExpressionAttributeValues: query11AttributeValues, + } + + query11Response, err := ddb.Query(ctx, query11Input) + if err != nil { + return fmt.Errorf("query11 failed: %w", err) + } + + // Assert 2 items returned: Expected to be `ticket1` and `ticket2` + if len(query11Response.Items) != 2 { + return fmt.Errorf("query11: expected 2 items, got %d", len(query11Response.Items)) + } + + // Known value test: Assert some properties on one of the items + foundKnownValueItemQuery11 := false + for _, item := range query11Response.Items { + if partitionKey, ok := item["partition_key"].(*types.AttributeValueMemberS); ok && partitionKey.Value == "ticket1" { + foundKnownValueItemQuery11 = true + + if ticketNumber, ok := item["TicketNumber"].(*types.AttributeValueMemberS); !ok || ticketNumber.Value != "ticket_001" { + return fmt.Errorf("query11: expected TicketNumber 'ticket_001'") + } + } + } + + if !foundKnownValueItemQuery11 { + return fmt.Errorf("query11: did not find expected item 'ticket1'") + } + + return nil +} + +// RunQuery12 executes Query 12: Get Ticket History by employee email +// Key condition: PK1=CreatorEmail +// Filter condition: PK=TicketNumber +func RunQuery12(ctx context.Context, ddbTableName string, ddb *dynamodb.Client) error { + query12AttributeNames := map[string]string{ + "#pk1": "PK1", + "#pk": "PK", + } + + query12AttributeValues := map[string]types.AttributeValue{ + ":email": stringAttr("CE-zorro@gmail.com"), + ":ticket": stringAttr("T-ticket_001"), + } + + query12Input := &dynamodb.QueryInput{ + TableName: &ddbTableName, + IndexName: StringPtr("GSI-1"), + KeyConditionExpression: StringPtr("#pk1 = :email"), + FilterExpression: StringPtr("#pk = :ticket"), + ExpressionAttributeNames: query12AttributeNames, + ExpressionAttributeValues: query12AttributeValues, + } + + query12Response, err := ddb.Query(ctx, query12Input) + if err != nil { + return fmt.Errorf("query12 failed: %w", err) + } + + // Assert 1 item was returned: `ticket1` + if len(query12Response.Items) != 1 { + return fmt.Errorf("query12: expected 1 item, got %d", len(query12Response.Items)) + } + + // Known value test: Assert some properties on one of the items + foundKnownValueItemQuery12 := false + for _, item := range query12Response.Items { + if partitionKey, ok := item["partition_key"].(*types.AttributeValueMemberS); ok && partitionKey.Value == "ticket1" { + foundKnownValueItemQuery12 = true + + if ticketNumber, ok := item["TicketNumber"].(*types.AttributeValueMemberS); !ok || ticketNumber.Value != "ticket_001" { + return fmt.Errorf("query12: expected TicketNumber 'ticket_001'") + } + } + } + + if !foundKnownValueItemQuery12 { + return fmt.Errorf("query12: did not find expected item 'ticket1'") + } + + return nil +} + +// RunQuery13 executes Query 13: Get ticket history by assignee email +// Key condition: PK=AssigneeEmail +// Filter condition: PK=ticketNumber +func RunQuery13(ctx context.Context, ddbTableName string, ddb *dynamodb.Client) error { + query13AttributeNames := map[string]string{ + "#pk2": "PK2", + "#pk": "PK", + } + + query13AttributeValues := map[string]types.AttributeValue{ + ":assigneeemail": stringAttr("AE-able@gmail.com"), + ":ticket": stringAttr("T-ticket_001"), + } + + query13Input := &dynamodb.QueryInput{ + TableName: &ddbTableName, + IndexName: StringPtr("GSI-2"), + KeyConditionExpression: StringPtr("#pk2 = :assigneeemail"), + FilterExpression: StringPtr("#pk = :ticket"), + ExpressionAttributeNames: query13AttributeNames, + ExpressionAttributeValues: query13AttributeValues, + } + + query13Response, err := ddb.Query(ctx, query13Input) + if err != nil { + return fmt.Errorf("query13 failed: %w", err) + } + + // Assert 1 item was returned: `ticket1` + if len(query13Response.Items) != 1 { + return fmt.Errorf("query13: expected 1 item, got %d", len(query13Response.Items)) + } + + // Known value test: Assert some properties on one of the items + foundKnownValueItemQuery13 := false + for _, item := range query13Response.Items { + if partitionKey, ok := item["partition_key"].(*types.AttributeValueMemberS); ok && partitionKey.Value == "ticket1" { + foundKnownValueItemQuery13 = true + + if subject, ok := item["Subject"].(*types.AttributeValueMemberS); !ok || subject.Value != "Bad bug" { + return fmt.Errorf("query13: expected Subject 'Bad bug'") + } + } + } + + if !foundKnownValueItemQuery13 { + return fmt.Errorf("query13: did not find expected item 'ticket1'") + } + + return nil +} + +// RunQuery14 executes Query 14: Get employees by city.building.floor.desk +// Key condition: PK3=city SK3 begins_with(building.floor.desk) +func RunQuery14(ctx context.Context, ddbTableName string, ddb *dynamodb.Client) error { + query14AttributeNames := map[string]string{ + "#pk3": "PK3", + "#sk3": "SK3", + } + + query14AttributeValues := map[string]types.AttributeValue{ + ":city": stringAttr("C-Seattle"), + ":location": stringAttr("B-44~F-12~D-3"), + } + + query14Input := &dynamodb.QueryInput{ + TableName: &ddbTableName, + IndexName: StringPtr("GSI-3"), + KeyConditionExpression: StringPtr("#pk3 = :city AND begins_with(#sk3, :location)"), + ExpressionAttributeNames: query14AttributeNames, + ExpressionAttributeValues: query14AttributeValues, + } + + // GSIs do not update instantly, so if the results come back empty + // we retry after a short sleep + for i := 0; i < 10; i++ { + query14Response, err := ddb.Query(ctx, query14Input) + if err != nil { + return fmt.Errorf("query14 failed: %w", err) + } + + // if no results, sleep and try again + if len(query14Response.Items) == 0 { + // Sleep for 20ms and continue + continue + } + + // Assert 1 item was returned: `employee1` + if len(query14Response.Items) != 1 { + return fmt.Errorf("query14: expected 1 item, got %d", len(query14Response.Items)) + } + + // Known value test: Assert some properties on one of the items + foundKnownValueItemQuery14 := false + for _, item := range query14Response.Items { + if partitionKey, ok := item["partition_key"].(*types.AttributeValueMemberS); ok && partitionKey.Value == "employee1" { + foundKnownValueItemQuery14 = true + + if employeeID, ok := item["EmployeeID"].(*types.AttributeValueMemberS); !ok || employeeID.Value != "emp_001" { + return fmt.Errorf("query14: expected EmployeeID 'emp_001'") + } + + if location, ok := item["Location"].(*types.AttributeValueMemberM); ok { + if desk, ok := location.Value["Desk"].(*types.AttributeValueMemberS); !ok || desk.Value != "3" { + return fmt.Errorf("query14: expected Desk '3'") + } + } else { + return fmt.Errorf("query14: expected Location map") + } + } + } + + if !foundKnownValueItemQuery14 { + return fmt.Errorf("query14: did not find expected item 'employee1'") + } + + return nil + } + + return fmt.Errorf("query14: failed after 10 retries") +} + +// RunQuery15 executes Query 15: Get employees by manager email +// Key condition: PK2 = ManagerEmail +func RunQuery15(ctx context.Context, ddbTableName string, ddb *dynamodb.Client) error { + query15AttributeNames := map[string]string{ + "#pk2": "PK2", + } + + query15AttributeValues := map[string]types.AttributeValue{ + ":manageremail": stringAttr("ME-zorro@gmail.com"), + } + + query15Input := &dynamodb.QueryInput{ + TableName: &ddbTableName, + IndexName: StringPtr("GSI-2"), + KeyConditionExpression: StringPtr("#pk2 = :manageremail"), + ExpressionAttributeNames: query15AttributeNames, + ExpressionAttributeValues: query15AttributeValues, + } + + query15Response, err := ddb.Query(ctx, query15Input) + if err != nil { + return fmt.Errorf("query15 failed: %w", err) + } + + // Assert 4 items returned: Expected to be `employee1`, `employee2`, `employee3`, and `employee4` + if len(query15Response.Items) != 4 { + return fmt.Errorf("query15: expected 4 items, got %d", len(query15Response.Items)) + } + + // Known value test: Assert some properties on one of the items + foundKnownValueItemQuery15 := false + for _, item := range query15Response.Items { + if partitionKey, ok := item["partition_key"].(*types.AttributeValueMemberS); ok && partitionKey.Value == "employee1" { + foundKnownValueItemQuery15 = true + + if employeeID, ok := item["EmployeeID"].(*types.AttributeValueMemberS); !ok || employeeID.Value != "emp_001" { + return fmt.Errorf("query15: expected EmployeeID 'emp_001'") + } + + if location, ok := item["Location"].(*types.AttributeValueMemberM); ok { + if desk, ok := location.Value["Desk"].(*types.AttributeValueMemberS); !ok || desk.Value != "3" { + return fmt.Errorf("query15: expected Desk '3'") + } + } else { + return fmt.Errorf("query15: expected Location map") + } + } + } + + if !foundKnownValueItemQuery15 { + return fmt.Errorf("query15: did not find expected item 'employee1'") + } + + return nil +} + +// RunQuery16 executes Query 16: Get assigned tickets by assignee email +// Key condition: PK2 = AssigneeEmail +func RunQuery16(ctx context.Context, ddbTableName string, ddb *dynamodb.Client) error { + query16AttributeNames := map[string]string{ + "#pk2": "PK2", + } + + query16AttributeValues := map[string]types.AttributeValue{ + ":assigneeemail": stringAttr("AE-able@gmail.com"), + } + + query16Input := &dynamodb.QueryInput{ + TableName: &ddbTableName, + IndexName: StringPtr("GSI-2"), + KeyConditionExpression: StringPtr("#pk2 = :assigneeemail"), + ExpressionAttributeNames: query16AttributeNames, + ExpressionAttributeValues: query16AttributeValues, + } + + query16Response, err := ddb.Query(ctx, query16Input) + if err != nil { + return fmt.Errorf("query16 failed: %w", err) + } + + // Assert 2 items returned: Expected to be `ticket1` and `ticket4` + if len(query16Response.Items) != 2 { + return fmt.Errorf("query16: expected 2 items, got %d", len(query16Response.Items)) + } + + // Known value test: Assert some properties on one of the items + foundKnownValueItemQuery16 := false + for _, item := range query16Response.Items { + if partitionKey, ok := item["partition_key"].(*types.AttributeValueMemberS); ok && partitionKey.Value == "ticket1" { + foundKnownValueItemQuery16 = true + + if ticketNumber, ok := item["TicketNumber"].(*types.AttributeValueMemberS); !ok || ticketNumber.Value != "ticket_001" { + return fmt.Errorf("query16: expected TicketNumber 'ticket_001'") + } + } + } + + if !foundKnownValueItemQuery16 { + return fmt.Errorf("query16: did not find expected item 'ticket1'") + } + + return nil +} + +// RunQuery17 executes Query 17: Get tickets updated within the last 24 hours +// Key condition: PK3 = Severity, SK3 > 24 hours ago +// (For the sake of this example, we will assume +// +// the date is 2022-10-08T09:30:00, such that "24 hours ago" +// is 2022-10-07T09:30:00, and that our sample ticket record +// with TicketModTime=2022-10-07T14:32:25 will be returned.) +func RunQuery17(ctx context.Context, ddbTableName string, ddb *dynamodb.Client) error { + query17AttributeNames := map[string]string{ + "#pk3": "PK3", + "#sk3": "SK3", + } + + query17AttributeValues := map[string]types.AttributeValue{ + ":severity": stringAttr("S-3"), + ":yesterday": stringAttr("M-2022-10-07T09:30:00"), + } + + query17Input := &dynamodb.QueryInput{ + TableName: &ddbTableName, + IndexName: StringPtr("GSI-3"), + KeyConditionExpression: StringPtr("#pk3 = :severity AND #sk3 > :yesterday"), + ExpressionAttributeNames: query17AttributeNames, + ExpressionAttributeValues: query17AttributeValues, + } + + query17Response, err := ddb.Query(ctx, query17Input) + if err != nil { + return fmt.Errorf("query17 failed: %w", err) + } + + // Assert 3 items returned: Expected to be `ticket1`, `ticket2`, and `ticket4` + if len(query17Response.Items) != 3 { + return fmt.Errorf("query17: expected 3 items, got %d", len(query17Response.Items)) + } + + // Known value test: Assert some properties on one of the items + foundKnownValueItemQuery17 := false + for _, item := range query17Response.Items { + if partitionKey, ok := item["partition_key"].(*types.AttributeValueMemberS); ok && partitionKey.Value == "ticket1" { + foundKnownValueItemQuery17 = true + + if ticketNumber, ok := item["TicketNumber"].(*types.AttributeValueMemberS); !ok || ticketNumber.Value != "ticket_001" { + return fmt.Errorf("query17: expected TicketNumber 'ticket_001'") + } + } + } + + if !foundKnownValueItemQuery17 { + return fmt.Errorf("query17: did not find expected item 'ticket1'") + } + + return nil +} + +// RunQuery18 executes Query 18: Get projects by status, start and target date +// Key condition: PK1 = Status, SK1 > StartDate +// Filter condition: TargetDelivery < TargetDate +func RunQuery18(ctx context.Context, ddbTableName string, ddb *dynamodb.Client) error { + query18AttributeNames := map[string]string{ + "#pk1": "PK1", + "#sk1": "SK1", + "#target": "ProjectTarget", + } + + query18AttributeValues := map[string]types.AttributeValue{ + ":status": stringAttr("PSts-Pending"), + ":startdate": stringAttr("PS-2022-01-01"), + ":target": stringAttr("2025-01-01"), + } + + query18Input := &dynamodb.QueryInput{ + TableName: &ddbTableName, + IndexName: StringPtr("GSI-1"), + KeyConditionExpression: StringPtr("#pk1 = :status AND #sk1 > :startdate"), + FilterExpression: StringPtr("#target < :target"), + ExpressionAttributeNames: query18AttributeNames, + ExpressionAttributeValues: query18AttributeValues, + } + + query18Response, err := ddb.Query(ctx, query18Input) + if err != nil { + return fmt.Errorf("query18 failed: %w", err) + } + + // Assert 1 item was returned: `project1` + if len(query18Response.Items) != 1 { + return fmt.Errorf("query18: expected 1 item, got %d", len(query18Response.Items)) + } + + // Known value test: Assert some properties on one of the items + foundKnownValueItemQuery18 := false + for _, item := range query18Response.Items { + if partitionKey, ok := item["partition_key"].(*types.AttributeValueMemberS); ok && partitionKey.Value == "project1" { + foundKnownValueItemQuery18 = true + + if projectName, ok := item["ProjectName"].(*types.AttributeValueMemberS); !ok || projectName.Value != "project_001" { + return fmt.Errorf("query18: expected ProjectName 'project_001'") + } + } + } + + if !foundKnownValueItemQuery18 { + return fmt.Errorf("query18: did not find expected item 'project1'") + } + + return nil +} + +// RunQuery19 executes Query 19: Get projects by name +// Key condition: PK = ProjectName, SK = ProjectName +func RunQuery19(ctx context.Context, ddbTableName string, ddb *dynamodb.Client) error { + query19AttributeNames := map[string]string{ + "#pk": "PK", + "#sk": "SK", + } + + query19AttributeValues := map[string]types.AttributeValue{ + ":projectname": stringAttr("P-project_001"), + } + + query19Input := &dynamodb.QueryInput{ + TableName: &ddbTableName, + IndexName: StringPtr("GSI-0"), + KeyConditionExpression: StringPtr("#pk = :projectname AND #sk = :projectname"), + ExpressionAttributeNames: query19AttributeNames, + ExpressionAttributeValues: query19AttributeValues, + } + + query19Response, err := ddb.Query(ctx, query19Input) + if err != nil { + return fmt.Errorf("query19 failed: %w", err) + } + + // Assert 1 item was returned: `project1` + if len(query19Response.Items) != 1 { + return fmt.Errorf("query19: expected 1 item, got %d", len(query19Response.Items)) + } + + // Known value test: Assert some properties on one of the items + foundKnownValueItemQuery19 := false + for _, item := range query19Response.Items { + if partitionKey, ok := item["partition_key"].(*types.AttributeValueMemberS); ok && partitionKey.Value == "project1" { + foundKnownValueItemQuery19 = true + + if projectName, ok := item["ProjectName"].(*types.AttributeValueMemberS); !ok || projectName.Value != "project_001" { + return fmt.Errorf("query19: expected ProjectName 'project_001'") + } + } + } + + if !foundKnownValueItemQuery19 { + return fmt.Errorf("query19: did not find expected item 'project1'") + } + + return nil +} + +// RunQuery20 executes Query 20: Get Project History by date range (against timecard record) +// Key condition: PK = ProjectName, SK between(date1, date2) +func RunQuery20(ctx context.Context, ddbTableName string, ddb *dynamodb.Client) error { + query20AttributeNames := map[string]string{ + "#pk": "PK", + "#sk": "SK", + } + + query20AttributeValues := map[string]types.AttributeValue{ + ":projectname": stringAttr("P-project_002"), + ":date1": stringAttr("TC-2022-01-01"), + ":date2": stringAttr("TC-2023-01-01"), + } + + query20Input := &dynamodb.QueryInput{ + TableName: &ddbTableName, + IndexName: StringPtr("GSI-0"), + KeyConditionExpression: StringPtr("#pk = :projectname AND #sk BETWEEN :date1 AND :date2"), + ExpressionAttributeNames: query20AttributeNames, + ExpressionAttributeValues: query20AttributeValues, + } + + query20Response, err := ddb.Query(ctx, query20Input) + if err != nil { + return fmt.Errorf("query20 failed: %w", err) + } + + // Assert 2 items returned: Expected to be `timecard1` and `timecard2` + if len(query20Response.Items) != 2 { + return fmt.Errorf("query20: expected 2 items, got %d", len(query20Response.Items)) + } + + // Known value test: Assert some properties on one of the items + foundKnownValueItemQuery20 := false + for _, item := range query20Response.Items { + if partitionKey, ok := item["partition_key"].(*types.AttributeValueMemberS); ok && partitionKey.Value == "timecard1" { + foundKnownValueItemQuery20 = true + + if projectName, ok := item["ProjectName"].(*types.AttributeValueMemberS); !ok || projectName.Value != "project_002" { + return fmt.Errorf("query20: expected ProjectName 'project_002'") + } + } + } + + if !foundKnownValueItemQuery20 { + return fmt.Errorf("query20: did not find expected item 'timecard1'") + } + + return nil +} + +// RunQuery21 executes Query 21: Get Project History by role +// Key condition: PK = ProjectName +// Filter condition: role=rolename +func RunQuery21(ctx context.Context, ddbTableName string, ddb *dynamodb.Client) error { + query21AttributeNames := map[string]string{ + "#pk": "PK", + "#role": "Role", + } + + query21AttributeValues := map[string]types.AttributeValue{ + ":projectname": stringAttr("P-project_002"), + ":role": stringAttr("SDE3"), + } + + query21Input := &dynamodb.QueryInput{ + TableName: &ddbTableName, + IndexName: StringPtr("GSI-0"), + KeyConditionExpression: StringPtr("#pk = :projectname"), + FilterExpression: StringPtr("#role = :role"), + ExpressionAttributeNames: query21AttributeNames, + ExpressionAttributeValues: query21AttributeValues, + } + + query21Response, err := ddb.Query(ctx, query21Input) + if err != nil { + return fmt.Errorf("query21 failed: %w", err) + } + + // Assert 1 item was returned: `timecard1` + if len(query21Response.Items) != 1 { + return fmt.Errorf("query21: expected 1 item, got %d", len(query21Response.Items)) + } + + // Known value test: Assert some properties on one of the items + foundKnownValueItemQuery21 := false + for _, item := range query21Response.Items { + if partitionKey, ok := item["partition_key"].(*types.AttributeValueMemberS); ok && partitionKey.Value == "timecard1" { + foundKnownValueItemQuery21 = true + + if projectName, ok := item["ProjectName"].(*types.AttributeValueMemberS); !ok || projectName.Value != "project_002" { + return fmt.Errorf("query21: expected ProjectName 'project_002'") + } + } + } + + if !foundKnownValueItemQuery21 { + return fmt.Errorf("query21: did not find expected item 'timecard1'") + } + + return nil +} + +// RunQuery22 executes Query 22: Get reservations by building ID +// Key condition: PK = Building ID +func RunQuery22(ctx context.Context, ddbTableName string, ddb *dynamodb.Client) error { + query22AttributeNames := map[string]string{ + "#pk": "PK", + } + + query22AttributeValues := map[string]types.AttributeValue{ + ":building": stringAttr("B-SEA33"), + } + + query22Input := &dynamodb.QueryInput{ + TableName: &ddbTableName, + IndexName: StringPtr("GSI-0"), + KeyConditionExpression: StringPtr("#pk = :building"), + ExpressionAttributeNames: query22AttributeNames, + ExpressionAttributeValues: query22AttributeValues, + } + + query22Response, err := ddb.Query(ctx, query22Input) + if err != nil { + return fmt.Errorf("query22 failed: %w", err) + } + + // Assert 2 items returned: Expected to be `reservation1` and `reservation2` + if len(query22Response.Items) != 2 { + return fmt.Errorf("query22: expected 2 items, got %d", len(query22Response.Items)) + } + + // Known value test: Assert some properties on one of the items + foundKnownValueItemQuery22 := false + for _, item := range query22Response.Items { + if partitionKey, ok := item["partition_key"].(*types.AttributeValueMemberS); ok && partitionKey.Value == "reservation1" { + foundKnownValueItemQuery22 = true + + if subject, ok := item["Subject"].(*types.AttributeValueMemberS); !ok || subject.Value != "Scan beacons" { + return fmt.Errorf("query22: expected Subject 'Scan beacons'") + } + } + } + + if !foundKnownValueItemQuery22 { + return fmt.Errorf("query22: did not find expected item 'reservation1'") + } + + return nil +} + +// RunQuery23 executes Query 23: Get reservations by building ID and time range +// Key condition: PK = Building ID, SK between(date1, date2) +// Filter condition: Duration > 0 +func RunQuery23(ctx context.Context, ddbTableName string, ddb *dynamodb.Client) error { + query23AttributeNames := map[string]string{ + "#pk": "PK", + "#sk": "SK", + "#duration": "Duration", + } + + query23AttributeValues := map[string]types.AttributeValue{ + ":building": stringAttr("B-SEA33"), + ":date1": stringAttr("MS-2022-07-01"), + ":date2": stringAttr("MS-2022-07-08"), + ":zero": stringAttr("0"), + } + + query23Input := &dynamodb.QueryInput{ + TableName: &ddbTableName, + IndexName: StringPtr("GSI-0"), + KeyConditionExpression: StringPtr("#pk = :building AND #sk BETWEEN :date1 AND :date2"), + FilterExpression: StringPtr("#duration > :zero"), + ExpressionAttributeNames: query23AttributeNames, + ExpressionAttributeValues: query23AttributeValues, + } + + query23Response, err := ddb.Query(ctx, query23Input) + if err != nil { + return fmt.Errorf("query23 failed: %w", err) + } + + // Assert 2 items returned: Expected to be `reservation1` and `reservation2` + if len(query23Response.Items) != 2 { + return fmt.Errorf("query23: expected 2 items, got %d", len(query23Response.Items)) + } + + // Known value test: Assert some properties on one of the items + foundKnownValueItemQuery23 := false + for _, item := range query23Response.Items { + if partitionKey, ok := item["partition_key"].(*types.AttributeValueMemberS); ok && partitionKey.Value == "reservation1" { + foundKnownValueItemQuery23 = true + + if subject, ok := item["Subject"].(*types.AttributeValueMemberS); !ok || subject.Value != "Scan beacons" { + return fmt.Errorf("query23: expected Subject 'Scan beacons'") + } + } + } + + if !foundKnownValueItemQuery23 { + return fmt.Errorf("query23: did not find expected item 'reservation1'") + } + + return nil +} + +// Helper function to create string pointer +func StringPtr(s string) *string { + return &s +}