diff --git a/modules/tests/test_dynamodb/src/lib.rs b/modules/tests/test_dynamodb/src/lib.rs index b6b66b49..45dd0b2e 100644 --- a/modules/tests/test_dynamodb/src/lib.rs +++ b/modules/tests/test_dynamodb/src/lib.rs @@ -40,10 +40,13 @@ fn main(log: String, _: LogSource) -> Result<(), i32> { ], "binary_field": { "_binary": BASE64_STANDARD.encode("binary_data") } // Binary }); - let item_hm = serde_json::from_value::>(item_json).unwrap(); + let item_hm = serde_json::from_value(item_json).unwrap(); let input = PutItemInput { table_name: table_name.clone(), item: item_hm, + // NONE - If ReturnValues is not specified, or if its value is NONE, then nothing is returned. (This setting is the default for ReturnValues.) + // ALL_OLD - If PutItem overwrote an attribute name-value pair, then the content of the old item is returned. + // https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html#DDB-PutItem-request-ReturnValues return_values: Some(String::from("ALL_OLD")), condition_expression: None, expression_attribute_names: None, @@ -56,8 +59,11 @@ fn main(log: String, _: LogSource) -> Result<(), i32> { return Err(1); } Ok(output) => { - let output_json = to_value(&output).unwrap(); - let expected = json!({"attributes":null}); + let output_json = + to_value(&output).expect("failed deserializing PutItemOutput to value"); + let expected = json!({ + "attributes":null + }); plaid::print_debug_string(&format!("put_item output: {output_json}")); if output_json != expected { plaid::print_debug_string(&format!( @@ -81,7 +87,44 @@ fn main(log: String, _: LogSource) -> Result<(), i32> { }; let output = dynamodb::query(input).unwrap(); let output_json = to_value(&output).unwrap(); - let expected = json!({"items":[{"age":33,"binaries":["ZGF0YTE=","ZGF0YTI="],"binary_field":{"_binary":"YmluYXJ5X2RhdGE="},"is_active":true,"metadata":{"city":"New York","country":"USA"},"name":"Jane Doe","null_field":null,"pk":"124","ratings":[3.8,4.5,5],"scores":[88,92,95],"tags":["aws","dev","rust"],"timestamp":"124"}]}); + let expected = json!({ + "items": [ + { + "age": 33, + "binaries": [ + "ZGF0YTE=", + "ZGF0YTI=" + ], + "binary_field": { + "_binary": "YmluYXJ5X2RhdGE=" + }, + "is_active": true, + "metadata": { + "city": "New York", + "country": "USA" + }, + "name": "Jane Doe", + "null_field": null, + "pk": "124", + "ratings": [ + 3.8, + 4.5, + 5 + ], + "scores": [ + 88, + 92, + 95 + ], + "tags": [ + "aws", + "dev", + "rust" + ], + "timestamp": "124" + } + ] + }); if output_json != expected { plaid::print_debug_string(&format!( "error: query output_json mismatch. expected {expected} got {output_json}" @@ -107,7 +150,42 @@ fn main(log: String, _: LogSource) -> Result<(), i32> { let output = dynamodb::delete_item(input).unwrap(); let output_json = to_value(&output).unwrap(); - let expected = json!({"attributes":{"age":33,"binaries":["ZGF0YTE=","ZGF0YTI="],"binary_field":{"_binary":"YmluYXJ5X2RhdGE="},"is_active":true,"metadata":{"city":"New York","country":"USA"},"name":"Jane Doe","null_field":null,"pk":"124","ratings":[3.8,4.5,5],"scores":[88,92,95],"tags":["aws","dev","rust"],"timestamp":"124"}}); + let expected = json!({ + "attributes": { + "age": 33, + "binaries": [ + "ZGF0YTE=", + "ZGF0YTI=" + ], + "binary_field": { + "_binary": "YmluYXJ5X2RhdGE=" + }, + "is_active": true, + "metadata": { + "city": "New York", + "country": "USA" + }, + "name": "Jane Doe", + "null_field": null, + "pk": "124", + "ratings": [ + 3.8, + 4.5, + 5 + ], + "scores": [ + 88, + 92, + 95 + ], + "tags": [ + "aws", + "dev", + "rust" + ], + "timestamp": "124" + } + }); if output_json != expected { plaid::print_debug_string(&format!( "error: delete_item output_json mismatch. expected {expected} got {output_json}" diff --git a/runtime/plaid-stl/src/aws/dynamodb.rs b/runtime/plaid-stl/src/aws/dynamodb.rs index a24bbedc..5a7fa355 100644 --- a/runtime/plaid-stl/src/aws/dynamodb.rs +++ b/runtime/plaid-stl/src/aws/dynamodb.rs @@ -8,246 +8,214 @@ use crate::PlaidFunctionError; const RETURN_BUFFER_SIZE: usize = 1024 * 1024 * 4; // 4 MiB #[derive(Serialize, Deserialize)] -///

Represents the input of a PutItem operation.

+/// Input for put_item operation pub struct PutItemInput { - ///

The name of the table to contain the item. You can also provide the Amazon Resource Name (ARN) of the table in this parameter.

+ /// The name of the table to contain the item. You can also provide the Amazon Resource Name (ARN) of the table in this parameter. pub table_name: String, - ///

A map of attribute name/value pairs, one for each attribute. Only the primary key attributes are required; you can optionally provide other attribute name-value pairs for the item.

- ///

You must provide all of the attributes for the primary key. For example, with a simple primary key, you only need to provide a value for the partition key. For a composite primary key, you must provide both values for both the partition key and the sort key.

- ///

If you specify any attributes that are part of an index key, then the data types for those attributes must match those of the schema in the table's attribute definition.

- ///

Empty String and Binary attribute values are allowed. Attribute values of type String and Binary must have a length greater than zero if the attribute is used as a key attribute for a table or index.

- ///

For more information about primary keys, see Primary Key in the Amazon DynamoDB Developer Guide.

- ///

Each element in the Item map is an AttributeValue object.

+ /// A map of attribute name/value pairs, one for each attribute. Only the primary key attributes are required; you can optionally provide other attribute name-value pairs for the item. + /// You must provide all of the attributes for the primary key. For example, with a simple primary key, you only need to provide a value for the partition key. For a composite primary key, you must provide both values for both the partition key and the sort key. + /// + /// More Info + /// https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/WorkingWithItems.html pub item: HashMap, - ///

One or more substitution tokens for attribute names in an expression. The following are some use cases for using ExpressionAttributeNames:

- /// - ///

Use the # character in an expression to dereference an attribute name. For example, consider the following attribute name:

- /// - ///

The name of this attribute conflicts with a reserved word, so it cannot be used directly in an expression. (For the complete list of reserved words, see Reserved Words in the Amazon DynamoDB Developer Guide). To work around this, you could specify the following for ExpressionAttributeNames:

- /// - ///

You could then use this substitution in an expression, as in this example:

- /// - ///

Tokens that begin with the : character are expression attribute values, which are placeholders for the actual value at runtime.

- ///
- ///

For more information on expression attribute names, see Specifying Item Attributes in the Amazon DynamoDB Developer Guide.

