Skip to content

Commit acf1576

Browse files
committed
feat: cursor based pagination [CAPI-2342]
1 parent b2ba23a commit acf1576

19 files changed

+681
-59
lines changed

lib/create-contentful-api.ts

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ import type {
2929
Concept,
3030
ConceptScheme,
3131
ConceptSchemeCollection,
32+
AssetCursorPaginatedCollection,
33+
CollectionForQuery,
34+
EntryCursorPaginatedCollection,
35+
Entry,
3236
} from './types/index.js'
3337
import normalizeSearchParameters from './utils/normalize-search-parameters.js'
3438
import normalizeSelect from './utils/normalize-select.js'
@@ -44,6 +48,8 @@ import {
4448
} from './utils/validate-params.js'
4549
import validateSearchParameters from './utils/validate-search-parameters.js'
4650
import { getTimelinePreviewParams } from './utils/timeline-preview-helpers.js'
51+
import { normalizeCursorPaginationParameters } from './utils/normalize-cursor-pagination-parameters.js'
52+
import { normalizeCursorPaginationResponse } from './utils/normalize-cursor-pagination-response.js'
4753

4854
const ASSET_KEY_MAX_LIFETIME = 48 * 60 * 60
4955

@@ -210,6 +216,13 @@ export default function createContentfulApi<OptionType extends ChainOptions>(
210216
return makeGetEntries(query, options)
211217
}
212218

