1 | 'use strict';
|
2 |
|
3 | const makeRequest = require('./makeRequest');
|
4 | const utils = require('./utils');
|
5 |
|
6 | function makeAutoPaginationMethods(self, requestArgs, spec, firstPagePromise) {
|
7 | const promiseCache = {currentPromise: null};
|
8 | let listPromise = firstPagePromise;
|
9 | let i = 0;
|
10 |
|
11 | function iterate(listResult) {
|
12 | if (
|
13 | !(
|
14 | listResult &&
|
15 | listResult.data &&
|
16 | typeof listResult.data.length === 'number'
|
17 | )
|
18 | ) {
|
19 | throw Error(
|
20 | 'Unexpected: Stripe API response does not have a well-formed `data` array.'
|
21 | );
|
22 | }
|
23 |
|
24 | if (i < listResult.data.length) {
|
25 | const value = listResult.data[i];
|
26 | i += 1;
|
27 | return {value, done: false};
|
28 | } else if (listResult.has_more) {
|
29 |
|
30 | i = 0;
|
31 | const lastId = getLastId(listResult);
|
32 | listPromise = makeRequest(self, requestArgs, spec, {
|
33 | starting_after: lastId,
|
34 | });
|
35 | return listPromise.then(iterate);
|
36 | }
|
37 | return {value: undefined, done: true};
|
38 | }
|
39 |
|
40 | function asyncIteratorNext() {
|
41 | return memoizedPromise(promiseCache, (resolve, reject) => {
|
42 | return listPromise
|
43 | .then(iterate)
|
44 | .then(resolve)
|
45 | .catch(reject);
|
46 | });
|
47 | }
|
48 |
|
49 | const autoPagingEach = makeAutoPagingEach(asyncIteratorNext);
|
50 | const autoPagingToArray = makeAutoPagingToArray(autoPagingEach);
|
51 |
|
52 | const autoPaginationMethods = {
|
53 | autoPagingEach,
|
54 | autoPagingToArray,
|
55 |
|
56 |
|
57 | next: asyncIteratorNext,
|
58 | return: () => {
|
59 |
|
60 | return {};
|
61 | },
|
62 | [getAsyncIteratorSymbol()]: () => {
|
63 | return autoPaginationMethods;
|
64 | },
|
65 | };
|
66 | return autoPaginationMethods;
|
67 | }
|
68 |
|
69 | module.exports.makeAutoPaginationMethods = makeAutoPaginationMethods;
|
70 |
|
71 |
|
72 |
|
73 |
|
74 |
|
75 |
|
76 |
|
77 | function getAsyncIteratorSymbol() {
|
78 | if (typeof Symbol !== 'undefined' && Symbol.asyncIterator) {
|
79 | return Symbol.asyncIterator;
|
80 | }
|
81 |
|
82 | return '@@asyncIterator';
|
83 | }
|
84 |
|
85 | function getDoneCallback(args) {
|
86 | if (args.length < 2) {
|
87 | return undefined;
|
88 | }
|
89 | const onDone = args[1];
|
90 | if (typeof onDone !== 'function') {
|
91 | throw Error(
|
92 | `The second argument to autoPagingEach, if present, must be a callback function; receieved ${typeof onDone}`
|
93 | );
|
94 | }
|
95 | return onDone;
|
96 | }
|
97 |
|
98 |
|
99 |
|
100 |
|
101 |
|
102 |
|
103 |
|
104 |
|
105 |
|
106 |
|
107 |
|
108 |
|
109 | function getItemCallback(args) {
|
110 | if (args.length === 0) {
|
111 | return undefined;
|
112 | }
|
113 | const onItem = args[0];
|
114 | if (typeof onItem !== 'function') {
|
115 | throw Error(
|
116 | `The first argument to autoPagingEach, if present, must be a callback function; receieved ${typeof onItem}`
|
117 | );
|
118 | }
|
119 |
|
120 |
|
121 | if (onItem.length === 2) {
|
122 | return onItem;
|
123 | }
|
124 |
|
125 | if (onItem.length > 2) {
|
126 | throw Error(
|
127 | `The \`onItem\` callback function passed to autoPagingEach must accept at most two arguments; got ${onItem}`
|
128 | );
|
129 | }
|
130 |
|
131 |
|
132 |
|
133 |
|
134 |
|
135 | return function _onItem(item, next) {
|
136 | const shouldContinue = onItem(item);
|
137 | next(shouldContinue);
|
138 | };
|
139 | }
|
140 |
|
141 | function getLastId(listResult) {
|
142 | const lastIdx = listResult.data.length - 1;
|
143 | const lastItem = listResult.data[lastIdx];
|
144 | const lastId = lastItem && lastItem.id;
|
145 | if (!lastId) {
|
146 | throw Error(
|
147 | 'Unexpected: No `id` found on the last item while auto-paging a list.'
|
148 | );
|
149 | }
|
150 | return lastId;
|
151 | }
|
152 |
|
153 |
|
154 |
|
155 |
|
156 |
|
157 |
|
158 | function memoizedPromise(promiseCache, cb) {
|
159 | if (promiseCache.currentPromise) {
|
160 | return promiseCache.currentPromise;
|
161 | }
|
162 | promiseCache.currentPromise = new Promise(cb).then((ret) => {
|
163 | promiseCache.currentPromise = undefined;
|
164 | return ret;
|
165 | });
|
166 | return promiseCache.currentPromise;
|
167 | }
|
168 |
|
169 | function makeAutoPagingEach(asyncIteratorNext) {
|
170 | return function autoPagingEach(/* onItem?, onDone? */) {
|
171 | const args = [].slice.call(arguments);
|
172 | const onItem = getItemCallback(args);
|
173 | const onDone = getDoneCallback(args);
|
174 | if (args.length > 2) {
|
175 | throw Error('autoPagingEach takes up to two arguments; received:', args);
|
176 | }
|
177 |
|
178 | const autoPagePromise = wrapAsyncIteratorWithCallback(
|
179 | asyncIteratorNext,
|
180 | onItem
|
181 | );
|
182 | return utils.callbackifyPromiseWithTimeout(autoPagePromise, onDone);
|
183 | };
|
184 | }
|
185 |
|
186 | function makeAutoPagingToArray(autoPagingEach) {
|
187 | return function autoPagingToArray(opts, onDone) {
|
188 | const limit = opts && opts.limit;
|
189 | if (!limit) {
|
190 | throw Error(
|
191 | 'You must pass a `limit` option to autoPagingToArray, e.g., `autoPagingToArray({limit: 1000});`.'
|
192 | );
|
193 | }
|
194 | if (limit > 10000) {
|
195 | throw Error(
|
196 | 'You cannot specify a limit of more than 10,000 items to fetch in `autoPagingToArray`; use `autoPagingEach` to iterate through longer lists.'
|
197 | );
|
198 | }
|
199 | const promise = new Promise((resolve, reject) => {
|
200 | const items = [];
|
201 | autoPagingEach((item) => {
|
202 | items.push(item);
|
203 | if (items.length >= limit) {
|
204 | return false;
|
205 | }
|
206 | })
|
207 | .then(() => {
|
208 | resolve(items);
|
209 | })
|
210 | .catch(reject);
|
211 | });
|
212 | return utils.callbackifyPromiseWithTimeout(promise, onDone);
|
213 | };
|
214 | }
|
215 |
|
216 | function wrapAsyncIteratorWithCallback(asyncIteratorNext, onItem) {
|
217 | return new Promise((resolve, reject) => {
|
218 | function handleIteration(iterResult) {
|
219 | if (iterResult.done) {
|
220 | resolve();
|
221 | return;
|
222 | }
|
223 |
|
224 | const item = iterResult.value;
|
225 | return new Promise((next) => {
|
226 |
|
227 |
|
228 |
|
229 | onItem(item, next);
|
230 | }).then((shouldContinue) => {
|
231 | if (shouldContinue === false) {
|
232 | return handleIteration({done: true});
|
233 | } else {
|
234 | return asyncIteratorNext().then(handleIteration);
|
235 | }
|
236 | });
|
237 | }
|
238 |
|
239 | asyncIteratorNext()
|
240 | .then(handleIteration)
|
241 | .catch(reject);
|
242 | });
|
243 | }
|