+ /// One or more substitution tokens for attribute names in an expression. The following are some use cases for using ExpressionAttributeNames: + /// * To access an attribute whose name conflicts with a DynamoDB reserved word. + /// * To create a placeholder for repeating occurrences of an attribute name in an expression. + /// * To prevent special characters in an attribute name from being misinterpreted in an expression. + /// Use the # character in an expression to dereference an attribute name. For example, consider the following attribute name: + /// + /// Percentile + /// + /// The name of this attribute conflicts with a reserved word, so it cannot be used directly in an expression. (For the complete list of reserved words, see Reserved Words in the Amazon DynamoDB Developer Guide). To work around this, you could specify the following for ExpressionAttributeNames: + /// + /// {"#P":"Percentile"} + /// + /// You could then use this substitution in an expression, as in this example: + /// + /// #P = :val + /// + /// More Info + /// https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.AccessingItemAttributes.html pub expression_attribute_names: Option>, - ///

One or more values that can be substituted in an expression.

- ///

Use the : (colon) character in an expression to dereference an attribute value. For example, suppose that you wanted to check whether the value of the ProductStatus attribute was one of the following:

- ///

Available | Backordered | Discontinued

- ///

You would first need to specify ExpressionAttributeValues as follows:

- ///

{ ":avail":{"S":"Available"}, ":back":{"S":"Backordered"}, ":disc":{"S":"Discontinued"} }

- ///

You could then use these values in an expression, such as this:

- ///

ProductStatus IN (:avail, :back, :disc)

- ///

For more information on expression attribute values, see Condition Expressions in the Amazon DynamoDB Developer Guide.

+ /// One or more values that can be substituted in an expression. + /// + /// Use the : (colon) character in an expression to dereference an attribute value. For example, suppose that you wanted to check whether the value of the ProductStatus attribute was one of the following: + /// + /// Available | Backordered | Discontinued + /// + /// You would first need to specify ExpressionAttributeValues as follows: + /// + /// { ":avail":{"S":"Available"}, ":back":{"S":"Backordered"}, ":disc":{"S":"Discontinued"} } + /// + /// You could then use these values in an expression, such as this: + /// + /// ProductStatus IN (:avail, :back, :disc) + /// + /// More info + /// https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.SpecifyingConditions.html pub expression_attribute_values: Option>, - ///

A condition that must be satisfied in order for a conditional PutItem operation to succeed.

- ///

An expression can contain any of the following:

- ///
    - ///
  • - ///

    Functions: attribute_exists | attribute_not_exists | attribute_type | contains | begins_with | size

    - ///

    These function names are case-sensitive.

  • - ///
  • - ///

    Comparison operators: = | <> | < | > | <= | >= | BETWEEN | IN

  • - ///
  • - ///

    Logical operators: AND | OR | NOT

  • - ///
- ///

For more information on condition expressions, see Condition Expressions in the Amazon DynamoDB Developer Guide.

+ /// A condition that must be satisfied in order for a conditional PutItem operation to succeed. + /// + /// An expression can contain any of the following: + /// + /// Functions: attribute_exists | attribute_not_exists | attribute_type | contains | begins_with | size + /// + /// These function names are case-sensitive. + /// + /// Comparison operators: = | <> | < | > | <= | >= | BETWEEN | IN + /// + /// Logical operators: AND | OR | NOT + /// + /// More Info + /// https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.SpecifyingConditions.html pub condition_expression: Option, - ///

Use ReturnValues if you want to get the item attributes as they appeared before they were updated with the PutItem request. For PutItem, the valid values are:

- ///
    - ///
  • - ///

    NONE - If ReturnValues is not specified, or if its value is NONE, then nothing is returned. (This setting is the default for ReturnValues.)

  • - ///
  • - ///

    ALL_OLD - If PutItem overwrote an attribute name-value pair, then the content of the old item is returned.

  • - ///
- ///

The values returned are strongly consistent.

- ///

There is no additional cost associated with requesting a return value aside from the small network and processing overhead of receiving a larger response. No read capacity units are consumed.

- ///

The ReturnValues parameter is used by several DynamoDB operations; however, PutItem does not recognize any values other than NONE or ALL_OLD.

- ///
+ /// Use ReturnValues if you want to get the item attributes as they appeared before they were updated with the PutItem request. For PutItem, the valid values are: + /// NONE - If ReturnValues is not specified, or if its value is NONE, then nothing is returned. (This setting is the default for ReturnValues.) + /// ALL_OLD - If PutItem overwrote an attribute name-value pair, then the content of the old item is returned. + /// The values returned are strongly consistent. pub return_values: Option, } #[derive(Serialize, Deserialize)] -///

Represents the output of a PutItem operation.

+/// Output for put_item operation pub struct PutItemOutput { - ///

The attribute values as they appeared before the PutItem operation, but only if ReturnValues is specified as ALL_OLD in the request. Each element consists of an attribute name and an attribute value.

+ /// The attribute values as they appeared before the PutItem operation, + /// but only if ReturnValues is specified as ALL_OLD in the request. + /// Each element consists of an attribute name and an attribute value. pub attributes: Option, } #[derive(Serialize, Deserialize)] -///

Represents the input of a DeleteItem operation.

+/// Input for delete_item operation pub struct DeleteItemInput { - ///

The name of the table from which to delete the item. You can also provide the Amazon Resource Name (ARN) of the table in this parameter.

+ /// The name of the table to contain the item. You can also provide the Amazon Resource Name (ARN) of the table in this parameter. pub table_name: String, - ///

A map of attribute names to AttributeValue objects, representing the primary key of the item to delete.

- ///

For the primary key, you must provide all of the key attributes. For example, with a simple primary key, you only need to provide a value for the partition key. For a composite primary key, you must provide values for both the partition key and the sort key.

+ /// A map of attribute names to AttributeValue objects, representing the primary key of the item to delete. + /// For the primary key, you must provide all of the key attributes. For example, with a simple primary key, you only need to provide a value for the partition key. For a composite primary key, you must provide values for both the partition key and the sort key. pub key: HashMap, - ///

A condition that must be satisfied in order for a conditional DeleteItem to succeed.

- ///

An expression can contain any of the following:

- ///
    - ///
  • - ///

    Functions: attribute_exists | attribute_not_exists | attribute_type | contains | begins_with | size

    - ///

    These function names are case-sensitive.

  • - ///
  • - ///

    Comparison operators: = | <> | < | > | <= | >= | BETWEEN | IN

  • - ///
  • - ///

    Logical operators: AND | OR | NOT

  • - ///
- ///

For more information about condition expressions, see Condition Expressions in the Amazon DynamoDB Developer Guide.

- pub condition_expression: Option, - ///

One or more substitution tokens for attribute names in an expression. The following are some use cases for using ExpressionAttributeNames:

- ///
    - ///
  • - ///

    To access an attribute whose name conflicts with a DynamoDB reserved word.

  • - ///
  • - ///

    To create a placeholder for repeating occurrences of an attribute name in an expression.

  • - ///
  • - ///

    To prevent special characters in an attribute name from being misinterpreted in an expression.

  • - ///
