Skip to content

Commit

Permalink
Add infrastructure for search.
Browse files Browse the repository at this point in the history
  • Loading branch information
dcr-stripe committed Jul 27, 2021
1 parent 713c73e commit bceadfd
Show file tree
Hide file tree
Showing 5 changed files with 193 additions and 17 deletions.
2 changes: 1 addition & 1 deletion lib/StripeMethod.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ function stripeMethod(spec) {
callback
);

if (spec.methodType === 'list') {
if (spec.methodType === 'list' || spec.methodType === 'search') {
const autoPaginationMethods = makeAutoPaginationMethods(
this,
args,
Expand Down
53 changes: 38 additions & 15 deletions lib/autoPagination.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,42 +6,65 @@ const utils = require('./utils');
function makeAutoPaginationMethods(self, requestArgs, spec, firstPagePromise) {
const promiseCache = {currentPromise: null};
const reverseIteration = isReverseIteration(requestArgs);
let listPromise = firstPagePromise;
let pagePromise = firstPagePromise;
let i = 0;

function iterate(listResult) {
// Search and List methods iterate differently.
// Search relies on a `next_page` token and can only iterate in one direction.
// List relies on either an `ending_before` or `starting_after` field with
// an item ID to paginate and is bi-directional.
let getNextPagePromise;
if (spec.methodType === 'search') {
getNextPagePromise = (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.'
);
}
return makeRequest(self, requestArgs, spec, {
next_page: pageResult.next_page,
});
};
} else {
getNextPagePromise = (pageResult) => {
const lastId = getLastId(pageResult, reverseIteration);
return makeRequest(self, requestArgs, spec, {
[reverseIteration ? 'ending_before' : 'starting_after']: lastId,
});
};
}

function iterate(pageResult) {
if (
!(
listResult &&
listResult.data &&
typeof listResult.data.length === 'number'
pageResult &&
pageResult.data &&
typeof pageResult.data.length === 'number'
)
) {
throw Error(
'Unexpected: Stripe API response does not have a well-formed `data` array.'
);
}

if (i < listResult.data.length) {
const idx = reverseIteration ? listResult.data.length - 1 - i : i;
const value = listResult.data[idx];
if (i < pageResult.data.length) {
const idx = reverseIteration ? pageResult.data.length - 1 - i : i;
const value = pageResult.data[idx];
i += 1;

return {value, done: false};
} else if (listResult.has_more) {
} else if (pageResult.has_more) {
// Reset counter, request next page, and recurse.
i = 0;
const lastId = getLastId(listResult, reverseIteration);
listPromise = makeRequest(self, requestArgs, spec, {
[reverseIteration ? 'ending_before' : 'starting_after']: lastId,
});
return listPromise.then(iterate);
pagePromise = getNextPagePromise(pageResult);
return pagePromise.then(iterate);
}
return {value: undefined, done: true};
}

function asyncIteratorNext() {
return memoizedPromise(promiseCache, (resolve, reject) => {
return listPromise
return pagePromise
.then(iterate)
.then(resolve)
.catch(reject);
Expand Down
111 changes: 111 additions & 0 deletions test/autoPagination.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -701,4 +701,115 @@ describe('auto pagination', function() {
});
});
});

describe('pagination logic using a mock search paginator', () => {
const mockPagination = (pages, initialArgs) => {
let i = 1;
const paramsLog = [];
const spec = {
method: 'GET',
methodType: 'search',
};

const addNextPage = (props) => {
let nextPageProperties = {};
if (props.has_more) {
nextPageProperties = {
next_page: `${props.data[props.data.length - 1]}-encoded`,
};
}
return {...props, ...nextPageProperties};
};

const paginator = makeAutoPaginationMethods(
{
createResourcePathWithSymbols: () => {},
createFullPath: () => {},
_request: (_1, _2, path, _4, _5, _6, callback) => {
paramsLog.push(path);

callback(
null,
Promise.resolve(
addNextPage({
data: pages[i].map((id) => ({id})),
has_more: i < pages.length - 1,
})
)
);
i += 1;
},
},
initialArgs || {},
spec,
Promise.resolve(
addNextPage({
data: pages[0].map((id) => ({id})),
has_more: pages.length > 1,
})
)
);
return {paginator, paramsLog};
};

const testCase = ({
pages,
limit,
expectedIds,
expectedParamsLog,
initialArgs,
}) => {
const {paginator, paramsLog} = mockPagination(pages, initialArgs);
expect(
paginator.autoPagingToArray({limit}).then((result) => ({
ids: result.map((x) => x.id),
paramsLog,
}))
).to.eventually.deep.equal({
ids: expectedIds,
paramsLog: expectedParamsLog,
});
};

it('paginates forwards as expected', () => {
testCase({
pages: [
[1, 2],
[3, 4],
],
limit: 5,
expectedIds: [1, 2, 3, 4],
expectedParamsLog: ['?next_page=2-encoded'],
});

testCase({
pages: [[1, 2], [3, 4], [5]],
limit: 5,
expectedIds: [1, 2, 3, 4, 5],
expectedParamsLog: ['?next_page=2-encoded', '?next_page=4-encoded'],
});

testCase({
pages: [
[1, 2],
[3, 4],
[5, 6],
],
limit: 5,
expectedIds: [1, 2, 3, 4, 5],
expectedParamsLog: ['?next_page=2-encoded', '?next_page=4-encoded'],
});

testCase({
pages: [
[1, 2],
[3, 4],
[5, 6],
],
limit: 6,
expectedIds: [1, 2, 3, 4, 5, 6],
expectedParamsLog: ['?next_page=2-encoded', '?next_page=4-encoded'],
});
});
});
});
39 changes: 38 additions & 1 deletion types/lib.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ declare module 'stripe' {
method: string;
path?: string;
fullPath?: string;
methodType?: 'list';
methodType?: 'list' | 'search';
}): (...args: any[]) => object; //eslint-disable-line @typescript-eslint/no-explicit-any
static BASIC_METHODS: {
create<T>(
Expand Down Expand Up @@ -202,6 +202,43 @@ declare module 'stripe' {
autoPagingToArray(opts: {limit: number}): Promise<Array<T>>;
}

/**
* A container for paginated lists of search results.
* The array of objects is on the `.data` property,
* and `.has_more` indicates whether there are additional objects beyond the end of this list.
* The `.next_page` field can be used to paginate forwards.
*/
export interface ApiSearchResult<T> {
object: 'search_result';

data: Array<T>;

/**
* True if this list has another page of items after this one that can be fetched.
*/
has_more: boolean;

/**
* The URL where this list can be accessed.
*/
url: string;

/**
* The page token to use to get the next page of results. If `has_more` is
* true, this will be set.
*/
next_page?: string;
}
export interface ApiSearchResultPromise<T>
extends Promise<Response<ApiSearchResult<T>>>,
AsyncIterableIterator<T> {
autoPagingEach(
handler: (item: T) => boolean | void | Promise<boolean | void>
): Promise<void>;

autoPagingToArray(opts: {limit: number}): Promise<Array<T>>;
}

export type StripeStreamResponse = NodeJS.ReadableStream;

/**
Expand Down
5 changes: 5 additions & 0 deletions types/test/typescriptTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,11 @@ Stripe.StripeResource.extend({
method: 'create',
fullPath: '/v1/full/path',
}),
search: Stripe.StripeResource.method({
method: 'create',
fullPath: 'foo',
methodType: 'search',
}),
});

const maxBufferedRequestMetrics: number =
Expand Down

0 comments on commit bceadfd

Please sign in to comment.