diff --git a/eng/code-quality-reports/src/main/resources/revapi/revapi.json b/eng/code-quality-reports/src/main/resources/revapi/revapi.json index bab5addd891f..d9dec8268668 100644 --- a/eng/code-quality-reports/src/main/resources/revapi/revapi.json +++ b/eng/code-quality-reports/src/main/resources/revapi/revapi.json @@ -169,6 +169,54 @@ "new": "method .* com\\.azure\\.resourcemanager\\..*", "justification": "resourcemanager interfaces are allowed to add methods." }, + { + "ignore": true, + "code": "java.method.addedToInterface", + "new": "method T com.azure.spring.data.cosmos.core.CosmosOperations::patch(java.lang.Object, com.azure.cosmos.models.PartitionKey, java.lang.Class, com.azure.cosmos.models.CosmosPatchOperations)", + "justification": "Spring interfaces are allowed to add methods." + }, + { + "ignore": true, + "code": "java.method.addedToInterface", + "new": "method T com.azure.spring.data.cosmos.core.CosmosOperations::patch(java.lang.Object, com.azure.cosmos.models.PartitionKey, java.lang.Class, com.azure.cosmos.models.CosmosPatchOperations, com.azure.cosmos.models.CosmosPatchItemRequestOptions)", + "justification": "Spring interfaces are allowed to add methods." + }, + { + "ignore": true, + "code": "java.method.addedToInterface", + "new": "method reactor.core.publisher.Mono com.azure.spring.data.cosmos.core.ReactiveCosmosOperations::patch(java.lang.Object, com.azure.cosmos.models.PartitionKey, java.lang.Class, com.azure.cosmos.models.CosmosPatchOperations)", + "justification": "Spring interfaces are allowed to add methods." + }, + { + "ignore": true, + "code": "java.method.addedToInterface", + "new": "method reactor.core.publisher.Mono com.azure.spring.data.cosmos.core.ReactiveCosmosOperations::patch(java.lang.Object, com.azure.cosmos.models.PartitionKey, java.lang.Class, com.azure.cosmos.models.CosmosPatchOperations, com.azure.cosmos.models.CosmosPatchItemRequestOptions)", + "justification": "Spring interfaces are allowed to add methods." + }, + { + "ignore": true, + "code": "java.method.addedToInterface", + "new": "method S com.azure.spring.data.cosmos.repository.CosmosRepository::save(ID, com.azure.cosmos.models.PartitionKey, java.lang.Class, com.azure.cosmos.models.CosmosPatchOperations)", + "justification": "Spring interfaces are allowed to add methods." + }, + { + "ignore": true, + "code": "java.method.addedToInterface", + "new": "method S com.azure.spring.data.cosmos.repository.CosmosRepository::save(ID, com.azure.cosmos.models.PartitionKey, java.lang.Class, com.azure.cosmos.models.CosmosPatchOperations, com.azure.cosmos.models.CosmosPatchItemRequestOptions)", + "justification": "Spring interfaces are allowed to add methods." + }, + { + "ignore": true, + "code": "java.method.addedToInterface", + "new": "method reactor.core.publisher.Mono com.azure.spring.data.cosmos.repository.ReactiveCosmosRepository::save(K, com.azure.cosmos.models.PartitionKey, java.lang.Class, com.azure.cosmos.models.CosmosPatchOperations)", + "justification": "Spring interfaces are allowed to add methods." + }, + { + "ignore": true, + "code": "java.method.addedToInterface", + "new": "method reactor.core.publisher.Mono com.azure.spring.data.cosmos.repository.ReactiveCosmosRepository::save(K, com.azure.cosmos.models.PartitionKey, java.lang.Class, com.azure.cosmos.models.CosmosPatchOperations, com.azure.cosmos.models.CosmosPatchItemRequestOptions)", + "justification": "Spring interfaces are allowed to add methods." + }, { "regex": true, "code": "java\\.class\\.externalClassExposedInAPI", diff --git a/sdk/cosmos/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/common/TestConstants.java b/sdk/cosmos/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/common/TestConstants.java index a3b231b1ffb5..22a87ef28100 100644 --- a/sdk/cosmos/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/common/TestConstants.java +++ b/sdk/cosmos/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/common/TestConstants.java @@ -4,6 +4,8 @@ import com.azure.cosmos.models.IndexingMode; import com.azure.spring.data.cosmos.domain.Address; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import java.util.Arrays; import java.util.HashMap; @@ -18,7 +20,9 @@ public final class TestConstants { private static final Address ADDRESS_1 = new Address("201107", "Zixing Road", "Shanghai"); private static final Address ADDRESS_2 = new Address("200000", "Xuhui", "Shanghai"); public static final String HOBBY1 = "photography"; + public static final String PATCH_HOBBY1 = "shopping"; public static final List HOBBIES = Arrays.asList(HOBBY1, "fishing"); + public static final List PATCH_HOBBIES = Arrays.asList(HOBBY1, "fishing", PATCH_HOBBY1); public static final List
ADDRESSES = Arrays.asList(ADDRESS_1, ADDRESS_2); public static final String ROLE_COLLECTION_NAME = "RoleCollectionName"; @@ -45,6 +49,7 @@ public final class TestConstants { public static final String DB_NAME = "testdb"; public static final String FIRST_NAME = "first_name_li"; + public static final String PATCH_FIRST_NAME = "first_name_replace"; public static final String LAST_NAME = "last_name_p"; public static final Integer ZIP_CODE = 12345; public static final String ID_1 = "id-1"; @@ -98,12 +103,19 @@ public final class TestConstants { public static final String DEPARTMENT = "test-department"; public static final Integer AGE = 24; + public static final Integer PATCH_AGE_1 = 25; + public static final Integer PATCH_AGE_INCREMENT = 2; public static final Map PASSPORT_IDS_BY_COUNTRY = new HashMap() {{ put("United States of America", "123456789"); put("Côte d'Ivoire", "IC1234567"); }}; + public static final Map NEW_PASSPORT_IDS_BY_COUNTRY = new HashMap() {{ + put("United Kingdom", "123456789"); + put("Germany", "IC1234567"); + }}; + private TestConstants() { } } diff --git a/sdk/cosmos/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/CosmosTemplateIT.java b/sdk/cosmos/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/CosmosTemplateIT.java index ee890f069e26..55bf81e7539f 100644 --- a/sdk/cosmos/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/CosmosTemplateIT.java +++ b/sdk/cosmos/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/CosmosTemplateIT.java @@ -7,7 +7,10 @@ import com.azure.cosmos.CosmosClientBuilder; import com.azure.cosmos.CosmosException; import com.azure.cosmos.implementation.ConflictException; +import com.azure.cosmos.implementation.PreconditionFailedException; import com.azure.cosmos.models.CosmosContainerProperties; +import com.azure.cosmos.models.CosmosPatchItemRequestOptions; +import com.azure.cosmos.models.CosmosPatchOperations; import com.azure.cosmos.models.PartitionKey; import com.azure.cosmos.models.SqlQuerySpec; import com.azure.cosmos.models.ThroughputResponse; @@ -35,6 +38,9 @@ import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; import com.azure.spring.data.cosmos.repository.repository.AuditableRepository; import com.azure.spring.data.cosmos.repository.support.CosmosEntityInformation; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import org.assertj.core.util.Lists; import org.junit.Before; import org.junit.ClassRule; @@ -71,11 +77,17 @@ import static com.azure.spring.data.cosmos.common.TestConstants.LAST_NAME; import static com.azure.spring.data.cosmos.common.TestConstants.NEW_FIRST_NAME; import static com.azure.spring.data.cosmos.common.TestConstants.NEW_LAST_NAME; +import static com.azure.spring.data.cosmos.common.TestConstants.NEW_PASSPORT_IDS_BY_COUNTRY; import static com.azure.spring.data.cosmos.common.TestConstants.NOT_EXIST_ID; import static com.azure.spring.data.cosmos.common.TestConstants.PAGE_SIZE_1; import static com.azure.spring.data.cosmos.common.TestConstants.PAGE_SIZE_2; import static com.azure.spring.data.cosmos.common.TestConstants.PAGE_SIZE_3; import static com.azure.spring.data.cosmos.common.TestConstants.PASSPORT_IDS_BY_COUNTRY; +import static com.azure.spring.data.cosmos.common.TestConstants.PATCH_AGE_1; +import static com.azure.spring.data.cosmos.common.TestConstants.PATCH_AGE_INCREMENT; +import static com.azure.spring.data.cosmos.common.TestConstants.PATCH_FIRST_NAME; +import static com.azure.spring.data.cosmos.common.TestConstants.PATCH_HOBBIES; +import static com.azure.spring.data.cosmos.common.TestConstants.PATCH_HOBBY1; import static com.azure.spring.data.cosmos.common.TestConstants.UPDATED_FIRST_NAME; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; @@ -98,6 +110,24 @@ public class CosmosTemplateIT { private static final String WRONG_ETAG = "WRONG_ETAG"; + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final JsonNode NEW_PASSPORT_IDS_BY_COUNTRY_JSON = OBJECT_MAPPER.convertValue(NEW_PASSPORT_IDS_BY_COUNTRY, JsonNode.class); + + private static final CosmosPatchOperations operations = CosmosPatchOperations + .create() + .replace("/age", PATCH_AGE_1); + + CosmosPatchOperations multiPatchOperations = CosmosPatchOperations + .create() + .set("/firstName", PATCH_FIRST_NAME) + .replace("/passportIdsByCountry", NEW_PASSPORT_IDS_BY_COUNTRY_JSON) + .add("/hobbies/2", PATCH_HOBBY1) + .remove("/shippingAddresses/1") + .increment("/age", PATCH_AGE_INCREMENT); + + private static final CosmosPatchItemRequestOptions options = new CosmosPatchItemRequestOptions(); + + @ClassRule public static final IntegrationTestCollectionManager collectionManager = new IntegrationTestCollectionManager(); @@ -117,6 +147,9 @@ public class CosmosTemplateIT { @Autowired private ResponseDiagnosticsTestUtils responseDiagnosticsTestUtils; + public CosmosTemplateIT() throws JsonProcessingException { + } + @Before public void setUp() throws ClassNotFoundException { if (cosmosTemplate == null) { @@ -267,6 +300,42 @@ public void testUpdate() { assertEquals(person, updated); } + @Test + public void testPatch() { + Person patchedPerson = cosmosTemplate.patch(insertedPerson.getId(), new PartitionKey(insertedPerson.getLastName()), Person.class, operations); + assertEquals(patchedPerson.getAge(), PATCH_AGE_1); + } + + @Test + public void testPatchMultiOperations() { + Person patchedPerson = cosmosTemplate.patch(insertedPerson.getId(), new PartitionKey(insertedPerson.getLastName()), Person.class, multiPatchOperations); + assertEquals(patchedPerson.getAge().intValue(), (AGE + PATCH_AGE_INCREMENT)); + assertEquals(patchedPerson.getHobbies(), PATCH_HOBBIES); + assertEquals(patchedPerson.getFirstName(), PATCH_FIRST_NAME); + assertEquals(patchedPerson.getShippingAddresses().size(), 1); + assertEquals(patchedPerson.getPassportIdsByCountry(), NEW_PASSPORT_IDS_BY_COUNTRY); + } + + @Test + public void testPatchPreConditionSuccess() { + options.setFilterPredicate("FROM person p WHERE p.lastName = '"+LAST_NAME+"'"); + Person patchedPerson = cosmosTemplate.patch(insertedPerson.getId(), new PartitionKey(insertedPerson.getLastName()), Person.class, operations, options); + assertEquals(patchedPerson.getAge(), PATCH_AGE_1); + } + + @Test + public void testPatchPreConditionFail() { + try { + options.setFilterPredicate("FROM person p WHERE p.lastName = 'dummy'"); + Person patchedPerson = cosmosTemplate.patch(insertedPerson.getId(), new PartitionKey(insertedPerson.getLastName()), Person.class, operations, options); + assertEquals(patchedPerson.getAge(), PATCH_AGE_1); + fail(); + } catch (CosmosAccessException ex) { + assertThat(ex.getCosmosException()).isInstanceOf(PreconditionFailedException.class); + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + } + } + @Test public void testOptimisticLockWhenUpdatingWithWrongEtag() { final Person updated = new Person(TEST_PERSON.getId(), UPDATED_FIRST_NAME, diff --git a/sdk/cosmos/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/ReactiveCosmosTemplateIT.java b/sdk/cosmos/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/ReactiveCosmosTemplateIT.java index 6177d42a6da1..0b8982ddab23 100644 --- a/sdk/cosmos/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/ReactiveCosmosTemplateIT.java +++ b/sdk/cosmos/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/ReactiveCosmosTemplateIT.java @@ -8,7 +8,10 @@ import com.azure.cosmos.CosmosClientBuilder; import com.azure.cosmos.CosmosException; import com.azure.cosmos.implementation.ConflictException; +import com.azure.cosmos.implementation.PreconditionFailedException; import com.azure.cosmos.models.CosmosContainerResponse; +import com.azure.cosmos.models.CosmosPatchItemRequestOptions; +import com.azure.cosmos.models.CosmosPatchOperations; import com.azure.cosmos.models.PartitionKey; import com.azure.cosmos.models.SqlQuerySpec; import com.azure.cosmos.models.ThroughputResponse; @@ -33,6 +36,8 @@ import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; import com.azure.spring.data.cosmos.repository.repository.AuditableRepository; import com.azure.spring.data.cosmos.repository.support.CosmosEntityInformation; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import org.assertj.core.api.Assertions; import org.junit.After; import org.junit.Assert; @@ -65,7 +70,13 @@ import static com.azure.spring.data.cosmos.common.TestConstants.FIRST_NAME; import static com.azure.spring.data.cosmos.common.TestConstants.HOBBIES; import static com.azure.spring.data.cosmos.common.TestConstants.LAST_NAME; +import static com.azure.spring.data.cosmos.common.TestConstants.NEW_PASSPORT_IDS_BY_COUNTRY; import static com.azure.spring.data.cosmos.common.TestConstants.PASSPORT_IDS_BY_COUNTRY; +import static com.azure.spring.data.cosmos.common.TestConstants.PATCH_AGE_1; +import static com.azure.spring.data.cosmos.common.TestConstants.PATCH_AGE_INCREMENT; +import static com.azure.spring.data.cosmos.common.TestConstants.PATCH_FIRST_NAME; +import static com.azure.spring.data.cosmos.common.TestConstants.PATCH_HOBBIES; +import static com.azure.spring.data.cosmos.common.TestConstants.PATCH_HOBBY1; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -89,6 +100,23 @@ public class ReactiveCosmosTemplateIT { private static final String PRECONDITION_IS_NOT_MET = "is not met"; private static final String WRONG_ETAG = "WRONG_ETAG"; + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final JsonNode NEW_PASSPORT_IDS_BY_COUNTRY_JSON = OBJECT_MAPPER.convertValue(NEW_PASSPORT_IDS_BY_COUNTRY, JsonNode.class); + + private static final CosmosPatchOperations operations = CosmosPatchOperations + .create() + .replace("/age", PATCH_AGE_1); + + CosmosPatchOperations multiPatchOperations = CosmosPatchOperations + .create() + .set("/firstName", PATCH_FIRST_NAME) + .replace("/passportIdsByCountry", NEW_PASSPORT_IDS_BY_COUNTRY_JSON) + .add("/hobbies/2", PATCH_HOBBY1) + .remove("/shippingAddresses/1") + .increment("/age", PATCH_AGE_INCREMENT); + + private static final CosmosPatchItemRequestOptions options = new CosmosPatchItemRequestOptions(); + @ClassRule public static final ReactiveIntegrationTestCollectionManager collectionManager = new ReactiveIntegrationTestCollectionManager(); @@ -281,6 +309,41 @@ public void testUpsert() { assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); } + @Test + public void testPatch() { + final Mono patch = cosmosTemplate.patch(insertedPerson.getId(), new PartitionKey(insertedPerson.getLastName()), Person.class, operations); + StepVerifier.create(patch).expectNextCount(1).verifyComplete(); + Mono patchedPerson = cosmosTemplate.findById(containerName, insertedPerson.getId(), Person.class); + StepVerifier.create(patchedPerson).expectNextMatches(person -> person.getAge() == PATCH_AGE_1).verifyComplete(); + } + + @Test + public void testPatchMultiOperations() { + final Mono patch = cosmosTemplate.patch(insertedPerson.getId(), new PartitionKey(insertedPerson.getLastName()), Person.class, multiPatchOperations); + StepVerifier.create(patch).expectNextCount(1).verifyComplete(); + Person patchedPerson = cosmosTemplate.findById(containerName, insertedPerson.getId(), Person.class).block(); + assertEquals(patchedPerson.getAge().intValue(), (AGE + PATCH_AGE_INCREMENT)); + assertEquals(patchedPerson.getHobbies(),PATCH_HOBBIES); + assertEquals(patchedPerson.getFirstName(), PATCH_FIRST_NAME); + assertEquals(patchedPerson.getShippingAddresses().size(), 1); + assertEquals(patchedPerson.getPassportIdsByCountry(), NEW_PASSPORT_IDS_BY_COUNTRY); + } + + @Test + public void testPatchPreConditionSuccess() { + options.setFilterPredicate("FROM person p WHERE p.lastName = '"+LAST_NAME+"'"); + Mono patchedPerson = cosmosTemplate.patch(insertedPerson.getId(), new PartitionKey(insertedPerson.getLastName()), Person.class, operations, options); + StepVerifier.create(patchedPerson).expectNextMatches(person -> person.getAge() == PATCH_AGE_1).verifyComplete(); + } + + @Test + public void testPatchPreConditionFail() { + options.setFilterPredicate("FROM person p WHERE p.lastName = 'dummy'"); + Mono person = cosmosTemplate.patch(insertedPerson.getId(), new PartitionKey(insertedPerson.getLastName()), Person.class, operations, options); + StepVerifier.create(person).expectErrorMatches(ex -> ex instanceof CosmosAccessException && + ((CosmosAccessException) ex).getCosmosException() instanceof PreconditionFailedException).verify(); + } + @Test public void testOptimisticLockWhenUpdatingWithWrongEtag() { final Person updated = new Person(TEST_PERSON.getId(), TestConstants.UPDATED_FIRST_NAME, diff --git a/sdk/cosmos/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/AddressRepositoryIT.java b/sdk/cosmos/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/AddressRepositoryIT.java index 14fded3d9d76..a6c105072307 100644 --- a/sdk/cosmos/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/AddressRepositoryIT.java +++ b/sdk/cosmos/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/AddressRepositoryIT.java @@ -2,12 +2,16 @@ // Licensed under the MIT License. package com.azure.spring.data.cosmos.repository.integration; +import com.azure.cosmos.implementation.PreconditionFailedException; import com.azure.cosmos.models.PartitionKey; import com.azure.spring.data.cosmos.IntegrationTestCollectionManager; import com.azure.spring.data.cosmos.common.TestConstants; import com.azure.spring.data.cosmos.common.TestUtils; import com.azure.spring.data.cosmos.core.CosmosTemplate; import com.azure.spring.data.cosmos.domain.Address; +import com.azure.cosmos.models.CosmosPatchItemRequestOptions; +import com.azure.cosmos.models.CosmosPatchOperations; +import com.azure.spring.data.cosmos.exception.CosmosAccessException; import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; import com.azure.spring.data.cosmos.repository.repository.AddressRepository; import org.assertj.core.util.Lists; @@ -27,12 +31,14 @@ import java.util.List; import java.util.Optional; +import static com.azure.spring.data.cosmos.common.TestConstants.CITY; import static com.azure.spring.data.cosmos.domain.Address.TEST_ADDRESS1_PARTITION1; import static com.azure.spring.data.cosmos.domain.Address.TEST_ADDRESS1_PARTITION2; import static com.azure.spring.data.cosmos.domain.Address.TEST_ADDRESS2_PARTITION1; import static com.azure.spring.data.cosmos.domain.Address.TEST_ADDRESS4_PARTITION3; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertNull; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = TestRepositoryConfig.class) @@ -50,6 +56,21 @@ public class AddressRepositoryIT { @Rule public ExpectedException expectedException = ExpectedException.none(); + CosmosPatchOperations patchSetOperation = CosmosPatchOperations + .create() + .set("/street", TestConstants.NEW_STREET); + + CosmosPatchOperations patchReplaceOperation = CosmosPatchOperations + .create() + .replace("/street", TestConstants.NEW_STREET); + + CosmosPatchOperations patchRemoveOperation = CosmosPatchOperations + .create() + .remove("/street"); + + private static final CosmosPatchItemRequestOptions options = new CosmosPatchItemRequestOptions(); + + @Before public void setUp() { collectionManager.ensureContainersCreatedAndEmpty(template, Address.class); @@ -233,4 +254,40 @@ public void testUpdateEntity() { assertThat(results.get(0).getStreet()).isEqualTo(updatedAddress.getStreet()); assertThat(results.get(0).getPostalCode()).isEqualTo(updatedAddress.getPostalCode()); } + + @Test + public void testPatchEntitySet() { + Address patchedAddress = repository.save(TestConstants.POSTAL_CODE, new PartitionKey(CITY), Address.class, patchSetOperation); + assertThat(patchedAddress.getStreet()).isEqualTo(TestConstants.NEW_STREET); + } + + @Test + public void testPatchEntityReplace() { + Address patchedAddress = repository.save(TestConstants.POSTAL_CODE, new PartitionKey(CITY), Address.class, patchReplaceOperation); + assertThat(patchedAddress.getStreet()).isEqualTo(TestConstants.NEW_STREET); + } + + @Test + public void testPatchEntityRemove() { + Address patchedAddress = repository.save(TestConstants.POSTAL_CODE, new PartitionKey(CITY), Address.class, patchRemoveOperation); + assertNull(patchedAddress.getStreet()); + } + @Test + public void testPatchPreConditionSuccess() { + options.setFilterPredicate("FROM address a WHERE a.city = '"+CITY+"'"); + Address patchedAddress = repository.save(TestConstants.POSTAL_CODE, new PartitionKey(CITY), Address.class, patchSetOperation, options); + assertThat(patchedAddress.getStreet()).isEqualTo(TestConstants.NEW_STREET); + } + + @Test + public void testPatchPreConditionFail() { + try { + options.setFilterPredicate("FROM address a WHERE a.city = 'dummy'"); + Address patchedAddress = repository.save(TestConstants.POSTAL_CODE, new PartitionKey(CITY), Address.class, patchSetOperation, options); + assertThat(patchedAddress.getStreet()).isEqualTo(TestConstants.NEW_STREET); + Assert.fail(); + } catch (CosmosAccessException ex) { + assertThat(ex.getCosmosException()).isInstanceOf(PreconditionFailedException.class); + } + } } diff --git a/sdk/cosmos/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ReactiveCourseRepositoryIT.java b/sdk/cosmos/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ReactiveCourseRepositoryIT.java index 72ebf0bc8108..4c05d5cd4359 100644 --- a/sdk/cosmos/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ReactiveCourseRepositoryIT.java +++ b/sdk/cosmos/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ReactiveCourseRepositoryIT.java @@ -2,6 +2,9 @@ // Licensed under the MIT License. package com.azure.spring.data.cosmos.repository.integration; +import com.azure.cosmos.implementation.PreconditionFailedException; +import com.azure.cosmos.models.CosmosPatchItemRequestOptions; +import com.azure.cosmos.models.CosmosPatchOperations; import com.azure.cosmos.models.PartitionKey; import com.azure.spring.data.cosmos.ReactiveIntegrationTestCollectionManager; import com.azure.spring.data.cosmos.core.ReactiveCosmosTemplate; @@ -45,6 +48,7 @@ public class ReactiveCourseRepositoryIT { private static final String COURSE_NAME_3 = "Course3"; private static final String COURSE_NAME_4 = "Course4"; private static final String COURSE_NAME_5 = "Course5"; + private static final String PATCH_COURSE_NAME_1 = "PathedCourse1"; private static final String DEPARTMENT_NAME_1 = "Department1"; private static final String DEPARTMENT_NAME_2 = "Department2"; @@ -66,6 +70,20 @@ public class ReactiveCourseRepositoryIT { private ReactiveCourseRepository repository; private CosmosEntityInformation entityInformation; + CosmosPatchOperations patchSetOperation = CosmosPatchOperations + .create() + .set("/name", PATCH_COURSE_NAME_1); + + CosmosPatchOperations patchReplaceOperation = CosmosPatchOperations + .create() + .replace("/name", PATCH_COURSE_NAME_1); + + CosmosPatchOperations patchRemoveOperation = CosmosPatchOperations + .create() + .remove("/name"); + + private static final CosmosPatchItemRequestOptions options = new CosmosPatchItemRequestOptions(); + @Before public void setUp() { collectionManager.ensureContainersCreatedAndEmpty(template, Course.class); @@ -310,4 +328,42 @@ public void testAnnotatedQueries() { StepVerifier.create(courseGroupBy).expectComplete(); StepVerifier.create(courseGroupBy).expectNextCount(1); } + + @Test + public void testPatchEntitySet() { + Mono patch = repository.save(COURSE_ID_1, new PartitionKey(DEPARTMENT_NAME_3), Course.class, patchSetOperation); + StepVerifier.create(patch).expectNextCount(1).verifyComplete(); + Mono patchedCourse = repository.findById(COURSE_ID_1); + StepVerifier.create(patchedCourse).expectNextMatches(course -> course.getName().equals(PATCH_COURSE_NAME_1)).verifyComplete(); + } + + @Test + public void testPatchEntityReplace() { + Mono patch = repository.save(COURSE_ID_2, new PartitionKey(DEPARTMENT_NAME_2), Course.class, patchReplaceOperation); + StepVerifier.create(patch).expectNextCount(1).verifyComplete(); + Mono patchedCourse = repository.findById(COURSE_ID_2); + StepVerifier.create(patchedCourse).expectNextMatches(course -> course.getName().equals(PATCH_COURSE_NAME_1)).verifyComplete(); + } + + @Test + public void testPatchEntityRemove() { + Mono patch = repository.save(COURSE_ID_1, new PartitionKey(DEPARTMENT_NAME_3), Course.class, patchRemoveOperation); + StepVerifier.create(patch).expectNextCount(1).verifyComplete(); + Mono patchedCourse = repository.findById(COURSE_ID_1); + StepVerifier.create(patchedCourse).expectNextMatches(course -> course.getName() == null).verifyComplete(); + } + @Test + public void testPatchPreConditionSuccess() { + options.setFilterPredicate("FROM course a WHERE a.department = '"+DEPARTMENT_NAME_3+"'"); + Mono patchedCourse = repository.save(COURSE_ID_1, new PartitionKey(DEPARTMENT_NAME_3), Course.class, patchSetOperation, options); + StepVerifier.create(patchedCourse).expectNextMatches(course -> course.getName().equals(PATCH_COURSE_NAME_1)).verifyComplete(); + } + + @Test + public void testPatchPreConditionFail() { + options.setFilterPredicate("FROM course a WHERE a.department = 'dummy'"); + Mono patchedCourse = repository.save(COURSE_ID_1, new PartitionKey(DEPARTMENT_NAME_3), Course.class, patchSetOperation, options); + StepVerifier.create(patchedCourse).expectErrorMatches(ex -> ex instanceof CosmosAccessException && + ((CosmosAccessException) ex).getCosmosException() instanceof PreconditionFailedException).verify(); + } } diff --git a/sdk/cosmos/azure-spring-data-cosmos/CHANGELOG.md b/sdk/cosmos/azure-spring-data-cosmos/CHANGELOG.md index 2d039b74398c..468d79184043 100644 --- a/sdk/cosmos/azure-spring-data-cosmos/CHANGELOG.md +++ b/sdk/cosmos/azure-spring-data-cosmos/CHANGELOG.md @@ -4,6 +4,7 @@ #### Features Added * Added support for multi-tenancy at the Database level via `CosmosFactory` - See [PR 32516](https://github.com/Azure/azure-sdk-for-java/pull/32516) +* Added support for patch - See [PR 32630](https://github.com/Azure/azure-sdk-for-java/pull/32630) #### Breaking Changes diff --git a/sdk/cosmos/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/CosmosOperations.java b/sdk/cosmos/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/CosmosOperations.java index 62d2f891ec16..e2f5909a7882 100644 --- a/sdk/cosmos/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/CosmosOperations.java +++ b/sdk/cosmos/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/CosmosOperations.java @@ -4,6 +4,8 @@ package com.azure.spring.data.cosmos.core; import com.azure.cosmos.models.CosmosContainerProperties; +import com.azure.cosmos.models.CosmosPatchItemRequestOptions; +import com.azure.cosmos.models.CosmosPatchOperations; import com.azure.cosmos.models.PartitionKey; import com.azure.cosmos.models.SqlQuerySpec; import com.azure.spring.data.cosmos.core.convert.MappingCosmosConverter; @@ -123,6 +125,28 @@ public interface CosmosOperations { */ T insert(T objectToSave, PartitionKey partitionKey); + /** + * patches item + * @param id must not be {@literal null} + * @param partitionKey must not be {@literal null} + * @param domainType must not be {@literal null} + * @param patchOperations must not be {@literal null}, max operations is 10 + * @param type class of domain type + * @return the patched item + */ + T patch(Object id, PartitionKey partitionKey, Class domainType, CosmosPatchOperations patchOperations); + + /** + * patches item + * @param id must not be {@literal null} + * @param partitionKey must not be {@literal null} + * @param domainType must not be {@literal null} + * @param patchOperations must not be {@literal null}, max operations is 10 + * @param options Optional CosmosPatchItemRequestOptions, e.g. options.setFilterPredicate("FROM products p WHERE p.used = false"); + * @param type class of domain type + * @return the patched item + */ + T patch(Object id, PartitionKey partitionKey, Class domainType, CosmosPatchOperations patchOperations, CosmosPatchItemRequestOptions options); /** * Inserts item * diff --git a/sdk/cosmos/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/CosmosTemplate.java b/sdk/cosmos/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/CosmosTemplate.java index 4768cd768e9e..7067bfb1d4ad 100644 --- a/sdk/cosmos/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/CosmosTemplate.java +++ b/sdk/cosmos/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/CosmosTemplate.java @@ -11,6 +11,8 @@ import com.azure.cosmos.models.CosmosDatabaseResponse; import com.azure.cosmos.models.CosmosItemRequestOptions; import com.azure.cosmos.models.CosmosItemResponse; +import com.azure.cosmos.models.CosmosPatchItemRequestOptions; +import com.azure.cosmos.models.CosmosPatchOperations; import com.azure.cosmos.models.CosmosQueryRequestOptions; import com.azure.cosmos.models.FeedResponse; import com.azure.cosmos.models.PartitionKey; @@ -236,6 +238,62 @@ public T insert(String containerName, T objectToSave, PartitionKey partition return toDomainObject(domainType, response.getItem()); } + /** + * Patches item + * + * applies partial update (patch) to an item + * @param id must not be {@literal null} + * @param partitionKey must not be {@literal null} + * @param patchOperations must not be {@literal null} + * @param type class of domain type + * @return the patched item + */ + @Override + public T patch(Object id, PartitionKey partitionKey, Class domainType, CosmosPatchOperations patchOperations) { + return patch(id, partitionKey, domainType, patchOperations, null); + } + + /** + * applies partial update (patch) to an item with CosmosPatchItemRequestOptions + * + * @param id must not be {@literal null} + * @param partitionKey must not be {@literal null} + * @param domainType must not be {@literal null} + * @param patchOperations must not be {@literal null} + * @param options Optional CosmosPatchItemRequestOptions, e.g. options.setFilterPredicate("FROM products p WHERE p.used = false"); + * @param type class of domain type + * @return the patched item + */ + public T patch(Object id, PartitionKey partitionKey, Class domainType, CosmosPatchOperations patchOperations, CosmosPatchItemRequestOptions options) { + Assert.notNull(patchOperations, "expected non-null cosmosPatchOperations"); + + final String containerName = getContainerName(domainType); + Assert.notNull(id, "id should not be null"); + Assert.notNull(partitionKey, "partitionKey should not be null, empty or only whitespaces"); + Assert.notNull(patchOperations, "patchOperations should not be null, empty or only whitespaces"); + + LOGGER.debug("execute patchItem in database {} container {}", this.getDatabaseName(), + containerName); + + if (options == null) { + options = new CosmosPatchItemRequestOptions(); + } + final CosmosItemResponse response = this.getCosmosAsyncClient() + .getDatabase(this.getDatabaseName()) + .getContainer(containerName) + .patchItem(id.toString(), partitionKey, patchOperations, options, JsonNode.class) + .publishOn(Schedulers.parallel()) + .doOnNext(cosmosItemResponse -> + CosmosUtils.fillAndProcessResponseDiagnostics(this.responseDiagnosticsProcessor, + cosmosItemResponse.getDiagnostics(), null)) + .onErrorResume(throwable -> + CosmosExceptionUtils.exceptionHandler("Failed to patch item", throwable, + this.responseDiagnosticsProcessor)) + .block(); + assert response != null; + return toDomainObject(domainType, response.getItem()); + } + @SuppressWarnings("unchecked") private void generateIdIfNullAndAutoGenerationEnabled(T originalItem, Class type) { CosmosEntityInformation entityInfo = CosmosEntityInformation.getInstance(type); @@ -591,8 +649,7 @@ public void deleteById(String containerName, Object id, PartitionKey partitionKe @Override public void deleteEntity(String containerName, T entity) { Assert.notNull(entity, "entity to be deleted should not be null"); - @SuppressWarnings("unchecked") - final Class domainType = (Class) entity.getClass(); + @SuppressWarnings("unchecked") final Class domainType = (Class) entity.getClass(); final JsonNode originalItem = mappingCosmosConverter.writeJsonNode(entity); final CosmosItemRequestOptions options = new CosmosItemRequestOptions(); applyVersioning(entity.getClass(), originalItem, options); @@ -689,8 +746,8 @@ public Iterable delete(@NonNull CosmosQuery query, @NonNull Class doma final List results = findItemsAsFlux(query, containerName, domainType).collectList().block(); assert results != null; return results.stream() - .map(item -> deleteItem(item, containerName, domainType)) - .collect(Collectors.toList()); + .map(item -> deleteItem(item, containerName, domainType)) + .collect(Collectors.toList()); } @Override @@ -737,8 +794,8 @@ public Slice runSliceQuery(SqlQuerySpec querySpec, Pageable pageable, Cla } private Page paginationQuery(SqlQuerySpec querySpec, SqlQuerySpec countQuerySpec, - Pageable pageable, Sort sort, - Class returnType, String containerName, + Pageable pageable, Sort sort, + Class returnType, String containerName, Optional partitionKeyValue) { Slice response = sliceQuery(querySpec, pageable, sort, returnType, containerName, partitionKeyValue); final long total = getCountValue(countQuerySpec, containerName); @@ -746,8 +803,8 @@ private Page paginationQuery(SqlQuerySpec querySpec, SqlQuerySpec countQu } private Slice sliceQuery(SqlQuerySpec querySpec, - Pageable pageable, Sort sort, - Class returnType, String containerName, + Pageable pageable, Sort sort, + Class returnType, String containerName, Optional partitionKeyValue) { Assert.isTrue(pageable.getPageSize() > 0, "pageable should have page size larger than 0"); @@ -884,9 +941,9 @@ public Iterable runQuery(SqlQuerySpec querySpec, Class domainType, Cla public Iterable runQuery(SqlQuerySpec querySpec, Sort sort, Class domainType, Class returnType) { querySpec = NativeQueryGenerator.getInstance().generateSortedQuery(querySpec, sort); return getJsonNodeFluxFromQuerySpec(getContainerName(domainType), querySpec) - .map(jsonNode -> emitOnLoadEventAndConvertToDomainObject(returnType, getContainerName(domainType), jsonNode)) - .collectList() - .block(); + .map(jsonNode -> emitOnLoadEventAndConvertToDomainObject(returnType, getContainerName(domainType), jsonNode)) + .collectList() + .block(); } private void markAuditedIfConfigured(Object object) { @@ -951,8 +1008,8 @@ private Flux findItemsAsFlux(@NonNull CosmosQuery query, .publishOn(Schedulers.parallel()) .flatMap(cosmosItemFeedResponse -> { CosmosUtils.fillAndProcessResponseDiagnostics(this.responseDiagnosticsProcessor, - cosmosItemFeedResponse.getCosmosDiagnostics(), - cosmosItemFeedResponse); + cosmosItemFeedResponse.getCosmosDiagnostics(), + cosmosItemFeedResponse); return Flux.fromIterable(cosmosItemFeedResponse.getResults()); }) .onErrorResume(throwable -> diff --git a/sdk/cosmos/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/ReactiveCosmosOperations.java b/sdk/cosmos/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/ReactiveCosmosOperations.java index 1f440e7c0345..f827271efcc3 100644 --- a/sdk/cosmos/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/ReactiveCosmosOperations.java +++ b/sdk/cosmos/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/ReactiveCosmosOperations.java @@ -5,6 +5,8 @@ import com.azure.cosmos.models.CosmosContainerProperties; import com.azure.cosmos.models.CosmosContainerResponse; +import com.azure.cosmos.models.CosmosPatchItemRequestOptions; +import com.azure.cosmos.models.CosmosPatchOperations; import com.azure.cosmos.models.PartitionKey; import com.azure.cosmos.models.SqlQuerySpec; import com.azure.spring.data.cosmos.core.convert.MappingCosmosConverter; @@ -145,6 +147,29 @@ Mono replaceContainerProperties(String containerName, */ Mono insert(String containerName, T objectToSave); + /** + * patches item + * @param id must not be {@literal null} + * @param partitionKey must not be {@literal null} + * @param domainType must not be {@literal null} + * @param patchOperations must not be {@literal null}, max operations is 10 + * @param type class of domain type + * @return the patched item + */ + Mono patch(Object id, PartitionKey partitionKey, Class domainType, CosmosPatchOperations patchOperations); + + /** + * patches item + * @param id must not be {@literal null} + * @param partitionKey must not be {@literal null} + * @param domainType must not be {@literal null} + * @param patchOperations must not be {@literal null}, max operations is 10 + * @param options Optional CosmosPatchItemRequestOptions, e.g. options.setFilterPredicate("FROM products p WHERE p.used = false"); + * @param type class of domain type + * @return Mono with the patched item + */ + Mono patch(Object id, PartitionKey partitionKey, Class domainType, CosmosPatchOperations patchOperations, CosmosPatchItemRequestOptions options); + /** * Upsert an item with partition key * diff --git a/sdk/cosmos/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/ReactiveCosmosTemplate.java b/sdk/cosmos/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/ReactiveCosmosTemplate.java index 9becf4409696..8fda51004b98 100644 --- a/sdk/cosmos/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/ReactiveCosmosTemplate.java +++ b/sdk/cosmos/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/ReactiveCosmosTemplate.java @@ -9,6 +9,8 @@ import com.azure.cosmos.models.CosmosContainerResponse; import com.azure.cosmos.models.CosmosDatabaseResponse; import com.azure.cosmos.models.CosmosItemRequestOptions; +import com.azure.cosmos.models.CosmosPatchItemRequestOptions; +import com.azure.cosmos.models.CosmosPatchOperations; import com.azure.cosmos.models.CosmosQueryRequestOptions; import com.azure.cosmos.models.FeedResponse; import com.azure.cosmos.models.PartitionKey; @@ -452,6 +454,63 @@ public Mono insert(String containerName, T objectToSave) { return insert(containerName, objectToSave, null); } + /** + * Patches item + * + * applies partial update (patch) to an item + * @param id must not be {@literal null} + * @param partitionKey must not be {@literal null} + * @param domainType must not be {@literal null} + * @param patchOperations must not be {@literal null} + * @param type class of domain type + * @return the patched item + */ + @Override + public Mono patch(Object id, PartitionKey partitionKey, Class domainType, CosmosPatchOperations patchOperations) { + return patch(id, partitionKey, domainType, patchOperations, null); + } + + /** + * applies partial update (patch) to an item with CosmosPatchItemRequestOptions + * + * @param id must not be {@literal null} + * @param partitionKey must not be {@literal null} + * @param domainType must not be {@literal null} + * @param patchOperations must not be {@literal null} + * @param options Optional CosmosPatchItemRequestOptions, e.g. options.setFilterPredicate("FROM products p WHERE p.used = false"); + * @param type class of domain type + * @return the patched item + */ + public Mono patch(Object id, PartitionKey partitionKey, Class domainType, CosmosPatchOperations patchOperations, CosmosPatchItemRequestOptions options) { + Assert.notNull(patchOperations, "expected non-null cosmosPatchOperations"); + + final String containerName = getContainerName(domainType); + Assert.notNull(id, "id should not be null"); + Assert.notNull(partitionKey, "partitionKey should not be null, empty or only whitespaces"); + Assert.notNull(patchOperations, "patchOperations should not be null, empty or only whitespaces"); + + LOGGER.debug("execute patchItem in database {} container {}", this.getDatabaseName(), containerName); + + if (options == null) { + options = new CosmosPatchItemRequestOptions(); + } + + return this.getCosmosAsyncClient() + .getDatabase(this.getDatabaseName()) + .getContainer(containerName) + .patchItem(id.toString(), partitionKey, patchOperations, options, JsonNode.class) + .publishOn(Schedulers.parallel()) + .onErrorResume(throwable -> + CosmosExceptionUtils.exceptionHandler("Failed to patch item", throwable, + this.responseDiagnosticsProcessor)) + .flatMap(cosmosItemResponse -> { + CosmosUtils.fillAndProcessResponseDiagnostics(this.responseDiagnosticsProcessor, + cosmosItemResponse.getDiagnostics(), null); + return Mono.just(toDomainObject(domainType, cosmosItemResponse.getItem())); + }); + + } + @SuppressWarnings("unchecked") private void generateIdIfNullAndAutoGenerationEnabled(T originalItem, Class type) { CosmosEntityInformation entityInfo = CosmosEntityInformation.getInstance(type); @@ -774,8 +833,8 @@ private void markAuditedIfConfigured(Object object) { } private Flux findItems(@NonNull CosmosQuery query, - @NonNull String containerName, - @NonNull Class domainType) { + @NonNull String containerName, + @NonNull Class domainType) { final SqlQuerySpec sqlQuerySpec = new FindQuerySpecGenerator().generateCosmos(query); final CosmosQueryRequestOptions cosmosQueryRequestOptions = new CosmosQueryRequestOptions(); cosmosQueryRequestOptions.setQueryMetricsEnabled(this.queryMetricsEnabled); diff --git a/sdk/cosmos/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/CosmosRepository.java b/sdk/cosmos/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/CosmosRepository.java index 9615df518584..5763e9695313 100644 --- a/sdk/cosmos/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/CosmosRepository.java +++ b/sdk/cosmos/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/CosmosRepository.java @@ -3,6 +3,8 @@ package com.azure.spring.data.cosmos.repository; +import com.azure.cosmos.models.CosmosPatchItemRequestOptions; +import com.azure.cosmos.models.CosmosPatchOperations; import com.azure.cosmos.models.PartitionKey; import org.springframework.data.repository.NoRepositoryBean; import org.springframework.data.repository.PagingAndSortingRepository; @@ -36,6 +38,33 @@ public interface CosmosRepository extends PagingAndS */ void deleteById(ID id, PartitionKey partitionKey); + /** + * Patches an entity by its id and partition key with CosmosPatchItemRequestOptions + * + * @param id must not be {@literal null} + * @param partitionKey must not be {@literal null} + * @param domainType must not be {@literal null} + * @param patchOperations must not be {@literal null}, max operations is 10 + * @param type class of domain type + * @return the patched entity + * @throws IllegalArgumentException in case the given {@code id} is {@literal null}. + */ + S save(ID id, PartitionKey partitionKey, Class domainType, CosmosPatchOperations patchOperations); + + /** + * Patches an entity by its id and partition key with CosmosPatchItemRequestOptions + * + * @param id must not be {@literal null} + * @param partitionKey must not be {@literal null} + * @param domainType must not be {@literal null} + * @param patchOperations must not be {@literal null}, max operations is 10 + * @param options Optional CosmosPatchItemRequestOptions, e.g. options.setFilterPredicate("FROM products p WHERE p.used = false"); + * @param type class of domain type + * @return the patched entity + * @throws IllegalArgumentException in case the given {@code id} is {@literal null}. + */ + S save(ID id, PartitionKey partitionKey, Class domainType, CosmosPatchOperations patchOperations, CosmosPatchItemRequestOptions options); + /** * Returns list of items in a specific partition * diff --git a/sdk/cosmos/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/ReactiveCosmosRepository.java b/sdk/cosmos/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/ReactiveCosmosRepository.java index 60a28bec9397..0499e2e9c679 100644 --- a/sdk/cosmos/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/ReactiveCosmosRepository.java +++ b/sdk/cosmos/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/ReactiveCosmosRepository.java @@ -2,6 +2,8 @@ // Licensed under the MIT License. package com.azure.spring.data.cosmos.repository; +import com.azure.cosmos.models.CosmosPatchItemRequestOptions; +import com.azure.cosmos.models.CosmosPatchOperations; import com.azure.cosmos.models.PartitionKey; import org.springframework.data.repository.NoRepositoryBean; import org.springframework.data.repository.reactive.ReactiveSortingRepository; @@ -31,6 +33,33 @@ public interface ReactiveCosmosRepository extends ReactiveSortingRepositor */ Mono deleteById(K id, PartitionKey partitionKey); + /** + * Patches an entity by its id and partition key with CosmosPatchItemRequestOptions + * + * @param id must not be {@literal null} + * @param partitionKey must not be {@literal null} + * @param domainType must not be {@literal null} + * @param patchOperations must not be {@literal null}, max operations is 10 + * @param type class of domain type + * @return the patched entity + * @throws IllegalArgumentException in case the given {@code id} is {@literal null}. + */ + Mono save(K id, PartitionKey partitionKey, Class domainType, CosmosPatchOperations patchOperations); + + /** + * Patches an entity by its id and partition key with CosmosPatchItemRequestOptions + * + * @param id must not be {@literal null} + * @param partitionKey must not be {@literal null} + * @param domainType must not be {@literal null} + * @param patchOperations must not be {@literal null}, max operations is 10 + * @param options Optional CosmosPatchItemRequestOptions, e.g. options.setFilterPredicate("FROM products p WHERE p.used = false"); + * @param type class of domain type + * @return the patched entity + * @throws IllegalArgumentException in case the given {@code id} is {@literal null}. + */ + Mono save(K id, PartitionKey partitionKey, Class domainType, CosmosPatchOperations patchOperations, CosmosPatchItemRequestOptions options); + /** * Returns Flux of items in a specific partition * @param partitionKey partition key value diff --git a/sdk/cosmos/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/SimpleCosmosRepository.java b/sdk/cosmos/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/SimpleCosmosRepository.java index 8b308a77e501..58ab930d2f85 100644 --- a/sdk/cosmos/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/SimpleCosmosRepository.java +++ b/sdk/cosmos/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/SimpleCosmosRepository.java @@ -5,6 +5,8 @@ import com.azure.cosmos.CosmosException; import com.azure.cosmos.models.CosmosContainerProperties; +import com.azure.cosmos.models.CosmosPatchItemRequestOptions; +import com.azure.cosmos.models.CosmosPatchOperations; import com.azure.cosmos.models.PartitionKey; import com.azure.spring.data.cosmos.core.CosmosOperations; import com.azure.spring.data.cosmos.core.query.CosmosQuery; @@ -97,6 +99,37 @@ public S save(S entity) { } } + /** + * patch entity with CosmosPatchItemRequestOptions + * @param id of entity to be patched + * @param partitionKey of entity to be patched + * @param patchOperations for entity to be patched + * @param domainType of entity + */ + @Override + public S save(ID id, PartitionKey partitionKey, Class domainType, CosmosPatchOperations patchOperations) { + Assert.notNull(id, "id must not be null"); + Assert.notNull(partitionKey, "partitionKey must not be null"); + // patch items + return operation.patch(id, partitionKey, domainType, patchOperations); + } + + /** + * patch entity with CosmosPatchItemRequestOptions + * @param id of entity to be patched + * @param partitionKey of entity to be patched + * @param patchOperations for entity to be patched + * @param options options + * @param domainType of entity + */ + @Override + public S save(ID id, PartitionKey partitionKey, Class domainType, CosmosPatchOperations patchOperations, CosmosPatchItemRequestOptions options) { + Assert.notNull(id, "id must not be null"); + Assert.notNull(partitionKey, "partitionKey must not be null"); + // patch items + return operation.patch(id, partitionKey, domainType, patchOperations, options); + } + /** * batch save entities * diff --git a/sdk/cosmos/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/SimpleReactiveCosmosRepository.java b/sdk/cosmos/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/SimpleReactiveCosmosRepository.java index dfe93ee68a6a..e8ce109fb7a9 100644 --- a/sdk/cosmos/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/SimpleReactiveCosmosRepository.java +++ b/sdk/cosmos/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/SimpleReactiveCosmosRepository.java @@ -5,6 +5,8 @@ import com.azure.cosmos.CosmosException; import com.azure.cosmos.models.CosmosContainerProperties; import com.azure.cosmos.models.CosmosContainerResponse; +import com.azure.cosmos.models.CosmosPatchItemRequestOptions; +import com.azure.cosmos.models.CosmosPatchOperations; import com.azure.cosmos.models.PartitionKey; import com.azure.spring.data.cosmos.core.ReactiveCosmosOperations; import com.azure.spring.data.cosmos.core.query.CosmosQuery; @@ -103,6 +105,20 @@ public Mono save(S entity) { } } + @Override + public Mono save(K id, PartitionKey partitionKey, Class domainType, CosmosPatchOperations patchOperations) { + Assert.notNull(id, "entity must not be null"); + // patch items + return cosmosOperations.patch(id, partitionKey, domainType, patchOperations); + } + + @Override + public Mono save(K id, PartitionKey partitionKey, Class domainType, CosmosPatchOperations patchOperations, CosmosPatchItemRequestOptions options) { + Assert.notNull(id, "entity must not be null"); + // patch items + return cosmosOperations.patch(id, partitionKey, domainType, patchOperations, options); + } + @Override public Flux saveAll(Iterable entities) {