diff --git a/DynamoDBGenerator.sln b/DynamoDBGenerator.sln index eb6a406f..563f44c6 100644 --- a/DynamoDBGenerator.sln +++ b/DynamoDBGenerator.sln @@ -2,7 +2,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DynamoDBGenerator.SourceGenerator", "src\DynamoDBGenerator.SourceGenerator\DynamoDBGenerator.SourceGenerator.csproj", "{648B1DF4-9684-4422-95F5-74BB89862E4D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApp", "samples\ConsoleApp\ConsoleApp.csproj", "{834A7F6C-3C82-427F-9BFB-9686672A5BDE}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DynatelloRepository", "samples\DynatelloRepository\DynatelloRepository.csproj", "{834A7F6C-3C82-427F-9BFB-9686672A5BDE}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DynamoDBGenerator", "src\DynamoDBGenerator\DynamoDBGenerator.csproj", "{53F899A8-28AA-450F-9C62-FD478119B2B7}" EndProject @@ -16,6 +16,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{CF34 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{11F1D954-39EC-4EDD-9460-04FCC216E97A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dynatello.Tests", "tests\Dynatello.Tests\Dynatello.Tests.csproj", "{D9C9C74E-B52C-4510-9258-BA78532EAABB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dynatello", "src\Dynatello\Dynatello.csproj", "{E4D47C8F-A0C8-4368-BBE0-DC6045B2D734}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -42,6 +46,14 @@ Global {A80AC940-3BD8-4377-BDB9-AE82FD4DF944}.Debug|Any CPU.Build.0 = Debug|Any CPU {A80AC940-3BD8-4377-BDB9-AE82FD4DF944}.Release|Any CPU.ActiveCfg = Release|Any CPU {A80AC940-3BD8-4377-BDB9-AE82FD4DF944}.Release|Any CPU.Build.0 = Release|Any CPU + {D9C9C74E-B52C-4510-9258-BA78532EAABB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D9C9C74E-B52C-4510-9258-BA78532EAABB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D9C9C74E-B52C-4510-9258-BA78532EAABB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D9C9C74E-B52C-4510-9258-BA78532EAABB}.Release|Any CPU.Build.0 = Release|Any CPU + {E4D47C8F-A0C8-4368-BBE0-DC6045B2D734}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E4D47C8F-A0C8-4368-BBE0-DC6045B2D734}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E4D47C8F-A0C8-4368-BBE0-DC6045B2D734}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E4D47C8F-A0C8-4368-BBE0-DC6045B2D734}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {D8EABA41-D014-49BD-B109-54829DB835E7} = {E84C4630-5241-4FAA-8F86-964AB25A2C6F} @@ -49,5 +61,7 @@ Global {834A7F6C-3C82-427F-9BFB-9686672A5BDE} = {CF34190F-AC01-42FC-B28A-DD820442243A} {53F899A8-28AA-450F-9C62-FD478119B2B7} = {11F1D954-39EC-4EDD-9460-04FCC216E97A} {648B1DF4-9684-4422-95F5-74BB89862E4D} = {11F1D954-39EC-4EDD-9460-04FCC216E97A} + {D9C9C74E-B52C-4510-9258-BA78532EAABB} = {E84C4630-5241-4FAA-8F86-964AB25A2C6F} + {E4D47C8F-A0C8-4368-BBE0-DC6045B2D734} = {11F1D954-39EC-4EDD-9460-04FCC216E97A} EndGlobalSection EndGlobal diff --git a/README.md b/README.md index 26de8862..cb7fe38f 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,15 @@ This source generator is crafted to simplify DynamoDB integration for your proje This project has not been tested in any real scenario and currently serves as a hobby project. ## Installation -Install the following packages from Nuget: +Theres two way to install this project either by the using `Dynatello` or by `DynamoDBGenerator.SourceGenerator` & `DynamoDBGenerator`. + +* `DynamoDBGenerator.SourceGenerator` & `DynamoDBGenerator` is the base functionality where you get reusable marshaller that's source generated. +* Dyntello extends the marshaller in order to create reusable generic request builders. See this [example](https://github.com/inputfalken/DynamoDB.SourceGenerator/blob/main/samples/DynatelloRepository/Program.cs). + * NOTE: If you install Dynatello, you do not need to specify `DynamoDBGenerator.SourceGenerator` & `DynamoDBGenerator` as dependencies. + +[![DynamoDBGenerator][5]][6] + +--- [![DynamoDBGenerator][1]][2] @@ -16,6 +24,8 @@ Install the following packages from Nuget: [2]: https://www.nuget.org/packages/DynamoDBGenerator [3]: https://img.shields.io/nuget/v/DynamoDBGenerator.SourceGenerator.svg?label=DynamoDBGenerator.SourceGenerator [4]: https://www.nuget.org/packages/DynamoDBGenerator.SourceGenerator +[5]: https://img.shields.io/nuget/v/Dynatello.svg?label=Dynatello +[6]: https://www.nuget.org/packages/Dynatello The `DynamoDBGenerator.SourceGenerator` is where the source generator is implemented. The source generator will look for attributes and implement interfaces that exists in `DynamoDBGenerator`. diff --git a/samples/ConsoleApp/Program.cs b/samples/ConsoleApp/Program.cs deleted file mode 100644 index 5ad2c947..00000000 --- a/samples/ConsoleApp/Program.cs +++ /dev/null @@ -1,11 +0,0 @@ -using DynamoDBGenerator.Attributes; - -namespace SampleApp; - -internal static class Program -{ - public static void Main() - { - } - -} \ No newline at end of file diff --git a/samples/ConsoleApp/ConsoleApp.csproj b/samples/DynatelloRepository/DynatelloRepository.csproj similarity index 54% rename from samples/ConsoleApp/ConsoleApp.csproj rename to samples/DynatelloRepository/DynatelloRepository.csproj index d7a49aaa..ed9cca51 100644 --- a/samples/ConsoleApp/ConsoleApp.csproj +++ b/samples/DynatelloRepository/DynatelloRepository.csproj @@ -9,8 +9,7 @@ - - + diff --git a/samples/DynatelloRepository/Program.cs b/samples/DynatelloRepository/Program.cs new file mode 100644 index 00000000..4644d670 --- /dev/null +++ b/samples/DynatelloRepository/Program.cs @@ -0,0 +1,114 @@ +using System.Net; +using Amazon.DynamoDBv2; +using Amazon.DynamoDBv2.DataModel; +using Amazon.DynamoDBv2.Model; +using DynamoDBGenerator.Attributes; +using Dynatello; +using Dynatello.Builders; +using Dynatello.Builders.Types; + +ProductRepository productRepository = new ProductRepository("MY_TABLE", new AmazonDynamoDBClient()); + +public class ProductRepository +{ + private readonly IAmazonDynamoDB _amazonDynamoDb; + private readonly GetRequestBuilder _getProductByTable; + private readonly UpdateRequestBuilder<(string Id, decimal NewPrice, DateTime TimeStamp)> _updatePrice; + private readonly PutRequestBuilder _createProduct; + private readonly QueryRequestBuilder _queryByPrice; + + public ProductRepository(string tableName, IAmazonDynamoDB amazonDynamoDb) + { + _amazonDynamoDb = amazonDynamoDb; + + _getProductByTable = Product.GetById + .OnTable(tableName) + .ToGetRequestBuilder(arg => arg); // Since the ArgumentType is set to string, we don't need to select a property. + + _updatePrice = Product.UpdatePrice + .OnTable(tableName) + .WithUpdateExpression((db, arg) => $"SET {db.Price} = {arg.NewPrice}, {db.Metadata.ModifiedAt} = {arg.TimeStamp}") // Specify the update operation + .ToUpdateItemRequestBuilder((marshaller, arg) => marshaller.PartitionKey(arg.Id)); + + _createProduct = Product.Put + .OnTable(tableName) + .WithConditionExpression((db, arg) => $"{db.Id} <> {arg.Id}") // Ensure we don't have an existing Product in DynamoDB + .ToPutRequestBuilder(); + + _queryByPrice = Product.QueryByPrice + .OnTable(tableName) + .WithKeyConditionExpression((db, arg) => $"{db.Price} = {arg}") + .ToQueryRequestBuilder() + with + { + IndexName = Product.PriceIndex + }; + } + + public async Task> SearchByPrice(decimal price) + { + QueryRequest request = _queryByPrice.Build(price); + QueryResponse? response = await _amazonDynamoDb.QueryAsync(request); + + if (response.HttpStatusCode is not HttpStatusCode.OK) + throw new Exception("..."); + + return response.Items + .Select(x => Product.QueryByPrice.Unmarshall(x)) + .ToArray(); + } + + public async Task Create(Product product) + { + PutItemRequest request = _createProduct.Build(product); + PutItemResponse response = await _amazonDynamoDb.PutItemAsync(request); + + if (response.HttpStatusCode is not HttpStatusCode.OK) + throw new Exception("..."); + } + + public async Task GetById(string id) + { + GetItemRequest request = _getProductByTable.Build(id); + GetItemResponse response = await _amazonDynamoDb.GetItemAsync(request); + + if (response.HttpStatusCode is HttpStatusCode.NotFound) + return null; + + if (response.HttpStatusCode is not HttpStatusCode.OK) + throw new Exception("..."); + + Product product = Product.GetById.Unmarshall(response.Item); + + return product; + } + + public async Task UpdatePrice(string id, decimal price) + { + UpdateItemRequest request = _updatePrice.Build((id, price, DateTime.UtcNow)); + UpdateItemResponse response = await _amazonDynamoDb.UpdateItemAsync(request); + + if (response.HttpStatusCode is not HttpStatusCode.OK) + return null; + + Product product = Product.UpdatePrice.Unmarshall(response.Attributes); + + return product; + } +} + +[DynamoDBMarshaller(typeof(Product), PropertyName = "Put")] +[DynamoDBMarshaller(typeof(Product), ArgumentType = typeof(string), PropertyName = "GetById")] +[DynamoDBMarshaller(typeof(Product), ArgumentType = typeof((string Id, decimal NewPrice, DateTime TimeStamp)), PropertyName = "UpdatePrice")] +[DynamoDBMarshaller(typeof(Product), ArgumentType = typeof(decimal), PropertyName = "QueryByPrice")] +public partial record Product( + [property: DynamoDBHashKey, DynamoDBGlobalSecondaryIndexRangeKey(Product.PriceIndex)] string Id, + [property: DynamoDBGlobalSecondaryIndexHashKey(Product.PriceIndex)] decimal Price, + string Description, + Product.MetadataEntity Metadata +) +{ + public const string PriceIndex = "PriceIndex"; + + public record MetadataEntity(DateTime CreatedAt, DateTime ModifiedAt); +} diff --git a/src/DynamoDBGenerator/Extensions/DynamoDBMarshallerExtensions.cs b/src/DynamoDBGenerator/Extensions/DynamoDBMarshallerExtensions.cs index 2a46234f..dd27ea6d 100644 --- a/src/DynamoDBGenerator/Extensions/DynamoDBMarshallerExtensions.cs +++ b/src/DynamoDBGenerator/Extensions/DynamoDBMarshallerExtensions.cs @@ -1,9 +1,7 @@ using System; using System.Collections.Generic; -using System.Linq; -using Amazon.DynamoDBv2; -using Amazon.DynamoDBv2.Model; using DynamoDBGenerator.Internal; + namespace DynamoDBGenerator.Extensions; /// @@ -74,86 +72,4 @@ params Func[] expressionBuilders expressionBuilders ); } - - /// - /// Converts the into an . - /// - public static IDynamoDBClient ToDynamoDBClient( - this IDynamoDBMarshaller item, - string tableName, - IAmazonDynamoDB dynamoDB - ) - where TReferences : IAttributeExpressionNameTracker - where TArgumentReferences : IAttributeExpressionValueTracker - { - return new DynamoDBClient(item, tableName, dynamoDB); - } - - /// - /// Creates a . - /// - public static PutItemRequest ToPutItemRequest( - this IDynamoDBMarshaller item, - T entity, - ReturnValue returnValue, - string tableName - ) - where TReferences : IAttributeExpressionNameTracker - where TArgumentReferences : IAttributeExpressionValueTracker - where T : TArg - { - return item.ToPutItemRequestInternal(entity, entity, null, returnValue, tableName); - } - - /// - /// Creates a with condition expression. - /// - public static PutItemRequest ToPutItemRequest( - this IDynamoDBMarshaller item, - T entity, - Func conditionExpressionBuilder, - ReturnValue returnValue, - string tableName - ) - where TReferences : IAttributeExpressionNameTracker - where TArgumentReferences : IAttributeExpressionValueTracker - where T : TArg - { - return item.ToPutItemRequestInternal(entity, entity, conditionExpressionBuilder, returnValue, tableName); - } - - /// - /// Creates a . - /// - public static UpdateItemRequest ToUpdateItemRequest( - this IDynamoDBMarshaller item, - TArg argument, - Func> keySelector, - Func updateExpressionBuilder, - ReturnValue returnValue, - string tableName - ) - where TReferences : IAttributeExpressionNameTracker - where TArgumentReferences : IAttributeExpressionValueTracker - { - return item.ToUpdateItemRequestInternal(argument, keySelector, updateExpressionBuilder, null, returnValue, tableName); - } - - /// - /// Creates a with a condition expression. - /// - public static UpdateItemRequest ToUpdateItemRequest( - this IDynamoDBMarshaller item, - TArg argument, - Func> keySelector, - Func updateExpressionBuilder, - Func conditionExpressionBuilder, - ReturnValue returnValue, - string tableName - ) - where TReferences : IAttributeExpressionNameTracker - where TArgumentReferences : IAttributeExpressionValueTracker - { - return item.ToUpdateItemRequestInternal(argument, keySelector, updateExpressionBuilder, conditionExpressionBuilder, returnValue, tableName); - } } \ No newline at end of file diff --git a/src/DynamoDBGenerator/IDynamoDBClient.cs b/src/DynamoDBGenerator/IDynamoDBClient.cs deleted file mode 100644 index d72b8530..00000000 --- a/src/DynamoDBGenerator/IDynamoDBClient.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Amazon.DynamoDBv2.Model; -namespace DynamoDBGenerator; - -/// -/// Represents a client with asynchronous methods for sending requests to DynamoDB. -/// -/// The type of entity associated with DynamoDB operations. -/// The type of argument used in DynamoDB operations. -/// The type for tracking attribute names related to . -/// The type for tracking argument attribute values related to . -public interface IDynamoDBClient - where TReferences : IAttributeExpressionNameTracker - where TArgumentReferences : IAttributeExpressionValueTracker -{ - /// - /// Saves an entity to DynamoDB with an optional condition expression builder. - /// - /// The type of entity to save. - /// The entity to be saved. - /// A function to build a condition expression. - /// A token to cancel the operation. - /// A Task representing the asynchronous operation. - Task Save( - T entity, - Func conditionExpressionBuilder, - CancellationToken cancellationToken = default - ) where T : TEntity, TArgument; - - /// - /// Saves an entity to DynamoDB without a condition expression. - /// - /// The type of entity to save. - /// The entity to be saved. - /// A token to cancel the operation. - /// A Task representing the asynchronous operation. - Task Save( - T entity, - CancellationToken cancellationToken = default - ) where T : TEntity, TArgument; - - /// - /// Updates an entity in DynamoDB. - /// - /// The entity to update. - /// A function to select the keys for the update operation. - /// A function to build the update expression. - /// A token to cancel the operation. - /// A Task representing the asynchronous operation. - Task Update( - TArgument entity, - Func> keySelector, - Func updateExpressionBuilder, - CancellationToken cancellationToken = default - ); - - /// - /// Updates an entity in DynamoDB with optional condition and update expression builders. - /// - /// The entity to update. - /// A function to select the keys for the update operation. - /// A function to build the update expression. - /// A function to build the condition expression. - /// A token to cancel the operation. - /// A Task representing the asynchronous operation. - Task Update( - TArgument entity, - Func> keySelector, - Func updateExpressionBuilder, - Func conditionExpressionBuilder, - CancellationToken cancellationToken = default - ); - - /// - /// Updates an entity in DynamoDB and returns the updated entity. - /// - /// The entity to update. - /// A function to select the keys for the update operation. - /// A function to build the update expression. - /// A function to build the condition expression. - /// A token to cancel the operation. - /// The updated entity. - Task UpdateReturned( - TArgument entity, - Func> keySelector, - Func updateExpressionBuilder, - Func conditionExpressionBuilder, - CancellationToken cancellationToken = default - ); -} \ No newline at end of file diff --git a/src/DynamoDBGenerator/Internal/DynamoDBClient.cs b/src/DynamoDBGenerator/Internal/DynamoDBClient.cs deleted file mode 100644 index 66ebb591..00000000 --- a/src/DynamoDBGenerator/Internal/DynamoDBClient.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Amazon.DynamoDBv2; -using Amazon.DynamoDBv2.Model; -namespace DynamoDBGenerator.Internal; - -internal class DynamoDBClient : IDynamoDBClient - where TReferences : IAttributeExpressionNameTracker - where TArgumentReferences : IAttributeExpressionValueTracker -{ - private readonly IAmazonDynamoDB _amazonDynamoDB; - private readonly IDynamoDBMarshaller _marshaller; - private readonly string _tableName; - - public DynamoDBClient(IDynamoDBMarshaller marshaller, string tableName, IAmazonDynamoDB amazonDynamoDB) - { - _marshaller = marshaller; - _tableName = tableName; - _amazonDynamoDB = amazonDynamoDB; - } - - public Task Save(T1 entity, Func conditionExpressionBuilder, CancellationToken cancellationToken = default) where T1 : T, TArg - { - var putRequest = _marshaller.ToPutItemRequestInternal(entity, entity, conditionExpressionBuilder, ReturnValue.NONE, _tableName); - return _amazonDynamoDB.PutItemAsync(putRequest, cancellationToken); - } - - public Task Save(T1 entity, CancellationToken cancellationToken = default) where T1 : T, TArg - { - var putRequest = _marshaller.ToPutItemRequestInternal(entity, entity, null, ReturnValue.NONE, _tableName); - return _amazonDynamoDB.PutItemAsync(putRequest, cancellationToken); - } - - public Task Update( - TArg entity, - Func> keySelector, - Func updateExpressionBuilder, - CancellationToken cancellationToken = default - ) - { - var updateItemRequest = _marshaller.ToUpdateItemRequestInternal(entity, keySelector, updateExpressionBuilder, null, ReturnValue.NONE, _tableName); - return _amazonDynamoDB.UpdateItemAsync(updateItemRequest, cancellationToken); - } - - public Task Update( - TArg entity, - Func> keySelector, - Func updateExpressionBuilder, - Func conditionExpressionBuilder, - CancellationToken cancellationToken = default - ) - { - var updateItemRequest = _marshaller.ToUpdateItemRequestInternal(entity, keySelector, updateExpressionBuilder, conditionExpressionBuilder, ReturnValue.NONE, _tableName); - return _amazonDynamoDB.UpdateItemAsync(updateItemRequest, cancellationToken); - } - - public async Task UpdateReturned(TArg entity, Func> keySelector, Func updateExpressionBuilder, - Func conditionExpressionBuilder, CancellationToken cancellationToken = default) - { - var updateItemRequest = _marshaller.ToUpdateItemRequestInternal(entity, keySelector, updateExpressionBuilder, conditionExpressionBuilder, ReturnValue.ALL_NEW, _tableName); - var result = await _amazonDynamoDB.UpdateItemAsync(updateItemRequest, cancellationToken); - - return _marshaller.Unmarshall(result.Attributes); - } -} \ No newline at end of file diff --git a/src/DynamoDBGenerator/Internal/ExceptionHelper.cs b/src/DynamoDBGenerator/Internal/ExceptionHelper.cs index 08bb9055..0845f7fe 100644 --- a/src/DynamoDBGenerator/Internal/ExceptionHelper.cs +++ b/src/DynamoDBGenerator/Internal/ExceptionHelper.cs @@ -23,7 +23,7 @@ public static DynamoDBMarshallingException KeysArgumentNotNull(string memberName public static DynamoDBMarshallingException KeysInvalidConversion(string memberName, string argumentName, object value, string expectedType) { - return new DynamoDBMarshallingException(memberName, $"Value '{{{value}}}' from argument '{{nameof({argumentName})}}' is not convertable to '{expectedType}'."); + return new DynamoDBMarshallingException(memberName, $"Value '{{{value}}}' from argument '{argumentName}' is not convertable to '{expectedType}'."); } public static InvalidOperationException KeysValueWithNoCorrespondence(string argumentName, object value) diff --git a/src/DynamoDBGenerator/Internal/RequestFactory.cs b/src/DynamoDBGenerator/Internal/RequestFactory.cs deleted file mode 100644 index 32eb2ef7..00000000 --- a/src/DynamoDBGenerator/Internal/RequestFactory.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Amazon.DynamoDBv2; -using Amazon.DynamoDBv2.Model; -namespace DynamoDBGenerator.Internal; - -internal static class RequestFactory -{ - internal static PutItemRequest ToPutItemRequestInternal( - this IDynamoDBMarshaller item, - T entity, - TArg argument, - Func? conditionExpressionBuilder, - ReturnValue returnValue, - string tableName - ) - where TReferences : IAttributeExpressionNameTracker - where TArgumentReferences : IAttributeExpressionValueTracker - { - - Dictionary? expressionAttributeValues = null; - Dictionary? expressionAttributeNames = null; - string? conditionExpression = null; - - if (conditionExpressionBuilder is not null) - { - var nameTracker = item.AttributeExpressionNameTracker(); - var valueTracker = item.AttributeExpressionValueTracker(); - conditionExpression = conditionExpressionBuilder.Invoke(nameTracker, valueTracker); - expressionAttributeNames = nameTracker.AccessedNames().ToDictionary(x => x.Key, x => x.Value); - expressionAttributeValues = valueTracker.AccessedValues(argument).ToDictionary(x => x.Key, x => x.Value); - } - - return new PutItemRequest - { - TableName = tableName, - ExpressionAttributeNames = expressionAttributeNames, - ExpressionAttributeValues = expressionAttributeValues, - ConditionExpression = conditionExpression, - Item = item.Marshall(entity), - ReturnValues = returnValue - }; - } - internal static UpdateItemRequest ToUpdateItemRequestInternal( - this IDynamoDBMarshaller item, - TArg argument, - Func> keySelector, - Func updateExpressionBuilder, - Func? conditionExpressionBuilder, - ReturnValue returnValue, - string tableName - ) - where TReferences : IAttributeExpressionNameTracker - where TArgumentReferences : IAttributeExpressionValueTracker - { - - var nameTracker = item.AttributeExpressionNameTracker(); - var argumentTracker = item.AttributeExpressionValueTracker(); - var updateExpression = updateExpressionBuilder(nameTracker, argumentTracker); - var conditionExpression = conditionExpressionBuilder?.Invoke(nameTracker, argumentTracker); - - return new UpdateItemRequest - { - Key = keySelector(item.PrimaryKeyMarshaller, argument), - TableName = tableName, - ExpressionAttributeNames = nameTracker.AccessedNames().ToDictionary(x => x.Key, x => x.Value), - ExpressionAttributeValues = argumentTracker.AccessedValues(argument).ToDictionary(x => x.Key, x => x.Value), - ConditionExpression = conditionExpression, - UpdateExpression = updateExpression, - ReturnValues = returnValue - }; - } -} \ No newline at end of file diff --git a/src/Dynatello/Builders/Extensions.cs b/src/Dynatello/Builders/Extensions.cs new file mode 100644 index 00000000..c296fc54 --- /dev/null +++ b/src/Dynatello/Builders/Extensions.cs @@ -0,0 +1,145 @@ +using Amazon.DynamoDBv2.Model; +using DynamoDBGenerator; +using Dynatello.Builders.Types; + +namespace Dynatello.Builders; + +public static class Extensions +{ + public static QueryRequestBuilder ToQueryRequestBuilder( + this KeyConditionedFilterExpression source + ) + where TReferences : IAttributeExpressionNameTracker + where TArgumentReferences : IAttributeExpressionValueTracker + { + return new QueryRequestBuilder( + source.TableAccess.Item.ComposeAttributeExpression(source.Condition, source.Filter), + source.TableAccess.TableName + ); + } + + public static QueryRequestBuilder ToQueryRequestBuilder( + this KeyConditionExpression source + ) + where TReferences : IAttributeExpressionNameTracker + where TArgumentReferences : IAttributeExpressionValueTracker + { + return new QueryRequestBuilder( + source.TableAccess.Item.ComposeAttributeExpression(source.Condition, null), + source.TableAccess.TableName + ); + } + + public static GetRequestBuilder ToGetRequestBuilder( + this TableAccess source) + where TReferences : IAttributeExpressionNameTracker + where TArgumentReferences : IAttributeExpressionValueTracker + where TArg : notnull + { + return new GetRequestBuilder( + source.TableName, + source.Item.PrimaryKeyMarshaller.ComposeKeys(y => y, null) + ); + } + + public static GetRequestBuilder ToGetRequestBuilder( + this TableAccess source, + Func partitionKeySelector) + where TReferences : IAttributeExpressionNameTracker + where TArgumentReferences : IAttributeExpressionValueTracker + where TPartition : notnull + { + return new GetRequestBuilder( + source.TableName, + source.Item.PrimaryKeyMarshaller.ComposeKeys(y => partitionKeySelector(y), null) + ); + } + + public static GetRequestBuilder ToGetRequestBuilder( + this TableAccess source, + Func partitionKeySelector, + Func rangeKeySelector) + where TReferences : IAttributeExpressionNameTracker + where TArgumentReferences : IAttributeExpressionValueTracker + where TPartition : notnull + where TRange : notnull + { + return new GetRequestBuilder( + source.TableName, + source.Item.PrimaryKeyMarshaller.ComposeKeys(y => partitionKeySelector(y), y => rangeKeySelector(y)) + ); + } + + + public static UpdateRequestBuilder ToUpdateItemRequestBuilder( + this UpdateExpression source, + Func> keySelector + ) + where TReferences : IAttributeExpressionNameTracker + where TArgumentReferences : IAttributeExpressionValueTracker + { + return new UpdateRequestBuilder( + source.TableAccess.Item.ComposeAttributeExpression(source.Update, null), + source.TableAccess.TableName, + keySelector, + source.TableAccess.Item.PrimaryKeyMarshaller + ); + } + + public static UpdateRequestBuilder ToUpdateItemRequestBuilder( + this ConditionalUpdateExpression source, + Func> keySelector + ) + where TReferences : IAttributeExpressionNameTracker + where TArgumentReferences : IAttributeExpressionValueTracker + { + return new UpdateRequestBuilder( + source.TableAccess.Item.ComposeAttributeExpression(source.Update, source.Condition), + source.TableAccess.TableName, + keySelector, + source.TableAccess.Item.PrimaryKeyMarshaller + ); + } + + public static KeyConditionExpression WithKeyConditionExpression( + this TableAccess source, + Func condition) + where TReferences : IAttributeExpressionNameTracker + where TArgumentReferences : IAttributeExpressionValueTracker + { + return new KeyConditionExpression(source, condition); + } + + public static PutRequestBuilder ToPutRequestBuilder( + this TableAccess source + ) + where TReferences : IAttributeExpressionNameTracker + where TArgumentReferences : IAttributeExpressionValueTracker + { + return new PutRequestBuilder + ( + null, + source.Item.Marshall, + source.TableName + ); + } + + public static PutRequestBuilder ToPutRequestBuilder( + this ConditionExpression source + ) + where TReferences : IAttributeExpressionNameTracker + where TArgumentReferences : IAttributeExpressionValueTracker + { + return new PutRequestBuilder + ( + source.TableAccess.Item.ComposeAttributeExpression(null, source.Condition), + source.TableAccess.Item.Marshall, + source.TableAccess.TableName + ); + } +} \ No newline at end of file diff --git a/src/Dynatello/Builders/GetRequestBuilder.cs b/src/Dynatello/Builders/GetRequestBuilder.cs new file mode 100644 index 00000000..311f58e1 --- /dev/null +++ b/src/Dynatello/Builders/GetRequestBuilder.cs @@ -0,0 +1,50 @@ +using Amazon.DynamoDBv2; +using Amazon.DynamoDBv2.Model; + +namespace Dynatello.Builders; + +public readonly record struct GetRequestBuilder +{ + private readonly Func> _keysSelector; + + internal GetRequestBuilder( + string tableName, + Func> keysSelector) + { + _keysSelector = keysSelector; + TableName = tableName; + } + + [Obsolete(Constants.ObsoleteConstructorMessage, true)] + public GetRequestBuilder() + { + throw Constants.InvalidConstructor(); + } + + /// + public string TableName { get; init; } + + /// + public bool? ConsistentRead { get; init; } = null; + + /// + public ReturnConsumedCapacity? ReturnConsumedCapacity { get; init; } = null; + + public GetItemRequest Build(T arg) + { + var request = new GetItemRequest + { + ReturnConsumedCapacity = ReturnConsumedCapacity, + TableName = TableName, + Key = _keysSelector(arg) + }; + + if (ConsistentRead is { } consistentRead) + request.ConsistentRead = consistentRead; + + if (ReturnConsumedCapacity is not null) + request.ReturnConsumedCapacity = ReturnConsumedCapacity; + + return request; + } +} \ No newline at end of file diff --git a/src/Dynatello/Builders/PutRequestBuilder.cs b/src/Dynatello/Builders/PutRequestBuilder.cs new file mode 100644 index 00000000..cf7b718b --- /dev/null +++ b/src/Dynatello/Builders/PutRequestBuilder.cs @@ -0,0 +1,86 @@ +using Amazon.DynamoDBv2; +using Amazon.DynamoDBv2.Model; +using DynamoDBGenerator; + +namespace Dynatello.Builders; + +/// +/// A record based builder for creating that can be configured via the `with` syntax. +/// +public readonly record struct PutRequestBuilder +{ + private readonly Func? _attributeExpressionSelector; + private readonly Func> _marshall; + + private readonly string _tableName; + + + internal PutRequestBuilder( + Func? attributeExpressionSelector, + Func> marshall, + string tableName + ) + { + _attributeExpressionSelector = attributeExpressionSelector; + _marshall = marshall; + _tableName = tableName; + } + + [Obsolete(Constants.ObsoleteConstructorMessage, true)] + public PutRequestBuilder() + { + throw Constants.InvalidConstructor(); + } + + /// + public string TableName + { + get => _tableName; + init => _tableName = value ?? throw new ArgumentNullException(nameof(value)); + } + + /// + public ReturnValue? ReturnValues { get; init; } = null; + + /// + public ReturnConsumedCapacity? ReturnConsumedCapacity { get; init; } = null; + + /// + public ReturnItemCollectionMetrics? ReturnItemCollectionMetrics { get; init; } = null; + + /// + public ReturnValuesOnConditionCheckFailure? ReturnValuesOnConditionCheckFailure { get; init; } = null; + + + public PutItemRequest Build(T element) + { + var request = new PutItemRequest + { + TableName = TableName, + Item = _marshall(element), + Expected = null, + ConditionalOperator = null, + ConditionExpression = null, + ExpressionAttributeNames = null, + ExpressionAttributeValues = null + }; + + if (ReturnValues is not null) + request.ReturnValues = ReturnValues; + if (ReturnConsumedCapacity is not null) + request.ReturnConsumedCapacity = ReturnConsumedCapacity; + if (ReturnItemCollectionMetrics is not null) + request.ReturnItemCollectionMetrics = ReturnItemCollectionMetrics; + if (ReturnValuesOnConditionCheckFailure is not null) + request.ReturnValuesOnConditionCheckFailure = ReturnValuesOnConditionCheckFailure; + + if (_attributeExpressionSelector is null) return request; + var attributeExpression = _attributeExpressionSelector(element); + + request.ExpressionAttributeNames = attributeExpression.Names; + request.ExpressionAttributeValues = attributeExpression.Values; + request.ConditionExpression = attributeExpression.Expressions[0]; + + return request; + } +} \ No newline at end of file diff --git a/src/Dynatello/Builders/QueryRequestBuilder.cs b/src/Dynatello/Builders/QueryRequestBuilder.cs new file mode 100644 index 00000000..2ecff3d0 --- /dev/null +++ b/src/Dynatello/Builders/QueryRequestBuilder.cs @@ -0,0 +1,86 @@ +using Amazon.DynamoDBv2; +using Amazon.DynamoDBv2.Model; +using DynamoDBGenerator; + +namespace Dynatello.Builders; + +public readonly record struct QueryRequestBuilder +{ + private readonly Func _attributeExpressionSelector; + + internal QueryRequestBuilder(Func attributeExpressionSelector, string tableName) + { + _attributeExpressionSelector = attributeExpressionSelector; + TableName = tableName; + } + + [Obsolete(Constants.ObsoleteConstructorMessage, true)] + public QueryRequestBuilder() + { + throw Constants.InvalidConstructor(); + } + + /// + public string TableName { get; init; } + + /// + public string? IndexName { get; init; } = null; + + /// + public int? Limit { get; init; } = null; + + /// + public bool? ConsistentRead { get; init; } = null; + + /// + public bool? ScanIndexForward { get; init; } = null; + + /// + public Select? Select { get; init; } = null; + + /// + public ReturnConsumedCapacity? ReturnConsumedCapacity { get; init; } = null; + + public QueryRequest Build(T arg) + { + var attributeExpression = _attributeExpressionSelector(arg); + + var queryRequest = new QueryRequest + { + AttributesToGet = null, + QueryFilter = null, + ConditionalOperator = null, + KeyConditions = null, + KeyConditionExpression = attributeExpression.Expressions[0], + ExpressionAttributeValues = attributeExpression.Values, + ExpressionAttributeNames = attributeExpression.Names, + TableName = TableName, + IndexName = null, + ProjectionExpression = null + }; + + + if (ReturnConsumedCapacity is not null) + queryRequest.ReturnConsumedCapacity = ReturnConsumedCapacity; + + if (ConsistentRead is { } consistentRead) + queryRequest.ConsistentRead = consistentRead; + + if (ScanIndexForward is { } scanIndexForward) + queryRequest.ScanIndexForward = scanIndexForward; + + if (Select is not null) + queryRequest.Select = Select; + + if (Limit is { } limit) + queryRequest.Limit = limit; + + if (IndexName is not null) + queryRequest.IndexName = IndexName; + + if (attributeExpression.Expressions.Count == 2) + queryRequest.FilterExpression = attributeExpression.Expressions[1]; + + return queryRequest; + } +} \ No newline at end of file diff --git a/src/Dynatello/Builders/Types/ConditionExpression.cs b/src/Dynatello/Builders/Types/ConditionExpression.cs new file mode 100644 index 00000000..2b856361 --- /dev/null +++ b/src/Dynatello/Builders/Types/ConditionExpression.cs @@ -0,0 +1,26 @@ +using DynamoDBGenerator; + +namespace Dynatello.Builders.Types; + +public readonly record struct ConditionExpression + where TReferences : IAttributeExpressionNameTracker + where TArgumentReferences : IAttributeExpressionValueTracker +{ + internal readonly Func Condition; + internal readonly TableAccess TableAccess; + + [Obsolete(Constants.ObsoleteConstructorMessage, true)] + public ConditionExpression() + { + throw Constants.InvalidConstructor(); + } + + internal ConditionExpression( + in TableAccess tableAccess, + in Func condition + ) + { + TableAccess = tableAccess; + Condition = condition; + } +} \ No newline at end of file diff --git a/src/Dynatello/Builders/Types/ConditionalUpdateExpression.cs b/src/Dynatello/Builders/Types/ConditionalUpdateExpression.cs new file mode 100644 index 00000000..911ad497 --- /dev/null +++ b/src/Dynatello/Builders/Types/ConditionalUpdateExpression.cs @@ -0,0 +1,28 @@ +using DynamoDBGenerator; + +namespace Dynatello.Builders.Types; + +public readonly record struct ConditionalUpdateExpression + where TReferences : IAttributeExpressionNameTracker + where TArgumentReferences : IAttributeExpressionValueTracker +{ + internal readonly Func Condition; + internal readonly TableAccess TableAccess; + internal readonly Func Update; + + [Obsolete(Constants.ObsoleteConstructorMessage, true)] + public ConditionalUpdateExpression() + { + throw Constants.InvalidConstructor(); + } + + internal ConditionalUpdateExpression( + in TableAccess tableAccess, + in Func update, + in Func condition) + { + TableAccess = tableAccess; + Update = update; + Condition = condition; + } +} \ No newline at end of file diff --git a/src/Dynatello/Builders/Types/Extensions.cs b/src/Dynatello/Builders/Types/Extensions.cs new file mode 100644 index 00000000..815183f1 --- /dev/null +++ b/src/Dynatello/Builders/Types/Extensions.cs @@ -0,0 +1,74 @@ +using DynamoDBGenerator; + +namespace Dynatello.Builders.Types; + +public static class Extensions +{ + public static KeyConditionedFilterExpression WithFilterExpression( + this KeyConditionExpression source, + Func filter + ) + where TReferences : IAttributeExpressionNameTracker + where TArgumentReferences : IAttributeExpressionValueTracker + { + return new KeyConditionedFilterExpression( + source.TableAccess, + source.Condition, + filter + ); + } + + public static ConditionalUpdateExpression WithConditionExpression( + this UpdateExpression source, + Func condition) + where TReferences : IAttributeExpressionNameTracker + where TArgumentReferences : IAttributeExpressionValueTracker + { + return new ConditionalUpdateExpression( + in source.TableAccess, + in source.Update, + condition + ); + } + + public static UpdateExpression WithUpdateExpression( + this TableAccess source, + Func updateExpression + ) + where TReferences : IAttributeExpressionNameTracker + where TArgumentReferences : IAttributeExpressionValueTracker + { + return new UpdateExpression(in source, in updateExpression); + } + + public static ConditionExpression WithConditionExpression( + this TableAccess source, + Func condition + ) + where TReferences : IAttributeExpressionNameTracker + where TArgumentReferences : IAttributeExpressionValueTracker + { + return new ConditionExpression(in source, in condition); + } + + public static ConditionalUpdateExpression WithUpdateExpression( + this ConditionExpression source, + Func update + ) + where TReferences : IAttributeExpressionNameTracker + where TArgumentReferences : IAttributeExpressionValueTracker + { + return new ConditionalUpdateExpression( + in source.TableAccess, + in update, + in source.Condition + ); + } +} \ No newline at end of file diff --git a/src/Dynatello/Builders/Types/KeyConditionExpression.cs b/src/Dynatello/Builders/Types/KeyConditionExpression.cs new file mode 100644 index 00000000..e6b61b7b --- /dev/null +++ b/src/Dynatello/Builders/Types/KeyConditionExpression.cs @@ -0,0 +1,26 @@ +using DynamoDBGenerator; + +namespace Dynatello.Builders.Types; + +public readonly struct KeyConditionExpression + where TReferences : IAttributeExpressionNameTracker + where TArgumentReferences : IAttributeExpressionValueTracker +{ + internal readonly Func Condition; + internal readonly TableAccess TableAccess; + + [Obsolete(Constants.ObsoleteConstructorMessage, true)] + public KeyConditionExpression() + { + throw Constants.InvalidConstructor(); + } + + internal KeyConditionExpression( + in TableAccess tableAccess, + in Func condition + ) + { + TableAccess = tableAccess; + Condition = condition; + } +} \ No newline at end of file diff --git a/src/Dynatello/Builders/Types/KeyConditionedFilterExpression.cs b/src/Dynatello/Builders/Types/KeyConditionedFilterExpression.cs new file mode 100644 index 00000000..ec610ac7 --- /dev/null +++ b/src/Dynatello/Builders/Types/KeyConditionedFilterExpression.cs @@ -0,0 +1,29 @@ +using DynamoDBGenerator; + +namespace Dynatello.Builders.Types; + +public readonly struct KeyConditionedFilterExpression + where TReferences : IAttributeExpressionNameTracker + where TArgumentReferences : IAttributeExpressionValueTracker +{ + internal readonly Func Condition; + internal readonly Func Filter; + internal readonly TableAccess TableAccess; + + [Obsolete(Constants.ObsoleteConstructorMessage, true)] + public KeyConditionedFilterExpression() + { + throw Constants.InvalidConstructor(); + } + + internal KeyConditionedFilterExpression( + in TableAccess tableAccess, + in Func condition, + in Func filter + ) + { + TableAccess = tableAccess; + Condition = condition; + Filter = filter; + } +} \ No newline at end of file diff --git a/src/Dynatello/Builders/Types/TableAccess.cs b/src/Dynatello/Builders/Types/TableAccess.cs new file mode 100644 index 00000000..c92f2f88 --- /dev/null +++ b/src/Dynatello/Builders/Types/TableAccess.cs @@ -0,0 +1,23 @@ +using DynamoDBGenerator; + +namespace Dynatello.Builders.Types; + +public readonly record struct TableAccess + where TReferences : IAttributeExpressionNameTracker + where TArgumentReferences : IAttributeExpressionValueTracker +{ + [Obsolete(Constants.ObsoleteConstructorMessage, true)] + public TableAccess() + { + throw Constants.InvalidConstructor(); + } + + internal TableAccess(in string tableName, in IDynamoDBMarshaller item) + { + TableName = tableName ?? throw new ArgumentNullException(nameof(tableName)); + Item = item ?? throw new ArgumentNullException(nameof(item)); + } + + internal string TableName { get; } + internal IDynamoDBMarshaller Item { get; } +} \ No newline at end of file diff --git a/src/Dynatello/Builders/Types/UpdateExpression.cs b/src/Dynatello/Builders/Types/UpdateExpression.cs new file mode 100644 index 00000000..80fd8b4e --- /dev/null +++ b/src/Dynatello/Builders/Types/UpdateExpression.cs @@ -0,0 +1,25 @@ +using DynamoDBGenerator; + +namespace Dynatello.Builders.Types; + +public readonly record struct UpdateExpression + where TReferences : IAttributeExpressionNameTracker + where TArgumentReferences : IAttributeExpressionValueTracker +{ + internal readonly TableAccess TableAccess; + internal readonly Func Update; + + [Obsolete(Constants.ObsoleteConstructorMessage, true)] + public UpdateExpression() + { + throw Constants.InvalidConstructor(); + } + + internal UpdateExpression( + in TableAccess tableAccess, + in Func update) + { + TableAccess = tableAccess; + Update = update; + } +} \ No newline at end of file diff --git a/src/Dynatello/Builders/UpdateRequestBuilder.cs b/src/Dynatello/Builders/UpdateRequestBuilder.cs new file mode 100644 index 00000000..bc643fd8 --- /dev/null +++ b/src/Dynatello/Builders/UpdateRequestBuilder.cs @@ -0,0 +1,100 @@ +using Amazon.DynamoDBv2; +using Amazon.DynamoDBv2.Model; +using DynamoDBGenerator; + +namespace Dynatello.Builders; + +/// +/// A record based builder for creating that can be configured via the `with` syntax. +/// +public readonly record struct UpdateRequestBuilder +{ + private readonly Func _attributeExpressionSelector; + private readonly IDynamoDBKeyMarshaller _keyMarshaller; + private readonly Func> _keySelector; + + private readonly string _tableName; + + [Obsolete(Constants.ObsoleteConstructorMessage, true)] + public UpdateRequestBuilder() + { + throw Constants.InvalidConstructor(); + } + + internal UpdateRequestBuilder( + Func attributeExpressionSelector, + string tableName, + Func> keySelector, + IDynamoDBKeyMarshaller keyMarshaller + ) + { + _attributeExpressionSelector = attributeExpressionSelector; + _tableName = tableName; + _keySelector = keySelector; + _keyMarshaller = keyMarshaller; + } + + /// + public string TableName + { + get => _tableName; + init => _tableName = value ?? throw new ArgumentNullException(nameof(value)); + } + + /// + /// A function to specify how the keys should be accessed through the . + /// + public Func> KeySelector + { + get => _keySelector; + init => _keySelector = value ?? throw new ArgumentNullException(nameof(value)); + } + + /// + public ReturnConsumedCapacity? ReturnConsumedCapacity { get; init; } = null; + + /// + public ReturnItemCollectionMetrics? ReturnItemCollectionMetrics { get; init; } = null; + + /// + public ReturnValue? ReturnValues { get; init; } = null; + + /// + public ReturnValuesOnConditionCheckFailure? ReturnValuesOnConditionCheckFailure { get; init; } = null; + + + /// + /// Will build a with the specified configurations. + /// + public UpdateItemRequest Build(T arg) + { + var expression = _attributeExpressionSelector(arg); + var update = new UpdateItemRequest + { + UpdateExpression = expression.Expressions[0], + ConditionExpression = expression.Expressions.Count is 2 ? expression.Expressions[1] : null, + TableName = TableName, + Key = KeySelector(_keyMarshaller, arg), + ExpressionAttributeNames = expression.Names, + ExpressionAttributeValues = expression.Values, + Expected = null, + AttributeUpdates = null, + ConditionalOperator = null + }; + + if (ReturnValues is not null) + update.ReturnValues = ReturnValues; + + if (ReturnConsumedCapacity is not null) + update.ReturnConsumedCapacity = ReturnConsumedCapacity; + + if (ReturnItemCollectionMetrics is not null) + update.ReturnItemCollectionMetrics = ReturnItemCollectionMetrics; + + if (ReturnValuesOnConditionCheckFailure is not null) + update.ReturnValuesOnConditionCheckFailure = ReturnValuesOnConditionCheckFailure; + + + return update; + } +} \ No newline at end of file diff --git a/src/Dynatello/Constants.cs b/src/Dynatello/Constants.cs new file mode 100644 index 00000000..53d74748 --- /dev/null +++ b/src/Dynatello/Constants.cs @@ -0,0 +1,13 @@ +namespace Dynatello; + +internal static class Constants +{ + internal const string ObsoleteConstructorMessage = "Do not use this constructor!"; + private const string ConstructorException = "This is an invalid constructor access."; + + internal static Exception InvalidConstructor() + { + return new InvalidOperationException(ConstructorException); + } + +} \ No newline at end of file diff --git a/src/Dynatello/DynamoDBMarshallerExtensions.cs b/src/Dynatello/DynamoDBMarshallerExtensions.cs new file mode 100644 index 00000000..16fc73cd --- /dev/null +++ b/src/Dynatello/DynamoDBMarshallerExtensions.cs @@ -0,0 +1,52 @@ +using Amazon.DynamoDBv2.Model; +using DynamoDBGenerator; +using Dynatello.Builders.Types; +using static DynamoDBGenerator.Extensions.DynamoDBMarshallerExtensions; + +namespace Dynatello; + +public static class DynamoDBMarshallerExtensions +{ + public static TableAccess OnTable + + (this IDynamoDBMarshaller item, string tableName) + where TReferences : IAttributeExpressionNameTracker + where TArgumentReferences : IAttributeExpressionValueTracker + { + return new TableAccess(in tableName, in item); + } + + internal static Func> ComposeKeys + ( + this IDynamoDBKeyMarshaller source, + Func partitionKeySelector, + Func? rangeKeySelector + ) + { + return (partitionKeySelector, rangeKeySelector) switch + { + (not null, not null) => y => source.Keys(partitionKeySelector(y), rangeKeySelector(y)), + (not null, null) => y => source.PartitionKey(partitionKeySelector(y)), + (null, not null) => y => source.RangeKey(rangeKeySelector(y)), + (null, null) => throw new ArgumentNullException("") + }; + } + + internal static Func ComposeAttributeExpression( + this IDynamoDBMarshaller source, + Func? update, + Func? condition + ) + where TReferences : IAttributeExpressionNameTracker + where TArgumentReferences : IAttributeExpressionValueTracker + { + return (update, condition) switch + { + (null, null) => throw new ArgumentNullException(""), + (not null, not null) => y => source.ToAttributeExpression(y, update, condition), + (not null, null) => y => source.ToAttributeExpression(y, update), + (null, not null) => y => source.ToAttributeExpression(y, condition) + }; + } +} \ No newline at end of file diff --git a/src/Dynatello/Dynatello.csproj b/src/Dynatello/Dynatello.csproj new file mode 100644 index 00000000..cc917ad9 --- /dev/null +++ b/src/Dynatello/Dynatello.csproj @@ -0,0 +1,26 @@ + + + + net6.0 + enable + enable + false + 0.0.0 + Dynatello + Robert Andersson + Contains functionality to improve the convinience when working the DynamoDB.SourceGenerator. + Robert Anderson + https://github.com/inputfalken/DynamoDB.SourceGenerator + MIT + https://github.com/inputfalken/DynamoDB.SourceGenerator + git + true + true + + + + + + + + diff --git a/tests/DynamoDBGenerator.SourceGenerator.Tests/Extensions/ToPutItemRequestTests.cs b/tests/DynamoDBGenerator.SourceGenerator.Tests/Extensions/ToPutItemRequestTests.cs deleted file mode 100644 index a7c841d7..00000000 --- a/tests/DynamoDBGenerator.SourceGenerator.Tests/Extensions/ToPutItemRequestTests.cs +++ /dev/null @@ -1,89 +0,0 @@ -using Amazon.DynamoDBv2; -using AutoFixture; -using DynamoDBGenerator.Attributes; -using DynamoDBGenerator.Extensions; -namespace DynamoDBGenerator.SourceGenerator.Tests.Extensions; - -[DynamoDBMarshaller(typeof(User))] -public partial class ToPutItemRequestTests -{ - private readonly Fixture _fixture = new(); - - [Fact] - public void Without_ConditionExpression_ShouldNotIncludeExpressionFields() - { - var user = _fixture.Create(); - var putItemRequest = UserMarshaller.ToPutItemRequest(user, ReturnValue.NONE, "TABLE"); - - putItemRequest.ConditionExpression.Should().BeNullOrWhiteSpace(); - putItemRequest.ExpressionAttributeNames.Should().BeNullOrEmpty(); - putItemRequest.ExpressionAttributeValues.Should().BeNullOrEmpty(); - putItemRequest.Item.Should().HaveCount(5); - putItemRequest.Item[nameof(user.Email)].S.Should().Be(user.Email); - putItemRequest.Item[nameof(user.Firstname)].S.Should().Be(user.Firstname); - putItemRequest.Item[nameof(user.Lastname)].S.Should().Be(user.Lastname); - putItemRequest.Item[nameof(user.Id)].S.Should().Be(user.Id); - putItemRequest.Item[nameof(user.Metadata)].M.Should().SatisfyRespectively(x => - { - x.Key.Should().Be(nameof(user.Metadata.ModifiedAt)); - x.Value.S.Should().Be(user.Metadata.ModifiedAt.ToString("O")); - }); - putItemRequest.ReturnValues.Should().Be(ReturnValue.NONE); - putItemRequest.TableName.Should().Be("TABLE"); - } - - [Fact] - public void With_ConditionExpression_ShouldIncludeExpressionFields() - { - var user = _fixture.Create(); - var putItemRequest = UserMarshaller.ToPutItemRequest(user, (x, y) => $"{x.Email} <> {y.Email} AND {x.Firstname} = {y.Firstname}", ReturnValue.NONE, "TABLE"); - - putItemRequest.ConditionExpression.Should().Be("#Email <> :p1 AND #Firstname = :p2"); - putItemRequest.ExpressionAttributeNames.Should().HaveCount(2); - putItemRequest.ExpressionAttributeNames["#Email"].Should().Be(nameof(user.Email)); - putItemRequest.ExpressionAttributeNames["#Firstname"].Should().Be(nameof(user.Firstname)); - putItemRequest.ExpressionAttributeValues.Should().HaveCount(2); - putItemRequest.ExpressionAttributeValues[":p1"].S.Should().Be(user.Email); - putItemRequest.ExpressionAttributeValues[":p2"].S.Should().Be(user.Firstname); - putItemRequest.Item.Should().HaveCount(5); - putItemRequest.Item[nameof(user.Email)].S.Should().Be(user.Email); - putItemRequest.Item[nameof(user.Firstname)].S.Should().Be(user.Firstname); - putItemRequest.Item[nameof(user.Lastname)].S.Should().Be(user.Lastname); - putItemRequest.Item[nameof(user.Id)].S.Should().Be(user.Id); - putItemRequest.Item[nameof(user.Metadata)].M.Should().SatisfyRespectively(x => - { - x.Key.Should().Be(nameof(user.Metadata.ModifiedAt)); - x.Value.S.Should().Be(user.Metadata.ModifiedAt.ToString("O")); - }); - putItemRequest.ReturnValues.Should().Be(ReturnValue.NONE); - putItemRequest.TableName.Should().Be("TABLE"); - } - -} - -public class User -{ - [DynamoDBHashKey] - public string Id { get; set; } = null!; - - [DynamoDBRangeKey] - public string Email { get; set; } = null!; - - public string Lastname { get; set; } = null!; - - public string Firstname { get; set; } = null!; - - public Meta Metadata { get; set; } = null!; - - public class Meta - { - public DateTimeOffset ModifiedAt { get; set; } - } -} - -public class UpdateUserEmail -{ - public string UserId { get; set; } = null!; - public string UserEmail { get; set; } = null!; - public DateTimeOffset TimeStamp { get; set; } -} \ No newline at end of file diff --git a/tests/DynamoDBGenerator.SourceGenerator.Tests/Extensions/ToUpdateItemRequestTests.cs b/tests/DynamoDBGenerator.SourceGenerator.Tests/Extensions/ToUpdateItemRequestTests.cs deleted file mode 100644 index 800976fb..00000000 --- a/tests/DynamoDBGenerator.SourceGenerator.Tests/Extensions/ToUpdateItemRequestTests.cs +++ /dev/null @@ -1,124 +0,0 @@ -using Amazon.DynamoDBv2; -using AutoFixture; -using DynamoDBGenerator.Attributes; -using DynamoDBGenerator.Extensions; -namespace DynamoDBGenerator.SourceGenerator.Tests.Extensions; - -[DynamoDBMarshaller(typeof(User))] -[DynamoDBMarshaller(typeof(User), PropertyName = "UpdateEmail", ArgumentType = typeof(UpdateUserEmail))] -public partial class ToUpdateItemRequestTests -{ - private readonly Fixture _fixture = new(); - - [Fact] - public void ArgumentTypeProvided_WithConditionExpression_ShouldIncludeUpdateAndConditionExpressionFields() - { - var updateUserEmail = _fixture.Create(); - var updateItemRequest = UpdateEmail.ToUpdateItemRequest( - updateUserEmail, - (x, y) => x.Keys(y.UserId, y.UserEmail), - (x, y) => $"SET {x.Email} = {y.UserEmail}, {x.Metadata.ModifiedAt} = {y.TimeStamp}", - (x,y) => $"{x.Id} = {y.UserId} AND {x.Email} <> {y.UserEmail}", - ReturnValue.NONE, - "TABLE" - ); - - updateItemRequest.ConditionExpression.Should().Be("#Id = :p3 AND #Email <> :p1"); - updateItemRequest.ExpressionAttributeNames.Should().HaveCount(3); - updateItemRequest.ExpressionAttributeNames["#Email"].Should().Be(nameof(User.Email)); - updateItemRequest.ExpressionAttributeNames["#Id"].Should().Be(nameof(User.Id)); - updateItemRequest.ExpressionAttributeNames["#Metadata.#ModifiedAt"].Should().Be(nameof(User.Metadata.ModifiedAt)); - updateItemRequest.ExpressionAttributeValues.Should().HaveCount(3); - updateItemRequest.ExpressionAttributeValues[":p1"].S.Should().Be(updateUserEmail.UserEmail); - updateItemRequest.ExpressionAttributeValues[":p2"].S.Should().Be(updateUserEmail.TimeStamp.ToString("O")); - updateItemRequest.ExpressionAttributeValues[":p3"].S.Should().Be(updateUserEmail.UserId); - updateItemRequest.Key[nameof(User.Email)].S.Should().Be(updateUserEmail.UserEmail); - updateItemRequest.Key[nameof(User.Id)].S.Should().Be(updateUserEmail.UserId); - updateItemRequest.ReturnValues.Should().Be(ReturnValue.NONE); - updateItemRequest.TableName.Should().Be("TABLE"); - updateItemRequest.UpdateExpression.Should().Be("SET #Email = :p1, #Metadata.#ModifiedAt = :p2"); - } - - [Fact] - public void ArgumentTypeProvided_WithoutConditionExpression_ShouldOnlyIncludeUpdateExpressionFields() - { - var updateUserEmail = _fixture.Create(); - var updateItemRequest = UpdateEmail.ToUpdateItemRequest( - updateUserEmail, - (x, y) => x.Keys(y.UserId, y.UserEmail), - (x, y) => $"SET {x.Email} = {y.UserEmail}, {x.Metadata.ModifiedAt} = {y.TimeStamp}", - ReturnValue.NONE, - "TABLE" - ); - - updateItemRequest.ConditionExpression.Should().BeNullOrWhiteSpace(); - updateItemRequest.ExpressionAttributeNames.Should().HaveCount(2); - updateItemRequest.ExpressionAttributeNames["#Email"].Should().Be(nameof(User.Email)); - updateItemRequest.ExpressionAttributeNames["#Metadata.#ModifiedAt"].Should().Be(nameof(User.Metadata.ModifiedAt)); - updateItemRequest.ExpressionAttributeValues.Should().HaveCount(2); - updateItemRequest.ExpressionAttributeValues[":p1"].S.Should().Be(updateUserEmail.UserEmail); - updateItemRequest.ExpressionAttributeValues[":p2"].S.Should().Be(updateUserEmail.TimeStamp.ToString("O")); - updateItemRequest.Key[nameof(User.Email)].S.Should().Be(updateUserEmail.UserEmail); - updateItemRequest.Key[nameof(User.Id)].S.Should().Be(updateUserEmail.UserId); - updateItemRequest.ReturnValues.Should().Be(ReturnValue.NONE); - updateItemRequest.TableName.Should().Be("TABLE"); - updateItemRequest.UpdateExpression.Should().Be("SET #Email = :p1, #Metadata.#ModifiedAt = :p2"); - } - - [Fact] - public void NoArgumentTypeProvided_WithoutConditionExpression_ShouldOnlyIncludeUpdateExpressionFields() - { - var user = _fixture.Create(); - var updateItemRequest = UserMarshaller - .ToUpdateItemRequest( - user, - (x, y) => x.Keys(y.Id, y.Lastname), - (x, y) => $"SET {x.Email} = {y.Email}, {x.Firstname} = {y.Firstname}", - ReturnValue.NONE, - "TABLE" - ); - - updateItemRequest.ConditionExpression.Should().BeNullOrWhiteSpace(); - updateItemRequest.ExpressionAttributeNames.Should().HaveCount(2); - updateItemRequest.ExpressionAttributeNames["#Email"].Should().Be(nameof(user.Email)); - updateItemRequest.ExpressionAttributeNames["#Firstname"].Should().Be(nameof(user.Firstname)); - updateItemRequest.ExpressionAttributeValues.Should().HaveCount(2); - updateItemRequest.ExpressionAttributeValues[":p1"].S.Should().Be(user.Email); - updateItemRequest.ExpressionAttributeValues[":p2"].S.Should().Be(user.Firstname); - updateItemRequest.Key[nameof(user.Id)].S.Should().Be(user.Id); - updateItemRequest.Key[nameof(user.Email)].S.Should().Be(user.Lastname); - updateItemRequest.ReturnValues.Should().Be(ReturnValue.NONE); - updateItemRequest.TableName.Should().Be("TABLE"); - updateItemRequest.UpdateExpression.Should().Be("SET #Email = :p1, #Firstname = :p2"); - } - - [Fact] - public void NoArgumentTypeProvided_WithConditionExpression_ShouldIncludeUpdateAndConditionExpressionFields() - { - var user = _fixture.Create(); - var updateItemRequest = UserMarshaller - .ToUpdateItemRequest( - user, - (x, y) => x.Keys(y.Id, y.Lastname), - (x, y) => $"SET {x.Email} = {y.Email}, {x.Firstname} = {y.Firstname}", - (x, y) => $"{x.Id} = {y.Id}", - ReturnValue.NONE, - "TABLE" - ); - - updateItemRequest.ConditionExpression.Should().Be("#Id = :p3"); - updateItemRequest.ExpressionAttributeNames.Should().HaveCount(3); - updateItemRequest.ExpressionAttributeNames["#Email"].Should().Be(nameof(user.Email)); - updateItemRequest.ExpressionAttributeNames["#Firstname"].Should().Be(nameof(user.Firstname)); - updateItemRequest.ExpressionAttributeNames["#Id"].Should().Be(nameof(user.Id)); - updateItemRequest.ExpressionAttributeValues.Should().HaveCount(3); - updateItemRequest.ExpressionAttributeValues[":p1"].S.Should().Be(user.Email); - updateItemRequest.ExpressionAttributeValues[":p2"].S.Should().Be(user.Firstname); - updateItemRequest.ExpressionAttributeValues[":p3"].S.Should().Be(user.Id); - updateItemRequest.Key[nameof(user.Id)].S.Should().Be(user.Id); - updateItemRequest.Key[nameof(user.Email)].S.Should().Be(user.Lastname); - updateItemRequest.ReturnValues.Should().Be(ReturnValue.NONE); - updateItemRequest.TableName.Should().Be("TABLE"); - updateItemRequest.UpdateExpression.Should().Be("SET #Email = :p1, #Firstname = :p2"); - } -} \ No newline at end of file diff --git a/tests/Dynatello.Tests/Dynatello.Tests.csproj b/tests/Dynatello.Tests/Dynatello.Tests.csproj new file mode 100644 index 00000000..94f35b92 --- /dev/null +++ b/tests/Dynatello.Tests/Dynatello.Tests.csproj @@ -0,0 +1,31 @@ + + + + net6.0 + enable + enable + + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + diff --git a/tests/Dynatello.Tests/ToGetItemRequestTests.cs b/tests/Dynatello.Tests/ToGetItemRequestTests.cs new file mode 100644 index 00000000..459e4af3 --- /dev/null +++ b/tests/Dynatello.Tests/ToGetItemRequestTests.cs @@ -0,0 +1,121 @@ +using Amazon.DynamoDBv2.Model; +using AutoFixture; +using DynamoDBGenerator.Exceptions; +using Dynatello.Builders; +using FluentAssertions; + +namespace Dynatello.Tests; + +public class ToGetItemRequestTests +{ + [Fact] + public void Build_Request_CompositeKeys_InvalidPartition() + { + var act = () => Cat.GetByCompositeInvalidPartition + .OnTable("TABLE") + .ToGetRequestBuilder(x => x.Id, x => x.HomeId) + .Build(("", Guid.Empty)); + + act.Should() + .Throw() + .WithMessage("Value '*' from argument '*' is not convertable*"); + } + + [Fact] + public void Build_Request_CompositeKeys_InvalidRange() + { + var act = () => Cat.GetByCompositeInvalidRange + .OnTable("TABLE") + .ToGetRequestBuilder(x => x.Id, x => x.HomeId) + .Build((Guid.Empty, "")); + + act.Should() + .Throw() + .WithMessage("Value '*' from argument '*' is not convertable*"); + } + + [Fact] + public void Build_Request_CompositeKeys_InvalidPartitionAndRange() + { + var act = () => Cat.GetByCompositeInvalidPartitionAndRange + .OnTable("TABLE") + .ToGetRequestBuilder(x => x.Id, x => x.HomeId) + .Build((2.3, "")); + + act.Should() + .Throw() + .WithMessage("Value '*' from argument '*' is not convertable*"); + } + + [Fact] + public void Build_Request_WithInvalidPartitionKey() + { + var act = () => Cat.GetByInvalidPartition + .OnTable("TABLE") + .ToGetRequestBuilder(x => x) + .Build("TEST"); + + act.Should() + .Throw() + .WithMessage("Value '*' from argument '*' is not convertable*"); + } + + [Fact] + public void Build_Request_PartitionKeyOnly() + { + var getCatByPartitionKey = Cat.GetById + .OnTable("TABLE") + .ToGetRequestBuilder(x => x); + + Cat.Fixture + .CreateMany() + .Should() + .AllSatisfy(x => getCatByPartitionKey + .Build(x) + .Should() + .BeEquivalentTo(new GetItemRequest + { + Key = new Dictionary + { + { nameof(Cat.Id), new AttributeValue { S = x.ToString() } } + }, + TableName = "TABLE", + ConsistentRead = false, + ExpressionAttributeNames = new Dictionary(), + ProjectionExpression = null, + ReturnConsumedCapacity = null, + AttributesToGet = new List() + })); + } + + [Fact] + public void Build_Request_CompositeKeys() + { + var getCatByCompositeKeys = Cat.GetByCompositeKey + .OnTable("TABLE") + .ToGetRequestBuilder(x => x.Id, x => x.HomeId); + + Cat.Fixture + .CreateMany<(Guid PartitionKey, Guid RangeKey)>() + .Should() + .AllSatisfy(x => getCatByCompositeKeys + .Build(x) + .Should() + .BeEquivalentTo(new GetItemRequest + { + Key = new Dictionary + { + { nameof(Cat.Id), new AttributeValue { S = x.PartitionKey.ToString() } }, + { nameof(Cat.HomeId), new AttributeValue { S = x.RangeKey.ToString() } } + }, + TableName = "TABLE", + ConsistentRead = false, + ExpressionAttributeNames = new Dictionary(), + ProjectionExpression = null, + ReturnConsumedCapacity = null, + AttributesToGet = new List() + } + ) + ); + } +} \ No newline at end of file diff --git a/tests/Dynatello.Tests/ToPutItemRequestTests.cs b/tests/Dynatello.Tests/ToPutItemRequestTests.cs new file mode 100644 index 00000000..cca3cc30 --- /dev/null +++ b/tests/Dynatello.Tests/ToPutItemRequestTests.cs @@ -0,0 +1,142 @@ +using Amazon.DynamoDBv2.DataModel; +using Amazon.DynamoDBv2.Model; +using AutoFixture; +using DynamoDBGenerator.Attributes; +using Dynatello.Builders; +using Dynatello.Builders.Types; +using FluentAssertions; + +namespace Dynatello.Tests; + +[DynamoDBMarshaller(typeof(User))] +public partial class ToPutItemRequestTests +{ + private readonly Fixture _fixture = new(); + + [Fact] + public void Without_ConditionExpression_ShouldNotIncludeExpressionFields() + { + var builder = UserMarshaller + .OnTable("TABLE") + .ToPutRequestBuilder(); + _fixture.CreateMany().Should().AllSatisfy(user => + { + builder.Build(user) + .Should() + .BeEquivalentTo(new PutItemRequest + { + ConditionExpression = null, + ExpressionAttributeNames = null, + ExpressionAttributeValues = null, + Item = new Dictionary + { + { nameof(user.Email), new AttributeValue { S = user.Email } }, + { nameof(user.Firstname), new AttributeValue { S = user.Firstname } }, + { nameof(user.Lastname), new AttributeValue { S = user.Lastname } }, + { nameof(user.Id), new AttributeValue { S = user.Id } }, + { + nameof(user.Metadata), new AttributeValue + { + M = new Dictionary + { + { + nameof(user.Metadata.ModifiedAt), + new AttributeValue { S = user.Metadata.ModifiedAt.ToString("O") } + } + } + } + }, + }, + ReturnValues = null, + TableName = "TABLE", + Expected = null, + ReturnConsumedCapacity = null, + ConditionalOperator = null, + ReturnItemCollectionMetrics = null, + ReturnValuesOnConditionCheckFailure = null + }); + }); + } + + [Fact] + public void With_ConditionExpression_ShouldIncludeExpressionFields() + { + var builder = UserMarshaller + .OnTable("TABLE") + .WithConditionExpression((x, y) => $"{x.Email} <> {y.Email} AND {x.Firstname} = {y.Firstname}") + .ToPutRequestBuilder(); + + _fixture.CreateMany().Should().AllSatisfy(user => + { + builder.Build(user) + .Should() + .BeEquivalentTo(new PutItemRequest + { + ConditionExpression = "#Email <> :p1 AND #Firstname = :p2", + ExpressionAttributeNames = new Dictionary + { + { "#Email", nameof(user.Email) }, + { "#Firstname", nameof(user.Firstname) } + }, + ExpressionAttributeValues = new Dictionary + { + { ":p1", new AttributeValue { S = user.Email } }, + { ":p2", new AttributeValue { S = user.Firstname } } + }, + Item = new Dictionary + { + { nameof(user.Email), new AttributeValue { S = user.Email } }, + { nameof(user.Firstname), new AttributeValue { S = user.Firstname } }, + { nameof(user.Lastname), new AttributeValue { S = user.Lastname } }, + { nameof(user.Id), new AttributeValue { S = user.Id } }, + { + nameof(user.Metadata), new AttributeValue + { + M = new Dictionary + { + { + nameof(user.Metadata.ModifiedAt), + new AttributeValue { S = user.Metadata.ModifiedAt.ToString("O") } + } + } + } + }, + }, + ReturnValues = null, + TableName = "TABLE", + Expected = null, + ReturnConsumedCapacity = null, + ConditionalOperator = null, + ReturnItemCollectionMetrics = null, + ReturnValuesOnConditionCheckFailure = null + }); + }); + } +} + +public class User +{ + [DynamoDBHashKey] + public string Id { get; set; } = null!; + + [DynamoDBRangeKey] + public string Email { get; set; } = null!; + + public string Lastname { get; set; } = null!; + + public string Firstname { get; set; } = null!; + + public Meta Metadata { get; set; } = null!; + + public class Meta + { + public DateTimeOffset ModifiedAt { get; set; } + } +} + +public class UpdateUserEmail +{ + public string UserId { get; set; } = null!; + public string UserEmail { get; set; } = null!; + public DateTimeOffset TimeStamp { get; set; } +} \ No newline at end of file diff --git a/tests/Dynatello.Tests/ToQueryRequestTests.cs b/tests/Dynatello.Tests/ToQueryRequestTests.cs new file mode 100644 index 00000000..f7064c27 --- /dev/null +++ b/tests/Dynatello.Tests/ToQueryRequestTests.cs @@ -0,0 +1,121 @@ +using System.Globalization; +using Amazon.DynamoDBv2.DataModel; +using Amazon.DynamoDBv2.Model; +using AutoFixture; +using DynamoDBGenerator.Attributes; +using Dynatello.Builders; +using Dynatello.Builders.Types; +using FluentAssertions; + +namespace Dynatello.Tests; + +public class ToQueryRequestTests +{ + [Fact] + public void Build_Request() + { + var builder = Cat.QueryWithCuteness + .OnTable("TABLE") + .WithKeyConditionExpression((x, y) => $"{x.Id} = {y.Id}") + .ToQueryRequestBuilder(); + Cat.Fixture.CreateMany<(Guid Id, double MinimumCuteness)>(10).Should().AllSatisfy(tuple => + { + builder.Build(tuple).Should().BeEquivalentTo( + new QueryRequest + { + TableName = "TABLE", + ExpressionAttributeNames = new Dictionary + { + { "#Id", nameof(Cat.Id) } + }, + + ExpressionAttributeValues = new Dictionary + { + { ":p1", new AttributeValue { S = tuple.Id.ToString() } } + }, + KeyConditionExpression = "#Id = :p1", + KeyConditions = null, + ConditionalOperator = null, + AttributesToGet = null, + ReturnConsumedCapacity = null, + FilterExpression = null, + Limit = 0, + ConsistentRead = false, + Select = null, + ProjectionExpression = null, + IndexName = null, + QueryFilter = null, + ExclusiveStartKey = new Dictionary(), + IsLimitSet = false, + ScanIndexForward = false + }); + }); + } + + [Fact] + public void Build_Request_FilterExpression() + { + var builder = Cat.QueryWithCuteness + .OnTable("TABLE") + .WithKeyConditionExpression((x, y) => $"{x.Id} = {y.Id}") + .WithFilterExpression((x, y) => $"{x.Cuteness} > {y.MinimumCuteness}") + .ToQueryRequestBuilder(); + Cat.Fixture.CreateMany<(Guid Id, double MinimumCuteness)>(10).Should().AllSatisfy(tuple => + { + builder.Build(tuple).Should().BeEquivalentTo( + new QueryRequest + { + TableName = "TABLE", + ExpressionAttributeNames = new Dictionary + { + { "#Id", nameof(Cat.Id) }, + { "#Cuteness", nameof(Cat.Cuteness) } + }, + + ExpressionAttributeValues = new Dictionary + { + { ":p1", new AttributeValue { S = tuple.Id.ToString() } }, + { + ":p2", + new AttributeValue { N = tuple.MinimumCuteness.ToString(CultureInfo.InvariantCulture) } + } + }, + KeyConditionExpression = "#Id = :p1", + KeyConditions = null, + ConditionalOperator = null, + AttributesToGet = null, + ReturnConsumedCapacity = null, + FilterExpression = "#Cuteness > :p2", + Limit = 0, + ConsistentRead = false, + Select = null, + ProjectionExpression = null, + IndexName = null, + QueryFilter = null, + ExclusiveStartKey = new Dictionary(), + IsLimitSet = false, + ScanIndexForward = false + }); + }); + } +} + +[DynamoDBMarshaller(typeof(Cat), PropertyName = "QueryWithCuteness", + ArgumentType = typeof((Guid Id, double MinimumCuteness)))] +[DynamoDBMarshaller(typeof(Cat), PropertyName = "GetByCompositeKey", ArgumentType = typeof((Guid Id, Guid HomeId)))] +[DynamoDBMarshaller(typeof(Cat), PropertyName = "GetById", ArgumentType = typeof(Guid))] +[DynamoDBMarshaller(typeof(Cat), PropertyName = "GetByInvalidPartition", ArgumentType = typeof(string))] +[DynamoDBMarshaller(typeof(Cat), PropertyName = "GetByCompositeInvalidPartition", + ArgumentType = typeof((string Id, Guid HomeId)))] +[DynamoDBMarshaller(typeof(Cat), PropertyName = "GetByCompositeInvalidRange", + ArgumentType = typeof((Guid Id, string HomeId)))] +[DynamoDBMarshaller(typeof(Cat), PropertyName = "GetByCompositeInvalidPartitionAndRange", + ArgumentType = typeof((double Id, string HomeId)))] +public readonly partial record struct Cat( + [property: DynamoDBHashKey] Guid Id, + [property: DynamoDBRangeKey] Guid HomeId, + string Name, + double Cuteness) +{ + public static readonly Fixture Fixture = new(); +} \ No newline at end of file diff --git a/tests/Dynatello.Tests/ToUpdateItemRequestTests.cs b/tests/Dynatello.Tests/ToUpdateItemRequestTests.cs new file mode 100644 index 00000000..4bd0f9bf --- /dev/null +++ b/tests/Dynatello.Tests/ToUpdateItemRequestTests.cs @@ -0,0 +1,213 @@ +using Amazon.DynamoDBv2.Model; +using AutoFixture; +using DynamoDBGenerator.Attributes; +using Dynatello.Builders; +using Dynatello.Builders.Types; +using FluentAssertions; + +namespace Dynatello.Tests; + +[DynamoDBMarshaller(typeof(User))] +[DynamoDBMarshaller(typeof(User), PropertyName = "UpdateEmail", ArgumentType = typeof(UpdateUserEmail))] +public partial class ToUpdateItemRequestTests +{ + private readonly Fixture _fixture = new(); + + [Fact] + public void ArgumentTypeProvided_WithConditionExpression_ShouldIncludeUpdateAndConditionExpressionFields() + { + var updateFirst = UpdateEmail + .OnTable("TABLE") + .WithUpdateExpression((x, y) => $"SET {x.Email} = {y.UserEmail}, {x.Metadata.ModifiedAt} = {y.TimeStamp}") + .WithConditionExpression((x, y) => $"{x.Id} = {y.UserId} AND {x.Email} <> {y.UserEmail}") + .ToUpdateItemRequestBuilder((x, y) => x.Keys(y.UserId, y.UserEmail)); + + var conditionFirst = UpdateEmail + .OnTable("TABLE") + .WithConditionExpression((x, y) => $"{x.Id} = {y.UserId} AND {x.Email} <> {y.UserEmail}") + .WithUpdateExpression((x, y) => $"SET {x.Email} = {y.UserEmail}, {x.Metadata.ModifiedAt} = {y.TimeStamp}") + .ToUpdateItemRequestBuilder((x, y) => x.Keys(y.UserId, y.UserEmail)); + + _fixture + .CreateMany() + .Should() + .AllSatisfy(x => Expected(x, updateFirst)); + _fixture + .CreateMany() + .Should() + .AllSatisfy(x => Expected(x, conditionFirst)); + + static void Expected(UpdateUserEmail updateUserEmail, UpdateRequestBuilder builder) => + builder.Build(updateUserEmail) + .Should() + .BeEquivalentTo(new UpdateItemRequest + { + AttributeUpdates = null, + ConditionalOperator = null, + ConditionExpression = "#Id = :p3 AND #Email <> :p1", + Expected = null, + ExpressionAttributeNames = new Dictionary + { + { "#Email", nameof(User.Email) }, { "#Id", nameof(User.Id) }, + { "#Metadata.#ModifiedAt", nameof(User.Metadata.ModifiedAt) } + }, + ExpressionAttributeValues = new Dictionary + { + { ":p1", new AttributeValue { S = updateUserEmail.UserEmail } }, + { ":p2", new AttributeValue { S = updateUserEmail.TimeStamp.ToString("O") } }, + { ":p3", new AttributeValue { S = updateUserEmail.UserId } } + }, + Key = new Dictionary + { + { nameof(User.Id), new AttributeValue { S = updateUserEmail.UserId } }, + { nameof(User.Email), new AttributeValue { S = updateUserEmail.UserEmail } } + }, + ReturnConsumedCapacity = null, + ReturnItemCollectionMetrics = null, + ReturnValues = null, + ReturnValuesOnConditionCheckFailure = null, + TableName = "TABLE", + UpdateExpression = "SET #Email = :p1, #Metadata.#ModifiedAt = :p2" + }); + } + + [Fact] + public void ArgumentTypeProvided_WithoutConditionExpression_ShouldOnlyIncludeUpdateExpressionFields() + { + var builder = UpdateEmail + .OnTable("TABLE") + .WithUpdateExpression((x, y) => $"SET {x.Email} = {y.UserEmail}, {x.Metadata.ModifiedAt} = {y.TimeStamp}") + .ToUpdateItemRequestBuilder((x, y) => x.Keys(y.UserId, y.UserEmail)); + + _fixture + .CreateMany() + .Should() + .AllSatisfy(updateUserEmail => builder + .Build(updateUserEmail) + .Should() + .BeEquivalentTo(new UpdateItemRequest + { + AttributeUpdates = null, + ConditionalOperator = null, + ConditionExpression = null, + Expected = null, + ExpressionAttributeNames = new Dictionary + { + { "#Email", nameof(User.Email) }, + { "#Metadata.#ModifiedAt", nameof(User.Metadata.ModifiedAt) } + }, + ExpressionAttributeValues = new Dictionary + { + { ":p1", new AttributeValue { S = updateUserEmail.UserEmail } }, + { ":p2", new AttributeValue { S = updateUserEmail.TimeStamp.ToString("O") } } + }, + Key = new Dictionary + { + { nameof(User.Id), new AttributeValue { S = updateUserEmail.UserId } }, + { nameof(User.Email), new AttributeValue { S = updateUserEmail.UserEmail } } + }, + ReturnConsumedCapacity = null, + ReturnItemCollectionMetrics = null, + ReturnValues = null, + ReturnValuesOnConditionCheckFailure = null, + TableName = "TABLE", + UpdateExpression = "SET #Email = :p1, #Metadata.#ModifiedAt = :p2" + })); + } + + [Fact] + public void NoArgumentTypeProvided_WithoutConditionExpression_ShouldOnlyIncludeUpdateExpressionFields() + { + var builder = UserMarshaller + .OnTable("TABLE") + .WithUpdateExpression((x, y) => $"SET {x.Email} = {y.Email}, {x.Firstname} = {y.Firstname}") + .ToUpdateItemRequestBuilder((x, y) => x.Keys(y.Id, y.Email)); + + _fixture.CreateMany() + .Should() + .AllSatisfy(x => builder + .Build(x) + .Should() + .BeEquivalentTo(new UpdateItemRequest + { + AttributeUpdates = null, + ConditionalOperator = null, + ConditionExpression = null, + Expected = null, + ExpressionAttributeNames = new Dictionary + { + { "#Email", nameof(User.Email) }, + { "#Firstname", nameof(User.Firstname) } + }, + ExpressionAttributeValues = new Dictionary + { + { ":p1", new AttributeValue { S = x.Email } }, + { ":p2", new AttributeValue { S = x.Firstname } } + }, + Key = new Dictionary + { + { nameof(User.Id), new AttributeValue { S = x.Id } }, + { nameof(User.Email), new AttributeValue { S = x.Email } } + }, + ReturnConsumedCapacity = null, + ReturnItemCollectionMetrics = null, + ReturnValues = null, + ReturnValuesOnConditionCheckFailure = null, + TableName = "TABLE", + UpdateExpression = "SET #Email = :p1, #Firstname = :p2" + })); + } + + [Fact] + public void NoArgumentTypeProvided_WithConditionExpression_ShouldIncludeUpdateAndConditionExpressionFields() + { + var updateFirst = UserMarshaller + .OnTable("TABLE") + .WithUpdateExpression((x, y) => $"SET {x.Email} = {y.Email}, {x.Firstname} = {y.Firstname}") + .WithConditionExpression((x, y) => $"{x.Id} = {y.Id}") + .ToUpdateItemRequestBuilder((x, y) => x.Keys(y.Id, y.Email)); + + var conditionFirst = UserMarshaller + .OnTable("TABLE") + .WithUpdateExpression((x, y) => $"SET {x.Email} = {y.Email}, {x.Firstname} = {y.Firstname}") + .WithConditionExpression((x, y) => $"{x.Id} = {y.Id}") + .ToUpdateItemRequestBuilder((x, y) => x.Keys(y.Id, y.Email)); + + _fixture.CreateMany().Should().AllSatisfy(x => Expected(x, conditionFirst)); + _fixture.CreateMany().Should().AllSatisfy(x => Expected(x, updateFirst)); + + static void Expected(User x, UpdateRequestBuilder builder) => + builder + .Build(x) + .Should() + .BeEquivalentTo(new UpdateItemRequest + { + AttributeUpdates = null, + ConditionalOperator = null, + ConditionExpression = "#Id = :p3", + Expected = null, + ExpressionAttributeNames = new Dictionary() + { + { "#Email", "Email" }, + { "#Firstname", "Firstname" }, + { "#Id", "Id" } + }, + ExpressionAttributeValues = new Dictionary() + { + { ":p1", new AttributeValue { S = x.Email } }, + { ":p2", new AttributeValue { S = x.Firstname } }, { ":p3", new AttributeValue { S = x.Id } } + }, + Key = new Dictionary + { + { "Id", new AttributeValue { S = x.Id } }, + { "Email", new AttributeValue { S = x.Email } } + }, + ReturnConsumedCapacity = null, + ReturnItemCollectionMetrics = null, + ReturnValues = null, + ReturnValuesOnConditionCheckFailure = null, + TableName = "TABLE", + UpdateExpression = "SET #Email = :p1, #Firstname = :p2" + }); + } +} \ No newline at end of file diff --git a/tests/Dynatello.Tests/Usings.cs b/tests/Dynatello.Tests/Usings.cs new file mode 100644 index 00000000..8c927eb7 --- /dev/null +++ b/tests/Dynatello.Tests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file