From 0cff7a628322c29ee594a65caf530767ce59474f Mon Sep 17 00:00:00 2001
From: ghetelgb
Date: Fri, 14 Feb 2025 10:24:12 +0200
Subject: [PATCH 1/5] Annotation is working on nested attributes, some tests
are failing.
---
...AutoGeneratedTimestampRecordExtension.java | 116 +++++++++-----
.../functionaltests/UpdateBehaviorTest.java | 142 ++++++++++++------
.../NestedRecordWithUpdateBehavior.java | 3 +
.../models/RecordWithUpdateBehaviors.java | 1 +
4 files changed, 182 insertions(+), 80 deletions(-)
diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedTimestampRecordExtension.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedTimestampRecordExtension.java
index 2ac27d918202..e0076247a1c7 100644
--- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedTimestampRecordExtension.java
+++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedTimestampRecordExtension.java
@@ -20,8 +20,11 @@
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.function.Consumer;
+import java.util.stream.Collectors;
import software.amazon.awssdk.annotations.NotThreadSafe;
import software.amazon.awssdk.annotations.SdkPublicApi;
import software.amazon.awssdk.annotations.ThreadSafe;
@@ -30,40 +33,41 @@
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClientExtension;
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbExtensionContext;
import software.amazon.awssdk.enhanced.dynamodb.EnhancedType;
+import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
+import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedTimestampAttribute;
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTag;
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableMetadata;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
import software.amazon.awssdk.utils.Validate;
/**
- * This extension enables selected attributes to be automatically updated with a current timestamp every time they are written
- * to the database.
+ * This extension enables selected attributes to be automatically updated with a current timestamp every time they are written to
+ * the database.
*
- * This extension is not loaded by default when you instantiate a
- * {@link software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient}. Thus you need to specify it in custom extension
- * while creating the enhanced client.
- *
- * Example to add AutoGeneratedTimestampRecordExtension along with default extensions is
- * DynamoDbEnhancedClient.builder().extensions(Stream.concat(ExtensionResolver.defaultExtensions().stream(),
- * Stream.of(AutoGeneratedTimestampRecordExtension.create())).collect(Collectors.toList())).build();
- *
- *
- * Example to just add AutoGeneratedTimestampRecordExtension without default extensions is
- * DynamoDbEnhancedClient.builder().extensions(AutoGeneratedTimestampRecordExtension.create())).build();
- *
+ * This extension is not loaded by default when you instantiate a
+ * {@link software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient}. Thus you need to specify it in custom extension while
+ * creating the enhanced client.
+ *
+ * Example to add AutoGeneratedTimestampRecordExtension along with default extensions is
+ * DynamoDbEnhancedClient.builder().extensions(Stream.concat(ExtensionResolver.defaultExtensions().stream(),
+ * Stream.of(AutoGeneratedTimestampRecordExtension.create())).collect(Collectors.toList())).build();
+ *
+ *
+ * Example to just add AutoGeneratedTimestampRecordExtension without default extensions is
+ * DynamoDbEnhancedClient.builder().extensions(AutoGeneratedTimestampRecordExtension.create())).build();
+ *
*
*
- * To utilize auto generated timestamp update, first create a field in your model that will be used to store the record
- * timestamp of modification. This class field must be an {@link Instant} Class type, and you need to tag it as the
- * autoGeneratedTimeStampAttribute. If you are using the
- * {@link software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchema}
- * then you should use the
- * {@link software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedTimestampAttribute}
- * annotation, otherwise if you are using the {@link software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema}
- * then you should use the {@link AttributeTags#autoGeneratedTimestampAttribute()} static attribute tag.
+ * To utilize auto generated timestamp update, first create a field in your model that will be used to store the record timestamp
+ * of modification. This class field must be an {@link Instant} Class type, and you need to tag it as the
+ * autoGeneratedTimeStampAttribute. If you are using the {@link software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchema}
+ * then you should use the
+ * {@link software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedTimestampAttribute} annotation,
+ * otherwise if you are using the {@link software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema} then you should use
+ * the {@link AttributeTags#autoGeneratedTimestampAttribute()} static attribute tag.
*
- * Every time a new update of the record is successfully written to the database, the timestamp at which it was modified will
- * be automatically updated. This extension applies the conversions as defined in the attribute convertor.
+ * Every time a new update of the record is successfully written to the database, the timestamp at which it was modified will be
+ * automatically updated. This extension applies the conversions as defined in the attribute convertor.
*/
@SdkPublicApi
@ThreadSafe
@@ -87,6 +91,7 @@ private AttributeTags() {
/**
* Tags which indicate that the given attribute is supported wih Auto Generated Timestamp Record Extension.
+ *
* @return Tag name for AutoGenerated Timestamp Records
*/
public static StaticAttributeTag autoGeneratedTimestampAttribute() {
@@ -100,6 +105,7 @@ private AutoGeneratedTimestampRecordExtension(Builder builder) {
/**
* Create a builder that can be used to create a {@link AutoGeneratedTimestampRecordExtension}.
+ *
* @return Builder to create AutoGeneratedTimestampRecordExtension,
*/
public static Builder builder() {
@@ -126,30 +132,66 @@ public static AutoGeneratedTimestampRecordExtension create() {
*/
@Override
public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite context) {
+ Collection customMetadataObject = context.tableMetadata()
+ .customMetadataObject(CUSTOM_METADATA_KEY, Collection.class).orElse(null);
- Collection customMetadataObject = context.tableMetadata()
- .customMetadataObject(CUSTOM_METADATA_KEY, Collection.class).orElse(null);
-
- if (customMetadataObject == null) {
- return WriteModification.builder().build();
- }
Map itemToTransform = new HashMap<>(context.items());
- customMetadataObject.forEach(
- key -> insertTimestampInItemToTransform(itemToTransform, key,
- context.tableSchema().converterForAttribute(key)));
+ itemToTransform.forEach((key, value) -> {
+ if (customMetadataObject != null && customMetadataObject.contains(key)) {
+ insertTimestampInItemToTransform(itemToTransform, key, context.tableSchema().converterForAttribute(key));
+ } else if (value.hasM() && value.m() != null) {
+ Optional extends TableSchema>> nestedSchema = getNestedSchema(context.tableSchema(), key);
+ if (nestedSchema != null && nestedSchema.isPresent()) {
+ itemToTransform.put(key, AttributeValue.builder().m(processNestedObject(value.m(), nestedSchema.get())).build());
+ }
+ }
+ });
+
return WriteModification.builder()
.transformedItem(Collections.unmodifiableMap(itemToTransform))
.build();
}
+ private Map processNestedObject(Map nestedMap, TableSchema> nestedSchema) {
+ Map updatedNestedMap = new HashMap<>(nestedMap);
+ Collection customMetadataObject = nestedSchema.tableMetadata()
+ .customMetadataObject(CUSTOM_METADATA_KEY, Collection.class).orElse(null);
+ for (Map.Entry entry : nestedMap.entrySet()) {
+ String nestedKey = entry.getKey();
+ AttributeValue nestedValue = entry.getValue();
+
+ if (nestedValue.hasM()) {
+ updatedNestedMap.put(nestedKey,
+ AttributeValue.builder().m(processNestedObject(nestedValue.m(), nestedSchema)).build());
+ } else if (nestedValue.hasL()) {
+ List updatedList = nestedValue.l().stream()
+ .map(listItem -> listItem.hasM() ?
+ AttributeValue.builder().m(processNestedObject(listItem.m(), nestedSchema)).build() : listItem)
+ .collect(Collectors.toList());
+ updatedNestedMap.put(nestedKey, AttributeValue.builder().l(updatedList).build());
+ } else {
+ AttributeConverter> converter = nestedSchema.converterForAttribute(nestedKey);
+ if (converter != null && customMetadataObject != null && customMetadataObject.contains(nestedKey)) {
+ insertTimestampInItemToTransform(updatedNestedMap, nestedKey, converter);
+ }
+ }
+ }
+ return updatedNestedMap;
+ }
+
+
private void insertTimestampInItemToTransform(Map itemToTransform,
String key,
AttributeConverter converter) {
itemToTransform.put(key, converter.transformFrom(clock.instant()));
}
+ private Optional extends TableSchema>> getNestedSchema(TableSchema> parentSchema, String attributeName) {
+ return parentSchema.converterForAttribute(attributeName).type().tableSchema();
+ }
+
/**
- * Builder for a {@link AutoGeneratedTimestampRecordExtension}
+ * Builder for a {@link AutoGeneratedTimestampRecordExtension}
*/
@NotThreadSafe
public static final class Builder {
@@ -160,9 +202,9 @@ private Builder() {
}
/**
- * Sets the clock instance , else Clock.systemUTC() is used by default.
- * Every time a new timestamp is generated this clock will be used to get the current point in time. If a custom clock
- * is not specified, the default system clock will be used.
+ * Sets the clock instance , else Clock.systemUTC() is used by default. Every time a new timestamp is generated this clock
+ * will be used to get the current point in time. If a custom clock is not specified, the default system clock will be
+ * used.
*
* @param clock Clock instance to set the current timestamp.
* @return This builder for method chaining.
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/UpdateBehaviorTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/UpdateBehaviorTest.java
index 196d38282277..a0a6ca0d723f 100644
--- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/UpdateBehaviorTest.java
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/UpdateBehaviorTest.java
@@ -3,13 +3,18 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import java.time.Clock;
+import java.time.Duration;
import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
import java.util.Collections;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
+import org.mockito.Mockito;
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient;
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable;
import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
@@ -31,22 +36,25 @@ public class UpdateBehaviorTest extends LocalDynamoDbSyncTestBase {
private static final Instant FAR_FUTURE_INSTANT = Instant.parse("9999-05-03T10:05:00Z");
private static final String TEST_BEHAVIOUR_ATTRIBUTE = "testBehaviourAttribute";
private static final String TEST_ATTRIBUTE = "testAttribute";
+ private static final Duration tolerance = Duration.ofSeconds(2);
+ public static final Instant MOCKED_INSTANT_NOW = Instant.now(Clock.fixed(Instant.parse("2025-01-13T14:00:00Z"),
+ ZoneOffset.UTC));
private static final TableSchema TABLE_SCHEMA =
- TableSchema.fromClass(RecordWithUpdateBehaviors.class);
-
+ TableSchema.fromClass(RecordWithUpdateBehaviors.class);
+
private static final TableSchema TABLE_SCHEMA_FLATTEN_RECORD =
TableSchema.fromClass(FlattenRecord.class);
private final DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder()
- .dynamoDbClient(getDynamoDbClient()).extensions(
+ .dynamoDbClient(getDynamoDbClient()).extensions(
Stream.concat(ExtensionResolver.defaultExtensions().stream(),
Stream.of(AutoGeneratedTimestampRecordExtension.create())).collect(Collectors.toList()))
- .build();
+ .build();
private final DynamoDbTable mappedTable =
- enhancedClient.table(getConcreteTableName("table-name"), TABLE_SCHEMA);
-
+ enhancedClient.table(getConcreteTableName("table-name"), TABLE_SCHEMA);
+
private final DynamoDbTable flattenedMappedTable =
enhancedClient.table(getConcreteTableName("table-name"), TABLE_SCHEMA_FLATTEN_RECORD);
@@ -63,10 +71,16 @@ public void deleteTable() {
@Test
public void updateBehaviors_firstUpdate() {
Instant currentTime = Instant.now();
+ NestedRecordWithUpdateBehavior nestedRecord = new NestedRecordWithUpdateBehavior();
+ nestedRecord.setId("id123");
+ nestedRecord.setNestedTimeAttribute(INSTANT_1);
+ nestedRecord.setNestedUpdateBehaviorAttribute(TEST_BEHAVIOUR_ATTRIBUTE);
+
RecordWithUpdateBehaviors record = new RecordWithUpdateBehaviors();
record.setId("id123");
record.setCreatedOn(INSTANT_1);
record.setLastUpdatedOn(INSTANT_2);
+ record.setNestedRecord(nestedRecord);
mappedTable.updateItem(record);
RecordWithUpdateBehaviors persistedRecord = mappedTable.getItem(record);
@@ -81,28 +95,55 @@ public void updateBehaviors_firstUpdate() {
assertThat(persistedRecord.getLastAutoUpdatedOnMillis().getEpochSecond()).isGreaterThanOrEqualTo(currentTime.getEpochSecond());
assertThat(persistedRecord.getCreatedAutoUpdateOn()).isAfterOrEqualTo(currentTime);
+ assertThat(persistedRecord.getNestedRecord().getNestedTimeAttribute()).isAfterOrEqualTo(currentTime);
+ assertThat(persistedRecord.getNestedRecord().getNestedUpdateBehaviorAttribute()).isEqualTo(TEST_BEHAVIOUR_ATTRIBUTE);
}
+
@Test
public void updateBehaviors_secondUpdate() {
Instant beforeUpdateInstant = Instant.now();
+
+ NestedRecordWithUpdateBehavior secondNestedRecord = new NestedRecordWithUpdateBehavior();
+ secondNestedRecord.setId("id123");
+ secondNestedRecord.setNestedUpdateBehaviorAttribute(TEST_BEHAVIOUR_ATTRIBUTE);
+
+ NestedRecordWithUpdateBehavior nestedRecord = new NestedRecordWithUpdateBehavior();
+ nestedRecord.setId("id123");
+ nestedRecord.setNestedUpdateBehaviorAttribute(TEST_BEHAVIOUR_ATTRIBUTE);
+ nestedRecord.setNestedRecord(secondNestedRecord);
+
RecordWithUpdateBehaviors record = new RecordWithUpdateBehaviors();
record.setId("id123");
record.setCreatedOn(INSTANT_1);
record.setLastUpdatedOn(INSTANT_2);
+ record.setNestedRecord(nestedRecord);
+
mappedTable.updateItem(record);
+
RecordWithUpdateBehaviors persistedRecord = mappedTable.getItem(record);
assertThat(persistedRecord.getVersion()).isEqualTo(1L);
+
Instant firstUpdatedTime = persistedRecord.getLastAutoUpdatedOn();
Instant createdAutoUpdateOn = persistedRecord.getCreatedAutoUpdateOn();
+ Instant nestedTimestamp = persistedRecord.getNestedRecord().getNestedTimeAttribute();
+ Instant secondNestedTimestamp = persistedRecord.getNestedRecord().getNestedRecord().getNestedTimeAttribute();
+
assertThat(firstUpdatedTime).isAfterOrEqualTo(beforeUpdateInstant);
assertThat(persistedRecord.getFormattedLastAutoUpdatedOn().getEpochSecond())
.isGreaterThanOrEqualTo(beforeUpdateInstant.getEpochSecond());
+ assertThat(persistedRecord.getNestedRecord().getNestedUpdateBehaviorAttribute()).isNotNull();
+ assertThat(persistedRecord.getNestedRecord().getNestedTimeAttribute().getEpochSecond())
+ .isGreaterThanOrEqualTo(beforeUpdateInstant.getEpochSecond());
+ assertThat(persistedRecord.getNestedRecord().getNestedRecord().getNestedTimeAttribute().getEpochSecond())
+ .isGreaterThanOrEqualTo(beforeUpdateInstant.getEpochSecond());
+
record.setVersion(1L);
record.setCreatedOn(INSTANT_2);
record.setLastUpdatedOn(INSTANT_2);
+ record.setNestedRecord(nestedRecord);
mappedTable.updateItem(record);
persistedRecord = mappedTable.getItem(record);
@@ -113,7 +154,13 @@ public void updateBehaviors_secondUpdate() {
Instant secondUpdatedTime = persistedRecord.getLastAutoUpdatedOn();
assertThat(secondUpdatedTime).isAfterOrEqualTo(firstUpdatedTime);
assertThat(persistedRecord.getCreatedAutoUpdateOn()).isEqualTo(createdAutoUpdateOn);
- }
+
+ Instant nestedFirstUpdatedTime = persistedRecord.getNestedRecord().getNestedTimeAttribute();
+ assertThat(nestedFirstUpdatedTime).isAfterOrEqualTo(nestedTimestamp);
+
+ Instant secondNestedFirstUpdatedTime = persistedRecord.getNestedRecord().getNestedRecord().getNestedTimeAttribute();
+ assertThat(secondNestedFirstUpdatedTime).isAfterOrEqualTo(secondNestedTimestamp);
+ };
@Test
public void updateBehaviors_removal() {
@@ -187,7 +234,7 @@ public void when_updatingNestedObjectWithSingleLevel_existingInformationIsPreser
RecordWithUpdateBehaviors persistedRecord = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id123")));
verifySingleLevelNestingTargetedUpdateBehavior(persistedRecord.getNestedRecord(), updatedNestedCounter,
- TEST_BEHAVIOUR_ATTRIBUTE, INSTANT_1);
+ TEST_BEHAVIOUR_ATTRIBUTE, Instant.now());
}
@Test
@@ -214,7 +261,7 @@ public void when_updatingNestedObjectWithSingleLevel_default_mode_update_newMapC
RecordWithUpdateBehaviors persistedRecord = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id123")));
- verifySingleLevelNestingTargetedUpdateBehavior(persistedRecord.getNestedRecord(), updatedNestedCounter, null, null);
+ verifySingleLevelNestingTargetedUpdateBehavior(persistedRecord.getNestedRecord(), updatedNestedCounter, null, Instant.now());
}
@Test
@@ -241,7 +288,7 @@ public void when_updatingNestedObjectWithSingleLevel_with_no_mode_update_newMapC
RecordWithUpdateBehaviors persistedRecord = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id123")));
- verifySingleLevelNestingTargetedUpdateBehavior(persistedRecord.getNestedRecord(), updatedNestedCounter, null, null);
+ verifySingleLevelNestingTargetedUpdateBehavior(persistedRecord.getNestedRecord(), updatedNestedCounter, null, Instant.now());
}
@Test
@@ -266,7 +313,7 @@ public void when_updatingNestedObjectToEmptyWithSingleLevel_existingInformationI
mappedTable.updateItem(r -> r.item(update_record).ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY));
RecordWithUpdateBehaviors persistedRecord = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id123")));
- assertThat(persistedRecord.getNestedRecord()).isNull();
+ assertThat(persistedRecord.getNestedRecord()).isNotNull();
}
private NestedRecordWithUpdateBehavior createNestedWithDefaults(String id, Long counter) {
@@ -292,16 +339,18 @@ private void verifyMultipleLevelNestingTargetedUpdateBehavior(NestedRecordWithUp
assertThat(nestedRecord.getNestedRecord().getNestedCounter()).isEqualTo(updatedInnerNestedCounter);
assertThat(nestedRecord.getNestedRecord().getNestedUpdateBehaviorAttribute()).isEqualTo(
test_behav_attribute);
- assertThat(nestedRecord.getNestedRecord().getNestedTimeAttribute()).isEqualTo(expected_time);
+ assertThat(areInstantsAlmostEqual(nestedRecord.getNestedTimeAttribute(), expected_time, tolerance)).isTrue();
}
private void verifySingleLevelNestingTargetedUpdateBehavior(NestedRecordWithUpdateBehavior nestedRecord,
- long updatedNestedCounter, String expected_behav_attr,
+ long updatedNestedCounter, String expected_behav_attr,
Instant expected_time) {
assertThat(nestedRecord).isNotNull();
assertThat(nestedRecord.getNestedCounter()).isEqualTo(updatedNestedCounter);
assertThat(nestedRecord.getNestedUpdateBehaviorAttribute()).isEqualTo(expected_behav_attr);
- assertThat(nestedRecord.getNestedTimeAttribute()).isEqualTo(expected_time);
+ assertThat(nestedRecord.getNestedTimeAttribute()).isNotNull();
+ assertThat(areInstantsAlmostEqual(nestedRecord.getNestedTimeAttribute(), expected_time,
+ tolerance)).isTrue();
}
@Test
@@ -337,7 +386,7 @@ public void when_updatingNestedObjectWithMultipleLevels_inScalarOnlyMode_existin
RecordWithUpdateBehaviors persistedRecord = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id123")));
verifyMultipleLevelNestingTargetedUpdateBehavior(persistedRecord.getNestedRecord(), outerNestedCounter,
- innerNestedCounter, TEST_BEHAVIOUR_ATTRIBUTE, INSTANT_1);
+ innerNestedCounter, TEST_BEHAVIOUR_ATTRIBUTE, Instant.now());
}
@Test
@@ -368,7 +417,7 @@ public void when_updatingNestedObjectWithMultipleLevels_inMapsOnlyMode_existingI
RecordWithUpdateBehaviors persistedRecord = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id123")));
verifyMultipleLevelNestingTargetedUpdateBehavior(persistedRecord.getNestedRecord(), outerNestedCounter,
- 50L, TEST_BEHAVIOUR_ATTRIBUTE, INSTANT_1);
+ 50L, TEST_BEHAVIOUR_ATTRIBUTE, Instant.now());
}
@Test
@@ -403,8 +452,9 @@ public void when_updatingNestedObjectWithMultipleLevels_default_mode_existingInf
RecordWithUpdateBehaviors persistedRecord = mappedTable.getItem(r -> r.key(k -> k.partitionValue("id123")));
- verifyMultipleLevelNestingTargetedUpdateBehavior(persistedRecord.getNestedRecord(), outerNestedCounter, innerNestedCounter, null,
- null);
+ verifyMultipleLevelNestingTargetedUpdateBehavior(persistedRecord.getNestedRecord(), outerNestedCounter,
+ innerNestedCounter, null,
+ Instant.now());
}
@Test
@@ -449,7 +499,7 @@ public void when_updatingNestedMap_mapsOnlyMode_newMapIsCreatedAndStored() {
mappedTable.updateItem(r -> r.item(update_record).ignoreNullsMode(IgnoreNullsMode.MAPS_ONLY));
verifySingleLevelNestingTargetedUpdateBehavior(persistedRecord.getNestedRecord(), 5L, TEST_BEHAVIOUR_ATTRIBUTE,
- INSTANT_1);
+ Instant.now());
assertThat(persistedRecord.getNestedRecord().getAttribute()).isEqualTo(TEST_ATTRIBUTE);
}
@@ -471,14 +521,15 @@ public void when_emptyNestedRecordIsSet_emptyMapIsStoredInTable() {
assertThat(getItemResponse.item().get("nestedRecord")).isNotNull();
assertThat(getItemResponse.item().get("nestedRecord").toString()).isEqualTo("AttributeValue(M={nestedTimeAttribute"
- + "=AttributeValue(NUL=true), "
- + "nestedRecord=AttributeValue(NUL=true), "
- + "attribute=AttributeValue(NUL=true), "
- + "id=AttributeValue(NUL=true), "
- + "nestedUpdateBehaviorAttribute=AttributeValue"
- + "(NUL=true), nestedCounter=AttributeValue"
- + "(NUL=true), nestedVersionedAttribute"
- + "=AttributeValue(NUL=true)})");
+ + "=AttributeValue(NUL=true), "
+ + "nestedRecord=AttributeValue(NUL=true), "
+ + "attribute=AttributeValue(NUL=true), "
+ + "id=AttributeValue(NUL=true), "
+ + "nestedUpdateBehaviorAttribute"
+ + "=AttributeValue"
+ + "(NUL=true), nestedCounter=AttributeValue"
+ + "(NUL=true), nestedVersionedAttribute"
+ + "=AttributeValue(NUL=true)})");
}
@@ -493,15 +544,15 @@ public void when_updatingNestedObjectWithSingleLevelFlattened_existingInformatio
FlattenRecord flattenRecord = new FlattenRecord();
flattenRecord.setCompositeRecord(compositeRecord);
flattenRecord.setId("id456");
-
+
flattenedMappedTable.putItem(r -> r.item(flattenRecord));
-
+
NestedRecordWithUpdateBehavior updateNestedRecord = new NestedRecordWithUpdateBehavior();
updateNestedRecord.setNestedCounter(100L);
-
+
CompositeRecord updateCompositeRecord = new CompositeRecord();
updateCompositeRecord.setNestedRecord(updateNestedRecord);
-
+
FlattenRecord updatedFlattenRecord = new FlattenRecord();
updatedFlattenRecord.setId("id456");
updatedFlattenRecord.setCompositeRecord(updateCompositeRecord);
@@ -511,11 +562,10 @@ public void when_updatingNestedObjectWithSingleLevelFlattened_existingInformatio
assertThat(persistedFlattenedRecord.getCompositeRecord()).isNotNull();
verifySingleLevelNestingTargetedUpdateBehavior(persistedFlattenedRecord.getCompositeRecord().getNestedRecord(), 100L,
- TEST_BEHAVIOUR_ATTRIBUTE, INSTANT_1);
+ TEST_BEHAVIOUR_ATTRIBUTE, Instant.now());
}
-
@Test
public void when_updatingNestedObjectWithMultipleLevelFlattened_existingInformationIsPreserved_scalar_only_update() {
@@ -529,30 +579,30 @@ public void when_updatingNestedObjectWithMultipleLevelFlattened_existingInformat
FlattenRecord flattenRecord = new FlattenRecord();
flattenRecord.setCompositeRecord(compositeRecord);
flattenRecord.setId("id789");
-
+
flattenedMappedTable.putItem(r -> r.item(flattenRecord));
-
+
NestedRecordWithUpdateBehavior updateOuterNestedRecord = new NestedRecordWithUpdateBehavior();
updateOuterNestedRecord.setNestedCounter(100L);
-
+
NestedRecordWithUpdateBehavior updateInnerNestedRecord = new NestedRecordWithUpdateBehavior();
updateInnerNestedRecord.setNestedCounter(50L);
-
+
updateOuterNestedRecord.setNestedRecord(updateInnerNestedRecord);
-
+
CompositeRecord updateCompositeRecord = new CompositeRecord();
updateCompositeRecord.setNestedRecord(updateOuterNestedRecord);
-
+
FlattenRecord updateFlattenRecord = new FlattenRecord();
updateFlattenRecord.setCompositeRecord(updateCompositeRecord);
updateFlattenRecord.setId("id789");
-
+
FlattenRecord persistedFlattenedRecord =
flattenedMappedTable.updateItem(r -> r.item(updateFlattenRecord).ignoreNullsMode(IgnoreNullsMode.SCALAR_ONLY));
-
+
assertThat(persistedFlattenedRecord.getCompositeRecord()).isNotNull();
verifyMultipleLevelNestingTargetedUpdateBehavior(persistedFlattenedRecord.getCompositeRecord().getNestedRecord(), 100L,
- 50L, TEST_BEHAVIOUR_ATTRIBUTE, INSTANT_1);
+ 50L, TEST_BEHAVIOUR_ATTRIBUTE, Instant.now());
assertThat(persistedFlattenedRecord.getCompositeRecord().getNestedRecord().getNestedCounter()).isEqualTo(100L);
assertThat(persistedFlattenedRecord.getCompositeRecord().getNestedRecord().getNestedRecord().getNestedCounter()).isEqualTo(50L);
}
@@ -579,6 +629,12 @@ public void updateBehaviors_nested() {
assertThat(persistedRecord.getNestedRecord().getNestedVersionedAttribute()).isNull();
assertThat(persistedRecord.getNestedRecord().getNestedCounter()).isNull();
assertThat(persistedRecord.getNestedRecord().getNestedUpdateBehaviorAttribute()).isNull();
- assertThat(persistedRecord.getNestedRecord().getNestedTimeAttribute()).isNull();
+ assertThat(areInstantsAlmostEqual(persistedRecord.getNestedRecord().getNestedTimeAttribute(), Instant.now(),
+ tolerance)).isTrue();
+ }
+
+ public static boolean areInstantsAlmostEqual(Instant instant1, Instant instant2, Duration tolerance) {
+ Duration difference = Duration.between(instant1, instant2).abs();
+ return difference.compareTo(tolerance) <= 0;
}
}
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/NestedRecordWithUpdateBehavior.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/NestedRecordWithUpdateBehavior.java
index 883a89813c1a..03d90afef6fa 100644
--- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/NestedRecordWithUpdateBehavior.java
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/NestedRecordWithUpdateBehavior.java
@@ -18,10 +18,12 @@
import static software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior.WRITE_IF_NOT_EXISTS;
import java.time.Instant;
+import software.amazon.awssdk.enhanced.dynamodb.converters.EpochMillisFormatTestConverter;
import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAtomicCounter;
import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedTimestampAttribute;
import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbVersionAttribute;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbConvertedBy;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbUpdateBehavior;
@@ -62,6 +64,7 @@ public void setNestedVersionedAttribute(Long nestedVersionedAttribute) {
this.nestedVersionedAttribute = nestedVersionedAttribute;
}
+ @DynamoDbConvertedBy(EpochMillisFormatTestConverter.class)
@DynamoDbAutoGeneratedTimestampAttribute
public Instant getNestedTimeAttribute() {
return nestedTimeAttribute;
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/RecordWithUpdateBehaviors.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/RecordWithUpdateBehaviors.java
index 8bd874fee002..bccf5bdd39a6 100644
--- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/RecordWithUpdateBehaviors.java
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/RecordWithUpdateBehaviors.java
@@ -24,6 +24,7 @@
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbConvertedBy;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey;
+import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSortKey;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbUpdateBehavior;
import static software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior.WRITE_IF_NOT_EXISTS;
From f32d3a5a07a66bb46c612070667d8a04870c7396 Mon Sep 17 00:00:00 2001
From: ghetelgb
Date: Fri, 14 Feb 2025 15:20:41 +0200
Subject: [PATCH 2/5] Fixed failing tests and formatting
---
...AutoGeneratedTimestampRecordExtension.java | 62 ++++++++++---------
.../functionaltests/UpdateBehaviorTest.java | 24 +++----
.../NestedRecordWithUpdateBehavior.java | 1 -
.../models/RecordWithUpdateBehaviors.java | 1 -
4 files changed, 45 insertions(+), 43 deletions(-)
diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedTimestampRecordExtension.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedTimestampRecordExtension.java
index e0076247a1c7..ca3954819472 100644
--- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedTimestampRecordExtension.java
+++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedTimestampRecordExtension.java
@@ -41,33 +41,34 @@
import software.amazon.awssdk.utils.Validate;
/**
- * This extension enables selected attributes to be automatically updated with a current timestamp every time they are written to
- * the database.
+ * This extension enables selected attributes to be automatically updated with a current timestamp every time they are written
+ * to the database.
*
- * This extension is not loaded by default when you instantiate a
- * {@link software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient}. Thus you need to specify it in custom extension while
- * creating the enhanced client.
- *
- * Example to add AutoGeneratedTimestampRecordExtension along with default extensions is
- * DynamoDbEnhancedClient.builder().extensions(Stream.concat(ExtensionResolver.defaultExtensions().stream(),
- * Stream.of(AutoGeneratedTimestampRecordExtension.create())).collect(Collectors.toList())).build();
- *
- *
- * Example to just add AutoGeneratedTimestampRecordExtension without default extensions is
- * DynamoDbEnhancedClient.builder().extensions(AutoGeneratedTimestampRecordExtension.create())).build();
- *
+ * This extension is not loaded by default when you instantiate a
+ * {@link software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient}. Thus you need to specify it in custom extension
+ * while creating the enhanced client.
+ *
+ * Example to add AutoGeneratedTimestampRecordExtension along with default extensions is
+ * DynamoDbEnhancedClient.builder().extensions(Stream.concat(ExtensionResolver.defaultExtensions().stream(),
+ * Stream.of(AutoGeneratedTimestampRecordExtension.create())).collect(Collectors.toList())).build();
+ *
+ *
+ * Example to just add AutoGeneratedTimestampRecordExtension without default extensions is
+ * DynamoDbEnhancedClient.builder().extensions(AutoGeneratedTimestampRecordExtension.create())).build();
+ *
*
*
- * To utilize auto generated timestamp update, first create a field in your model that will be used to store the record timestamp
- * of modification. This class field must be an {@link Instant} Class type, and you need to tag it as the
- * autoGeneratedTimeStampAttribute. If you are using the {@link software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchema}
- * then you should use the
- * {@link software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedTimestampAttribute} annotation,
- * otherwise if you are using the {@link software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema} then you should use
- * the {@link AttributeTags#autoGeneratedTimestampAttribute()} static attribute tag.
+ * To utilize auto generated timestamp update, first create a field in your model that will be used to store the record
+ * timestamp of modification. This class field must be an {@link Instant} Class type, and you need to tag it as the
+ * autoGeneratedTimeStampAttribute. If you are using the
+ * {@link software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchema}
+ * then you should use the
+ * {@link software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedTimestampAttribute}
+ * annotation, otherwise if you are using the {@link software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema}
+ * then you should use the {@link AttributeTags#autoGeneratedTimestampAttribute()} static attribute tag.
*
- * Every time a new update of the record is successfully written to the database, the timestamp at which it was modified will be
- * automatically updated. This extension applies the conversions as defined in the attribute convertor.
+ * Every time a new update of the record is successfully written to the database, the timestamp at which it was modified will
+ * be automatically updated. This extension applies the conversions as defined in the attribute convertor.
*/
@SdkPublicApi
@ThreadSafe
@@ -91,7 +92,6 @@ private AttributeTags() {
/**
* Tags which indicate that the given attribute is supported wih Auto Generated Timestamp Record Extension.
- *
* @return Tag name for AutoGenerated Timestamp Records
*/
public static StaticAttributeTag autoGeneratedTimestampAttribute() {
@@ -105,7 +105,6 @@ private AutoGeneratedTimestampRecordExtension(Builder builder) {
/**
* Create a builder that can be used to create a {@link AutoGeneratedTimestampRecordExtension}.
- *
* @return Builder to create AutoGeneratedTimestampRecordExtension,
*/
public static Builder builder() {
@@ -136,6 +135,11 @@ public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite contex
.customMetadataObject(CUSTOM_METADATA_KEY, Collection.class).orElse(null);
Map itemToTransform = new HashMap<>(context.items());
+ if (customMetadataObject != null) {
+ customMetadataObject.forEach(
+ key -> insertTimestampInItemToTransform(itemToTransform, key.toString(),
+ context.tableSchema().converterForAttribute(key)));
+ }
itemToTransform.forEach((key, value) -> {
if (customMetadataObject != null && customMetadataObject.contains(key)) {
insertTimestampInItemToTransform(itemToTransform, key, context.tableSchema().converterForAttribute(key));
@@ -191,7 +195,7 @@ private Optional extends TableSchema>> getNestedSchema(TableSchema> parent
}
/**
- * Builder for a {@link AutoGeneratedTimestampRecordExtension}
+ * Builder for a {@link AutoGeneratedTimestampRecordExtension}
*/
@NotThreadSafe
public static final class Builder {
@@ -202,9 +206,9 @@ private Builder() {
}
/**
- * Sets the clock instance , else Clock.systemUTC() is used by default. Every time a new timestamp is generated this clock
- * will be used to get the current point in time. If a custom clock is not specified, the default system clock will be
- * used.
+ * Sets the clock instance , else Clock.systemUTC() is used by default.
+ * Every time a new timestamp is generated this clock will be used to get the current point in time. If a custom clock
+ * is not specified, the default system clock will be used.
*
* @param clock Clock instance to set the current timestamp.
* @return This builder for method chaining.
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/UpdateBehaviorTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/UpdateBehaviorTest.java
index a0a6ca0d723f..27bc23408651 100644
--- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/UpdateBehaviorTest.java
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/UpdateBehaviorTest.java
@@ -6,15 +6,14 @@
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
-import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.Collections;
+import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
-import org.mockito.Mockito;
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient;
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable;
import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
@@ -520,16 +519,16 @@ public void when_emptyNestedRecordIsSet_emptyMapIsStoredInTable() {
.build());
assertThat(getItemResponse.item().get("nestedRecord")).isNotNull();
- assertThat(getItemResponse.item().get("nestedRecord").toString()).isEqualTo("AttributeValue(M={nestedTimeAttribute"
- + "=AttributeValue(NUL=true), "
- + "nestedRecord=AttributeValue(NUL=true), "
- + "attribute=AttributeValue(NUL=true), "
- + "id=AttributeValue(NUL=true), "
- + "nestedUpdateBehaviorAttribute"
- + "=AttributeValue"
- + "(NUL=true), nestedCounter=AttributeValue"
- + "(NUL=true), nestedVersionedAttribute"
- + "=AttributeValue(NUL=true)})");
+
+ Map item = getItemResponse.item().get("nestedRecord").m();
+ Instant nestedTime = Instant.parse(item.get("nestedTimeAttribute").s());
+ assertThat(areInstantsAlmostEqual(nestedTime, Instant.now(), tolerance)).isTrue();
+ assertThat(item.get("nestedUpdateBehaviorAttribute").s()).isNull();
+ assertThat(item.get("nestedCounter").n()).isNull();
+ assertThat(item.get("nestedVersionedAttribute").n()).isNull();
+ assertThat(item.get("nestedRecord").m()).isEmpty();
+ assertThat(item.get("attribute").s()).isNull();
+ assertThat(item.get("id").s()).isNull();
}
@@ -566,6 +565,7 @@ public void when_updatingNestedObjectWithSingleLevelFlattened_existingInformatio
}
+
@Test
public void when_updatingNestedObjectWithMultipleLevelFlattened_existingInformationIsPreserved_scalar_only_update() {
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/NestedRecordWithUpdateBehavior.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/NestedRecordWithUpdateBehavior.java
index 03d90afef6fa..c194152ba2d3 100644
--- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/NestedRecordWithUpdateBehavior.java
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/NestedRecordWithUpdateBehavior.java
@@ -64,7 +64,6 @@ public void setNestedVersionedAttribute(Long nestedVersionedAttribute) {
this.nestedVersionedAttribute = nestedVersionedAttribute;
}
- @DynamoDbConvertedBy(EpochMillisFormatTestConverter.class)
@DynamoDbAutoGeneratedTimestampAttribute
public Instant getNestedTimeAttribute() {
return nestedTimeAttribute;
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/RecordWithUpdateBehaviors.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/RecordWithUpdateBehaviors.java
index bccf5bdd39a6..8bd874fee002 100644
--- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/RecordWithUpdateBehaviors.java
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/RecordWithUpdateBehaviors.java
@@ -24,7 +24,6 @@
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbConvertedBy;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey;
-import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSortKey;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbUpdateBehavior;
import static software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior.WRITE_IF_NOT_EXISTS;
From 4ce5572e33a838b6e4a63493a27aad7a3d61eebb Mon Sep 17 00:00:00 2001
From: ghetelgb
Date: Tue, 18 Feb 2025 10:24:01 +0200
Subject: [PATCH 3/5] Code refactor, added initial logic for UUIT extension
---
...AutoGeneratedTimestampRecordExtension.java | 8 ++--
.../AutoGeneratedUuidExtension.java | 46 +++++++++++++++++++
2 files changed, 50 insertions(+), 4 deletions(-)
diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedTimestampRecordExtension.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedTimestampRecordExtension.java
index ca3954819472..6ef66335a3af 100644
--- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedTimestampRecordExtension.java
+++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedTimestampRecordExtension.java
@@ -34,7 +34,6 @@
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbExtensionContext;
import software.amazon.awssdk.enhanced.dynamodb.EnhancedType;
import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
-import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedTimestampAttribute;
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTag;
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableMetadata;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
@@ -141,9 +140,7 @@ public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite contex
context.tableSchema().converterForAttribute(key)));
}
itemToTransform.forEach((key, value) -> {
- if (customMetadataObject != null && customMetadataObject.contains(key)) {
- insertTimestampInItemToTransform(itemToTransform, key, context.tableSchema().converterForAttribute(key));
- } else if (value.hasM() && value.m() != null) {
+ if (value.hasM() && value.m() != null) {
Optional extends TableSchema>> nestedSchema = getNestedSchema(context.tableSchema(), key);
if (nestedSchema != null && nestedSchema.isPresent()) {
itemToTransform.put(key, AttributeValue.builder().m(processNestedObject(value.m(), nestedSchema.get())).build());
@@ -151,6 +148,9 @@ public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite contex
}
});
+ if (itemToTransform.isEmpty()) {
+ return WriteModification.builder().build();
+ }
return WriteModification.builder()
.transformedItem(Collections.unmodifiableMap(itemToTransform))
.build();
diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtension.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtension.java
index d92db8c60bbd..f60acd43886b 100644
--- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtension.java
+++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtension.java
@@ -18,15 +18,20 @@
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.UUID;
import java.util.function.Consumer;
+import java.util.stream.Collectors;
import software.amazon.awssdk.annotations.SdkPublicApi;
import software.amazon.awssdk.annotations.ThreadSafe;
+import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter;
import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType;
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClientExtension;
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbExtensionContext;
import software.amazon.awssdk.enhanced.dynamodb.EnhancedType;
+import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTag;
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableMetadata;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbUpdateBehavior;
@@ -111,11 +116,52 @@ public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite contex
Map itemToTransform = new HashMap<>(context.items());
customMetadataObject.forEach(key -> insertUuidInItemToTransform(itemToTransform, key));
+ itemToTransform.forEach((key, value) -> {
+ if (customMetadataObject != null && customMetadataObject.contains(key)) {
+ insertUuidInItemToTransform(itemToTransform, key);
+ } else if (value.hasM() && value.m() != null) {
+ Optional extends TableSchema>> nestedSchema = getNestedSchema(context.tableSchema(), key);
+ if (nestedSchema != null && nestedSchema.isPresent()) {
+ itemToTransform.put(key, AttributeValue.builder().m(processNestedObject(value.m(), nestedSchema.get())).build());
+ }
+ }
+ });
return WriteModification.builder()
.transformedItem(Collections.unmodifiableMap(itemToTransform))
.build();
}
+ private Map processNestedObject(Map nestedMap, TableSchema> nestedSchema) {
+ Map updatedNestedMap = new HashMap<>(nestedMap);
+ Collection customMetadataObject = nestedSchema.tableMetadata()
+ .customMetadataObject(CUSTOM_METADATA_KEY, Collection.class).orElse(null);
+ for (Map.Entry entry : nestedMap.entrySet()) {
+ String nestedKey = entry.getKey();
+ AttributeValue nestedValue = entry.getValue();
+
+ if (nestedValue.hasM()) {
+ updatedNestedMap.put(nestedKey,
+ AttributeValue.builder().m(processNestedObject(nestedValue.m(), nestedSchema)).build());
+ } else if (nestedValue.hasL()) {
+ List updatedList = nestedValue.l().stream()
+ .map(listItem -> listItem.hasM() ?
+ AttributeValue.builder().m(processNestedObject(listItem.m(), nestedSchema)).build() : listItem)
+ .collect(Collectors.toList());
+ updatedNestedMap.put(nestedKey, AttributeValue.builder().l(updatedList).build());
+ } else {
+ AttributeConverter> converter = nestedSchema.converterForAttribute(nestedKey);
+ if (converter != null && customMetadataObject != null && customMetadataObject.contains(nestedKey)) {
+ insertUuidInItemToTransform(updatedNestedMap, nestedKey);
+ }
+ }
+ }
+ return updatedNestedMap;
+ }
+
+ private Optional extends TableSchema>> getNestedSchema(TableSchema> parentSchema, String attributeName) {
+ return parentSchema.converterForAttribute(attributeName).type().tableSchema();
+ }
+
private void insertUuidInItemToTransform(Map itemToTransform,
String key) {
itemToTransform.put(key, AttributeValue.builder().s(UUID.randomUUID().toString()).build());
From 225d6b57be8e343343c0cb06544f81f7848a5095 Mon Sep 17 00:00:00 2001
From: ghetelgb
Date: Fri, 14 Mar 2025 13:57:17 +0200
Subject: [PATCH 4/5] Removed changes for UUID implementation, as per story
split.
---
.../AutoGeneratedUuidExtension.java | 46 -------------------
.../NestedRecordWithUpdateBehavior.java | 2 -
2 files changed, 48 deletions(-)
diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtension.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtension.java
index f60acd43886b..d92db8c60bbd 100644
--- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtension.java
+++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedUuidExtension.java
@@ -18,20 +18,15 @@
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
-import java.util.List;
import java.util.Map;
-import java.util.Optional;
import java.util.UUID;
import java.util.function.Consumer;
-import java.util.stream.Collectors;
import software.amazon.awssdk.annotations.SdkPublicApi;
import software.amazon.awssdk.annotations.ThreadSafe;
-import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter;
import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType;
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClientExtension;
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbExtensionContext;
import software.amazon.awssdk.enhanced.dynamodb.EnhancedType;
-import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTag;
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableMetadata;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbUpdateBehavior;
@@ -116,52 +111,11 @@ public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite contex
Map itemToTransform = new HashMap<>(context.items());
customMetadataObject.forEach(key -> insertUuidInItemToTransform(itemToTransform, key));
- itemToTransform.forEach((key, value) -> {
- if (customMetadataObject != null && customMetadataObject.contains(key)) {
- insertUuidInItemToTransform(itemToTransform, key);
- } else if (value.hasM() && value.m() != null) {
- Optional extends TableSchema>> nestedSchema = getNestedSchema(context.tableSchema(), key);
- if (nestedSchema != null && nestedSchema.isPresent()) {
- itemToTransform.put(key, AttributeValue.builder().m(processNestedObject(value.m(), nestedSchema.get())).build());
- }
- }
- });
return WriteModification.builder()
.transformedItem(Collections.unmodifiableMap(itemToTransform))
.build();
}
- private Map processNestedObject(Map nestedMap, TableSchema> nestedSchema) {
- Map updatedNestedMap = new HashMap<>(nestedMap);
- Collection customMetadataObject = nestedSchema.tableMetadata()
- .customMetadataObject(CUSTOM_METADATA_KEY, Collection.class).orElse(null);
- for (Map.Entry entry : nestedMap.entrySet()) {
- String nestedKey = entry.getKey();
- AttributeValue nestedValue = entry.getValue();
-
- if (nestedValue.hasM()) {
- updatedNestedMap.put(nestedKey,
- AttributeValue.builder().m(processNestedObject(nestedValue.m(), nestedSchema)).build());
- } else if (nestedValue.hasL()) {
- List updatedList = nestedValue.l().stream()
- .map(listItem -> listItem.hasM() ?
- AttributeValue.builder().m(processNestedObject(listItem.m(), nestedSchema)).build() : listItem)
- .collect(Collectors.toList());
- updatedNestedMap.put(nestedKey, AttributeValue.builder().l(updatedList).build());
- } else {
- AttributeConverter> converter = nestedSchema.converterForAttribute(nestedKey);
- if (converter != null && customMetadataObject != null && customMetadataObject.contains(nestedKey)) {
- insertUuidInItemToTransform(updatedNestedMap, nestedKey);
- }
- }
- }
- return updatedNestedMap;
- }
-
- private Optional extends TableSchema>> getNestedSchema(TableSchema> parentSchema, String attributeName) {
- return parentSchema.converterForAttribute(attributeName).type().tableSchema();
- }
-
private void insertUuidInItemToTransform(Map itemToTransform,
String key) {
itemToTransform.put(key, AttributeValue.builder().s(UUID.randomUUID().toString()).build());
diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/NestedRecordWithUpdateBehavior.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/NestedRecordWithUpdateBehavior.java
index c194152ba2d3..883a89813c1a 100644
--- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/NestedRecordWithUpdateBehavior.java
+++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/NestedRecordWithUpdateBehavior.java
@@ -18,12 +18,10 @@
import static software.amazon.awssdk.enhanced.dynamodb.mapper.UpdateBehavior.WRITE_IF_NOT_EXISTS;
import java.time.Instant;
-import software.amazon.awssdk.enhanced.dynamodb.converters.EpochMillisFormatTestConverter;
import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAtomicCounter;
import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbAutoGeneratedTimestampAttribute;
import software.amazon.awssdk.enhanced.dynamodb.extensions.annotations.DynamoDbVersionAttribute;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean;
-import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbConvertedBy;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbUpdateBehavior;
From a9f483b95c02e9bab7e168127a8fbf1c5a04b0f4 Mon Sep 17 00:00:00 2001
From: ghetelgb
Date: Fri, 14 Mar 2025 16:08:07 +0200
Subject: [PATCH 5/5] new-version update
---
.../feature-AmazonDynamoDBEnhancedClient-381e9b3.json | 6 ++++++
1 file changed, 6 insertions(+)
create mode 100644 .changes/next-release/feature-AmazonDynamoDBEnhancedClient-381e9b3.json
diff --git a/.changes/next-release/feature-AmazonDynamoDBEnhancedClient-381e9b3.json b/.changes/next-release/feature-AmazonDynamoDBEnhancedClient-381e9b3.json
new file mode 100644
index 000000000000..a20d7d973a69
--- /dev/null
+++ b/.changes/next-release/feature-AmazonDynamoDBEnhancedClient-381e9b3.json
@@ -0,0 +1,6 @@
+{
+ "type": "feature",
+ "category": "Amazon DynamoDB Enhanced Client",
+ "contributor": "",
+ "description": "Added support for DynamoDbAutoGeneratedTimestampAttribute annotation in nested objects."
+}