- ///

Use the # character in an expression to dereference an attribute name. For example, consider the following attribute name:

- ///
    - ///
  • - ///

    Percentile

  • - ///
- ///

The name of this attribute conflicts with a reserved word, so it cannot be used directly in an expression. (For the complete list of reserved words, see Reserved Words in the Amazon DynamoDB Developer Guide). To work around this, you could specify the following for ExpressionAttributeNames:

- ///
    - ///
  • - ///

    {"#P":"Percentile"}

  • - ///
- ///

You could then use this substitution in an expression, as in this example:

- ///
    - ///
  • - ///

    #P = :val

  • - ///
- ///

Tokens that begin with the : character are expression attribute values, which are placeholders for the actual value at runtime.

- ///
- ///

For more information on expression attribute names, see Specifying Item Attributes in the Amazon DynamoDB Developer Guide.

+ /// One or more substitution tokens for attribute names in an expression. The following are some use cases for using ExpressionAttributeNames: + /// * To access an attribute whose name conflicts with a DynamoDB reserved word. + /// * To create a placeholder for repeating occurrences of an attribute name in an expression. + /// * To prevent special characters in an attribute name from being misinterpreted in an expression. + /// Use the # character in an expression to dereference an attribute name. For example, consider the following attribute name: + /// + /// Percentile + /// + /// The name of this attribute conflicts with a reserved word, so it cannot be used directly in an expression. (For the complete list of reserved words, see Reserved Words in the Amazon DynamoDB Developer Guide). To work around this, you could specify the following for ExpressionAttributeNames: + /// + /// {"#P":"Percentile"} + /// + /// You could then use this substitution in an expression, as in this example: + /// + /// #P = :val + /// + /// More Info + /// https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.AccessingItemAttributes.html pub expression_attribute_names: Option>, - ///

One or more values that can be substituted in an expression.

- ///

Use the : (colon) character in an expression to dereference an attribute value. For example, suppose that you wanted to check whether the value of the ProductStatus attribute was one of the following:

- ///

Available | Backordered | Discontinued

- ///

You would first need to specify ExpressionAttributeValues as follows:

- ///

{ ":avail":{"S":"Available"}, ":back":{"S":"Backordered"}, ":disc":{"S":"Discontinued"} }

- ///

You could then use these values in an expression, such as this:

- ///

ProductStatus IN (:avail, :back, :disc)

- ///

For more information on expression attribute values, see Condition Expressions in the Amazon DynamoDB Developer Guide.

+ /// One or more values that can be substituted in an expression. + /// + /// Use the : (colon) character in an expression to dereference an attribute value. For example, suppose that you wanted to check whether the value of the ProductStatus attribute was one of the following: + /// + /// Available | Backordered | Discontinued + /// + /// You would first need to specify ExpressionAttributeValues as follows: + /// + /// { ":avail":{"S":"Available"}, ":back":{"S":"Backordered"}, ":disc":{"S":"Discontinued"} } + /// + /// You could then use these values in an expression, such as this: + /// + /// ProductStatus IN (:avail, :back, :disc) + /// + /// More info + /// https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.SpecifyingConditions.html pub expression_attribute_values: Option>, - ///

Use ReturnValues if you want to get the item attributes as they appeared before they were deleted. For DeleteItem, the valid values are:

- ///
    - ///
  • - ///

    NONE - If ReturnValues is not specified, or if its value is NONE, then nothing is returned. (This setting is the default for ReturnValues.)

  • - ///
  • - ///

    ALL_OLD - The content of the old item is returned.

  • - ///
- ///

There is no additional cost associated with requesting a return value aside from the small network and processing overhead of receiving a larger response. No read capacity units are consumed.

- ///

The ReturnValues parameter is used by several DynamoDB operations; however, DeleteItem does not recognize any values other than NONE or ALL_OLD.

- ///
+ /// A condition that must be satisfied in order for a conditional PutItem operation to succeed. + /// + /// An expression can contain any of the following: + /// + /// Functions: attribute_exists | attribute_not_exists | attribute_type | contains | begins_with | size + /// + /// These function names are case-sensitive. + /// + /// Comparison operators: = | <> | < | > | <= | >= | BETWEEN | IN + /// + /// Logical operators: AND | OR | NOT + /// + /// More Info + /// https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.SpecifyingConditions.html + pub condition_expression: Option, + /// Use ReturnValues if you want to get the item attributes as they appeared before they were updated with the PutItem request. For PutItem, the valid values are: + /// NONE - If ReturnValues is not specified, or if its value is NONE, then nothing is returned. (This setting is the default for ReturnValues.) + /// ALL_OLD - If PutItem overwrote an attribute name-value pair, then the content of the old item is returned. + /// The values returned are strongly consistent. pub return_values: Option, } #[derive(Serialize, Deserialize)] pub struct DeleteItemOutput { - ///

A map of attribute names to AttributeValue objects, representing the item as it appeared before the DeleteItem operation. This map appears in the response only if ReturnValues was specified as ALL_OLD in the request.

+ /// The attribute values as they appeared before the PutItem operation, + /// but only if ReturnValues is specified as ALL_OLD in the request. + /// Each element consists of an attribute name and an attribute value. pub attributes: Option, } #[derive(Serialize, Deserialize)] +/// Input for query operation pub struct QueryInput { - ///

The name of the table containing the requested items. You can also provide the Amazon - /// Resource Name (ARN) of the table in this parameter.

+ /// The name of the table to contain the item. You can also provide the Amazon Resource Name (ARN) of the table in this parameter. pub table_name: String, - ///

The name of an index to query. This index can be any local secondary index or global secondary index on the table. Note that if you use the IndexName parameter, you must also provide TableName.

+ /// The name of an index to query. This index can be any local secondary index or global secondary index on the table. + /// Note that if you use the IndexName parameter, you must also provide TableName. pub index_name: Option, - ///

The condition that specifies the key values for items to be retrieved by the Query action.

- ///

The condition must perform an equality test on a single partition key value.

- ///

The condition can optionally perform one of several comparison tests on a single sort key value. This allows Query to retrieve one item with a given partition key value and sort key value, or several items that have the same partition key value but different sort key values.

- ///

The partition key equality test is required, and must be specified in the following format:

- ///

partitionKeyName = :partitionkeyval

- ///

If you also want to provide a condition for the sort key, it must be combined using AND with the condition for the sort key. Following is an example, using the = comparison operator for the sort key:

- ///

partitionKeyName = :partitionkeyval AND sortKeyName = :sortkeyval

- ///

Valid comparisons for the sort key condition are as follows:

