Skip to content

Commit fef3026

Browse files
Fix idgen+auditable (Azure#19348)
* add tests demonstrating failure * apply auditing before generating the id - by default, a null id is interpreted as a new object, so if the id is set, created by/on will not be set * added license headers
1 parent 54241f2 commit fef3026

File tree

7 files changed

+150
-9
lines changed

7 files changed

+150
-9
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
package com.azure.spring.data.cosmos.domain;
4+
5+
import com.azure.spring.data.cosmos.core.mapping.Container;
6+
import com.azure.spring.data.cosmos.core.mapping.GeneratedValue;
7+
import org.springframework.data.annotation.CreatedBy;
8+
import org.springframework.data.annotation.CreatedDate;
9+
import org.springframework.data.annotation.Id;
10+
import org.springframework.data.annotation.LastModifiedBy;
11+
import org.springframework.data.annotation.LastModifiedDate;
12+
import org.springframework.data.annotation.Version;
13+
14+
import java.time.OffsetDateTime;
15+
16+
@Container
17+
public class AuditableIdGeneratedEntity {
18+
19+
@Id
20+
@GeneratedValue
21+
String id;
22+
@CreatedBy
23+
private String createdBy;
24+
@CreatedDate
25+
private OffsetDateTime createdDate;
26+
@LastModifiedBy
27+
private String lastModifiedBy;
28+
@LastModifiedDate
29+
private OffsetDateTime lastModifiedByDate;
30+
31+
public String getId() {
32+
return id;
33+
}
34+
35+
public void setId(String id) {
36+
this.id = id;
37+
}
38+
39+
public String getCreatedBy() {
40+
return createdBy;
41+
}
42+
43+
public void setCreatedBy(String createdBy) {
44+
this.createdBy = createdBy;
45+
}
46+
47+
public OffsetDateTime getCreatedDate() {
48+
return createdDate;
49+
}
50+
51+
public void setCreatedDate(OffsetDateTime createdDate) {
52+
this.createdDate = createdDate;
53+
}
54+
55+
public String getLastModifiedBy() {
56+
return lastModifiedBy;
57+
}
58+
59+
public void setLastModifiedBy(String lastModifiedBy) {
60+
this.lastModifiedBy = lastModifiedBy;
61+
}
62+
63+
public OffsetDateTime getLastModifiedByDate() {
64+
return lastModifiedByDate;
65+
}
66+
67+
public void setLastModifiedByDate(OffsetDateTime lastModifiedByDate) {
68+
this.lastModifiedByDate = lastModifiedByDate;
69+
}
70+
71+
}

sdk/cosmos/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/AuditableIT.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
package com.azure.spring.data.cosmos.repository.integration;
44

55
import com.azure.spring.data.cosmos.domain.AuditableEntity;
6+
import com.azure.spring.data.cosmos.domain.AuditableIdGeneratedEntity;
67
import com.azure.spring.data.cosmos.repository.StubAuditorProvider;
78
import com.azure.spring.data.cosmos.repository.StubDateTimeProvider;
89
import com.azure.spring.data.cosmos.repository.TestRepositoryConfig;
10+
import com.azure.spring.data.cosmos.repository.repository.AuditableIdGeneratedRepository;
911
import com.azure.spring.data.cosmos.repository.repository.AuditableRepository;
1012
import org.junit.After;
1113
import org.junit.Test;
@@ -27,6 +29,8 @@ public class AuditableIT {
2729
@Autowired
2830
private AuditableRepository auditableRepository;
2931
@Autowired
32+
private AuditableIdGeneratedRepository auditableIdGeneratedRepository;
33+
@Autowired
3034
private StubDateTimeProvider stubDateTimeProvider;
3135
@Autowired
3236
private StubAuditorProvider stubAuditorProvider;
@@ -73,4 +77,19 @@ public void testUpdateShouldNotOverwriteCreatedEntries() {
7377
assertThat(modifiedEntity.getLastModifiedByDate()).isEqualTo(modifiedOn);
7478
}
7579

80+
@Test
81+
public void testInsertShouldSetAuditableEntriesIfIdAutoGenerated() {
82+
final AuditableIdGeneratedEntity entity = new AuditableIdGeneratedEntity();
83+
final OffsetDateTime now = OffsetDateTime.now(ZoneId.of("UTC"));
84+
85+
stubDateTimeProvider.setNow(now);
86+
stubAuditorProvider.setCurrentAuditor("created-by");
87+
final AuditableIdGeneratedEntity savedEntity = auditableIdGeneratedRepository.save(entity);
88+
89+
assertThat(savedEntity.getCreatedBy()).isEqualTo("created-by");
90+
assertThat(savedEntity.getCreatedDate()).isEqualTo(now);
91+
assertThat(savedEntity.getLastModifiedBy()).isEqualTo("created-by");
92+
assertThat(savedEntity.getLastModifiedByDate()).isEqualTo(now);
93+
}
94+
7695
}

sdk/cosmos/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ReactiveAuditableIT.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
package com.azure.spring.data.cosmos.repository.integration;
44

55
import com.azure.spring.data.cosmos.domain.AuditableEntity;
6+
import com.azure.spring.data.cosmos.domain.AuditableIdGeneratedEntity;
67
import com.azure.spring.data.cosmos.repository.StubAuditorProvider;
78
import com.azure.spring.data.cosmos.repository.StubDateTimeProvider;
89
import com.azure.spring.data.cosmos.repository.TestRepositoryConfig;
10+
import com.azure.spring.data.cosmos.repository.repository.ReactiveAuditableIdGeneratedRepository;
911
import com.azure.spring.data.cosmos.repository.repository.ReactiveAuditableRepository;
1012
import org.junit.After;
1113
import org.junit.Test;
@@ -29,6 +31,8 @@ public class ReactiveAuditableIT {
2931
@Autowired
3032
private ReactiveAuditableRepository auditableRepository;
3133
@Autowired
34+
private ReactiveAuditableIdGeneratedRepository reactiveAuditableIdGeneratedRepository;
35+
@Autowired
3236
private StubDateTimeProvider stubDateTimeProvider;
3337
@Autowired
3438
private StubAuditorProvider stubAuditorProvider;
@@ -92,4 +96,30 @@ private boolean validateAuditableFields(AuditableEntity entity,
9296
return true;
9397
}
9498

99+
@Test
100+
public void testInsertShouldSetAuditableEntriesIfIdAutoGenerated() {
101+
final AuditableIdGeneratedEntity entity = new AuditableIdGeneratedEntity();
102+
final OffsetDateTime now = OffsetDateTime.now(ZoneId.of("UTC"));
103+
104+
stubDateTimeProvider.setNow(now);
105+
stubAuditorProvider.setCurrentAuditor("created-by");
106+
final Mono<AuditableIdGeneratedEntity> savedEntity = reactiveAuditableIdGeneratedRepository.save(entity);
107+
108+
StepVerifier
109+
.create(savedEntity)
110+
.expectNextMatches(actual -> validateAuditableFields(actual,
111+
"created-by", now,
112+
"created-by", now))
113+
.verifyComplete();
114+
}
115+
116+
private boolean validateAuditableFields(AuditableIdGeneratedEntity entity,
117+
String expectedCreatedBy, OffsetDateTime expectedCreatedDate,
118+
String expectedModifiedBy, OffsetDateTime expectedModifiedTime) {
119+
assertThat(entity.getCreatedBy()).isEqualTo(expectedCreatedBy);
120+
assertThat(entity.getCreatedDate()).isEqualTo(expectedCreatedDate);
121+
assertThat(entity.getLastModifiedBy()).isEqualTo(expectedModifiedBy);
122+
assertThat(entity.getLastModifiedByDate()).isEqualTo(expectedModifiedTime);
123+
return true;
124+
}
95125
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
package com.azure.spring.data.cosmos.repository.repository;
4+
5+
import com.azure.spring.data.cosmos.domain.AuditableIdGeneratedEntity;
6+
import com.azure.spring.data.cosmos.repository.CosmosRepository;
7+
8+
public interface AuditableIdGeneratedRepository extends CosmosRepository<AuditableIdGeneratedEntity, String> {
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
package com.azure.spring.data.cosmos.repository.repository;
4+
5+
import com.azure.spring.data.cosmos.domain.AuditableIdGeneratedEntity;
6+
import com.azure.spring.data.cosmos.repository.ReactiveCosmosRepository;
7+
8+
public interface ReactiveAuditableIdGeneratedRepository extends ReactiveCosmosRepository<AuditableIdGeneratedEntity, String> {
9+
}

sdk/cosmos/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/CosmosTemplate.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -180,9 +180,10 @@ public <T> T insert(String containerName, T objectToSave, PartitionKey partition
180180

181181
@SuppressWarnings("unchecked") final Class<T> domainType = (Class<T>) objectToSave.getClass();
182182

183+
markAuditedIfConfigured(objectToSave);
183184
generateIdIfNullAndAutoGenerationEnabled(objectToSave, domainType);
184185

185-
final JsonNode originalItem = prepareToPersistAndConvertToItemProperties(objectToSave);
186+
final JsonNode originalItem = mappingCosmosConverter.writeJsonNode(objectToSave);
186187

187188
LOGGER.debug("execute createItem in database {} container {}", this.databaseName,
188189
containerName);
@@ -321,7 +322,9 @@ public <T> T upsertAndReturnEntity(String containerName, T object) {
321322
Assert.hasText(containerName, "containerName should not be null, empty or only whitespaces");
322323
Assert.notNull(object, "Upsert object should not be null");
323324

324-
final JsonNode originalItem = prepareToPersistAndConvertToItemProperties(object);
325+
markAuditedIfConfigured(object);
326+
327+
final JsonNode originalItem = mappingCosmosConverter.writeJsonNode(object);
325328

326329
LOGGER.debug("execute upsert item in database {} container {}", this.databaseName,
327330
containerName);
@@ -749,11 +752,10 @@ public <T> Iterable<T> runQuery(SqlQuerySpec querySpec, Class<?> domainType, Cla
749752
.block();
750753
}
751754

752-
private JsonNode prepareToPersistAndConvertToItemProperties(Object object) {
755+
private void markAuditedIfConfigured(Object object) {
753756
if (cosmosAuditingHandler != null) {
754757
cosmosAuditingHandler.markAudited(object);
755758
}
756-
return mappingCosmosConverter.writeJsonNode(object);
757759
}
758760

759761
private Long getCountValue(CosmosQuery query, String containerName) {

sdk/cosmos/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/ReactiveCosmosTemplate.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -362,8 +362,9 @@ public <T> Mono<T> insert(String containerName, Object objectToSave,
362362
Assert.notNull(objectToSave, "objectToSave should not be null");
363363

364364
final Class<T> domainType = (Class<T>) objectToSave.getClass();
365+
markAuditedIfConfigured(objectToSave);
365366
generateIdIfNullAndAutoGenerationEnabled(objectToSave, domainType);
366-
final JsonNode originalItem = prepareToPersistAndConvertToItemProperties(objectToSave);
367+
final JsonNode originalItem = mappingCosmosConverter.writeJsonNode(objectToSave);
367368
final CosmosItemRequestOptions options = new CosmosItemRequestOptions();
368369
// if the partition key is null, SDK will get the partitionKey from the object
369370
return cosmosAsyncClient
@@ -422,7 +423,8 @@ public <T> Mono<T> upsert(T object) {
422423
@Override
423424
public <T> Mono<T> upsert(String containerName, T object) {
424425
final Class<T> domainType = (Class<T>) object.getClass();
425-
final JsonNode originalItem = prepareToPersistAndConvertToItemProperties(object);
426+
markAuditedIfConfigured(object);
427+
final JsonNode originalItem = mappingCosmosConverter.writeJsonNode(object);
426428
final CosmosItemRequestOptions options = new CosmosItemRequestOptions();
427429

428430
applyVersioning(object.getClass(), originalItem, options);
@@ -673,14 +675,13 @@ public String getContainerName(Class<?> domainType) {
673675
return CosmosEntityInformation.getInstance(domainType).getContainerName();
674676
}
675677

676-
private JsonNode prepareToPersistAndConvertToItemProperties(Object object) {
678+
679+
private void markAuditedIfConfigured(Object object) {
677680
if (cosmosAuditingHandler != null) {
678681
cosmosAuditingHandler.markAudited(object);
679682
}
680-
return mappingCosmosConverter.writeJsonNode(object);
681683
}
682684

683-
684685
private Flux<JsonNode> findItems(@NonNull CosmosQuery query,
685686
@NonNull String containerName) {
686687
final SqlQuerySpec sqlQuerySpec = new FindQuerySpecGenerator().generateCosmos(query);

0 commit comments

Comments
 (0)