Skip to content

Commit f28c686

Browse files
Added support for creating or updating DynamoDB items
1 parent 1bad628 commit f28c686

File tree

10 files changed

+464
-86
lines changed

10 files changed

+464
-86
lines changed

GuiStack/Controllers/DynamoDB/TablesController.cs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
namespace GuiStack.Controllers.DynamoDB
2020
{
2121
[ApiController]
22-
[Route("api/" + nameof(DynamoDB) + "/[controller]")]
22+
[Route("api/" + nameof(DynamoDB))]
2323
public class TablesController : Controller
2424
{
2525
private IDynamoDBRepository dynamodbRepository;
@@ -39,6 +39,9 @@ private ActionResult HandleException(Exception ex)
3939
if(dynamodbEx.StatusCode == HttpStatusCode.NotFound)
4040
return StatusCode((int)dynamodbEx.StatusCode, new { error = dynamodbEx.Message });
4141

42+
if(dynamodbEx.StatusCode == HttpStatusCode.BadRequest && dynamodbEx.ErrorCode == "GuiStack_InvalidField")
43+
return StatusCode((int)dynamodbEx.StatusCode, new { error = dynamodbEx.Message });
44+
4245
Console.Error.WriteLine(dynamodbEx);
4346
return StatusCode((int)HttpStatusCode.InternalServerError, new { error = ex.Message });
4447
}
@@ -71,8 +74,6 @@ public async Task<ActionResult> DeleteTable([FromRoute] string tableName)
7174
if(string.IsNullOrWhiteSpace(tableName))
7275
return StatusCode((int)HttpStatusCode.BadRequest);
7376

74-
tableName = tableName.DecodeRouteParameter();
75-
7677
try
7778
{
7879
await dynamodbRepository.DeleteTableAsync(tableName);
@@ -83,5 +84,23 @@ public async Task<ActionResult> DeleteTable([FromRoute] string tableName)
8384
return HandleException(ex);
8485
}
8586
}
87+
88+
[HttpPut("{tableName}")]
89+
[Consumes("application/json")]
90+
public async Task<ActionResult> PutItem([FromRoute] string tableName, [FromBody] DynamoDBItemModel model)
91+
{
92+
if(string.IsNullOrWhiteSpace(tableName))
93+
return StatusCode((int)HttpStatusCode.BadRequest);
94+
95+
try
96+
{
97+
await dynamodbRepository.PutItemAsync(tableName, model);
98+
return Ok();
99+
}
100+
catch(Exception ex)
101+
{
102+
return HandleException(ex);
103+
}
104+
}
86105
}
87106
}

GuiStack/Extensions/AWSExtensions.cs

Lines changed: 1 addition & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,21 @@
33
* License, v. 2.0. If a copy of the MPL was not distributed with this
44
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
55
*
6-
* Copyright © Vincent Bengtsson & Contributors 2022
6+
* Copyright © Vincent Bengtsson & Contributors 2022-2024
77
* https://github.com/Visual-Vincent/GuiStack
88
*/
99

1010
using System;
1111
using System.Collections.Generic;
1212
using System.Net;
1313
using System.Runtime.CompilerServices;
14-
using Amazon.DynamoDBv2;
1514
using Amazon.Runtime;
1615
using Amazon.SQS.Model;
1716

