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> 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> 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> 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> 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> 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> 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> 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> 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." +}