Skip to content

Commit 714b5df

Browse files
authored
fix/get api pagination support (#20)
* fix? * lint
1 parent 4b59239 commit 714b5df

File tree

3 files changed

+129
-53
lines changed

3 files changed

+129
-53
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414

1515
## [Released]
1616

17+
### [1.0.7] - 2024-10-11
18+
- fixed `pageCurrent` not being set properly for all responses
19+
20+
1721
### [1.0.6] - 2024-06-17
1822
- Duplciate API catching improvements
1923
- Previously successful API calls were being caught as duplicates

TODO.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@
5252
### [1.1.0] - 2024-MM-DD
5353
#### TODO
5454

55+
---
56+
57+
### [1.0.7] - 2024-10-11
58+
- fixed `pageCurrent` not being set properly for all responses
59+
5560
-------------------------------------------------------
5661

5762
##### [https://danielnazarian.com](https://danielnazarian.com)

src/api/django_service/methods/get.ts

Lines changed: 120 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,10 @@ export default class DjangoGet<Model, TypeFilters extends object | null = null>
4141
*
4242
* GET Django list from this API
4343
*
44-
* @param {Boolean=} paginated - Treat this API result like a paginated one (i.e., it contains 'next', 'prev', etc.)
44+
* @param {boolean=} paginated - Treat this API result like a paginated one (i.e., it contains 'next', 'prev', etc.)
4545
* @param {TypeFilters=} filters - Filters to send with request
4646
* @param {Record<string, unknown>=} extraHeaders - Extra headers to add to request
47-
* @return {ApiResponse} Api response object
47+
* @return {ApiResponse<Model[]>} Api response object
4848
*/
4949
public async getList (
5050
paginated: boolean = true,
@@ -60,16 +60,16 @@ export default class DjangoGet<Model, TypeFilters extends object | null = null>
6060
}
6161

6262
/**
63-
* getList
63+
* getListAll
6464
*
6565
* GET Django list from this API for ALL pages if paginated
6666
*
6767
* @param {Record<string, unknown>=} extraHeaders - Extra headers to add to request
68-
* @returns {Array} List of all objects paginated out
68+
* @returns {Model[]} List of all objects paginated out
6969
*/
7070
public async getListAll (extraHeaders?: Record<string, unknown>): Promise<Model[]> {
7171
this.loading = true
72-
let res = []
72+
let res: Model[] = []
7373
const first = await this.getList(true, undefined, extraHeaders)
7474
res = first.obj ?? []
7575
let pages = 1
@@ -78,10 +78,8 @@ export default class DjangoGet<Model, TypeFilters extends object | null = null>
7878
const nextPage = await this.getNext()
7979
if (typeof nextPage !== 'undefined') {
8080
const nextList = nextPage.obj
81-
if (!!nextList && nextList.length > 0) {
82-
nextList.map(function (i: Model) {
83-
return res.push(i)
84-
})
81+
if (nextList && nextList.length > 0) {
82+
nextList.map((i: Model) => res.push(i))
8583
}
8684
}
8785
}
@@ -95,11 +93,11 @@ export default class DjangoGet<Model, TypeFilters extends object | null = null>
9593
*
9694
* Django GET item and details
9795
*
98-
* @param {string=} id - ID of object to retrieve
99-
* @param {Boolean=} paginated - Treat this API result like a paginated one (i.e., it contains 'next', 'prev', etc.)
96+
* @param {string} id - ID of object to retrieve
97+
* @param {boolean=} paginated - Treat this API result like a paginated one (i.e., it contains 'next', 'prev', etc.)
10098
* @param {Record<string, unknown>=} extraHeaders - Extra headers to add to request
10199
* @param {TypeFilters=} filters - Filters to send with request
102-
* @return {ApiResponse} Api response object
100+
* @return {ApiResponse<Model | Model[]>} Api response object
103101
*/
104102
public async getRetrieve (
105103
id: string,
@@ -118,18 +116,23 @@ export default class DjangoGet<Model, TypeFilters extends object | null = null>
118116
/**
119117
* handleDjangoGet
120118
*
121-
* @param ApiResponse
122-
* @param paginated
123-
* @returns ApiResponse
119+
* Helper function to handle the response from a GET request.
120+
*
121+
* @param {ApiResponse<Model | Model[]>} apiResponse - API response object
122+
* @param {boolean} paginated - Whether the response is paginated
123+
* @returns {Promise<ApiResponse<Model | Model[]>>} Api response object
124124
*/
125-
protected async handleDjangoGet (apiResponse: ApiResponse<Model | Model[]>, paginated: boolean): Promise<ApiResponse<Model | Model[]>> {
126-
// handle duplicate response
125+
protected async handleDjangoGet (
126+
apiResponse: ApiResponse<Model | Model[]>,
127+
paginated: boolean
128+
): Promise<ApiResponse<Model | Model[]>> {
129+
// Handle duplicate response
127130
if (apiResponse.duplicate) {
128131
return apiResponse
129132
}
130133

131-
// helper function to clean up get and retrieve methods
132134
if (!paginated) {
135+
// Non-paginated response
133136
try {
134137
if (apiResponse.obj instanceof Array) {
135138
this.list = apiResponse.obj
@@ -141,10 +144,11 @@ export default class DjangoGet<Model, TypeFilters extends object | null = null>
141144
}
142145
return apiResponse
143146
} else {
147+
// Paginated response
144148
try {
145149
this.count = (apiResponse.response.data as { count: number }).count
146150
this.list = (apiResponse.obj as Model[]) ?? []
147-
this.calculatePageTotal() // this should only be called during the initial call NOT during any next/prev calls
151+
this.calculatePageTotal() // This should only be called during the initial call NOT during any next/prev calls
148152
} catch (e) {
149153
console.warn(e)
150154
}
@@ -160,10 +164,13 @@ export default class DjangoGet<Model, TypeFilters extends object | null = null>
160164
* this API data
161165
*
162166
* @param {ApiResponse<Model[]>} apiResponse - Api response object
163-
* @param {Boolean=} combineLists - Whether to add next page to the current list or replace it
164-
* @return {ApiResponse} Api response object
167+
* @param {boolean=} combineLists - Whether to add next page to the current list or replace it
168+
* @return {ApiResponse<Model[]>} Api response object
165169
*/
166-
protected async handlePaginatedResponse (apiResponse: ApiResponse<Model[]>, combineLists: boolean = false): Promise<ApiResponse<Model[]>> {
170+
protected async handlePaginatedResponse (
171+
apiResponse: ApiResponse<Model[]>,
172+
combineLists: boolean = false
173+
): Promise<ApiResponse<Model[]>> {
167174
try {
168175
const responseData = apiResponse.response.data as { count: number, next: string, previous: string }
169176
if (responseData) {
@@ -175,13 +182,13 @@ export default class DjangoGet<Model, TypeFilters extends object | null = null>
175182
if (!combineLists) {
176183
this.list = apiResponse.obj ?? []
177184
} else {
178-
if (!!this.list && this.list.length > 0) {
185+
if (this.list && this.list.length > 0) {
179186
this.list = [...this.list, ...(apiResponse.obj ?? [])]
180187
} else {
181188
this.list = apiResponse.obj ?? []
182189
}
183190
}
184-
this.calculatePageCurrent()
191+
this.calculatePageCurrent() // Update current page number based on 'next' and 'prev'
185192
} catch (e) {
186193
console.error(e)
187194
}
@@ -191,17 +198,27 @@ export default class DjangoGet<Model, TypeFilters extends object | null = null>
191198
/**
192199
* getNext
193200
*
194-
* @param {Boolean=} combineLists - Whether to add next page to the current list or replace it
201+
* Get the next page of results.
202+
*
203+
* @param {boolean=} combineLists - Whether to add next page to the current list or replace it
195204
* @param {Record<string, unknown>=} extraHeaders - Extra headers to add to request
196-
* @return {ApiResponse} Api response object
205+
* @return {ApiResponse<Model[]> | undefined} Api response object
197206
*/
198-
public async getNext (combineLists: boolean = false, extraHeaders?: Record<string, unknown>): Promise<ApiResponse<Model[]> | undefined> {
207+
public async getNext (
208+
combineLists: boolean = false,
209+
extraHeaders?: Record<string, unknown>
210+
): Promise<ApiResponse<Model[]> | undefined> {
199211
this.loading = true
200-
if (typeof this.next !== 'undefined') {
212+
if (typeof this.next !== 'undefined' && this.next !== null) {
201213
const apiResponse = await this.httpGet(this.next, extraHeaders)
214+
if (apiResponse.duplicate) {
215+
this.loading = false
216+
return apiResponse as ApiResponse<Model[]>
217+
}
218+
202219
if (apiResponse.obj instanceof Array) {
203-
// If the response is a list, return it as-is
204220
const response = await this.handlePaginatedResponse(apiResponse as ApiResponse<Model[]>, combineLists)
221+
this.pageCurrent += 1 // Increment current page
205222
this.loading = false
206223
return response
207224
} else {
@@ -215,17 +232,27 @@ export default class DjangoGet<Model, TypeFilters extends object | null = null>
215232
/**
216233
* getPrev
217234
*
218-
* @param {Boolean=} combineLists - Whether to add next page to the current list or replace it
235+
* Get the previous page of results.
236+
*
237+
* @param {boolean=} combineLists - Whether to add previous page to the current list or replace it
219238
* @param {Record<string, unknown>=} extraHeaders - Extra headers to add to request
220-
* @return {ApiResponse} Api response object
239+
* @return {ApiResponse<Model[]> | undefined} Api response object
221240
*/
222-
public async getPrev (combineLists: boolean = false, extraHeaders?: Record<string, unknown>): Promise<ApiResponse<Model[]> | undefined> {
241+
public async getPrev (
242+
combineLists: boolean = false,
243+
extraHeaders?: Record<string, unknown>
244+
): Promise<ApiResponse<Model[]> | undefined> {
223245
this.loading = true
224-
if (typeof this.prev !== 'undefined') {
246+
if (typeof this.prev !== 'undefined' && this.prev !== null) {
225247
const apiResponse = await this.httpGet(this.prev, extraHeaders)
248+
if (apiResponse.duplicate) {
249+
this.loading = false
250+
return apiResponse as ApiResponse<Model[]>
251+
}
252+
226253
if (apiResponse.obj instanceof Array) {
227-
// If the response is a list, return it as-is
228254
const response = await this.handlePaginatedResponse(apiResponse as ApiResponse<Model[]>, combineLists)
255+
this.pageCurrent -= 1 // Decrement current page
229256
this.loading = false
230257
return response
231258
} else {
@@ -239,26 +266,34 @@ export default class DjangoGet<Model, TypeFilters extends object | null = null>
239266
/**
240267
* getPage
241268
*
242-
* @param {Number} page - Specific page number to retrieve
269+
* Get a specific page of results.
270+
*
271+
* @param {number} page - Specific page number to retrieve
243272
* @param {Record<string, unknown>=} extraHeaders - Extra headers to add to request
244-
* @return {ApiResponse} Api response object
273+
* @return {ApiResponse<Model[]>} Api response object
245274
*/
246-
247275
public async getPage (page: number, extraHeaders?: Record<string, unknown>): Promise<ApiResponse<Model[]>> {
248276
this.loading = true
249277
const pageUrl = `${this.urlApi()}?page=${page}`
250278
const apiResponse = await this.httpGet(pageUrl, extraHeaders)
251279

252-
if (apiResponse.obj instanceof Array) {
253-
// If the response is a list, return it as-is
280+
if (apiResponse.duplicate) {
254281
this.loading = false
255282
return apiResponse as ApiResponse<Model[]>
283+
}
284+
285+
if (apiResponse.obj instanceof Array) {
286+
const response = await this.handlePaginatedResponse(apiResponse as ApiResponse<Model[]>)
287+
this.pageCurrent = page // Set the current page here
288+
this.loading = false
289+
return response
256290
} else {
257291
// If the response is not a list, wrap the result in an array
258292
const response: ApiResponse<Model[]> = {
259293
...apiResponse,
260294
obj: [apiResponse.obj as Model]
261295
}
296+
this.pageCurrent = page // Set the current page here
262297
this.loading = false
263298
return response
264299
}
@@ -267,25 +302,57 @@ export default class DjangoGet<Model, TypeFilters extends object | null = null>
267302
/**
268303
* PAGINATION HELPERS
269304
*/
305+
306+
/**
307+
* calculatePageCurrent
308+
*
309+
* Calculate the current page number based on 'next' and 'prev' URLs.
310+
*/
270311
protected calculatePageCurrent (): void {
271-
if (typeof this.next !== 'undefined' && this.next != null) {
272-
const num = Number(this.getQueryString('page', this.next)) || 2
273-
this.pageCurrent = num - 1
274-
} else if (typeof this.prev !== 'undefined' && this.prev != null) {
275-
const num = Number(this.getQueryString('page', this.prev)) || 0
276-
this.pageCurrent = num + 1
312+
if (this.next) {
313+
const num = Number(this.getQueryStringValue('page', this.next))
314+
if (!isNaN(num)) {
315+
this.pageCurrent = num - 1
316+
}
317+
} else if (this.prev) {
318+
const num = Number(this.getQueryStringValue('page', this.prev))
319+
if (!isNaN(num)) {
320+
this.pageCurrent = num + 1
321+
}
322+
} else {
323+
// If both next and prev are null, we're on the first or last page
324+
if (this.pageCurrent === undefined || this.pageCurrent === null) {
325+
this.pageCurrent = 1 // Default to page 1
326+
}
277327
}
278328
}
279329

330+
/**
331+
* getQueryStringValue
332+
*
333+
* Extracts the value of a query parameter from a URL.
334+
*
335+
* @param {string} field - The name of the query parameter to extract.
336+
* @param {string} url - The URL to parse.
337+
* @return {string | null} The value of the query parameter, or null if not found.
338+
*/
339+
protected getQueryStringValue (field: string, url: string): number {
340+
// Parse the URL to extract the query parameter
341+
const reg = new RegExp(`[?&]${field}=([^&#]*)`, 'i')
342+
const string = reg.exec(url)
343+
return string ? Number(string[1]) : NaN
344+
}
345+
346+
/**
347+
* calculatePageTotal
348+
*
349+
* Calculate the total number of pages.
350+
*/
280351
protected calculatePageTotal (): void {
281-
if (typeof this.list !== 'undefined') {
282-
if (this.list?.length > 0 && typeof this.count !== 'undefined') {
283-
let pageTotal = Math.floor(this.count / this.list?.length)
284-
const remainder = this.count % this.list?.length
285-
if (remainder !== 0) {
286-
pageTotal++
287-
}
288-
this.pageTotal = pageTotal
352+
if (this.list && this.count !== undefined) {
353+
const itemsPerPage = this.list.length
354+
if (itemsPerPage > 0) {
355+
this.pageTotal = Math.ceil(this.count / itemsPerPage)
289356
}
290357
}
291358
}

0 commit comments

Comments
 (0)