Skip to content

Commit 7173e17

Browse files
Implemented support for editing existing DynamoDB items
1 parent 6762977 commit 7173e17

16 files changed

+787
-90
lines changed

GuiStack/Controllers/DynamoDB/TablesController.cs

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/*
1+
/*
22
* This Source Code Form is subject to the terms of the Mozilla Public
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/.
@@ -8,6 +8,7 @@
88
*/
99

1010
using System;
11+
using System.Collections.Generic;
1112
using System.IO;
1213
using System.IO.Compression;
1314
using System.Linq;
@@ -68,6 +69,26 @@ private static string SerializeItem(DynamoDBItemModel model)
6869
return WebEncoders.Base64UrlEncode(stream.ToArray());
6970
}
7071

72+
private static bool IsPrimaryKeyModified(DynamoDBUpdateItemModel model)
73+
{
74+
var pkName = model.PartitionKey.Name;
75+
var skName = model.SortKey?.Name;
76+
77+
var pkValue = model.PartitionKey.Value.Value;
78+
var skValue = model.SortKey?.Value.Value;
79+
80+
if(model.Item.TryGetValue(pkName, out var newPk) && (!newPk?.Value?.Equals(pkValue) ?? false))
81+
return true;
82+
83+
if(model.SortKey != null)
84+
{
85+
if(model.Item.TryGetValue(skName, out var newSk) && (!newSk?.Value?.Equals(skValue) ?? false))
86+
return true;
87+
}
88+
89+
return false;
90+
}
91+
7192
private ActionResult HandleException(Exception ex)
7293
{
7394
if(ex == null)
@@ -171,13 +192,16 @@ public async Task<ActionResult> GetItems([FromRoute] string tableName, [FromQuer
171192

172193
[HttpPut("{tableName}")]
173194
[Consumes("application/json")]
174-
public async Task<ActionResult> PutItem([FromRoute] string tableName, [FromBody] DynamoDBItemModel model)
195+
public async Task<ActionResult> PutItem([FromRoute] string tableName)
175196
{
176197
if(string.IsNullOrWhiteSpace(tableName))
177198
return StatusCode((int)HttpStatusCode.BadRequest);
178199

179200
try
180201
{
202+
using var reader = new StreamReader(HttpContext.Request.Body);
203+
var model = JsonConvert.DeserializeObject<DynamoDBItemModel>(await reader.ReadToEndAsync());
204+
181205
await dynamodbRepository.PutItemAsync(tableName, model);
182206
return Ok();
183207
}
@@ -186,5 +210,57 @@ public async Task<ActionResult> PutItem([FromRoute] string tableName, [FromBody]
186210
return HandleException(ex);
187211
}
188212
}
213+
214+
[HttpPatch("{tableName}")]
215+
[Consumes("application/json")]
216+
public async Task<ActionResult> UpdateItem([FromRoute] string tableName)
217+
{
218+
if(string.IsNullOrWhiteSpace(tableName))
219+
return StatusCode((int)HttpStatusCode.BadRequest);
220+
221+
using var reader = new StreamReader(HttpContext.Request.Body);
222+
223+
DynamoDBUpdateItemModel model = null;
224+
bool isKeyModified = false;
225+
226+
try
227+
{
228+
model = JsonConvert.DeserializeObject<DynamoDBUpdateItemModel>(await reader.ReadToEndAsync());
229+
230+
if(model == null || model.Item == null || model.Item.Count <= 0 || model.PartitionKey?.Value == null || model.SortKey?.Value == null)
231+
return StatusCode((int)HttpStatusCode.BadRequest);
232+
233+
isKeyModified = IsPrimaryKeyModified(model);
234+
235+
var oldItem = await dynamodbRepository.GetItemAsync(tableName, model.PartitionKey, model.SortKey);
236+
if(oldItem == null)
237+
{
238+
return NotFound(new {
239+
error = $"Item with Partition Key '{model.PartitionKey.Value?.Value}'{(model.SortKey != null ? $" and Sort Key '{model.SortKey.Value?.Value}'" : "")} not found"
240+
});
241+
}
242+
243+
await dynamodbRepository.PutItemAsync(tableName, model.Item);
244+
}
245+
catch(Exception ex)
246+
{
247+
return HandleException(ex);
248+
}
249+
250+
// If the primary key has been modified, a completely new item is created. Thus, delete the old one
251+
if(isKeyModified)
252+
{
253+
try
254+
{
255+
await dynamodbRepository.DeleteItemAsync(tableName, model.PartitionKey, model.SortKey);
256+
}
257+
catch(Exception ex)
258+
{
259+
return HandleException(new AmazonDynamoDBException($"Failed to delete old item after creating new one. Both items may be present in the DB. Error message: {ex.Message}", ex));
260+
}
261+
}
262+
263+
return Ok();
264+
}
189265
}
190266
}

GuiStack/Extensions/DynamoDBExtensions.cs

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,13 @@ public static class DynamoDBExtensions
3333
{ TableClass.STANDARD_INFREQUENT_ACCESS.Value, "Standard-IA" }
3434
};
3535

