diff --git a/settings.gradle.kts b/settings.gradle.kts index 33196ad..19fc610 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,3 +1,4 @@ rootProject.name = "spring-undo" include(":spring-undo-core") -include(":spring-undo-redis") \ No newline at end of file +include(":spring-undo-redis") +include(":spring-undo-mongo") \ No newline at end of file diff --git a/spring-undo-mongo/build.gradle.kts b/spring-undo-mongo/build.gradle.kts new file mode 100644 index 0000000..3c42c28 --- /dev/null +++ b/spring-undo-mongo/build.gradle.kts @@ -0,0 +1,18 @@ +import Java_conventions_gradle.Versions + +plugins { + id("java-conventions") + id("testing-conventions") + id("publishing-conventions") +} + +description = "Spring-Undo persistence module. Stores records in mongodb and provides access to them." + +dependencies { + compileOnly("org.springframework.boot:spring-boot-starter-data-mongodb:${Versions.springBootVersion}") + compileOnly(project(":spring-undo-core")) + + testImplementation("org.springframework.boot:spring-boot-starter-data-mongodb:${Versions.springBootVersion}") + testImplementation("org.testcontainers:testcontainers:1.17.1") + testImplementation(project(":spring-undo-core")) +} \ No newline at end of file diff --git a/spring-undo-mongo/src/main/java/dev/fomenko/springundomongo/MongoConverter.java b/spring-undo-mongo/src/main/java/dev/fomenko/springundomongo/MongoConverter.java new file mode 100644 index 0000000..30d2bf1 --- /dev/null +++ b/spring-undo-mongo/src/main/java/dev/fomenko/springundomongo/MongoConverter.java @@ -0,0 +1,26 @@ +package dev.fomenko.springundomongo; + +import dev.fomenko.springundocore.dto.ActionRecord; + +public final class MongoConverter { + + private MongoConverter() { + } + + public static RecordEntity toEntity(ActionRecord dto) { + return RecordEntity.builder() + .action(dto.getAction()) + .expiresAt(dto.getExpiresAt()) + .recordId(dto.getRecordId()) + .build(); + } + + + public static ActionRecord toDto(RecordEntity dto) { + return ActionRecord.builder() + .action(dto.getAction()) + .expiresAt(dto.getExpiresAt()) + .recordId(dto.getRecordId()) + .build(); + } +} diff --git a/spring-undo-mongo/src/main/java/dev/fomenko/springundomongo/MongoEventRecorder.java b/spring-undo-mongo/src/main/java/dev/fomenko/springundomongo/MongoEventRecorder.java new file mode 100644 index 0000000..fb8e631 --- /dev/null +++ b/spring-undo-mongo/src/main/java/dev/fomenko/springundomongo/MongoEventRecorder.java @@ -0,0 +1,54 @@ +package dev.fomenko.springundomongo; + + +import com.mongodb.client.result.DeleteResult; +import dev.fomenko.springundocore.dto.ActionRecord; +import dev.fomenko.springundocore.service.EventRecorder; +import lombok.RequiredArgsConstructor; +import lombok.extern.apachecommons.CommonsLog; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + + +@CommonsLog +@RequiredArgsConstructor +public class MongoEventRecorder implements EventRecorder { + public static final String COLLECTION_NAME = "recoverableEvents"; + + private final MongoTemplate mongoTemplate; + + @Override + public void saveRecord(ActionRecord actionRecord) { + RecordEntity recordEntity = MongoConverter.toEntity(actionRecord); + mongoTemplate.save(recordEntity, COLLECTION_NAME); + } + + @Override + public List> getAllRecords() { + List results = mongoTemplate.findAll(RecordEntity.class, COLLECTION_NAME); + return results.stream() + .map(MongoConverter::toDto) + .collect(Collectors.toList()); + } + + @Override + public Optional> getRecordById(String recordId) { + Query query = new Query(Criteria.where("recordId").is(recordId)); + RecordEntity recordEntity = mongoTemplate.findOne(query, RecordEntity.class, COLLECTION_NAME); + return Optional.of(MongoConverter.toDto(recordEntity)); + } + + @Override + public boolean deleteRecordById(String recordId) { + Query query = new Query(Criteria.where("recordId").is(recordId)); + DeleteResult deleteResult = mongoTemplate.remove(query, RecordEntity.class, COLLECTION_NAME); + long deletedCount = deleteResult.getDeletedCount(); + return deletedCount > 0; + } + +} diff --git a/spring-undo-mongo/src/main/java/dev/fomenko/springundomongo/MongoUndoRecorderConfiguration.java b/spring-undo-mongo/src/main/java/dev/fomenko/springundomongo/MongoUndoRecorderConfiguration.java new file mode 100644 index 0000000..6086c4f --- /dev/null +++ b/spring-undo-mongo/src/main/java/dev/fomenko/springundomongo/MongoUndoRecorderConfiguration.java @@ -0,0 +1,16 @@ +package dev.fomenko.springundomongo; + +import dev.fomenko.springundocore.service.EventRecorder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.mongodb.core.MongoTemplate; + +@Configuration +public class MongoUndoRecorderConfiguration { + + @Bean + public EventRecorder mongoEventRecorder(MongoTemplate mongoTemplate) { + return new MongoEventRecorder(mongoTemplate); + } + +} diff --git a/spring-undo-mongo/src/main/java/dev/fomenko/springundomongo/RecordEntity.java b/spring-undo-mongo/src/main/java/dev/fomenko/springundomongo/RecordEntity.java new file mode 100644 index 0000000..27dcae4 --- /dev/null +++ b/spring-undo-mongo/src/main/java/dev/fomenko/springundomongo/RecordEntity.java @@ -0,0 +1,22 @@ +package dev.fomenko.springundomongo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.time.LocalDateTime; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Document +public class RecordEntity { + @Id + private String recordId; + private T action; + private LocalDateTime expiresAt; +} diff --git a/spring-undo-mongo/src/main/resources/META-INF/spring.factories b/spring-undo-mongo/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..ca1f00e --- /dev/null +++ b/spring-undo-mongo/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=dev.fomenko.springundomongo.MongoUndoRecorderConfiguration \ No newline at end of file diff --git a/spring-undo-mongo/src/test/java/dev/fomenko/springundomongo/MongoEventRecorderTest.java b/spring-undo-mongo/src/test/java/dev/fomenko/springundomongo/MongoEventRecorderTest.java new file mode 100644 index 0000000..0bc376c --- /dev/null +++ b/spring-undo-mongo/src/test/java/dev/fomenko/springundomongo/MongoEventRecorderTest.java @@ -0,0 +1,176 @@ +package dev.fomenko.springundomongo; + +import dev.fomenko.springundocore.dto.ActionRecord; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.mongodb.core.MongoTemplate; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import static dev.fomenko.springundomongo.MongoEventRecorder.COLLECTION_NAME; + +@SpringBootTest +class MongoEventRecorderTest { + @Autowired + private MongoEventRecorder mongoEventRecorder; + + @Autowired + private MongoTemplate mongoTemplate; + + @AfterEach + void setup() { + mongoTemplate.dropCollection(COLLECTION_NAME); + } + + + @Test + void shouldSaveRecordCorrectly() { + //given + String recordId = "record_name"; + LocalDateTime expiresAt = LocalDateTime.now(); + String actionName = "action1"; + String description = "desc"; + int size = 382; + + ActionRecord actionTest = ActionRecord.builder() + .recordId(recordId) + .action(TestDto.builder() + .testName(actionName) + .testDescription(description) + .size(size).build()) + .expiresAt(expiresAt).build(); + + //when + mongoEventRecorder.saveRecord(actionTest); + + //then + List recordEntities = mongoTemplate.findAll(RecordEntity.class, COLLECTION_NAME); + RecordEntity record = recordEntities.get(0); + + Assertions.assertEquals(1, recordEntities.size()); + Assertions.assertEquals(actionName, record.getAction().getTestName()); + Assertions.assertEquals(description, record.getAction().getTestDescription()); + Assertions.assertEquals(size, record.getAction().getSize()); + Assertions.assertEquals(recordId, record.getRecordId()); + } + + + @Test + void shouldGetAllRecords() { + //given + LocalDateTime expiresAt1 = LocalDateTime.now(); + String recordId1 = "event1"; + String testName1 = "name1"; + ActionRecord actionTest1 = ActionRecord.builder() + .recordId(recordId1) + .action(TestDto.builder() + .testName(testName1) + .testDescription("desc") + .size(382).build()) + .expiresAt(expiresAt1).build(); + + + LocalDateTime expiresAt2 = LocalDateTime.now(); + String recordId2 = "event2"; + String testName2 = "name2"; + ActionRecord actionTest2 = ActionRecord.builder() + .recordId(recordId2) + .action(TestDto.builder() + .testName(testName2) + .testDescription("desc") + .size(382).build()) + .expiresAt(expiresAt2).build(); + + //when + mongoTemplate.save(MongoConverter.toEntity(actionTest1), COLLECTION_NAME); + mongoTemplate.save(MongoConverter.toEntity(actionTest2), COLLECTION_NAME); + + //then + List> allRecords = mongoEventRecorder.getAllRecords(); + Assertions.assertEquals(2, allRecords.size()); + + Assertions.assertEquals(recordId1, allRecords.get(0).getRecordId()); + Assertions.assertEquals(testName1, ((TestDto) allRecords.get(0).getAction()).getTestName()); + + Assertions.assertEquals(recordId2, allRecords.get(1).getRecordId()); + Assertions.assertEquals(testName2, ((TestDto) allRecords.get(1).getAction()).getTestName()); + } + + + @Test + void shouldGetRecordById() { + //given + String description = "description01"; + String recordId = "record01"; + int size = 2; + String testName = "test01"; + + ActionRecord actionTest1 = ActionRecord.builder() + .recordId(recordId) + .action(TestDto.builder() + .testName(testName) + .testDescription(description) + .size(size) + .build()) + .build(); + + ActionRecord actionTest2 = ActionRecord.builder() + .recordId("record02") + .action(TestDto.builder() + .testDescription("description02") + .size(12) + .build()) + .build(); + + mongoTemplate.save(MongoConverter.toEntity(actionTest1), COLLECTION_NAME); + mongoTemplate.save(MongoConverter.toEntity(actionTest2), COLLECTION_NAME); + + //when + Optional> recordById = mongoEventRecorder.getRecordById(recordId); + + //then + ActionRecord resultRecord = (ActionRecord) recordById.get(); + + Assertions.assertEquals(recordId, resultRecord.getRecordId()); + Assertions.assertEquals(testName, resultRecord.getAction().getTestName()); + Assertions.assertEquals(description, resultRecord.getAction().getTestDescription()); + Assertions.assertEquals(size, resultRecord.getAction().getSize()); + } + + + @Test + void shouldDeleteRecordById() { + //given + String recordId1 = "event1"; + ActionRecord actionTest1 = ActionRecord.builder() + .recordId(recordId1) + .action(TestDto.builder().build()) + .expiresAt(LocalDateTime.now()).build(); + + String recordId2 = "event2"; + ActionRecord actionTest2 = ActionRecord.builder() + .recordId(recordId2) + .action(TestDto.builder().build()) + .expiresAt(LocalDateTime.now()).build(); + + mongoTemplate.save(MongoConverter.toEntity(actionTest1), COLLECTION_NAME); + mongoTemplate.save(MongoConverter.toEntity(actionTest2), COLLECTION_NAME); + + //when + boolean wasDeleted = mongoEventRecorder.deleteRecordById(recordId2); + + + //then + List results = mongoTemplate.findAll(RecordEntity.class, COLLECTION_NAME); + + Assertions.assertTrue(wasDeleted); + Assertions.assertEquals(1, results.size()); + Assertions.assertEquals(recordId1, results.get(0).getRecordId()); + } + +} \ No newline at end of file diff --git a/spring-undo-mongo/src/test/java/dev/fomenko/springundomongo/MongoEventRecorderTestConfiguration.java b/spring-undo-mongo/src/test/java/dev/fomenko/springundomongo/MongoEventRecorderTestConfiguration.java new file mode 100644 index 0000000..4333b18 --- /dev/null +++ b/spring-undo-mongo/src/test/java/dev/fomenko/springundomongo/MongoEventRecorderTestConfiguration.java @@ -0,0 +1,36 @@ +package dev.fomenko.springundomongo; + +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration; +import org.testcontainers.containers.GenericContainer; + +@SpringBootApplication +public class MongoEventRecorderTestConfiguration extends AbstractMongoClientConfiguration { + private static final int MONGO_PORT = 27017; + + private final static GenericContainer mongo = new GenericContainer<>("mongo:5.0.8") + .withExposedPorts(MONGO_PORT); + + static { + mongo.start(); + } + + @Override + protected String getDatabaseName() { + return "test"; + } + + + @Override + public MongoClient mongoClient() { + String url = String.format("mongodb://%s:%s", mongo.getHost(), mongo.getMappedPort(MONGO_PORT)); + return MongoClients.create(MongoClientSettings.builder() + .applyConnectionString(new ConnectionString(url)) + .build()); + } +} + diff --git a/spring-undo-mongo/src/test/java/dev/fomenko/springundomongo/TestDto.java b/spring-undo-mongo/src/test/java/dev/fomenko/springundomongo/TestDto.java new file mode 100644 index 0000000..79992c9 --- /dev/null +++ b/spring-undo-mongo/src/test/java/dev/fomenko/springundomongo/TestDto.java @@ -0,0 +1,18 @@ +package dev.fomenko.springundomongo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class TestDto implements Serializable { + private String testName; + private String testDescription; + private Integer size; +}