-
Notifications
You must be signed in to change notification settings - Fork 14
[CHA-3618] feat: support custom_set and custom_unset in batch channel update #270
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
11f8281
561771f
51290ff
4019485
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ | |
|
|
||
| import io.getstream.chat.java.models.Channel.*; | ||
| import java.util.ArrayList; | ||
| import java.util.HashMap; | ||
| import java.util.List; | ||
| import org.jetbrains.annotations.NotNull; | ||
|
|
||
|
|
@@ -194,4 +195,29 @@ public ChannelsBatchUpdateRequest updateData( | |
| options.setData(data); | ||
| return Channel.updateBatch(options); | ||
| } | ||
|
|
||
| /** | ||
| * Updates data on channels matching the filter. | ||
| * | ||
| * <p>{@code customSet} and {@code customUnset} patch individual custom keys, leaving every other | ||
| * custom key untouched. They cannot be combined with {@code data.custom}, which replaces the | ||
| * whole custom object; the backend validates that and the other combinations it rejects. | ||
| * | ||
| * @param filter the filter to match channels | ||
| * @param update options containing channel data and custom keys to update | ||
| * @return the batch update request | ||
| */ | ||
| @NotNull | ||
| public ChannelsBatchUpdateRequest updateData( | ||
| @NotNull ChannelsBatchFilters filter, @NotNull ChannelBatchDataUpdateOptions update) { | ||
| ChannelsBatchOptions options = new ChannelsBatchOptions(); | ||
| options.setOperation(ChannelBatchOperation.UPDATE_DATA); | ||
| options.setFilter(filter); | ||
| options.setData(update.getData()); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Must Fix]
On the v1 route
That is the second example in the PR description and in Same root cause, already shipping today on the existing Fix: How I checkedJackson side, same mapper config as Backend side, feeding that exact body through
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in |
||
| options.setCustomSet( | ||
| update.getCustomSet() != null ? new HashMap<>(update.getCustomSet()) : null); | ||
| options.setCustomUnset( | ||
| update.getCustomUnset() != null ? new ArrayList<>(update.getCustomUnset()) : null); | ||
| return Channel.updateBatch(options); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,158 @@ | ||
| package io.getstream.chat.java; | ||
|
|
||
| import com.fasterxml.jackson.annotation.JsonAutoDetect; | ||
| import com.fasterxml.jackson.annotation.PropertyAccessor; | ||
| import com.fasterxml.jackson.databind.JsonNode; | ||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||
| import io.getstream.chat.java.models.Channel; | ||
| import io.getstream.chat.java.models.Channel.ChannelBatchDataUpdateOptions; | ||
| import io.getstream.chat.java.models.Channel.ChannelBatchOperation; | ||
| import io.getstream.chat.java.models.Channel.ChannelDataUpdate; | ||
| import io.getstream.chat.java.models.Channel.ChannelsBatchFilters; | ||
| import io.getstream.chat.java.models.Channel.ChannelsBatchOptions; | ||
| import java.util.HashMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import org.junit.jupiter.api.Assertions; | ||
| import org.junit.jupiter.api.DisplayName; | ||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| public class ChannelBatchCustomPatchTest { | ||
|
|
||
| // Mirrors the visibility configuration of DefaultClient's mapper. | ||
| private static final ObjectMapper MAPPER = | ||
| new ObjectMapper() | ||
| .setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE) | ||
| .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); | ||
|
|
||
| private static ChannelsBatchFilters filterByCids() { | ||
| var filter = new ChannelsBatchFilters(); | ||
| Map<String, Object> cids = new HashMap<>(); | ||
| cids.put("$in", List.of("messaging:a", "messaging:b")); | ||
| filter.setCids(cids); | ||
| return filter; | ||
| } | ||
|
|
||
| @DisplayName("The custom patch serializes at the request root, not inside data") | ||
| @Test | ||
| void whenSettingCustomPatch_thenSerializedAtRequestRoot() throws Exception { | ||
| var options = new ChannelsBatchOptions(); | ||
| options.setOperation(ChannelBatchOperation.UPDATE_DATA); | ||
| options.setFilter(filterByCids()); | ||
| Map<String, Object> customSet = new HashMap<>(); | ||
| customSet.put("group", "old"); | ||
| options.setCustomSet(customSet); | ||
| options.setCustomUnset(List.of("location_id")); | ||
|
|
||
| JsonNode root = MAPPER.readTree(MAPPER.writeValueAsString(options)); | ||
|
|
||
| Assertions.assertEquals("old", root.path("custom_set").path("group").asText()); | ||
| Assertions.assertEquals(1, root.path("custom_unset").size()); | ||
| Assertions.assertEquals("location_id", root.path("custom_unset").get(0).asText()); | ||
| // The fields are siblings of operation and filter. Inside data they would be | ||
| // collected into custom by the v1 extra-fields sink instead. | ||
| Assertions.assertEquals("updateData", root.path("operation").asText()); | ||
| Assertions.assertTrue(root.hasNonNull("filter")); | ||
| Assertions.assertFalse(root.path("data").has("custom_set")); | ||
| Assertions.assertFalse(root.path("data").has("custom_unset")); | ||
| } | ||
|
|
||
| @DisplayName("The custom patch fields are omitted when not set") | ||
| @Test | ||
| void whenCustomPatchNotSet_thenOmittedFromTheRequest() throws Exception { | ||
| var options = new ChannelsBatchOptions(); | ||
| options.setOperation(ChannelBatchOperation.UPDATE_DATA); | ||
| options.setFilter(filterByCids()); | ||
| var data = new ChannelDataUpdate(); | ||
| data.setFrozen(true); | ||
| options.setData(data); | ||
|
|
||
| JsonNode root = MAPPER.readTree(MAPPER.writeValueAsString(options)); | ||
|
|
||
| Assertions.assertFalse(root.has("custom_set")); | ||
| Assertions.assertFalse(root.has("custom_unset")); | ||
| Assertions.assertFalse(root.path("data").has("custom")); | ||
| } | ||
|
|
||
| @DisplayName("updateData supports a custom-only patch without a null data placeholder") | ||
| @Test | ||
| void whenUpdatingCustomOnly_thenOptionsCarryTheFieldsWithoutData() { | ||
| var options = | ||
| Channel.channelBatchUpdater() | ||
| .updateData( | ||
| filterByCids(), | ||
| ChannelBatchDataUpdateOptions.builder() | ||
| .customSet(Map.of("group", "old")) | ||
| .customUnset(List.of("location_id")) | ||
| .build()) | ||
| .getOptions(); | ||
|
|
||
| Assertions.assertEquals(ChannelBatchOperation.UPDATE_DATA, options.getOperation()); | ||
| Assertions.assertNull(options.getData()); | ||
| Assertions.assertEquals(Map.of("group", "old"), options.getCustomSet()); | ||
| Assertions.assertEquals(List.of("location_id"), options.getCustomUnset()); | ||
| } | ||
|
|
||
| @DisplayName("updateData carries channel data and custom patches together") | ||
| @Test | ||
| void whenUpdatingDataWithAPatch_thenOptionsCarryBoth() { | ||
| var data = new ChannelDataUpdate(); | ||
| data.setFrozen(true); | ||
|
|
||
| var options = | ||
| Channel.channelBatchUpdater() | ||
| .updateData( | ||
| filterByCids(), | ||
| ChannelBatchDataUpdateOptions.builder() | ||
| .data(data) | ||
| .customSet(Map.of("group", "old")) | ||
| .customUnset(List.of("location_id")) | ||
| .build()) | ||
| .getOptions(); | ||
|
|
||
| Assertions.assertEquals(ChannelBatchOperation.UPDATE_DATA, options.getOperation()); | ||
| Assertions.assertEquals(Boolean.TRUE, options.getData().getFrozen()); | ||
| Assertions.assertEquals(Map.of("group", "old"), options.getCustomSet()); | ||
| Assertions.assertEquals(List.of("location_id"), options.getCustomUnset()); | ||
| } | ||
|
|
||
| @DisplayName("updateData leaves the custom patch unset when only data is given") | ||
| @Test | ||
| void whenUpdatingDataOnly_thenCustomPatchStaysNull() { | ||
| var data = new ChannelDataUpdate(); | ||
| data.setFrozen(true); | ||
|
|
||
| var options = Channel.channelBatchUpdater().updateData(filterByCids(), data).getOptions(); | ||
|
|
||
| Assertions.assertNull(options.getCustomSet()); | ||
| Assertions.assertNull(options.getCustomUnset()); | ||
| } | ||
|
|
||
| @DisplayName("The helper options are unpacked and never serialized") | ||
| @Test | ||
| void whenUpdatingDataWithAPatch_thenThePatchItselfIsAbsentFromTheRequest() throws Exception { | ||
| var data = new ChannelDataUpdate(); | ||
| data.setFrozen(true); | ||
|
|
||
| var options = | ||
| Channel.channelBatchUpdater() | ||
| .updateData( | ||
| filterByCids(), | ||
| ChannelBatchDataUpdateOptions.builder() | ||
| .data(data) | ||
| .customSet(Map.of("group", "old")) | ||
| .customUnset(List.of("location_id")) | ||
| .build()) | ||
| .getOptions(); | ||
|
|
||
| JsonNode root = MAPPER.readTree(MAPPER.writeValueAsString(options)); | ||
|
|
||
| Assertions.assertEquals("old", root.path("custom_set").path("group").asText()); | ||
| Assertions.assertEquals( | ||
| List.of("location_id"), List.of(root.path("custom_unset").get(0).asText())); | ||
| Assertions.assertFalse(root.path("data").has("custom")); | ||
| Assertions.assertFalse(root.has("customSet")); | ||
| Assertions.assertFalse(root.has("customUnset")); | ||
| Assertions.assertFalse(root.has("update")); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Should Fix] This also changes behavior for plain
updateDatacalls that never touchcustom: before this line the SDK always sent"custom": null, which replaced each matched channel's whole custom object. Please call that out in the release notes so upgrading users know their earlier batchupdateDatacalls were affected.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a Release note section to the PR description covering the impact on existing
updateDatacalls.