-
Notifications
You must be signed in to change notification settings - Fork 996
Added support for DynamoDbAutoGeneratedKey annotation #6373
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
anasatirbasa
wants to merge
26
commits into
aws:master
Choose a base branch
from
anasatirbasa:feature/define-dynamo-db-autogenerated-key-annotation
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
7cec4a2
Added support for DynamoDbAutoGeneratedKey annotation
anasatirbasa 8b80b18
Merge branch 'master' into feature/define-dynamo-db-autogenerated-key…
anasatirbasa d615605
Added support for DynamoDbAutoGeneratedKey annotation
anasatirbasa da14ad5
Merge branch 'master' into feature/define-dynamo-db-autogenerated-key…
anasatirbasa 031ef44
Added support for DynamoDbAutoGeneratedKey annotation
anasatirbasa a47b1d7
Merge branch 'master' into feature/define-dynamo-db-autogenerated-key…
anasatirbasa 0a29161
Merge branch 'master' into feature/define-dynamo-db-autogenerated-key…
anasatirbasa 1bf7a3a
Merge branch 'aws:master' into feature/define-dynamo-db-autogenerated…
anasatirbasa 8c4826d
Increased unit and integration test coverage to 100%
anasatirbasa 8a3048f
Merge branch 'master' into feature/define-dynamo-db-autogenerated-key…
anasatirbasa 79ba90f
Increased code coverage
anasatirbasa 459fae8
Merge branch 'master' into feature/define-dynamo-db-autogenerated-key…
anasatirbasa 8ae8405
Tests refactoring
anasatirbasa 08012f9
Tests refactoring
anasatirbasa 67cd161
Addressed PR feedback and refactored tests
anasatirbasa a4bfb1e
Addressed PR feedback
anasatirbasa fd877c7
Merge branch 'master' into feature/define-dynamo-db-autogenerated-key…
anasatirbasa bbd03ac
Added tests with composite gsi
anasatirbasa e654ef5
Added tests with composite gsi
anasatirbasa 8c3d7d1
Added tests with composite gsi
anasatirbasa 6d66b77
Added tests with composite gsi
anasatirbasa 5bad65a
Merge branch 'master' into feature/define-dynamo-db-autogenerated-key…
anasatirbasa b935acf
Fixed assertions on generated uuids
anasatirbasa 45c4000
Merge remote-tracking branch 'origin/feature/define-dynamo-db-autogen…
anasatirbasa 407703e
Merge branch 'master' into feature/define-dynamo-db-autogenerated-key…
anasatirbasa 9a44ed2
Merge branch 'master' into feature/define-dynamo-db-autogenerated-key…
anasatirbasa File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
6 changes: 6 additions & 0 deletions
6
.changes/next-release/feature-AmazonDynamoDBEnhancedClient-cbcc2bb.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| { | ||
| "type": "feature", | ||
| "category": "Amazon DynamoDB Enhanced Client", | ||
| "contributor": "", | ||
| "description": "Added support for DynamoDbAutoGeneratedKey annotation" | ||
| } |
254 changes: 254 additions & 0 deletions
254
...n/java/software/amazon/awssdk/enhanced/dynamodb/extensions/AutoGeneratedKeyExtension.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,254 @@ | ||
| /* | ||
| * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"). | ||
| * You may not use this file except in compliance with the License. | ||
| * A copy of the License is located at | ||
| * | ||
| * http://aws.amazon.com/apache2.0 | ||
| * | ||
| * or in the "license" file accompanying this file. This file is distributed | ||
| * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either | ||
| * express or implied. See the License for the specific language governing | ||
| * permissions and limitations under the License. | ||
| */ | ||
|
|
||
| package software.amazon.awssdk.enhanced.dynamodb.extensions; | ||
|
|
||
| import static java.util.Collections.newSetFromMap; | ||
|
|
||
| import java.util.Collection; | ||
| import java.util.Collections; | ||
| import java.util.HashMap; | ||
| import java.util.HashSet; | ||
| import java.util.Map; | ||
| import java.util.Objects; | ||
| import java.util.Set; | ||
| import java.util.UUID; | ||
| import java.util.concurrent.ConcurrentHashMap; | ||
| import java.util.function.Consumer; | ||
| import software.amazon.awssdk.annotations.SdkPublicApi; | ||
| import software.amazon.awssdk.annotations.ThreadSafe; | ||
| 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.IndexMetadata; | ||
| import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; | ||
| 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.StringUtils; | ||
| import software.amazon.awssdk.utils.Validate; | ||
|
|
||
| /** | ||
| * Generates a random UUID (via {@link java.util.UUID#randomUUID()}) for attributes tagged with {@code @DynamoDbAutoGeneratedKey} | ||
| * when the attribute value is missing or empty during write operations (put, update, batch write, or transact write). | ||
| * | ||
| * <p><b>Difference from {@code @DynamoDbAutoGeneratedUuid}:</b> | ||
| * This extension generates a UUID only when the attribute is null or empty, preserving existing values. In contrast, | ||
| * {@code @DynamoDbAutoGeneratedUuid} always generates a new UUID regardless of the current value. | ||
| * | ||
| * <p><b>Conflict Detection:</b> | ||
| * {@code @DynamoDbAutoGeneratedKey} and {@code @DynamoDbAutoGeneratedUuid} cannot be applied to the same attribute. If both | ||
| * annotations are present, an {@link IllegalArgumentException} is thrown during schema validation. | ||
| * | ||
| * <p><b>Supported Attributes:</b> | ||
| * The annotation may only be applied to key attributes: | ||
| * <ul> | ||
| * <li>Primary partition key (PK)</li> | ||
| * <li>Primary sort key (SK)</li> | ||
| * <li>Partition or sort keys of secondary indexes (GSI or LSI)</li> | ||
| * </ul> | ||
| * | ||
| * <p><b>Validation Behavior:</b> | ||
| * Annotation conflict detection and key-placement validation are performed once per {@link TableMetadata} | ||
| * instance and cached to avoid repeated validation on subsequent writes. | ||
| * | ||
| * <p><b>UpdateBehavior Limitation:</b> | ||
| * {@code @DynamoDbUpdateBehavior} does not apply to primary keys due to DynamoDB API constraints. | ||
| * It only affects secondary index keys. | ||
| */ | ||
| @SdkPublicApi | ||
| @ThreadSafe | ||
| public final class AutoGeneratedKeyExtension implements DynamoDbEnhancedClientExtension { | ||
|
|
||
| /** | ||
| * Metadata keys used to record attributes annotated with {@code @DynamoDbAutoGeneratedKey} and | ||
| * {@code @DynamoDbAutoGeneratedUuid}. These are used during schema validation to detect annotation conflicts. | ||
| */ | ||
| private static final String CUSTOM_METADATA_KEY = | ||
| "software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedKeyExtension:AutoGeneratedKeyAttribute"; | ||
| private static final String UUID_EXTENSION_METADATA_KEY = | ||
| "software.amazon.awssdk.enhanced.dynamodb.extensions.AutoGeneratedUuidExtension:AutoGeneratedUuidAttribute"; | ||
|
|
||
| private static final StaticAttributeTag AUTO_GENERATED_KEY_ATTRIBUTE = new AutoGeneratedKeyAttribute(); | ||
| private static final String AUTOGENERATED_KEY_ANNOTATION = "@DynamoDbAutoGeneratedKey"; | ||
| private static final String AUTOGENERATED_UUID_ANNOTATION = "@DynamoDbAutoGeneratedUuid"; | ||
|
|
||
| /** | ||
| * Stores the TableMetadata instances that have already been validated by this extension. Uses a ConcurrentHashMap to ensure | ||
| * thread-safe access during concurrent write operations. | ||
| */ | ||
| private final Set<TableMetadata> validatedSchemas = newSetFromMap(new ConcurrentHashMap<>()); | ||
|
|
||
| /** | ||
| * Caches the set of valid key attribute names per TableMetadata instance. Computed once per schema. | ||
| */ | ||
| private final Map<TableMetadata, Set<String>> allowedKeysCache = new ConcurrentHashMap<>(); | ||
|
|
||
| private AutoGeneratedKeyExtension() { | ||
| } | ||
|
|
||
| /** | ||
| * @return an Instance of {@link AutoGeneratedKeyExtension} | ||
| */ | ||
| public static AutoGeneratedKeyExtension create() { | ||
| return new AutoGeneratedKeyExtension(); | ||
| } | ||
|
|
||
| public static Builder builder() { | ||
| return new Builder(); | ||
| } | ||
|
|
||
| /** | ||
| * Inserts a UUID for attributes tagged with {@code @DynamoDbAutoGeneratedKey} when the attribute is null or empty, preserving | ||
| * existing values. | ||
| * | ||
| * <p>Schema-level validation (annotation conflict detection and key-placement checks) | ||
| * is executed once per schema instance and cached. | ||
| */ | ||
| @Override | ||
| public WriteModification beforeWrite(DynamoDbExtensionContext.BeforeWrite context) { | ||
|
|
||
|
|
||
| Collection<String> taggedAttributes = context.tableMetadata() | ||
| .customMetadataObject(CUSTOM_METADATA_KEY, Collection.class) | ||
| .orElse(null); | ||
|
|
||
| if (taggedAttributes == null) { | ||
| return WriteModification.builder().build(); | ||
| } | ||
|
|
||
| TableMetadata tableMetadata = context.tableMetadata(); | ||
| if (validatedSchemas.add(tableMetadata)) { | ||
| validateNoAutoGeneratedAnnotationConflict(tableMetadata); | ||
| validateAutoGeneratedKeyPlacement(tableMetadata, taggedAttributes); | ||
| } | ||
|
|
||
| Map<String, AttributeValue> itemToTransform = new HashMap<>(context.items()); | ||
| taggedAttributes.forEach(attr -> insertUuidIfMissing(itemToTransform, attr)); | ||
|
|
||
| return WriteModification.builder() | ||
| .transformedItem(Collections.unmodifiableMap(itemToTransform)) | ||
| .build(); | ||
| } | ||
|
|
||
| /** | ||
| * Validates (once per TableMetadata instance) that @DynamoDbAutoGeneratedKey and @DynamoDbAutoGeneratedUuid are not applied | ||
| * to the same attribute. | ||
| */ | ||
| private static void validateNoAutoGeneratedAnnotationConflict(TableMetadata tableMetadata) { | ||
| ExtensionsValidationUtils.validateNoAnnotationConflict( | ||
| tableMetadata, | ||
| CUSTOM_METADATA_KEY, | ||
| UUID_EXTENSION_METADATA_KEY, | ||
| AUTOGENERATED_KEY_ANNOTATION, | ||
| AUTOGENERATED_UUID_ANNOTATION | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Validates that all attributes tagged with @DynamoDbAutoGeneratedKey are either primary keys or secondary index keys. | ||
| **/ | ||
| private void validateAutoGeneratedKeyPlacement(TableMetadata tableMetadata, | ||
| Collection<String> taggedAttributeNames) { | ||
|
|
||
| Set<String> allowedKeyAttributes = allowedKeysCache.computeIfAbsent(tableMetadata, metadata -> { | ||
|
|
||
| // Add primary keys | ||
| Set<String> keyAttributes = new HashSet<>(metadata.primaryKeys()); | ||
|
|
||
| // Add all secondary index keys | ||
| metadata.indices().stream().map(IndexMetadata::name).forEach(indexName -> { | ||
| keyAttributes.addAll(metadata.indexPartitionKeys(indexName)); | ||
| keyAttributes.addAll(metadata.indexSortKeys(indexName)); | ||
| }); | ||
|
|
||
| return keyAttributes; | ||
| }); | ||
|
|
||
| taggedAttributeNames.stream() | ||
| .filter(attrName -> !allowedKeyAttributes.contains(attrName)) | ||
| .findFirst() | ||
| .ifPresent(invalidAttribute -> { | ||
| throw new IllegalArgumentException( | ||
| "@DynamoDbAutoGeneratedKey can only be applied to key attributes: " | ||
| + "primary partition key, primary sort key, or GSI/LSI partition/sort keys. " | ||
| + "Invalid placement on attribute: " + invalidAttribute); | ||
| }); | ||
| } | ||
|
|
||
| private void insertUuidIfMissing(Map<String, AttributeValue> itemToTransform, String key) { | ||
| AttributeValue existing = itemToTransform.get(key); | ||
| if (Objects.isNull(existing) || StringUtils.isBlank(existing.s())) { | ||
| itemToTransform.put(key, AttributeValue.builder().s(UUID.randomUUID().toString()).build()); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Static helpers used by the {@code @BeanTableSchemaAttributeTag}-based annotation tag. | ||
| */ | ||
| public static final class AttributeTags { | ||
| private AttributeTags() { | ||
| } | ||
|
|
||
| /** | ||
| * @return a {@link StaticAttributeTag} that marks the attribute for auto-generated key behavior. | ||
| */ | ||
| public static StaticAttributeTag autoGeneratedKeyAttribute() { | ||
| return AUTO_GENERATED_KEY_ATTRIBUTE; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Stateless builder. | ||
| */ | ||
| public static final class Builder { | ||
| private Builder() { | ||
| } | ||
|
|
||
| public AutoGeneratedKeyExtension build() { | ||
| return new AutoGeneratedKeyExtension(); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Validates Java type and records the tagged attribute into table metadata so {@link #beforeWrite} can find it at runtime. | ||
| */ | ||
| private static final class AutoGeneratedKeyAttribute implements StaticAttributeTag { | ||
|
|
||
| @Override | ||
| public <R> void validateType(String attributeName, | ||
| EnhancedType<R> type, | ||
| AttributeValueType attributeValueType) { | ||
|
|
||
| Validate.notNull(type, "type is null"); | ||
| Validate.notNull(type.rawClass(), "rawClass is null"); | ||
| Validate.notNull(attributeValueType, "attributeValueType is null"); | ||
|
|
||
| if (!type.rawClass().equals(String.class)) { | ||
| throw new IllegalArgumentException(String.format( | ||
| "Attribute '%s' of Class type %s is not a suitable Java Class type to be used as a Auto Generated " | ||
| + "Key attribute. Only String Class type is supported.", attributeName, type.rawClass())); | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public Consumer<StaticTableMetadata.Builder> modifyMetadata(String attributeName, | ||
| AttributeValueType attributeValueType) { | ||
| return metadata -> metadata.addCustomMetadataObject(CUSTOM_METADATA_KEY, Collections.singleton(attributeName)) | ||
| .markAttributeAsKey(attributeName, attributeValueType); | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.