- ///
    - ///
  • - ///

    sortKeyName = :sortkeyval - true if the sort key value is equal to :sortkeyval.

  • - ///
  • - ///

    sortKeyName < :sortkeyval - true if the sort key value is less than :sortkeyval.

  • - ///
  • - ///

    sortKeyName <= :sortkeyval - true if the sort key value is less than or equal to :sortkeyval.

  • - ///
  • - ///

    sortKeyName > :sortkeyval - true if the sort key value is greater than :sortkeyval.

  • - ///
  • - ///

    sortKeyName >= :sortkeyval - true if the sort key value is greater than or equal to :sortkeyval.

  • - ///
  • - ///

    sortKeyName BETWEEN :sortkeyval1 AND :sortkeyval2 - true if the sort key value is greater than or equal to :sortkeyval1, and less than or equal to :sortkeyval2.

  • - ///
  • - ///

    begins_with ( sortKeyName, :sortkeyval ) - true if the sort key value begins with a particular operand. (You cannot use this function with a sort key that is of type Number.) Note that the function name begins_with is case-sensitive.

  • - ///
- ///

Use the ExpressionAttributeValues parameter to replace tokens such as :partitionval and :sortval with actual values at runtime.

- ///

You can optionally use the ExpressionAttributeNames parameter to replace the names of the partition key and sort key with placeholder tokens. This option might be necessary if an attribute name conflicts with a DynamoDB reserved word. For example, the following KeyConditionExpression parameter causes an error because Size is a reserved word:

- ///
    - ///
  • - ///

    Size = :myval

  • - ///
- ///

