UNPKG

stripe

Version:
298 lines 11.6 kB
import { getAPIMode } from './utils.js'; class V1Iterator { constructor(firstPagePromise, params, options, method, path, spec, stripeResource) { this.index = 0; this.pagePromise = firstPagePromise; this.promiseCache = { currentPromise: null }; this.params = params; this.options = options; this.method = method; this.path = path; this.spec = spec; this.stripeResource = stripeResource; } async iterate(pageResult) { if (!(pageResult && pageResult.data && typeof pageResult.data.length === 'number')) { throw Error('Unexpected: Stripe API response does not have a well-formed `data` array.'); } const reverseIteration = !!this.params.ending_before; if (this.index < pageResult.data.length) { const idx = reverseIteration ? pageResult.data.length - 1 - this.index : this.index; const value = pageResult.data[idx]; this.index += 1; return { value, done: false }; } else if (pageResult.has_more) { // Reset counter, request next page, and recurse. this.index = 0; this.pagePromise = this.getNextPage(pageResult); const nextPageResult = await this.pagePromise; return this.iterate(nextPageResult); } return { done: true, value: undefined }; } /** @abstract */ getNextPage(_pageResult) { throw new Error('Unimplemented'); } async _next() { return this.iterate(await this.pagePromise); } next() { /** * If a user calls `.next()` multiple times in parallel, * return the same result until something has resolved * to prevent page-turning race conditions. */ if (this.promiseCache.currentPromise) { return this.promiseCache.currentPromise; } const nextPromise = (async () => { const ret = await this._next(); this.promiseCache.currentPromise = null; return ret; })(); this.promiseCache.currentPromise = nextPromise; return nextPromise; } } class V1ListIterator extends V1Iterator { getNextPage(pageResult) { const reverseIteration = !!this.params.ending_before; const lastId = getLastId(pageResult, reverseIteration); const nextParams = { ...this.params, [reverseIteration ? 'ending_before' : 'starting_after']: lastId, }; return this.stripeResource._makeRequest(this.method, this.path, nextParams, this.options, this.spec); } } class V1SearchIterator extends V1Iterator { getNextPage(pageResult) { if (!pageResult.next_page) { throw Error('Unexpected: Stripe API response does not have a well-formed `next_page` field, but `has_more` was true.'); } const nextParams = { ...this.params, page: pageResult.next_page, }; return this.stripeResource._makeRequest(this.method, this.path, nextParams, this.options, this.spec); } } class V2ListIterator { constructor(firstPagePromise, options, spec, stripeResource) { this.firstPagePromise = firstPagePromise; this.currentPageIterator = null; this.nextPageUrl = null; this.options = options; this.spec = spec; this.stripeResource = stripeResource; } async initFirstPage() { if (this.firstPagePromise) { const page = await this.firstPagePromise; this.firstPagePromise = null; this.currentPageIterator = page.data[Symbol.iterator](); this.nextPageUrl = page.next_page_url || null; } } async turnPage() { if (!this.nextPageUrl) return null; const page = await this.stripeResource._makeRequest('GET', this.nextPageUrl, undefined, this.options, this.spec); this.nextPageUrl = page.next_page_url || null; this.currentPageIterator = page.data[Symbol.iterator](); return this.currentPageIterator; } async next() { await this.initFirstPage(); if (this.currentPageIterator) { const result = this.currentPageIterator.next(); if (!result.done) return { done: false, value: result.value }; } const nextPageIterator = await this.turnPage(); if (!nextPageIterator) { return { done: true, value: undefined }; } const result = nextPageIterator.next(); if (!result.done) return { done: false, value: result.value }; return { done: true, value: undefined }; } } export const makeAutoPaginationMethods = (stripeResource, params, options, method, path, spec, firstPagePromise) => { const apiMode = getAPIMode(path); const methodType = spec?.methodType; if (apiMode !== 'v2' && methodType === 'search') { return makeAutoPaginationMethodsFromIterator(new V1SearchIterator(firstPagePromise, params, options, method, path, spec, stripeResource)); } if (apiMode !== 'v2' && methodType === 'list') { return makeAutoPaginationMethodsFromIterator(new V1ListIterator(firstPagePromise, params, options, method, path, spec, stripeResource)); } if (apiMode === 'v2' && methodType === 'list') { return makeAutoPaginationMethodsFromIterator(new V2ListIterator(firstPagePromise, options, spec, stripeResource)); } return null; }; const makeAutoPaginationMethodsFromIterator = (iterator) => { const autoPagingEach = makeAutoPagingEach((...args) => iterator.next(...args)); const autoPagingToArray = makeAutoPagingToArray(autoPagingEach); const autoPaginationMethods = { autoPagingEach, autoPagingToArray, // Async iterator functions: next: () => iterator.next(), return: () => { // This is required for `break`. return {}; }, [getAsyncIteratorSymbol()]: () => { return autoPaginationMethods; }, }; return autoPaginationMethods; }; /** * ---------------- * Private Helpers: * ---------------- */ function getAsyncIteratorSymbol() { if (typeof Symbol !== 'undefined' && Symbol.asyncIterator) { return Symbol.asyncIterator; } // Follow the convention from libraries like iterall: https://github.com/leebyron/iterall#asynciterator-1 return '@@asyncIterator'; } function getDoneCallback(args) { if (args.length < 2) { return null; } const onDone = args[1]; if (typeof onDone !== 'function') { throw Error(`The second argument to autoPagingEach, if present, must be a callback function; received ${typeof onDone}`); } return onDone; } /** * We allow four forms of the `onItem` callback (the middle two being equivalent), * * 1. `.autoPagingEach((item) => { doSomething(item); return false; });` * 2. `.autoPagingEach(async (item) => { await doSomething(item); return false; });` * 3. `.autoPagingEach((item) => doSomething(item).then(() => false));` * 4. `.autoPagingEach((item, next) => { doSomething(item); next(false); });` * * In addition to standard validation, this helper * coalesces the former forms into the latter form. */ function getItemCallback(args) { if (args.length === 0) { return undefined; } const onItem = args[0]; if (typeof onItem !== 'function') { throw Error(`The first argument to autoPagingEach, if present, must be a callback function; received ${typeof onItem}`); } // 4. `.autoPagingEach((item, next) => { doSomething(item); next(false); });` if (onItem.length === 2) { return onItem; } if (onItem.length > 2) { throw Error(`The \`onItem\` callback function passed to autoPagingEach must accept at most two arguments; got ${onItem}`); } // This magically handles all three of these usecases (the latter two being functionally identical): // 1. `.autoPagingEach((item) => { doSomething(item); return false; });` // 2. `.autoPagingEach(async (item) => { await doSomething(item); return false; });` // 3. `.autoPagingEach((item) => doSomething(item).then(() => false));` return function _onItem(item, next) { const shouldContinue = onItem(item); next(shouldContinue); }; } function getLastId(listResult, reverseIteration) { const lastIdx = reverseIteration ? 0 : listResult.data.length - 1; const lastItem = listResult.data[lastIdx]; const lastId = lastItem && lastItem.id; if (!lastId) { throw Error('Unexpected: No `id` found on the last item while auto-paging a list.'); } return lastId; } function makeAutoPagingEach(asyncIteratorNext) { return function autoPagingEach( /* onItem?, onDone? */) { const args = [].slice.call(arguments); const onItem = getItemCallback(args); const onDone = getDoneCallback(args); if (args.length > 2) { throw Error(`autoPagingEach takes up to two arguments; received ${args}`); } const autoPagePromise = wrapAsyncIteratorWithCallback(asyncIteratorNext, // @ts-ignore we might need a null check onItem); if (onDone) { autoPagePromise.then(() => onDone(), (err) => onDone(err)); } return autoPagePromise; }; } function makeAutoPagingToArray(autoPagingEach) { return function autoPagingToArray(opts, onDone) { const limit = opts && opts.limit; if (!limit) { throw Error('You must pass a `limit` option to autoPagingToArray, e.g., `autoPagingToArray({limit: 1000});`.'); } if (limit > 10000) { throw Error('You cannot specify a limit of more than 10,000 items to fetch in `autoPagingToArray`; use `autoPagingEach` to iterate through longer lists.'); } const promise = new Promise((resolve, reject) => { const items = []; autoPagingEach((item) => { items.push(item); if (items.length >= limit) { return false; } }) .then(() => { resolve(items); }) .catch(reject); }); if (onDone) { promise.then((items) => onDone(null, items), (err) => onDone(err)); } return promise; }; } function wrapAsyncIteratorWithCallback(asyncIteratorNext, onItem) { return new Promise((resolve, reject) => { function handleIteration(iterResult) { if (iterResult.done) { resolve(); return; } const item = iterResult.value; return new Promise((next) => { // Bit confusing, perhaps; we pass a `resolve` fn // to the user, so they can decide when and if to continue. // They can return false, or a promise which resolves to false, to break. onItem(item, next); }).then((shouldContinue) => { if (shouldContinue === false) { return handleIteration({ done: true, value: undefined }); } else { return asyncIteratorNext().then(handleIteration); } }); } asyncIteratorNext() .then(handleIteration) .catch(reject); }); } //# sourceMappingURL=autoPagination.js.map