diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 6854a797c..538c8803a 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -35,6 +35,7 @@ and this project adheres to https://semver.org/spec/v2.0.0.html[Semantic Version === Added - Include support to Oracle NoSQL database +- Include support to Document API for DynamoDB database == [1.0.4] - 2023-12-19 diff --git a/README.adoc b/README.adoc index 0b23fbf0a..ee27c57f7 100644 --- a/README.adoc +++ b/README.adoc @@ -390,7 +390,6 @@ jnosql.couchbase.user=root jnosql.couchbase.password=123456 ---- - The config settings are the default behavior; nevertheless, there is an option to do it programmatically. Create a class that implements the `Supplier` and then defines it as an `@Alternative` and the `Priority`. [source,java] @@ -519,9 +518,9 @@ jnosql.couchdb.password=password image::https://user-images.githubusercontent.com/6509926/70553550-f033b980-1b40-11ea-9192-759b3b1053b3.png[Redis Project,align="center" width=50%,height=50%] -https://aws.amazon.com/dynamodb/[Amazon DynamoDB] is a fully managed, serverless, key-value NoSQL database designed to run high-performance applications at any scale. DynamoDB offers built-in security, continuous backups, automated multi-Region replication, in-memory caching, and data import and export tools. +https://aws.amazon.com/dynamodb/[Amazon DynamoDB] is a fully managed, serverless, key-value and document NoSQL database designed to run high-performance applications at any scale. DynamoDB offers built-in security, continuous backups, automated multi-Region replication, in-memory caching, and data import and export tools. -This driver provides support for the *Key-Value* NoSQL API. +This driver has support for two NoSQL API types: *Key-Value* and *Document*. === How To Install @@ -539,6 +538,7 @@ You can use either the Maven or Gradle dependencies: === Configuration This API provides the ```DynamoDBConfigurations``` class to programmatically establish the credentials. + Please note that you can establish properties using the https://microprofile.io/microprofile-config/[MicroProfile Config] specification. [cols="DynamoDB"] @@ -560,9 +560,10 @@ Please note that you can establish properties using the https://microprofile.io/ |`jnosql.dynamodb.secretaccess` |The AWS secret access key, used to authenticate the user interacting with AWS. - |=== +=== Using the Key-value API + This is an example using DynamoDB's Key-Value API with MicroProfile Config. [source,properties] @@ -571,6 +572,95 @@ jnosql.keyvalue.provider=org.eclipse.jnosql.databases.dynamodb.communication.Dyn jnosql.keyvalue.database=heroes ---- +=== Using the Document API + +The DynamoDB's Document API implementation follows the *SINGLE TABLE* strategy, it means, the table will store multiple entity types. To satisfy this strategy, the implementation assumes that the target table will have a composed primary key: + +- The `entityType` field as the partitioning key; +- The `id` field as the sort key; + +To customize the partitioning key field name, you can define the following configuration + +[source,properties] +---- +jnosql.dynamodb.entity.pk=entityType +---- + +By default, the implementation doesn't create the table on-the-fly, letting this requirement for the users. If you prefer, the implementation is able to create the table on-the-fly as well. To activate this capability you should define explicitly the following configuration: + +[source,properties] +---- +jnosql.dynamodb.create.tables=true +---- + +The table will be created with the composed primary key mentioned previously. + +Here's an example using DynamoDB's Document API with MicroProfile Config. + +[source,properties] +---- +jnosql.document.provider=org.eclipse.jnosql.databases.dynamodb.communication.DynamoDBDocumentConfiguration +jnosql.document.database=heroes +---- + +The config settings are the default behavior; nevertheless, there is an option to do it programmatically. Create a class that implements the `Supplier` and then defines it as an `@Alternative` and the `Priority`. + +[source,java] +---- +@ApplicationScoped +@Alternative +@Priority(Interceptor.Priority.APPLICATION) +public class ManagerSupplier implements Supplier { + + @Produces + public DynamoDBDocumentManager get() { + Settings settings = Settings.builder().put("credential", "value").build(); + DynamoDBDocumentConfiguration configuration = new DynamoDBDocumentConfiguration(); + DynamoDBDocumentManagerFactory factory = configuration.apply(settings); + return factory.apply("database"); + } +} +---- + + +=== Repository + +The ```DynamoDBRepository``` interface is an extension of the ```Repository``` interface that allows execution of PartiQL via the ```@PartiQL``` annotation. + +WARNING: DynamoDB supports a limited subset of +https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ql-reference.html[PartiQL]. + +NOTE: This implementation doesn't provide pagination on the queries. + +[source,java] +---- +@Repository +interface PersonRepository extends DynamoDBRepository { + +@PartiQL("select * from Person") +List findAll(); + +@PartiQL("select * from Person where name = ?") +List findByName(@Param("") String name); + +} +---- + + +=== Template + +The ```DynamoDBTemplate``` interface is a specialization of the ```DocumentTemplate``` interface that allows using PartiQL queries. + +WARNING: DynamoDB supports a limited subset of +https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ql-reference.html[PartiQL]. + +NOTE: This implementation doesn't provide pagination on the queries. + +[source,java] +---- +List people = template.partiQL("select * from Person where name = ? ", params); +---- + == Elasticsearch image::https://jnosql.github.io/img/logos/elastic.svg[Elasticsearch Project,align="center"width=25%,height=25%] diff --git a/jnosql-dynamodb/pom.xml b/jnosql-dynamodb/pom.xml index 7f174f93a..0c1214d09 100644 --- a/jnosql-dynamodb/pom.xml +++ b/jnosql-dynamodb/pom.xml @@ -22,7 +22,7 @@ The Eclipse JNoSQL layer implementation AWS DynamoDB - 2.21.21 + 2.23.12 @@ -30,6 +30,10 @@ org.eclipse.jnosql.mapping jnosql-mapping-key-value + + org.eclipse.jnosql.mapping + jnosql-mapping-document + ${project.groupId} jnosql-database-commons @@ -40,11 +44,5 @@ dynamodb ${dynamodb.version} - - org.testcontainers - testcontainers - ${testcontainers.version} - test - \ No newline at end of file diff --git a/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DefaultDynamoDBDatabaseManager.java b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DefaultDynamoDBDatabaseManager.java new file mode 100644 index 000000000..518e2fb45 --- /dev/null +++ b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DefaultDynamoDBDatabaseManager.java @@ -0,0 +1,338 @@ +/* + * Copyright (c) 2023 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Maximillian Arruda + */ + +package org.eclipse.jnosql.databases.dynamodb.communication; + +import org.eclipse.jnosql.communication.Settings; +import org.eclipse.jnosql.communication.semistructured.CommunicationEntity; +import org.eclipse.jnosql.communication.semistructured.DeleteQuery; +import org.eclipse.jnosql.communication.semistructured.SelectQuery; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.AttributeValueUpdate; +import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest; +import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest; +import software.amazon.awssdk.services.dynamodb.model.DescribeTableRequest; +import software.amazon.awssdk.services.dynamodb.model.DescribeTableResponse; +import software.amazon.awssdk.services.dynamodb.model.DescribeTimeToLiveRequest; +import software.amazon.awssdk.services.dynamodb.model.DescribeTimeToLiveResponse; +import software.amazon.awssdk.services.dynamodb.model.ExecuteStatementRequest; +import software.amazon.awssdk.services.dynamodb.model.ExecuteStatementResponse; +import software.amazon.awssdk.services.dynamodb.model.KeySchemaElement; +import software.amazon.awssdk.services.dynamodb.model.KeyType; +import software.amazon.awssdk.services.dynamodb.model.ProvisionedThroughput; +import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; +import software.amazon.awssdk.services.dynamodb.model.ResourceNotFoundException; +import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType; +import software.amazon.awssdk.services.dynamodb.model.ScanRequest; +import software.amazon.awssdk.services.dynamodb.model.Select; +import software.amazon.awssdk.services.dynamodb.model.TimeToLiveStatus; +import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest; + +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import static java.util.Objects.requireNonNull; +import static org.eclipse.jnosql.databases.dynamodb.communication.DynamoDBConverter.entityAttributeName; +import static org.eclipse.jnosql.databases.dynamodb.communication.DynamoDBConverter.toAttributeValue; +import static org.eclipse.jnosql.databases.dynamodb.communication.DynamoDBConverter.toCommunicationEntity; +import static org.eclipse.jnosql.databases.dynamodb.communication.DynamoDBConverter.toItem; +import static org.eclipse.jnosql.databases.dynamodb.communication.DynamoDBConverter.toItemUpdate; + +public class DefaultDynamoDBDatabaseManager implements DynamoDBDatabaseManager { + + private final String database; + + private final Settings settings; + + private final DynamoDbClient dynamoDbClient; + + private final ConcurrentHashMap> ttlAttributeNamesByTable = new ConcurrentHashMap<>(); + + private final ConcurrentHashMap tables = new ConcurrentHashMap<>(); + + public DefaultDynamoDBDatabaseManager(String database, DynamoDbClient dynamoDbClient, Settings settings) { + this.settings = settings; + this.database = database; + this.dynamoDbClient = dynamoDbClient; + } + + private String resolveEntityNameAttributeName(String entityName) { + return this.settings.get(DynamoDBConfigurations.ENTITY_PARTITION_KEY, String.class).orElse(entityName); + } + + public DynamoDbClient dynamoDbClient() { + return dynamoDbClient; + } + + @Override + public String name() { + return database; + } + + @Override + public CommunicationEntity insert(CommunicationEntity documentEntity) { + requireNonNull(documentEntity, "documentEntity is required"); + dynamoDbClient().putItem(PutItemRequest.builder() + .tableName(createTableIfNeeded(documentEntity.name()).table().tableName()) + .item(toItem(this::resolveEntityNameAttributeName, documentEntity)) + .build()); + return documentEntity; + } + + private Supplier getTTLAttributeName(String tableName) { + return this.ttlAttributeNamesByTable.computeIfAbsent(tableName, this::getTTLAttributeNameSupplier); + } + + private Supplier getTTLAttributeNameSupplier(String tableName) { + createTableIfNeeded(tableName); + DescribeTimeToLiveResponse describeTimeToLiveResponse = dynamoDbClient().describeTimeToLive(DescribeTimeToLiveRequest.builder() + .tableName(tableName).build()); + if (TimeToLiveStatus.ENABLED.equals(describeTimeToLiveResponse.timeToLiveDescription().timeToLiveStatus())) { + var ttlAttributeName = describeTimeToLiveResponse.timeToLiveDescription().attributeName(); + return () -> ttlAttributeName; + } + return () -> tableName + " don't support TTL operations. Check if TTL support is enabled for this table."; + } + + private DescribeTableResponse createTableIfNeeded(String tableName) { + return this.tables.computeIfAbsent(tableName, this::resolveTable); + } + + private DescribeTableResponse resolveTable(String tableName) { + try { + return getDescribeTableResponse(tableName); + } catch (ResourceNotFoundException ex) { + if (!shouldCreateTables()) + throw ex; + return createTable(tableName); + } + } + + private DescribeTableResponse getDescribeTableResponse(String tableName) { + return dynamoDbClient().describeTable(DescribeTableRequest.builder() + .tableName(tableName) + .build()); + } + + private DescribeTableResponse createTable(String tableName) { + try (var waiter = dynamoDbClient().waiter()) { + dynamoDbClient().createTable(CreateTableRequest.builder() + .tableName(tableName) + .keySchema(defaultKeySchemaFor()) + .attributeDefinitions(defaultAttributeDefinitionsFor()) + .provisionedThroughput(defaultProvisionedThroughputFor()) + .build()); + + var tableRequest = DescribeTableRequest.builder().tableName(tableName).build(); + var waiterResponse = waiter.waitUntilTableExists(tableRequest); + return waiterResponse.matched().response().orElseThrow(); + } + } + + private ProvisionedThroughput defaultProvisionedThroughputFor() { + return DynamoTableUtils.createProvisionedThroughput(null, null); + } + + private Collection defaultAttributeDefinitionsFor() { + return List.of( + AttributeDefinition.builder().attributeName(getEntityAttributeName()).attributeType(ScalarAttributeType.S).build(), + AttributeDefinition.builder().attributeName(DynamoDBConverter.ID).attributeType(ScalarAttributeType.S).build() + ); + } + + private Collection defaultKeySchemaFor() { + return List.of( + KeySchemaElement.builder().attributeName(getEntityAttributeName()).keyType(KeyType.HASH).build(), + KeySchemaElement.builder().attributeName(DynamoDBConverter.ID).keyType(KeyType.RANGE).build() + ); + } + + private boolean shouldCreateTables() { + return this.settings + .get(DynamoDBConfigurations.CREATE_TABLES, Boolean.class) + .orElse(false); + } + + private String getEntityAttributeName() { + return entityAttributeName(this::resolveEntityNameAttributeName); + } + + @Override + public CommunicationEntity insert(CommunicationEntity documentEntity, Duration ttl) { + requireNonNull(documentEntity, "documentEntity is required"); + requireNonNull(ttl, "ttl is required"); + documentEntity.add(getTTLAttributeName(documentEntity.name()).get(), Instant.now().plus(ttl).truncatedTo(ChronoUnit.SECONDS)); + return insert(documentEntity); + } + + @Override + public Iterable insert(Iterable entities) { + requireNonNull(entities, "entities are required"); + return StreamSupport.stream(entities.spliterator(), false) + .map(this::insert) + .toList(); + } + + @Override + public Iterable insert(Iterable entities, Duration ttl) { + requireNonNull(entities, "entities is required"); + requireNonNull(ttl, "ttl is required"); + return StreamSupport.stream(entities.spliterator(), false) + .map(e -> this.insert(e, ttl)) + .toList(); + } + + @Override + public CommunicationEntity update(CommunicationEntity documentEntity) { + requireNonNull(documentEntity, "entity is required"); + Map itemKey = getItemKey(documentEntity); + Map attributeUpdates = asItemToUpdate(documentEntity); + itemKey.keySet().forEach(attributeUpdates::remove); + dynamoDbClient().updateItem(UpdateItemRequest.builder() + .tableName(createTableIfNeeded(documentEntity.name()).table().tableName()) + .key(itemKey) + .attributeUpdates(attributeUpdates) + .build()); + return documentEntity; + } + + private Map getItemKey(CommunicationEntity documentEntity) { + DescribeTableResponse describeTableResponse = this.tables.computeIfAbsent(documentEntity.name(), this::getDescribeTableResponse); + Map itemKey = describeTableResponse + .table() + .keySchema() + .stream() + .map(attribute -> Map.of(attribute.attributeName(), + toAttributeValue(documentEntity.find(attribute.attributeName(), Object.class).orElse(null)))) + .reduce(new HashMap<>(), (a, b) -> { + a.putAll(b); + return a; + }); + itemKey.put(getEntityAttributeName(), toAttributeValue(documentEntity.name())); + return itemKey; + } + + private Map asItemToUpdate(CommunicationEntity documentEntity) { + return toItemUpdate(this::resolveEntityNameAttributeName, documentEntity); + } + + @Override + public Iterable update(Iterable entities) { + requireNonNull(entities, "entities is required"); + return StreamSupport.stream(entities.spliterator(), false) + .map(this::update) + .toList(); + } + + @Override + public void delete(DeleteQuery deleteQuery) { + Objects.requireNonNull(deleteQuery, "deleteQuery is required"); + + List primaryKeys = getDescribeTableResponse(deleteQuery.name()) + .table() + .keySchema() + .stream() + .map(KeySchemaElement::attributeName).toList(); + + + var selectQueryBuilder = SelectQuery.builder() + .select(primaryKeys.toArray(new String[0])) + .from(deleteQuery.name()); + + deleteQuery.condition().ifPresent(selectQueryBuilder::where); + + select(selectQueryBuilder.build()).forEach( + documentEntity -> + dynamoDbClient().deleteItem(DeleteItemRequest.builder() + .tableName(deleteQuery.name()) + .key(getItemKey(documentEntity)) + .build())); + } + + @Override + public Stream select(SelectQuery query) { + Objects.requireNonNull(query, "query is required"); + DynamoDBQuery dynamoDBQuery = DynamoDBQuery + .builderOf(query.name(), getEntityAttributeName(), query) + .get(); + + ScanRequest.Builder selectRequest = ScanRequest.builder() + .consistentRead(true) + .tableName(dynamoDBQuery.table()) + .projectionExpression(dynamoDBQuery.projectionExpression()) + .filterExpression(dynamoDBQuery.filterExpression()) + .expressionAttributeNames(dynamoDBQuery.expressionAttributeNames()) + .expressionAttributeValues(dynamoDBQuery.expressionAttributeValues()) + .select(dynamoDBQuery.projectionExpression() != null ? Select.SPECIFIC_ATTRIBUTES : Select.ALL_ATTRIBUTES); + + return StreamSupport + .stream(dynamoDbClient().scanPaginator(selectRequest.build()).spliterator(), false) + .flatMap(scanResponse -> scanResponse.items().stream() + .map(item -> toCommunicationEntity(this::resolveEntityNameAttributeName, item))); + } + + @Override + public long count(String tableName) { + Objects.requireNonNull(tableName, "tableName is required"); + try { + return getDescribeTableResponse(tableName) + .table() + .itemCount(); + } catch (ResourceNotFoundException ex) { + return 0; + } + } + + @Override + public void close() { + this.dynamoDbClient.close(); + } + + @Override + public Stream partiQL(String query, Object... params) { + Objects.requireNonNull(query, "query is required"); + List parameters = Stream.of(params).map(DynamoDBConverter::toAttributeValue).toList(); + ExecuteStatementResponse executeStatementResponse = dynamoDbClient() + .executeStatement(ExecuteStatementRequest.builder() + .statement(query) + .parameters(parameters) + .build()); + List result = new LinkedList<>(); + executeStatementResponse.items().forEach(item -> result.add(toCommunicationEntity(this::resolveEntityNameAttributeName, item))); + while (executeStatementResponse.nextToken() != null) { + executeStatementResponse = dynamoDbClient() + .executeStatement(ExecuteStatementRequest.builder() + .statement(query) + .parameters(parameters) + .nextToken(executeStatementResponse.nextToken()) + .build()); + executeStatementResponse.items().forEach(item -> result.add(toCommunicationEntity(this::resolveEntityNameAttributeName, item))); + } + return result.stream(); + } +} diff --git a/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBConfigurations.java b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBConfigurations.java index 0cf8275f7..34d03ec8c 100644 --- a/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBConfigurations.java +++ b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBConfigurations.java @@ -22,7 +22,9 @@ public enum DynamoDBConfigurations implements Supplier { REGION("jnosql.dynamodb.region"), PROFILE("jnosql.dynamodb.profile"), AWS_ACCESSKEY("jnosql.dynamodb.awsaccesskey"), - AWS_SECRET_ACCESS("jnosql.dynamodb.secretaccess"); + AWS_SECRET_ACCESS("jnosql.dynamodb.secretaccess"), + ENTITY_PARTITION_KEY("jnosql.dynamodb.entity.pk"), + CREATE_TABLES("jnosql.dynamodb.create.tables"); private final String configuration; diff --git a/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBConverter.java b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBConverter.java new file mode 100644 index 000000000..d233def3a --- /dev/null +++ b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBConverter.java @@ -0,0 +1,209 @@ +/* + * Copyright (c) 2023 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Maximillian Arruda + */ + +package org.eclipse.jnosql.databases.dynamodb.communication; + +import org.eclipse.jnosql.communication.ValueUtil; +import org.eclipse.jnosql.communication.semistructured.CommunicationEntity; +import org.eclipse.jnosql.communication.semistructured.Element; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.services.dynamodb.model.AttributeAction; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.AttributeValueUpdate; + +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.UnaryOperator; +import java.util.stream.StreamSupport; + +import static java.util.Collections.singletonMap; + +class DynamoDBConverter { + + static final String ENTITY = "@entity"; + static final String ID = "id"; + + private DynamoDBConverter() { + } + + private static Object convertValue(Object value) { + if (value instanceof AttributeValue attributeValue) { + switch (attributeValue.type()) { + case S: + return attributeValue.s(); + case N: + return Double.valueOf(attributeValue.n()); + case B: + return attributeValue.b().asByteArray(); + case SS: + return attributeValue.ss(); + case NS: + return attributeValue.ns().stream().map(Double::valueOf).toList(); + case BS: + return attributeValue.bs().stream().map(SdkBytes::asByteArray).toList(); + case L: + return attributeValue.l().stream().map(DynamoDBConverter::convertValue).toList(); + case M: + return attributeValue.m().entrySet().stream().map(e -> Element.of(e.getKey(), convertValue(e.getValue()))).toList(); + case NUL: + return null; + case BOOL: + return attributeValue.bool(); + case UNKNOWN_TO_SDK_VERSION: + default: + return null; // map type + } + } + return value; + } + + static Map getMap(UnaryOperator entityNameResolver, CommunicationEntity entity) { + var nameResolver = Optional.ofNullable(entityNameResolver).orElse(UnaryOperator.identity()); + Map jsonObject = new HashMap<>(); + entity.elements().forEach(feedJSON(jsonObject)); + jsonObject.put(entityAttributeName(nameResolver), entity.name()); + return jsonObject; + } + + public static String entityAttributeName(UnaryOperator nameResolver) { + return Optional.ofNullable(nameResolver.apply(ENTITY)).orElse(ENTITY); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private static Consumer feedJSON(Map jsonObject) { + return d -> { + Object value = ValueUtil.convert(d.value()); + if (value instanceof Element subElement) { + jsonObject.put(d.name(), singletonMap(subElement.name(), subElement.get())); + } else if (isSudDocument(value)) { + Map subDocument = getMap(value); + jsonObject.put(d.name(), subDocument); + } else if (isSudDocumentList(value)) { + jsonObject.put(d.name(), StreamSupport.stream(((Iterable) value).spliterator(), false) + .map(DynamoDBConverter::getMap).toList()); + } else { + jsonObject.put(d.name(), value); + } + }; + } + + private static Map getMap(Object value) { + Map subDocument = new HashMap<>(); + StreamSupport.stream(Iterable.class.cast(value).spliterator(), + false).forEach(feedJSON(subDocument)); + return subDocument; + } + + private static boolean isSudDocument(Object value) { + return value instanceof Iterable && StreamSupport.stream(Iterable.class.cast(value).spliterator(), false). + allMatch(Element.class::isInstance); + } + + private static boolean isSudDocumentList(Object value) { + return value instanceof Iterable && StreamSupport.stream(Iterable.class.cast(value).spliterator(), false). + allMatch(d -> d instanceof Iterable && isSudDocument(d)); + } + + public static Map toItem(UnaryOperator entityNameResolver, CommunicationEntity entity) { + UnaryOperator resolver = Optional.ofNullable(entityNameResolver).orElse(UnaryOperator.identity()); + Map documentAttributes = getMap(resolver, entity); + return toItem(documentAttributes); + } + + + private static Map toItem(Map documentAttributes) { + HashMap result = new HashMap<>(); + documentAttributes.forEach((attribute, value) -> result.put(attribute, toAttributeValue(value))); + return result; + } + + public static AttributeValue toAttributeValue(Object value) { + if (value == null) + return AttributeValue.builder().nul(true).build(); + if (value instanceof String str) + return AttributeValue.builder().s(str).build(); + if (value instanceof Number number) + return AttributeValue.builder().n(String.valueOf(number)).build(); + if (value instanceof Boolean bool) + return AttributeValue.builder().bool(bool).build(); + if (value instanceof List list) + return AttributeValue.builder().l(list.stream().filter(Objects::nonNull) + .map(DynamoDBConverter::toAttributeValue).toList()).build(); + if (value instanceof Map mapValue) { + HashMap values = new HashMap<>(); + mapValue.forEach((k, v) -> values.put(String.valueOf(k), toAttributeValue(v))); + return AttributeValue.builder().m(values).build(); + } + if (value instanceof byte[] data) { + return AttributeValue.builder().b(SdkBytes.fromByteArray(data)).build(); + } + if (value instanceof ByteBuffer byteBuffer) { + return AttributeValue.builder().b(SdkBytes.fromByteBuffer(byteBuffer)).build(); + } + if (value instanceof InputStream input) { + return AttributeValue.builder().b(SdkBytes.fromInputStream(input)).build(); + } + if (value instanceof Element element) { + return toAttributeValue(getMap(element)); + } + return AttributeValue.builder().s(String.valueOf(value)).build(); + } + + public static Map toItemUpdate(UnaryOperator entityNameResolver, CommunicationEntity entity) { + UnaryOperator resolver = Optional.ofNullable(entityNameResolver).orElse(UnaryOperator.identity()); + Map documentAttributes = getMap(resolver, entity); + return toItemUpdate(documentAttributes); + } + + private static Map toItemUpdate(Map documentAttributes) { + return documentAttributes + .entrySet() + .stream() + .map(entry -> Map.of(entry.getKey(), toAttributeValueUpdate(entry.getValue()))) + .reduce(new HashMap<>(), (a, b) -> { + a.putAll(b); + return a; + }); + } + + public static AttributeValueUpdate toAttributeValueUpdate(Object value) { + return AttributeValueUpdate.builder().value(toAttributeValue(value)).action(AttributeAction.PUT).build(); + } + + + public static CommunicationEntity toCommunicationEntity(UnaryOperator entityNameResolver, Map item) { + if (item == null) { + return null; + } + if (item.isEmpty()) { + return null; + } + UnaryOperator resolver = Optional.ofNullable(entityNameResolver).orElse(UnaryOperator.identity()); + String entityAttribute = resolver.apply(ENTITY); + var entityName = item.containsKey(entityAttribute) ? item.get(entityAttribute).s() : entityAttribute; + var elements = item.entrySet() + .stream() + .filter(entry -> !Objects.equals(entityAttribute, entry.getKey())) + .map(entry -> Element.of(entry.getKey(), convertValue(entry.getValue()))) + .toList(); + return CommunicationEntity.of(entityName, elements); + } +} diff --git a/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBDatabaseManager.java b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBDatabaseManager.java new file mode 100644 index 000000000..fca88ca2f --- /dev/null +++ b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBDatabaseManager.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Maximillian Arruda + */ + +package org.eclipse.jnosql.databases.dynamodb.communication; + +import org.eclipse.jnosql.communication.semistructured.CommunicationEntity; +import org.eclipse.jnosql.communication.semistructured.DatabaseManager; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; + +import java.util.stream.Stream; + +/** + * A document manager interface for DynamoDB database operations. + */ +public interface DynamoDBDatabaseManager extends DatabaseManager { + + /** + * DynamoDB supports a limited subset of PartiQL. + * This method executes a PartiQL query with parameters and returns a stream of CommunicationEntity objects. + *

