From cd8d735a47bdcef465e2c79f6cb19a8076ae8c52 Mon Sep 17 00:00:00 2001 From: Ebrahem Farooqui Date: Wed, 19 Nov 2025 14:46:08 +0100 Subject: [PATCH 01/12] feat: add dedicated getManyWithCursor and getPublishedWithCursor methods for entry, asset, and content-type [CAPI-2357] Adds getManyWithCursor as a dedicated cursor based method for entry, asset, and content-type and getPublishedWithCursor for only the asset and entry entities --- lib/common-types.ts | 27 ++++++++++ lib/create-environment-api.ts | 92 ++++++++++++++++++++++++++++++----- lib/entities/asset.ts | 11 ++++- lib/entities/content-type.ts | 14 +++++- lib/entities/entry.ts | 11 ++++- lib/plain/common-types.ts | 23 +++++++++ lib/plain/plain-client.ts | 5 ++ 7 files changed, 169 insertions(+), 14 deletions(-) diff --git a/lib/common-types.ts b/lib/common-types.ts index 76b6b96261..fac0757873 100644 --- a/lib/common-types.ts +++ b/lib/common-types.ts @@ -487,7 +487,9 @@ type MRInternal = { ): 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', 'getPublishedWithCursor', UA>): MRReturn<'Asset', 'getPublishedWithCursor'> (opts: MROpts<'Asset', 'get', UA>): MRReturn<'Asset', 'get'> (opts: MROpts<'Asset', 'update', UA>): MRReturn<'Asset', 'update'> (opts: MROpts<'Asset', 'delete', UA>): MRReturn<'Asset', 'delete'> @@ -567,6 +569,7 @@ type MRInternal = { (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'> @@ -616,7 +619,9 @@ type MRInternal = { ): 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', 'getPublishedWithCursor', UA>): MRReturn<'Entry', 'getPublishedWithCursor'> (opts: MROpts<'Entry', 'get', UA>): MRReturn<'Entry', 'get'> (opts: MROpts<'Entry', 'patch', UA>): MRReturn<'Entry', 'patch'> (opts: MROpts<'Entry', 'update', UA>): MRReturn<'Entry', 'update'> @@ -1229,11 +1234,21 @@ export type MRActions = { headers?: RawAxiosRequestHeaders return: CollectionProp } + getPublishedWithCursor: { + params: GetSpaceEnvironmentParams & QueryParams + headers?: RawAxiosRequestHeaders + return: CursorPaginatedCollectionProp + } getMany: { params: GetSpaceEnvironmentParams & QueryParams & { releaseId?: string } headers?: RawAxiosRequestHeaders return: CollectionProp } + getManyWithCursor: { + params: GetSpaceEnvironmentParams & QueryParams & { releaseId?: string } + headers?: RawAxiosRequestHeaders + return: CursorPaginatedCollectionProp + } get: { params: GetSpaceEnvironmentParams & { assetId: string; releaseId?: string } & QueryParams headers?: RawAxiosRequestHeaders @@ -1482,6 +1497,10 @@ export type MRActions = { params: GetSpaceEnvironmentParams & QueryParams return: CollectionProp } + getManyWithCursor: { + params: GetSpaceEnvironmentParams & QueryParams + return: CursorPaginatedCollectionProp + } create: { params: GetSpaceEnvironmentParams payload: CreateContentTypeProps @@ -1646,10 +1665,18 @@ export type MRActions = { params: GetSpaceEnvironmentParams & QueryParams return: CollectionProp> } + getPublishedWithCursor: { + params: GetSpaceEnvironmentParams & QueryParams + return: CursorPaginatedCollectionProp> + } getMany: { params: GetSpaceEnvironmentParams & QueryParams & { releaseId?: string } return: CollectionProp> } + getManyWithCursor: { + params: GetSpaceEnvironmentParams & QueryParams & { releaseId?: string } + return: CursorPaginatedCollectionProp> + } get: { params: GetSpaceEnvironmentParams & { entryId: string; releaseId?: string } & QueryParams return: EntryProps diff --git a/lib/create-environment-api.ts b/lib/create-environment-api.ts index fb20b81f9f..fee7eed355 100644 --- a/lib/create-environment-api.ts +++ b/lib/create-environment-api.ts @@ -15,11 +15,12 @@ import type { CreateAppActionCallProps, AppActionCallRawResponseProps, } from './entities/app-action-call' -import type { - AssetFileProp, - AssetProps, - CreateAssetFromFilesOptions, - CreateAssetProps, +import { + wrapAssetTypeCursorPaginatedCollection, + type AssetFileProp, + type AssetProps, + type CreateAssetFromFilesOptions, + type CreateAssetProps, } from './entities/asset' import type { CreateAssetKeyProps } from './entities/asset-key' import type { @@ -40,12 +41,13 @@ import type { } from './entities/release' import { wrapRelease, wrapReleaseCollection } from './entities/release' -import type { ContentTypeProps, CreateContentTypeProps } from './entities/content-type' -import type { - CreateEntryProps, - EntryProps, - EntryReferenceOptionsProps, - EntryReferenceProps, +import { wrapContentTypeCursorPaginatedCollection, type ContentTypeProps, type CreateContentTypeProps } from './entities/content-type' +import { + wrapEntryTypeCursorPaginatedCollection, + type CreateEntryProps, + type EntryProps, + type EntryReferenceOptionsProps, + type EntryReferenceProps, } from './entities/entry' import type { EnvironmentProps } from './entities/environment' import type { CreateExtensionProps } from './entities/extension' @@ -492,6 +494,20 @@ export default function createEnvironmentApi(makeRequest: MakeRequest) { }, }).then((data) => wrapContentTypeCollection(makeRequest, data)) }, + + getContentTypesWithCursor(query: QueryOptions = {}) { + const raw = this.toPlainObject() as EnvironmentProps + return makeRequest({ + entityType: 'ContentType', + action: 'getManyWithCursor', + params: { + spaceId: raw.sys.space.sys.id, + environmentId: raw.sys.id, + query: createRequestConfig({ query }).params, + }, + }).then((data) => wrapContentTypeCursorPaginatedCollection(makeRequest, data)) + }, + /** * Creates a Content Type * @param data - Object representation of the Content Type to be created @@ -740,6 +756,19 @@ export default function createEnvironmentApi(makeRequest: MakeRequest) { }).then((data) => wrapEntryCollection(makeRequest, data)) }, + getEntriesWithCursor(query: QueryOptions = {}) { + const raw = this.toPlainObject() as EnvironmentProps + return makeRequest({ + entityType: 'Entry', + action: 'getManyWithCursor', + params: { + spaceId: raw.sys.space.sys.id, + environmentId: raw.sys.id, + query: createRequestConfig({ query: query }).params, + }, + }).then((data) => wrapEntryTypeCursorPaginatedCollection(makeRequest, data)) + }, + /** * Gets a collection of published Entries * @param query - Object with search parameters. Check the JS SDK tutorial and the REST API reference for more details. @@ -771,6 +800,19 @@ export default function createEnvironmentApi(makeRequest: MakeRequest) { }).then((data) => wrapEntryCollection(makeRequest, data)) }, + getPublishedEntriesWithCursor(query: QueryOptions = {}) { + const raw = this.toPlainObject() as EnvironmentProps + return makeRequest({ + entityType: 'Entry', + action: 'getPublishedWithCursor', + params: { + spaceId: raw.sys.space.sys.id, + environmentId: raw.sys.id, + query: createRequestConfig({ query: query }).params, + }, + }).then((data) => wrapEntryTypeCursorPaginatedCollection(makeRequest, data)) + }, + /** * Creates a Entry * @param contentTypeId - The Content Type ID of the newly created Entry @@ -955,6 +997,20 @@ export default function createEnvironmentApi(makeRequest: MakeRequest) { }, }).then((data) => wrapAssetCollection(makeRequest, data)) }, + + getAssetsWithCursor(query: QueryOptions = {}) { + const raw = this.toPlainObject() as EnvironmentProps + return makeRequest({ + entityType: 'Asset', + action: 'getManyWithCursor', + params: { + spaceId: raw.sys.space.sys.id, + environmentId: raw.sys.id, + query: createRequestConfig({ query: query }).params, + }, + }).then((data) => wrapAssetTypeCursorPaginatedCollection(makeRequest, data)) + }, + /** * Gets a collection of published Assets * @param query - Object with search parameters. Check the JS SDK tutorial and the REST API reference for more details. @@ -985,6 +1041,20 @@ export default function createEnvironmentApi(makeRequest: MakeRequest) { }, }).then((data) => wrapAssetCollection(makeRequest, data)) }, + + getPublishedAssetsWithCursor(query: QueryOptions = {}) { + const raw = this.toPlainObject() as EnvironmentProps + return makeRequest({ + entityType: 'Asset', + action: 'getPublishedWithCursor', + params: { + spaceId: raw.sys.space.sys.id, + environmentId: raw.sys.id, + query: createRequestConfig({ query: query }).params, + }, + }).then((data) => wrapAssetTypeCursorPaginatedCollection(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. diff --git a/lib/entities/asset.ts b/lib/entities/asset.ts index 332b2ebf87..5fa5f4a590 100644 --- a/lib/entities/asset.ts +++ b/lib/entities/asset.ts @@ -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 = { @@ -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, +) => CursorPaginatedCollectionProp = wrapCursorPaginatedCollection(wrapAsset) diff --git a/lib/entities/content-type.ts b/lib/entities/content-type.ts index cb8d3e40bc..7f4bd3f8fe 100644 --- a/lib/entities/content-type.ts +++ b/lib/entities/content-type.ts @@ -4,13 +4,14 @@ import type { Except, RequireAtLeastOne, SetOptional } from 'type-fest' import type { BasicMetaSysProps, Collection, + CursorPaginatedCollectionProp, DefaultElements, Link, MakeRequest, QueryOptions, SysLink, } from '../common-types' -import { wrapCollection } from '../common-utils' +import { wrapCollection, wrapCursorPaginatedCollection } from '../common-utils' import enhanceWithMethods from '../enhance-with-methods' import { isDraft, isPublished, isUpdated } from '../plain/checks' import type { ContentFields } from './content-type-fields' @@ -359,3 +360,14 @@ export function wrapContentType(makeRequest: MakeRequest, data: ContentTypeProps * @private */ export const wrapContentTypeCollection = wrapCollection(wrapContentType) + +/** + * @private + */ +export const wrapContentTypeCursorPaginatedCollection: ( + makeRequest: MakeRequest, + data: CursorPaginatedCollectionProp, +) => CursorPaginatedCollectionProp = wrapCursorPaginatedCollection( + wrapContentType +) + diff --git a/lib/entities/entry.ts b/lib/entities/entry.ts index afc0bb4736..45da1a7e1f 100644 --- a/lib/entities/entry.ts +++ b/lib/entities/entry.ts @@ -2,13 +2,14 @@ import { freezeSys, toPlainObject } from 'contentful-sdk-core' import copy from 'fast-copy' import type { CollectionProp, + CursorPaginatedCollectionProp, DefaultElements, EntryMetaSysProps, KeyValueMap, MakeRequest, MetadataProps, } from '../common-types' -import { wrapCollection } from '../common-utils' +import { wrapCollection, wrapCursorPaginatedCollection } from '../common-utils' import type { ContentfulEntryApi } from '../create-entry-api' import createEntryApi from '../create-entry-api' import enhanceWithMethods from '../enhance-with-methods' @@ -71,3 +72,11 @@ export function wrapEntry(makeRequest: MakeRequest, data: EntryProps): Entry { * @private */ export const wrapEntryCollection = wrapCollection(wrapEntry) + +/** + * @private + */ +export const wrapEntryTypeCursorPaginatedCollection: ( + makeRequest: MakeRequest, + data: CursorPaginatedCollectionProp, +) => CursorPaginatedCollectionProp = wrapCursorPaginatedCollection(wrapEntry) diff --git a/lib/plain/common-types.ts b/lib/plain/common-types.ts index 1a98cf55d5..a55991d56b 100644 --- a/lib/plain/common-types.ts +++ b/lib/plain/common-types.ts @@ -274,6 +274,9 @@ export type PlainClientAPI = { getMany( params: OptionalDefaults, ): Promise> + getManyWithCursor( + params: OptionalDefaults, + ): Promise> update( params: OptionalDefaults, rawData: ContentTypeProps, @@ -306,11 +309,21 @@ export type PlainClientAPI = { rawData?: unknown, headers?: RawAxiosRequestHeaders, ): Promise>> + getPublishedWithCursor( + params: OptionalDefaults, + rawData?: unknown, + headers?: RawAxiosRequestHeaders, + ): Promise>> getMany( params: OptionalDefaults, rawData?: unknown, headers?: RawAxiosRequestHeaders, ): Promise>> + getManyWithCursor( + params: OptionalDefaults, + rawData?: unknown, + headers?: RawAxiosRequestHeaders, + ): Promise>> get( params: OptionalDefaults, rawData?: unknown, @@ -370,11 +383,21 @@ export type PlainClientAPI = { rawData?: unknown, headers?: RawAxiosRequestHeaders, ): Promise> + getPublishedWithCursor( + params: OptionalDefaults, + rawData?: unknown, + headers?: RawAxiosRequestHeaders, + ): Promise> getMany( params: OptionalDefaults, rawData?: unknown, headers?: RawAxiosRequestHeaders, ): Promise> + getManyWithCursor( + params: OptionalDefaults, + rawData?: unknown, + headers?: RawAxiosRequestHeaders, + ): Promise> get( params: OptionalDefaults< GetSpaceEnvironmentParams & { assetId: string; releaseId?: string } & QueryParams diff --git a/lib/plain/plain-client.ts b/lib/plain/plain-client.ts index d2ff42ec53..7a61d1a0c9 100644 --- a/lib/plain/plain-client.ts +++ b/lib/plain/plain-client.ts @@ -217,6 +217,7 @@ export const createPlainClient = ( contentType: { get: wrap(wrapParams, 'ContentType', 'get'), getMany: wrap(wrapParams, 'ContentType', 'getMany'), + getManyWithCursor: wrap(wrapParams, 'ContentType', 'getManyWithCursor'), update: wrap(wrapParams, 'ContentType', 'update'), delete: wrap(wrapParams, 'ContentType', 'delete'), publish: wrap(wrapParams, 'ContentType', 'publish'), @@ -246,7 +247,9 @@ export const createPlainClient = ( }, entry: { getPublished: wrap(wrapParams, 'Entry', 'getPublished'), + getPublishedWithCursor: wrap(wrapParams, 'Entry', 'getPublishedWithCursor'), getMany: wrap(wrapParams, 'Entry', 'getMany'), + getManyWithCursor: wrap(wrapParams, 'Entry', 'getManyWithCursor'), get: wrap(wrapParams, 'Entry', 'get'), update: wrap(wrapParams, 'Entry', 'update'), patch: wrap(wrapParams, 'Entry', 'patch'), @@ -261,7 +264,9 @@ export const createPlainClient = ( }, asset: { getPublished: wrap(wrapParams, 'Asset', 'getPublished'), + getPublishedWithCursor: wrap(wrapParams, 'Asset', 'getPublishedWithCursor'), getMany: wrap(wrapParams, 'Asset', 'getMany'), + getManyWithCursor: wrap(wrapParams, 'Asset', 'getManyWithCursor'), get: wrap(wrapParams, 'Asset', 'get'), update: wrap(wrapParams, 'Asset', 'update'), delete: wrap(wrapParams, 'Asset', 'delete'), From 263f23a70fc78d64f5f32ab0feabc17bf9f7d443 Mon Sep 17 00:00:00 2001 From: Ebrahem Farooqui Date: Fri, 21 Nov 2025 10:51:31 +0100 Subject: [PATCH 02/12] fix: removing getPublishedWithCursor methods, normalizing pagination response and params and building utilities to do so [CAPI-2357] * fix: getPublished does not currently support cursor based pagination, removing relevant methods for entities * fix: normalize pagination params to filter prevPage and prevNext if falsey * fix: normalize pagination response to parse next, prev tokens if present --- lib/adapters/REST/make-request.ts | 2 + lib/common-types.ts | 17 +--- lib/common-utils.ts | 49 ++++++++++++ lib/create-environment-api.ts | 124 +++++++++++++++++++----------- lib/plain/common-types.ts | 10 --- lib/plain/plain-client.ts | 2 - 6 files changed, 134 insertions(+), 70 deletions(-) diff --git a/lib/adapters/REST/make-request.ts b/lib/adapters/REST/make-request.ts index 6e6d599ed1..33a6eb038c 100644 --- a/lib/adapters/REST/make-request.ts +++ b/lib/adapters/REST/make-request.ts @@ -29,6 +29,8 @@ export const makeRequest = async ({ // @ts-ignore endpoints[entityType]?.[action] + console.debug(endpoint) + if (endpoint === undefined) { throw new Error('Unknown endpoint') } diff --git a/lib/common-types.ts b/lib/common-types.ts index fac0757873..0619666f56 100644 --- a/lib/common-types.ts +++ b/lib/common-types.ts @@ -489,7 +489,6 @@ type MRInternal = { (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', 'getPublishedWithCursor', UA>): MRReturn<'Asset', 'getPublishedWithCursor'> (opts: MROpts<'Asset', 'get', UA>): MRReturn<'Asset', 'get'> (opts: MROpts<'Asset', 'update', UA>): MRReturn<'Asset', 'update'> (opts: MROpts<'Asset', 'delete', UA>): MRReturn<'Asset', 'delete'> @@ -621,7 +620,6 @@ type MRInternal = { (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', 'getPublishedWithCursor', UA>): MRReturn<'Entry', 'getPublishedWithCursor'> (opts: MROpts<'Entry', 'get', UA>): MRReturn<'Entry', 'get'> (opts: MROpts<'Entry', 'patch', UA>): MRReturn<'Entry', 'patch'> (opts: MROpts<'Entry', 'update', UA>): MRReturn<'Entry', 'update'> @@ -1234,18 +1232,13 @@ export type MRActions = { headers?: RawAxiosRequestHeaders return: CollectionProp } - getPublishedWithCursor: { - params: GetSpaceEnvironmentParams & QueryParams - headers?: RawAxiosRequestHeaders - return: CursorPaginatedCollectionProp - } getMany: { params: GetSpaceEnvironmentParams & QueryParams & { releaseId?: string } headers?: RawAxiosRequestHeaders return: CollectionProp } getManyWithCursor: { - params: GetSpaceEnvironmentParams & QueryParams & { releaseId?: string } + params: GetSpaceEnvironmentParams & CursorBasedParams & { releaseId?: string } headers?: RawAxiosRequestHeaders return: CursorPaginatedCollectionProp } @@ -1498,7 +1491,7 @@ export type MRActions = { return: CollectionProp } getManyWithCursor: { - params: GetSpaceEnvironmentParams & QueryParams + params: GetSpaceEnvironmentParams & CursorBasedParams return: CursorPaginatedCollectionProp } create: { @@ -1665,16 +1658,12 @@ export type MRActions = { params: GetSpaceEnvironmentParams & QueryParams return: CollectionProp> } - getPublishedWithCursor: { - params: GetSpaceEnvironmentParams & QueryParams - return: CursorPaginatedCollectionProp> - } getMany: { params: GetSpaceEnvironmentParams & QueryParams & { releaseId?: string } return: CollectionProp> } getManyWithCursor: { - params: GetSpaceEnvironmentParams & QueryParams & { releaseId?: string } + params: GetSpaceEnvironmentParams & CursorBasedParams & { releaseId?: string } return: CursorPaginatedCollectionProp> } get: { diff --git a/lib/common-utils.ts b/lib/common-utils.ts index 0da3ea5f8b..0babeab288 100644 --- a/lib/common-utils.ts +++ b/lib/common-utils.ts @@ -3,8 +3,10 @@ import { toPlainObject } from 'contentful-sdk-core' import copy from 'fast-copy' import type { + BasicCursorPaginationOptions, Collection, CollectionProp, + CursorBasedParams, CursorPaginatedCollection, CursorPaginatedCollectionProp, MakeRequest, @@ -47,3 +49,50 @@ export function shouldRePoll(statusCode: number) { 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 +} + +const Pages = { + prev: 'pagePrev', + next: 'pageNext', +} as const + +const PAGE_KEYS = ['prev', 'next'] as const + +export function normalizeCursorPaginationResponse( + data: CursorPaginatedCollectionProp +): CursorPaginatedCollectionProp { + 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, + ...(Object.keys(pages).length && { pages }), + } +} diff --git a/lib/create-environment-api.ts b/lib/create-environment-api.ts index fee7eed355..bd50a7576d 100644 --- a/lib/create-environment-api.ts +++ b/lib/create-environment-api.ts @@ -7,6 +7,7 @@ import type { 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' @@ -16,7 +17,6 @@ import type { AppActionCallRawResponseProps, } from './entities/app-action-call' import { - wrapAssetTypeCursorPaginatedCollection, type AssetFileProp, type AssetProps, type CreateAssetFromFilesOptions, @@ -41,9 +41,8 @@ import type { } from './entities/release' import { wrapRelease, wrapReleaseCollection } from './entities/release' -import { wrapContentTypeCursorPaginatedCollection, type ContentTypeProps, type CreateContentTypeProps } from './entities/content-type' +import { type ContentTypeProps, type CreateContentTypeProps } from './entities/content-type' import { - wrapEntryTypeCursorPaginatedCollection, type CreateEntryProps, type EntryProps, type EntryReferenceOptionsProps, @@ -77,9 +76,9 @@ export type ContentfulEnvironmentAPI = ReturnType */ 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 @@ -495,17 +494,36 @@ export default function createEnvironmentApi(makeRequest: MakeRequest) { }).then((data) => wrapContentTypeCollection(makeRequest, data)) }, - getContentTypesWithCursor(query: QueryOptions = {}) { + /** + * Gets a collection of Content Types with cursor based pagination + * @param query - Object with search parameters. Check the JS SDK tutorial and the REST API reference for more details. + * @return Promise for a collection of Content Types + * @example ```javascript + * const contentful = require('contentful-management') + * + * const client = contentful.createClient({ + * accessToken: '' + * }) + * + * client.getSpace('') + * .then((space) => space.getEnvironment('')) + * .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: 'getManyWithCursor', + action: 'getMany', params: { spaceId: raw.sys.space.sys.id, environmentId: raw.sys.id, - query: createRequestConfig({ query }).params, + query: createRequestConfig({ query: normalizedQueryParams }).params, }, - }).then((data) => wrapContentTypeCursorPaginatedCollection(makeRequest, data)) + }).then((data) => wrapContentTypeCursorPaginatedCollection(makeRequest, normalizeCursorPaginationResponse(data))) }, /** @@ -756,17 +774,38 @@ export default function createEnvironmentApi(makeRequest: MakeRequest) { }).then((data) => wrapEntryCollection(makeRequest, data)) }, - getEntriesWithCursor(query: QueryOptions = {}) { + /** + * 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 JS SDK tutorial and the REST API reference for more details. + * @return Promise for a collection of Entries + * @example ```javascript + * const contentful = require('contentful-management') + * + * const client = contentful.createClient({ + * accessToken: '' + * }) + * + * client.getSpace('') + * .then((space) => space.getEnvironment('')) + * .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: 'getManyWithCursor', + action: 'getMany', params: { spaceId: raw.sys.space.sys.id, environmentId: raw.sys.id, - query: createRequestConfig({ query: query }).params, + query: createRequestConfig({ query: normalizedQueryParams }).params, }, - }).then((data) => wrapEntryTypeCursorPaginatedCollection(makeRequest, data)) + }).then((data) => wrapEntryTypeCursorPaginatedCollection(makeRequest, normalizeCursorPaginationResponse(data))) }, /** @@ -800,19 +839,6 @@ export default function createEnvironmentApi(makeRequest: MakeRequest) { }).then((data) => wrapEntryCollection(makeRequest, data)) }, - getPublishedEntriesWithCursor(query: QueryOptions = {}) { - const raw = this.toPlainObject() as EnvironmentProps - return makeRequest({ - entityType: 'Entry', - action: 'getPublishedWithCursor', - params: { - spaceId: raw.sys.space.sys.id, - environmentId: raw.sys.id, - query: createRequestConfig({ query: query }).params, - }, - }).then((data) => wrapEntryTypeCursorPaginatedCollection(makeRequest, data)) - }, - /** * Creates a Entry * @param contentTypeId - The Content Type ID of the newly created Entry @@ -998,17 +1024,40 @@ export default function createEnvironmentApi(makeRequest: MakeRequest) { }).then((data) => wrapAssetCollection(makeRequest, data)) }, - getAssetsWithCursor(query: QueryOptions = {}) { + /** + * 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 JS SDK tutorial and the REST API reference for more details. + * @return Promise for a collection of Assets + * @example ```javascript + * const contentful = require('contentful-management') + * + * const client = contentful.createClient({ + * accessToken: '' + * }) + * + * client.getSpace('') + * .then((space) => space.getEnvironment('')) + * .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: 'getManyWithCursor', + action: 'getMany', params: { spaceId: raw.sys.space.sys.id, environmentId: raw.sys.id, - query: createRequestConfig({ query: query }).params, + query: createRequestConfig({ query: normalizedQueryParams }).params, }, - }).then((data) => wrapAssetTypeCursorPaginatedCollection(makeRequest, data)) + }).then((data) => + wrapAssetTypeCursorPaginatedCollection(makeRequest, normalizeCursorPaginationResponse(data)) + ) }, /** @@ -1042,19 +1091,6 @@ export default function createEnvironmentApi(makeRequest: MakeRequest) { }).then((data) => wrapAssetCollection(makeRequest, data)) }, - getPublishedAssetsWithCursor(query: QueryOptions = {}) { - const raw = this.toPlainObject() as EnvironmentProps - return makeRequest({ - entityType: 'Asset', - action: 'getPublishedWithCursor', - params: { - spaceId: raw.sys.space.sys.id, - environmentId: raw.sys.id, - query: createRequestConfig({ query: query }).params, - }, - }).then((data) => wrapAssetTypeCursorPaginatedCollection(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. diff --git a/lib/plain/common-types.ts b/lib/plain/common-types.ts index a55991d56b..232ce73a93 100644 --- a/lib/plain/common-types.ts +++ b/lib/plain/common-types.ts @@ -309,11 +309,6 @@ export type PlainClientAPI = { rawData?: unknown, headers?: RawAxiosRequestHeaders, ): Promise>> - getPublishedWithCursor( - params: OptionalDefaults, - rawData?: unknown, - headers?: RawAxiosRequestHeaders, - ): Promise>> getMany( params: OptionalDefaults, rawData?: unknown, @@ -383,11 +378,6 @@ export type PlainClientAPI = { rawData?: unknown, headers?: RawAxiosRequestHeaders, ): Promise> - getPublishedWithCursor( - params: OptionalDefaults, - rawData?: unknown, - headers?: RawAxiosRequestHeaders, - ): Promise> getMany( params: OptionalDefaults, rawData?: unknown, diff --git a/lib/plain/plain-client.ts b/lib/plain/plain-client.ts index 7a61d1a0c9..dfa0528167 100644 --- a/lib/plain/plain-client.ts +++ b/lib/plain/plain-client.ts @@ -247,7 +247,6 @@ export const createPlainClient = ( }, entry: { getPublished: wrap(wrapParams, 'Entry', 'getPublished'), - getPublishedWithCursor: wrap(wrapParams, 'Entry', 'getPublishedWithCursor'), getMany: wrap(wrapParams, 'Entry', 'getMany'), getManyWithCursor: wrap(wrapParams, 'Entry', 'getManyWithCursor'), get: wrap(wrapParams, 'Entry', 'get'), @@ -264,7 +263,6 @@ export const createPlainClient = ( }, asset: { getPublished: wrap(wrapParams, 'Asset', 'getPublished'), - getPublishedWithCursor: wrap(wrapParams, 'Asset', 'getPublishedWithCursor'), getMany: wrap(wrapParams, 'Asset', 'getMany'), getManyWithCursor: wrap(wrapParams, 'Asset', 'getManyWithCursor'), get: wrap(wrapParams, 'Asset', 'get'), From de2e56cf6769d2d0c9d321334c8e11bdc1b7e1a3 Mon Sep 17 00:00:00 2001 From: Ebrahem Farooqui Date: Fri, 21 Nov 2025 10:56:38 +0100 Subject: [PATCH 03/12] test: add unit tests for create-environment-api and integration tests for getManyWithCursor for content-type, asset, and entry entities [CAPI-2357] --- test/integration/asset-integration.test.ts | 65 +++++++++++++++++ .../content-type-integration.test.ts | 70 +++++++++++++++++++ test/integration/entry-integration.test.ts | 66 +++++++++++++++++ test/unit/create-environment-api.test.ts | 49 ++++++++++++- test/unit/mocks/entities.ts | 19 ++++- .../test-creators/static-entity-methods.ts | 17 +++++ 6 files changed, 283 insertions(+), 3 deletions(-) diff --git a/test/integration/asset-integration.test.ts b/test/integration/asset-integration.test.ts index 02b4ec2847..8ae2fac774 100644 --- a/test/integration/asset-integration.test.ts +++ b/test/integration/asset-integration.test.ts @@ -39,6 +39,71 @@ describe('Asset API - Read', () => { expect(response.items).toBeTruthy() }) +describe('Gets assets with cursor pagination', () => { + test('gets assets with cursor pagination with items', async () => { + const response = await environment.getAssetsWithCursor() + expect(response.items).toBeTruthy() + }) + + test('returns a cursor paginated asset collection when no query is provided', async () => { + const response = await environment.getAssetsWithCursor() + + expect(response.items).not.toHaveLength(0) + expect(response.pages).toBeDefined() + expect((response as { total?: number }).total).toBeUndefined() + + response.items.forEach((item) => { + expect(item.sys.type).toEqual('Asset') + expect(item.fields).toBeDefined() + }) + }) + + test('returns [limit] number of items', async () => { + const response = await environment.getAssetsWithCursor({ limit: 3 }) + + expect(response.items).toHaveLength(3) + expect(response.pages).toBeDefined() + expect((response as { total?: number }).total).toBeUndefined() + + response.items.forEach((item) => { + expect(item.sys.type).toEqual('Asset') + expect(item.fields).toBeDefined() + }) + }) + + test('supports forward pagination', async () => { + const firstPage = await environment.getAssetsWithCursor({ limit: 2 }) + const secondPage = await environment.getAssetsWithCursor({ + limit: 2, + pageNext: firstPage?.pages?.next, + }) + + expect(secondPage.items).toHaveLength(2) + expect(firstPage.items[0].sys.id).not.toEqual(secondPage.items[0].sys.id) + }) + + test('should support backward pagination', async () => { + const firstPage = await environment.getAssetsWithCursor({ limit: 2, order: ['sys.createdAt'] }) + const secondPage = await environment.getAssetsWithCursor({ + limit: 2, + pageNext: firstPage?.pages?.next, + order: ['sys.createdAt'], + }) + const result = await environment.getAssetsWithCursor({ + limit: 2, + pagePrev: secondPage?.pages?.prev, + order: ['sys.createdAt'], + }) + + expect(result.items).toHaveLength(2) + + firstPage.items.forEach((item, index) => { + expect(item.sys.id).equal(result.items[index].sys.id) + }) + }) +}) + + test('Gets published assets', async () => { const response = await environment.getPublishedAssets() expect(response.items).toBeTruthy() diff --git a/test/integration/content-type-integration.test.ts b/test/integration/content-type-integration.test.ts index 5f67d58473..2ac0a6ed79 100644 --- a/test/integration/content-type-integration.test.ts +++ b/test/integration/content-type-integration.test.ts @@ -51,6 +51,76 @@ describe('ContentType Api', () => { const response = await readEnvironment.getContentTypes() expect(response.items).toBeTruthy() }) + +describe('Gets content types with cursor pagination', () => { + it('gets content types with cursor pagination with items', async () => { + const response = await readEnvironment.getContentTypesWithCursor() + expect(response.items).toBeTruthy() + }) + + it('returns a cursor paginated content type collection when no query is provided', async () => { + const response = await readEnvironment.getContentTypesWithCursor() + + expect(response.items).not.toHaveLength(0) + expect(response.pages).toBeDefined() + expect((response as { total?: number }).total).toBeUndefined() + + response.items.forEach((ct) => { + expect(ct.sys.type).toEqual('ContentType') + expect(ct.name).toBeDefined() + expect(ct.fields).toBeDefined() + expect(Array.isArray(ct.fields)).toBe(true) + }) + }) + + it('returns [limit] number of items', async () => { + const response = await readEnvironment.getContentTypesWithCursor({ limit: 3 }) + + expect(response.items).toHaveLength(3) + expect(response.pages).toBeDefined() + expect((response as { total?: number }).total).toBeUndefined() + + response.items.forEach((ct) => { + expect(ct.sys.type).toEqual('ContentType') + expect(ct.name).toBeDefined() + expect(Array.isArray(ct.fields)).toBe(true) + }) + }) + + it('supports forward pagination', async () => { + const firstPage = await readEnvironment.getContentTypesWithCursor({ limit: 2 }) + const secondPage = await readEnvironment.getContentTypesWithCursor({ + limit: 2, + pageNext: firstPage?.pages?.next, + }) + + expect(secondPage.items).toHaveLength(2) + expect(firstPage.items[0].sys.id).not.toEqual(secondPage.items[0].sys.id) + }) + + it('should support backward pagination', async () => { + const firstPage = await readEnvironment.getContentTypesWithCursor({ limit: 2, order: ['sys.createdAt'] }) + const secondPage = await readEnvironment.getContentTypesWithCursor({ + limit: 2, + pageNext: firstPage?.pages?.next, + order: ['sys.createdAt'], + }) + const result = await readEnvironment.getContentTypesWithCursor({ + limit: 2, + pagePrev: secondPage?.pages?.prev, + order: ['sys.createdAt'], + }) + + expect(result.items).toHaveLength(2) + + firstPage.items.forEach((item, index) => { + expect(item.sys.id).toEqual(result.items[index].sys.id) + }) + }) +}) + + + }) describe('write', () => { diff --git a/test/integration/entry-integration.test.ts b/test/integration/entry-integration.test.ts index cfcd9b8f0a..47a979d793 100644 --- a/test/integration/entry-integration.test.ts +++ b/test/integration/entry-integration.test.ts @@ -45,6 +45,72 @@ describe('Entry Api', () => { expect(response.fields, 'fields').to.be.ok }) }) + + describe('Gets entries with cursor pagination', () => { + test('gets entries with cursor pagination with items', async () => { + const response = await environment.getEntriesWithCursor() + expect(response.items).toBeTruthy() + }) + + test('returns a cursor paginated entry collection when no query is provided', async () => { + const response = await environment.getEntriesWithCursor() + + expect(response.items).not.toHaveLength(0) + expect(response.pages).toBeDefined() + expect((response as { total?: number }).total).toBeUndefined() + + response.items.forEach((item) => { + expect(item.sys.type).toEqual('Entry') + expect(item.fields).toBeDefined() + }) + }) + + test('returns [limit] number of items', async () => { + const response = await environment.getEntriesWithCursor({ limit: 3 }) + + expect(response.items).toHaveLength(3) + expect(response.pages).toBeDefined() + expect((response as { total?: number }).total).toBeUndefined() + + response.items.forEach((item) => { + expect(item.sys.type).toEqual('Entry') + expect(item.fields).toBeDefined() + }) + }) + + test('supports forward pagination', async () => { + const firstPage = await environment.getEntriesWithCursor({ limit: 2 }) + const secondPage = await environment.getEntriesWithCursor({ + limit: 2, + pageNext: firstPage?.pages?.next, + }) + + expect(secondPage.items).toHaveLength(2) + expect(firstPage.items[0].sys.id).not.toEqual(secondPage.items[0].sys.id) + }) + + test('should support backward pagination', async () => { + const firstPage = await environment.getEntriesWithCursor({ limit: 2, order: ['sys.createdAt'] }) + const secondPage = await environment.getEntriesWithCursor({ + limit: 2, + pageNext: firstPage?.pages?.next, + order: ['sys.createdAt'], + }) + const result = await environment.getEntriesWithCursor({ + limit: 2, + pagePrev: secondPage?.pages?.prev, + order: ['sys.createdAt'], + }) + + expect(result.items).toHaveLength(2) + + firstPage.items.forEach((item, index) => { + expect(item.sys.id).equal(result.items[index].sys.id) + }) + }) +}) + + test('Gets published entries', async () => { return environment.getPublishedEntries().then((response) => { expect(response.items[0].sys.firstPublishedAt).to.not.be.undefined diff --git a/test/unit/create-environment-api.test.ts b/test/unit/create-environment-api.test.ts index df3bf07458..d97a137129 100644 --- a/test/unit/create-environment-api.test.ts +++ b/test/unit/create-environment-api.test.ts @@ -18,6 +18,7 @@ import { extensionMock, functionCollectionMock, functionLogMock, + mockCursorPaginatedCollection, } from './mocks/entities' import { describe, test, expect } from 'vitest' import { toPlainObject } from 'contentful-sdk-core' @@ -27,14 +28,16 @@ import { makeEntityMethodFailingTest, makeGetCollectionTest, makeGetEntityTest, + makeGetPaginatedCollectionTest, testGettingEntrySDKObject, } from './test-creators/static-entity-methods' -import { wrapEntry } from '../../lib/entities/entry' -import { wrapAsset } from '../../lib/entities/asset' +import { EntryProps, wrapEntry } from '../../lib/entities/entry' +import { AssetProps, wrapAsset } from '../../lib/entities/asset' import { wrapTagCollection } from '../../lib/entities/tag' import setupMakeRequest from './mocks/makeRequest' import createEnvironmentApi from '../../lib/create-environment-api' import { AppActionCallRawResponseProps } from '../../lib/entities/app-action-call' +import { ContentTypeProps } from '../../lib/entities/content-type' function setup(promise: Promise) { const entitiesMock = setupEntitiesMock() @@ -131,6 +134,20 @@ describe('A createEnvironmentApi', () => { }) }) + test('API call getContentTypesWithCursor', async () => { + return makeGetPaginatedCollectionTest(setup, { + entityType: 'contentType', + mockToReturn: mockCursorPaginatedCollection(contentTypeMock), + methodToTest: 'getContentTypesWithCursor' + }) + }) + + test('API call getContentTypesWithCursor fails', async () => { + return makeEntityMethodFailingTest(setup, { + methodToTest: 'getContentTypesWithCursor', + }) + }) + test('API call getBulkAction', async () => { return makeGetEntityTest(setup, { entityType: 'bulkAction', @@ -256,6 +273,20 @@ describe('A createEnvironmentApi', () => { }) }) + test('API call getEntriesWithCursor', async () => { + return makeGetPaginatedCollectionTest(setup, { + entityType: 'entry', + mockToReturn: mockCursorPaginatedCollection(entryMock), + methodToTest: 'getEntriesWithCursor' + }) + }) + + test('API call getEntriesWithCursor fails', async () => { + return makeEntityMethodFailingTest(setup, { + methodToTest: 'getEntriesWithCursor', + }) + }) + test('API call createEntry', async () => { const { api, makeRequest, entitiesMock } = setup(Promise.resolve(entryMock)) entitiesMock.entry.wrapEntry.mockReturnValue(entryMock) @@ -328,6 +359,20 @@ describe('A createEnvironmentApi', () => { }) }) + test('API call getAssetsWithCursor', async () => { + return makeGetPaginatedCollectionTest(setup, { + entityType: 'asset', + mockToReturn: mockCursorPaginatedCollection(assetMock), + methodToTest: 'getAssetsWithCursor' + }) + }) + + test('API call getAssetsWithCursor fails', async () => { + return makeEntityMethodFailingTest(setup, { + methodToTest: 'getAssetsWithCursor', + }) + }) + test('API call createAsset', async () => { return makeCreateEntityTest(setup, { entityType: 'asset', diff --git a/test/unit/mocks/entities.ts b/test/unit/mocks/entities.ts index 42478f1fa2..a78c9c0c9f 100644 --- a/test/unit/mocks/entities.ts +++ b/test/unit/mocks/entities.ts @@ -4,7 +4,7 @@ import cloneDeep from 'lodash/cloneDeep' import { makeLink, makeVersionedLink } from '../../utils' import type { ContentFields } from '../../../lib/entities/content-type-fields' import type { AppSigningSecretProps } from '../../../lib/entities/app-signing-secret' -import type { CollectionProp, Link, MetaLinkProps, MetaSysProps } from '../../../lib/common-types' +import type { CollectionProp, CursorPaginatedCollectionProp, Link, MetaLinkProps, MetaSysProps } from '../../../lib/common-types' import type { AppEventSubscriptionProps } from '../../../lib/entities/app-event-subscription' import type { SpaceProps } from '../../../lib/entities/space' import type { EnvironmentProps } from '../../../lib/entities/environment' @@ -1481,6 +1481,22 @@ function mockCollection(entityMock): CollectionProp { } } +function mockCursorPaginatedCollection( + entityMock +): CursorPaginatedCollectionProp { + return { + sys: { + type: 'Array', + }, + limit: 100, + items: [entityMock as T], + pages: { + next: undefined, + prev: undefined, + }, + } +} + function setupEntitiesMock() { const entitiesMock = { aiAction: { @@ -1759,6 +1775,7 @@ export { errorMock, cloneMock, mockCollection, + mockCursorPaginatedCollection, setupEntitiesMock, uploadMock, uploadCredentialMock, diff --git a/test/unit/test-creators/static-entity-methods.ts b/test/unit/test-creators/static-entity-methods.ts index 9db9bfb08c..1fd5b661af 100644 --- a/test/unit/test-creators/static-entity-methods.ts +++ b/test/unit/test-creators/static-entity-methods.ts @@ -7,6 +7,7 @@ export async function makeGetEntityTest( { entityType, mockToReturn, methodToTest, wrapperSuffix = '' }, ) { const { api, entitiesMock } = setup(Promise.resolve(mockToReturn)) + console.debug('HERE', entitiesMock[entityType]) entitiesMock[entityType][`wrap${upperFirst(entityType)}${wrapperSuffix}`].mockReturnValue( mockToReturn, ) @@ -28,6 +29,22 @@ export async function makeGetCollectionTest(setup, { entityType, mockToReturn, m }) } +export async function makeGetPaginatedCollectionTest(setup, { entityType, mockToReturn, methodToTest }) { + await makeGetEntityTest(setup, { + entityType: entityType, + mockToReturn: { + limit: 100, + items: [mockToReturn], + pages: { + next: undefined, + prev: undefined, + }, + }, + methodToTest: methodToTest, + wrapperSuffix: 'Collection', + }) +} + export async function makeEntityMethodFailingTest(setup, { methodToTest }) { const error = cloneMock('error') const { api } = setup(Promise.reject(error)) From 84d693afb06659da1227cecefa187217f1f74515 Mon Sep 17 00:00:00 2001 From: Ebrahem Farooqui Date: Fri, 21 Nov 2025 11:11:13 +0100 Subject: [PATCH 04/12] chore: updating README.md to reflect new cursor pagination methods [CAPI-2357] --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 1119c89a68..7bd1c5726b 100644 --- a/README.md +++ b/README.md @@ -227,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 + +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: From 790f711aa48b3bea1f22f6c900f7cdb35c880630 Mon Sep 17 00:00:00 2001 From: Ebrahem Farooqui Date: Fri, 21 Nov 2025 11:21:32 +0100 Subject: [PATCH 05/12] chore: fix linting errors in new test suites and lib/ --- lib/adapters/REST/make-request.ts | 2 +- lib/common-types.ts | 4 +- lib/common-utils.ts | 4 +- lib/create-environment-api.ts | 29 ++++- lib/entities/content-type.ts | 6 +- test/integration/asset-integration.test.ts | 102 +++++++-------- .../content-type-integration.test.ts | 118 +++++++++--------- test/integration/entry-integration.test.ts | 76 +++++------ test/unit/create-environment-api.test.ts | 18 +-- test/unit/mocks/entities.ts | 12 +- .../test-creators/static-entity-methods.ts | 5 +- 11 files changed, 202 insertions(+), 174 deletions(-) diff --git a/lib/adapters/REST/make-request.ts b/lib/adapters/REST/make-request.ts index 33a6eb038c..95ba543135 100644 --- a/lib/adapters/REST/make-request.ts +++ b/lib/adapters/REST/make-request.ts @@ -29,7 +29,7 @@ export const makeRequest = async ({ // @ts-ignore endpoints[entityType]?.[action] - console.debug(endpoint) + console.debug(endpoint) if (endpoint === undefined) { throw new Error('Unknown endpoint') diff --git a/lib/common-types.ts b/lib/common-types.ts index 0619666f56..7e4892c1c1 100644 --- a/lib/common-types.ts +++ b/lib/common-types.ts @@ -568,7 +568,9 @@ type MRInternal = { (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', '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'> diff --git a/lib/common-utils.ts b/lib/common-utils.ts index 0babeab288..d8edeea188 100644 --- a/lib/common-utils.ts +++ b/lib/common-utils.ts @@ -52,7 +52,7 @@ export async function waitFor(ms = 1000) { export function normalizeCursorPaginationParameters( query: BasicCursorPaginationOptions, -): CursorBasedParams{ +): CursorBasedParams { const { pagePrev, pageNext, ...rest } = query return { @@ -82,7 +82,7 @@ const Pages = { const PAGE_KEYS = ['prev', 'next'] as const export function normalizeCursorPaginationResponse( - data: CursorPaginatedCollectionProp + data: CursorPaginatedCollectionProp, ): CursorPaginatedCollectionProp { const pages: { prev?: string; next?: string } = {} diff --git a/lib/create-environment-api.ts b/lib/create-environment-api.ts index bd50a7576d..117f1063de 100644 --- a/lib/create-environment-api.ts +++ b/lib/create-environment-api.ts @@ -7,7 +7,10 @@ import type { CursorBasedParams, QueryOptions, } from './common-types' -import { normalizeCursorPaginationParameters, normalizeCursorPaginationResponse } from './common-utils' +import { + normalizeCursorPaginationParameters, + normalizeCursorPaginationResponse, +} from './common-utils' import type { BasicQueryOptions, MakeRequest } from './common-types' import entities from './entities' import type { CreateAppInstallationProps } from './entities/app-installation' @@ -76,7 +79,8 @@ export type ContentfulEnvironmentAPI = ReturnType */ export default function createEnvironmentApi(makeRequest: MakeRequest) { const { wrapEnvironment } = entities.environment - const { wrapContentType, wrapContentTypeCollection, wrapContentTypeCursorPaginatedCollection } = entities.contentType + const { wrapContentType, wrapContentTypeCollection, wrapContentTypeCursorPaginatedCollection } = + entities.contentType const { wrapEntry, wrapEntryCollection, wrapEntryTypeCursorPaginatedCollection } = entities.entry const { wrapAsset, wrapAssetCollection, wrapAssetTypeCursorPaginatedCollection } = entities.asset const { wrapAssetKey } = entities.assetKey @@ -523,7 +527,12 @@ export default function createEnvironmentApi(makeRequest: MakeRequest) { environmentId: raw.sys.id, query: createRequestConfig({ query: normalizedQueryParams }).params, }, - }).then((data) => wrapContentTypeCursorPaginatedCollection(makeRequest, normalizeCursorPaginationResponse(data))) + }).then((data) => + wrapContentTypeCursorPaginatedCollection( + makeRequest, + normalizeCursorPaginationResponse(data), + ), + ) }, /** @@ -805,7 +814,12 @@ export default function createEnvironmentApi(makeRequest: MakeRequest) { environmentId: raw.sys.id, query: createRequestConfig({ query: normalizedQueryParams }).params, }, - }).then((data) => wrapEntryTypeCursorPaginatedCollection(makeRequest, normalizeCursorPaginationResponse(data))) + }).then((data) => + wrapEntryTypeCursorPaginatedCollection( + makeRequest, + normalizeCursorPaginationResponse(data), + ), + ) }, /** @@ -1055,8 +1069,11 @@ export default function createEnvironmentApi(makeRequest: MakeRequest) { environmentId: raw.sys.id, query: createRequestConfig({ query: normalizedQueryParams }).params, }, - }).then((data) => - wrapAssetTypeCursorPaginatedCollection(makeRequest, normalizeCursorPaginationResponse(data)) + }).then((data) => + wrapAssetTypeCursorPaginatedCollection( + makeRequest, + normalizeCursorPaginationResponse(data), + ), ) }, diff --git a/lib/entities/content-type.ts b/lib/entities/content-type.ts index 7f4bd3f8fe..d8cc7dc46f 100644 --- a/lib/entities/content-type.ts +++ b/lib/entities/content-type.ts @@ -367,7 +367,5 @@ export const wrapContentTypeCollection = wrapCollection(wrapContentType) export const wrapContentTypeCursorPaginatedCollection: ( makeRequest: MakeRequest, data: CursorPaginatedCollectionProp, -) => CursorPaginatedCollectionProp = wrapCursorPaginatedCollection( - wrapContentType -) - +) => CursorPaginatedCollectionProp = + wrapCursorPaginatedCollection(wrapContentType) diff --git a/test/integration/asset-integration.test.ts b/test/integration/asset-integration.test.ts index 8ae2fac774..17d3339da1 100644 --- a/test/integration/asset-integration.test.ts +++ b/test/integration/asset-integration.test.ts @@ -39,70 +39,72 @@ describe('Asset API - Read', () => { expect(response.items).toBeTruthy() }) -describe('Gets assets with cursor pagination', () => { - test('gets assets with cursor pagination with items', async () => { - const response = await environment.getAssetsWithCursor() - expect(response.items).toBeTruthy() - }) + describe('Gets assets with cursor pagination', () => { + test('gets assets with cursor pagination with items', async () => { + const response = await environment.getAssetsWithCursor() + expect(response.items).toBeTruthy() + }) - test('returns a cursor paginated asset collection when no query is provided', async () => { - const response = await environment.getAssetsWithCursor() + test('returns a cursor paginated asset collection when no query is provided', async () => { + const response = await environment.getAssetsWithCursor() - expect(response.items).not.toHaveLength(0) - expect(response.pages).toBeDefined() - expect((response as { total?: number }).total).toBeUndefined() + expect(response.items).not.toHaveLength(0) + expect(response.pages).toBeDefined() + expect((response as { total?: number }).total).toBeUndefined() - response.items.forEach((item) => { - expect(item.sys.type).toEqual('Asset') - expect(item.fields).toBeDefined() + response.items.forEach((item) => { + expect(item.sys.type).toEqual('Asset') + expect(item.fields).toBeDefined() + }) }) - }) - test('returns [limit] number of items', async () => { - const response = await environment.getAssetsWithCursor({ limit: 3 }) + test('returns [limit] number of items', async () => { + const response = await environment.getAssetsWithCursor({ limit: 3 }) - expect(response.items).toHaveLength(3) - expect(response.pages).toBeDefined() - expect((response as { total?: number }).total).toBeUndefined() + expect(response.items).toHaveLength(3) + expect(response.pages).toBeDefined() + expect((response as { total?: number }).total).toBeUndefined() - response.items.forEach((item) => { - expect(item.sys.type).toEqual('Asset') - expect(item.fields).toBeDefined() + response.items.forEach((item) => { + expect(item.sys.type).toEqual('Asset') + expect(item.fields).toBeDefined() + }) }) - }) - test('supports forward pagination', async () => { - const firstPage = await environment.getAssetsWithCursor({ limit: 2 }) - const secondPage = await environment.getAssetsWithCursor({ - limit: 2, - pageNext: firstPage?.pages?.next, - }) + test('supports forward pagination', async () => { + const firstPage = await environment.getAssetsWithCursor({ limit: 2 }) + const secondPage = await environment.getAssetsWithCursor({ + limit: 2, + pageNext: firstPage?.pages?.next, + }) - expect(secondPage.items).toHaveLength(2) - expect(firstPage.items[0].sys.id).not.toEqual(secondPage.items[0].sys.id) - }) + expect(secondPage.items).toHaveLength(2) + expect(firstPage.items[0].sys.id).not.toEqual(secondPage.items[0].sys.id) + }) - test('should support backward pagination', async () => { - const firstPage = await environment.getAssetsWithCursor({ limit: 2, order: ['sys.createdAt'] }) - const secondPage = await environment.getAssetsWithCursor({ - limit: 2, - pageNext: firstPage?.pages?.next, - order: ['sys.createdAt'], - }) - const result = await environment.getAssetsWithCursor({ - limit: 2, - pagePrev: secondPage?.pages?.prev, - order: ['sys.createdAt'], - }) + test('should support backward pagination', async () => { + const firstPage = await environment.getAssetsWithCursor({ + limit: 2, + order: ['sys.createdAt'], + }) + const secondPage = await environment.getAssetsWithCursor({ + limit: 2, + pageNext: firstPage?.pages?.next, + order: ['sys.createdAt'], + }) + const result = await environment.getAssetsWithCursor({ + limit: 2, + pagePrev: secondPage?.pages?.prev, + order: ['sys.createdAt'], + }) - expect(result.items).toHaveLength(2) + expect(result.items).toHaveLength(2) - firstPage.items.forEach((item, index) => { - expect(item.sys.id).equal(result.items[index].sys.id) - }) + firstPage.items.forEach((item, index) => { + expect(item.sys.id).equal(result.items[index].sys.id) }) -}) - + }) + }) test('Gets published assets', async () => { const response = await environment.getPublishedAssets() diff --git a/test/integration/content-type-integration.test.ts b/test/integration/content-type-integration.test.ts index 2ac0a6ed79..2dac9aa71e 100644 --- a/test/integration/content-type-integration.test.ts +++ b/test/integration/content-type-integration.test.ts @@ -52,76 +52,76 @@ describe('ContentType Api', () => { expect(response.items).toBeTruthy() }) -describe('Gets content types with cursor pagination', () => { - it('gets content types with cursor pagination with items', async () => { - const response = await readEnvironment.getContentTypesWithCursor() - expect(response.items).toBeTruthy() - }) - - it('returns a cursor paginated content type collection when no query is provided', async () => { - const response = await readEnvironment.getContentTypesWithCursor() - - expect(response.items).not.toHaveLength(0) - expect(response.pages).toBeDefined() - expect((response as { total?: number }).total).toBeUndefined() + describe('Gets content types with cursor pagination', () => { + it('gets content types with cursor pagination with items', async () => { + const response = await readEnvironment.getContentTypesWithCursor() + expect(response.items).toBeTruthy() + }) - response.items.forEach((ct) => { - expect(ct.sys.type).toEqual('ContentType') - expect(ct.name).toBeDefined() - expect(ct.fields).toBeDefined() - expect(Array.isArray(ct.fields)).toBe(true) - }) - }) + it('returns a cursor paginated content type collection when no query is provided', async () => { + const response = await readEnvironment.getContentTypesWithCursor() - it('returns [limit] number of items', async () => { - const response = await readEnvironment.getContentTypesWithCursor({ limit: 3 }) + expect(response.items).not.toHaveLength(0) + expect(response.pages).toBeDefined() + expect((response as { total?: number }).total).toBeUndefined() - expect(response.items).toHaveLength(3) - expect(response.pages).toBeDefined() - expect((response as { total?: number }).total).toBeUndefined() + response.items.forEach((ct) => { + expect(ct.sys.type).toEqual('ContentType') + expect(ct.name).toBeDefined() + expect(ct.fields).toBeDefined() + expect(Array.isArray(ct.fields)).toBe(true) + }) + }) - response.items.forEach((ct) => { - expect(ct.sys.type).toEqual('ContentType') - expect(ct.name).toBeDefined() - expect(Array.isArray(ct.fields)).toBe(true) - }) - }) + it('returns [limit] number of items', async () => { + const response = await readEnvironment.getContentTypesWithCursor({ limit: 3 }) - it('supports forward pagination', async () => { - const firstPage = await readEnvironment.getContentTypesWithCursor({ limit: 2 }) - const secondPage = await readEnvironment.getContentTypesWithCursor({ - limit: 2, - pageNext: firstPage?.pages?.next, - }) + expect(response.items).toHaveLength(3) + expect(response.pages).toBeDefined() + expect((response as { total?: number }).total).toBeUndefined() - expect(secondPage.items).toHaveLength(2) - expect(firstPage.items[0].sys.id).not.toEqual(secondPage.items[0].sys.id) - }) + response.items.forEach((ct) => { + expect(ct.sys.type).toEqual('ContentType') + expect(ct.name).toBeDefined() + expect(Array.isArray(ct.fields)).toBe(true) + }) + }) - it('should support backward pagination', async () => { - const firstPage = await readEnvironment.getContentTypesWithCursor({ limit: 2, order: ['sys.createdAt'] }) - const secondPage = await readEnvironment.getContentTypesWithCursor({ - limit: 2, - pageNext: firstPage?.pages?.next, - order: ['sys.createdAt'], - }) - const result = await readEnvironment.getContentTypesWithCursor({ - limit: 2, - pagePrev: secondPage?.pages?.prev, - order: ['sys.createdAt'], - }) + it('supports forward pagination', async () => { + const firstPage = await readEnvironment.getContentTypesWithCursor({ limit: 2 }) + const secondPage = await readEnvironment.getContentTypesWithCursor({ + limit: 2, + pageNext: firstPage?.pages?.next, + }) - expect(result.items).toHaveLength(2) + expect(secondPage.items).toHaveLength(2) + expect(firstPage.items[0].sys.id).not.toEqual(secondPage.items[0].sys.id) + }) - firstPage.items.forEach((item, index) => { - expect(item.sys.id).toEqual(result.items[index].sys.id) + it('should support backward pagination', async () => { + const firstPage = await readEnvironment.getContentTypesWithCursor({ + limit: 2, + order: ['sys.createdAt'], + }) + const secondPage = await readEnvironment.getContentTypesWithCursor({ + limit: 2, + pageNext: firstPage?.pages?.next, + order: ['sys.createdAt'], + }) + const result = await readEnvironment.getContentTypesWithCursor({ + limit: 2, + pagePrev: secondPage?.pages?.prev, + order: ['sys.createdAt'], + }) + + expect(result.items).toHaveLength(2) + + firstPage.items.forEach((item, index) => { + expect(item.sys.id).toEqual(result.items[index].sys.id) + }) + }) }) }) -}) - - - - }) describe('write', () => { it('Create, update, publish, getEditorInterface, unpublish and delete content type', async () => { diff --git a/test/integration/entry-integration.test.ts b/test/integration/entry-integration.test.ts index 47a979d793..db5338f39b 100644 --- a/test/integration/entry-integration.test.ts +++ b/test/integration/entry-integration.test.ts @@ -47,50 +47,53 @@ describe('Entry Api', () => { }) describe('Gets entries with cursor pagination', () => { - test('gets entries with cursor pagination with items', async () => { - const response = await environment.getEntriesWithCursor() - expect(response.items).toBeTruthy() - }) + test('gets entries with cursor pagination with items', async () => { + const response = await environment.getEntriesWithCursor() + expect(response.items).toBeTruthy() + }) - test('returns a cursor paginated entry collection when no query is provided', async () => { - const response = await environment.getEntriesWithCursor() + test('returns a cursor paginated entry collection when no query is provided', async () => { + const response = await environment.getEntriesWithCursor() - expect(response.items).not.toHaveLength(0) - expect(response.pages).toBeDefined() - expect((response as { total?: number }).total).toBeUndefined() + expect(response.items).not.toHaveLength(0) + expect(response.pages).toBeDefined() + expect((response as { total?: number }).total).toBeUndefined() - response.items.forEach((item) => { - expect(item.sys.type).toEqual('Entry') - expect(item.fields).toBeDefined() - }) - }) + response.items.forEach((item) => { + expect(item.sys.type).toEqual('Entry') + expect(item.fields).toBeDefined() + }) + }) - test('returns [limit] number of items', async () => { - const response = await environment.getEntriesWithCursor({ limit: 3 }) + test('returns [limit] number of items', async () => { + const response = await environment.getEntriesWithCursor({ limit: 3 }) - expect(response.items).toHaveLength(3) - expect(response.pages).toBeDefined() - expect((response as { total?: number }).total).toBeUndefined() + expect(response.items).toHaveLength(3) + expect(response.pages).toBeDefined() + expect((response as { total?: number }).total).toBeUndefined() - response.items.forEach((item) => { - expect(item.sys.type).toEqual('Entry') - expect(item.fields).toBeDefined() - }) - }) + response.items.forEach((item) => { + expect(item.sys.type).toEqual('Entry') + expect(item.fields).toBeDefined() + }) + }) - test('supports forward pagination', async () => { - const firstPage = await environment.getEntriesWithCursor({ limit: 2 }) - const secondPage = await environment.getEntriesWithCursor({ - limit: 2, - pageNext: firstPage?.pages?.next, - }) + test('supports forward pagination', async () => { + const firstPage = await environment.getEntriesWithCursor({ limit: 2 }) + const secondPage = await environment.getEntriesWithCursor({ + limit: 2, + pageNext: firstPage?.pages?.next, + }) - expect(secondPage.items).toHaveLength(2) - expect(firstPage.items[0].sys.id).not.toEqual(secondPage.items[0].sys.id) - }) + expect(secondPage.items).toHaveLength(2) + expect(firstPage.items[0].sys.id).not.toEqual(secondPage.items[0].sys.id) + }) - test('should support backward pagination', async () => { - const firstPage = await environment.getEntriesWithCursor({ limit: 2, order: ['sys.createdAt'] }) + test('should support backward pagination', async () => { + const firstPage = await environment.getEntriesWithCursor({ + limit: 2, + order: ['sys.createdAt'], + }) const secondPage = await environment.getEntriesWithCursor({ limit: 2, pageNext: firstPage?.pages?.next, @@ -108,8 +111,7 @@ describe('Entry Api', () => { expect(item.sys.id).equal(result.items[index].sys.id) }) }) -}) - + }) test('Gets published entries', async () => { return environment.getPublishedEntries().then((response) => { diff --git a/test/unit/create-environment-api.test.ts b/test/unit/create-environment-api.test.ts index d97a137129..ef17702752 100644 --- a/test/unit/create-environment-api.test.ts +++ b/test/unit/create-environment-api.test.ts @@ -134,15 +134,15 @@ describe('A createEnvironmentApi', () => { }) }) - test('API call getContentTypesWithCursor', async () => { + test('API call getContentTypesWithCursor', async () => { return makeGetPaginatedCollectionTest(setup, { entityType: 'contentType', mockToReturn: mockCursorPaginatedCollection(contentTypeMock), - methodToTest: 'getContentTypesWithCursor' + methodToTest: 'getContentTypesWithCursor', }) }) - test('API call getContentTypesWithCursor fails', async () => { + test('API call getContentTypesWithCursor fails', async () => { return makeEntityMethodFailingTest(setup, { methodToTest: 'getContentTypesWithCursor', }) @@ -273,15 +273,15 @@ describe('A createEnvironmentApi', () => { }) }) - test('API call getEntriesWithCursor', async () => { + test('API call getEntriesWithCursor', async () => { return makeGetPaginatedCollectionTest(setup, { entityType: 'entry', mockToReturn: mockCursorPaginatedCollection(entryMock), - methodToTest: 'getEntriesWithCursor' + methodToTest: 'getEntriesWithCursor', }) }) - test('API call getEntriesWithCursor fails', async () => { + test('API call getEntriesWithCursor fails', async () => { return makeEntityMethodFailingTest(setup, { methodToTest: 'getEntriesWithCursor', }) @@ -359,15 +359,15 @@ describe('A createEnvironmentApi', () => { }) }) - test('API call getAssetsWithCursor', async () => { + test('API call getAssetsWithCursor', async () => { return makeGetPaginatedCollectionTest(setup, { entityType: 'asset', mockToReturn: mockCursorPaginatedCollection(assetMock), - methodToTest: 'getAssetsWithCursor' + methodToTest: 'getAssetsWithCursor', }) }) - test('API call getAssetsWithCursor fails', async () => { + test('API call getAssetsWithCursor fails', async () => { return makeEntityMethodFailingTest(setup, { methodToTest: 'getAssetsWithCursor', }) diff --git a/test/unit/mocks/entities.ts b/test/unit/mocks/entities.ts index a78c9c0c9f..48ad6697a9 100644 --- a/test/unit/mocks/entities.ts +++ b/test/unit/mocks/entities.ts @@ -4,7 +4,13 @@ import cloneDeep from 'lodash/cloneDeep' import { makeLink, makeVersionedLink } from '../../utils' import type { ContentFields } from '../../../lib/entities/content-type-fields' import type { AppSigningSecretProps } from '../../../lib/entities/app-signing-secret' -import type { CollectionProp, CursorPaginatedCollectionProp, Link, MetaLinkProps, MetaSysProps } from '../../../lib/common-types' +import type { + CollectionProp, + CursorPaginatedCollectionProp, + Link, + MetaLinkProps, + MetaSysProps, +} from '../../../lib/common-types' import type { AppEventSubscriptionProps } from '../../../lib/entities/app-event-subscription' import type { SpaceProps } from '../../../lib/entities/space' import type { EnvironmentProps } from '../../../lib/entities/environment' @@ -1481,9 +1487,7 @@ function mockCollection(entityMock): CollectionProp { } } -function mockCursorPaginatedCollection( - entityMock -): CursorPaginatedCollectionProp { +function mockCursorPaginatedCollection(entityMock): CursorPaginatedCollectionProp { return { sys: { type: 'Array', diff --git a/test/unit/test-creators/static-entity-methods.ts b/test/unit/test-creators/static-entity-methods.ts index 1fd5b661af..2750cc2d33 100644 --- a/test/unit/test-creators/static-entity-methods.ts +++ b/test/unit/test-creators/static-entity-methods.ts @@ -29,7 +29,10 @@ export async function makeGetCollectionTest(setup, { entityType, mockToReturn, m }) } -export async function makeGetPaginatedCollectionTest(setup, { entityType, mockToReturn, methodToTest }) { +export async function makeGetPaginatedCollectionTest( + setup, + { entityType, mockToReturn, methodToTest }, +) { await makeGetEntityTest(setup, { entityType: entityType, mockToReturn: { From a2a540e11b334fb382252bd258539a712a1c6b68 Mon Sep 17 00:00:00 2001 From: Ebrahem Farooqui Date: Fri, 21 Nov 2025 13:40:05 +0100 Subject: [PATCH 06/12] chore: removing unused console.debug line [CAPI-2357] --- lib/adapters/REST/make-request.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/adapters/REST/make-request.ts b/lib/adapters/REST/make-request.ts index 95ba543135..6e6d599ed1 100644 --- a/lib/adapters/REST/make-request.ts +++ b/lib/adapters/REST/make-request.ts @@ -29,8 +29,6 @@ export const makeRequest = async ({ // @ts-ignore endpoints[entityType]?.[action] - console.debug(endpoint) - if (endpoint === undefined) { throw new Error('Unknown endpoint') } From 09e8e075a5fc8d76b6867666fcacbce6bd26244f Mon Sep 17 00:00:00 2001 From: Ebrahem Farooqui Date: Fri, 21 Nov 2025 16:41:37 +0100 Subject: [PATCH 07/12] fix: enforcing no skip param for query options, removing uneccessary console statements, adding cbp to toc in readme [CAPI-2357] --- README.md | 1 + lib/common-types.ts | 1 + lib/common-utils.ts | 2 +- test/integration/asset-integration.test.ts | 2 +- test/unit/test-creators/static-entity-methods.ts | 1 - 5 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7bd1c5726b..fb6004542b 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,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) diff --git a/lib/common-types.ts b/lib/common-types.ts index 7e4892c1c1..1a5483801e 100644 --- a/lib/common-types.ts +++ b/lib/common-types.ts @@ -359,6 +359,7 @@ export interface BasicQueryOptions { } export interface BasicCursorPaginationOptions extends Omit { + skip?: never pageNext?: string pagePrev?: string } diff --git a/lib/common-utils.ts b/lib/common-utils.ts index d8edeea188..a4ef5582bb 100644 --- a/lib/common-utils.ts +++ b/lib/common-utils.ts @@ -93,6 +93,6 @@ export function normalizeCursorPaginationResponse( return { ...data, - ...(Object.keys(pages).length && { pages }), + pages, } } diff --git a/test/integration/asset-integration.test.ts b/test/integration/asset-integration.test.ts index 17d3339da1..c18eebed2a 100644 --- a/test/integration/asset-integration.test.ts +++ b/test/integration/asset-integration.test.ts @@ -59,7 +59,7 @@ describe('Asset API - Read', () => { }) test('returns [limit] number of items', async () => { - const response = await environment.getAssetsWithCursor({ limit: 3 }) + const response = await environment.getAssetsWithCursor({ limit: 3}) expect(response.items).toHaveLength(3) expect(response.pages).toBeDefined() diff --git a/test/unit/test-creators/static-entity-methods.ts b/test/unit/test-creators/static-entity-methods.ts index 2750cc2d33..415d2af1be 100644 --- a/test/unit/test-creators/static-entity-methods.ts +++ b/test/unit/test-creators/static-entity-methods.ts @@ -7,7 +7,6 @@ export async function makeGetEntityTest( { entityType, mockToReturn, methodToTest, wrapperSuffix = '' }, ) { const { api, entitiesMock } = setup(Promise.resolve(mockToReturn)) - console.debug('HERE', entitiesMock[entityType]) entitiesMock[entityType][`wrap${upperFirst(entityType)}${wrapperSuffix}`].mockReturnValue( mockToReturn, ) From 06035083adef544a6dca07f0cd5737743d30580f Mon Sep 17 00:00:00 2001 From: Ely Lucas Date: Tue, 18 Nov 2025 09:47:01 -0700 Subject: [PATCH 08/12] chore: update readme badge and change to master branch in workflow (#2819) Co-authored-by: Ely Lucas --- .github/workflows/main.yaml | 2 +- README.md | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index d4540195c8..8faa324f43 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -25,7 +25,7 @@ jobs: secrets: inherit release: - if: github.event_name == 'push' && contains(fromJSON('["refs/heads/main", "refs/heads/beta", "refs/heads/canary", "refs/heads/dev"]'), github.ref) + if: github.event_name == 'push' && contains(fromJSON('["refs/heads/master", "refs/heads/beta", "refs/heads/canary", "refs/heads/dev"]'), github.ref) needs: [build, check, test-demo-projects, test-integration] permissions: contents: write diff --git a/README.md b/README.md index fb6004542b..fc1469289c 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,7 @@ ## Introduction -[![Build Status](https://circleci.com/gh/contentful/contentful-management.js.svg?style=svg)](https://circleci.com/gh/contentful/contentful-management.js) -[![npm](https://img.shields.io/npm/v/contentful-management.svg)](https://www.npmjs.com/package/contentful-management) +[![CI](https://github.com/contentful/contentful-management.js/actions/workflows/main.yaml/badge.svg?branch=master)](https://github.com/contentful/contentful-management.js/actions/workflows/main.yaml) [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) [![npm downloads](https://img.shields.io/npm/dm/contentful-management.svg)](http://npm-stat.com/charts.html?package=contentful-management) [![gzip bundle size](http://img.badgesize.io/https://unpkg.com/contentful-management/dist/contentful-management.browser.min.js?compression=gzip)](https://unpkg.com/contentful-management/dist/contentful-management.browser.min.js) From 725fc0d968a7f7c8ecf4a789c4163e37c45dd2ea Mon Sep 17 00:00:00 2001 From: Ebrahem Farooqui Date: Fri, 21 Nov 2025 16:50:07 +0100 Subject: [PATCH 09/12] chore: fixing linting errors in asset-integration-test [CAPI-2357] --- test/integration/asset-integration.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/asset-integration.test.ts b/test/integration/asset-integration.test.ts index c18eebed2a..17d3339da1 100644 --- a/test/integration/asset-integration.test.ts +++ b/test/integration/asset-integration.test.ts @@ -59,7 +59,7 @@ describe('Asset API - Read', () => { }) test('returns [limit] number of items', async () => { - const response = await environment.getAssetsWithCursor({ limit: 3}) + const response = await environment.getAssetsWithCursor({ limit: 3 }) expect(response.items).toHaveLength(3) expect(response.pages).toBeDefined() From 5fc9db5f67f351ca767caf2d8ed78d5896456c56 Mon Sep 17 00:00:00 2001 From: Ebrahem Farooqui Date: Fri, 21 Nov 2025 17:24:56 +0100 Subject: [PATCH 10/12] fix: fixing typing to include CursorBasedParams not QueryParams for getManyWithCursor methods [CAPI-2357] --- lib/plain/common-types.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/plain/common-types.ts b/lib/plain/common-types.ts index 232ce73a93..904a45aa15 100644 --- a/lib/plain/common-types.ts +++ b/lib/plain/common-types.ts @@ -8,6 +8,7 @@ import type { CreateWithFilesReleaseAssetParams, CreateWithIdReleaseAssetParams, CreateWithIdReleaseEntryParams, + CursorBasedParams, CursorPaginatedCollectionProp, EnvironmentTemplateParams, GetBulkActionParams, @@ -275,7 +276,7 @@ export type PlainClientAPI = { params: OptionalDefaults, ): Promise> getManyWithCursor( - params: OptionalDefaults, + params: OptionalDefaults, ): Promise> update( params: OptionalDefaults, @@ -315,7 +316,9 @@ export type PlainClientAPI = { headers?: RawAxiosRequestHeaders, ): Promise>> getManyWithCursor( - params: OptionalDefaults, + params: OptionalDefaults< + GetSpaceEnvironmentParams & CursorBasedParams & { releaseId?: string } + >, rawData?: unknown, headers?: RawAxiosRequestHeaders, ): Promise>> @@ -384,7 +387,9 @@ export type PlainClientAPI = { headers?: RawAxiosRequestHeaders, ): Promise> getManyWithCursor( - params: OptionalDefaults, + params: OptionalDefaults< + GetSpaceEnvironmentParams & CursorBasedParams & { releaseId?: string } + >, rawData?: unknown, headers?: RawAxiosRequestHeaders, ): Promise> From fa0dca8f9bf91e2f3e94e7cde2716311d92eb327 Mon Sep 17 00:00:00 2001 From: Ebrahem Farooqui Date: Mon, 24 Nov 2025 16:46:25 +0100 Subject: [PATCH 11/12] fix: infer typing for cursor pagination collection method per entity [CAPI-2357] --- lib/entities/asset.ts | 5 +---- lib/entities/content-type.ts | 5 +---- lib/entities/entry.ts | 5 +---- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/lib/entities/asset.ts b/lib/entities/asset.ts index 5fa5f4a590..cd63c9f109 100644 --- a/lib/entities/asset.ts +++ b/lib/entities/asset.ts @@ -415,7 +415,4 @@ export const wrapAssetCollection = wrapCollection(wrapAsset) /** * @private */ -export const wrapAssetTypeCursorPaginatedCollection: ( - makeRequest: MakeRequest, - data: CursorPaginatedCollectionProp, -) => CursorPaginatedCollectionProp = wrapCursorPaginatedCollection(wrapAsset) +export const wrapAssetTypeCursorPaginatedCollection = wrapCursorPaginatedCollection(wrapAsset) diff --git a/lib/entities/content-type.ts b/lib/entities/content-type.ts index d8cc7dc46f..fb8402a16d 100644 --- a/lib/entities/content-type.ts +++ b/lib/entities/content-type.ts @@ -364,8 +364,5 @@ export const wrapContentTypeCollection = wrapCollection(wrapContentType) /** * @private */ -export const wrapContentTypeCursorPaginatedCollection: ( - makeRequest: MakeRequest, - data: CursorPaginatedCollectionProp, -) => CursorPaginatedCollectionProp = +export const wrapContentTypeCursorPaginatedCollection = wrapCursorPaginatedCollection(wrapContentType) diff --git a/lib/entities/entry.ts b/lib/entities/entry.ts index 45da1a7e1f..31c65559b7 100644 --- a/lib/entities/entry.ts +++ b/lib/entities/entry.ts @@ -76,7 +76,4 @@ export const wrapEntryCollection = wrapCollection(wrapEntry) /** * @private */ -export const wrapEntryTypeCursorPaginatedCollection: ( - makeRequest: MakeRequest, - data: CursorPaginatedCollectionProp, -) => CursorPaginatedCollectionProp = wrapCursorPaginatedCollection(wrapEntry) +export const wrapEntryTypeCursorPaginatedCollection = wrapCursorPaginatedCollection(wrapEntry) From 49dbd1e1051ca90bb837c81652a2fe365638f2e7 Mon Sep 17 00:00:00 2001 From: Ebrahem Farooqui Date: Wed, 26 Nov 2025 16:47:22 +0100 Subject: [PATCH 12/12] chore: updating param comment to reflect appropriate url, adding pagination example to readme [CAPI-2357] --- README.md | 8 ++++++++ lib/create-environment-api.ts | 6 +++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index fc1469289c..def4e9b231 100644 --- a/README.md +++ b/README.md @@ -238,6 +238,14 @@ console.log(response.pages?.next); // Cursor for next page ``` Use the value from `response.pages.next` to fetch the next page. +```js +const secondPage = await environment.getEntriesWithCursor({ + limit: 2, + pageNext: response.pages?.next, +}); +console.log(secondPage.items); // Array of items +``` + ## 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: diff --git a/lib/create-environment-api.ts b/lib/create-environment-api.ts index 117f1063de..8425088dbd 100644 --- a/lib/create-environment-api.ts +++ b/lib/create-environment-api.ts @@ -500,7 +500,7 @@ export default function createEnvironmentApi(makeRequest: MakeRequest) { /** * Gets a collection of Content Types with cursor based pagination - * @param query - Object with search parameters. Check the JS SDK tutorial and the REST API reference for more details. + * @param query - Object with cursor pagination parameters. Check the REST API reference for more details. * @return Promise for a collection of Content Types * @example ```javascript * const contentful = require('contentful-management') @@ -787,7 +787,7 @@ export default function createEnvironmentApi(makeRequest: MakeRequest) { * 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 JS SDK tutorial and the REST API reference for more details. + * @param query - Object with cursor pagination parameters. Check the REST API reference for more details. * @return Promise for a collection of Entries * @example ```javascript * const contentful = require('contentful-management') @@ -1042,7 +1042,7 @@ export default function createEnvironmentApi(makeRequest: MakeRequest) { * 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 JS SDK tutorial and the REST API reference for more details. + * @param query - Object with cursor pagination parameters. Check the REST API reference for more details. * @return Promise for a collection of Assets * @example ```javascript * const contentful = require('contentful-management')