219+
async function getEntriesCursor(
220+
query = {},
221+
): Promise<EntryCursorPaginatedCollection<EntrySkeletonType>> {
222+
const response = await makeGetEntries(normalizeCursorPaginationParameters(query), options)
223+
return normalizeCursorPaginationResponse(response)
224+
}
225+
213226
async function makeGetEntry<EntrySkeleton extends EntrySkeletonType>(
214227
id: string,
215228
query,
@@ -242,10 +255,12 @@ export default function createContentfulApi<OptionType extends ChainOptions>(
242255
throw notFoundError(id)
243256
}
244257
try {
245-
const response = await internalGetEntries<EntrySkeletonType<EntrySkeleton>, Locales, Options>(
246-
{ 'sys.id': id, ...maybeEnableSourceMaps(query) },
247-
options,
248-
)
258+
const response = await internalGetEntries<
259+
EntrySkeletonType<EntrySkeleton>,
260+
Locales,
261+
Options,
262+
Record<string, unknown>
263+
>({ 'sys.id': id, ...maybeEnableSourceMaps(query) }, options)
249264
if (response.items.length > 0) {
250265
return response.items[0]
251266
} else {
@@ -256,8 +271,11 @@ export default function createContentfulApi<OptionType extends ChainOptions>(
256271
}
257272
}
258273

259-
async function makeGetEntries<EntrySkeleton extends EntrySkeletonType>(
260-
query,
274+
async function makeGetEntries<
275+
EntrySkeleton extends EntrySkeletonType,
276+
Query extends Record<string, unknown>,
277+
>(
278+
query: Query,
261279
options: ChainOptions = {
262280
withAllLocales: false,
263281
withoutLinkResolution: false,
@@ -271,7 +289,7 @@ export default function createContentfulApi<OptionType extends ChainOptions>(
271289
validateRemoveUnresolvedParam(query)
272290
validateSearchParameters(query)
273291

274-
return internalGetEntries<EntrySkeleton, any, Extract<ChainOptions, typeof options>>(
292+
return internalGetEntries<EntrySkeleton, any, Extract<ChainOptions, typeof options>, Query>(
275293
withAllLocales
276294
? {
277295
...query,
@@ -293,10 +311,13 @@ export default function createContentfulApi<OptionType extends ChainOptions>(
293311
EntrySkeleton extends EntrySkeletonType,
294312
Locales extends LocaleCode,
295313
Options extends ChainOptions,
314+
Query extends Record<string, unknown>,
296315
>(
297-
query: Record<string, any>,
316+
query: Query,
298317
options: Options,
299-
): Promise<EntryCollection<EntrySkeleton, ModifiersFromOptions<Options>, Locales>> {
318+
): Promise<
319+
CollectionForQuery<Entry<EntrySkeleton, ModifiersFromOptions<Options>, Locales>, Query>
320+
> {
300321
const { withoutLinkResolution, withoutUnresolvableLinks } = options
301322
try {
302323
const entries = await get({
@@ -324,8 +345,15 @@ export default function createContentfulApi<OptionType extends ChainOptions>(
324345
return makeGetAssets(query, options)
325346
}
326347

327-
async function makeGetAssets(
328-
query: Record<string, any>,
348+
async function getAssetsCursor(
349+
query: Record<string, any> = {},
350+
): Promise<AssetCursorPaginatedCollection> {
351+
const response = await makeGetAssets(normalizeCursorPaginationParameters(query), options)
352+
return normalizeCursorPaginationResponse(response)
353+
}
354+
355+
async function makeGetAssets<Query extends Record<string, any>>(
356+
query: Query,
329357
options: ChainOptions = {
330358
withAllLocales: false,
331359
withoutLinkResolution: false,
@@ -339,7 +367,7 @@ export default function createContentfulApi<OptionType extends ChainOptions>(
339367

340368
const localeSpecificQuery = withAllLocales ? { ...query, locale: '*' } : query
341369

342-
return internalGetAssets<any, Extract<ChainOptions, typeof options>>(localeSpecificQuery)
370+
return internalGetAssets<any, Extract<ChainOptions, typeof options>, Query>(localeSpecificQuery)
343371
}
344372

345373
async function internalGetAsset<Locales extends LocaleCode, Options extends ChainOptions>(
@@ -376,9 +404,13 @@ export default function createContentfulApi<OptionType extends ChainOptions>(
376404
return internalGetAsset<any, Extract<ChainOptions, typeof options>>(id, localeSpecificQuery)
377405
}
378406

379-
async function internalGetAssets<Locales extends LocaleCode, Options extends ChainOptions>(
380-
query: Record<string, any>,
381-
): Promise<AssetCollection<ModifiersFromOptions<Options>, Locales>> {
407+
async function internalGetAssets<
408+
Locales extends LocaleCode,
409+
Options extends ChainOptions,
410+
Query extends Record<string, any>,
411+
>(
412+
query: Query,
413+
): Promise<CollectionForQuery<Asset<ModifiersFromOptions<Options>, Locales>, Query>> {
382414
try {
383415
return get({
384416
context: 'environment',
@@ -657,6 +689,7 @@ export default function createContentfulApi<OptionType extends ChainOptions>(
657689

658690
getAsset,
659691
getAssets,
692+
getAssetsCursor,
660693

661694
getTag,
662695
getTags,
@@ -667,6 +700,7 @@ export default function createContentfulApi<OptionType extends ChainOptions>(
667700

668701
getEntry,
669702
getEntries,
703+
getEntriesCursor,
670704

671705
getConceptScheme,
672706
getConceptSchemes,

lib/types/asset.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ContentfulCollection } from './collection.js'
1+
import type { ContentfulCollection, CursorPaginatedCollection } from './collection.js'
22
import type { LocaleCode } from './locale.js'
33
import type { Metadata } from './metadata.js'
44
import type { EntitySys } from './sys.js'
@@ -85,6 +85,18 @@ export type AssetCollection<
8585
Locales extends LocaleCode = LocaleCode,
8686
> = ContentfulCollection<Asset<Modifiers, Locales>>
8787

88+
/**
89+
* A cursor paginated collection of assets
90+
* @category Asset
91+
* @typeParam Modifiers - The chain modifiers used to configure the client. They’re set automatically when using the client chain modifiers.
92+
* @typeParam Locales - If provided for a client using `allLocales` modifier, response type defines locale keys for asset field values.
93+
* @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/assets | Documentation}
94+
*/
95+
export type AssetCursorPaginatedCollection<
96+
Modifiers extends ChainModifiers = ChainModifiers,
97+
Locales extends LocaleCode = LocaleCode,
98+
> = CursorPaginatedCollection<Asset<Modifiers, Locales>>
99+
88100
/**
89101
* System managed metadata for assets
90102
* @category Asset

lib/types/client.ts

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,26 @@ import type { LocaleCode, LocaleCollection } from './locale.js'
44
import type {
55
AssetQueries,
66
AssetsQueries,
7+
AssetsQueriesCursor,
78
ConceptAncestorsDescendantsQueries,
89
ConceptSchemesQueries,
910
ConceptsQueries,
1011
EntriesQueries,
12+
EntriesQueriesCursor,
1113
EntryQueries,
1214
EntrySkeletonType,
1315
TagQueries,
1416
} from './query/index.js'
1517
import type { SyncCollection, SyncOptions, SyncQuery } from './sync.js'
1618
import type { Tag, TagCollection } from './tag.js'
1719
import type { AssetKey } from './asset-key.js'
18-
import type { Entry, EntryCollection } from './entry.js'
19-
import type { Asset, AssetCollection, AssetFields } from './asset.js'
20+
import type { Entry, EntryCollection, EntryCursorPaginatedCollection } from './entry.js'
21+
import type {
22+
Asset,
23+
AssetCollection,
24+
AssetCursorPaginatedCollection,
25+
AssetFields,
26+
} from './asset.js'
2027
import type { Concept, ConceptCollection } from './concept.js'
2128
import type { ConceptScheme, ConceptSchemeCollection } from './concept-scheme.js'
2229

@@ -407,6 +414,36 @@ export interface ContentfulClientApi<Modifiers extends ChainModifiers> {
407414
query?: EntriesQueries<EntrySkeleton, Modifiers>,
408415
): Promise<EntryCollection<EntrySkeleton, Modifiers, Locales>>
409416

417+
/**
418+
* Fetches a cursor paginated collection of Entries
419+
* @param pagination - Object with cursor pagination options
420+
* @param query - Object with search parameters
421+
* @returns Promise for a cursor paginated collection of Entries
422+
* @typeParam EntrySkeleton - Shape of entry fields used to calculate dynamic keys
423+
* @typeParam Locales - If provided for a client using `allLocales` modifier, response type defines locale keys for entry field values.
424+
* @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/introduction/cursor-pagination | REST API cursor pagination reference}
425+
* @see {@link https://www.contentful.com/developers/docs/javascript/tutorials/using-js-cda-sdk/#retrieving-entries-with-search-parameters | JS SDK tutorial}
426+
* @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters | REST API reference}
427+
* @example
428+
* ```typescript
429+
* const contentful = require('contentful')
430+
*
431+
* const client = contentful.createClient({
432+
* space: '<space_id>',
433+
* accessToken: '<content_delivery_api_key>'
434+
* })
435+
*
436+
* const response = await client.getEntriesCursor()
437+
* console.log(response.items)
438+
* ```
439+
*/
440+
getEntriesCursor<
441+
EntrySkeleton extends EntrySkeletonType = EntrySkeletonType,
442+
Locales extends LocaleCode = LocaleCode,
443+
>(
444+
query?: EntriesQueriesCursor<EntrySkeleton, Modifiers>,
445+
): Promise<EntryCursorPaginatedCollection<EntrySkeleton, Modifiers, Locales>>
446+
410447
/**
411448
* Parse raw json data into a collection of entries. objects.Links will be resolved also
412449
* @param data - json data
@@ -495,6 +532,30 @@ export interface ContentfulClientApi<Modifiers extends ChainModifiers> {
495532
query?: AssetsQueries<AssetFields, Modifiers>,
496533
): Promise<AssetCollection<Modifiers, Locales>>
497534

535+
/**
536+
* Fetches a cursor paginated collection of assets
537+
* @param pagination - Object with cursor pagination options
538+
* @param query - Object with search parameters
539+
* @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/introduction/cursor-pagination | REST API cursor pagination reference}
540+
* @see {@link https://www.contentful.com/developers/docs/javascript/tutorials/using-js-cda-sdk/#retrieving-entries-with-search-parameters | JS SDK tutorial}
541+
* @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters | REST API reference}
542+
* @returns Promise for a cursor paginated collection of Assets
543+
* @typeParam Locales - If provided for a client using `allLocales` modifier, response type defines locale keys for asset field values.
544+
* @example
545+
* const contentful = require('contentful')
546+
*
547+
* const client = contentful.createClient({
548+
* space: '<space_id>',
549+
* accessToken: '<content_delivery_api_key>'
550+
* })
551+
*
552+
* const response = await client.getAssetsCursor()
553+
* console.log(response.items)
554+
*/
555+
getAssetsCursor<Locales extends LocaleCode = LocaleCode>(
556+
query?: AssetsQueriesCursor<AssetFields, Modifiers>,
557+
): Promise<AssetCursorPaginatedCollection<Modifiers, Locales>>
558+
498559
/**
499560
* A client that will fetch assets and entries with all locales. Only available if not already enabled.
500561
*/

lib/types/collection.ts

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,50 @@
1-
import type { AssetSys } from './asset.js'
2-
import type { EntrySys } from './entry.js'
1+
export type CollectionBase<T> = {
2+
limit: number
3+
items: Array<T>
4+
sys?: {
5+
type: 'Array'
6+
}
7+
}
8+
9+
export type OffsetPagination = {
10+
total: number
11+
skip: number
12+
}
13+
14+
export type CursorPagination = {
15+
pages: {
16+
next?: string
17+
prev?: string
18+
}
19+
}
320

421
/**
522
* A wrapper object containing additional information for
6-
* a collection of Contentful resources
23+
* an offset paginated collection of Contentful resources
724
* @category Entity
825
* @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/introduction/collection-resources-and-pagination | Documentation}
926
*/
10-
export interface ContentfulCollection<T> {
11-
total: number
12-
skip: number
13-
limit: number
14-
items: Array<T>
15-
sys?: AssetSys | EntrySys
16-
}
27+
export type OffsetPaginatedCollection<T = unknown> = CollectionBase<T> & OffsetPagination
28+
29+
/**
30+
* A wrapper object containing additional information for
31+
* an offset paginated collection of Contentful resources
32+
* @category Entity
33+
* @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/introduction/collection-resources-and-pagination | Documentation}
34+
*/
35+
export interface ContentfulCollection<T = unknown> extends OffsetPaginatedCollection<T> {}
36+
37+
/**
38+
* A wrapper object containing additional information for
39+
* a curisor paginated collection of Contentful resources
40+
* @category Entity
41+
* @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/introduction/cursor-pagination | Documentation}
42+
*/
43+
export type CursorPaginatedCollection<T = unknown> = CollectionBase<T> & CursorPagination
44+
45+
export type WithCursorPagination = { cursor: true }
46+
47+
export type CollectionForQuery<
48+
T = unknown,
49+
Query extends Record<string, unknown> = Record<string, unknown>,
50+
> = Query extends WithCursorPagination ? CursorPaginatedCollection<T> : OffsetPaginatedCollection<T>

lib/types/concept-scheme.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { CursorPaginatedCollection } from './collection'
12
import type { UnresolvedLink } from './link'
23
import type { LocaleCode } from './locale'
34

@@ -27,14 +28,6 @@ export interface ConceptScheme<Locales extends LocaleCode> {
2728
totalConcepts: number
2829
}
2930

30-
export type ConceptSchemeCollection<Locale extends LocaleCode> = {
31-
sys: {
32-
type: 'Array'
33-
}
34-
items: ConceptScheme<Locale>[]
35-
limit: number
36-
pages?: {
37-
prev?: string
38-
next?: string
39-
}
40-
}
31+
export type ConceptSchemeCollection<Locale extends LocaleCode> = CursorPaginatedCollection<
32+
ConceptScheme<Locale>
33+
>

lib/types/concept.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { CursorPaginatedCollection } from './collection'
12
import type { UnresolvedLink } from './link'
23
import type { LocaleCode } from './locale'
34

@@ -64,14 +65,6 @@ export interface Concept<Locales extends LocaleCode> {
6465
conceptSchemes?: UnresolvedLink<'TaxonomyConceptScheme'>[]
6566
}
6667

67-
export type ConceptCollection<Locale extends LocaleCode> = {
68-
sys: {
69-
type: 'Array'
70-
}
71-
items: Concept<Locale>[]
72-
limit: number
73-
pages?: {
74-
prev?: string
75-
next?: string
76-
}
77-
}
68+
export type ConceptCollection<Locale extends LocaleCode> = CursorPaginatedCollection<
69+
Concept<Locale>
70+
>

0 commit comments

Comments
 (0)