Example query: {@code SELECT * FROM users WHERE status = ?}

+ * + * @param query the PartiQL query + * @return a {@link Stream} of {@link CommunicationEntity} representing the query result + * @throws NullPointerException when the query is null + */ + Stream partiQL(String query, Object... params); + + + /** + * @return a {@link DynamoDbClient} instance for custom utilization + */ + DynamoDbClient dynamoDbClient(); + + +} diff --git a/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBDatabaseManagerFactory.java b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBDatabaseManagerFactory.java new file mode 100644 index 000000000..2fddf20d9 --- /dev/null +++ b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBDatabaseManagerFactory.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2023 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Maximillian Arruda + */ + +package org.eclipse.jnosql.databases.dynamodb.communication; + +import org.eclipse.jnosql.communication.Settings; +import org.eclipse.jnosql.communication.semistructured.DatabaseManagerFactory; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; + +import java.util.Optional; + +public class DynamoDBDatabaseManagerFactory implements DatabaseManagerFactory { + + private final DynamoDbClient dynamoDB; + private final Settings settings; + + public DynamoDBDatabaseManagerFactory(DynamoDbClient dynamoDB, Settings settings) { + this.dynamoDB = dynamoDB; + this.settings = settings; + } + + @Override + public DynamoDBDatabaseManager apply(String database) { + return new DefaultDynamoDBDatabaseManager(database, dynamoDB, settings); + } + + @Override + public void close() { + Optional.ofNullable(this.dynamoDB).ifPresent(DynamoDbClient::close); + } +} diff --git a/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBDocumentConfiguration.java b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBDocumentConfiguration.java new file mode 100644 index 000000000..84b24f341 --- /dev/null +++ b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBDocumentConfiguration.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Maximillian Arruda + */ + +package org.eclipse.jnosql.databases.dynamodb.communication; + +import org.eclipse.jnosql.communication.Settings; +import org.eclipse.jnosql.communication.semistructured.DatabaseConfiguration; + +public class DynamoDBDocumentConfiguration extends DynamoDBConfiguration + implements DatabaseConfiguration { + @Override + public DynamoDBDatabaseManagerFactory apply(Settings settings) { + var dynamoDB = getDynamoDB(settings); + return new DynamoDBDatabaseManagerFactory(dynamoDB, settings); + } +} diff --git a/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBQuery.java b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBQuery.java new file mode 100644 index 000000000..bad0a0a06 --- /dev/null +++ b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBQuery.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Maximillian Arruda + */ + +package org.eclipse.jnosql.databases.dynamodb.communication; + +import org.eclipse.jnosql.communication.semistructured.SelectQuery; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +import java.util.Map; +import java.util.function.Supplier; + +public record DynamoDBQuery(String table, + String projectionExpression, + String filterExpression, + Map expressionAttributeNames, + Map expressionAttributeValues) { + + public static Supplier builderOf(String table, + String partitionKey, + SelectQuery query) { + return new DynamoDBQuerySelectBuilder(table, partitionKey, query); + } +} diff --git a/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBQueryBuilder.java b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBQueryBuilder.java new file mode 100644 index 000000000..0ead881d0 --- /dev/null +++ b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBQueryBuilder.java @@ -0,0 +1,198 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Maximillian Arruda + */ + +package org.eclipse.jnosql.databases.dynamodb.communication; + +import org.eclipse.jnosql.communication.TypeReference; +import org.eclipse.jnosql.communication.semistructured.CriteriaCondition; +import org.eclipse.jnosql.communication.semistructured.Element; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +import static org.eclipse.jnosql.databases.dynamodb.communication.DynamoDBConverter.toAttributeValue; + +abstract class DynamoDBQueryBuilder implements Supplier { + + protected void condition(CriteriaCondition condition, + StringBuilder filterExpression, + Map expressionAttributeNames, + Map expressionAttributeValues) { + var element = condition.element(); + + switch (condition.condition()) { + case EQUALS: + predicate(" = ", element, filterExpression, expressionAttributeNames, expressionAttributeValues); + break; + case LIKE: + predicateLike(element, filterExpression, expressionAttributeNames, expressionAttributeValues); + break; + case IN: + predicateIn(element, filterExpression, expressionAttributeNames, expressionAttributeValues); + break; + case GREATER_THAN: + predicate(" > ", element, filterExpression, expressionAttributeNames, expressionAttributeValues); + break; + case LESSER_THAN: + predicate(" < ", element, filterExpression, expressionAttributeNames, expressionAttributeValues); + break; + case GREATER_EQUALS_THAN: + predicate(" >= ", element, filterExpression, expressionAttributeNames, expressionAttributeValues); + break; + case LESSER_EQUALS_THAN: + predicate(" <= ", element, filterExpression, expressionAttributeNames, expressionAttributeValues); + break; + case BETWEEN: + predicateBetween(element, filterExpression, expressionAttributeNames, expressionAttributeValues); + break; + case AND: + appendCondition(element.get(new TypeReference<>() { + }), " AND ", filterExpression, expressionAttributeNames, expressionAttributeValues); + break; + case OR: + appendCondition(element.get(new TypeReference<>() { + }), " OR ", filterExpression, expressionAttributeNames, expressionAttributeValues); + break; + case NOT: + filterExpression.append(" NOT "); + condition(element.get(CriteriaCondition.class), filterExpression, expressionAttributeNames, expressionAttributeValues); + break; + default: + throw new IllegalArgumentException("Unknown condition " + condition.condition()); + } + } + + private void predicateIn(Element element, + StringBuilder filterExpression, + Map expressionAttributeNames, + Map expressionAttributeValues) { + var name = element.name(); + + var attributeName = "#" + name; + expressionAttributeNames.put(attributeName, name); + filterExpression.append(attributeName).append(" IN ("); + + List valuesExpressionNames = new LinkedList<>(); + ((Iterable) element.get()).forEach(value -> { + var attributeValueName = ":" + name + "_" + expressionAttributeValues.size(); + valuesExpressionNames.add(attributeValueName); + expressionAttributeValues.put(attributeValueName, toAttributeValue(value)); + }); + + filterExpression.append(String.join(", ", valuesExpressionNames)); + filterExpression.append(") "); + } + + private void appendCondition(List conditions, + String condition, + StringBuilder filterExpression, + Map expressionAttributeNames, + Map expressionAttributeValues) { + + boolean isFirstCondition = true; + for (CriteriaCondition criteriaCondition : conditions) { + StringBuilder tempFilterExpression = new StringBuilder(); + HashMap tempExpressionAttributeNames = new HashMap<>(expressionAttributeNames); + HashMap tempExpressionAttributeValues = new HashMap<>(expressionAttributeValues); + condition(criteriaCondition, + tempFilterExpression, + tempExpressionAttributeNames, + tempExpressionAttributeValues); + if (isFirstCondition && !tempFilterExpression.isEmpty()) { + filterExpression.append(tempFilterExpression); + expressionAttributeNames.putAll(tempExpressionAttributeNames); + expressionAttributeValues.putAll(tempExpressionAttributeValues); + } else if (!tempFilterExpression.isEmpty()) { + if (!filterExpression.substring(filterExpression.length() - condition.length()).equals(condition)) { + filterExpression.append(condition); + } + filterExpression.append(tempFilterExpression); + expressionAttributeNames.putAll(tempExpressionAttributeNames); + expressionAttributeValues.putAll(tempExpressionAttributeValues); + } + isFirstCondition = false; + } + + + } + + private void predicateBetween(Element element, + StringBuilder filterExpression, + Map expressionAttributeNames, + Map expressionAttributeValues) { + + var name = element.name(); + + List values = new ArrayList<>(); + ((Iterable) element.get()).forEach(values::add); + + var attributeName = "#" + name; + expressionAttributeNames.put(attributeName, name); + + filterExpression.append(attributeName).append(" BETWEEN "); + + var fistAttributeValueName = ":" + name + "_" + expressionAttributeValues.size(); + expressionAttributeValues.put(fistAttributeValueName, toAttributeValue(values.get(0))); + filterExpression.append(fistAttributeValueName).append(" AND "); + + var secondAttributeValueName = ":" + name + "_" + expressionAttributeValues.size(); + expressionAttributeValues.put(secondAttributeValueName, toAttributeValue(values.get(1))); + filterExpression.append(secondAttributeValueName); + + } + + private void predicateLike(Element element, + StringBuilder filterExpression, + Map expressionAttributeNames, + Map expressionAttributeValues) { + + var name = element.name(); + var value = toAttributeValue(element.get()); + + var attributeName = "#" + name; + var attributeValueName = ":" + name + "_" + expressionAttributeValues.size(); + + filterExpression.append("begins_with(") + .append(attributeName).append(',') + .append(attributeValueName).append(')'); + + expressionAttributeNames.put(attributeName, name); + expressionAttributeValues.put(attributeValueName, value); + } + + protected void predicate(String operator, + Element element, + StringBuilder filterExpression, + Map expressionAttributeNames, + Map expressionAttributeValues) { + + var name = element.name(); + var value = toAttributeValue(element.get()); + + var attributeName = "#" + name; + var attributeValueName = ":" + name + "_" + expressionAttributeValues.size(); + + filterExpression.append(attributeName).append(operator).append(attributeValueName); + expressionAttributeNames.put(attributeName, name); + expressionAttributeValues.put(attributeValueName, value); + + } + +} diff --git a/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBQuerySelectBuilder.java b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBQuerySelectBuilder.java new file mode 100644 index 000000000..c0ae3d83b --- /dev/null +++ b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBQuerySelectBuilder.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Maximillian Arruda + */ + +package org.eclipse.jnosql.databases.dynamodb.communication; + +import org.eclipse.jnosql.communication.semistructured.CriteriaCondition; +import org.eclipse.jnosql.communication.semistructured.Element; +import org.eclipse.jnosql.communication.semistructured.SelectQuery; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +import java.util.HashMap; + +class DynamoDBQuerySelectBuilder extends DynamoDBQueryBuilder { + + private final String table; + + private final String partitionKey; + + private final SelectQuery selectQuery; + + public DynamoDBQuerySelectBuilder(String table, + String partitionKey, + SelectQuery selectQuery) { + this.table = table; + this.partitionKey = partitionKey; + this.selectQuery = selectQuery; + } + + @Override + public DynamoDBQuery get() { + + var filterExpression = new StringBuilder(); + var expressionAttributeNames = new HashMap(); + var expressionAttributeValues = new HashMap(); + + super.condition( + CriteriaCondition.eq(Element.of(partitionKey, selectQuery.name())), + filterExpression, expressionAttributeNames, expressionAttributeValues); + + this.selectQuery.condition().ifPresent(c -> { + filterExpression.append(" AND "); + super.condition(c, + filterExpression, + expressionAttributeNames, + expressionAttributeValues); + }); + + + return new DynamoDBQuery( + table, + projectionExpression(), + filterExpression.toString(), + expressionAttributeNames, + expressionAttributeValues); + } + + String projectionExpression() { + var columns = selectQuery.columns(); + if (columns.isEmpty()) { + return null; + } + return String.join(", ", columns); + } + + +} diff --git a/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/mapping/DefaultDynamoDBTemplate.java b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/mapping/DefaultDynamoDBTemplate.java new file mode 100644 index 000000000..80f4aecba --- /dev/null +++ b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/mapping/DefaultDynamoDBTemplate.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Maximillian Arruda + */ + +package org.eclipse.jnosql.databases.dynamodb.mapping; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.Typed; +import jakarta.inject.Inject; +import org.eclipse.jnosql.communication.semistructured.DatabaseManager; +import org.eclipse.jnosql.databases.dynamodb.communication.DynamoDBDatabaseManager; +import org.eclipse.jnosql.mapping.core.Converters; +import org.eclipse.jnosql.mapping.metadata.EntitiesMetadata; +import org.eclipse.jnosql.mapping.semistructured.AbstractSemistructuredTemplate; +import org.eclipse.jnosql.mapping.semistructured.EntityConverter; +import org.eclipse.jnosql.mapping.semistructured.EventPersistManager; + +import java.util.Objects; +import java.util.stream.Stream; + +@Typed(DynamoDBTemplate.class) +@ApplicationScoped +class DefaultDynamoDBTemplate extends AbstractSemistructuredTemplate implements DynamoDBTemplate { + + private final Instance manager; + + private final EntityConverter converter; + + private final EventPersistManager persistManager; + + private final EntitiesMetadata entitiesMetadata; + + private final Converters converters; + + @Inject + DefaultDynamoDBTemplate(Instance manager, + EntityConverter converter, + EventPersistManager persistManager, + EntitiesMetadata entitiesMetadata, + Converters converters) { + this.manager = manager; + this.converter = converter; + this.persistManager = persistManager; + this.entitiesMetadata = entitiesMetadata; + this.converters = converters; + } + + /** + * Required by CDI/Reflection/Test purposes + * Don't use it + */ + DefaultDynamoDBTemplate() { + this(null, null, null, null, null); + } + + @Override + protected EntityConverter converter() { + return converter; + } + + @Override + protected DatabaseManager manager() { + return manager.get(); + } + + @Override + protected EventPersistManager eventManager() { + return persistManager; + } + + @Override + protected EntitiesMetadata entities() { + return entitiesMetadata; + } + + @Override + protected Converters converters() { + return converters; + } + + @Override + @SuppressWarnings("unchecked") + public Stream partiQL(String query) { + Objects.requireNonNull(query, "query is required"); + return manager.get().partiQL(query).map(converter::toEntity).map(d -> (T) d); + } + + @Override + @SuppressWarnings("unchecked") + public Stream partiQL(String query, Object... params) { + Objects.requireNonNull(query, "query is required"); + Objects.requireNonNull(params, "params is required"); + return manager.get().partiQL(query, params).map(converter::toEntity).map(d -> (T) d); + } +} diff --git a/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/mapping/DocumentManagerSupplier.java b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/mapping/DocumentManagerSupplier.java new file mode 100644 index 000000000..8013b9485 --- /dev/null +++ b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/mapping/DocumentManagerSupplier.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Maximillian Arruda + */ + +package org.eclipse.jnosql.databases.dynamodb.mapping; + +import jakarta.data.exceptions.MappingException; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Disposes; +import jakarta.enterprise.inject.Produces; +import jakarta.enterprise.inject.Typed; +import org.eclipse.jnosql.communication.Settings; +import org.eclipse.jnosql.databases.dynamodb.communication.DynamoDBDocumentConfiguration; +import org.eclipse.jnosql.databases.dynamodb.communication.DynamoDBDatabaseManager; +import org.eclipse.jnosql.mapping.core.config.MicroProfileSettings; + +import java.util.Optional; +import java.util.function.Supplier; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static org.eclipse.jnosql.mapping.core.config.MappingConfigurations.DOCUMENT_DATABASE; + +@ApplicationScoped +public class DocumentManagerSupplier implements Supplier { + + private static final Logger LOGGER = Logger.getLogger(DocumentManagerSupplier.class.getName()); + + @Override + @Produces + @Typed(DynamoDBDatabaseManager.class) + @ApplicationScoped + public DynamoDBDatabaseManager get() { + Settings settings = MicroProfileSettings.INSTANCE; + var configuration = new DynamoDBDocumentConfiguration(); + var factory = configuration.apply(settings); + Optional database = settings.get(DOCUMENT_DATABASE, String.class); + String db = database.orElseThrow(() -> new MappingException("Please, inform the database filling up the property " + + DOCUMENT_DATABASE.get())); + DynamoDBDatabaseManager manager = (DynamoDBDatabaseManager) factory.apply(db); + if (LOGGER.isLoggable(Level.FINEST)) { + LOGGER.log(Level.FINEST, """ + Starting a DynamoDBDocumentManager instance using Eclipse MicroProfile Config,\ + database name: %s + """.formatted(db)); + } + return manager; + } + + public void close(@Disposes DynamoDBDatabaseManager manager) { + if (LOGGER.isLoggable(Level.FINEST)) { + LOGGER.log(Level.FINEST, "Closing OracleDocumentManager resource, database name: %s".formatted(manager.name())); + } + manager.close(); + } +} diff --git a/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/mapping/DynamoDBExtension.java b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/mapping/DynamoDBExtension.java new file mode 100644 index 000000000..37fe6a6c7 --- /dev/null +++ b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/mapping/DynamoDBExtension.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Maximillian Arruda + */ + +package org.eclipse.jnosql.databases.dynamodb.mapping; + +import jakarta.enterprise.event.Observes; +import jakarta.enterprise.inject.spi.AfterBeanDiscovery; +import jakarta.enterprise.inject.spi.Extension; +import org.eclipse.jnosql.mapping.metadata.ClassScanner; + +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class DynamoDBExtension implements Extension { + + private static final Logger LOGGER = Logger.getLogger(DynamoDBExtension.class.getName()); + + void onAfterBeanDiscovery(@Observes final AfterBeanDiscovery afterBeanDiscovery) { + + ClassScanner scanner = ClassScanner.load(); + Set> crudTypes = scanner.repositories(DynamoDBRepository.class); + + if (LOGGER.isLoggable(Level.INFO)) + LOGGER.info("Starting the onAfterBeanDiscovery with elements number: %s".formatted(crudTypes.size())); + + crudTypes.forEach(type -> afterBeanDiscovery.addBean(new DynamoDBRepositoryBean<>(type))); + + if (LOGGER.isLoggable(Level.INFO)) + LOGGER.info("Finished the onAfterBeanDiscovery"); + } +} diff --git a/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/mapping/DynamoDBRepository.java b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/mapping/DynamoDBRepository.java new file mode 100644 index 000000000..7d741cd0c --- /dev/null +++ b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/mapping/DynamoDBRepository.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Maximillian Arruda + */ + +package org.eclipse.jnosql.databases.dynamodb.mapping; + +import org.eclipse.jnosql.mapping.NoSQLRepository; + +public interface DynamoDBRepository extends NoSQLRepository { +} diff --git a/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/mapping/DynamoDBRepositoryBean.java b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/mapping/DynamoDBRepositoryBean.java new file mode 100644 index 000000000..be50d23de --- /dev/null +++ b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/mapping/DynamoDBRepositoryBean.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Maximillian Arruda + */ + +package org.eclipse.jnosql.databases.dynamodb.mapping; + +import jakarta.enterprise.context.spi.CreationalContext; +import jakarta.enterprise.inject.Default; +import jakarta.enterprise.util.AnnotationLiteral; +import org.eclipse.jnosql.mapping.core.Converters; +import org.eclipse.jnosql.mapping.core.spi.AbstractBean; +import org.eclipse.jnosql.mapping.metadata.EntitiesMetadata; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Proxy; +import java.lang.reflect.Type; +import java.util.Collections; +import java.util.Set; + +class DynamoDBRepositoryBean extends AbstractBean> { + + private final Class type; + + private final Set types; + + private final Set qualifiers = Collections.singleton(new AnnotationLiteral() { + }); + + @SuppressWarnings({"rawtypes", "unchecked"}) + DynamoDBRepositoryBean(Class type) { + this.type = type; + this.types = Collections.singleton(type); + } + + @Override + public Class getBeanClass() { + return type; + } + + @Override + @SuppressWarnings("unchecked") + public DynamoDBRepository create(CreationalContext> creationalContext) { + + DynamoDBTemplate template = getInstance(DynamoDBTemplate.class); + Converters converters = getInstance(Converters.class); + EntitiesMetadata entitiesMetadata = getInstance(EntitiesMetadata.class); + + DynamoDBRepositoryProxy handler = new DynamoDBRepositoryProxy<>( + template, type, converters, entitiesMetadata); + + return (DynamoDBRepository) Proxy.newProxyInstance(type.getClassLoader(), + new Class[]{type}, + handler); + } + + @Override + public Set getTypes() { + return types; + } + + @Override + public Set getQualifiers() { + return qualifiers; + } + + @Override + public String getId() { + return type.getName() + '@' + "dynamodb"; + } + +} diff --git a/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/mapping/DynamoDBRepositoryProxy.java b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/mapping/DynamoDBRepositoryProxy.java new file mode 100644 index 000000000..6cb9cd3d4 --- /dev/null +++ b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/mapping/DynamoDBRepositoryProxy.java @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Maximillian Arruda + */ + +package org.eclipse.jnosql.databases.dynamodb.mapping; + +import jakarta.data.repository.Param; +import jakarta.inject.Inject; +import org.eclipse.jnosql.mapping.core.Converters; +import org.eclipse.jnosql.mapping.core.query.AbstractRepository; +import org.eclipse.jnosql.mapping.core.repository.DynamicReturn; +import org.eclipse.jnosql.mapping.metadata.EntitiesMetadata; +import org.eclipse.jnosql.mapping.metadata.EntityMetadata; +import org.eclipse.jnosql.mapping.semistructured.SemistructuredTemplate; +import org.eclipse.jnosql.mapping.semistructured.query.AbstractSemistructuredRepositoryProxy; +import org.eclipse.jnosql.mapping.semistructured.query.SemistructuredRepositoryProxy; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Stream; + +import static org.eclipse.jnosql.mapping.core.repository.DynamicReturn.toSingleResult; + +class DynamoDBRepositoryProxy extends AbstractSemistructuredRepositoryProxy { + + private final DynamoDBTemplate template; + + private final Class type; + + private final Converters converters; + + private final Class typeClass; + + private final EntityMetadata entityMetadata; + + private final AbstractRepository repository; + + @Inject + @SuppressWarnings("unchecked") + DynamoDBRepositoryProxy(DynamoDBTemplate template, + Class type, + Converters converters, + EntitiesMetadata entitiesMetadata) { + + this.template = template; + this.type = type; + this.typeClass = (Class) ((ParameterizedType) type.getGenericInterfaces()[0]).getActualTypeArguments()[0]; + this.converters = converters; + this.entityMetadata = entitiesMetadata.get(typeClass); + this.repository = SemistructuredRepositoryProxy.SemistructuredRepository.of(template, entityMetadata); + } + + /** + * Required by CDI/Reflection/Test purposes + * Don't use it + */ + DynamoDBRepositoryProxy() { + this.template = null; + this.type = null; + this.typeClass = null; + this.converters = null; + this.entityMetadata = null; + this.repository = null; + } + + @Override + @SuppressWarnings("unchecked") + public Object invoke(Object instance, Method method, Object[] args) throws Throwable { + PartiQL sql = method.getAnnotation(PartiQL.class); + if (Objects.nonNull(sql)) { + Stream result; + List params = getParams(args, method); + if (params.isEmpty()) { + result = template.partiQL(sql.value()); + } else { + result = template.partiQL(sql.value(), params.toArray()); + } + return DynamicReturn.builder() + .withClassSource(typeClass) + .withMethodSource(method) + .withResult(() -> result) + .withSingleResult(toSingleResult(method).apply(() -> result)) + .build().execute(); + } + return super.invoke(instance, method, args); + } + + private List getParams(Object[] args, Method method) { + + List params = new ArrayList<>(); + Annotation[][] annotations = method.getParameterAnnotations(); + + for (int index = 0; index < annotations.length; index++) { + + final Object arg = args[index]; + + Optional param = Stream.of(annotations[index]) + .filter(Param.class::isInstance) + .map(Param.class::cast) + .findFirst(); + param.ifPresent(p -> params.add(arg)); + + } + + return params; + } + + @Override + protected Converters converters() { + return converters; + } + + @Override + @SuppressWarnings({"rawtypes","unchecked"}) + protected AbstractRepository repository() { + return repository; + } + + @Override + protected Class repositoryType() { + return type; + } + + @Override + protected EntityMetadata entityMetadata() { + return entityMetadata; + } + + @Override + protected SemistructuredTemplate template() { + return template; + } +} diff --git a/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/mapping/DynamoDBTemplate.java b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/mapping/DynamoDBTemplate.java new file mode 100644 index 000000000..57bd7c557 --- /dev/null +++ b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/mapping/DynamoDBTemplate.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Maximillian Arruda + */ + +package org.eclipse.jnosql.databases.dynamodb.mapping; + +import org.eclipse.jnosql.mapping.document.DocumentTemplate; + +import java.util.stream.Stream; + +/** + * The {@code DynamoDBTemplate} is an interface that extends {@link DocumentTemplate} and + * provides methods for executing Dynamo DB queries using the PartiQL Language. + *

DynamoDB supports a limited subset of + * PartiQL. + *

+ *

+ * It allows you to interact with the DynamoDB database using PartiQL queries to retrieve and + * process data in a more flexible and customizable way. + *

+ * + * @see DocumentTemplate + */ +public interface DynamoDBTemplate extends DocumentTemplate { + + /** + * Executes a DynamoDB query using + * PartiQL. + * + * @param query the PartiQL query + * @return a {@link Stream} of results representing the query result + * @throws NullPointerException when the query is null + */ + Stream partiQL(String query); + + /** + * Executes a DynamoDB query using + * PartiQL with parameters. + *

Example query: {@code SELECT * FROM users WHERE status = ?}

+ * + * @param query the PartiQL query + * @return a {@link Stream} of results representing the query result + * @throws NullPointerException when the query is null + */ + Stream partiQL(String query, Object... params); +} diff --git a/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/mapping/PartiQL.java b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/mapping/PartiQL.java new file mode 100644 index 000000000..128919a01 --- /dev/null +++ b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/mapping/PartiQL.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Maximillian Arruda + */ + +package org.eclipse.jnosql.databases.dynamodb.mapping; + + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * The {@code PartiQL} annotation is a custom annotation used to associate PartiQL query strings + * with methods or elements in your code. + * + *

