diff --git a/CHANGELOG.md b/CHANGELOG.md index 33453f5..8dbdd18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,13 @@ -## 0.4.2 - 2022-23-05 +## 0.4.4 - 2023-07-21 +* [enhancement] Support cursor based incremental [#84](https://github.com/treasure-data/embulk-input-zendesk/pull/84) + +## 0.4.3 - 2022-10-21 +* [enhancement] Bump up to v0.4.3, built with the Gradle plugin v0.5.5 [#78](https://github.com/treasure-data/embulk-input-zendesk/pull/78) + +## 0.4.2 - 2022-05-23 * [enhancement] Catchup embulk v0.10.32 [#77](https://github.com/treasure-data/embulk-input-zendesk/pull/77) -## 0.4.1 - 2022-29-03 +## 0.4.1 - 2022-03-29 * [enhancement] Remove deprecated functions [#76](https://github.com/treasure-data/embulk-input-zendesk/pull/76) ## 0.4.0 - 2022-03-03 diff --git a/README.md b/README.md index bf9d16d..b229b4d 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ Required Embulk version >= 0.9.6. - **profile_source**: Profile source of user event, required if `target` is `user_events`. - **user_event_source**: Source of user event, required if `target` is `user_events`. - **user_event_type**: Type of user event, required if `target` is `user_events`. +- **enable_cursor_based_api**: Enable to use cursor based api endpoint for tickets and users target (boolean, default: `false`) ## Example diff --git a/build.gradle b/build.gradle index 5b2e0e9..1ebacfd 100644 --- a/build.gradle +++ b/build.gradle @@ -15,7 +15,7 @@ repositories { def embulkVersion = '0.10.31' group = "com.treasuredata.embulk.plugins" -version = "0.4.3-SNAPSHOT" +version = "0.4.4-SNAPSHOT" description = "Loads records From Zendesk" sourceCompatibility = 1.8 diff --git a/src/main/java/org/embulk/input/zendesk/ZendeskInputPlugin.java b/src/main/java/org/embulk/input/zendesk/ZendeskInputPlugin.java index 4ab0707..1dec60b 100644 --- a/src/main/java/org/embulk/input/zendesk/ZendeskInputPlugin.java +++ b/src/main/java/org/embulk/input/zendesk/ZendeskInputPlugin.java @@ -13,6 +13,7 @@ import org.embulk.input.zendesk.models.AuthenticationMethod; import org.embulk.input.zendesk.models.Target; import org.embulk.input.zendesk.services.ZendeskChatService; +import org.embulk.input.zendesk.services.ZendeskCursorBasedService; import org.embulk.input.zendesk.services.ZendeskCustomObjectService; import org.embulk.input.zendesk.services.ZendeskNPSService; import org.embulk.input.zendesk.services.ZendeskService; @@ -157,6 +158,10 @@ public interface PluginTask @ConfigDefault("null") Optional getUserEventSource(); + @Config("enable_cursor_based_api") + @ConfigDefault("false") + boolean getEnableCursorBasedApi(); + @Config("columns") SchemaConfig getColumns(); } @@ -301,7 +306,7 @@ private JsonNode addAllColumnsToSchema(final JsonNode jsonNode, final Target tar ConfigDiff configDiff = guessData(jsonNode, target.getJsonName()); ConfigDiff parser = configDiff.getNested("parser"); if (parser.has("columns")) { - JsonNode columns = parser.get(JsonNode.class, "columns"); + JsonNode columns = parser.get(JsonNode.class, "columns"); final Iterator ite = columns.elements(); while (ite.hasNext()) { @@ -444,6 +449,12 @@ protected ZendeskService dispatchPerTarget(ZendeskInputPlugin.PluginTask task) switch (task.getTarget()) { case TICKETS: case USERS: + /* + The cursor based incremental API is enabled only tickets and users targets + It allows to fetch more than 10.000 records which is now the limitation of the old incremental api + https://developer.zendesk.com/documentation/ticketing/managing-tickets/using-the-incremental-export-api/#cursor-based-incremental-exports + */ + return task.getEnableCursorBasedApi() ? new ZendeskCursorBasedService(task) : new ZendeskSupportAPIService(task); case ORGANIZATIONS: case TICKET_METRICS: case TICKET_EVENTS: diff --git a/src/main/java/org/embulk/input/zendesk/services/ZendeskCursorBasedService.java b/src/main/java/org/embulk/input/zendesk/services/ZendeskCursorBasedService.java new file mode 100644 index 0000000..4c28644 --- /dev/null +++ b/src/main/java/org/embulk/input/zendesk/services/ZendeskCursorBasedService.java @@ -0,0 +1,174 @@ +package org.embulk.input.zendesk.services; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.annotations.VisibleForTesting; +import org.apache.http.HttpStatus; +import org.apache.http.client.utils.URIBuilder; +import org.embulk.config.ConfigException; +import org.embulk.config.TaskReport; +import org.embulk.input.zendesk.RecordImporter; +import org.embulk.input.zendesk.ZendeskInputPlugin; +import org.embulk.input.zendesk.clients.ZendeskRestClient; +import org.embulk.input.zendesk.models.ZendeskException; +import org.embulk.input.zendesk.utils.ZendeskConstants; +import org.embulk.input.zendesk.utils.ZendeskDateUtils; +import org.embulk.input.zendesk.utils.ZendeskUtils; +import org.embulk.spi.DataException; +import org.embulk.spi.Exec; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.URISyntaxException; +import java.util.Iterator; + +import static org.embulk.input.zendesk.ZendeskInputPlugin.CONFIG_MAPPER_FACTORY; + +public class ZendeskCursorBasedService + implements ZendeskService +{ + private static final Logger logger = LoggerFactory.getLogger(ZendeskNormalServices.class); + + protected ZendeskInputPlugin.PluginTask task; + + private ZendeskRestClient zendeskRestClient; + + public ZendeskCursorBasedService(final ZendeskInputPlugin.PluginTask task) + { + this.task = task; + } + + @Override + public boolean isSupportIncremental() + { + return true; + } + + @Override + public TaskReport addRecordToImporter(int taskIndex, RecordImporter recordImporter) + { + TaskReport taskReport = CONFIG_MAPPER_FACTORY.newTaskReport(); + importData(task, recordImporter, taskReport); + + return taskReport; + } + + @Override + public JsonNode getDataFromPath(String path, int page, boolean isPreview, long startTime) + { + try { + String buildPath = buildPath(0); + final String response = getZendeskRestClient().doGet(buildPath, task, Exec.isPreview()); + return ZendeskUtils.parseJsonObject(response); + } + catch (URISyntaxException e) { + throw new ConfigException(e); + } + } + + @VisibleForTesting + protected ZendeskRestClient getZendeskRestClient() + { + if (zendeskRestClient == null) { + zendeskRestClient = new ZendeskRestClient(); + } + return zendeskRestClient; + } + + private void importData(final ZendeskInputPlugin.PluginTask task, final RecordImporter recordImporter, final TaskReport taskReport) + { + long initStartTime = 0; + + if (task.getStartTime().isPresent()) { + initStartTime = ZendeskDateUtils.getStartTime(task.getStartTime().get()); + } + + long nextStartTime = initStartTime; + long totalRecords = 0; + try { + String path = buildPath(initStartTime); + + while (true) { + final JsonNode result = fetchResultFromPath(path); + + final Iterator iterator = ZendeskUtils.getListRecords(result, task.getTarget().getJsonName()); + + int numberOfRecords = 0; + + while (iterator.hasNext()) { + final JsonNode recordJsonNode = iterator.next(); + fetchSubResourceAndAddToImporter(recordJsonNode, task, recordImporter); + numberOfRecords++; + // Store nextStartTime of last item + if (!iterator.hasNext() && task.getIncremental()) { + nextStartTime = ZendeskDateUtils.isoToEpochSecond(recordJsonNode.get(ZendeskConstants.Field.UPDATED_AT).asText()); + } + } + + totalRecords = totalRecords + numberOfRecords; + if (result.has(ZendeskConstants.Field.END_OF_STREAM)) { + if (result.get(ZendeskConstants.Field.END_OF_STREAM).asBoolean()) { + break; + } + } + else { + throw new DataException("Missing end of stream, please double-check the endpoint"); + } + if (Exec.isPreview()) { + break; + } + + path = result.get(ZendeskConstants.Field.AFTER_URL).asText(); + } + + logger.info("import records total " + totalRecords); + + if (!Exec.isPreview() && task.getIncremental()) { + storeStartTimeForConfigDiff(taskReport, nextStartTime); + } + } + catch (Exception e) { + throw new DataException(e); + } + } + + private String buildPath(long startTime) + throws URISyntaxException + { + return ZendeskUtils.getURIBuilder(task.getLoginUrl()).setPath(ZendeskConstants.Url.API + "/" + "incremental" + "/" + task.getTarget().toString() + "/" + "cursor.json").build().toString() + "?start_time=" + startTime; + } + + private JsonNode fetchResultFromPath(String path) + { + final String response = getZendeskRestClient().doGet(path, task, Exec.isPreview()); + return ZendeskUtils.parseJsonObject(response); + } + + private void fetchSubResourceAndAddToImporter(final JsonNode jsonNode, final ZendeskInputPlugin.PluginTask task, final RecordImporter recordImporter) + { + task.getIncludes().forEach(include -> { + final String relatedObjectName = include.trim(); + + final URIBuilder uriBuilder = ZendeskUtils.getURIBuilder(task.getLoginUrl()).setPath(ZendeskConstants.Url.API + "/" + task.getTarget().toString() + "/" + jsonNode.get(ZendeskConstants.Field.ID).asText() + "/" + relatedObjectName + ".json"); + try { + final JsonNode result = getDataFromPath(uriBuilder.toString(), 0, false, 0); + if (result != null && result.has(relatedObjectName)) { + ((ObjectNode) jsonNode).set(include, result.get(relatedObjectName)); + } + } + catch (final ConfigException e) { + // Sometimes we get 404 when having invalid endpoint, so ignore when we get 404 InvalidEndpoint + if (!(e.getCause() instanceof ZendeskException && ((ZendeskException) e.getCause()).getStatusCode() == HttpStatus.SC_NOT_FOUND)) { + throw e; + } + } + }); + + recordImporter.addRecord(jsonNode); + } + + private void storeStartTimeForConfigDiff(final TaskReport taskReport, final long nextStartTime) + { + taskReport.set(ZendeskConstants.Field.START_TIME, nextStartTime); + } +} diff --git a/src/main/java/org/embulk/input/zendesk/utils/ZendeskConstants.java b/src/main/java/org/embulk/input/zendesk/utils/ZendeskConstants.java index f63cc00..895491b 100644 --- a/src/main/java/org/embulk/input/zendesk/utils/ZendeskConstants.java +++ b/src/main/java/org/embulk/input/zendesk/utils/ZendeskConstants.java @@ -28,6 +28,8 @@ public static class Field public static final String GENERATED_TIMESTAMP = "generated_timestamp"; public static final String UPDATED_AT = "updated_at"; public static final String ID = "id"; + public static final String END_OF_STREAM = "end_of_stream"; + public static final String AFTER_URL = "after_url"; } public static class Url diff --git a/src/main/java/org/embulk/input/zendesk/utils/ZendeskDateUtils.java b/src/main/java/org/embulk/input/zendesk/utils/ZendeskDateUtils.java index 5bb96c2..0ecfd1e 100644 --- a/src/main/java/org/embulk/input/zendesk/utils/ZendeskDateUtils.java +++ b/src/main/java/org/embulk/input/zendesk/utils/ZendeskDateUtils.java @@ -56,7 +56,7 @@ public static String convertToDateTimeFormat(String datetime, String dateTimeFor } // start_time should be start from 0 - public static long getStartTime(final String time) + public static long getStartTime(final String time) { try { return isoToEpochSecond(time); diff --git a/src/test/java/org/embulk/input/zendesk/TestZendeskInputPlugin.java b/src/test/java/org/embulk/input/zendesk/TestZendeskInputPlugin.java index 666f9b0..49f95b9 100644 --- a/src/test/java/org/embulk/input/zendesk/TestZendeskInputPlugin.java +++ b/src/test/java/org/embulk/input/zendesk/TestZendeskInputPlugin.java @@ -8,6 +8,7 @@ import org.embulk.config.TaskSource; import org.embulk.input.zendesk.models.Target; import org.embulk.input.zendesk.services.ZendeskChatService; +import org.embulk.input.zendesk.services.ZendeskCursorBasedService; import org.embulk.input.zendesk.services.ZendeskCustomObjectService; import org.embulk.input.zendesk.services.ZendeskNPSService; import org.embulk.input.zendesk.services.ZendeskService; @@ -205,6 +206,25 @@ public void testDispatchPerTargetShouldReturnSupportAPIService() testReturnSupportAPIService(Target.ORGANIZATIONS); } + @Test + public void testDispatchPerTargetShouldReturn() + { + zendeskInputPlugin = spy(new ZendeskInputPlugin()); + + final ConfigSource src = ZendeskTestHelper.getConfigSource("base.yml"); + src.set("target", Target.TICKETS.name().toLowerCase()); + src.set("columns", Collections.EMPTY_LIST); + src.set("enable_cursor_based_api", true); + ZendeskInputPlugin.PluginTask task = CONFIG_MAPPER.map(src, ZendeskInputPlugin.PluginTask.class); + ZendeskService zendeskService = zendeskInputPlugin.dispatchPerTarget(task); + assertTrue(zendeskService instanceof ZendeskCursorBasedService); + + src.set("target", Target.USERS.name().toLowerCase()); + task = CONFIG_MAPPER.map(src, ZendeskInputPlugin.PluginTask.class); + zendeskService = zendeskInputPlugin.dispatchPerTarget(task); + assertTrue(zendeskService instanceof ZendeskCursorBasedService); + } + @Test public void testDispatchPerTargetShouldReturnNPSService() { diff --git a/src/test/java/org/embulk/input/zendesk/services/TestZendeskCursorBasedService.java b/src/test/java/org/embulk/input/zendesk/services/TestZendeskCursorBasedService.java new file mode 100644 index 0000000..5e86e19 --- /dev/null +++ b/src/test/java/org/embulk/input/zendesk/services/TestZendeskCursorBasedService.java @@ -0,0 +1,108 @@ +package org.embulk.input.zendesk.services; + +import com.fasterxml.jackson.databind.JsonNode; +import org.embulk.EmbulkTestRuntime; + +import org.embulk.config.TaskReport; +import org.embulk.input.zendesk.RecordImporter; +import org.embulk.input.zendesk.ZendeskInputPlugin; +import org.embulk.input.zendesk.clients.ZendeskRestClient; +import org.embulk.input.zendesk.utils.ZendeskConstants; +import org.embulk.input.zendesk.utils.ZendeskTestHelper; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import static org.embulk.input.zendesk.ZendeskInputPlugin.CONFIG_MAPPER; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class TestZendeskCursorBasedService +{ + @Rule + public EmbulkTestRuntime runtime = new EmbulkTestRuntime(); + + private ZendeskRestClient zendeskRestClient; + + private ZendeskCursorBasedService zendeskCursorBasedService; + + private RecordImporter recordImporter; + + @Before + public void prepare() + { + zendeskRestClient = mock(ZendeskRestClient.class); + recordImporter = mock(RecordImporter.class); + } + + @Test + public void testRunNonIncremental() + { + setup("incremental.yml"); + loadData("data/cursor_based_tickets.json"); + + String expectedString = "https://abc.zendesk.com/api/v2/incremental/tickets/cursor.json?start_time=1547275910"; + + zendeskCursorBasedService.addRecordToImporter(0, recordImporter); + final ArgumentCaptor actualString = ArgumentCaptor.forClass(String.class); + verify(zendeskRestClient, times(1)).doGet(actualString.capture(), any(), anyBoolean()); + assertTrue(actualString.getAllValues().contains(expectedString)); + + verify(recordImporter, times(1)).addRecord(any()); + } + + @Test + public void testRunIncremental() + { + setup("incremental.yml"); + loadData("data/cursor_based_tickets_incremental.json", "data/cursor_based_tickets.json"); + + String expectedString = "https://abc.zendesk.com/api/v2/incremental/tickets/cursor.json?start_time=1547275910"; + String expectedNextString = "https://treasuredata.zendesk.com/api/v2/incremental/tickets/cursor.json?cursor=xxxx"; + + TaskReport taskReport = zendeskCursorBasedService.addRecordToImporter(0, recordImporter); + final ArgumentCaptor actualString = ArgumentCaptor.forClass(String.class); + verify(zendeskRestClient, times(2)).doGet(actualString.capture(), any(), anyBoolean()); + assertTrue(actualString.getAllValues().get(0).contains(expectedString)); + assertTrue(actualString.getAllValues().get(1).contains(expectedNextString)); + + verify(recordImporter, times(2)).addRecord(any()); + assertTrue(taskReport.get(String.class, ZendeskConstants.Field.START_TIME).equals("1437638600")); + } + + private void loadData(String fileName) + { + JsonNode dataJson = ZendeskTestHelper.getJsonFromFile(fileName); + when(zendeskRestClient.doGet(any(), any(), anyBoolean())).thenReturn(dataJson.toString()); + } + + private void loadData(String fileName, String nextFile) + { + JsonNode dataJson = ZendeskTestHelper.getJsonFromFile(fileName); + JsonNode dataJsonNextFile = ZendeskTestHelper.getJsonFromFile(nextFile); + + when(zendeskRestClient.doGet(any(), any(), anyBoolean())) + .thenReturn(dataJson.toString()) + .thenReturn(dataJsonNextFile.toString()); + } + + private void setupZendeskSupportAPIService(ZendeskInputPlugin.PluginTask task) + { + zendeskCursorBasedService = spy(new ZendeskCursorBasedService(task)); + when(zendeskCursorBasedService.getZendeskRestClient()).thenReturn(zendeskRestClient); + } + + private void setup(String file) + { + ZendeskInputPlugin.PluginTask task = + CONFIG_MAPPER.map(ZendeskTestHelper.getConfigSource(file), ZendeskInputPlugin.PluginTask.class); + setupZendeskSupportAPIService(task); + } +} diff --git a/src/test/resources/data/cursor_based_tickets.json b/src/test/resources/data/cursor_based_tickets.json new file mode 100644 index 0000000..aad49f9 --- /dev/null +++ b/src/test/resources/data/cursor_based_tickets.json @@ -0,0 +1,408 @@ +{ + "tickets": [ + { + "url": "https://treasuredata.zendesk.com/api/v2/tickets/10046.json", + "id": 10046, + "external_id": null, + "via": { + "channel": "api", + "source": { + "from": {}, + "to": {}, + "rel": null + } + }, + "created_at": "2015-07-17T09:44:45Z", + "updated_at": "2015-07-23T08:03:20Z", + "type": null, + "subject": "YBI Incident Report (20150717)", + "raw_subject": "YBI Incident Report (20150717)", + "description": "YBI ご担当者様\r\n\r\nYBIサービスにおきまして、下記の障害が発生しましたので報告します。\r\n\r\n発生日時: 18:20頃\r\n終了日時: \r\nステータス: 対応中\r\n\r\nIN_CODE: 11\r\n影響範囲: 複数\r\n優先度: 高\r\n障害症状: APIアクセス不良\r\nhttp://ybi-status.idcfcloud.com/incidents/f4972585dzw1\r\n\r\n原因としては、オブスト障害で、IDCF様側で対応中。\r\n\r\n\r\n本件についてのお問い合わせは、Treasure Data サポートデスク(support@treasure-data.com)にお願いします。\r\n\r\n以上。", + "priority": "urgent", + "status": "closed", + "recipient": null, + "requester_id": 1149067997, + "submitter_id": 1098710047, + "assignee_id": 1098710047, + "organization_id": 260110007, + "group_id": 25952037, + "collaborator_ids": [], + "follower_ids": [], + "email_cc_ids": [], + "forum_topic_id": null, + "problem_id": null, + "has_incidents": false, + "is_public": true, + "due_at": null, + "tags": [ + "idcf", + "problem-report" + ], + "custom_fields": [ + { + "id": 80429327, + "value": null + }, + { + "id": 360057418513, + "value": null + }, + { + "id": 10682994795795, + "value": null + }, + { + "id": 18713266567699, + "value": null + }, + { + "id": 10682857506067, + "value": null + }, + { + "id": 18713263822355, + "value": null + }, + { + "id": 18713307078931, + "value": null + }, + { + "id": 10682946518035, + "value": null + }, + { + "id": 10682908596243, + "value": null + }, + { + "id": 81568788, + "value": null + }, + { + "id": 77496087, + "value": null + }, + { + "id": 81568808, + "value": null + }, + { + "id": 1500003342381, + "value": null + }, + { + "id": 360029416254, + "value": null + }, + { + "id": 360047029573, + "value": null + }, + { + "id": 360031529033, + "value": null + }, + { + "id": 77984587, + "value": null + }, + { + "id": 360051649613, + "value": null + }, + { + "id": 360000277327, + "value": null + }, + { + "id": 360000277347, + "value": null + }, + { + "id": 360029761653, + "value": null + }, + { + "id": 360044991094, + "value": null + }, + { + "id": 360026802814, + "value": null + }, + { + "id": 360000448128, + "value": null + }, + { + "id": 360000047248, + "value": null + }, + { + "id": 18713273505171, + "value": null + }, + { + "id": 10682899319443, + "value": null + }, + { + "id": 18713301837715, + "value": null + }, + { + "id": 10682905434515, + "value": null + }, + { + "id": 10682945281427, + "value": null + }, + { + "id": 10577093706899, + "value": null + }, + { + "id": 18713241832595, + "value": null + }, + { + "id": 18713228737939, + "value": null + }, + { + "id": 360043549854, + "value": null + }, + { + "id": 26703268, + "value": false + }, + { + "id": 1500001360042, + "value": null + }, + { + "id": 360047049654, + "value": null + }, + { + "id": 360036700353, + "value": null + }, + { + "id": 32334037, + "value": null + }, + { + "id": 1500011161301, + "value": null + }, + { + "id": 27013087, + "value": null + }, + { + "id": 360039452393, + "value": null + }, + { + "id": 1500001359081, + "value": null + } + ], + "satisfaction_rating": { + "score": "unoffered" + }, + "sharing_agreement_ids": [], + "custom_status_id": 1500007502322, + "fields": [ + { + "id": 80429327, + "value": null + }, + { + "id": 360057418513, + "value": null + }, + { + "id": 10682994795795, + "value": null + }, + { + "id": 18713266567699, + "value": null + }, + { + "id": 10682857506067, + "value": null + }, + { + "id": 18713263822355, + "value": null + }, + { + "id": 18713307078931, + "value": null + }, + { + "id": 10682946518035, + "value": null + }, + { + "id": 10682908596243, + "value": null + }, + { + "id": 81568788, + "value": null + }, + { + "id": 77496087, + "value": null + }, + { + "id": 81568808, + "value": null + }, + { + "id": 1500003342381, + "value": null + }, + { + "id": 360029416254, + "value": null + }, + { + "id": 360047029573, + "value": null + }, + { + "id": 360031529033, + "value": null + }, + { + "id": 77984587, + "value": null + }, + { + "id": 360051649613, + "value": null + }, + { + "id": 360000277327, + "value": null + }, + { + "id": 360000277347, + "value": null + }, + { + "id": 360029761653, + "value": null + }, + { + "id": 360044991094, + "value": null + }, + { + "id": 360026802814, + "value": null + }, + { + "id": 360000448128, + "value": null + }, + { + "id": 360000047248, + "value": null + }, + { + "id": 18713273505171, + "value": null + }, + { + "id": 10682899319443, + "value": null + }, + { + "id": 18713301837715, + "value": null + }, + { + "id": 10682905434515, + "value": null + }, + { + "id": 10682945281427, + "value": null + }, + { + "id": 10577093706899, + "value": null + }, + { + "id": 18713241832595, + "value": null + }, + { + "id": 18713228737939, + "value": null + }, + { + "id": 360043549854, + "value": null + }, + { + "id": 26703268, + "value": false + }, + { + "id": 1500001360042, + "value": null + }, + { + "id": 360047049654, + "value": null + }, + { + "id": 360036700353, + "value": null + }, + { + "id": 32334037, + "value": null + }, + { + "id": 1500011161301, + "value": null + }, + { + "id": 27013087, + "value": null + }, + { + "id": 360039452393, + "value": null + }, + { + "id": 1500001359081, + "value": null + } + ], + "followup_ids": [], + "ticket_form_id": null, + "brand_id": 381217, + "allow_channelback": false, + "allow_attachments": true, + "from_messaging_channel": false, + "generated_timestamp": 1437638600 + } + ], + "after_url": "https://treasuredata.zendesk.com/api/v2/incremental/tickets/cursor.json?cursor=xxxx", + "before_url": null, + "after_cursor": "xxxx", + "before_cursor": null, + "end_of_stream": true +} \ No newline at end of file diff --git a/src/test/resources/data/cursor_based_tickets_incremental.json b/src/test/resources/data/cursor_based_tickets_incremental.json new file mode 100644 index 0000000..794321d --- /dev/null +++ b/src/test/resources/data/cursor_based_tickets_incremental.json @@ -0,0 +1,408 @@ +{ + "tickets": [ + { + "url": "https://treasuredata.zendesk.com/api/v2/tickets/10046.json", + "id": 10046, + "external_id": null, + "via": { + "channel": "api", + "source": { + "from": {}, + "to": {}, + "rel": null + } + }, + "created_at": "2015-07-17T09:44:45Z", + "updated_at": "2015-07-23T08:03:20Z", + "type": null, + "subject": "YBI Incident Report (20150717)", + "raw_subject": "YBI Incident Report (20150717)", + "description": "YBI ご担当者様\r\n\r\nYBIサービスにおきまして、下記の障害が発生しましたので報告します。\r\n\r\n発生日時: 18:20頃\r\n終了日時: \r\nステータス: 対応中\r\n\r\nIN_CODE: 11\r\n影響範囲: 複数\r\n優先度: 高\r\n障害症状: APIアクセス不良\r\nhttp://ybi-status.idcfcloud.com/incidents/f4972585dzw1\r\n\r\n原因としては、オブスト障害で、IDCF様側で対応中。\r\n\r\n\r\n本件についてのお問い合わせは、Treasure Data サポートデスク(support@treasure-data.com)にお願いします。\r\n\r\n以上。", + "priority": "urgent", + "status": "closed", + "recipient": null, + "requester_id": 1149067997, + "submitter_id": 1098710047, + "assignee_id": 1098710047, + "organization_id": 260110007, + "group_id": 25952037, + "collaborator_ids": [], + "follower_ids": [], + "email_cc_ids": [], + "forum_topic_id": null, + "problem_id": null, + "has_incidents": false, + "is_public": true, + "due_at": null, + "tags": [ + "idcf", + "problem-report" + ], + "custom_fields": [ + { + "id": 80429327, + "value": null + }, + { + "id": 360057418513, + "value": null + }, + { + "id": 10682994795795, + "value": null + }, + { + "id": 18713266567699, + "value": null + }, + { + "id": 10682857506067, + "value": null + }, + { + "id": 18713263822355, + "value": null + }, + { + "id": 18713307078931, + "value": null + }, + { + "id": 10682946518035, + "value": null + }, + { + "id": 10682908596243, + "value": null + }, + { + "id": 81568788, + "value": null + }, + { + "id": 77496087, + "value": null + }, + { + "id": 81568808, + "value": null + }, + { + "id": 1500003342381, + "value": null + }, + { + "id": 360029416254, + "value": null + }, + { + "id": 360047029573, + "value": null + }, + { + "id": 360031529033, + "value": null + }, + { + "id": 77984587, + "value": null + }, + { + "id": 360051649613, + "value": null + }, + { + "id": 360000277327, + "value": null + }, + { + "id": 360000277347, + "value": null + }, + { + "id": 360029761653, + "value": null + }, + { + "id": 360044991094, + "value": null + }, + { + "id": 360026802814, + "value": null + }, + { + "id": 360000448128, + "value": null + }, + { + "id": 360000047248, + "value": null + }, + { + "id": 18713273505171, + "value": null + }, + { + "id": 10682899319443, + "value": null + }, + { + "id": 18713301837715, + "value": null + }, + { + "id": 10682905434515, + "value": null + }, + { + "id": 10682945281427, + "value": null + }, + { + "id": 10577093706899, + "value": null + }, + { + "id": 18713241832595, + "value": null + }, + { + "id": 18713228737939, + "value": null + }, + { + "id": 360043549854, + "value": null + }, + { + "id": 26703268, + "value": false + }, + { + "id": 1500001360042, + "value": null + }, + { + "id": 360047049654, + "value": null + }, + { + "id": 360036700353, + "value": null + }, + { + "id": 32334037, + "value": null + }, + { + "id": 1500011161301, + "value": null + }, + { + "id": 27013087, + "value": null + }, + { + "id": 360039452393, + "value": null + }, + { + "id": 1500001359081, + "value": null + } + ], + "satisfaction_rating": { + "score": "unoffered" + }, + "sharing_agreement_ids": [], + "custom_status_id": 1500007502322, + "fields": [ + { + "id": 80429327, + "value": null + }, + { + "id": 360057418513, + "value": null + }, + { + "id": 10682994795795, + "value": null + }, + { + "id": 18713266567699, + "value": null + }, + { + "id": 10682857506067, + "value": null + }, + { + "id": 18713263822355, + "value": null + }, + { + "id": 18713307078931, + "value": null + }, + { + "id": 10682946518035, + "value": null + }, + { + "id": 10682908596243, + "value": null + }, + { + "id": 81568788, + "value": null + }, + { + "id": 77496087, + "value": null + }, + { + "id": 81568808, + "value": null + }, + { + "id": 1500003342381, + "value": null + }, + { + "id": 360029416254, + "value": null + }, + { + "id": 360047029573, + "value": null + }, + { + "id": 360031529033, + "value": null + }, + { + "id": 77984587, + "value": null + }, + { + "id": 360051649613, + "value": null + }, + { + "id": 360000277327, + "value": null + }, + { + "id": 360000277347, + "value": null + }, + { + "id": 360029761653, + "value": null + }, + { + "id": 360044991094, + "value": null + }, + { + "id": 360026802814, + "value": null + }, + { + "id": 360000448128, + "value": null + }, + { + "id": 360000047248, + "value": null + }, + { + "id": 18713273505171, + "value": null + }, + { + "id": 10682899319443, + "value": null + }, + { + "id": 18713301837715, + "value": null + }, + { + "id": 10682905434515, + "value": null + }, + { + "id": 10682945281427, + "value": null + }, + { + "id": 10577093706899, + "value": null + }, + { + "id": 18713241832595, + "value": null + }, + { + "id": 18713228737939, + "value": null + }, + { + "id": 360043549854, + "value": null + }, + { + "id": 26703268, + "value": false + }, + { + "id": 1500001360042, + "value": null + }, + { + "id": 360047049654, + "value": null + }, + { + "id": 360036700353, + "value": null + }, + { + "id": 32334037, + "value": null + }, + { + "id": 1500011161301, + "value": null + }, + { + "id": 27013087, + "value": null + }, + { + "id": 360039452393, + "value": null + }, + { + "id": 1500001359081, + "value": null + } + ], + "followup_ids": [], + "ticket_form_id": null, + "brand_id": 381217, + "allow_channelback": false, + "allow_attachments": true, + "from_messaging_channel": false, + "generated_timestamp": 1437638600 + } + ], + "after_url": "https://treasuredata.zendesk.com/api/v2/incremental/tickets/cursor.json?cursor=xxxx", + "before_url": null, + "after_cursor": "xxxx", + "before_cursor": null, + "end_of_stream": false +} \ No newline at end of file