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
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ JavaScript library for the Contentful [Content Delivery API](https://www.content
- [Your first request](#your-first-request)
- [Using this library with the Preview API](#using-this-library-with-the-preview-api)
- [Authentication](#authentication)
- [Cursor-based Pagination](#cursor-based-pagination)
- [Documentation \& References](#documentation--references)
- [Configuration](#configuration)
- [Request configuration options](#request-configuration-options)
Expand Down Expand Up @@ -133,6 +134,7 @@ In order to get started with the Contentful JS library you'll need not only to i
- [Your first request](#your-first-request)
- [Using this library with the Preview API](#using-this-library-with-the-preview-api)
- [Authentication](#authentication)
- [Cursor-based pagination](#cursor-based-pagination)
- [Documentation & References](#documentation--references)

### Installation
Expand Down Expand Up @@ -227,6 +229,29 @@ Don't forget to also get your Space ID.

For more information, check the [Contentful REST API reference on Authentication](https://www.contentful.com/developers/docs/references/authentication/).

### Cursor-based Pagination

Cursor-based pagination is supported on collection endpoints for entries and assets:

```js
const response = await client.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 or `response.pages.prev` to fetch the previous page.

```js
const nextPageResponse = await client.getEntriesWithCursor({
limit: 10,
pageNext: response.pages?.next,
})

console.log(nextPageResponse.items) // Array of items
console.log(nextPageResponse.pages?.next) // Cursor for next page
console.log(nextPageResponse.pages?.prev) // Cursor for prev page
```

## Documentation & References

- [Configuration](#configuration)
Expand Down
64 changes: 49 additions & 15 deletions lib/create-contentful-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ import type {
Concept,
ConceptScheme,
ConceptSchemeCollection,
AssetCursorPaginatedCollection,
CollectionForQuery,
EntryCursorPaginatedCollection,
Entry,
} from './types/index.js'
import normalizeSearchParameters from './utils/normalize-search-parameters.js'
import normalizeSelect from './utils/normalize-select.js'
Expand All @@ -44,6 +48,8 @@ import {
} from './utils/validate-params.js'
import validateSearchParameters from './utils/validate-search-parameters.js'
import { getTimelinePreviewParams } from './utils/timeline-preview-helpers.js'
import { normalizeCursorPaginationParameters } from './utils/normalize-cursor-pagination-parameters.js'
import { normalizeCursorPaginationResponse } from './utils/normalize-cursor-pagination-response.js'

const ASSET_KEY_MAX_LIFETIME = 48 * 60 * 60

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

async function getEntriesWithCursor(
query = {},
): Promise<EntryCursorPaginatedCollection<EntrySkeletonType>> {
const response = await makeGetEntries(normalizeCursorPaginationParameters(query), options)
return normalizeCursorPaginationResponse(response)
}

async function makeGetEntry<EntrySkeleton extends EntrySkeletonType>(
id: string,
query,
Expand Down Expand Up @@ -242,10 +255,12 @@ export default function createContentfulApi<OptionType extends ChainOptions>(
throw notFoundError(id)
}
try {
const response = await internalGetEntries<EntrySkeletonType<EntrySkeleton>, Locales, Options>(
{ 'sys.id': id, ...maybeEnableSourceMaps(query) },
options,
)
const response = await internalGetEntries<
EntrySkeletonType<EntrySkeleton>,
Locales,
Options,
Record<string, unknown>
>({ 'sys.id': id, ...maybeEnableSourceMaps(query) }, options)
if (response.items.length > 0) {
return response.items[0]
} else {
Expand All @@ -256,8 +271,11 @@ export default function createContentfulApi<OptionType extends ChainOptions>(
}
}

async function makeGetEntries<EntrySkeleton extends EntrySkeletonType>(
query,
async function makeGetEntries<
EntrySkeleton extends EntrySkeletonType,
Query extends Record<string, unknown>,
>(
query: Query,
options: ChainOptions = {
withAllLocales: false,
withoutLinkResolution: false,
Expand All @@ -271,7 +289,7 @@ export default function createContentfulApi<OptionType extends ChainOptions>(
validateRemoveUnresolvedParam(query)
validateSearchParameters(query)

return internalGetEntries<EntrySkeleton, any, Extract<ChainOptions, typeof options>>(
return internalGetEntries<EntrySkeleton, any, Extract<ChainOptions, typeof options>, Query>(
withAllLocales
? {
...query,
Expand All @@ -293,10 +311,13 @@ export default function createContentfulApi<OptionType extends ChainOptions>(
EntrySkeleton extends EntrySkeletonType,
Locales extends LocaleCode,
Options extends ChainOptions,
Query extends Record<string, unknown>,
>(
query: Record<string, any>,
query: Query,
options: Options,
): Promise<EntryCollection<EntrySkeleton, ModifiersFromOptions<Options>, Locales>> {
): Promise<
CollectionForQuery<Entry<EntrySkeleton, ModifiersFromOptions<Options>, Locales>, Query>
> {
const { withoutLinkResolution, withoutUnresolvableLinks } = options
try {
const entries = await get({
Expand Down Expand Up @@ -324,8 +345,15 @@ export default function createContentfulApi<OptionType extends ChainOptions>(
return makeGetAssets(query, options)
}

async function makeGetAssets(
query: Record<string, any>,
async function getAssetsWithCursor(
query: Record<string, any> = {},
): Promise<AssetCursorPaginatedCollection> {
const response = await makeGetAssets(normalizeCursorPaginationParameters(query), options)
return normalizeCursorPaginationResponse(response)
}

async function makeGetAssets<Query extends Record<string, any>>(
query: Query,
options: ChainOptions = {
withAllLocales: false,
withoutLinkResolution: false,
Expand All @@ -339,7 +367,7 @@ export default function createContentfulApi<OptionType extends ChainOptions>(

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

return internalGetAssets<any, Extract<ChainOptions, typeof options>>(localeSpecificQuery)
return internalGetAssets<any, Extract<ChainOptions, typeof options>, Query>(localeSpecificQuery)
}

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

async function internalGetAssets<Locales extends LocaleCode, Options extends ChainOptions>(
query: Record<string, any>,
): Promise<AssetCollection<ModifiersFromOptions<Options>, Locales>> {
async function internalGetAssets<
Locales extends LocaleCode,
Options extends ChainOptions,
Query extends Record<string, any>,
>(
query: Query,
): Promise<CollectionForQuery<Asset<ModifiersFromOptions<Options>, Locales>, Query>> {
try {
return get({
context: 'environment',
Expand Down Expand Up @@ -657,6 +689,7 @@ export default function createContentfulApi<OptionType extends ChainOptions>(

getAsset,
getAssets,
getAssetsWithCursor,

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

getEntry,
getEntries,
getEntriesWithCursor,

getConceptScheme,
getConceptSchemes,
Expand Down
14 changes: 13 additions & 1 deletion lib/types/asset.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ContentfulCollection } from './collection.js'
import type { ContentfulCollection, CursorPaginatedCollection } from './collection.js'
import type { LocaleCode } from './locale.js'
import type { Metadata } from './metadata.js'
import type { EntitySys } from './sys.js'
Expand Down Expand Up @@ -85,6 +85,18 @@ export type AssetCollection<
Locales extends LocaleCode = LocaleCode,
> = ContentfulCollection<Asset<Modifiers, Locales>>

/**
* A cursor paginated collection of assets
* @category Asset
* @typeParam Modifiers - The chain modifiers used to configure the client. They’re set automatically when using the client chain modifiers.
* @typeParam Locales - If provided for a client using `allLocales` modifier, response type defines locale keys for asset field values.
* @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/assets | Documentation}
*/
export type AssetCursorPaginatedCollection<
Modifiers extends ChainModifiers = ChainModifiers,
Locales extends LocaleCode = LocaleCode,
> = CursorPaginatedCollection<Asset<Modifiers, Locales>>

/**
* System managed metadata for assets
* @category Asset
Expand Down
65 changes: 63 additions & 2 deletions lib/types/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,26 @@ import type { LocaleCode, LocaleCollection } from './locale.js'
import type {
AssetQueries,
AssetsQueries,
AssetsQueriesWithCursor,
ConceptAncestorsDescendantsQueries,
ConceptSchemesQueries,
ConceptsQueries,
EntriesQueries,
EntriesQueriesWithCursor,
EntryQueries,
EntrySkeletonType,
TagQueries,
} from './query/index.js'
import type { SyncCollection, SyncOptions, SyncQuery } from './sync.js'
import type { Tag, TagCollection } from './tag.js'
import type { AssetKey } from './asset-key.js'
import type { Entry, EntryCollection } from './entry.js'
import type { Asset, AssetCollection, AssetFields } from './asset.js'
import type { Entry, EntryCollection, EntryCursorPaginatedCollection } from './entry.js'
import type {
Asset,
AssetCollection,
AssetCursorPaginatedCollection,
AssetFields,
} from './asset.js'
import type { Concept, ConceptCollection } from './concept.js'
import type { ConceptScheme, ConceptSchemeCollection } from './concept-scheme.js'

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

/**
* Fetches a cursor paginated collection of Entries
* @param pagination - Object with cursor pagination options
* @param query - Object with search parameters
* @returns Promise for a cursor paginated collection of Entries
* @typeParam EntrySkeleton - Shape of entry fields used to calculate dynamic keys
* @typeParam Locales - If provided for a client using `allLocales` modifier, response type defines locale keys for entry field values.
* @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/introduction/cursor-pagination | REST API cursor pagination reference}
* @see {@link https://www.contentful.com/developers/docs/javascript/tutorials/using-js-cda-sdk/#retrieving-entries-with-search-parameters | JS SDK tutorial}
* @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters | REST API reference}
* @example
* ```typescript
* const contentful = require('contentful')
*
* const client = contentful.createClient({
* space: '<space_id>',
* accessToken: '<content_delivery_api_key>'
* })
*
* const response = await client.getEntriesWithCursor()
* console.log(response.items)
* ```
*/
getEntriesWithCursor<
EntrySkeleton extends EntrySkeletonType = EntrySkeletonType,
Locales extends LocaleCode = LocaleCode,
>(
query?: EntriesQueriesWithCursor<EntrySkeleton, Modifiers>,
): Promise<EntryCursorPaginatedCollection<EntrySkeleton, Modifiers, Locales>>

/**
* Parse raw json data into a collection of entries. objects.Links will be resolved also
* @param data - json data
Expand Down Expand Up @@ -495,6 +532,30 @@ export interface ContentfulClientApi<Modifiers extends ChainModifiers> {
query?: AssetsQueries<AssetFields, Modifiers>,
): Promise<AssetCollection<Modifiers, Locales>>

/**
* Fetches a cursor paginated collection of assets
* @param pagination - Object with cursor pagination options
* @param query - Object with search parameters
* @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/introduction/cursor-pagination | REST API cursor pagination reference}
* @see {@link https://www.contentful.com/developers/docs/javascript/tutorials/using-js-cda-sdk/#retrieving-entries-with-search-parameters | JS SDK tutorial}
* @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters | REST API reference}
* @returns Promise for a cursor paginated collection of Assets
* @typeParam Locales - If provided for a client using `allLocales` modifier, response type defines locale keys for asset field values.
* @example
* const contentful = require('contentful')
*
* const client = contentful.createClient({
* space: '<space_id>',
* accessToken: '<content_delivery_api_key>'
* })
*
* const response = await client.getAssetsWithCursor()
* console.log(response.items)
*/
getAssetsWithCursor<Locales extends LocaleCode = LocaleCode>(
query?: AssetsQueriesWithCursor<AssetFields, Modifiers>,
): Promise<AssetCursorPaginatedCollection<Modifiers, Locales>>

/**
* A client that will fetch assets and entries with all locales. Only available if not already enabled.
*/
Expand Down
54 changes: 44 additions & 10 deletions lib/types/collection.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,50 @@
import type { AssetSys } from './asset.js'
import type { EntrySys } from './entry.js'
export type CollectionBase<T> = {
limit: number
items: Array<T>
sys?: {
type: 'Array'
}
}

export type OffsetPagination = {
total: number
skip: number
}

export type CursorPagination = {
pages: {
next?: string
prev?: string
}
}

/**
* A wrapper object containing additional information for
* a collection of Contentful resources
* an offset paginated collection of Contentful resources
* @category Entity
* @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/introduction/collection-resources-and-pagination | Documentation}
*/
export interface ContentfulCollection<T> {
total: number
skip: number
limit: number
items: Array<T>
sys?: AssetSys | EntrySys
}
export type OffsetPaginatedCollection<T = unknown> = CollectionBase<T> & OffsetPagination

/**
* A wrapper object containing additional information for
* an offset paginated collection of Contentful resources
* @category Entity
* @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/introduction/collection-resources-and-pagination | Documentation}
*/
export interface ContentfulCollection<T = unknown> extends OffsetPaginatedCollection<T> {}

/**
* A wrapper object containing additional information for
* a curisor paginated collection of Contentful resources
* @category Entity
* @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/introduction/cursor-pagination | Documentation}
*/
export type CursorPaginatedCollection<T = unknown> = CollectionBase<T> & CursorPagination

export type WithCursorPagination = { cursor: true }

export type CollectionForQuery<
T = unknown,
Query extends Record<string, unknown> = Record<string, unknown>,
> = Query extends WithCursorPagination ? CursorPaginatedCollection<T> : OffsetPaginatedCollection<T>
Loading