DynamoDB supports a limited subset of + * PartiQL. + *

+ *

+ *

+ * When applied to a method, field, or other element, this annotation provides a convenient + * way to specify an PartiQL query that should be associated with that element. + *

+ *

+ * For example, you can use this annotation to specify a PartiQL query for a method as follows: + *

+ *
+ * {@code
+ * @PartiQL("SELECT * FROM users WHERE status = 'active'")
+ * public void getUserData() {
+ *     // Method implementation
+ * }
+ * }
+ * 
+ *

+ * In the context of an {@code DynamoDBRepository}, you can use the {@code @PartiQL} annotation + * to define custom PartiQL queries for repository methods. Here's an example: + *

+ *
+ * {@code
+ * public interface UserRepository extends DynamoDBRepository {
+ *     // Find all active users using a custom SQL query
+ *     @PartiQL("SELECT * FROM users WHERE status = 'active'")
+ *     List findActiveUsers();
+ * }
+ * }
+ * 
+ *

+ * In this example, the {@code @PartiQL} annotation is used to define a custom PartiQL query for the + * {@code findActiveUsers} method in an {@code DynamoDBRepository} interface, allowing you + * to execute the specified PartiQL query when calling the repository method. + *

+ */ + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface PartiQL { + + /** + * The PartiQL query to associated with the annotated method or element. + * + *

DynamoDB supports a limited subset of + * PartiQL. + *

+ * + * @return the PartiQL query + */ + String value(); +} diff --git a/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/package-info.java b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/package-info.java new file mode 100644 index 000000000..62858f422 --- /dev/null +++ b/jnosql-dynamodb/src/main/java/org/eclipse/jnosql/databases/dynamodb/package-info.java @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Maximillian Arruda + */ + +package org.eclipse.jnosql.databases.dynamodb; \ No newline at end of file diff --git a/jnosql-dynamodb/src/main/resources/META-INF/beans.xml b/jnosql-dynamodb/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..6130628ce --- /dev/null +++ b/jnosql-dynamodb/src/main/resources/META-INF/beans.xml @@ -0,0 +1,21 @@ + + + + \ No newline at end of file diff --git a/jnosql-dynamodb/src/main/resources/META-INF/services/jakarta.enterprise.inject.spi.Extension b/jnosql-dynamodb/src/main/resources/META-INF/services/jakarta.enterprise.inject.spi.Extension new file mode 100644 index 000000000..cd65d5e74 --- /dev/null +++ b/jnosql-dynamodb/src/main/resources/META-INF/services/jakarta.enterprise.inject.spi.Extension @@ -0,0 +1,15 @@ +# +# Copyright (c) 2024 Contributors to the Eclipse Foundation +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v1.0 +# and Apache License v2.0 which accompanies this distribution. +# The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html +# and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. +# +# You may elect to redistribute this code under either of these licenses. +# +# Contributors: +# +# Maximillian Arruda +# +org.eclipse.jnosql.databases.dynamodb.mapping.DynamoDBExtension \ No newline at end of file diff --git a/jnosql-dynamodb/src/main/resources/META-INF/services/org.eclipse.jnosql.communication.semistructured.DatabaseConfiguration b/jnosql-dynamodb/src/main/resources/META-INF/services/org.eclipse.jnosql.communication.semistructured.DatabaseConfiguration new file mode 100644 index 000000000..3d6d49086 --- /dev/null +++ b/jnosql-dynamodb/src/main/resources/META-INF/services/org.eclipse.jnosql.communication.semistructured.DatabaseConfiguration @@ -0,0 +1 @@ +org.eclipse.jnosql.databases.dynamodb.communication.DynamoDBDocumentConfiguration \ No newline at end of file diff --git a/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/communication/CommunicationEntityGenerator.java b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/communication/CommunicationEntityGenerator.java new file mode 100644 index 000000000..e5a72205f --- /dev/null +++ b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/communication/CommunicationEntityGenerator.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2023 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Maximillian Arruda + */ + +package org.eclipse.jnosql.databases.dynamodb.communication; + +import net.datafaker.Faker; +import org.eclipse.jnosql.communication.semistructured.CommunicationEntity; +import org.eclipse.jnosql.communication.semistructured.Element; +import org.eclipse.jnosql.communication.semistructured.Elements; +import org.jetbrains.annotations.NotNull; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.function.Consumer; + +final class CommunicationEntityGenerator { + + static final String COLLECTION_NAME = "entityA"; + static final Faker faker = new Faker(); + + static CommunicationEntity createRandomEntity() { + return createRandomEntityWithSubDocuments(0); + } + + static CommunicationEntity createRandomEntity(String collectionName) { + return createRandomEntityWithSubDocuments(collectionName,0); + } + + static CommunicationEntity createRandomEntityWithSubDocuments(int levels) { + return createRandomEntityWithSubDocuments(COLLECTION_NAME, levels); + } + + @NotNull + private static CommunicationEntity createRandomEntityWithSubDocuments(String collectionName, int levels) { + Map map = new HashMap<>(); + map.put(DynamoDBConverter.ID, UUID.randomUUID().toString()); + map.put("name", faker.name().firstName()); + map.put("hoje", LocalDate.now()); + map.put("agora", LocalDateTime.now()); + map.put("guessingNumber", faker.random().nextInt(1, 10)); + map.put("bigdecimal", BigDecimal.valueOf(10.10)); + map.put("city", faker.address().city()); + map.put("texts", List.of("A", "B", "C")); + if (levels > 0) { + addSubDocument(m -> map.put("level" + levels, m), levels - 1); + } + var entity = CommunicationEntity.of(collectionName); + List documents = Elements.of(map); + documents.forEach(entity::add); + return entity; + } + + static void addSubDocument(Consumer> owner, int level) { + Map map = new HashMap<>(); + map.put("level", level); + map.put("text", UUID.randomUUID().toString()); + map.put("hoje", LocalDate.now()); + map.put("agora", LocalDateTime.now()); + map.put("integerNumber", faker.random().nextInt(1, 10)); + map.put("floatNumber", (float) faker.random().nextDouble(1.0, 10.0)); + map.put("doubleNumber", faker.random().nextDouble(1.0, 10.0)); + map.put("bigdecimal", BigDecimal.valueOf(10.10)); + if (level > 0) { + addSubDocument(m -> map.put("level" + level, m), level - 1); + } + owner.accept(map); + } +} \ No newline at end of file diff --git a/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/communication/DefaultDynamoDBDatabaseManagerFactoryTest.java b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/communication/DefaultDynamoDBDatabaseManagerFactoryTest.java new file mode 100644 index 000000000..08b2b8ece --- /dev/null +++ b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/communication/DefaultDynamoDBDatabaseManagerFactoryTest.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2023 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Maximillian Arruda + */ + +package org.eclipse.jnosql.databases.dynamodb.communication; + +import org.eclipse.jnosql.communication.semistructured.DatabaseManagerFactory; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; + +import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static org.eclipse.jnosql.communication.driver.IntegrationTest.MATCHES; +import static org.eclipse.jnosql.communication.driver.IntegrationTest.NAMED; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +@EnabledIfSystemProperty(named = NAMED, matches = MATCHES) +class DefaultDynamoDBDatabaseManagerFactoryTest { + + private DatabaseManagerFactory databaseManagerFactory; + + @BeforeEach + void setup() { + this.databaseManagerFactory = DynamoDBTestUtils.CONFIG.getDocumentManagerFactory(); + assertSoftly(softly -> { + softly.assertThat(databaseManagerFactory).isNotNull(); + softly.assertThat(databaseManagerFactory).isInstanceOf(DynamoDBDatabaseManagerFactory.class); + }); + } + @AfterEach + void tearDown() { + assertDoesNotThrow(databaseManagerFactory::close, "DocumentManagerFactory.close() should be not throw exceptions"); + } + @Test + void shouldCreateDocumentManager() { + var documentManager = databaseManagerFactory.apply("anydatabase"); + assertSoftly(softly -> { + softly.assertThat(documentManager).isNotNull(); + }); + } + +} diff --git a/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/communication/DefaultDynamoDBDatabaseManagerTest.java b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/communication/DefaultDynamoDBDatabaseManagerTest.java new file mode 100644 index 000000000..7f7fd303c --- /dev/null +++ b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/communication/DefaultDynamoDBDatabaseManagerTest.java @@ -0,0 +1,453 @@ +/* + * Copyright (c) 2023 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Maximillian Arruda + */ + +package org.eclipse.jnosql.databases.dynamodb.communication; + + +import net.datafaker.Faker; +import org.assertj.core.api.Assertions; +import org.assertj.core.api.SoftAssertions; +import org.eclipse.jnosql.communication.Settings; +import org.eclipse.jnosql.communication.semistructured.CommunicationEntity; +import org.eclipse.jnosql.communication.semistructured.DatabaseManager; +import org.eclipse.jnosql.communication.semistructured.Element; +import org.eclipse.jnosql.mapping.core.config.MappingConfigurations; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; +import software.amazon.awssdk.services.dynamodb.model.DescribeTableRequest; +import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; +import software.amazon.awssdk.services.dynamodb.model.ResourceNotFoundException; + +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.function.BiConsumer; +import java.util.function.UnaryOperator; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static org.eclipse.jnosql.communication.driver.IntegrationTest.MATCHES; +import static org.eclipse.jnosql.communication.driver.IntegrationTest.NAMED; +import static org.eclipse.jnosql.communication.semistructured.DeleteQuery.delete; +import static org.eclipse.jnosql.communication.semistructured.SelectQuery.select; +import static org.eclipse.jnosql.databases.dynamodb.communication.CommunicationEntityGenerator.createRandomEntity; +import static org.eclipse.jnosql.databases.dynamodb.communication.DynamoDBConverter.ID; +import static org.eclipse.jnosql.databases.dynamodb.communication.DynamoDBTestUtils.CONFIG; + +@EnabledIfSystemProperty(named = NAMED, matches = MATCHES) +class DefaultDynamoDBDatabaseManagerTest { + + private static Faker faker = new Faker(); + + private DynamoDbClient dynamoDbClient; + + private UnaryOperator entityNameResolver; + + @BeforeEach + void setUp() { + var settings = CONFIG.getSettings(); + entityNameResolver = entityName -> settings.get(DynamoDBConfigurations.ENTITY_PARTITION_KEY, String.class).orElse(entityName); + dynamoDbClient = CONFIG.getDynamoDbClient(settings); + tearDown(); + } + + private DatabaseManager getDatabaseManagerCannotCreateTables() { + var settings = CONFIG.customSetting(Settings.builder() + .put(DynamoDBConfigurations.CREATE_TABLES, "false")); + var database = settings.get(MappingConfigurations.DOCUMENT_DATABASE, String.class).orElseThrow(); + var documentManagerFactory = CONFIG.getDocumentManagerFactory(settings); + return documentManagerFactory.apply(database); + } + + private DatabaseManager getDatabaseManagerCanCreateTables() { + var settings = CONFIG.customSetting(Settings.builder() + .put(DynamoDBConfigurations.CREATE_TABLES, "true")); + var database = settings.get(MappingConfigurations.DOCUMENT_DATABASE, String.class).orElseThrow(); + var documentManagerFactory = CONFIG.getDocumentManagerFactory(settings); + return documentManagerFactory.apply(database); + } + + + @AfterEach + void tearDown() { + dynamoDbClient.listTables() + .tableNames() + .forEach(tableName -> + dynamoDbClient.deleteTable(DeleteTableRequest.builder().tableName(tableName).build()) + ); + } + + @Test + void shouldReturnName() { + try (var manager = getDatabaseManagerCannotCreateTables()) { + var database = CONFIG + .getSettings() + .get(MappingConfigurations.DOCUMENT_DATABASE, String.class).orElseThrow(); + Assertions.assertThat(manager.name()).isEqualTo(database); + } + } + + @Test + void shouldReturnErrorWhenInsertWithInvalidInputs() { + + try (var manager = getDatabaseManagerCannotCreateTables()) { + assertSoftly(softly -> { + softly.assertThatThrownBy(() -> manager.insert((CommunicationEntity) null)) + .as("should return error when insert a null DocumentEntity reference") + .isExactlyInstanceOf(NullPointerException.class); + softly.assertThatThrownBy(() -> manager.insert((CommunicationEntity) null, Duration.ofSeconds(1))) + .as("should return error when insert a null DocumentEntity reference with TTL param") + .isInstanceOfAny(NullPointerException.class); + softly.assertThatThrownBy(() -> manager.insert((CommunicationEntity) null, null)) + .as("should return error when insert a null DocumentEntity reference with nullable TTL param") + .isInstanceOfAny(NullPointerException.class); + softly.assertThatThrownBy(() -> manager.insert(CommunicationEntityGenerator.createRandomEntity(), null)) + .as("should return error when insert a null DocumentEntity reference with nullable TTL param") + .isInstanceOfAny(NullPointerException.class); + softly.assertThatThrownBy(() -> manager.insert((Iterable) null)) + .as("should return error when insert a null Iterable reference") + .isInstanceOfAny(NullPointerException.class); + softly.assertThatThrownBy(() -> manager.insert((Iterable) null, Duration.ofSeconds(1))) + .as("should return error when insert a null Iterable reference with TTL param") + .isInstanceOfAny(NullPointerException.class); + softly.assertThatThrownBy(() -> manager.insert(List.of(CommunicationEntityGenerator.createRandomEntity()), null)) + .as("should return error when insert a null Iterable reference with nullable TTL param") + .isInstanceOfAny(NullPointerException.class); + }); + } + } + + @Test + void shouldInsert() { + + try (var manager = getDatabaseManagerCanCreateTables()) { + + assertSoftly(softly -> { + var entity = createRandomEntity(); + var _entityType = entity.name(); + var id = entity.find(DynamoDBConverter.ID, String.class).orElseThrow(); + var persistedEntity = manager.insert(entity); + + softly.assertThat(persistedEntity) + .as("manager.insert(DocumentEntity) method should return a non-null persistent DocumentEntity") + .isNotNull(); + + var persistedItem = getItem(_entityType, id); + + softly.assertThat(persistedItem).as("should return the item from dynamodb").isNotNull(); + }); + + assertSoftly(softly -> { + var entities = List.of(createRandomEntity(), createRandomEntity(), createRandomEntity()); + var persistedEntities = manager.insert(entities); + softly.assertThat(persistedEntities) + .as("manager.insert(Iterable<>) should returns the non-null list of DocumentEntity").isNotNull(); + + assertThat(persistedEntities) + .as("documentmanager.insert(iterable<>) should returns a corresponded list of DocumentEntity") + .hasSize(3); + + persistedEntities.forEach(entity -> { + var _entityType = entity.name(); + var id = entity.find(ID, String.class).orElseThrow(); + var persistedItem = getItem(_entityType, id); + softly.assertThat(persistedItem) + .as("all items of the list of DocumentEntity should be stored on dynamodb database. the entity %s not found" + .formatted(id)) + .isNotNull(); + }); + }); + } + } + + @Test + void shouldReturnErrorWhenUpdateWithInvalidInputs() { + + try (var manager = getDatabaseManagerCannotCreateTables()) { + assertSoftly(softly -> { + softly.assertThatThrownBy(() -> manager.update((CommunicationEntity) null)) + .as("should return error when insert a null DocumentEntity reference") + .isExactlyInstanceOf(NullPointerException.class); + softly.assertThatThrownBy(() -> manager.update((Iterable) null)) + .as("should return error when insert a null Iterable reference") + .isInstanceOfAny(NullPointerException.class); + }); + } + } + + @Test + void shouldUpdate() { + try (var manager = getDatabaseManagerCanCreateTables()) { + + var entity1 = createRandomEntity(); + var entity2 = createRandomEntity(); + var entity3 = createRandomEntity(); + + manager.insert(List.of(entity1, entity2, entity3)); + + final BiConsumer assertions = (softly, updatedEntity) -> { + Map item = getItem(updatedEntity.name(), updatedEntity.find(DynamoDBConverter.ID, String.class).orElseThrow()); + softly.assertThat(item.get("name")) + .as("the name attribute should exists in the returned item from dynamodb") + .isNotNull(); + softly.assertThat(item.get("name").s()) + .as("the name attribute should had be updated successfully") + .isEqualTo(updatedEntity.find("name", String.class).orElse(null)); + }; + + assertSoftly(softly -> { + entity1.add(Element.of("name", faker.name().fullName())); + var updatedEntity = manager.update(entity1); + softly.assertThat(updatedEntity) + .as("manager.update(DocumentEntity) method should return a non-null persistent DocumentEntity") + .isNotNull(); + assertions.accept(softly, updatedEntity); + }); + + assertSoftly(softly -> { + entity2.add(Element.of("name", faker.name().fullName())); + entity3.add(Element.of("name", faker.name().fullName())); + + var updatedEntities = manager.update(List.of(entity2, entity2)); + softly.assertThat(updatedEntities) + .as("manager.update(Iterable<>) method should return a non-null list of DocumentEntity") + .isNotNull(); + softly.assertThat(updatedEntities) + .as("the size of the returned list of DocumentEntity from " + + "manager.update(Iterable<>) method should be equals to the size of the submitted list of DocumentEntity") + .hasSize(2); + updatedEntities.forEach(updatedEntity -> assertions.accept(softly, updatedEntity)); + }); + } + } + + private Map getItem(String _entityType, String id) { + return dynamoDbClient + .getItem(GetItemRequest.builder() + .tableName(_entityType) + .key(Map.of( + entityNameResolver.apply(_entityType), AttributeValue.builder().s(_entityType).build(), + DynamoDBConverter.ID, AttributeValue.builder().s(id).build() + )) + .build()) + .item(); + } + + + @Test + void shouldCountByCollectionName() { + + try (var dmCanCreateTable = getDatabaseManagerCanCreateTables(); + var dmCannotCreateTable = getDatabaseManagerCannotCreateTables()) { + assertSoftly(softly -> { + + var entity = createRandomEntity(); + var entity2 = createRandomEntity(); + + dmCanCreateTable.insert(entity); + dmCanCreateTable.insert(entity2); + + softly.assertThatThrownBy(() -> dmCanCreateTable.count((String) null)) + .as("should return an error when a nullable String is passed as arg") + .isInstanceOfAny(NullPointerException.class); + + softly.assertThat(dmCanCreateTable.count(entity.name())) + .as("the returned count number of items from an given existent table name is incorrect") + .isEqualTo(2L); + + String nonExistentTable = UUID.randomUUID().toString(); + + softly.assertThat(dmCannotCreateTable.count(nonExistentTable)) + .as("the returned count number of items from a given an non-existent table name is incorrect") + .isEqualTo(0L); + + softly.assertThatThrownBy(() -> dynamoDbClient + .describeTable(DescribeTableRequest + .builder() + .tableName(nonExistentTable + ).build())) + .as("it must not create a table") + .isInstanceOfAny(ResourceNotFoundException.class); + + var entityBName = "entityB"; + var entity3 = createRandomEntity(entityBName); + dmCanCreateTable.insert(entity3); + + softly.assertThat(dmCannotCreateTable.count(entity3.name())) + .as("the returned count number of items from a given table name is incorrect") + .isEqualTo(1L); + }); + + } + } + + @Test + void shouldDelete() { + try (var manager = getDatabaseManagerCanCreateTables()) { + + CommunicationEntity entity1, entity2, entity3, entity4; + + var entities = List.of( + entity1 = createRandomEntity(), + entity2 = createRandomEntity(), + entity3 = createRandomEntity(), + entity4 = createRandomEntity()); + + manager.insert(entities); + + var entityType = entity1.name(); + var id1 = entity1.find(ID, String.class).orElseThrow(); + var id2 = entity2.find(ID, String.class).orElseThrow(); + var id3 = entity3.find(ID, String.class).orElseThrow(); + var id4 = entity4.find(ID, String.class).orElseThrow(); + + assertSoftly(softly -> { + + manager.delete(delete(). + from(entityType) + .where(ID).eq(id1) + .build() + ); + + softly.assertThat(manager.count(entityType)) + .as("the returned count number of items from a given table name is incorrect") + .isEqualTo(entities.size() - 1L); + + softly.assertThat(getItem(entityType, id1)) + .as("the item should be deleted") + .hasSize(0); + + manager.delete(delete(). + from(entityType) + .where(ID).in(List.of(id2, id3)) + .build() + ); + + softly.assertThat(manager.count(entityType)) + .as("the returned count number of items from a given table name is incorrect") + .isEqualTo(entities.size() - 3L); + + softly.assertThat(getItem(entityType, id2)) + .as("the item should be deleted") + .hasSize(0); + + softly.assertThat(getItem(entityType, id3)) + .as("the item should be deleted") + .hasSize(0); + + + manager.delete(delete(). + from(entityType) + .build() + ); + + softly.assertThat(getItem(entityType, id4)) + .as("the item should be deleted") + .hasSize(0); + + softly.assertThat(manager.count(entityType)) + .as("the returned count number of items from a given table name is incorrect") + .isEqualTo(0); + + + }); + } + } + + @Test + void shouldCountByDocumentQuery() { + + try (var manager = getDatabaseManagerCanCreateTables()) { + + CommunicationEntity entity1, entity2, entity3; + + var entities = List.of(entity1 = createRandomEntity(), entity2 = createRandomEntity(), entity3 = createRandomEntity()); + + manager.insert(entities); + + assertSoftly(softly -> { + + var documentQuery1 = select() + .from(entity1.name()) + .where(ID).eq(entity1.find(ID, String.class).orElseThrow()) + .build(); + + softly.assertThat(manager.count(documentQuery1)) + .as("the returned count number of items from a given DocumentQuery is incorrect") + .isEqualTo(1L); + + + var documentQuery2 = select() + .from(entity1.name()) + .where(ID).eq(entity1.find(ID, String.class).orElseThrow()) + .or(ID).eq(entity2.find(ID, String.class).orElseThrow()) + .build(); + + softly.assertThat(manager.count(documentQuery2)) + .as("the returned count number of items from a given DocumentQuery is incorrect") + .isEqualTo(2L); + + var documentQuery3 = select() + .from(entity1.name()) + .where(ID).eq(entity1.find(ID, String.class).orElseThrow()) + .or(ID).eq(entity2.find(ID, String.class).orElseThrow()) + .or(ID).eq(entity3.find(ID, String.class).orElseThrow()) + .build(); + + softly.assertThat(manager.count(documentQuery3)) + .as("the returned count number of items from a given DocumentQuery is incorrect") + .isEqualTo(3L); + + }); + } + } + + @Test + void shouldExecutePartiQL() { + + try (var manager = getDatabaseManagerCanCreateTables()) { + + CommunicationEntity entity1; + var entities = List.of(entity1 = createRandomEntity(), createRandomEntity(), createRandomEntity()); + manager.insert(entities); + + if (manager instanceof DynamoDBDatabaseManager partiManager) { + + assertSoftly(softly -> { + softly.assertThat(partiManager.partiQL("SELECT * FROM " + entity1.name())) + .as("the returned count number of items from a given DocumentQuery is incorrect") + .hasSize(3); + + }); + + assertSoftly(softly -> { + softly.assertThat(partiManager.partiQL(""" + SELECT * FROM %s WHERE %s = ? + """.formatted(entity1.name(), ID), + entity1.find(ID).orElseThrow().get())) + .as("the returned count number of items from a given DocumentQuery is incorrect") + .hasSize(1); + + }); + } + } + } +} diff --git a/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBConverterTest.java b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBConverterTest.java new file mode 100644 index 000000000..67f3c56ee --- /dev/null +++ b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBConverterTest.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2023 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Maximillian Arruda + */ + +package org.eclipse.jnosql.databases.dynamodb.communication; + +import jakarta.json.Json; +import jakarta.json.bind.Jsonb; +import net.datafaker.Faker; +import org.eclipse.jnosql.communication.driver.JsonbSupplier; +import org.eclipse.jnosql.communication.semistructured.CommunicationEntity; +import org.eclipse.jnosql.communication.semistructured.Element; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; + +import java.io.StringReader; +import java.util.List; +import java.util.UUID; +import java.util.function.UnaryOperator; + +import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static org.eclipse.jnosql.communication.driver.IntegrationTest.MATCHES; +import static org.eclipse.jnosql.communication.driver.IntegrationTest.NAMED; + +@EnabledIfSystemProperty(named = NAMED, matches = MATCHES) +class DynamoDBConverterTest { + + static final Faker faker = new Faker(); + + private static final Jsonb JSONB = JsonbSupplier.getInstance().get(); + + private static final UnaryOperator entityNameResolver = UnaryOperator.identity(); + + @Test + void shouldConvertToItemRequest() { + + assertSoftly(softly -> { + var entity = CommunicationEntity.of("entityA", + List.of( + Element.of("_id", UUID.randomUUID().toString()), + Element.of("city", faker.address().city()), + Element.of("total", 10.0), + Element.of("address", List.of( + Element.of("zipcode", faker.address().zipCode()), + Element.of("city", faker.address().cityName()))), + Element.of("phones", List.of(faker.name().firstName(), faker.name().firstName(), faker.name().firstName())) + )); + + var item = DynamoDBConverter.toItem(entityNameResolver, entity); + + var entityFromItem = DynamoDBConverter.toCommunicationEntity(entityNameResolver, item); + + var expected = Json.createReader(new StringReader(JSONB.toJson(DynamoDBConverter.getMap(entityNameResolver, entity)))).readObject(); + + var actual = Json.createReader(new StringReader(JSONB.toJson(DynamoDBConverter.getMap(entityNameResolver, entityFromItem)))).readObject(); + + softly.assertThat(actual).as("cannot convert a simple DocumentEntity") + .isEqualTo(expected); + }); + + + } + +} diff --git a/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBDocumentConfigurationTest.java b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBDocumentConfigurationTest.java new file mode 100644 index 000000000..10b9122e0 --- /dev/null +++ b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBDocumentConfigurationTest.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2023 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Maximillian Arruda + */ +package org.eclipse.jnosql.databases.dynamodb.communication; + + +import org.eclipse.jnosql.communication.semistructured.DatabaseConfiguration; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; + +import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static org.eclipse.jnosql.communication.driver.IntegrationTest.MATCHES; +import static org.eclipse.jnosql.communication.driver.IntegrationTest.NAMED; + +@EnabledIfSystemProperty(named = NAMED, matches = MATCHES) +class DynamoDBDocumentConfigurationTest { + + @Test + void shouldReturnFromServiceLoaderConfiguration() { + var configuration = DatabaseConfiguration.getConfiguration(); + Assertions.assertNotNull(configuration); + Assertions.assertInstanceOf(DatabaseConfiguration.class, configuration); + } + + @Test + void shouldReturnFromServiceLoaderConfigurationQuery() { + var configuration = DatabaseConfiguration + .getConfiguration(DynamoDBDocumentConfiguration.class); + Assertions.assertNotNull(configuration); + } + + @Test + void shouldReturnDocumentManagerFactory() { + var configuration = DatabaseConfiguration + .getConfiguration(DynamoDBDocumentConfiguration.class); + + var settings = DynamoDBTestUtils.CONFIG.getSettings(); + + assertSoftly(softly -> { + softly.assertThat(configuration) + .describedAs("DocumentConfiguration.getConfiguration(DynamoDBDocumentConfiguration.class) must return a non-null instance") + .isNotNull(); + + DynamoDBDatabaseManagerFactory documentManagerFactory = configuration.apply(settings); + + softly.assertThat(documentManagerFactory) + .describedAs("DynamoDBDocumentConfiguration.apply(Settings.class) should returns a non-null DocumentManagerFactory instance") + .isNotNull(); + + }); + } + +} diff --git a/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBKeyValueEntityManagerTest.java b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBKeyValueEntityManagerTest.java index 0ff1beece..922601982 100644 --- a/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBKeyValueEntityManagerTest.java +++ b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBKeyValueEntityManagerTest.java @@ -38,7 +38,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; @EnabledIfSystemProperty(named = NAMED, matches = MATCHES) -public class DynamoDBKeyValueEntityManagerTest { +class DynamoDBKeyValueEntityManagerTest { private BucketManager keyValueEntityManager; @@ -53,14 +53,14 @@ public class DynamoDBKeyValueEntityManagerTest { private KeyValueEntity keyValueSoro = KeyValueEntity.of("soro", Value.of(userSoro)); @BeforeEach - public void init() { - keyValueEntityManagerFactory = DynamoDBTestUtils.INSTANCE.get(); + void init() { + keyValueEntityManagerFactory = DynamoDBTestUtils.CONFIG.getBucketManagerFactory(); keyValueEntityManager = keyValueEntityManagerFactory.apply("users-entity"); } @Test - public void shouldPutValue() { + void shouldPutValue() { keyValueEntityManager.put("otavio", userOtavio); Optional otavio = keyValueEntityManager.get("otavio"); assertTrue(otavio.isPresent()); @@ -68,7 +68,7 @@ public void shouldPutValue() { } @Test - public void shouldPutKeyValue() { + void shouldPutKeyValue() { keyValueEntityManager.put(keyValueOtavio); Optional otavio = keyValueEntityManager.get("otavio"); assertTrue(otavio.isPresent()); @@ -76,7 +76,7 @@ public void shouldPutKeyValue() { } @Test - public void shouldPutIterableKeyValue() { + void shouldPutIterableKeyValue() { keyValueEntityManager.put(asList(keyValueSoro, keyValueOtavio)); Optional otavio = keyValueEntityManager.get("otavio"); @@ -89,7 +89,7 @@ public void shouldPutIterableKeyValue() { } @Test - public void shouldMultiGet() { + void shouldMultiGet() { User user = new User("otavio"); KeyValueEntity keyValue = KeyValueEntity.of("otavio", Value.of(user)); keyValueEntityManager.put(keyValue); @@ -97,7 +97,7 @@ public void shouldMultiGet() { } @Test - public void shouldRemoveKey() { + void shouldRemoveKey() { keyValueEntityManager.put(keyValueOtavio); assertTrue(keyValueEntityManager.get("otavio").isPresent()); keyValueEntityManager.delete("otavio"); @@ -105,7 +105,7 @@ public void shouldRemoveKey() { } @Test - public void shouldRemoveMultiKey() { + void shouldRemoveMultiKey() { keyValueEntityManager.put(asList(keyValueSoro, keyValueOtavio)); List keys = asList("otavio", "soro"); Iterable values = keyValueEntityManager.get(keys); @@ -117,8 +117,8 @@ public void shouldRemoveMultiKey() { } @AfterAll - public static void shutDown() { - DynamoDBTestUtils.INSTANCE.shutDown(); + static void shutDown() { + DynamoDBTestUtils.CONFIG.shutDown(); } } diff --git a/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBTestUtils.java b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBTestUtils.java index 6ad74b0da..66c593c4c 100644 --- a/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBTestUtils.java +++ b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/communication/DynamoDBTestUtils.java @@ -15,30 +15,106 @@ package org.eclipse.jnosql.databases.dynamodb.communication; import org.eclipse.jnosql.communication.Settings; +import org.eclipse.jnosql.communication.SettingsBuilder; import org.eclipse.jnosql.communication.keyvalue.BucketManagerFactory; +import org.eclipse.jnosql.mapping.core.config.MappingConfigurations; +import org.jetbrains.annotations.NotNull; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; -import java.util.function.Supplier; +import java.util.function.UnaryOperator; -public enum DynamoDBTestUtils implements Supplier { +public enum DynamoDBTestUtils { - INSTANCE; + CONFIG; private final GenericContainer dynamodb = new GenericContainer("amazon/dynamodb-local:latest") + .withReuse(true) .withExposedPorts(8000) .waitingFor(Wait.defaultWaitStrategy()); - public BucketManagerFactory get() { - dynamodb.start(); + BucketManagerFactory getBucketManagerFactory() { + Settings settings = getSettings(); + return getBucketManagerFactory(settings); + } + + private static DynamoDBBucketManagerFactory getBucketManagerFactory(Settings settings) { DynamoDBKeyValueConfiguration configuration = new DynamoDBKeyValueConfiguration(); - String endpoint = "http://" + dynamodb.getHost() + ":" + dynamodb.getFirstMappedPort(); - return configuration.apply(Settings.builder() - .put(DynamoDBConfigurations.ENDPOINT, endpoint).build()); + return configuration.apply(settings); + } + + DynamoDBDatabaseManagerFactory getDocumentManagerFactory() { + Settings settings = getSettings(); + return getDocumentManagerFactory(settings); + } + + DynamoDBDatabaseManagerFactory getDocumentManagerFactory(Settings settings) { + var configuration = new DynamoDBDocumentConfiguration(); + return configuration.apply(settings); } - public void shutDown() { + public Settings getSettings() { + dynamodb.start(); + String dynamoDBHost = getDynamoDBHost(dynamodb.getHost(), dynamodb.getFirstMappedPort()); + return getSettings(dynamoDBHost); + } + + Settings getSettings(String dynamoDBHost) { + return getSettingsBuilder(builder -> builder + .put(DynamoDBConfigurations.ENDPOINT, dynamoDBHost)) + .build(); + } + + public Settings customSetting(SettingsBuilder builder) { + var defaultSetting = getSettings(); + var customSetting = builder.build(); + return Settings.builder() + .putAll(defaultSetting.toMap()) + .putAll(customSetting.toMap()) + .build(); + } + + @NotNull + public static SettingsBuilder getSettingsBuilder(UnaryOperator builder) { + return builder.apply(Settings.builder()) + .put(MappingConfigurations.DOCUMENT_DATABASE, "test") + .put(DynamoDBConfigurations.AWS_ACCESSKEY, System.getProperty("AWS_ACCESS_KEY_ID", "AKIAIOSFODNN7EXAMPLE")) + .put(DynamoDBConfigurations.AWS_SECRET_ACCESS, System.getProperty("AWS_SECRET_ACCESS_KEY", "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY")) + .put(DynamoDBConfigurations.PROFILE, System.getProperty("AWS_PROFILE", "default")) + .put(DynamoDBConfigurations.REGION, "us-west-2") + .put(DynamoDBConfigurations.ENTITY_PARTITION_KEY, "entityType"); + } + + public void setupSystemProperties(SettingsBuilder builder) { + Settings settings = customSetting(builder); + System.getProperties().putAll(settings.toMap()); + System.out.println(System.getProperties()); + } + + @NotNull + String getDynamoDBHost(String host, int port) { + return "http://" + host + ":" + port; + } + + void shutDown() { dynamodb.close(); } + + DynamoDbClient getDynamoDbClient() { + var settings = getSettings(); + return getDynamoDbClient(settings); + } + + DynamoDbClient getDynamoDbClient(Settings settings) { + DynamoDBBuilderSync builderSync = new DynamoDBBuilderSync(); + settings.get(DynamoDBConfigurations.ENDPOINT, String.class).ifPresent(builderSync::endpoint); + settings.get(DynamoDBConfigurations.AWS_ACCESSKEY, String.class).ifPresent(builderSync::awsAccessKey); + settings.get(DynamoDBConfigurations.AWS_SECRET_ACCESS, String.class).ifPresent(builderSync::awsSecretAccess); + settings.get(DynamoDBConfigurations.PROFILE, String.class).ifPresent(builderSync::profile); + settings.get(DynamoDBConfigurations.REGION, String.class).ifPresent(builderSync::region); + return builderSync.build(); + } + } diff --git a/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/integration/Book.java b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/integration/Book.java new file mode 100644 index 000000000..b6160ec61 --- /dev/null +++ b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/integration/Book.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Maximillian Arruda + */ + +package org.eclipse.jnosql.databases.dynamodb.integration; + +import jakarta.nosql.Column; +import jakarta.nosql.Entity; +import jakarta.nosql.Id; + +import java.util.Objects; + +@Entity +public class Book { + + @Id("id") + private String id; + + @Column("title") + private String title; + + @Column("edition") + private int edition; + + public Book(String id, String title, int edition) { + this.id = id; + this.title = title; + this.edition = edition; + } + + Book() { + } + + public String id() { + return id; + } + + public String title() { + return title; + } + + public int edition() { + return edition; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Book book = (Book) o; + return edition == book.edition + && Objects.equals(id, book.id) + && Objects.equals(title, book.title); + } + + @Override + public int hashCode() { + return Objects.hash(id, title, edition); + } + + @Override + public String toString() { + return "Book{" + + "id='" + id + '\'' + + ", title='" + title + '\'' + + ", edition=" + edition + + '}'; + } +} \ No newline at end of file diff --git a/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/integration/DynamoDBTemplateIntegrationTest.java b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/integration/DynamoDBTemplateIntegrationTest.java new file mode 100644 index 000000000..6d9b4684a --- /dev/null +++ b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/integration/DynamoDBTemplateIntegrationTest.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Maximillian Arruda + */ + +package org.eclipse.jnosql.databases.dynamodb.integration; + +import jakarta.inject.Inject; +import org.eclipse.jnosql.communication.Settings; +import org.eclipse.jnosql.databases.dynamodb.communication.DynamoDBConfigurations; +import org.eclipse.jnosql.databases.dynamodb.communication.DynamoDBTestUtils; +import org.eclipse.jnosql.databases.dynamodb.mapping.DynamoDBExtension; +import org.eclipse.jnosql.databases.dynamodb.mapping.DynamoDBTemplate; +import org.eclipse.jnosql.mapping.core.Converters; +import org.eclipse.jnosql.mapping.core.spi.EntityMetadataExtension; +import org.eclipse.jnosql.mapping.document.spi.DocumentExtension; +import org.eclipse.jnosql.mapping.reflection.Reflections; +import org.eclipse.jnosql.mapping.semistructured.EntityConverter; +import org.jboss.weld.junit5.auto.AddExtensions; +import org.jboss.weld.junit5.auto.AddPackages; +import org.jboss.weld.junit5.auto.EnableAutoWeld; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; + +import java.util.Optional; + +import static java.util.UUID.randomUUID; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static org.eclipse.jnosql.communication.driver.IntegrationTest.MATCHES; +import static org.eclipse.jnosql.communication.driver.IntegrationTest.NAMED; + +@EnableAutoWeld +@AddPackages(value = {Converters.class, EntityConverter.class}) +@AddPackages(Book.class) +@AddPackages(DynamoDBTemplate.class) +@AddExtensions({EntityMetadataExtension.class, DocumentExtension.class, DynamoDBExtension.class}) +@AddPackages(Reflections.class) +@AddPackages(Converters.class) +@EnabledIfSystemProperty(named = NAMED, matches = MATCHES) +class DynamoDBTemplateIntegrationTest { + + static { + DynamoDBTestUtils.CONFIG.setupSystemProperties(Settings.builder() + .put(DynamoDBConfigurations.CREATE_TABLES, "true")); + } + + @Inject + private DynamoDBTemplate template; + + @Test + void shouldInsert() { + Book book = new Book(randomUUID().toString(), "Effective Java", 1); + template.insert(book); + Optional optional = template.find(Book.class, book.id()); + assertThat(optional).isNotNull().isNotEmpty() + .get().isEqualTo(book); + } + + @Test + void shouldUpdate() { + Book book = new Book(randomUUID().toString(), "Effective Java", 1); + assertThat(template.insert(book)) + .isNotNull() + .isEqualTo(book); + + Book updated = new Book(book.id(), book.title() + " updated", 2); + + assertThat(template.update(updated)) + .isNotNull() + .isNotEqualTo(book); + + assertThat(template.find(Book.class, book.id())) + .isNotNull().get().isEqualTo(updated); + + } + + @Test + void shouldFindById() { + Book book = new Book(randomUUID().toString(), "Effective Java", 1); + assertThat(template.insert(book)) + .isNotNull() + .isEqualTo(book); + + assertThat(template.find(Book.class, book.id())) + .isNotNull().get().isEqualTo(book); + } + + @Test + void shouldDelete() { + Book book = new Book(randomUUID().toString(), "Effective Java", 1); + assertThat(template.insert(book)) + .isNotNull() + .isEqualTo(book); + + template.delete(Book.class, book.id()); + assertThat(template.find(Book.class, book.id())) + .isNotNull().isEmpty(); + } + + @Test + void shouldDeleteAll() { + for (int index = 0; index < 20; index++) { + Book book = new Book(randomUUID().toString(), "Effective Java", 1); + assertThat(template.insert(book)) + .isNotNull() + .isEqualTo(book); + } + + template.delete(Book.class).execute(); + assertThat(template.select(Book.class).result()).isEmpty(); + } + + @Test + void shouldUpdateNullValues() { + var book = new Book(randomUUID().toString(), "Effective Java", 1); + template.insert(book); + template.update(new Book(book.id(), null, 2)); + Optional optional = template.select(Book.class).where("id") + .eq(book.id()).singleResult(); + assertSoftly(softly -> { + softly.assertThat(optional).isPresent(); + softly.assertThat(optional).get().extracting(Book::title).isNull(); + softly.assertThat(optional).get().extracting(Book::edition).isEqualTo(2); + }); + } + + +} diff --git a/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/DefaultDynamoDBTemplateTest.java b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/DefaultDynamoDBTemplateTest.java new file mode 100644 index 000000000..a34399d4a --- /dev/null +++ b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/DefaultDynamoDBTemplateTest.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Maximillian Arruda + */ + +package org.eclipse.jnosql.databases.dynamodb.mapping; + +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; +import org.assertj.core.api.SoftAssertions; +import org.eclipse.jnosql.communication.semistructured.DeleteQuery; +import org.eclipse.jnosql.databases.dynamodb.communication.DynamoDBDatabaseManager; +import org.eclipse.jnosql.mapping.core.Converters; +import org.eclipse.jnosql.mapping.core.spi.EntityMetadataExtension; +import org.eclipse.jnosql.mapping.document.spi.DocumentExtension; +import org.eclipse.jnosql.mapping.metadata.EntitiesMetadata; +import org.eclipse.jnosql.mapping.reflection.Reflections; +import org.eclipse.jnosql.mapping.semistructured.EntityConverter; +import org.eclipse.jnosql.mapping.semistructured.EventPersistManager; +import org.jboss.weld.junit5.auto.AddExtensions; +import org.jboss.weld.junit5.auto.AddPackages; +import org.jboss.weld.junit5.auto.EnableAutoWeld; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; + +import static org.mockito.Mockito.when; + +@EnableAutoWeld +@AddPackages(value = {Converters.class, EntityConverter.class, PartiQL.class}) +@AddPackages(MockProducer.class) +@AddExtensions({EntityMetadataExtension.class, DocumentExtension.class, DynamoDBExtension.class}) +@ExtendWith(MockitoExtension.class) +@AddPackages(Reflections.class) +class DefaultDynamoDBTemplateTest { + + @Inject + private EntityConverter converter; + + @Inject + private EventPersistManager persistManager; + + @Inject + private EntitiesMetadata entities; + + @Inject + private Converters converters; + + private DynamoDBDatabaseManager manager; + + private DynamoDBTemplate template; + + @BeforeEach + void setup() { + manager = Mockito.mock(DynamoDBDatabaseManager.class); + Instance instance = Mockito.mock(Instance.class); + when(instance.get()).thenReturn(manager); + template = new DefaultDynamoDBTemplate(instance, converter, persistManager, entities, converters); + } + + @Test + void shouldFindSQL() { + template.partiQL("select from database"); + Mockito.verify(manager).partiQL("select from database"); + } + + @Test + void shouldFindSQLWithTypeAndParameters() { + template.partiQL("select from database where content.name = ?", List.of("Ada"), String.class); + Mockito.verify(manager).partiQL("select from database where content.name = ?", List.of("Ada"), String.class); + } + + @Test + void shouldDeleteAll(){ + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(DeleteQuery.class); + template.deleteAll(Person.class); + Mockito.verify(manager).delete(argumentCaptor.capture()); + var query = argumentCaptor.getValue(); + SoftAssertions.assertSoftly(soft -> { + soft.assertThat(query.name()).isEqualTo("Person"); + soft.assertThat(query.columns()).isEmpty(); + soft.assertThat(query.condition()).isEmpty(); + }); + + } + + +} diff --git a/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/DynamoDBExtensionTest.java b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/DynamoDBExtensionTest.java new file mode 100644 index 000000000..524f3c696 --- /dev/null +++ b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/DynamoDBExtensionTest.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Maximillian Arruda + */ + +package org.eclipse.jnosql.databases.dynamodb.mapping; + +import jakarta.inject.Inject; +import org.eclipse.jnosql.mapping.core.Converters; +import org.eclipse.jnosql.mapping.core.spi.EntityMetadataExtension; +import org.eclipse.jnosql.mapping.document.spi.DocumentExtension; +import org.eclipse.jnosql.mapping.reflection.Reflections; +import org.jboss.weld.junit5.auto.AddExtensions; +import org.jboss.weld.junit5.auto.AddPackages; +import org.jboss.weld.junit5.auto.EnableAutoWeld; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +@EnableAutoWeld +@AddPackages(value = {Converters.class}) +@AddPackages(MockProducer.class) +@AddPackages(Reflections.class) +@AddExtensions({EntityMetadataExtension.class, + DocumentExtension.class, DynamoDBExtension.class}) +@ExtendWith(MockitoExtension.class) +public class DynamoDBExtensionTest { + + @Inject + private PersonNoSQLRepository repository; + + @Test + public void shouldSave() { + Assertions.assertNotNull(repository); + } +} diff --git a/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/DynamoDBRepositoryProxyTest.java b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/DynamoDBRepositoryProxyTest.java new file mode 100644 index 000000000..4596ebd8e --- /dev/null +++ b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/DynamoDBRepositoryProxyTest.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Maximillian Arruda + */ + +package org.eclipse.jnosql.databases.dynamodb.mapping; + +import jakarta.data.repository.Param; +import jakarta.inject.Inject; +import org.assertj.core.api.Assertions; +import org.eclipse.jnosql.mapping.core.Converters; +import org.eclipse.jnosql.mapping.core.spi.EntityMetadataExtension; +import org.eclipse.jnosql.mapping.document.spi.DocumentExtension; +import org.eclipse.jnosql.mapping.metadata.EntitiesMetadata; +import org.eclipse.jnosql.mapping.reflection.Reflections; +import org.eclipse.jnosql.mapping.semistructured.EntityConverter; +import org.jboss.weld.junit5.auto.AddExtensions; +import org.jboss.weld.junit5.auto.AddPackages; +import org.jboss.weld.junit5.auto.EnableAutoWeld; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +import java.lang.reflect.Proxy; +import java.time.Duration; +import java.util.List; +import java.util.Optional; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@EnableAutoWeld +@AddPackages(value = {Converters.class, EntityConverter.class, PartiQL.class}) +@AddPackages(MockProducer.class) +@AddPackages(Reflections.class) +@AddExtensions({EntityMetadataExtension.class, DocumentExtension.class, DynamoDBExtension.class}) +class DynamoDBRepositoryProxyTest { + + private DynamoDBTemplate template; + + @Inject + private EntitiesMetadata entitiesMetadata; + + @Inject + private Converters converters; + + private PersonNoSQLRepository personRepository; + + @SuppressWarnings("rawtypes") + @BeforeEach + void setUp() { + this.template = Mockito.mock(DynamoDBTemplate.class); + + DynamoDBRepositoryProxy handler = new DynamoDBRepositoryProxy<>(template, + PersonNoSQLRepository.class, converters, entitiesMetadata); + + when(template.insert(any(Person.class))).thenReturn(new Person()); + when(template.insert(any(Person.class), any(Duration.class))).thenReturn(new Person()); + when(template.update(any(Person.class))).thenReturn(new Person()); + + this.personRepository = (PersonNoSQLRepository) Proxy.newProxyInstance(PersonNoSQLRepository.class.getClassLoader(), + new Class[]{PersonNoSQLRepository.class}, + handler); + } + + @Test + public void shouldFindAll() { + personRepository.findAllQuery(); + verify(template).partiQL("select * from Person"); + } + + @Test + public void shouldFindByNameSQL() { + ArgumentCaptor captor = ArgumentCaptor.forClass(Object[].class); + personRepository.findByName("Ada"); + verify(template).partiQL(eq("select * from Person where name= ?"), captor.capture()); + + Object[] value = captor.getValue(); + Assertions.assertThat(value).hasSize(1).contains("Ada"); + } + + @Test + public void shouldSaveUsingInsert() { + Person person = Person.of("Ada", 10); + personRepository.save(person); + verify(template).insert(eq(person)); + } + + + @Test + public void shouldSaveUsingUpdate() { + Person person = Person.of("Ada-2", 10); + when(template.find(Person.class, "Ada-2")).thenReturn(Optional.of(person)); + personRepository.save(person); + verify(template).update(eq(person)); + } + + @Test + public void shouldDelete(){ + personRepository.deleteById("id"); + verify(template).delete(Person.class, "id"); + } + + + @Test + public void shouldDeleteEntity(){ + Person person = Person.of("Ada", 10); + personRepository.delete(person); + verify(template).delete(Person.class, person.getName()); + } + + @Test + public void shouldDeleteAll() { + ArgumentCaptor> queryCaptor = ArgumentCaptor.forClass(Class.class); + + personRepository.deleteAll(); + verify(template).deleteAll(queryCaptor.capture()); + + Class query = queryCaptor.getValue(); + Assertions.assertThat(query).isEqualTo(Person.class); + } + + interface PersonNoSQLRepository extends DynamoDBRepository { + + @PartiQL("select * from Person") + List findAllQuery(); + + @PartiQL("select * from Person where name= ?") + List findByName(@Param("") String name); + } + +} diff --git a/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/MockProducer.java b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/MockProducer.java new file mode 100644 index 000000000..818521d9b --- /dev/null +++ b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/MockProducer.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Maximillian Arruda + */ + +package org.eclipse.jnosql.databases.dynamodb.mapping; + +import jakarta.annotation.Priority; +import jakarta.enterprise.inject.Alternative; +import jakarta.enterprise.inject.Produces; +import jakarta.interceptor.Interceptor; +import org.eclipse.jnosql.communication.semistructured.CommunicationEntity; +import org.eclipse.jnosql.communication.semistructured.Element; +import org.eclipse.jnosql.databases.dynamodb.communication.DynamoDBDatabaseManager; +import org.mockito.Mockito; + +import java.util.function.Supplier; + +@Alternative +@Priority(Interceptor.Priority.APPLICATION) +public class MockProducer implements Supplier { + + @Produces + @Override + public DynamoDBDatabaseManager get() { + DynamoDBDatabaseManager manager = Mockito.mock(DynamoDBDatabaseManager.class); + var entity = CommunicationEntity.of("Person"); + entity.add(Element.of("name", "Ada")); + Mockito.when(manager.insert(Mockito.any(CommunicationEntity.class))).thenReturn(entity); + return manager; + } + +} \ No newline at end of file diff --git a/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/Person.java b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/Person.java new file mode 100644 index 000000000..40e3705a5 --- /dev/null +++ b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/Person.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Maximillian Arruda + */ + +package org.eclipse.jnosql.databases.dynamodb.mapping; + +import jakarta.nosql.Entity; +import jakarta.nosql.Id; +import jakarta.nosql.Column; + +import java.util.Objects; + +@Entity +public class Person { + + @Id + private String name; + + @Column + private Integer age; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Integer getAge() { + return age; + } + + public void setAge(Integer age) { + this.age = age; + } + + public Person(String name, Integer age) { + this.name = name; + this.age = age; + } + + public Person() { + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Person person = (Person) o; + return Objects.equals(name, person.name) && + Objects.equals(age, person.age); + } + + @Override + public int hashCode() { + return Objects.hash(name, age); + } + + @Override + public String toString() { + return "Person{" + + "name='" + name + '\'' + + ", age=" + age + + '}'; + } + + public static Person of(String name, Integer age) { + return new Person(name, age); + } +} \ No newline at end of file diff --git a/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/PersonNoSQLRepository.java b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/PersonNoSQLRepository.java new file mode 100644 index 000000000..d05fd8084 --- /dev/null +++ b/jnosql-dynamodb/src/test/java/org/eclipse/jnosql/databases/dynamodb/mapping/PersonNoSQLRepository.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Apache License v2.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Apache License v2.0 is available at http://www.opensource.org/licenses/apache2.0.php. + * + * You may elect to redistribute this code under either of these licenses. + * + * Contributors: + * + * Maximillian Arruda + */ + +package org.eclipse.jnosql.databases.dynamodb.mapping; + +import jakarta.data.repository.Repository; + +@Repository +public interface PersonNoSQLRepository extends DynamoDBRepository { +}