36-
private static readonly Dictionary<DynamoDBAttributeType, ScalarAttributeType> DynamoDBScalarAttributeMap = new Dictionary<DynamoDBAttributeType, ScalarAttributeType>() {
37-
{ DynamoDBAttributeType.String, ScalarAttributeType.S },
38-
{ DynamoDBAttributeType.Number, ScalarAttributeType.N },
39-
{ DynamoDBAttributeType.Binary, ScalarAttributeType.B }
36+
private static readonly Dictionary<DynamoDBKeyAttributeType, ScalarAttributeType> DynamoDBScalarAttributeMap = new Dictionary<DynamoDBKeyAttributeType, ScalarAttributeType>() {
37+
{ DynamoDBKeyAttributeType.String, ScalarAttributeType.S },
38+
{ DynamoDBKeyAttributeType.Number, ScalarAttributeType.N },
39+
{ DynamoDBKeyAttributeType.Binary, ScalarAttributeType.B }
4040
};
4141

42-
private static readonly Dictionary<string, DynamoDBAttributeType> ScalarDynamoDBAttributeMap =
42+
private static readonly Dictionary<string, DynamoDBKeyAttributeType> ScalarDynamoDBAttributeMap =
4343
DynamoDBScalarAttributeMap.ToDictionary(kvp => kvp.Value.Value, kvp => kvp.Key);
4444

4545
private static readonly Dictionary<DynamoDBFieldType, string> FieldTypeDynamoDBMap = new Dictionary<DynamoDBFieldType, string>() {
@@ -307,19 +307,19 @@ public static string ToHumanReadableString(this TableClass entity)
307307
}
308308

309309
/// <summary>
310-
/// Converts the <see cref="ScalarAttributeType"/> to <see cref="DynamoDBAttributeType"/>.
310+
/// Converts the <see cref="ScalarAttributeType"/> to <see cref="DynamoDBKeyAttributeType"/>.
311311
/// </summary>
312312
/// <param name="attributeType">The <see cref="ScalarAttributeType"/> to convert.</param>
313-
public static DynamoDBAttributeType ToDynamoDBAttributeType(this ScalarAttributeType attributeType)
313+
public static DynamoDBKeyAttributeType ToDynamoDBAttributeType(this ScalarAttributeType attributeType)
314314
{
315315
return ScalarDynamoDBAttributeMap.GetValueOrDefault(attributeType.Value);
316316
}
317317

318318
/// <summary>
319-
/// Converts the <see cref="DynamoDBAttributeType"/> to <see cref="ScalarAttributeType"/>.
319+
/// Converts the <see cref="DynamoDBKeyAttributeType"/> to <see cref="ScalarAttributeType"/>.
320320
/// </summary>
321-
/// <param name="attributeType">The <see cref="DynamoDBAttributeType"/> to convert.</param>
322-
public static ScalarAttributeType ToScalarAttributeType(this DynamoDBAttributeType attributeType)
321+
/// <param name="attributeType">The <see cref="DynamoDBKeyAttributeType"/> to convert.</param>
322+
public static ScalarAttributeType ToScalarAttributeType(this DynamoDBKeyAttributeType attributeType)
323323
{
324324
return DynamoDBScalarAttributeMap.GetValueOrDefault(attributeType);
325325
}
@@ -342,6 +342,18 @@ public static DynamoDBFieldType StringToDynamoDBFieldType(string dynamoDbType)
342342
return DynamoDBFieldTypeMap.GetValueOrDefault(dynamoDbType);
343343
}
344344

345+
/// <summary>
346+
/// Converts the <see cref="DynamoDBAttributeValue"/> to an <see cref="AttributeValue"/>.
347+
/// </summary>
348+
/// <param name="attributeValue">The attribute value to convert.</param>
349+
public static AttributeValue ToDynamoDBAttributeValue(this DynamoDBAttributeValue attributeValue)
350+
{
351+
if(string.IsNullOrWhiteSpace(attributeValue.Name))
352+
throw new AmazonDynamoDBException($"DynamoDB attribute name cannot be empty", ErrorType.Sender, "GuiStack_InvalidField", null, HttpStatusCode.BadRequest);
353+
354+
return GetDynamoDBAttributeValue(attributeValue.Name, attributeValue.Value);
355+
}
356+
345357
/// <summary>
346358
/// Converts the item data into a DynamoDB item.
347359
/// </summary>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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 2024
7+
* https://github.com/Visual-Vincent/GuiStack
8+
*/
9+
10+
using System;
11+
12+
namespace GuiStack.Models
13+
{
14+
public class DynamoDBAttributeValue
15+
{
16+
public string Name { get; set; }
17+
public DynamoDBFieldModel Value { get; set; }
18+
19+
public DynamoDBAttributeValue()
20+
{
21+
}
22+
23+
public DynamoDBAttributeValue(string name, DynamoDBFieldModel value)
24+
{
25+
Name = name;
26+
Value = value;
27+
}
28+
}
29+
}