1817
namespace GuiStack.Extensions
1918
{
2019
public static class AWSExtensions
2120
{
22-
private static readonly Dictionary<string, string> DynamoDBBillingModeMap = new Dictionary<string, string>() {
23-
{ BillingMode.PAY_PER_REQUEST.Value, "On-demand" },
24-
{ BillingMode.PROVISIONED.Value, "Provisioned" }
25-
};
26-
27-
private static readonly Dictionary<string, string> DynamoDBTableClassMap = new Dictionary<string, string>() {
28-
{ TableClass.STANDARD.Value, "Standard" },
29-
{ TableClass.STANDARD_INFREQUENT_ACCESS.Value, "Standard-IA" }
30-
};
31-
3221
/// <summary>
3322
/// Throws a <see cref="WebException"/> if the response returns a non-successful status code (includes 3xx redirects, since they are unusable by the caller).
3423
/// </summary>
@@ -48,30 +37,6 @@ public static void ThrowIfUnsuccessful(this AmazonWebServiceResponse response, s
4837
}
4938
}
5039

51-
/// <summary>
52-
/// Converts the <see cref="BillingMode"/> into a human-readable string.
53-
/// </summary>
54-
/// <param name="entity">The <see cref="BillingMode"/> to convert.</param>
55-
public static string ToHumanReadableString(this BillingMode entity)
56-
{
57-
if(entity == null)
58-
return "(Unknown)";
59-
60-
return DynamoDBBillingModeMap.GetValueOrDefault(entity?.Value) ?? "(Unknown)";
61-
}
62-
63-
/// <summary>
64-
/// Converts the <see cref="TableClass"/> into a human-readable string.
65-
/// </summary>
66-
/// <param name="entity">The <see cref="TableClass"/> to convert.</param>
67-
public static string ToHumanReadableString(this TableClass entity)
68-
{
69-
if(entity == null)
70-
return "(Unknown)";
71-
72-
return DynamoDBTableClassMap.GetValueOrDefault(entity?.Value) ?? "(Unknown)";
73-
}
74-
7540
public static Dictionary<string, string> ToStringAttributes(this Dictionary<string, MessageAttributeValue> attributes)
7641
{
7742
Dictionary<string, string> strAttributes = new Dictionary<string, string>();
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
/*
2+
* This Source Code Form is subject to the terms of the Mozilla Public
3+
* License, v. 2.0. If a copy of the MPL was not distributed with this
4+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
5+
*
6+
* Copyright © Vincent Bengtsson & Contributors 2022-2024
7+
* https://github.com/Visual-Vincent/GuiStack
8+
*/
9+
10+
using System;
11+
using System.Collections;
12+
using System.Collections.Generic;
13+
using System.IO;
14+
using System.Linq;
15+
using System.Net;
16+
using Amazon.DynamoDBv2;
17+
using Amazon.DynamoDBv2.Model;
18+
using Amazon.Runtime;
19+
using GuiStack.Models;
20+
21+
namespace GuiStack.Extensions
22+
{
23+
public static class DynamoDBExtensions
24+
{
25+
private static readonly Dictionary<string, string> DynamoDBBillingModeMap = new Dictionary<string, string>() {
26+
{ BillingMode.PAY_PER_REQUEST.Value, "On-demand" },
27+
{ BillingMode.PROVISIONED.Value, "Provisioned" }
28+
};
29+
30+
private static readonly Dictionary<string, string> DynamoDBTableClassMap = new Dictionary<string, string>() {
31+
{ TableClass.STANDARD.Value, "Standard" },
32+
{ TableClass.STANDARD_INFREQUENT_ACCESS.Value, "Standard-IA" }
33+
};
34+
35+
private static readonly Dictionary<DynamoDBAttributeType, ScalarAttributeType> DynamoDBScalarAttributeMap = new Dictionary<DynamoDBAttributeType, ScalarAttributeType>() {
36+
{ DynamoDBAttributeType.String, ScalarAttributeType.S },
37+
{ DynamoDBAttributeType.Number, ScalarAttributeType.N },
38+
{ DynamoDBAttributeType.Binary, ScalarAttributeType.B }
39+
};
40+
41+
private static readonly Dictionary<string, DynamoDBAttributeType> ScalarDynamoDBAttributeMap =
42+
DynamoDBScalarAttributeMap.ToDictionary(kvp => kvp.Value.Value, kvp => kvp.Key);
43+
44+
private static readonly Dictionary<DynamoDBFieldType, string> FieldTypeDynamoDBMap = new Dictionary<DynamoDBFieldType, string>() {
45+
{ DynamoDBFieldType.Binary, "B" },
46+
{ DynamoDBFieldType.BinarySet, "BS" },
47+
{ DynamoDBFieldType.Bool, "BOOL" },
48+
{ DynamoDBFieldType.List, "L" },
49+
{ DynamoDBFieldType.Map, "M" },
50+
{ DynamoDBFieldType.Null, "NULL" },
51+
{ DynamoDBFieldType.Number, "N" },
52+
{ DynamoDBFieldType.NumberSet, "NS" },
53+
{ DynamoDBFieldType.String, "S" },
54+
{ DynamoDBFieldType.StringSet, "SS" },
55+
};
56+
57+
private static readonly Dictionary<string, DynamoDBFieldType> DynamoDBFieldTypeMap =
58+
FieldTypeDynamoDBMap.ToDictionary(kvp => kvp.Value, kvp => kvp.Key);
59+
60+
private static AttributeValue GetDynamoDBAttributeValue(KeyValuePair<string, DynamoDBFieldModel> field)
61+
{
62+
if(field.Value?.Value == null || field.Value.Type == DynamoDBFieldType.Null)
63+
return new AttributeValue() { NULL = true };
64+
65+
var name = field.Key;
66+
var value = field.Value.Value;
67+
var attributeValue = new AttributeValue();
68+
69+
switch(field.Value.Type)
70+
{
71+
case DynamoDBFieldType.String:
72+
if(value is not string str)
73+
throw new AmazonDynamoDBException($"Field '{name}' was expected to be a string", ErrorType.Sender, "GuiStack_InvalidField", null, HttpStatusCode.BadRequest);
74+
75+
attributeValue.S = str;
76+
break;
77+
78+
case DynamoDBFieldType.StringSet:
79+
if(value is not IEnumerable<string> strList)
80+
throw new AmazonDynamoDBException($"Field '{name}' was expected to be a list of strings", ErrorType.Sender, "GuiStack_InvalidField", null, HttpStatusCode.BadRequest);
81+
82+
attributeValue.SS = strList.ToList();
83+
break;
84+
85+
case DynamoDBFieldType.Number:
86+
string numValue;
87+
88+
// Not validating numbers as strings: Let DynamoDB handle the parsing and validation
89+
if(value is string || value is decimal || (value.GetType().IsPrimitive && value is not char))
90+
numValue = value.ToString();
91+
else
92+
throw new AmazonDynamoDBException($"Field '{name}' was expected to be a number", ErrorType.Sender, "GuiStack_InvalidField", null, HttpStatusCode.BadRequest);
93+
94+
attributeValue.N = numValue;
95+
break;
96+
97+
case DynamoDBFieldType.NumberSet:
98+
if(value is not IEnumerable numList)
99+
throw new AmazonDynamoDBException($"Field '{name}' was expected to be a list of numbers", ErrorType.Sender, "GuiStack_InvalidField", null, HttpStatusCode.BadRequest);
100+
101+
attributeValue.NS = new List<string>();
102+
103+
int ni = -1;
104+
105+
foreach(var item in numList)
106+
{
107+
if(!(value is string || value is decimal || (value.GetType().IsPrimitive && value is not char)))
108+
throw new AmazonDynamoDBException($"Item at index {ni} of field '{name}' was expected to be a number", ErrorType.Sender, "GuiStack_InvalidField", null, HttpStatusCode.BadRequest);
109+
110+
attributeValue.NS.Add(value.ToString());
111+
ni++;
112+
}
113+
break;
114+
115+
case DynamoDBFieldType.Binary:
116+
if(value is not string binData)
117+
throw new AmazonDynamoDBException($"Field '{name}' was expected to be a Base64-encoded string", ErrorType.Sender, "GuiStack_InvalidField", null, HttpStatusCode.BadRequest);
118+
119+
try
120+
{
121+
var data = Convert.FromBase64String(binData);
122+
attributeValue.B = new MemoryStream(data);
123+
}
124+
catch(Exception ex)
125+
{
126+
throw new AmazonDynamoDBException($"Field '{name}' contains invalid Base64-encoded data", ex, ErrorType.Sender, "GuiStack_InvalidField", null, HttpStatusCode.BadRequest);
127+
}
128+
break;
129+
130+
case DynamoDBFieldType.BinarySet:
131+
if(value is not IEnumerable<string> binDataList)
132+
throw new AmazonDynamoDBException($"Field '{name}' was expected to be a list of Base64-encoded strings", ErrorType.Sender, "GuiStack_InvalidField", null, HttpStatusCode.BadRequest);
133+
134+
attributeValue.BS = new List<MemoryStream>();
135+
136+
int bi = 0;
137+
138+
try
139+
{
140+
foreach(var strData in binDataList)
141+
{
142+
var data = Convert.FromBase64String(strData);
143+
attributeValue.BS.Add(new MemoryStream(data));
144+
bi++;
145+
}
146+
}
147+
catch(Exception ex)
148+
{
149+
throw new AmazonDynamoDBException($"Item at index {bi} of field '{name}' contains invalid Base64-encoded data", ex, ErrorType.Sender, "GuiStack_InvalidField", null, HttpStatusCode.BadRequest);
150+
}
151+
break;
152+
153+
case DynamoDBFieldType.Bool:
154+
if(value is not bool boolVal)
155+
throw new AmazonDynamoDBException($"Field '{name}' was expected to be a boolean", ErrorType.Sender, "GuiStack_InvalidField", null, HttpStatusCode.BadRequest);
156+
157+
attributeValue.BOOL = boolVal;
158+
break;
159+
160+
case DynamoDBFieldType.List: // TODO: Add support in the future?
161+
case DynamoDBFieldType.Map: // TODO: Add support in the future?
162+
default:
163+
throw new AmazonDynamoDBException($"Unexpected field type '{field.Value.Type}'", ErrorType.Sender, "GuiStack_InvalidField", null, HttpStatusCode.BadRequest);
164+
}
165+
166+
return attributeValue;
167+
}
168+
169+
/// <summary>
170+
/// Converts the <see cref="BillingMode"/> into a human-readable string.
171+
/// </summary>
172+
/// <param name="entity">The <see cref="BillingMode"/> to convert.</param>
173+
public static string ToHumanReadableString(this BillingMode entity)
174+
{
175+
if(entity == null)
176+
return "(Unknown)";
177+
178+
return DynamoDBBillingModeMap.GetValueOrDefault(entity?.Value) ?? "(Unknown)";
179+
}
180+
181+
/// <summary>
182+
/// Converts the <see cref="TableClass"/> into a human-readable string.
183+
/// </summary>
184+
/// <param name="entity">The <see cref="TableClass"/> to convert.</param>
185+
public static string ToHumanReadableString(this TableClass entity)
186+
{
187+
if(entity == null)
188+
return "(Unknown)";
189+
190+
return DynamoDBTableClassMap.GetValueOrDefault(entity?.Value) ?? "(Unknown)";
191+
}
192+
193+
/// <summary>
194+
/// Converts the <see cref="ScalarAttributeType"/> to <see cref="DynamoDBAttributeType"/>.
195+
/// </summary>
196+
/// <param name="attributeType">The <see cref="ScalarAttributeType"/> to convert.</param>
197+
public static DynamoDBAttributeType ToDynamoDBAttributeType(this ScalarAttributeType attributeType)
198+
{
199+
return ScalarDynamoDBAttributeMap.GetValueOrDefault(attributeType.Value);
200+
}
201+
202+
/// <summary>
203+
/// Converts the <see cref="DynamoDBAttributeType"/> to <see cref="ScalarAttributeType"/>.
204+
/// </summary>
205+
/// <param name="attributeType">The <see cref="DynamoDBAttributeType"/> to convert.</param>
206+
public static ScalarAttributeType ToScalarAttributeType(this DynamoDBAttributeType attributeType)
207+
{
208+
return DynamoDBScalarAttributeMap.GetValueOrDefault(attributeType);
209+
}
210+
211+
/// <summary>
212+
/// Converts the <see cref="DynamoDBFieldType"/> to the respective DynamoDB type string.
213+
/// </summary>
214+
/// <param name="fieldType">The <see cref="DynamoDBFieldType"/> to convert.</param>
215+
public static string ToDynamoDBType(this DynamoDBFieldType fieldType)
216+
{
217+
return FieldTypeDynamoDBMap.GetValueOrDefault(fieldType);
218+
}
219+
220+
/// <summary>
221+
/// Converts a DynamoDB type string into a <see cref="DynamoDBFieldType"/>.
222+
/// </summary>
223+
/// <param name="dynamoDbType">The type string to convert.</param>
224+
public static DynamoDBFieldType StringToDynamoDBFieldType(string dynamoDbType)
225+
{
226+
return DynamoDBFieldTypeMap.GetValueOrDefault(dynamoDbType);
227+
}
228+
229+
/// <summary>
230+
/// Converts the item data into a DynamoDB item.
231+
/// </summary>
232+
/// <param name="itemData">The item data to convert.</param>
233+
public static DynamoDBItem ToDynamoDBItem(this IDictionary<string, DynamoDBFieldModel> itemData)
234+
{
235+
var result = new DynamoDBItem();
236+
237+
foreach(var field in itemData)
238+
{
239+
if(string.IsNullOrWhiteSpace(field.Key))
240+
throw new AmazonDynamoDBException($"DynamoDB field name cannot be empty", ErrorType.Sender, "GuiStack_InvalidField", null, HttpStatusCode.BadRequest);
241+
242+
var name = field.Key;
243+
var attributeValue = GetDynamoDBAttributeValue(field);
244+
245+
result.Attributes.Add(name, attributeValue);
246+
}
247+
248+
return result;
249+
}
250+
}
251+
}

0 commit comments

Comments
 (0)