Skip to content

Commit e20046c

Browse files
Merge branch 'master' into groot-2165
2 parents e2cc822 + c06b462 commit e20046c

17 files changed

+642
-20
lines changed

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
- [Configuration](#configuration)
6060
- [Reference Documentation](#reference-documentation)
6161
- [Contentful Javascript resources](#contentful-javascript-resources)
62+
- [Cursor Based Pagination](#cursor-based-pagination)
6263
- [REST API reference](#rest-api-reference)
6364
- [Versioning](#versioning)
6465
- [Reach out to us](#reach-out-to-us)
@@ -226,6 +227,25 @@ The benefits of using the "plain" version of the client, over the legacy version
226227
- The ability to scope CMA client instance to a specific `spaceId`, `environmentId`, and `organizationId` when initializing the client.
227228
- You can pass a concrete values to `defaults` and omit specifying these params in actual CMA methods calls.
228229

230+
## Cursor Based Pagination
231+
232+
Cursor-based pagination is supported on collection endpoints for content types, entries, and assets. To use cursor-based pagination, use the related entity methods `getAssetsWithCursor`, `getContentTypesWithCursor`, and `getEntriesWithCursor`
233+
234+
```js
235+
const response = await environment.getEntriesWithCursor({ limit: 10 });
236+
console.log(response.items); // Array of items
237+
console.log(response.pages?.next); // Cursor for next page
238+
```
239+
Use the value from `response.pages.next` to fetch the next page.
240+
241+
```js
242+
const secondPage = await environment.getEntriesWithCursor({
243+
limit: 2,
244+
pageNext: response.pages?.next,
245+
});
246+
console.log(secondPage.items); // Array of items
247+
```
248+
229249
## Legacy Client Interface
230250
231251
The following code snippet is an example of the legacy client interface, which reads and writes data as a sequence of nested requests:

lib/common-types.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,7 @@ export interface BasicQueryOptions {
376376
}
377377

378378
export interface BasicCursorPaginationOptions extends Omit<BasicQueryOptions, 'skip'> {
379+
skip?: never
379380
pageNext?: string
380381
pagePrev?: string
381382
}
@@ -504,6 +505,7 @@ type MRInternal<UA extends boolean> = {
504505
): MRReturn<'AppInstallation', 'getForOrganization'>
505506

506507
(opts: MROpts<'Asset', 'getMany', UA>): MRReturn<'Asset', 'getMany'>
508+
(opts: MROpts<'Asset', 'getManyWithCursor', UA>): MRReturn<'Asset', 'getManyWithCursor'>
507509
(opts: MROpts<'Asset', 'getPublished', UA>): MRReturn<'Asset', 'getPublished'>
508510
(opts: MROpts<'Asset', 'get', UA>): MRReturn<'Asset', 'get'>
509511
(opts: MROpts<'Asset', 'update', UA>): MRReturn<'Asset', 'update'>
@@ -584,6 +586,9 @@ type MRInternal<UA extends boolean> = {
584586

585587
(opts: MROpts<'ContentType', 'get', UA>): MRReturn<'ContentType', 'get'>
586588
(opts: MROpts<'ContentType', 'getMany', UA>): MRReturn<'ContentType', 'getMany'>
589+
(
590+
opts: MROpts<'ContentType', 'getManyWithCursor', UA>,
591+
): MRReturn<'ContentType', 'getManyWithCursor'>
587592
(opts: MROpts<'ContentType', 'update', UA>): MRReturn<'ContentType', 'update'>
588593
(opts: MROpts<'ContentType', 'create', UA>): MRReturn<'ContentType', 'create'>
589594
(opts: MROpts<'ContentType', 'createWithId', UA>): MRReturn<'ContentType', 'createWithId'>
@@ -633,6 +638,7 @@ type MRInternal<UA extends boolean> = {
633638
): MRReturn<'EnvironmentTemplateInstallation', 'getForEnvironment'>
634639

635640
(opts: MROpts<'Entry', 'getMany', UA>): MRReturn<'Entry', 'getMany'>
641+
(opts: MROpts<'Entry', 'getManyWithCursor', UA>): MRReturn<'Entry', 'getManyWithCursor'>
636642
(opts: MROpts<'Entry', 'getPublished', UA>): MRReturn<'Entry', 'getPublished'>
637643
(opts: MROpts<'Entry', 'get', UA>): MRReturn<'Entry', 'get'>
638644
(opts: MROpts<'Entry', 'patch', UA>): MRReturn<'Entry', 'patch'>
@@ -1260,6 +1266,11 @@ export type MRActions = {
12601266
headers?: RawAxiosRequestHeaders
12611267
return: CollectionProp<AssetProps>
12621268
}
1269+
getManyWithCursor: {
1270+
params: GetSpaceEnvironmentParams & CursorBasedParams & { releaseId?: string }
1271+
headers?: RawAxiosRequestHeaders
1272+
return: CursorPaginatedCollectionProp<AssetProps>
1273+
}
12631274
get: {
12641275
params: GetSpaceEnvironmentParams & { assetId: string; releaseId?: string } & QueryParams
12651276
headers?: RawAxiosRequestHeaders
@@ -1508,6 +1519,10 @@ export type MRActions = {
15081519
params: GetSpaceEnvironmentParams & QueryParams
15091520
return: CollectionProp<ContentTypeProps>
15101521
}
1522+
getManyWithCursor: {
1523+
params: GetSpaceEnvironmentParams & CursorBasedParams
1524+
return: CursorPaginatedCollectionProp<ContentTypeProps>
1525+
}
15111526
create: {
15121527
params: GetSpaceEnvironmentParams
15131528
payload: CreateContentTypeProps
@@ -1676,6 +1691,10 @@ export type MRActions = {
16761691
params: GetSpaceEnvironmentParams & QueryParams & { releaseId?: string }
16771692
return: CollectionProp<EntryProps<any>>
16781693
}
1694+
getManyWithCursor: {
1695+
params: GetSpaceEnvironmentParams & CursorBasedParams & { releaseId?: string }
1696+
return: CursorPaginatedCollectionProp<EntryProps<any>>
1697+
}
16791698
get: {
16801699
params: GetSpaceEnvironmentParams & { entryId: string; releaseId?: string } & QueryParams
16811700
return: EntryProps<any>

lib/common-utils.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
import { toPlainObject } from 'contentful-sdk-core'
44
import copy from 'fast-copy'
55
import type {
6+
BasicCursorPaginationOptions,
67
Collection,
78
CollectionProp,
9+
CursorBasedParams,
810
CursorPaginatedCollection,
911
CursorPaginatedCollectionProp,
1012
MakeRequest,
@@ -47,3 +49,50 @@ export function shouldRePoll(statusCode: number) {
4749
export async function waitFor(ms = 1000) {
4850
return new Promise((resolve) => setTimeout(resolve, ms))
4951
}
52+
53+
export function normalizeCursorPaginationParameters(
54+
query: BasicCursorPaginationOptions,
55+
): CursorBasedParams {
56+
const { pagePrev, pageNext, ...rest } = query
57+
58+
return {
59+
...rest,
60+
cursor: true,
61+
// omit pagePrev and pageNext if the value is falsy
62+
...(pagePrev ? { pagePrev } : null),
63+
...(pageNext ? { pageNext } : null),
64+
} as CursorBasedParams
65+
}
66+
67+
function extractQueryParam(key: string, url?: string): string | undefined {
68+
if (!url) return
69+
70+
const queryIndex = url.indexOf('?')
71+
if (queryIndex === -1) return
72+
73+
const queryString = url.slice(queryIndex + 1)
74+
return new URLSearchParams(queryString).get(key) ?? undefined
75+
}
76+
77+
const Pages = {
78+
prev: 'pagePrev',
79+
next: 'pageNext',
80+
} as const
81+
82+
const PAGE_KEYS = ['prev', 'next'] as const
83+
84+
export function normalizeCursorPaginationResponse<T>(
85+
data: CursorPaginatedCollectionProp<T>,
86+
): CursorPaginatedCollectionProp<T> {
87+
const pages: { prev?: string; next?: string } = {}
88+
89+
for (const key of PAGE_KEYS) {
90+
const token = extractQueryParam(Pages[key], data.pages?.[key])
91+
if (token) pages[key] = token
92+
}
93+
94+
return {
95+
...data,
96+
pages,
97+
}
98+
}

lib/create-environment-api.ts

Lines changed: 137 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import type {
77
CursorBasedParams,
88
QueryOptions,
99
} from './common-types'
10+
import {
11+
normalizeCursorPaginationParameters,
12+
normalizeCursorPaginationResponse,
13+
} from './common-utils'
1014
import type { BasicQueryOptions, MakeRequest } from './common-types'
1115
import entities from './entities'
1216
import type { CreateAppInstallationProps } from './entities/app-installation'
@@ -15,11 +19,11 @@ import type {
1519
CreateAppActionCallProps,
1620
AppActionCallRawResponseProps,
1721
} from './entities/app-action-call'
18-
import type {
19-
AssetFileProp,
20-
AssetProps,
21-
CreateAssetFromFilesOptions,
22-
CreateAssetProps,
22+
import {
23+
type AssetFileProp,
24+
type AssetProps,
25+
type CreateAssetFromFilesOptions,
26+
type CreateAssetProps,
2327
} from './entities/asset'
2428
import type { CreateAssetKeyProps } from './entities/asset-key'
2529
import type {
@@ -40,12 +44,12 @@ import type {
4044
} from './entities/release'
4145
import { wrapRelease, wrapReleaseCollection } from './entities/release'
4246

43-
import type { ContentTypeProps, CreateContentTypeProps } from './entities/content-type'
44-
import type {
45-
CreateEntryProps,
46-
EntryProps,
47-
EntryReferenceOptionsProps,
48-
EntryReferenceProps,
47+
import { type ContentTypeProps, type CreateContentTypeProps } from './entities/content-type'
48+
import {
49+
type CreateEntryProps,
50+
type EntryProps,
51+
type EntryReferenceOptionsProps,
52+
type EntryReferenceProps,
4953
} from './entities/entry'
5054
import type { EnvironmentProps } from './entities/environment'
5155
import type { CreateExtensionProps } from './entities/extension'
@@ -79,9 +83,10 @@ export type ContentfulEnvironmentAPI = ReturnType<typeof createEnvironmentApi>
7983
*/
8084
export default function createEnvironmentApi(makeRequest: MakeRequest) {
8185
const { wrapEnvironment } = entities.environment
82-
const { wrapContentType, wrapContentTypeCollection } = entities.contentType
83-
const { wrapEntry, wrapEntryCollection } = entities.entry
84-
const { wrapAsset, wrapAssetCollection } = entities.asset
86+
const { wrapContentType, wrapContentTypeCollection, wrapContentTypeCursorPaginatedCollection } =
87+
entities.contentType
88+
const { wrapEntry, wrapEntryCollection, wrapEntryTypeCursorPaginatedCollection } = entities.entry
89+
const { wrapAsset, wrapAssetCollection, wrapAssetTypeCursorPaginatedCollection } = entities.asset
8590
const { wrapAssetKey } = entities.assetKey
8691
const { wrapLocale, wrapLocaleCollection } = entities.locale
8792
const { wrapSnapshotCollection } = entities.snapshot
@@ -500,6 +505,44 @@ export default function createEnvironmentApi(makeRequest: MakeRequest) {
500505
},
501506
}).then((data) => wrapContentTypeCollection(makeRequest, data))
502507
},
508+
509+
/**
510+
* Gets a collection of Content Types with cursor based pagination
511+
* @param query - Object with cursor pagination parameters. Check the <a href="https://www.contentful.com/developers/docs/references/content-management-api/#/introduction/cursor-pagination">REST API reference</a> for more details.
512+
* @return Promise for a collection of Content Types
513+
* @example ```javascript
514+
* const contentful = require('contentful-management')
515+
*
516+
* const client = contentful.createClient({
517+
* accessToken: '<content_management_api_key>'
518+
* })
519+
*
520+
* client.getSpace('<space_id>')
521+
* .then((space) => space.getEnvironment('<environment-id>'))
522+
* .then((environment) => environment.getContentTypesWithCursor())
523+
* .then((response) => console.log(response.items))
524+
* .catch(console.error)
525+
* ```
526+
*/
527+
getContentTypesWithCursor(query: BasicCursorPaginationOptions = {}) {
528+
const raw = this.toPlainObject() as EnvironmentProps
529+
const normalizedQueryParams = normalizeCursorPaginationParameters(query)
530+
return makeRequest({
531+
entityType: 'ContentType',
532+
action: 'getMany',
533+
params: {
534+
spaceId: raw.sys.space.sys.id,
535+
environmentId: raw.sys.id,
536+
query: createRequestConfig({ query: normalizedQueryParams }).params,
537+
},
538+
}).then((data) =>
539+
wrapContentTypeCursorPaginatedCollection(
540+
makeRequest,
541+
normalizeCursorPaginationResponse(data),
542+
),
543+
)
544+
},
545+
503546
/**
504547
* Creates a Content Type
505548
* @param data - Object representation of the Content Type to be created
@@ -748,6 +791,45 @@ export default function createEnvironmentApi(makeRequest: MakeRequest) {
748791
}).then((data) => wrapEntryCollection(makeRequest, data))
749792
},
750793

794+
/**
795+
* Gets a collection of Entries with cursor based pagination
796+
* Warning: if you are using the select operator, when saving, any field that was not selected will be removed
797+
* from your entry in the backend
798+
* @param query - Object with cursor pagination parameters. Check the <a href="https://www.contentful.com/developers/docs/references/content-management-api/#/introduction/cursor-pagination">REST API reference</a> for more details.
799+
* @return Promise for a collection of Entries
800+
* @example ```javascript
801+
* const contentful = require('contentful-management')
802+
*
803+
* const client = contentful.createClient({
804+
* accessToken: '<content_management_api_key>'
805+
* })
806+
*
807+
* client.getSpace('<space_id>')
808+
* .then((space) => space.getEnvironment('<environment-id>'))
809+
* .then((environment) => environment.getEntriesWithCursor({'content_type': 'foo'})) // you can add more queries as 'key': 'value'
810+
* .then((response) => console.log(response.items))
811+
* .catch(console.error)
812+
* ```
813+
*/
814+
getEntriesWithCursor(query: BasicCursorPaginationOptions = {}) {
815+
const raw = this.toPlainObject() as EnvironmentProps
816+
const normalizedQueryParams = normalizeCursorPaginationParameters(query)
817+
return makeRequest({
818+
entityType: 'Entry',
819+
action: 'getMany',
820+
params: {
821+
spaceId: raw.sys.space.sys.id,
822+
environmentId: raw.sys.id,
823+
query: createRequestConfig({ query: normalizedQueryParams }).params,
824+
},
825+
}).then((data) =>
826+
wrapEntryTypeCursorPaginatedCollection(
827+
makeRequest,
828+
normalizeCursorPaginationResponse(data),
829+
),
830+
)
831+
},
832+
751833
/**
752834
* Gets a collection of published Entries
753835
* @param query - Object with search parameters. Check the <a href="https://www.contentful.com/developers/docs/javascript/tutorials/using-js-cda-sdk/#retrieving-entries-with-search-parameters">JS SDK tutorial</a> and the <a href="https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters">REST API reference</a> for more details.
@@ -963,6 +1045,46 @@ export default function createEnvironmentApi(makeRequest: MakeRequest) {
9631045
},
9641046
}).then((data) => wrapAssetCollection(makeRequest, data))
9651047
},
1048+
1049+
/**
1050+
* Gets a collection of Assets with cursor based pagination
1051+
* Warning: if you are using the select operator, when saving, any field that was not selected will be removed
1052+
* from your entry in the backend
1053+
* @param query - Object with cursor pagination parameters. Check the <a href="https://www.contentful.com/developers/docs/references/content-management-api/#/introduction/cursor-pagination">REST API reference</a> for more details.
1054+
* @return Promise for a collection of Assets
1055+
* @example ```javascript
1056+
* const contentful = require('contentful-management')
1057+
*
1058+
* const client = contentful.createClient({
1059+
* accessToken: '<content_management_api_key>'
1060+
* })
1061+
*
1062+
* client.getSpace('<space_id>')
1063+
* .then((space) => space.getEnvironment('<environment-id>'))
1064+
* .then((environment) => environment.getAssetsWithCursor())
1065+
* .then((response) => console.log(response.items))
1066+
* .catch(console.error)
1067+
* ```
1068+
*/
1069+
getAssetsWithCursor(query: BasicCursorPaginationOptions = {}) {
1070+
const raw = this.toPlainObject() as EnvironmentProps
1071+
const normalizedQueryParams = normalizeCursorPaginationParameters(query)
1072+
return makeRequest({
1073+
entityType: 'Asset',
1074+
action: 'getMany',
1075+
params: {
1076+
spaceId: raw.sys.space.sys.id,
1077+
environmentId: raw.sys.id,
1078+
query: createRequestConfig({ query: normalizedQueryParams }).params,
1079+
},
1080+
}).then((data) =>
1081+
wrapAssetTypeCursorPaginatedCollection(
1082+
makeRequest,
1083+
normalizeCursorPaginationResponse(data),
1084+
),
1085+
)
1086+
},
1087+
9661088
/**
9671089
* Gets a collection of published Assets
9681090
* @param query - Object with search parameters. Check the <a href="https://www.contentful.com/developers/docs/javascript/tutorials/using-js-cda-sdk/#retrieving-entries-with-search-parameters">JS SDK tutorial</a> and the <a href="https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters">REST API reference</a> for more details.
@@ -993,6 +1115,7 @@ export default function createEnvironmentApi(makeRequest: MakeRequest) {
9931115
},
9941116
}).then((data) => wrapAssetCollection(makeRequest, data))
9951117
},
1118+
9961119
/**
9971120
* Creates a Asset. After creation, call asset.processForLocale or asset.processForAllLocales to start asset processing.
9981121
* @param data - Object representation of the Asset to be created. Note that the field object should have an upload property on asset creation, which will be removed and replaced with an url property when processing is finished.

lib/entities/asset.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ import type {
88
EntityMetaSysProps,
99
MetadataProps,
1010
MakeRequest,
11+
CursorPaginatedCollectionProp,
1112
} from '../common-types'
12-
import { wrapCollection } from '../common-utils'
13+
import { wrapCollection, wrapCursorPaginatedCollection } from '../common-utils'
1314
import * as checks from '../plain/checks'
1415

1516
export type AssetProps<S = {}> = {
@@ -410,3 +411,8 @@ export function wrapAsset(makeRequest: MakeRequest, data: AssetProps): Asset {
410411
* @private
411412
*/
412413
export const wrapAssetCollection = wrapCollection(wrapAsset)
414+
415+
/**
416+
* @private
417+
*/
418+
export const wrapAssetTypeCursorPaginatedCollection = wrapCursorPaginatedCollection(wrapAsset)

0 commit comments

Comments
 (0)