예제 #1
0
// 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
}
예제 #2
0
// 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
}
예제 #3
0
// 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
}