GuiStack/Models/DynamoDBCreateTableModel.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ namespace GuiStack.Models
1414
public class DynamoDBCreateTableModel
1515
{
1616
public string TableName { get; set; }
17-
public DynamoDBAttribute PartitionKey { get; set; }
18-
public DynamoDBAttribute SortKey { get; set; }
17+
public DynamoDBKeyAttribute PartitionKey { get; set; }
18+
public DynamoDBKeyAttribute SortKey { get; set; }
1919
}
2020
}

GuiStack/Models/DynamoDBItem.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,19 @@ public DynamoDBItem(IDictionary<string, AttributeValue> attributes)
4444
Attributes = new Dictionary<string, AttributeValue>(attributes);
4545
}
4646

47+
/// <summary>
48+
/// Initializes a new instance of <see cref="DynamoDBItem"/> and sets the collection of attributes.
49+
/// </summary>
50+
/// <param name="attributes">The collection of attributes that the <see cref="DynamoDBItem"/> should contain.</param>
51+
/// <remarks>
52+
/// The difference between initializing an instance using this method versus the <see cref="DynamoDBItem(IDictionary{string, AttributeValue})"/> constructor,
53+
/// is that the constructor creates a shallow copy of the dictionary, whereas this method uses the exact same dictionary reference.
54+
/// </remarks>
55+
public static DynamoDBItem FromAttributes(Dictionary<string, AttributeValue> attributes)
56+
{
57+
return new DynamoDBItem() { Attributes = attributes };
58+
}
59+
4760
#region IDisposable Support
4861
private bool disposedValue;
4962

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,23 @@
1111

1212
namespace GuiStack.Models
1313
{
14-
public class DynamoDBAttribute
14+
public class DynamoDBKeyAttribute
1515
{
1616
public string Name { get; set; }
17-
public DynamoDBAttributeType Type { get; set; }
17+
public DynamoDBKeyAttributeType Type { get; set; }
1818

19-
public DynamoDBAttribute()
19+
public DynamoDBKeyAttribute()
2020
{
2121
}
2222

23-
public DynamoDBAttribute(string name, DynamoDBAttributeType type)
23+
public DynamoDBKeyAttribute(string name, DynamoDBKeyAttributeType type)
2424
{
2525
Name = name;
2626
Type = type;
2727
}
2828
}
2929

30-
public enum DynamoDBAttributeType
30+
public enum DynamoDBKeyAttributeType
3131
{
3232
Unknown = 0,
3333
String,

GuiStack/Models/DynamoDBTableContentsModel.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ namespace GuiStack.Models
1414
{
1515
public class DynamoDBTableContentsModel
1616
{
17-
public DynamoDBAttribute PartitionKey { get; set; }
18-
public DynamoDBAttribute SortKey { get; set; }
17+
public DynamoDBKeyAttribute PartitionKey { get; set; }
18+
public DynamoDBKeyAttribute SortKey { get; set; }
1919
public string[] AttributeNames { get; set; }
2020
public string LastEvaluatedKey { get; set; }
2121

GuiStack/Models/DynamoDBTableModel.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public class DynamoDBTableModel
2525
public bool DeletionProtectionEnabled { get; set; }
2626
public string Status { get; set; }
2727

28-
public DynamoDBAttribute PartitionKey { get; set; }
29-
public DynamoDBAttribute SortKey { get; set; }
28+
public DynamoDBKeyAttribute PartitionKey { get; set; }
29+
public DynamoDBKeyAttribute SortKey { get; set; }
3030
}
3131
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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 2024
7+
* https://github.com/Visual-Vincent/GuiStack
8+
*/
9+
10+
using System;
11+
12+
namespace GuiStack.Models
13+
{
14+
public class DynamoDBUpdateItemModel
15+
{
16+
public DynamoDBAttributeValue PartitionKey { get; set; }
17+
public DynamoDBAttributeValue SortKey { get; set; }
18+
public DynamoDBItemModel Item { get; set; }
19+
}
20+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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 2024
7+
* https://github.com/Visual-Vincent/GuiStack
8+
*/
9+
10+
using System;
11+
12+
namespace GuiStack.Models
13+
{
14+
public class FieldsEditorModalModel
15+
{
16+
public string ElementId { get; set; }
17+
public string FunctionName { get; set; }
18+
public string ModalTitle { get; set; }
19+
public string SaveButtonText { get; set; }
20+
21+
public FieldsEditorModalModel(string elementId, string functionName, string modalTitle, string saveButtonText)
22+
{
23+
ElementId = elementId;
24+
FunctionName = functionName;
25+
ModalTitle = modalTitle;
26+
SaveButtonText = saveButtonText;
27+
}
28+
}
29+
}

0 commit comments

Comments
 (0)