Source: util/paging-iterator.ts

  1. /**
  2. * @fileoverview Iterator for paged responses
  3. */
  4. import * as qs from 'querystring';
  5. import { Promise } from 'bluebird';
  6. import PromiseQueue = require('promise-queue');
  7. // -----------------------------------------------------------------------------
  8. // Typedefs
  9. // -----------------------------------------------------------------------------
  10. /**
  11. * The iterator response object
  12. * @typedef {Object} IteratorData
  13. * @property {Array} [value] - The next set of values from the iterator
  14. * @property {boolean} done - Whether the iterator is completed
  15. */
  16. /**
  17. * Iterator callback
  18. * @callback IteratorCallback
  19. * @param {?Error} err - An error if the iterator encountered one
  20. * @param {IteratorData} [data] - New data from the iterator
  21. * @returns {void}
  22. */
  23. // -----------------------------------------------------------------------------
  24. // Requirements
  25. // -----------------------------------------------------------------------------
  26. const errors = require('./errors');
  27. PromiseQueue.configure(Promise as any);
  28. // -----------------------------------------------------------------------------
  29. // Private
  30. // -----------------------------------------------------------------------------
  31. const PAGING_MODES = Object.freeze({
  32. MARKER: 'marker',
  33. OFFSET: 'offset',
  34. });
  35. // -----------------------------------------------------------------------------
  36. // Public
  37. // -----------------------------------------------------------------------------
  38. /**
  39. * Asynchronous iterator for paged collections
  40. */
  41. class PagingIterator {
  42. /**
  43. * Determine if a response is iterable
  44. * @param {Object} response - The API response
  45. * @returns {boolean} Whether the response is iterable
  46. */
  47. static isIterable(response: any /* FIXME */) {
  48. // POST responses for uploading a file are explicitly excluded here because, while the response is iterable,
  49. // it always contains only a single entry and historically has not been handled as iterable in the SDK.
  50. // This behavior is being preserved here to avoid a breaking change.
  51. let UPLOAD_PATTERN = /.*upload\.box\.com.*\/content/;
  52. var isGetOrPostRequest =
  53. response.request &&
  54. (response.request.method === 'GET' ||
  55. (response.request.method === 'POST' &&
  56. !UPLOAD_PATTERN.test(response.request.uri.href))),
  57. hasEntries = response.body && Array.isArray(response.body.entries),
  58. notEventStream = response.body && !response.body.next_stream_position;
  59. return Boolean(isGetOrPostRequest && hasEntries && notEventStream);
  60. }
  61. nextField: any /* FIXME */;
  62. nextValue: any /* FIXME */;
  63. limit: any /* FIXME */;
  64. done: boolean;
  65. options: Record<string, any> /* FIMXE */;
  66. fetch: any /* FIXME */;
  67. buffer: any /* FIXME */;
  68. queue: any /* FIXME */;
  69. /**
  70. * @constructor
  71. * @param {Object} response - The original API response
  72. * @param {BoxClient} client - An API client to make further requests
  73. * @returns {void}
  74. * @throws {Error} Will throw when collection cannot be paged
  75. */
  76. constructor(response: any /* FIXME */, client: any /* FIXME */) {
  77. if (!PagingIterator.isIterable(response)) {
  78. throw new Error('Cannot create paging iterator for non-paged response!');
  79. }
  80. var data = response.body;
  81. if (Number.isSafeInteger(data.offset)) {
  82. this.nextField = PAGING_MODES.OFFSET;
  83. this.nextValue = data.offset;
  84. } else if (typeof data.next_marker === 'undefined') {
  85. // Default to a finished marker collection when there's no field present,
  86. // since some endpoints indicate completed paging this way
  87. this.nextField = PAGING_MODES.MARKER;
  88. this.nextValue = null;
  89. } else {
  90. this.nextField = PAGING_MODES.MARKER;
  91. this.nextValue = data.next_marker;
  92. }
  93. this.limit = data.limit || data.entries.length;
  94. this.done = false;
  95. var href = response.request.href.split('?')[0];
  96. this.options = {
  97. headers: response.request.headers,
  98. qs: qs.parse(response.request.uri.query),
  99. };
  100. if (response.request.body) {
  101. if (
  102. Object.prototype.toString.call(response.request.body) ===
  103. '[object Object]'
  104. ) {
  105. this.options.body = response.request.body;
  106. } else {
  107. this.options.body = JSON.parse(response.request.body);
  108. }
  109. }
  110. // querystring.parse() makes everything a string, ensure numeric params are the correct type
  111. if (this.options.qs.limit) {
  112. this.options.qs.limit = parseInt(this.options.qs.limit, 10);
  113. }
  114. if (this.options.qs.offset) {
  115. this.options.qs.offset = parseInt(this.options.qs.offset, 10);
  116. }
  117. delete this.options.headers.Authorization;
  118. if (response.request.method === 'GET') {
  119. this.fetch = client.get.bind(client, href);
  120. }
  121. if (response.request.method === 'POST') {
  122. this.fetch = client.post.bind(client, href);
  123. }
  124. this.buffer = response.body.entries;
  125. this.queue = new PromiseQueue(1, Infinity);
  126. this._updatePaging(response);
  127. }
  128. /**
  129. * Update the paging parameters for the iterator
  130. * @private
  131. * @param {Object} response - The latest API response
  132. * @returns {void}
  133. */
  134. _updatePaging(response: any /* FIXME */) {
  135. var data = response.body;
  136. if (this.nextField === PAGING_MODES.OFFSET) {
  137. this.nextValue += this.limit;
  138. if (Number.isSafeInteger(data.total_count)) {
  139. this.done = data.offset + this.limit >= data.total_count;
  140. } else {
  141. this.done = data.entries.length === 0;
  142. }
  143. } else if (this.nextField === PAGING_MODES.MARKER) {
  144. if (data.next_marker) {
  145. this.nextValue = data.next_marker;
  146. } else {
  147. this.nextValue = null;
  148. this.done = true;
  149. }
  150. }
  151. if (response.request.method === 'GET') {
  152. this.options.qs[this.nextField] = this.nextValue;
  153. } else if (response.request.method === 'POST') {
  154. if (!this.options.body) {
  155. this.options.body = {};
  156. }
  157. this.options.body[this.nextField] = this.nextValue;
  158. let bodyString = JSON.stringify(this.options.body);
  159. this.options.headers['content-length'] = bodyString.length;
  160. }
  161. }
  162. /**
  163. * Fetch the next page of results
  164. * @returns {Promise} Promise resolving to iterator state
  165. */
  166. _getData() {
  167. return this.fetch(this.options).then((response: any /* FIXME */) => {
  168. if (response.statusCode !== 200) {
  169. throw errors.buildUnexpectedResponseError(response);
  170. }
  171. this._updatePaging(response);
  172. this.buffer = this.buffer.concat(response.body.entries);
  173. if (this.buffer.length === 0) {
  174. if (this.done) {
  175. return {
  176. value: undefined,
  177. done: true,
  178. };
  179. }
  180. // If we didn't get any data in this page, but the paging
  181. // parameters indicate that there is more data, attempt
  182. // to fetch more. This occurs in multiple places in the API
  183. return this._getData();
  184. }
  185. return {
  186. value: this.buffer.shift(),
  187. done: false,
  188. };
  189. });
  190. }
  191. /**
  192. * Fetch the next page of the collection
  193. * @returns {Promise} Promise resolving to iterator state
  194. */
  195. next() {
  196. if (this.buffer.length > 0) {
  197. return Promise.resolve({
  198. value: this.buffer.shift(),
  199. done: false,
  200. });
  201. }
  202. if (this.done) {
  203. return Promise.resolve({
  204. value: undefined,
  205. done: true,
  206. });
  207. }
  208. return this.queue.add(this._getData.bind(this));
  209. }
  210. /**
  211. * Fetch the next marker
  212. * @returns {string|int} String that is the next marker or int that is the next offset
  213. */
  214. getNextMarker() {
  215. return this.nextValue;
  216. }
  217. }
  218. export = PagingIterator;