Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
- [Configuration](#configuration)
- [Reference Documentation](#reference-documentation)
- [Contentful Javascript resources](#contentful-javascript-resources)
- [Cursor Based Pagination](#cursor-based-pagination)
- [REST API reference](#rest-api-reference)
- [Versioning](#versioning)
- [Reach out to us](#reach-out-to-us)
Expand Down Expand Up @@ -226,6 +227,17 @@ The benefits of using the "plain" version of the client, over the legacy version
- The ability to scope CMA client instance to a specific `spaceId`, `environmentId`, and `organizationId` when initializing the client.
- You can pass a concrete values to `defaults` and omit specifying these params in actual CMA methods calls.

## Cursor Based Pagination

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also add the title in TOC?


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`

```js
const response = await environment.getEntriesWithCursor({ limit: 10 });
console.log(response.items); // Array of items
console.log(response.pages?.next); // Cursor for next page
```
Use the value from `response.pages.next` to fetch the next page.

## Legacy Client Interface

The following code snippet is an example of the legacy client interface, which reads and writes data as a sequence of nested requests:
Expand Down
19 changes: 19 additions & 0 deletions lib/common-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@
select?: string
links_to_entry?: string

[key: string]: any

Check warning on line 250 in lib/common-types.ts

View workflow job for this annotation

GitHub Actions / check / lint

Unexpected any. Specify a different type
}

export interface SpaceQueryOptions extends PaginationQueryOptions {
Expand Down Expand Up @@ -359,6 +359,7 @@
}

export interface BasicCursorPaginationOptions extends Omit<BasicQueryOptions, 'skip'> {
skip?: never
pageNext?: string
pagePrev?: string
}
Expand Down Expand Up @@ -487,6 +488,7 @@
): MRReturn<'AppInstallation', 'getForOrganization'>

(opts: MROpts<'Asset', 'getMany', UA>): MRReturn<'Asset', 'getMany'>
(opts: MROpts<'Asset', 'getManyWithCursor', UA>): MRReturn<'Asset', 'getManyWithCursor'>
(opts: MROpts<'Asset', 'getPublished', UA>): MRReturn<'Asset', 'getPublished'>
(opts: MROpts<'Asset', 'get', UA>): MRReturn<'Asset', 'get'>
(opts: MROpts<'Asset', 'update', UA>): MRReturn<'Asset', 'update'>
Expand Down Expand Up @@ -567,6 +569,9 @@

(opts: MROpts<'ContentType', 'get', UA>): MRReturn<'ContentType', 'get'>
(opts: MROpts<'ContentType', 'getMany', UA>): MRReturn<'ContentType', 'getMany'>
(
opts: MROpts<'ContentType', 'getManyWithCursor', UA>,
): MRReturn<'ContentType', 'getManyWithCursor'>
(opts: MROpts<'ContentType', 'update', UA>): MRReturn<'ContentType', 'update'>
(opts: MROpts<'ContentType', 'create', UA>): MRReturn<'ContentType', 'create'>
(opts: MROpts<'ContentType', 'createWithId', UA>): MRReturn<'ContentType', 'createWithId'>
Expand Down Expand Up @@ -616,6 +621,7 @@
): MRReturn<'EnvironmentTemplateInstallation', 'getForEnvironment'>

(opts: MROpts<'Entry', 'getMany', UA>): MRReturn<'Entry', 'getMany'>
(opts: MROpts<'Entry', 'getManyWithCursor', UA>): MRReturn<'Entry', 'getManyWithCursor'>
(opts: MROpts<'Entry', 'getPublished', UA>): MRReturn<'Entry', 'getPublished'>
(opts: MROpts<'Entry', 'get', UA>): MRReturn<'Entry', 'get'>
(opts: MROpts<'Entry', 'patch', UA>): MRReturn<'Entry', 'patch'>
Expand Down Expand Up @@ -1234,6 +1240,11 @@
headers?: RawAxiosRequestHeaders
return: CollectionProp<AssetProps>
}
getManyWithCursor: {
params: GetSpaceEnvironmentParams & CursorBasedParams & { releaseId?: string }
headers?: RawAxiosRequestHeaders
return: CursorPaginatedCollectionProp<AssetProps>
}
get: {
params: GetSpaceEnvironmentParams & { assetId: string; releaseId?: string } & QueryParams
headers?: RawAxiosRequestHeaders
Expand Down Expand Up @@ -1482,6 +1493,10 @@
params: GetSpaceEnvironmentParams & QueryParams
return: CollectionProp<ContentTypeProps>
}
getManyWithCursor: {
params: GetSpaceEnvironmentParams & CursorBasedParams
return: CursorPaginatedCollectionProp<ContentTypeProps>
}
create: {
params: GetSpaceEnvironmentParams
payload: CreateContentTypeProps
Expand Down Expand Up @@ -1650,6 +1665,10 @@
params: GetSpaceEnvironmentParams & QueryParams & { releaseId?: string }
return: CollectionProp<EntryProps<any>>
}
getManyWithCursor: {
params: GetSpaceEnvironmentParams & CursorBasedParams & { releaseId?: string }
return: CursorPaginatedCollectionProp<EntryProps<any>>
}
get: {
params: GetSpaceEnvironmentParams & { entryId: string; releaseId?: string } & QueryParams
return: EntryProps<any>
Expand Down
49 changes: 49 additions & 0 deletions lib/common-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
import { toPlainObject } from 'contentful-sdk-core'
import copy from 'fast-copy'
import type {
BasicCursorPaginationOptions,
Collection,
CollectionProp,
CursorBasedParams,
CursorPaginatedCollection,
CursorPaginatedCollectionProp,
MakeRequest,
Expand All @@ -14,7 +16,7 @@
* @private
*/
export const wrapCollection =
<R, T, Rest extends any[]>(fn: (makeRequest: MakeRequest, entity: T, ...rest: Rest) => R) =>

Check warning on line 19 in lib/common-utils.ts

View workflow job for this annotation

GitHub Actions / check / lint

Unexpected any. Specify a different type
(makeRequest: MakeRequest, data: CollectionProp<T>, ...rest: Rest): Collection<R, T> => {
const collectionData = toPlainObject(copy(data))
// @ts-expect-error
Expand All @@ -24,7 +26,7 @@
}

export const wrapCursorPaginatedCollection =
<R, T, Rest extends any[]>(fn: (makeRequest: MakeRequest, entity: T, ...rest: Rest) => R) =>

Check warning on line 29 in lib/common-utils.ts

View workflow job for this annotation

GitHub Actions / check / lint

Unexpected any. Specify a different type
(
makeRequest: MakeRequest,
data: CursorPaginatedCollectionProp<T>,
Expand All @@ -47,3 +49,50 @@
export async function waitFor(ms = 1000) {
return new Promise((resolve) => setTimeout(resolve, ms))
}

export function normalizeCursorPaginationParameters(
query: BasicCursorPaginationOptions,
): CursorBasedParams {
const { pagePrev, pageNext, ...rest } = query

return {
...rest,
cursor: true,
// omit pagePrev and pageNext if the value is falsy
...(pagePrev ? { pagePrev } : null),
...(pageNext ? { pageNext } : null),
} as CursorBasedParams
}

function extractQueryParam(key: string, url?: string): string | undefined {
if (!url) return

const queryIndex = url.indexOf('?')
if (queryIndex === -1) return

const queryString = url.slice(queryIndex + 1)
return new URLSearchParams(queryString).get(key) ?? undefined
Comment on lines +70 to +74

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const queryIndex = url.indexOf('?')
if (queryIndex === -1) return
const queryString = url.slice(queryIndex + 1)
return new URLSearchParams(queryString).get(key) ?? undefined
const parsedURL = new URL(url)
return parsedURL.searchParams.get(key) ?? undefined

Copy link
Author

@ebefarooqui ebefarooqui Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The url we are parsing from the response is a relative url

pagePrev: "/spaces/segpl12szpe6/environments/master/assets?pageNext=SMK3D8KEZk..."

so we couldn't use URL here unless we mocked an appropriate base domain. Thoughts?

}

const Pages = {
prev: 'pagePrev',
next: 'pageNext',
} as const

const PAGE_KEYS = ['prev', 'next'] as const

export function normalizeCursorPaginationResponse<T>(
data: CursorPaginatedCollectionProp<T>,
): CursorPaginatedCollectionProp<T> {
const pages: { prev?: string; next?: string } = {}

for (const key of PAGE_KEYS) {
const token = extractQueryParam(Pages[key], data.pages?.[key])
if (token) pages[key] = token
}

return {
...data,
pages,
}
}
151 changes: 137 additions & 14 deletions lib/create-environment-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
CursorBasedParams,
QueryOptions,
} from './common-types'
import {
normalizeCursorPaginationParameters,
normalizeCursorPaginationResponse,
} from './common-utils'
import type { BasicQueryOptions, MakeRequest } from './common-types'
import entities from './entities'
import type { CreateAppInstallationProps } from './entities/app-installation'
Expand All @@ -15,11 +19,11 @@
CreateAppActionCallProps,
AppActionCallRawResponseProps,
} from './entities/app-action-call'
import type {
AssetFileProp,
AssetProps,
CreateAssetFromFilesOptions,
CreateAssetProps,
import {
type AssetFileProp,
type AssetProps,
type CreateAssetFromFilesOptions,
type CreateAssetProps,
} from './entities/asset'
import type { CreateAssetKeyProps } from './entities/asset-key'
import type {
Expand All @@ -40,12 +44,12 @@
} from './entities/release'
import { wrapRelease, wrapReleaseCollection } from './entities/release'

import type { ContentTypeProps, CreateContentTypeProps } from './entities/content-type'
import type {
CreateEntryProps,
EntryProps,
EntryReferenceOptionsProps,
EntryReferenceProps,
import { type ContentTypeProps, type CreateContentTypeProps } from './entities/content-type'
import {
type CreateEntryProps,
type EntryProps,
type EntryReferenceOptionsProps,
type EntryReferenceProps,
} from './entities/entry'
import type { EnvironmentProps } from './entities/environment'
import type { CreateExtensionProps } from './entities/extension'
Expand Down Expand Up @@ -75,9 +79,10 @@
*/
export default function createEnvironmentApi(makeRequest: MakeRequest) {
const { wrapEnvironment } = entities.environment
const { wrapContentType, wrapContentTypeCollection } = entities.contentType
const { wrapEntry, wrapEntryCollection } = entities.entry
const { wrapAsset, wrapAssetCollection } = entities.asset
const { wrapContentType, wrapContentTypeCollection, wrapContentTypeCursorPaginatedCollection } =
entities.contentType
const { wrapEntry, wrapEntryCollection, wrapEntryTypeCursorPaginatedCollection } = entities.entry
const { wrapAsset, wrapAssetCollection, wrapAssetTypeCursorPaginatedCollection } = entities.asset
const { wrapAssetKey } = entities.assetKey
const { wrapLocale, wrapLocaleCollection } = entities.locale
const { wrapSnapshotCollection } = entities.snapshot
Expand Down Expand Up @@ -238,7 +243,7 @@
* .then((bulkAction) => console.log(bulkAction))
* ```
*/
getBulkAction<T extends BulkActionPayload = any>(bulkActionId: string): Promise<BulkAction<T>> {

Check warning on line 246 in lib/create-environment-api.ts

View workflow job for this annotation

GitHub Actions / check / lint

Unexpected any. Specify a different type
const raw: EnvironmentProps = this.toPlainObject()

return makeRequest({
Expand Down Expand Up @@ -492,6 +497,44 @@
},
}).then((data) => wrapContentTypeCollection(makeRequest, data))
},

/**
* Gets a collection of Content Types with cursor based pagination
* @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.
* @return Promise for a collection of Content Types
* @example ```javascript
* const contentful = require('contentful-management')
*
* const client = contentful.createClient({
* accessToken: '<content_management_api_key>'
* })
*
* client.getSpace('<space_id>')
* .then((space) => space.getEnvironment('<environment-id>'))
* .then((environment) => environment.getContentTypesWithCursor())
* .then((response) => console.log(response.items))
* .catch(console.error)
* ```
*/
getContentTypesWithCursor(query: BasicCursorPaginationOptions = {}) {
const raw = this.toPlainObject() as EnvironmentProps
const normalizedQueryParams = normalizeCursorPaginationParameters(query)
return makeRequest({
entityType: 'ContentType',
action: 'getMany',
params: {
spaceId: raw.sys.space.sys.id,
environmentId: raw.sys.id,
query: createRequestConfig({ query: normalizedQueryParams }).params,
},
}).then((data) =>
wrapContentTypeCursorPaginatedCollection(
makeRequest,
normalizeCursorPaginationResponse(data),
),
)
},

/**
* Creates a Content Type
* @param data - Object representation of the Content Type to be created
Expand Down Expand Up @@ -740,6 +783,45 @@
}).then((data) => wrapEntryCollection(makeRequest, data))
},

/**
* Gets a collection of Entries with cursor based pagination
* Warning: if you are using the select operator, when saving, any field that was not selected will be removed
* from your entry in the backend
* @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.
* @return Promise for a collection of Entries
* @example ```javascript
* const contentful = require('contentful-management')
*
* const client = contentful.createClient({
* accessToken: '<content_management_api_key>'
* })
*
* client.getSpace('<space_id>')
* .then((space) => space.getEnvironment('<environment-id>'))
* .then((environment) => environment.getEntriesWithCursor({'content_type': 'foo'})) // you can add more queries as 'key': 'value'
* .then((response) => console.log(response.items))
* .catch(console.error)
* ```
*/
getEntriesWithCursor(query: BasicCursorPaginationOptions = {}) {
const raw = this.toPlainObject() as EnvironmentProps
const normalizedQueryParams = normalizeCursorPaginationParameters(query)
return makeRequest({
entityType: 'Entry',
action: 'getMany',
params: {
spaceId: raw.sys.space.sys.id,
environmentId: raw.sys.id,
query: createRequestConfig({ query: normalizedQueryParams }).params,
},
}).then((data) =>
wrapEntryTypeCursorPaginatedCollection(
makeRequest,
normalizeCursorPaginationResponse(data),
),
)
},

/**
* Gets a collection of published Entries
* @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.
Expand Down Expand Up @@ -955,6 +1037,46 @@
},
}).then((data) => wrapAssetCollection(makeRequest, data))
},

/**
* Gets a collection of Assets with cursor based pagination
* Warning: if you are using the select operator, when saving, any field that was not selected will be removed
* from your entry in the backend
* @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.
* @return Promise for a collection of Assets
* @example ```javascript
* const contentful = require('contentful-management')
*
* const client = contentful.createClient({
* accessToken: '<content_management_api_key>'
* })
*
* client.getSpace('<space_id>')
* .then((space) => space.getEnvironment('<environment-id>'))
* .then((environment) => environment.getAssetsWithCursor())
* .then((response) => console.log(response.items))
* .catch(console.error)
* ```
*/
getAssetsWithCursor(query: BasicCursorPaginationOptions = {}) {
const raw = this.toPlainObject() as EnvironmentProps
const normalizedQueryParams = normalizeCursorPaginationParameters(query)
return makeRequest({
entityType: 'Asset',
action: 'getMany',
params: {
spaceId: raw.sys.space.sys.id,
environmentId: raw.sys.id,
query: createRequestConfig({ query: normalizedQueryParams }).params,
},
}).then((data) =>
wrapAssetTypeCursorPaginatedCollection(
makeRequest,
normalizeCursorPaginationResponse(data),
),
)
},

/**
* Gets a collection of published Assets
* @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.
Expand Down Expand Up @@ -985,6 +1107,7 @@
},
}).then((data) => wrapAssetCollection(makeRequest, data))
},

/**
* Creates a Asset. After creation, call asset.processForLocale or asset.processForAllLocales to start asset processing.
* @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.
Expand Down
11 changes: 10 additions & 1 deletion lib/entities/asset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import type {
EntityMetaSysProps,
MetadataProps,
MakeRequest,
CursorPaginatedCollectionProp,
} from '../common-types'
import { wrapCollection } from '../common-utils'
import { wrapCollection, wrapCursorPaginatedCollection } from '../common-utils'
import * as checks from '../plain/checks'

export type AssetProps<S = {}> = {
Expand Down Expand Up @@ -410,3 +411,11 @@ export function wrapAsset(makeRequest: MakeRequest, data: AssetProps): Asset {
* @private
*/
export const wrapAssetCollection = wrapCollection(wrapAsset)

/**
* @private
*/
export const wrapAssetTypeCursorPaginatedCollection: (
makeRequest: MakeRequest,
data: CursorPaginatedCollectionProp<AssetProps>,
) => CursorPaginatedCollectionProp<AssetProps> = wrapCursorPaginatedCollection(wrapAsset)
Loading
Loading