Skip to content

Commit a826100

Browse files
authored
feat(PR-40): auto-page list requests (#100)
When calling `workspaces.list`, `forms.list`, `themes.list` or `responses.list` you can supply `page: "auto"` and the library will fetch all items across all pages for you automatically. It will fetch with maximum available `pageSize` to minimize number of requests.
1 parent 5bf4e0f commit a826100

14 files changed

+316
-56
lines changed

README.md

+4
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ Each one of them encapsulates the operations related to it (like listing, updati
156156

157157
- Get a list of your typeforms
158158
- Returns a list of typeforms with the payload [referenced here](https://developer.typeform.com/create/reference/retrieve-forms/).
159+
- You can set `page: "auto"` to automatically fetch all pages if there are more. It fetches with maximum `pageSize: 200`.
159160

160161
#### `forms.get({ uid })`
161162

@@ -214,6 +215,7 @@ Each one of them encapsulates the operations related to it (like listing, updati
214215

215216
- Gets your themes collection
216217
- `page`: default `1`
218+
- set `page: "auto"` to automatically fetch all pages if there are more, it fetches with maximum `pageSize: 200`
217219
- `pageSize: default `10`
218220

219221
#### `themes.get({ id })`
@@ -262,6 +264,7 @@ Each one of them encapsulates the operations related to it (like listing, updati
262264

263265
- Retrieve all workspaces in your account.
264266
- `page`: The page of results to retrieve. Default `1` is the first page of results.
267+
- set `page: "auto"` to automatically fetch all pages if there are more, it fetches with maximum `pageSize: 200`
265268
- `pageSize`: Number of results to retrieve per page. Default is 10. Maximum is 200.
266269
- `search`: Returns items that contain the specified string.
267270

@@ -290,6 +293,7 @@ Each one of them encapsulates the operations related to it (like listing, updati
290293
- Returns form responses and date and time of form landing and submission.
291294
- `uid`: Unique ID for the form.
292295
- `pageSize`: Maximum number of responses. Default value is 25. Maximum value is 1000.
296+
- `page`: Set to `"auto"` to automatically fetch all pages if there are more. It fetches with maximum `pageSize: 1000`. The `after` value is ignored when automatic paging is enabled. The responses will be sorted in the order that our system processed them (instead of the default order, `submitted_at`). **Note that it does not accept numeric value to identify page number.**
293297
- `since`: Limit request to responses submitted since the specified date and time. In ISO 8601 format, UTC time, to the second, with T as a delimiter between the date and time.
294298
- `until`: Limit request to responses submitted until the specified date and time. In ISO 8601 format, UTC time, to the second, with T as a delimiter between the date and time.
295299
- `after`: Limit request to responses submitted after the specified token. If you use the `after` parameter, the responses will be sorted in the order that our system processed them (instead of the default order, `submitted_at`).

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
"rollup-plugin-typescript2": "^0.24.1",
8585
"semantic-release": "^17.0.7",
8686
"ts-jest": "^24.0.2",
87+
"tslib": "^2.6.2",
8788
"typescript": "^4.9.5"
8889
},
8990
"jest": {

src/auto-page-items.ts

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { rateLimit } from './utils'
2+
3+
// request with maximum available page size to minimize number of requests
4+
const MAX_PAGE_SIZE = 200
5+
6+
type RequestItemsFn<Item> = (
7+
page: number,
8+
pageSize: number
9+
) => Promise<{
10+
total_items: number
11+
page_count: number
12+
items: Item[]
13+
}>
14+
15+
const requestPageItems = async <Item>(
16+
requestFn: RequestItemsFn<Item>,
17+
page = 1
18+
): Promise<Item[]> => {
19+
await rateLimit()
20+
const { items = [] } = (await requestFn(page, MAX_PAGE_SIZE)) || {}
21+
const moreItems =
22+
items.length === MAX_PAGE_SIZE
23+
? await requestPageItems(requestFn, page + 1)
24+
: []
25+
return [...items, ...moreItems]
26+
}
27+
28+
export const autoPageItems = async <Item>(
29+
requestFn: RequestItemsFn<Item>
30+
): Promise<{
31+
total_items: number
32+
page_count: 1
33+
items: Item[]
34+
}> => {
35+
const { total_items = 0, items = [] } =
36+
(await requestFn(1, MAX_PAGE_SIZE)) || {}
37+
return {
38+
total_items,
39+
page_count: 1,
40+
items: [
41+
...items,
42+
...(total_items > items.length
43+
? await requestPageItems(requestFn, 2)
44+
: []),
45+
],
46+
}
47+
}

src/bin.ts

+9-8
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ if (!token) {
1616
const typeformAPI = createClient({ token })
1717

1818
const [, , ...args] = process.argv
19-
const [methodName, methodParams] = args
19+
const [methodName, ...methodParams] = args
2020

2121
if (!methodName || methodName === '-h' || methodName === '--help') {
2222
print('usage: typeform-api <method> [params]')
@@ -35,17 +35,18 @@ if (!typeformAPI[property]?.[method]) {
3535

3636
let parsedParams = undefined
3737

38-
if (methodParams) {
38+
if (methodParams && methodParams.length > 0) {
39+
const methodParamsString = methodParams.join(',')
40+
const normalizedParams = methodParamsString.startsWith('{')
41+
? methodParamsString
42+
: `{${methodParamsString}}`
43+
3944
try {
4045
// this eval executes code supplied by user on their own machine, this is safe
4146
// eslint-disable-next-line no-eval
42-
eval(`parsedParams = ${methodParams}`)
47+
eval(`parsedParams = ${normalizedParams}`)
4348
} catch (err) {
44-
throw new Error(`Invalid params: ${methodParams}`)
45-
}
46-
47-
if (typeof parsedParams !== 'object') {
48-
throw new Error(`Invalid params: ${methodParams}`)
49+
throw new Error(`Invalid params: ${normalizedParams}`)
4950
}
5051
}
5152

src/forms.ts

+19-11
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Typeform } from './typeform-types'
2+
import { autoPageItems } from './auto-page-items'
23

34
export class Forms {
45
private _messages: FormMessages
@@ -40,7 +41,7 @@ export class Forms {
4041
}
4142

4243
public list(args?: {
43-
page?: number
44+
page?: number | 'auto'
4445
pageSize?: number
4546
search?: string
4647
workspaceId?: string
@@ -52,16 +53,23 @@ export class Forms {
5253
workspaceId: null,
5354
}
5455

55-
return this._http.request({
56-
method: 'get',
57-
url: `/forms`,
58-
params: {
59-
page,
60-
page_size: pageSize,
61-
search,
62-
workspace_id: workspaceId,
63-
},
64-
})
56+
const request = (page: number, pageSize: number) =>
57+
this._http.request({
58+
method: 'get',
59+
url: `/forms`,
60+
params: {
61+
page,
62+
page_size: pageSize,
63+
search,
64+
workspace_id: workspaceId,
65+
},
66+
})
67+
68+
if (page === 'auto') {
69+
return autoPageItems(request)
70+
}
71+
72+
return request(page, pageSize)
6573
}
6674

6775
public update(args: {

src/responses.ts

+67-16
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Typeform } from './typeform-types'
2+
import { rateLimit } from './utils'
23

34
export class Responses {
45
constructor(private _http: Typeform.HTTPClient) {}
@@ -27,6 +28,7 @@ export class Responses {
2728
sort?: string
2829
query?: string
2930
fields?: string | string[]
31+
page?: 'auto'
3032
}): Promise<Typeform.API.Responses.List> {
3133
const {
3234
uid,
@@ -40,24 +42,32 @@ export class Responses {
4042
sort,
4143
query,
4244
fields,
45+
page,
4346
} = args
4447

45-
return this._http.request({
46-
method: 'get',
47-
url: `/forms/${uid}/responses`,
48-
params: {
49-
page_size: pageSize,
50-
since,
51-
until,
52-
after,
53-
before,
54-
included_response_ids: toCSL(ids),
55-
completed,
56-
sort,
57-
query,
58-
fields: toCSL(fields),
59-
},
60-
})
48+
const request = (pageSize: number, before: string) =>
49+
this._http.request({
50+
method: 'get',
51+
url: `/forms/${uid}/responses`,
52+
params: {
53+
page_size: pageSize,
54+
since,
55+
until,
56+
after,
57+
before,
58+
included_response_ids: toCSL(ids),
59+
completed,
60+
sort,
61+
query,
62+
fields: toCSL(fields),
63+
},
64+
})
65+
66+
if (page === 'auto') {
67+
return autoPageResponses(request)
68+
}
69+
70+
return request(pageSize, before)
6171
}
6272
}
6373

@@ -68,3 +78,44 @@ const toCSL = (args: string | string[]): string => {
6878

6979
return typeof args === 'string' ? args : args.join(',')
7080
}
81+
82+
// when auto-paginating, request with maximum available page size to minimize number of requests
83+
const MAX_RESULTS_PAGE_SIZE = 1000
84+
85+
type RequestResultsFn = (
86+
pageSize: number,
87+
before?: string
88+
) => Promise<Typeform.API.Responses.List>
89+
90+
const getLastResponseId = (items: Typeform.Response[]) =>
91+
items.length > 0 ? items[items.length - 1]?.response_id : null
92+
93+
const requestPageResponses = async (
94+
requestFn: RequestResultsFn,
95+
before: string = undefined
96+
): Promise<Typeform.Response[]> => {
97+
await rateLimit()
98+
const { items = [] } = (await requestFn(MAX_RESULTS_PAGE_SIZE, before)) || {}
99+
const moreItems =
100+
items.length === MAX_RESULTS_PAGE_SIZE
101+
? await requestPageResponses(requestFn, getLastResponseId(items))
102+
: []
103+
return [...items, ...moreItems]
104+
}
105+
106+
const autoPageResponses = async (
107+
requestFn: RequestResultsFn
108+
): Promise<Typeform.API.Responses.List> => {
109+
const { total_items = 0, items = [] } =
110+
(await requestFn(MAX_RESULTS_PAGE_SIZE)) || {}
111+
return {
112+
total_items,
113+
page_count: 1,
114+
items: [
115+
...items,
116+
...(total_items > items.length
117+
? await requestPageResponses(requestFn, getLastResponseId(items))
118+
: []),
119+
],
120+
}
121+
}

src/themes.ts

+17-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Typeform } from './typeform-types'
22
import { FONTS_AVAILABLE } from './constants'
3+
import { autoPageItems } from './auto-page-items'
34

45
export class Themes {
56
constructor(private _http: Typeform.HTTPClient) {}
@@ -34,19 +35,26 @@ export class Themes {
3435
}
3536

3637
public list(args?: {
37-
page?: number
38+
page?: number | 'auto'
3839
pageSize?: number
3940
}): Promise<Typeform.API.Themes.List> {
4041
const { page, pageSize } = args || { page: null, pageSize: null }
4142

42-
return this._http.request({
43-
method: 'get',
44-
url: '/themes',
45-
params: {
46-
page,
47-
page_size: pageSize,
48-
},
49-
})
43+
const request = (page: number, pageSize: number) =>
44+
this._http.request({
45+
method: 'get',
46+
url: '/themes',
47+
params: {
48+
page,
49+
page_size: pageSize,
50+
},
51+
})
52+
53+
if (page === 'auto') {
54+
return autoPageItems(request)
55+
}
56+
57+
return request(page, pageSize)
5058
}
5159

5260
public update(args: {

src/utils.ts

+5
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,8 @@ export const createMemberPatchQuery = (
1616
export const isMemberPropValid = (members: string | string[]): boolean => {
1717
return members && (typeof members === 'string' || Array.isArray(members))
1818
}
19+
20+
// two requests per second, per Typeform account
21+
// https://www.typeform.com/developers/get-started/#rate-limits
22+
export const rateLimit = () =>
23+
new Promise((resolve) => setTimeout(resolve, 500))

src/workspaces.ts

+18-10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Typeform } from './typeform-types'
22
import { isMemberPropValid, createMemberPatchQuery } from './utils'
3+
import { autoPageItems } from './auto-page-items'
34

45
export class Workspaces {
56
constructor(private _http: Typeform.HTTPClient) {}
@@ -53,7 +54,7 @@ export class Workspaces {
5354

5455
public list(args?: {
5556
search?: string
56-
page?: number
57+
page?: number | 'auto'
5758
pageSize?: number
5859
}): Promise<Typeform.API.Workspaces.List> {
5960
const { search, page, pageSize } = args || {
@@ -62,15 +63,22 @@ export class Workspaces {
6263
pageSize: null,
6364
}
6465

65-
return this._http.request({
66-
method: 'get',
67-
url: '/workspaces',
68-
params: {
69-
page,
70-
page_size: pageSize,
71-
search,
72-
},
73-
})
66+
const request = (page: number, pageSize: number) =>
67+
this._http.request({
68+
method: 'get',
69+
url: '/workspaces',
70+
params: {
71+
page,
72+
page_size: pageSize,
73+
search,
74+
},
75+
})
76+
77+
if (page === 'auto') {
78+
return autoPageItems(request)
79+
}
80+
81+
return request(page, pageSize)
7482
}
7583

7684
public removeMembers(args: {

0 commit comments

Comments
 (0)