From 112ca64a54f1f4d1786168efb0f48e097cab82a5 Mon Sep 17 00:00:00 2001 From: Andrei Bordak Date: Wed, 4 Dec 2024 22:16:50 +0400 Subject: [PATCH] parent a75ab33c5895eca734dc18fec4a12adf90ec652f author Andrei Bordak 1733336210 +0400 committer Andrei Bordak 1733990640 +0400 MODLD-594: Resource date fields MODLD-594: Remove unnecessary test MODLD-594: Extracted migration MODLD-594 MErge commit MODLD-594 MErge commit MODLD-594: Remove temp dependency MODLD-594: Redundant import --- .../audit/LinkedDataAuditEntityListener.java | 29 +++++++++ .../jpa/audit/LinkedDataAuditorAware.java | 22 +++++++ .../SourceRecordDomainEventDeserializer.java | 17 +++++ .../linked/data/model/entity/Resource.java | 36 +++++++++++ .../service/resource/ResourceServiceImpl.java | 6 ++ .../v-1.0.0/resource_graph/changelog.xml | 1 + .../migration/resources_date_fields.sql | 12 ++++ .../resource/ResourceControllerITBase.java | 64 ++++++++++++++++++- .../SourceRecordDomainEventListenerIT.java | 3 +- .../resource/ResourceServiceImplTest.java | 5 ++ .../org/folio/linked/data/test/TestUtil.java | 17 +++++ 11 files changed, 210 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/folio/linked/data/configuration/jpa/audit/LinkedDataAuditEntityListener.java create mode 100644 src/main/java/org/folio/linked/data/configuration/jpa/audit/LinkedDataAuditorAware.java create mode 100644 src/main/resources/changelog/scripts/v-1.0.0/resource_graph/migration/resources_date_fields.sql diff --git a/src/main/java/org/folio/linked/data/configuration/jpa/audit/LinkedDataAuditEntityListener.java b/src/main/java/org/folio/linked/data/configuration/jpa/audit/LinkedDataAuditEntityListener.java new file mode 100644 index 00000000..7d66fcf9 --- /dev/null +++ b/src/main/java/org/folio/linked/data/configuration/jpa/audit/LinkedDataAuditEntityListener.java @@ -0,0 +1,29 @@ +package org.folio.linked.data.configuration.jpa.audit; + +import static java.util.Optional.ofNullable; + +import jakarta.persistence.PrePersist; +import lombok.AllArgsConstructor; +import org.folio.linked.data.model.entity.Resource; +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.data.auditing.AuditingHandler; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import org.springframework.stereotype.Component; + +@Component +@AllArgsConstructor +public class LinkedDataAuditEntityListener extends AuditingEntityListener { + + private ObjectFactory handler; + + @Override + @PrePersist + public void touchForCreate(Object target) { + if (target instanceof Resource resource) { + if (resource.getCreatedBy() == null) { + ofNullable(handler.getObject()) + .ifPresent(object -> object.markCreated(target)); + } + } + } +} diff --git a/src/main/java/org/folio/linked/data/configuration/jpa/audit/LinkedDataAuditorAware.java b/src/main/java/org/folio/linked/data/configuration/jpa/audit/LinkedDataAuditorAware.java new file mode 100644 index 00000000..86bd00b8 --- /dev/null +++ b/src/main/java/org/folio/linked/data/configuration/jpa/audit/LinkedDataAuditorAware.java @@ -0,0 +1,22 @@ +package org.folio.linked.data.configuration.jpa.audit; + +import java.util.Optional; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.folio.spring.FolioExecutionContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.domain.AuditorAware; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@Configuration +@EnableJpaAuditing(modifyOnCreate = false) +@RequiredArgsConstructor +public class LinkedDataAuditorAware implements AuditorAware { + + private final FolioExecutionContext folioExecutionContext; + + @Override + public Optional getCurrentAuditor() { + return Optional.ofNullable(folioExecutionContext.getUserId()); + } +} diff --git a/src/main/java/org/folio/linked/data/configuration/json/deserialization/event/SourceRecordDomainEventDeserializer.java b/src/main/java/org/folio/linked/data/configuration/json/deserialization/event/SourceRecordDomainEventDeserializer.java index a50e4837..60167cc0 100644 --- a/src/main/java/org/folio/linked/data/configuration/json/deserialization/event/SourceRecordDomainEventDeserializer.java +++ b/src/main/java/org/folio/linked/data/configuration/json/deserialization/event/SourceRecordDomainEventDeserializer.java @@ -11,6 +11,7 @@ import java.io.IOException; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; +import org.folio.linked.data.domain.dto.EventMetadata; import org.folio.linked.data.domain.dto.ParsedRecord; import org.folio.linked.data.domain.dto.SourceRecord; import org.folio.linked.data.domain.dto.SourceRecordDomainEvent; @@ -25,6 +26,7 @@ public class SourceRecordDomainEventDeserializer extends JsonDeserializer { @Id @@ -93,6 +102,25 @@ public class Resource implements Persistable { @PrimaryKeyJoinColumn private FolioMetadata folioMetadata; + @Column(name = "created_date", updatable = false, nullable = false) + private Timestamp createdDate; + + @UpdateTimestamp + @Column(name = "updated_date", nullable = false) + private Timestamp updatedDate; + + @CreatedBy + @Column(name = "created_by") + private UUID createdBy; + + @LastModifiedBy + @Column(name = "updated_by") + private UUID updatedBy; + + @Version + @Column(name = "version", nullable = false) + private long version; + @Transient private boolean managed; @@ -103,6 +131,11 @@ public Resource(@NonNull Resource that) { this.folioMetadata = that.folioMetadata; this.indexDate = that.indexDate; this.types = new LinkedHashSet<>(that.getTypes()); + this.createdDate = that.createdDate; + this.createdBy = that.createdBy; + this.updatedDate = that.updatedDate; + this.updatedBy = that.updatedBy; + this.version = that.version; this.outgoingEdges = ofNullable(that.getOutgoingEdges()) .map(outEdges -> outEdges.stream().map(ResourceEdge::new).collect(Collectors.toSet())) .orElse(null); @@ -193,6 +226,9 @@ void prePersist() { throw new IllegalStateException("Cannot save resource [" + id + "] with types " + types + ". " + "Folio metadata can be set only for instance and authority resources"); } + if (isNull(this.createdDate)) { + this.createdDate = new Timestamp(System.currentTimeMillis()); + } } @PostRemove diff --git a/src/main/java/org/folio/linked/data/service/resource/ResourceServiceImpl.java b/src/main/java/org/folio/linked/data/service/resource/ResourceServiceImpl.java index cec05043..d7ce144a 100644 --- a/src/main/java/org/folio/linked/data/service/resource/ResourceServiceImpl.java +++ b/src/main/java/org/folio/linked/data/service/resource/ResourceServiceImpl.java @@ -19,6 +19,7 @@ import org.folio.linked.data.service.resource.edge.ResourceEdgeService; import org.folio.linked.data.service.resource.graph.ResourceGraphService; import org.folio.linked.data.service.resource.meta.MetadataService; +import org.folio.spring.FolioExecutionContext; import org.springframework.context.ApplicationEventPublisher; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; @@ -38,6 +39,7 @@ public class ResourceServiceImpl implements ResourceService { private final RequestProcessingExceptionBuilder exceptionBuilder; private final ApplicationEventPublisher applicationEventPublisher; private final ResourceEdgeService resourceEdgeService; + private final FolioExecutionContext folioExecutionContext; @Override public ResourceResponseDto createResource(ResourceRequestDto resourceDto) { @@ -103,6 +105,10 @@ private Resource saveNewResource(ResourceRequestDto resourceDto, Resource old) { var mapped = resourceDtoMapper.toEntity(resourceDto); metadataService.ensure(mapped, old.getFolioMetadata()); resourceEdgeService.copyOutgoingEdges(old, mapped); + mapped.setCreatedDate(old.getCreatedDate()); + mapped.setVersion(old.getVersion() + 1); + mapped.setCreatedBy(old.getCreatedBy()); + mapped.setUpdatedBy(folioExecutionContext.getUserId()); return resourceGraphService.saveMergingGraph(mapped); } diff --git a/src/main/resources/changelog/scripts/v-1.0.0/resource_graph/changelog.xml b/src/main/resources/changelog/scripts/v-1.0.0/resource_graph/changelog.xml index ef035704..12bb7845 100644 --- a/src/main/resources/changelog/scripts/v-1.0.0/resource_graph/changelog.xml +++ b/src/main/resources/changelog/scripts/v-1.0.0/resource_graph/changelog.xml @@ -18,4 +18,5 @@ + diff --git a/src/main/resources/changelog/scripts/v-1.0.0/resource_graph/migration/resources_date_fields.sql b/src/main/resources/changelog/scripts/v-1.0.0/resource_graph/migration/resources_date_fields.sql new file mode 100644 index 00000000..35e7e198 --- /dev/null +++ b/src/main/resources/changelog/scripts/v-1.0.0/resource_graph/migration/resources_date_fields.sql @@ -0,0 +1,12 @@ +alter table resources + add column created_date timestamp default current_timestamp not null, + add column updated_date timestamp default current_timestamp not null, + add column created_by uuid, + add column updated_by uuid, + add column version int default 0 not null; + +comment on column resources.created_date is 'Date and time when resource first added to data graph'; +comment on column resources.created_by is 'UUID of user who added resource to data graph'; +comment on column resources.updated_date is 'Date and time when resource last updated'; +comment on column resources.updated_by is 'UUID of user who performed the last update to the resource'; +comment on column resources.version is 'Version of the resource'; diff --git a/src/test/java/org/folio/linked/data/e2e/resource/ResourceControllerITBase.java b/src/test/java/org/folio/linked/data/e2e/resource/ResourceControllerITBase.java index 7d6d6282..b43917a7 100644 --- a/src/test/java/org/folio/linked/data/e2e/resource/ResourceControllerITBase.java +++ b/src/test/java/org/folio/linked/data/e2e/resource/ResourceControllerITBase.java @@ -124,8 +124,10 @@ import static org.folio.linked.data.test.TestUtil.OBJECT_MAPPER; import static org.folio.linked.data.test.TestUtil.SIMPLE_WORK_WITH_INSTANCE_REF_SAMPLE; import static org.folio.linked.data.test.TestUtil.WORK_WITH_INSTANCE_REF_SAMPLE; +import static org.folio.linked.data.test.TestUtil.assertResourceMetadata; import static org.folio.linked.data.test.TestUtil.cleanResourceTables; import static org.folio.linked.data.test.TestUtil.defaultHeaders; +import static org.folio.linked.data.test.TestUtil.defaultHeadersWithUserId; import static org.folio.linked.data.test.TestUtil.getSampleInstanceDtoMap; import static org.folio.linked.data.test.TestUtil.getSampleWorkDtoMap; import static org.folio.linked.data.test.TestUtil.randomLong; @@ -133,7 +135,9 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.notNullValue; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; @@ -217,6 +221,7 @@ public abstract class ResourceControllerITBase { private static final String GRANTING_INSTITUTION_REF = "_grantingInstitutionReference"; private static final String WORK_ID_PLACEHOLDER = "%WORK_ID%"; private static final String INSTANCE_ID_PLACEHOLDER = "%INSTANCE_ID%"; + private static final UUID USER_ID = UUID.randomUUID(); @Autowired private MockMvc mockMvc; @Autowired @@ -502,7 +507,7 @@ void createWorkWithInstanceRef_shouldSaveEntityCorrectly() throws Exception { resourceTestService.saveGraph(instanceForReference); var requestBuilder = post(RESOURCE_URL) .contentType(APPLICATION_JSON) - .headers(defaultHeaders(env)) + .headers(defaultHeadersWithUserId(env, USER_ID.toString())) .content( WORK_WITH_INSTANCE_REF_SAMPLE.replaceAll(INSTANCE_ID_PLACEHOLDER, instanceForReference.getId().toString()) ); @@ -523,6 +528,63 @@ void createWorkWithInstanceRef_shouldSaveEntityCorrectly() throws Exception { validateWork(workResource, true); checkSearchIndexMessage(workResource.getId(), CREATE); checkIndexDate(workResource.getId().toString()); + assertResourceMetadata(workResource, USER_ID, null); + } + + @Test + void update_shouldReturnCorrectlyUpdateMetadataFields() throws Exception { + // given + var instanceForReference = getSampleInstanceResource(null, null); + setExistingResourcesIds(instanceForReference); + resourceTestService.saveGraph(instanceForReference); + var requestBuilder = post(RESOURCE_URL) + .contentType(APPLICATION_JSON) + .headers(defaultHeadersWithUserId(env, USER_ID.toString())) + .content( + WORK_WITH_INSTANCE_REF_SAMPLE.replaceAll(INSTANCE_ID_PLACEHOLDER, instanceForReference.getId().toString()) + ); + + var response = mockMvc.perform(requestBuilder) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + var resourceResponse = OBJECT_MAPPER.readValue(response, ResourceResponseDto.class); + var originalWorkId = ((WorkResponseField) resourceResponse.getResource()).getWork().getId(); + var originalWorkResource = resourceTestService.getResourceById(originalWorkId, 4); + + + var updateDto = getSampleWorkDtoMap(); + var workMap = (LinkedHashMap) ((LinkedHashMap) updateDto.get("resource")).get(WORK.getUri()); + workMap.put(PropertyDictionary.LANGUAGE.getValue(), + Map.of( + LINK.getValue(), List.of("http://id.loc.gov/vocabulary/languages/rus"), + TERM.getValue(), List.of("Russian") + )); + var updatedById = UUID.randomUUID(); + + // when + var updateRequest = put(RESOURCE_URL + "/" + originalWorkId) + .contentType(APPLICATION_JSON) + .headers(defaultHeadersWithUserId(env, updatedById.toString())) + .content(OBJECT_MAPPER.writeValueAsString(updateDto) + .replaceAll(INSTANCE_ID_PLACEHOLDER, instanceForReference.getId().toString()) + ); + + // then + var updatedResponse = mockMvc.perform(updateRequest).andReturn().getResponse().getContentAsString(); + var updatedResourceResponse = OBJECT_MAPPER.readValue(updatedResponse, ResourceResponseDto.class); + var updatedWorkId = ((WorkResponseField) updatedResourceResponse.getResource()).getWork().getId(); + var updatedWorkResource = resourceTestService.getResourceById(updatedWorkId, 4); + compareResourceMetadataOfOriginalAndUpdated(originalWorkResource, updatedWorkResource, updatedById); + } + + private void compareResourceMetadataOfOriginalAndUpdated(Resource original, Resource updated, UUID updatedById) { + assertEquals(USER_ID, updated.getCreatedBy()); + assertEquals(updatedById, updated.getUpdatedBy()); + assertTrue(updated.getUpdatedDate().after(original.getUpdatedDate())); + assertEquals(original.getCreatedDate(), updated.getCreatedDate()); + assertEquals(original.getCreatedBy(), updated.getCreatedBy()); + assertNull(original.getUpdatedBy()); + assertEquals(1, updated.getVersion() - original.getVersion()); } @Test diff --git a/src/test/java/org/folio/linked/data/integration/kafka/listener/SourceRecordDomainEventListenerIT.java b/src/test/java/org/folio/linked/data/integration/kafka/listener/SourceRecordDomainEventListenerIT.java index 47a9ea0c..a834ecc8 100644 --- a/src/test/java/org/folio/linked/data/integration/kafka/listener/SourceRecordDomainEventListenerIT.java +++ b/src/test/java/org/folio/linked/data/integration/kafka/listener/SourceRecordDomainEventListenerIT.java @@ -12,6 +12,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import org.folio.linked.data.domain.dto.EventMetadata; import org.folio.linked.data.domain.dto.ParsedRecord; import org.folio.linked.data.domain.dto.SourceRecord; import org.folio.linked.data.domain.dto.SourceRecordDomainEvent; @@ -50,7 +51,7 @@ void shouldHandleSrsDomainEvent_whenSourceRecordType_isMarcBib() { .content(TestUtil.loadResourceAsString("samples/srsDomainEventParsedContent.txt")) ) .deleted(true) - ); + ).eventMetadata(new EventMetadata().eventTTL(1).publishedBy("mod-source-record-storage-5.9.0-SNAPSHOT")); // when eventKafkaTemplate.send(eventProducerRecord); diff --git a/src/test/java/org/folio/linked/data/service/resource/ResourceServiceImplTest.java b/src/test/java/org/folio/linked/data/service/resource/ResourceServiceImplTest.java index f8a6bf4c..5245c721 100644 --- a/src/test/java/org/folio/linked/data/service/resource/ResourceServiceImplTest.java +++ b/src/test/java/org/folio/linked/data/service/resource/ResourceServiceImplTest.java @@ -42,6 +42,7 @@ import org.folio.linked.data.service.resource.edge.ResourceEdgeService; import org.folio.linked.data.service.resource.graph.ResourceGraphService; import org.folio.linked.data.service.resource.meta.MetadataService; +import org.folio.spring.FolioExecutionContext; import org.folio.spring.testing.type.UnitTest; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -74,6 +75,8 @@ class ResourceServiceImplTest { private RequestProcessingExceptionBuilder exceptionBuilder; @Mock private ResourceEdgeService resourceEdgeService; + @Mock + private FolioExecutionContext folioExecutionContext; @Test void create_shouldPersistMappedResourceAndNotPublishResourceCreatedEvent_forResourceWithNoWork() { @@ -218,6 +221,7 @@ void update_shouldSaveUpdatedResourceAndSendResourceUpdatedEvent_forResourceWith ); when(resourceDtoMapper.toDto(work)).thenReturn(expectedDto); when(resourceGraphService.saveMergingGraph(work)).thenReturn(work); + when(folioExecutionContext.getUserId()).thenReturn(UUID.randomUUID()); // when var result = resourceService.updateResource(id, workDto); @@ -226,6 +230,7 @@ void update_shouldSaveUpdatedResourceAndSendResourceUpdatedEvent_forResourceWith assertThat(expectedDto).isEqualTo(result); verify(resourceGraphService).breakEdgesAndDelete(oldWork); verify(resourceGraphService).saveMergingGraph(work); + verify(folioExecutionContext).getUserId(); verify(applicationEventPublisher).publishEvent(new ResourceUpdatedEvent(work)); } diff --git a/src/test/java/org/folio/linked/data/test/TestUtil.java b/src/test/java/org/folio/linked/data/test/TestUtil.java index 8feb3e4d..1c9c38e2 100644 --- a/src/test/java/org/folio/linked/data/test/TestUtil.java +++ b/src/test/java/org/folio/linked/data/test/TestUtil.java @@ -13,7 +13,10 @@ import static org.folio.linked.data.util.Constants.STANDALONE_PROFILE; import static org.folio.spring.integration.XOkapiHeaders.TENANT; import static org.folio.spring.integration.XOkapiHeaders.URL; +import static org.folio.spring.integration.XOkapiHeaders.USER_ID; import static org.jeasy.random.FieldPredicates.named; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.testcontainers.shaded.org.awaitility.Awaitility.await; import static org.testcontainers.shaded.org.awaitility.Durations.FIVE_SECONDS; @@ -28,6 +31,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.UUID; import java.util.stream.Collectors; import java.util.stream.IntStream; import lombok.SneakyThrows; @@ -98,6 +102,12 @@ public static HttpHeaders defaultHeaders(Environment env) { return httpHeaders; } + public static HttpHeaders defaultHeadersWithUserId(Environment env, String value) { + var httpHeaders = defaultHeaders(env); + httpHeaders.add(USER_ID, value); + return httpHeaders; + } + public static List defaultKafkaHeaders() { return List.of( new RecordHeader(TENANT, TENANT_ID.getBytes(UTF_8)), @@ -169,6 +179,13 @@ public static void assertAuthority(Resource resource, ); } + public static void assertResourceMetadata(Resource resource, UUID createdBy, UUID updatedBy) { + assertNotNull(resource.getCreatedDate()); + assertNotNull(resource.getUpdatedDate()); + assertEquals(createdBy, resource.getCreatedBy()); + assertEquals(updatedBy, resource.getUpdatedBy()); + } + public static void cleanResourceTables(JdbcTemplate jdbcTemplate) { JdbcTestUtils.deleteFromTables(jdbcTemplate, "folio_metadata", "resource_edges", "resource_type_map", "resources");