// Delete a key in DynamoDB func (db DynamoDB) Delete(opts config.Options) (config.Item, error) { var err error svc := Svc(opts) item := config.Item{Key: opts.Key} params := &dynamodb.DeleteItemInput{ Key: map[string]*dynamodb.AttributeValue{ "key": { S: aws.String(opts.Key), }, }, TableName: aws.String(opts.CfgName), ReturnValues: aws.String("ALL_OLD"), // TODO: think about this for statistics // INDEXES | TOTAL | NONE //ReturnConsumedCapacity: aws.String("ReturnConsumedCapacity"), } // Conditional delete operation if opts.ConditionalValue != "" { // Alias value since it's a reserved word params.ExpressionAttributeNames = make(map[string]*string) params.ExpressionAttributeNames["#v"] = aws.String("value") // Set the condition expression value and compare params.ExpressionAttributeValues = make(map[string]*dynamodb.AttributeValue) params.ExpressionAttributeValues[":condition"] = &dynamodb.AttributeValue{B: []byte(opts.ConditionalValue)} params.ConditionExpression = aws.String("#v = :condition") } response, err := svc.DeleteItem(params) if err == nil { if len(response.Attributes) > 0 { item.Value = response.Attributes["value"].B item.Version, _ = strconv.ParseInt(*response.Attributes["version"].N, 10, 64) } } return item, err }
// Get a key in DynamoDB func (db DynamoDB) Get(opts config.Options) (config.Item, error) { var err error svc := Svc(opts) item := config.Item{Key: opts.Key} params := &dynamodb.QueryInput{ TableName: aws.String(opts.CfgName), // KEY and VALUE are reserved words so the query needs to dereference them ExpressionAttributeNames: map[string]*string{ "#k": aws.String("key"), }, ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{ ":key": { S: aws.String(opts.Key), }, }, KeyConditionExpression: aws.String("#k = :key"), // TODO: Return more? It's nice to have a history now whereas previously I thought I might now have one...But what's the use? Limit: aws.Int64(1), // INDEXES | TOTAL | NONE (not required - not even sure if I need to worry about it) ReturnConsumedCapacity: aws.String("TOTAL"), // Important: This needs to be false so it returns results in descending order. If it's true (the default), it's sorted in the // order values were stored. So the first item stored for the key ever would be returned...But the latest item is needed. ScanIndexForward: aws.Bool(false), // http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Query.html#DDB-Query-request-Select Select: aws.String("ALL_ATTRIBUTES"), } response, err := svc.Query(params) if err == nil { // Print the error, cast err to awserr.Error to get the Code and // Message from an error. //fmt.Println(err.Error()) if len(response.Items) > 0 { // Every field should now be checked because it's possible to have a response without a value or version. // For example, the root key "/" may only hold information about the config version and modified time. // It may not have a set value and therefore it also won't have a relative version either. // TODO: Maybe it should? We can always version it as 1 even if empty value. Perhaps also an empty string value... // But the update config version would need to have a compare for an empty value. See if DynamoDB can do that. // For now, just check the existence of keys in the map. if val, ok := response.Items[0]["value"]; ok { item.Value = val.B } if val, ok := response.Items[0]["version"]; ok { item.Version, _ = strconv.ParseInt(*val.N, 10, 64) } // Expiration/TTL (only set if > 0) if val, ok := response.Items[0]["ttl"]; ok { ttl, _ := strconv.ParseInt(*val.N, 10, 64) if ttl > 0 { item.TTL = ttl } } if val, ok := response.Items[0]["expires"]; ok { expiresNano, _ := strconv.ParseInt(*val.N, 10, 64) if expiresNano > 0 { item.Expiration = time.Unix(0, expiresNano) } } // If cfgVersion and cfgModified are set because it's the root key "/" then set those too. // This is only returned for the root key. no sense in making a separate get function because operations like // exporting would then require more queries than necessary. However, it won't be displayed in the item's JSON output. if val, ok := response.Items[0]["cfgVersion"]; ok { item.CfgVersion, _ = strconv.ParseInt(*val.N, 10, 64) } if val, ok := response.Items[0]["cfgModified"]; ok { item.CfgModifiedNanoseconds, _ = strconv.ParseInt(*val.N, 10, 64) } } // Check the TTL if item.TTL > 0 { // If expired, return an empty item if item.Expiration.UnixNano() < time.Now().UnixNano() { item = config.Item{Key: opts.Key} // Delete the now expired item // NOTE: This does mean waiting on another DynamoDB request and that technically means slower performance in these situations, but is it a conern? // A goroutine doesn't help because there's not guarantee there's time for it to complete. db.Delete(opts) } } } return item, err }
// Update a key in DynamoDB func (db DynamoDB) Update(opts config.Options) (config.Item, error) { var err error svc := Svc(opts) item := config.Item{Key: opts.Key} ttlString := strconv.FormatInt(opts.TTL, 10) expires := time.Now().Add(time.Duration(opts.TTL) * time.Second) expiresInt := expires.UnixNano() expiresString := strconv.FormatInt(expiresInt, 10) // If no TTL was passed in the options, set 0. Anything 0 is indefinite in these cases. if opts.TTL == 0 { expiresString = "0" } // DynamoDB type cheat sheet: // B: []byte("some bytes") // BOOL: aws.Bool(true) // BS: [][]byte{[]byte("bytes and bytes")} // L: []*dynamodb.AttributeValue{{...recursive values...}} // M: map[string]*dynamodb.AttributeValue{"key": {...recursive...} } // N: aws.String("number") // NS: []*String{aws.String("number"), aws.String("number")} // NULL: aws.Bool(true) // S: aws.String("string") // SS: []*string{aws.String("string"), aws.String("string")} // If always putting new items, there's no conditional update. // But the only way to update is to make the items have a HASH only index instead of HASH + RANGE. params := &dynamodb.UpdateItemInput{ Key: map[string]*dynamodb.AttributeValue{ "key": { S: aws.String(opts.Key), }, }, TableName: aws.String(opts.CfgName), // KEY and VALUE are reserved words so the query needs to dereference them ExpressionAttributeNames: map[string]*string{ //"#k": aws.String("key"), "#v": aws.String("value"), // If TTL is a reserved word in DynamoDB...Then why doesn't it seem to have a TTL feature?? "#t": aws.String("ttl"), }, ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{ // value ":value": { //B: []byte(opts.Value), // <-- sure, if all we ever stored as strings. B: opts.Value, }, // TTL ":ttl": { N: aws.String(ttlString), }, // Expiration timestamp ":expires": { N: aws.String(expiresString), }, // version increment ":i": { N: aws.String("1"), }, }, //ReturnConsumedCapacity: aws.String("TOTAL"), //ReturnItemCollectionMetrics: aws.String("ReturnItemCollectionMetrics"), ReturnValues: aws.String("ALL_OLD"), UpdateExpression: aws.String("SET #v = :value, #t = :ttl, expires = :expires ADD version :i"), } // Conditional write operation (CAS) if opts.ConditionalValue != "" { params.ExpressionAttributeValues[":condition"] = &dynamodb.AttributeValue{B: []byte(opts.ConditionalValue)} params.ConditionExpression = aws.String("#v = :condition") } response, err := svc.UpdateItem(params) if err == nil { // The old values if val, ok := response.Attributes["value"]; ok { item.Value = val.B item.Version, _ = strconv.ParseInt(*response.Attributes["version"].N, 10, 64) } } return item, err }