To work around this, define a placeholder (such a #S) to represent the attribute name Size. KeyConditionExpression then is as follows:

- ///
    - ///
  • - ///

    #S = :myval

  • - ///
- ///

For a list of reserved words, see Reserved Words in the Amazon DynamoDB Developer Guide.

- ///

For more information on ExpressionAttributeNames and ExpressionAttributeValues, see Using Placeholders for Attribute Names and Values in the Amazon DynamoDB Developer Guide.

+ /// The condition that specifies the key values for items to be retrieved by the Query action. + /// The condition must perform an equality test on a single partition key value. + /// The condition can optionally perform one of several comparison tests on a single sort key value. This allows Query to retrieve one item with a given partition key value and sort key value, or several items that have the same partition key value but different sort key values. + /// The partition key equality test is required, and must be specified in the following format: + /// + /// partitionKeyName = :partitionkeyval + /// + /// If you also want to provide a condition for the sort key, it must be combined using AND with the condition for the sort key. Following is an example, using the = comparison operator for the sort key: + /// + /// partitionKeyName = :partitionkeyval AND sortKeyName = :sortkeyval + /// + /// More Info + /// https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Query.html#DDB-Query-request-KeyConditionExpression pub key_condition_expression: String, - ///

One or more substitution tokens for attribute names in an expression. The following are some use cases for using ExpressionAttributeNames:

- ///
    - ///
  • - ///

    To access an attribute whose name conflicts with a DynamoDB reserved word.

  • - ///
  • - ///

    To create a placeholder for repeating occurrences of an attribute name in an expression.

  • - ///
  • - ///

    To prevent special characters in an attribute name from being misinterpreted in an expression.

  • - ///
- ///

Use the # character in an expression to dereference an attribute name. For example, consider the following attribute name:

- ///
    - ///
  • - ///

    Percentile

  • - ///
- ///

The name of this attribute conflicts with a reserved word, so it cannot be used directly in an expression. (For the complete list of reserved words, see Reserved Words in the Amazon DynamoDB Developer Guide). To work around this, you could specify the following for ExpressionAttributeNames:

- ///
    - ///
  • - ///

    {"#P":"Percentile"}

  • - ///
- ///

You could then use this substitution in an expression, as in this example:

- ///
    - ///
  • - ///

    #P = :val

  • - ///
- ///

Tokens that begin with the : character are expression attribute values, which are placeholders for the actual value at runtime.

- ///
- ///

For more information on expression attribute names, see Specifying Item Attributes in the Amazon DynamoDB Developer Guide.

+ /// One or more substitution tokens for attribute names in an expression. The following are some use cases for using ExpressionAttributeNames: + /// * To access an attribute whose name conflicts with a DynamoDB reserved word. + /// * To create a placeholder for repeating occurrences of an attribute name in an expression. + /// * To prevent special characters in an attribute name from being misinterpreted in an expression. + /// Use the # character in an expression to dereference an attribute name. For example, consider the following attribute name: + /// + /// Percentile + /// + /// The name of this attribute conflicts with a reserved word, so it cannot be used directly in an expression. (For the complete list of reserved words, see Reserved Words in the Amazon DynamoDB Developer Guide). To work around this, you could specify the following for ExpressionAttributeNames: + /// + /// {"#P":"Percentile"} + /// + /// You could then use this substitution in an expression, as in this example: + /// + /// #P = :val + /// + /// More Info + /// https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.AccessingItemAttributes.html pub expression_attribute_names: Option>, - ///

One or more values that can be substituted in an expression.

- ///

Use the : (colon) character in an expression to dereference an attribute value. For example, suppose that you wanted to check whether the value of the ProductStatus attribute was one of the following:

- ///

Available | Backordered | Discontinued

- ///

You would first need to specify ExpressionAttributeValues as follows:

- ///

{ ":avail":{"S":"Available"}, ":back":{"S":"Backordered"}, ":disc":{"S":"Discontinued"} }

- ///

You could then use these values in an expression, such as this:

- ///

ProductStatus IN (:avail, :back, :disc)

- ///

For more information on expression attribute values, see Specifying Conditions in the Amazon DynamoDB Developer Guide.

+ /// One or more values that can be substituted in an expression. + /// + /// Use the : (colon) character in an expression to dereference an attribute value. For example, suppose that you wanted to check whether the value of the ProductStatus attribute was one of the following: + /// + /// Available | Backordered | Discontinued + /// + /// You would first need to specify ExpressionAttributeValues as follows: + /// + /// { ":avail":{"S":"Available"}, ":back":{"S":"Backordered"}, ":disc":{"S":"Discontinued"} } + /// + /// You could then use these values in an expression, such as this: + /// + /// ProductStatus IN (:avail, :back, :disc) + /// + /// More info + /// https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.SpecifyingConditions.html pub expression_attribute_values: Option>, } @@ -256,7 +224,15 @@ pub struct QueryOutput { pub items: Vec, } -/// Put item in dynamodb table. +/// Creates a new item, or replaces an old item with a new item. +/// If an item that has the same primary key as the new item already exists in the specified table, +/// the new item completely replaces the existing item. You can perform a conditional put operation +/// (add a new item if one with the specified primary key doesn't exist), +/// or replace an existing item if it has certain attribute values. +/// You can return the item's attribute values in the same operation, using the ReturnValues parameter. +/// +/// More Info: +/// https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html pub fn put_item(input: PutItemInput) -> Result { extern "C" { new_host_function_with_error_buffer!(aws_dynamodb, put_item); @@ -287,7 +263,13 @@ pub fn put_item(input: PutItemInput) -> Result Result { extern "C" { new_host_function_with_error_buffer!(aws_dynamodb, delete_item); @@ -318,7 +300,15 @@ pub fn delete_item(input: DeleteItemInput) -> Result Result { extern "C" { new_host_function_with_error_buffer!(aws_dynamodb, query); diff --git a/runtime/plaid/src/apis/aws/dynamodb.rs b/runtime/plaid/src/apis/aws/dynamodb.rs index 8a73c87a..9b580b5e 100644 --- a/runtime/plaid/src/apis/aws/dynamodb.rs +++ b/runtime/plaid/src/apis/aws/dynamodb.rs @@ -1,10 +1,7 @@ -use aws_sdk_dynamodb::primitives::Blob; -use aws_sdk_dynamodb::types::{AttributeValue, ReturnValue}; use aws_sdk_dynamodb::Client; use plaid_stl::aws::dynamodb::{ DeleteItemInput, DeleteItemOutput, PutItemInput, PutItemOutput, QueryInput, QueryOutput, }; -use serde_json::{json, Map, Value}; use std::{ collections::{HashMap, HashSet}, @@ -13,7 +10,10 @@ use std::{ use serde::{Deserialize, Serialize}; -use crate::apis::ApiError; +use crate::apis::{ + aws::dynamodb_utils::{attributes_into_json, json_into_attributes, return_value_from_string}, + ApiError, +}; use crate::{get_aws_sdk_config, loader::PlaidModule, AwsAuthentication}; /// Defines configuration for the DynamoDB API @@ -30,6 +30,9 @@ pub struct DynamoDbConfig { rw: HashMap>, /// Configured readers - maps a table name to a list of rules that are allowed to READ data r: HashMap>, + /// Reserved tables - list of 'reserved' table names which rules cannot access + #[serde(default)] + reserved_tables: Option>, } /// Represents the DynamoDB API client. @@ -41,9 +44,12 @@ pub struct DynamoDb { rw: HashMap>, /// Configured readers - maps a table name to a list of rules that are allowed to READ data r: HashMap>, + /// Reserved tables - list of 'reserved' table names which rules cannot access + reserved_tables: Option>, } -#[derive(PartialEq, PartialOrd)] +#[derive(PartialEq, PartialOrd, Debug)] +/// Represents an access scope that a rule has to modify a DynamoDB table enum AccessScope { Read, Write, @@ -57,22 +63,29 @@ impl DynamoDb { rw, r, local_endpoint, + reserved_tables, } = config; if local_endpoint { - return DynamoDb::local_endpoint(r, rw).await; + return DynamoDb::local_endpoint(r, rw, reserved_tables).await; } let sdk_config = get_aws_sdk_config(&authentication).await; let client = Client::new(&sdk_config); - Self { client, rw, r } + Self { + client, + rw, + r, + reserved_tables, + } } - /// constructor for the local instance of DynamoDB + /// Constructor for the local instance of DynamoDB async fn local_endpoint( r: HashMap>, rw: HashMap>, + reserved_tables: Option>, ) -> Self { let config = aws_config::defaults(aws_config::BehaviorVersion::latest()) // DynamoDB run locally uses port 8000 by default. @@ -83,15 +96,34 @@ impl DynamoDb { let client = aws_sdk_dynamodb::Client::from_conf(dynamodb_local_config); - Self { client, r, rw } + Self { + client, + r, + rw, + reserved_tables, + } } + /// Checks if a module can perform a given action + /// Modules are registered as as read (R) or write (RW) under self. + /// This function checks: + /// * If the table is a reserved table i.e. no Module is allowed to access reserved tables. + /// * If the module is configured as a Reader or Writer of a given table fn check_module_permissions( &self, access_scope: AccessScope, module: Arc, table_name: &str, ) -> Result<(), ApiError> { + // Check if table is reserved table + // no rule is allowed to operate on reserved table + if let Some(inner) = &self.reserved_tables { + if inner.contains(table_name) { + warn!("[{module}] failed {access_scope:?} access reserved dynamodb table [{table_name}]"); + return Err(ApiError::BadRequest); + } + } + match access_scope { AccessScope::Read => { // check if read access is configured for this table @@ -125,6 +157,17 @@ impl DynamoDb { }; } + // check if read access is configured for this table + if let Some(table_readers) = self.r.get(table_name) { + // check if this module has read access to this table + if table_readers.contains(&module.to_string()) { + warn!( + "[{module}] trying to [write] but only has [read] permission for dynamodb table [{table_name}]" + ); + return Err(ApiError::BadRequest); + } + } + warn!( "[{module}] failed [write] permission check for dynamodb table [{table_name}]" ); @@ -133,6 +176,15 @@ impl DynamoDb { } } + /// Creates a new item, or replaces an old item with a new item. + /// If an item that has the same primary key as the new item already exists in the specified table, + /// the new item completely replaces the existing item. You can perform a conditional put operation + /// (add a new item if one with the specified primary key doesn't exist), + /// or replace an existing item if it has certain attribute values. + /// You can return the item's attribute values in the same operation, using the ReturnValues parameter. + /// + /// More Info: + /// https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html pub async fn put_item( &self, params: &str, @@ -177,6 +229,13 @@ impl DynamoDb { serde_json::to_string(&out).map_err(|err| ApiError::SerdeError(err.to_string())) } + /// Deletes a single item in a table by primary key. You can perform a conditional delete operation that deletes the item if it exists, or if it has an expected attribute value. + /// In addition to deleting an item, you can also return the item's attribute values in the same operation, using the ReturnValues parameter. + /// Unless you specify conditions, the DeleteItem is an idempotent operation; running it multiple times on the same item or attribute does not result in an error response. + /// Conditional deletes are useful for deleting items only if specific conditions are met. If those conditions are met, DynamoDB performs the delete. Otherwise, the item is not deleted. + /// + /// More Info + /// https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html pub async fn delete_item( &self, params: &str, @@ -220,6 +279,15 @@ impl DynamoDb { serde_json::to_string(&out).map_err(|err| ApiError::SerdeError(err.to_string())) } + /// You must provide the name of the partition key attribute and a single value for that attribute. + /// Query returns all items with that partition key value. + /// Optionally, you can provide a sort key attribute and use a comparison operator to refine the search results. + /// + /// Use the KeyConditionExpression parameter to provide a specific value for the partition key. + /// The Query operation will return all of the items from the table or index with that partition key value. + /// + /// More Info + /// https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Query.html pub async fn query(&self, params: &str, module: Arc) -> Result { let QueryInput { table_name, @@ -254,282 +322,9 @@ impl DynamoDb { } } -/// Converts DynamoDB Attributes into JSON -fn attributes_into_json(attrs: &HashMap) -> Result { - let mut result = Map::new(); - for (k, v) in attrs.iter() { - let new_val = to_json_value(v)?; - result.insert(k.to_string(), new_val); - } - Ok(Value::Object(result)) -} - -/// Converts JSON into DynamoDB Attributes -fn json_into_attributes( - value: Option>, -) -> Result>, ApiError> { - if let Some(expr_vals) = value { - let mut express_attribute_values = HashMap::::new(); - for (key, value) in expr_vals { - let attr_value = to_attribute_value(value)?; - express_attribute_values.insert(key, attr_value); - } - Ok(Some(express_attribute_values)) - } else { - Ok(None) - } -} - -/// converts String into Typed ReturnValue enum for use with DynamoDB api -fn return_value_from_string(value: Option) -> Result, ApiError> { - value - .as_ref() - .map(|rv| match rv.as_str() { - "ALL_NEW" => Ok(ReturnValue::AllNew), - "ALL_OLD" => Ok(ReturnValue::AllOld), - "UPDATED_NEW" => Ok(ReturnValue::UpdatedNew), - "UPDATED_OLD" => Ok(ReturnValue::UpdatedOld), - "NONE" | "" => Ok(ReturnValue::None), - _ => Err(ApiError::SerdeError(format!( - "Invalid return_values: {}.", - rv - ))), - }) - .transpose() -} - -enum ArrayMembers { - AllStrings, - AllNumbers, - AllBinary, - NonUniform, -} - -/// helper function for use when converting JSON array to strongly typed array -fn inspect_array_members(arr: &Vec) -> ArrayMembers { - // Check if all elements are strings (for SS) - if arr.iter().all(|v| v.is_string()) { - return ArrayMembers::AllStrings; - } - // Check if all elements are numbers (for NS) - if arr.iter().all(|v| v.is_number()) { - return ArrayMembers::AllNumbers; - } - // Check if all elements are binary (assuming base64-encoded strings for B) - let all_binary = arr.iter().all(|v| { - v.as_str() - .map(|s| base64::decode(s).is_ok()) - .unwrap_or(false) - }); - - if all_binary { - return ArrayMembers::AllBinary; - } - ArrayMembers::NonUniform -} - -/// helper function to convert JSON Value to DynamoDB AttributeValue, supporting all types -fn to_attribute_value(value: Value) -> Result { - match value { - // String: Direct string value - Value::String(s) => Ok(AttributeValue::S(s)), - - // Number: Convert to string for DynamoDB - Value::Number(n) => { - if let Some(i) = n.as_i64() { - Ok(AttributeValue::N(i.to_string())) - } else if let Some(f) = n.as_f64() { - Ok(AttributeValue::N(f.to_string())) - } else { - Err(ApiError::SerdeError(String::from( - "Unsupported number format", - ))) - } - } - - // Boolean: Direct boolean value - Value::Bool(b) => Ok(AttributeValue::Bool(b)), - - // Null: Direct null value - Value::Null => Ok(AttributeValue::Null(true)), - - // Array: Handle lists and sets - Value::Array(arr) => { - if arr.is_empty() { - return Err(ApiError::SerdeError(String::from( - "Lists and sets cannot be empty", - ))); - } - match inspect_array_members(&arr) { - ArrayMembers::AllStrings => { - // String Set (SS) - let strings: Vec = arr - .into_iter() - .map(|v| v.as_str().unwrap().to_string()) - .collect(); - Ok(AttributeValue::Ss(strings)) - } - ArrayMembers::AllNumbers => { - // Number Set (NS) - let numbers: Result, ApiError> = arr - .into_iter() - .map(|v| { - if let Some(i) = v.as_i64() { - Ok(i.to_string()) - } else if let Some(f) = v.as_f64() { - Ok(f.to_string()) - } else { - // Err("Invalid number in number set".to_string()) - Err(ApiError::SerdeError(String::from( - "Invalid number in number set", - ))) - } - }) - .collect(); - Ok(AttributeValue::Ns(numbers?)) - } - ArrayMembers::AllBinary => { - // Binary Set (BS) - let binaries: Result, ApiError> = arr - .into_iter() - .map(|v| { - let s = v.as_str().ok_or(ApiError::SerdeError(String::from( - "Invalid binary value", - )))?; - let decoded = base64::decode(s).map_err(|e| { - ApiError::SerdeError(format!("Failed to decode base64: {}", e)) - })?; - Ok(Blob::new(decoded)) - }) - .collect(); - Ok(AttributeValue::Bs(binaries?)) - } - ArrayMembers::NonUniform => { - // List (L) - let items: Result, ApiError> = - arr.into_iter().map(to_attribute_value).collect(); - Ok(AttributeValue::L(items?)) - } - } - } - - // Object: Handle maps and binary values - Value::Object(obj) => { - // Check if the object represents a binary value (e.g., {"_binary": "base64string"}) - if obj.len() == 1 && obj.contains_key("_binary") { - if let Some(Value::String(base64_str)) = obj.get("_binary") { - let decoded = base64::decode(base64_str).map_err(|e| { - ApiError::SerdeError(format!("Failed to decode base64: {}", e)) - })?; - return Ok(AttributeValue::B(Blob::new(decoded))); - } else { - return Err(ApiError::SerdeError(String::from( - "_binary must be a base64-encoded string", - ))); - } - } - - // Otherwise, treat as a map (M) - let mut map = HashMap::new(); - for (k, v) in obj { - map.insert(k, to_attribute_value(v)?); - } - Ok(AttributeValue::M(map)) - } - } -} - -// helper function to convert DynamoDB AttributeValue to JSON Value -fn to_json_value(attr_value: &AttributeValue) -> Result { - match attr_value { - AttributeValue::S(s) => Ok(Value::String(s.clone())), - AttributeValue::N(n) => { - // Try parsing as integer first, then float - if let Ok(int_val) = n.parse::() { - Ok(json!(int_val)) - } else if let Ok(float_val) = n.parse::() { - Ok(json!(float_val)) - } else { - Err(ApiError::SerdeError(format!( - "Invalid number format: {}", - n - ))) - } - } - AttributeValue::B(blob) => { - let base64_str = base64::encode(blob.as_ref()); - Ok(json!({ "_binary": base64_str })) - } - AttributeValue::Bool(b) => Ok(Value::Bool(*b)), - AttributeValue::Null(_) => Ok(Value::Null), - AttributeValue::L(list) => { - let json_list: Result, ApiError> = list.iter().map(to_json_value).collect(); - Ok(Value::Array(json_list?)) - } - AttributeValue::M(map) => { - let mut json_map = serde_json::Map::new(); - for (key, value) in map { - let json_value = to_json_value(value)?; - json_map.insert(key.clone(), json_value); - } - Ok(Value::Object(json_map)) - } - AttributeValue::Ss(strings) => { - if strings.is_empty() { - return Err(ApiError::SerdeError( - "String set cannot be empty".to_string(), - )); - } - Ok(Value::Array( - strings.iter().map(|s| Value::String(s.clone())).collect(), - )) - } - AttributeValue::Ns(numbers) => { - if numbers.is_empty() { - return Err(ApiError::SerdeError( - "Number set cannot be empty".to_string(), - )); - } - let json_numbers: Result, ApiError> = numbers - .iter() - .map(|n| { - if let Ok(int_val) = n.parse::() { - Ok(json!(int_val)) - } else if let Ok(float_val) = n.parse::() { - Ok(json!(float_val)) - } else { - Err(ApiError::SerdeError(format!( - "Invalid number in number set: {}", - n - ))) - } - }) - .collect(); - Ok(Value::Array(json_numbers?)) - } - AttributeValue::Bs(blobs) => { - if blobs.is_empty() { - return Err(ApiError::SerdeError( - "Binary set cannot be empty".to_string(), - )); - } - let json_binaries: Vec = blobs - .iter() - .map(|blob| Value::String(base64::encode(blob.as_ref()))) - .collect(); - Ok(Value::Array(json_binaries)) - } - // Handle any unexpected variants - _ => Err(ApiError::SerdeError(format!( - "Unsupported AttributeValue variant: {:?}", - attr_value - ))), - } -} - #[cfg(test)] pub mod tests { - use serde_json::{from_str, from_value}; + use serde_json::{from_str, from_value, json, Value}; use wasmer::{ sys::{Cranelift, EngineBuilder}, Module, Store, @@ -586,6 +381,7 @@ pub mod tests { local_endpoint: true, r: readers, rw: writers, + reserved_tables: None, }; println!("{}", toml::to_string(&cfg).unwrap()); @@ -595,19 +391,28 @@ pub mod tests { async fn permission_checks() { let table_name = String::from("local_test"); // permissions + let reserved: HashSet = vec![String::from("reserved")].into_iter().collect(); let readers = json!({table_name.clone(): ["module_a"]}); let readers = from_value::>>(readers).unwrap(); let writers = json!({table_name.clone(): ["module_b"]}); let writers = from_value::>>(writers).unwrap(); - let client = DynamoDb::local_endpoint(readers, writers).await; + let client = DynamoDb::local_endpoint(readers, writers, Some(reserved)).await; // modules let module_a = test_module("module_a", true); // reader let module_b = test_module("module_b", true); // writer let module_c = test_module("module_c", true); // no access + // try access reserved table + client + .check_module_permissions(AccessScope::Read, module_a.clone(), "reserved") + .expect_err("expect to fail with BadRequest"); + client + .check_module_permissions(AccessScope::Write, module_a.clone(), "reserved") + .expect_err("expect to fail with BadRequest"); + // modules can read table client .check_module_permissions(AccessScope::Read, module_a.clone(), &table_name) @@ -653,6 +458,7 @@ pub mod tests { authentication: AwsAuthentication::Iam {}, rw, r: HashMap::new(), + reserved_tables: None, }; let client = DynamoDb::new(cfg).await; let m = test_module("test_module", true); @@ -715,7 +521,44 @@ pub mod tests { let output_json = from_str::(&output).unwrap(); assert_eq!( output_json, - json!({"items":[{"age":33,"binaries":["ZGF0YTE=","ZGF0YTI="],"binary_field":{"_binary":"YmluYXJ5X2RhdGE="},"is_active":true,"metadata":{"city":"New York","country":"USA"},"name":"Jane Doe","null_field":null,"pk":"124","ratings":[3.8,4.5,5],"scores":[88,92,95],"tags":["aws","dev","rust"],"timestamp":"124"}]}) + json!({ + "items":[ + { + "age":33, + "binaries":[ + "ZGF0YTE=", + "ZGF0YTI=" + ], + "binary_field":{ + "_binary":"YmluYXJ5X2RhdGE=" + }, + "is_active":true, + "metadata":{ + "city":"New York", + "country":"USA" + }, + "name":"Jane Doe", + "null_field":null, + "pk":"124", + "ratings":[ + 3.8, + 4.5, + 5 + ], + "scores":[ + 88, + 92, + 95 + ], + "tags":[ + "aws", + "dev", + "rust" + ], + "timestamp":"124" + } + ] + }) ); // Delete Item @@ -739,7 +582,42 @@ pub mod tests { let output_json = from_str::(&output).unwrap(); assert_eq!( output_json, - json!({"attributes":{"age":33,"binaries":["ZGF0YTE=","ZGF0YTI="],"binary_field":{"_binary":"YmluYXJ5X2RhdGE="},"is_active":true,"metadata":{"city":"New York","country":"USA"},"name":"Jane Doe","null_field":null,"pk":"124","ratings":[3.8,4.5,5],"scores":[88,92,95],"tags":["aws","dev","rust"],"timestamp":"124"}}) + json!({ + "attributes":{ + "age":33, + "binaries":[ + "ZGF0YTE=", + "ZGF0YTI=" + ], + "binary_field":{ + "_binary":"YmluYXJ5X2RhdGE=" + }, + "is_active":true, + "metadata":{ + "city":"New York", + "country":"USA" + }, + "name":"Jane Doe", + "null_field":null, + "pk":"124", + "ratings":[ + 3.8, + 4.5, + 5 + ], + "scores":[ + 88, + 92, + 95 + ], + "tags":[ + "aws", + "dev", + "rust" + ], + "timestamp":"124" + } + }) ); } } diff --git a/runtime/plaid/src/apis/aws/dynamodb_utils.rs b/runtime/plaid/src/apis/aws/dynamodb_utils.rs new file mode 100644 index 00000000..0c5dba80 --- /dev/null +++ b/runtime/plaid/src/apis/aws/dynamodb_utils.rs @@ -0,0 +1,292 @@ +use crate::apis::ApiError; +use aws_sdk_dynamodb::primitives::Blob; +use aws_sdk_dynamodb::types::{AttributeValue, ReturnValue}; +use serde_json::{json, Map, Value}; +use std::collections::HashMap; + +/// Converts DynamoDB Attributes into JSON Object +/// Convience method so the caller of the plaid DynamoDB API can submit items in simple +/// JSON format and we will automatically convert into DynamoDB attributes which +/// is the internal representation of the Item data. +/// More Info +/// https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/WorkingWithItems.html +/// https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_AttributeValue.html +pub fn attributes_into_json(attrs: &HashMap) -> Result { + let mut result = Map::new(); + for (k, v) in attrs.iter() { + let new_val = to_json_value(v)?; + result.insert(k.to_string(), new_val); + } + Ok(Value::Object(result)) +} + +/// Converts JSON Object into DynamoDB Attributes +/// Convience method so the caller of the plaid DynamoDB API can submit items in simple +/// JSON format and we will automatically convert into DynamoDB attributes which +/// is the internal representation of the Item data. +/// More Info +/// https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/WorkingWithItems.html +/// https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_AttributeValue.html +pub fn json_into_attributes( + value: Option>, +) -> Result>, ApiError> { + if let Some(expr_vals) = value { + let mut express_attribute_values = HashMap::::new(); + for (key, value) in expr_vals { + let attr_value = to_attribute_value(value)?; + express_attribute_values.insert(key, attr_value); + } + Ok(Some(express_attribute_values)) + } else { + Ok(None) + } +} + +/// converts String into ReturnValue for DynamoDB API +pub fn return_value_from_string(value: Option) -> Result, ApiError> { + value + .as_ref() + .map(|rv| match rv.as_str() { + "ALL_NEW" => Ok(ReturnValue::AllNew), + "ALL_OLD" => Ok(ReturnValue::AllOld), + "UPDATED_NEW" => Ok(ReturnValue::UpdatedNew), + "UPDATED_OLD" => Ok(ReturnValue::UpdatedOld), + "NONE" | "" => Ok(ReturnValue::None), + _ => Err(ApiError::SerdeError(format!( + "Invalid return_values: {}.", + rv + ))), + }) + .transpose() +} + +/// categories of array members +/// used for conversion of json to strongly typed array +enum ArrayMembers { + AllStrings, + AllNumbers, + AllBinary, + NonUniform, +} + +/// helper function for use when converting JSON array to strongly typed array +fn inspect_array_members(arr: &Vec) -> ArrayMembers { + // Check if all elements are strings (for SS) + if arr.iter().all(|v| v.is_string()) { + return ArrayMembers::AllStrings; + } + // Check if all elements are numbers (for NS) + if arr.iter().all(|v| v.is_number()) { + return ArrayMembers::AllNumbers; + } + // Check if all elements are binary (assuming base64-encoded strings for B) + let all_binary = arr.iter().all(|v| { + v.as_str() + .map(|s| base64::decode(s).is_ok()) + .unwrap_or(false) + }); + + if all_binary { + return ArrayMembers::AllBinary; + } + ArrayMembers::NonUniform +} + +/// helper function to convert JSON Value to DynamoDB AttributeValue, supporting all types +fn to_attribute_value(value: Value) -> Result { + match value { + // String: Direct string value + Value::String(s) => Ok(AttributeValue::S(s)), + + // Number: Convert to string for DynamoDB + Value::Number(n) => { + if let Some(i) = n.as_i64() { + Ok(AttributeValue::N(i.to_string())) + } else if let Some(f) = n.as_f64() { + Ok(AttributeValue::N(f.to_string())) + } else { + Err(ApiError::SerdeError(String::from( + "Unsupported number format", + ))) + } + } + + // Boolean: Direct boolean value + Value::Bool(b) => Ok(AttributeValue::Bool(b)), + + // Null: Direct null value + Value::Null => Ok(AttributeValue::Null(true)), + + // Array: Handle lists and sets + Value::Array(arr) => { + if arr.is_empty() { + return Err(ApiError::SerdeError(String::from( + "Lists and sets cannot be empty", + ))); + } + match inspect_array_members(&arr) { + ArrayMembers::AllStrings => { + // String Set (SS) + let strings: Vec = arr + .into_iter() + .map(|v| v.as_str().unwrap().to_string()) + .collect(); + Ok(AttributeValue::Ss(strings)) + } + ArrayMembers::AllNumbers => { + // Number Set (NS) + let numbers: Result, ApiError> = arr + .into_iter() + .map(|v| { + if let Some(i) = v.as_i64() { + Ok(i.to_string()) + } else if let Some(f) = v.as_f64() { + Ok(f.to_string()) + } else { + // Err("Invalid number in number set".to_string()) + Err(ApiError::SerdeError(String::from( + "Invalid number in number set", + ))) + } + }) + .collect(); + Ok(AttributeValue::Ns(numbers?)) + } + ArrayMembers::AllBinary => { + // Binary Set (BS) + let binaries: Result, ApiError> = arr + .into_iter() + .map(|v| { + let s = v.as_str().ok_or(ApiError::SerdeError(String::from( + "Invalid binary value", + )))?; + let decoded = base64::decode(s).map_err(|e| { + ApiError::SerdeError(format!("Failed to decode base64: {}", e)) + })?; + Ok(Blob::new(decoded)) + }) + .collect(); + Ok(AttributeValue::Bs(binaries?)) + } + ArrayMembers::NonUniform => { + // List (L) + let items: Result, ApiError> = + arr.into_iter().map(to_attribute_value).collect(); + Ok(AttributeValue::L(items?)) + } + } + } + + // Object: Handle maps and binary values + Value::Object(obj) => { + // Check if the object represents a binary value (e.g., {"_binary": "base64string"}) + if obj.len() == 1 && obj.contains_key("_binary") { + if let Some(Value::String(base64_str)) = obj.get("_binary") { + let decoded = base64::decode(base64_str).map_err(|e| { + ApiError::SerdeError(format!("Failed to decode base64: {}", e)) + })?; + return Ok(AttributeValue::B(Blob::new(decoded))); + } else { + return Err(ApiError::SerdeError(String::from( + "_binary must be a base64-encoded string", + ))); + } + } + + // Otherwise, treat as a map (M) + let mut map = HashMap::new(); + for (k, v) in obj { + map.insert(k, to_attribute_value(v)?); + } + Ok(AttributeValue::M(map)) + } + } +} + +/// helper function to convert DynamoDB AttributeValue to JSON Value +fn to_json_value(attr_value: &AttributeValue) -> Result { + match attr_value { + AttributeValue::S(s) => Ok(Value::String(s.clone())), + AttributeValue::N(n) => { + // Try parsing as integer first, then float + if let Ok(int_val) = n.parse::() { + Ok(json!(int_val)) + } else if let Ok(float_val) = n.parse::() { + Ok(json!(float_val)) + } else { + Err(ApiError::SerdeError(format!( + "Invalid number format: {}", + n + ))) + } + } + AttributeValue::B(blob) => { + let base64_str = base64::encode(blob.as_ref()); + Ok(json!({ "_binary": base64_str })) + } + AttributeValue::Bool(b) => Ok(Value::Bool(*b)), + AttributeValue::Null(_) => Ok(Value::Null), + AttributeValue::L(list) => { + let json_list: Result, ApiError> = list.iter().map(to_json_value).collect(); + Ok(Value::Array(json_list?)) + } + AttributeValue::M(map) => { + let mut json_map = serde_json::Map::new(); + for (key, value) in map { + let json_value = to_json_value(value)?; + json_map.insert(key.clone(), json_value); + } + Ok(Value::Object(json_map)) + } + AttributeValue::Ss(strings) => { + if strings.is_empty() { + return Err(ApiError::SerdeError( + "String set cannot be empty".to_string(), + )); + } + Ok(Value::Array( + strings.iter().map(|s| Value::String(s.clone())).collect(), + )) + } + AttributeValue::Ns(numbers) => { + if numbers.is_empty() { + return Err(ApiError::SerdeError( + "Number set cannot be empty".to_string(), + )); + } + let json_numbers: Result, ApiError> = numbers + .iter() + .map(|n| { + if let Ok(int_val) = n.parse::() { + Ok(json!(int_val)) + } else if let Ok(float_val) = n.parse::() { + Ok(json!(float_val)) + } else { + Err(ApiError::SerdeError(format!( + "Invalid number in number set: {}", + n + ))) + } + }) + .collect(); + Ok(Value::Array(json_numbers?)) + } + AttributeValue::Bs(blobs) => { + if blobs.is_empty() { + return Err(ApiError::SerdeError( + "Binary set cannot be empty".to_string(), + )); + } + let json_binaries: Vec = blobs + .iter() + .map(|blob| Value::String(base64::encode(blob.as_ref()))) + .collect(); + Ok(Value::Array(json_binaries)) + } + // Handle any unexpected variants + _ => Err(ApiError::SerdeError(format!( + "Unsupported AttributeValue variant: {:?}", + attr_value + ))), + } +} diff --git a/runtime/plaid/src/apis/aws/mod.rs b/runtime/plaid/src/apis/aws/mod.rs index ba313526..fd31647b 100644 --- a/runtime/plaid/src/apis/aws/mod.rs +++ b/runtime/plaid/src/apis/aws/mod.rs @@ -4,6 +4,7 @@ use s3::{S3Config, S3}; use serde::Deserialize; pub mod dynamodb; +pub mod dynamodb_utils; pub mod kms; pub mod s3;