├── .nvmrc ├── .husky ├── pre-commit └── pre-push ├── lib ├── global.d.ts ├── types │ ├── asset-key.ts │ ├── timeline-preview.ts │ ├── resource-link.ts │ ├── metadata.ts │ ├── query │ │ ├── index.ts │ │ ├── reference.ts │ │ ├── set.ts │ │ ├── existence.ts │ │ ├── search.ts │ │ ├── range.ts │ │ ├── select.ts │ │ ├── location.ts │ │ ├── subset.ts │ │ ├── util.ts │ │ ├── equality.ts │ │ └── order.ts │ ├── index.ts │ ├── space.ts │ ├── locale.ts │ ├── concept-scheme.ts │ ├── sys.ts │ ├── tag.ts │ ├── link.ts │ ├── concept.ts │ ├── collection.ts │ ├── sync.ts │ ├── asset.ts │ └── content-type.ts ├── utils │ ├── validation-error.ts │ ├── validate-search-parameters.ts │ ├── resolve-circular.ts │ ├── query-selection-set.ts │ ├── normalize-search-parameters.ts │ ├── normalize-cursor-pagination-parameters.ts │ ├── client-helpers.ts │ ├── normalize-select.ts │ ├── normalize-cursor-pagination-response.ts │ ├── validate-timestamp.ts │ ├── timeline-preview-helpers.ts │ └── validate-params.ts ├── index.ts ├── mixins │ └── stringify-safe.ts ├── create-global-options.ts └── make-client.ts ├── test ├── output-integration │ ├── browser │ │ ├── .gitignore │ │ ├── vitest.config.ts │ │ ├── README.md │ │ ├── public │ │ │ ├── index.html │ │ │ └── index.js │ │ ├── package.json │ │ ├── vitest.setup.ts │ │ └── test │ │ │ └── index.test.js │ └── node │ │ ├── README.md │ │ ├── vitest.config.ts │ │ ├── package.json │ │ ├── index.test.js │ │ └── package-lock.json ├── integration │ ├── getConcept.test.ts │ ├── getConceptScheme.test.ts │ ├── getConceptAncestors.test.ts │ ├── getConceptDescendants.test.ts │ ├── getConceptSchemes.test.ts │ ├── getConcepts.test.ts │ ├── getAsset.test.ts │ ├── getAssetsWithCursor.test.ts │ ├── getEntriesWithCursor.test.ts │ ├── utils.ts │ ├── getTags.test.ts │ ├── sync.test.ts │ └── getAssets.test.ts ├── types │ ├── query-types │ │ ├── tagName.test-d.ts │ │ ├── object.test-d.ts │ │ ├── richtext.test-d.ts │ │ ├── boolean.test-d.ts │ │ ├── date.test-d.ts │ │ ├── text.test-d.ts │ │ ├── number.test-d.ts │ │ ├── symbol.test-d.ts │ │ ├── integer.test-d.ts │ │ ├── location.test-d.ts │ │ └── symbol-array.test-d.ts │ ├── client │ │ ├── getAssets.test-d.ts │ │ ├── createClient.test-d.ts │ │ └── parseEntries.test-d.ts │ ├── queries │ │ └── cursor-pagination-queries.test-d.ts │ ├── chain-options.test-d.ts │ ├── asset-d.ts │ ├── mocks.ts │ └── query.test-d.ts └── unit │ ├── utils │ ├── normalize-search-parameters.test.ts │ ├── validate-search-parameters.test.ts │ ├── normalize-select.test.ts │ ├── validate-timestamp.test.ts │ ├── validate-params.test.ts │ ├── normalize-cursor-pagination-parameters.test.ts │ └── normalize-cursor-pagination-response.test.ts │ ├── timeline-preview.test.ts │ └── contentful.test.ts ├── .npmrc ├── .prettierignore ├── vitest.setup.ts ├── .github ├── CODEOWNERS ├── workflows │ ├── dependabot-approve-and-request-merge.yaml │ ├── codeql.yaml │ ├── build.yaml │ ├── main.yaml │ ├── release.yaml │ ├── check.yaml │ └── failure-notification.yaml ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── ISSUE_TEMPLATE.md ├── images ├── contentful-icon.png ├── static-query-keys.png └── dynamic-query-keys.png ├── .prettierrc ├── esdoc.json ├── .contentful └── vault-secrets.yaml ├── .puppeteerrc.cjs ├── typedoc.json ├── vitest.config.ts ├── catalog-info.yaml ├── tsconfig.json ├── LICENSE ├── eslint.config.js ├── DOCS.md ├── .gitignore └── rollup.config.js /.nvmrc: -------------------------------------------------------------------------------- 1 | v18 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | lint-staged -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | npm run test:prepush -------------------------------------------------------------------------------- /lib/global.d.ts: -------------------------------------------------------------------------------- 1 | declare const __VERSION__: string 2 | -------------------------------------------------------------------------------- /test/output-integration/browser/.gitignore: -------------------------------------------------------------------------------- 1 | public/contentful*.js 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | @contentful:registry=https://registry.npmjs.org 2 | ignore-scripts=true 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | test/output-integration/browser/public/contentful.browser.min.js -------------------------------------------------------------------------------- /vitest.setup.ts: -------------------------------------------------------------------------------- 1 | import { version } from './package.json' 2 | 3 | global.__VERSION__ = version 4 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @contentful/team-developer-experience 2 | 3 | package.json 4 | package-lock.json 5 | -------------------------------------------------------------------------------- /images/contentful-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/contentful.js/master/images/contentful-icon.png -------------------------------------------------------------------------------- /images/static-query-keys.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/contentful.js/master/images/static-query-keys.png -------------------------------------------------------------------------------- /images/dynamic-query-keys.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/contentful.js/master/images/dynamic-query-keys.png -------------------------------------------------------------------------------- /lib/types/asset-key.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @category Asset 3 | */ 4 | export type AssetKey = { 5 | secret: string 6 | policy: string 7 | } 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "singleQuote": true, 6 | "semi": false 7 | } 8 | -------------------------------------------------------------------------------- /esdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": "./lib", 3 | "destination": "./out", 4 | "index": "./README.md", 5 | "package": "./package.json", 6 | "title": "contentful.js" 7 | } 8 | -------------------------------------------------------------------------------- /.contentful/vault-secrets.yaml: -------------------------------------------------------------------------------- 1 | version: 1 2 | services: 3 | github-action: 4 | policies: 5 | - dependabot 6 | - packages-read 7 | - semantic-release 8 | -------------------------------------------------------------------------------- /lib/types/timeline-preview.ts: -------------------------------------------------------------------------------- 1 | export type TimelinePreview = { 2 | release?: { lte: string } 3 | timestamp?: { lte: string | Date } 4 | } & ({ release: { lte: string } } | { timestamp: { lte: string | Date } }) 5 | -------------------------------------------------------------------------------- /.puppeteerrc.cjs: -------------------------------------------------------------------------------- 1 | const { join } = require('path') 2 | 3 | /** 4 | * @type {import("puppeteer").Configuration} 5 | */ 6 | module.exports = { 7 | cacheDirectory: join(__dirname, 'node_modules', '.puppeteer_cache'), 8 | } 9 | -------------------------------------------------------------------------------- /lib/utils/validation-error.ts: -------------------------------------------------------------------------------- 1 | export class ValidationError extends Error { 2 | constructor(name: string, message: string) { 3 | super(`Invalid "${name}" provided, ` + message) 4 | this.name = 'ValidationError' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/output-integration/node/README.md: -------------------------------------------------------------------------------- 1 | ## Node demo 2 | 3 | This small demo application shows how the contentful.js library can be used in a node client. 4 | Furthermore it is also used to test if the build bundles can be correctly accessed by a client. 5 | -------------------------------------------------------------------------------- /test/output-integration/node/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | dir: '.', 7 | environment: 'node', 8 | testTimeout: 10000, 9 | }, 10 | }) 11 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './contentful.js' 2 | export * from './create-global-options.js' 3 | export * from './mixins/stringify-safe.js' 4 | export * from './utils/normalize-select.js' 5 | export * from './utils/resolve-circular.js' 6 | 7 | export * from './types/index.js' 8 | -------------------------------------------------------------------------------- /lib/types/resource-link.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Definition of an external resource link 3 | * @category Link 4 | */ 5 | export interface ResourceLink { 6 | type: 'ResourceLink' 7 | linkType: LinkType 8 | urn: string 9 | } 10 | -------------------------------------------------------------------------------- /lib/types/metadata.ts: -------------------------------------------------------------------------------- 1 | import type { TagLink, TaxonomyConceptLink } from './link.js' 2 | 3 | /** 4 | * User-controlled metadata 5 | * @category Entity 6 | */ 7 | export type Metadata = { 8 | tags: { sys: TagLink }[] 9 | concepts?: { sys: TaxonomyConceptLink }[] 10 | } 11 | -------------------------------------------------------------------------------- /test/output-integration/browser/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | environment: 'node', 7 | setupFiles: ['./vitest.setup.ts'], 8 | testTimeout: 10000, 9 | }, 10 | }) 11 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryPoints": ["lib/index.ts"], 3 | "out": "./out", 4 | "readme": "DOCS.md", 5 | "name": "contentful.js", 6 | "exclude": [], 7 | "includeVersion": false, 8 | "hideGenerator": true, 9 | "excludePrivate": true, 10 | "categorizeByGroup": false 11 | } 12 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | environment: 'node', 7 | coverage: { 8 | include: ['lib/**/*.{ts,tsx,js,jsx}'], 9 | }, 10 | setupFiles: ['./vitest.setup.ts'], 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /test/output-integration/browser/README.md: -------------------------------------------------------------------------------- 1 | ## Browser demo 2 | 3 | This small demo application shows how the contentful.js library can be used in a browser client. 4 | Furthermore it is also used to test if the build bundles can be correctly accessed by a client. 5 | 6 | To run the test execute: 7 | 8 | ```sh 9 | npm install && npm run test 10 | ``` 11 | -------------------------------------------------------------------------------- /test/output-integration/node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "contentful-js-node-demo", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "vitest --run" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "contentful": "file:../../.." 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-approve-and-request-merge.yaml: -------------------------------------------------------------------------------- 1 | name: "dependabot approve-and-request-merge" 2 | 3 | on: pull_request_target 4 | 5 | jobs: 6 | worker: 7 | permissions: 8 | contents: write 9 | id-token: write 10 | runs-on: ubuntu-latest 11 | if: github.actor == 'dependabot[bot]' 12 | steps: 13 | - uses: contentful/github-auto-merge@v1 14 | with: 15 | VAULT_URL: ${{ secrets.VAULT_URL }} 16 | -------------------------------------------------------------------------------- /lib/utils/validate-search-parameters.ts: -------------------------------------------------------------------------------- 1 | export default function validateSearchParameters(query: Record): void { 2 | for (const key in query) { 3 | const value = query[key] 4 | // We don’t allow any objects as values for query parameters 5 | if (typeof value === 'object' && value !== null && !Array.isArray(value)) { 6 | throw new Error(`Objects are not supported as value for the "${key}" query parameter.`) 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/utils/resolve-circular.ts: -------------------------------------------------------------------------------- 1 | import resolveResponse from 'contentful-resolve-response' 2 | 3 | import mixinStringifySafe from '../mixins/stringify-safe.js' 4 | 5 | export default function resolveCircular(data: any, { resolveLinks, removeUnresolved }): any { 6 | const wrappedData = mixinStringifySafe(data) 7 | if (resolveLinks) { 8 | wrappedData.items = resolveResponse(wrappedData, { 9 | removeUnresolved, 10 | itemEntryPoints: ['fields'], 11 | }) 12 | } 13 | return wrappedData 14 | } 15 | -------------------------------------------------------------------------------- /lib/types/query/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | type FieldsType, 3 | EntrySkeletonType, 4 | type ConditionalFixedQueries, 5 | type ConditionalListQueries, 6 | } from './util.js' 7 | 8 | export * from './equality.js' 9 | export * from './existence.js' 10 | export * from './location.js' 11 | export * from './order.js' 12 | export * from './query.js' 13 | export * from './range.js' 14 | export * from './reference.js' 15 | export * from './search.js' 16 | export * from './select.js' 17 | export * from './set.js' 18 | export * from './subset.js' 19 | -------------------------------------------------------------------------------- /test/integration/getConcept.test.ts: -------------------------------------------------------------------------------- 1 | import * as contentful from '../../lib/contentful' 2 | import { params } from './utils' 3 | 4 | if (process.env.API_INTEGRATION_TESTS) { 5 | params.host = '127.0.0.1:5000' 6 | params.insecure = true 7 | } 8 | 9 | const client = contentful.createClient(params) 10 | 11 | describe('getConcept', () => { 12 | it('returns a single concept', async () => { 13 | const response = await client.getConcept('3eXhEIEzcZqwHyYWHbzSoS') 14 | 15 | expect(response.sys.type).toBe('TaxonomyConcept') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /lib/utils/query-selection-set.ts: -------------------------------------------------------------------------------- 1 | export default function getQuerySelectionSet(query: Record): Set { 2 | if (!query.select) { 3 | return new Set() 4 | } 5 | 6 | // The selection of fields for the query is limited 7 | // Get the different parts that are listed for selection 8 | const allSelects = Array.isArray(query.select) 9 | ? query.select 10 | : query.select.split(',').map((q) => q.trim()) 11 | 12 | // Move the parts into a set for easy access and deduplication 13 | return new Set(allSelects) 14 | } 15 | -------------------------------------------------------------------------------- /test/integration/getConceptScheme.test.ts: -------------------------------------------------------------------------------- 1 | import * as contentful from '../../lib/contentful' 2 | import { params } from './utils' 3 | 4 | if (process.env.API_INTEGRATION_TESTS) { 5 | params.host = '127.0.0.1:5000' 6 | params.insecure = true 7 | } 8 | 9 | const client = contentful.createClient(params) 10 | 11 | describe('getConceptScheme', () => { 12 | it('returns a single concept scheme', async () => { 13 | const response = await client.getConceptScheme('7lcOh0M5JAu5xvEwWzs00H') 14 | 15 | expect(response.sys.type).toBe('TaxonomyConceptScheme') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /catalog-info.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: backstage.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: contentful.js 5 | description: | 6 | JavaScript library for Contentful's Delivery API (node & browser). 7 | annotations: 8 | circleci.com/project-slug: github/contentful/contentful.js 9 | github.com/project-slug: contentful/contentful.js 10 | contentful.com/ci-alert-slack: prd-ecosystem-dx-bots 11 | contentful.com/service-tier: "4" 12 | tags: 13 | - tier-4 14 | spec: 15 | type: library 16 | lifecycle: production 17 | owner: group:team-developer-experience 18 | -------------------------------------------------------------------------------- /lib/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './asset.js' 2 | export * from './asset-key.js' 3 | export { AddChainModifier, ChainModifiers, ContentfulClientApi } from './client.js' 4 | export * from './collection.js' 5 | export * from './concept-scheme.js' 6 | export * from './concept.js' 7 | export * from './content-type.js' 8 | export * from './entry.js' 9 | export * from './link.js' 10 | export * from './locale.js' 11 | export * from './metadata.js' 12 | export * from './query/index.js' 13 | export * from './resource-link.js' 14 | export * from './space.js' 15 | export * from './sync.js' 16 | export * from './sys.js' 17 | export * from './tag.js' 18 | -------------------------------------------------------------------------------- /test/output-integration/browser/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Contentful.js Test Page 8 | 9 | 10 | 11 | 12 |
This text should be replaced by the response
13 |
This text should be replaced by the client version
14 | 15 | 16 | -------------------------------------------------------------------------------- /lib/utils/normalize-search-parameters.ts: -------------------------------------------------------------------------------- 1 | export default function normalizeSearchParameters(query: Record): Record { 2 | const convertedQuery = {} 3 | let hasConverted = false 4 | for (const key in query) { 5 | // We allow multiple values to be passed as arrays 6 | // which have to be converted to comma-separated strings before being sent to the API 7 | if (Array.isArray(query[key])) { 8 | convertedQuery[key] = query[key].join(',') 9 | hasConverted = true 10 | } 11 | } 12 | 13 | if (hasConverted) { 14 | return { ...query, ...convertedQuery } 15 | } 16 | 17 | return query 18 | } 19 | -------------------------------------------------------------------------------- /lib/mixins/stringify-safe.ts: -------------------------------------------------------------------------------- 1 | import jsonStringifySafe from 'json-stringify-safe' 2 | 3 | export default function mixinStringifySafe(data) { 4 | return Object.defineProperty(data, 'stringifySafe', { 5 | enumerable: false, 6 | configurable: false, 7 | writable: false, 8 | value: function (serializer = null, indent = '') { 9 | return jsonStringifySafe(this, serializer, indent, (key, value) => { 10 | return { 11 | sys: { 12 | type: 'Link', 13 | linkType: 'Entry', 14 | id: value.sys.id, 15 | circular: true, 16 | }, 17 | } 18 | }) 19 | }, 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/esm-raw", 4 | "rootDir": "./lib", 5 | "lib": ["dom", "esnext"], 6 | "target": "ES2017", 7 | "module": "es2020", 8 | "moduleResolution": "node", 9 | "allowJs": true, 10 | "declarationDir": "dist/types", 11 | "declaration": true, 12 | "strict": true, 13 | "importHelpers": true, 14 | "isolatedModules": false, 15 | "esModuleInterop": true, 16 | "noImplicitThis": false, 17 | "noImplicitAny": false, 18 | "skipLibCheck": true, 19 | }, 20 | "include": ["./lib/**/*", "types"], 21 | "exclude": ["./node_modules/**/*", "dist", "test"], 22 | } 23 | -------------------------------------------------------------------------------- /test/output-integration/browser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "contentful-js-browser-demo", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.test.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "vitest --run", 9 | "install:bundle": "cp ../../../dist/contentful.browser.min.js ./public/.", 10 | "install:browser": "npx puppeteer browser install", 11 | "setup-puppeteer": "node ./node_modules/puppeteer/install.mjs", 12 | "setup-test-env": "npm run setup-puppeteer && npm run install:browser && npm run install:bundle" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "devDependencies": { 17 | "puppeteer": "^23.9.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/utils/normalize-cursor-pagination-parameters.ts: -------------------------------------------------------------------------------- 1 | type NormalizedCursorPaginationParams> = Omit< 2 | Query, 3 | 'cursor' | 'skip' 4 | > & { cursor: true } 5 | 6 | export function normalizeCursorPaginationParameters>( 7 | query: Query, 8 | ): NormalizedCursorPaginationParams { 9 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 10 | const { cursor, pagePrev, pageNext, skip, ...rest } = query 11 | 12 | return { 13 | ...rest, 14 | cursor: true, 15 | ...(!!pagePrev && { pagePrev }), 16 | ...(!!pageNext && { pageNext }), 17 | } as NormalizedCursorPaginationParams 18 | } 19 | -------------------------------------------------------------------------------- /lib/types/query/reference.ts: -------------------------------------------------------------------------------- 1 | import type { EntryFieldTypes } from '../entry.js' 2 | import type { ConditionalPick } from 'type-fest' 3 | 4 | type SupportedTypes = 5 | | EntryFieldTypes.Array> 6 | | EntryFieldTypes.EntryLink 7 | | undefined 8 | 9 | /** 10 | * Search on references in provided fields 11 | * @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/search-on-references | Documentation} 12 | * @internal 13 | */ 14 | export type ReferenceSearchFilters = { 15 | [FieldName in keyof ConditionalPick as `${Prefix}.${string & 16 | FieldName}.${string}`]?: any 17 | } 18 | -------------------------------------------------------------------------------- /test/output-integration/browser/public/index.js: -------------------------------------------------------------------------------- 1 | async function run() { 2 | if (!contentful) { 3 | throw 'Contentful.js could not be loaded. Please check the build output.' 4 | } 5 | 6 | const client = contentful.createClient({ 7 | accessToken: 'QGT8WxED1nwrbCUpY6VEK6eFvZwvlC5ujlX-rzUq97U', 8 | space: 'ezs1swce23xe', 9 | }) 10 | 11 | const response = await client.getEntry('nyancat') 12 | 13 | const loadedDiv = document.createElement('div') 14 | loadedDiv.id = 'contentful-loaded' 15 | document.querySelector('body').appendChild(loadedDiv) 16 | 17 | document.querySelector('#content').innerHTML = response.sys.id 18 | 19 | document.querySelector('#version').innerHTML = client.version 20 | } 21 | 22 | run() 23 | -------------------------------------------------------------------------------- /test/integration/getConceptAncestors.test.ts: -------------------------------------------------------------------------------- 1 | import * as contentful from '../../lib/contentful' 2 | import { params } from './utils' 3 | 4 | if (process.env.API_INTEGRATION_TESTS) { 5 | params.host = '127.0.0.1:5000' 6 | params.insecure = true 7 | } 8 | 9 | type AvailableLocales = 'de-de' | 'en-US' 10 | 11 | const client = contentful.createClient(params) 12 | 13 | describe('getConcept', () => { 14 | it('returns a single concept', async () => { 15 | const response = await client.getConceptAncestors('3eXhEIEzcZqwHyYWHbzSoS') 16 | 17 | expect(response.sys.type).toBe('Array') 18 | expect(response.items.length).toBeGreaterThan(0) 19 | expect(response.items?.[0]?.sys.type).toBe('TaxonomyConcept') 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /test/integration/getConceptDescendants.test.ts: -------------------------------------------------------------------------------- 1 | import * as contentful from '../../lib/contentful' 2 | import { params } from './utils' 3 | 4 | if (process.env.API_INTEGRATION_TESTS) { 5 | params.host = '127.0.0.1:5000' 6 | params.insecure = true 7 | } 8 | 9 | type AvailableLocales = 'de-de' | 'en-US' 10 | 11 | const client = contentful.createClient(params) 12 | 13 | describe('getConcept', () => { 14 | it('returns a single concept', async () => { 15 | const response = await client.getConceptDescendants('3eXhEIEzcZqwHyYWHbzSoS') 16 | 17 | expect(response.sys.type).toBe('Array') 18 | expect(response.items.length).toBeGreaterThan(0) 19 | expect(response.items[0].sys.type).toBe('TaxonomyConcept') 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /lib/types/space.ts: -------------------------------------------------------------------------------- 1 | import type { Locale } from './locale.js' 2 | import type { BaseSys } from './sys.js' 3 | 4 | /** 5 | * System managed metadata for spaces 6 | * @category Entity 7 | * @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/introduction/common-resource-attributes | CDA documentation on common attributes} 8 | */ 9 | export interface SpaceSys extends BaseSys { 10 | type: 'Space' 11 | } 12 | 13 | /** 14 | * Properties of a space 15 | * @category Entity 16 | * @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/spaces | CDA documentation on Spaces} 17 | */ 18 | export interface Space { 19 | sys: SpaceSys 20 | name: string 21 | locales: Array> 22 | } 23 | -------------------------------------------------------------------------------- /lib/types/locale.ts: -------------------------------------------------------------------------------- 1 | import type { ContentfulCollection } from './collection.js' 2 | import type { BaseSys } from './sys.js' 3 | 4 | /** 5 | * @category Entity 6 | */ 7 | export type LocaleCode = string 8 | 9 | /** 10 | * System managed metadata for locale 11 | * @category Entity 12 | */ 13 | export interface LocaleSys extends BaseSys { 14 | type: 'Locale' 15 | version: number 16 | } 17 | 18 | /** 19 | * Properties of a single locale definition 20 | * @category Entity 21 | */ 22 | export interface Locale { 23 | code: string 24 | name: string 25 | default: boolean 26 | fallbackCode: string | null 27 | sys: LocaleSys 28 | } 29 | 30 | /** 31 | * Collection of locales 32 | * @category Entity 33 | */ 34 | export type LocaleCollection = ContentfulCollection 35 | -------------------------------------------------------------------------------- /lib/types/query/set.ts: -------------------------------------------------------------------------------- 1 | import type { EntryFieldType, EntryFieldTypes } from '../index.js' 2 | import type { EntryFieldsConditionalListQueries } from './util.js' 3 | 4 | type SupportedTypes = 5 | | EntryFieldTypes.Symbol 6 | | EntryFieldTypes.Array 7 | | EntryFieldTypes.Text 8 | | undefined 9 | 10 | /** 11 | * Match multiple values in provided fields of an entry 12 | * @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/array-with-multiple-values | Documentation} 13 | * @internal 14 | */ 15 | export type EntryFieldsSetFilter< 16 | Fields extends Record>, 17 | Prefix extends string, 18 | > = EntryFieldsConditionalListQueries 19 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "CodeQL Scan for GitHub Actions Workflows" 3 | 4 | on: 5 | push: 6 | branches: [main, master] 7 | paths: [".github/workflows/**"] 8 | pull_request: 9 | branches: [main, master] 10 | paths: [".github/workflows/**"] 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze GitHub Actions workflows 15 | runs-on: ubuntu-latest 16 | permissions: 17 | actions: read 18 | contents: read 19 | security-events: write 20 | 21 | steps: 22 | - uses: actions/checkout@v5 23 | 24 | - name: Initialize CodeQL 25 | uses: github/codeql-action/init@v4 26 | with: 27 | languages: actions 28 | 29 | - name: Run CodeQL Analysis 30 | uses: github/codeql-action/analyze@v4 31 | with: 32 | category: actions 33 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 7 | 8 | ## Summary 9 | 10 | 11 | 12 | ## Description 13 | 14 | 15 | 16 | ## Motivation and Context 17 | 18 | 22 | 23 | ## PR Checklist 24 | 25 | - [ ] I have read the `CONTRIBUTING.md` file 26 | - [ ] All commits follow [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) 27 | - [ ] Documentation is updated (if necessary) 28 | - [ ] PR doesn't contain any sensitive information 29 | - [ ] There are no breaking changes 30 | -------------------------------------------------------------------------------- /test/output-integration/browser/vitest.setup.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, beforeEach, afterAll } from 'vitest' 2 | 3 | import puppeteer, { Browser, Page, PuppeteerNode } from 'puppeteer' 4 | import path from 'path' 5 | 6 | let browser: Browser 7 | let page: Page 8 | 9 | beforeAll(async () => { 10 | browser = await puppeteer.launch({ 11 | headless: true, 12 | args: ['--no-sandbox', '--disable-setuid-sandbox'], 13 | }) 14 | }) 15 | 16 | beforeEach(async () => { 17 | page = await browser.newPage() 18 | 19 | page.on('console', (msg) => console.log('CONSOLE LOG:', msg.text)) 20 | page.on('error', (err) => console.log('CONSOLE ERROR:', err, err.message)) 21 | 22 | await page.goto(`file:${path.join(__dirname, 'public/index.html')}`) 23 | 24 | await page.waitForSelector('#contentful-loaded', { timeout: 5_000 }) 25 | }) 26 | 27 | afterAll(async () => { 28 | await browser.close() 29 | }) 30 | 31 | export { page } 32 | -------------------------------------------------------------------------------- /lib/utils/client-helpers.ts: -------------------------------------------------------------------------------- 1 | import type { ChainModifiers } from '../types/client.js' 2 | 3 | export type ChainOption = { 4 | withoutLinkResolution: ChainModifiers extends Modifiers 5 | ? boolean 6 | : 'WITHOUT_LINK_RESOLUTION' extends Modifiers 7 | ? true 8 | : false 9 | withAllLocales: ChainModifiers extends Modifiers 10 | ? boolean 11 | : 'WITH_ALL_LOCALES' extends Modifiers 12 | ? true 13 | : false 14 | withoutUnresolvableLinks: ChainModifiers extends Modifiers 15 | ? boolean 16 | : 'WITHOUT_UNRESOLVABLE_LINKS' extends Modifiers 17 | ? true 18 | : false 19 | } 20 | 21 | export type DefaultChainOption = ChainOption 22 | 23 | export type ChainOptions = ChainOption 24 | 25 | export type ModifiersFromOptions = 26 | Options extends ChainOption ? Modifiers : never 27 | -------------------------------------------------------------------------------- /lib/utils/normalize-select.ts: -------------------------------------------------------------------------------- 1 | import getQuerySelectionSet from './query-selection-set.js' 2 | 3 | /* 4 | * sdk relies heavily on sys metadata 5 | * so we cannot omit the sys property on sdk level entirely 6 | * and we have to ensure that at least `id` and `type` are present 7 | * */ 8 | 9 | export default function normalizeSelect(query) { 10 | if (!query.select) { 11 | return query 12 | } 13 | 14 | const selectedSet = getQuerySelectionSet(query) 15 | 16 | // If we already select all of `sys` we can just return 17 | // since we're anyway fetching everything that is needed 18 | if (selectedSet.has('sys')) { 19 | return query 20 | } 21 | 22 | // We don't select `sys` so we need to ensure the minimum set 23 | selectedSet.add('sys.id') 24 | selectedSet.add('sys.type') 25 | 26 | // Reassign the normalized sys properties 27 | return { 28 | ...query, 29 | select: [...selectedSet].join(','), 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/utils/normalize-cursor-pagination-response.ts: -------------------------------------------------------------------------------- 1 | import type { CursorPagination } from '../types' 2 | 3 | function extractQueryParam(key: string, url?: string): string | undefined | null { 4 | const queryString = url?.split('?')[1] 5 | 6 | if (!queryString) { 7 | return 8 | } 9 | 10 | return new URLSearchParams(queryString).get(key) 11 | } 12 | 13 | const Pages = { 14 | prev: 'pagePrev', 15 | next: 'pageNext', 16 | } as const 17 | 18 | export function normalizeCursorPaginationResponse( 19 | response: Response, 20 | ): Response { 21 | const pages: CursorPagination['pages'] = {} 22 | 23 | for (const [responseKey, queryKey] of Object.entries(Pages)) { 24 | const cursorToken = extractQueryParam(queryKey, response.pages[responseKey]) 25 | 26 | if (cursorToken) { 27 | pages[responseKey] = cursorToken 28 | } 29 | } 30 | 31 | return { 32 | ...response, 33 | pages, 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/types/concept-scheme.ts: -------------------------------------------------------------------------------- 1 | import type { CursorPaginatedCollection } from './collection' 2 | import type { UnresolvedLink } from './link' 3 | import type { LocaleCode } from './locale' 4 | 5 | type ISODateString = string 6 | 7 | export type ConceptSchemeSys = { 8 | id: string 9 | type: 'TaxonomyConceptScheme' 10 | createdAt: ISODateString 11 | updatedAt: ISODateString 12 | version: number 13 | } 14 | 15 | export interface ConceptScheme { 16 | sys: ConceptSchemeSys 17 | uri?: string 18 | prefLabel: { 19 | [locale in Locales]: string 20 | } 21 | definition?: 22 | | { 23 | [locale in Locales]: string 24 | } 25 | | null 26 | topConcepts: UnresolvedLink<'TaxonomyConcept'>[] 27 | concepts: UnresolvedLink<'TaxonomyConcept'>[] 28 | totalConcepts: number 29 | } 30 | 31 | export type ConceptSchemeCollection = CursorPaginatedCollection< 32 | ConceptScheme 33 | > 34 | -------------------------------------------------------------------------------- /lib/utils/validate-timestamp.ts: -------------------------------------------------------------------------------- 1 | import { ValidationError } from './validation-error.js' 2 | 3 | type Options = { 4 | maximum?: number 5 | now?: number 6 | } 7 | 8 | export default function validateTimestamp( 9 | name: string, 10 | timestamp: number, 11 | options?: Options, 12 | ): void { 13 | options = options || {} 14 | 15 | if (typeof timestamp !== 'number') { 16 | throw new ValidationError( 17 | name, 18 | `only numeric values are allowed for timestamps, provided type was "${typeof timestamp}"`, 19 | ) 20 | } 21 | if (options.maximum && timestamp > options.maximum) { 22 | throw new ValidationError( 23 | name, 24 | `value (${timestamp}) cannot be further in the future than expected maximum (${options.maximum})`, 25 | ) 26 | } 27 | if (options.now && timestamp < options.now) { 28 | throw new ValidationError( 29 | name, 30 | `value (${timestamp}) cannot be in the past, current time was ${options.now}`, 31 | ) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/types/query-types/tagName.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectAssignable, expectNotAssignable } from 'tsd' 2 | import { TagNameFilters } from '../../../lib' 3 | 4 | // @ts-ignore 5 | import * as mocks from '../mocks' 6 | 7 | expectAssignable({ 8 | 'name[exists]': mocks.booleanValue, 9 | name: mocks.stringValue, 10 | 'name[match]': mocks.stringValue, 11 | 'name[ne]': mocks.stringValue, 12 | 'name[in]': mocks.stringArrayValue, 13 | 'name[nin]': mocks.stringArrayValue, 14 | }) 15 | 16 | expectNotAssignable({ 'name[near]': mocks.anyValue }) 17 | expectNotAssignable({ 'name[within]': mocks.anyValue }) 18 | expectNotAssignable({ select: mocks.anyValue }) 19 | expectNotAssignable({ 'name[lt]': mocks.anyValue }) 20 | expectNotAssignable({ 'name[lte]': mocks.anyValue }) 21 | expectNotAssignable({ 'name[gt]': mocks.anyValue }) 22 | expectNotAssignable({ 'name[gte]': mocks.anyValue }) 23 | -------------------------------------------------------------------------------- /test/unit/utils/normalize-search-parameters.test.ts: -------------------------------------------------------------------------------- 1 | import normalizeSearchParameters from '../../../lib/utils/normalize-search-parameters' 2 | 3 | describe('normalizeSearchParameters', () => { 4 | test('normalizeSearchParameters does nothing if all values are string values', () => { 5 | const query = { 6 | 'fields.stringParameter[in]': 'string1,string2', 7 | 'fields.locationParameter[within]': '0,1,2,3', 8 | } 9 | const normalized = normalizeSearchParameters(query) 10 | expect(normalized).toBe(query) 11 | }) 12 | 13 | test('normalizeSelect converts array values into string values', () => { 14 | const query = { 15 | 'fields.stringParameter[in]': ['string1', 'string2'], 16 | 'fields.locationParameter[within]': [0, 1, 2, 3], 17 | } 18 | const normalized = normalizeSearchParameters(query) 19 | expect(normalized).toEqual({ 20 | 'fields.stringParameter[in]': 'string1,string2', 21 | 'fields.locationParameter[within]': '0,1,2,3', 22 | }) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /lib/types/sys.ts: -------------------------------------------------------------------------------- 1 | import type { ContentSourceMapsLookup, CPAContentSourceMaps } from '@contentful/content-source-maps' 2 | import type { EntryFields } from './entry.js' 3 | import type { EnvironmentLink, SpaceLink } from './link.js' 4 | 5 | /** 6 | * Definition of common part of system managed metadata 7 | */ 8 | export interface BaseSys { 9 | type: string 10 | id: string 11 | } 12 | 13 | /** 14 | * System managed metadata for entities 15 | * @category Entity 16 | * @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/introduction/common-resource-attributes | CDA documentation on common attributes} 17 | */ 18 | export interface EntitySys extends BaseSys { 19 | createdAt: EntryFields.Date 20 | updatedAt: EntryFields.Date 21 | revision: number 22 | space: { sys: SpaceLink } 23 | environment: { sys: EnvironmentLink } 24 | locale?: string 25 | contentSourceMaps?: CPAContentSourceMaps 26 | contentSourceMapsLookup?: ContentSourceMapsLookup 27 | publishedVersion: number 28 | } 29 | -------------------------------------------------------------------------------- /test/output-integration/browser/test/index.test.js: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest' 2 | import { page } from './vitest.setup' 3 | import { version as packageVersion } from '../../../../package.json' 4 | 5 | describe('Contentful.js Browser Test', () => { 6 | it('Entry has been loaded successfully', async () => { 7 | const text = await page.$eval('#content', (el) => el.innerHTML) 8 | expect(text).toEqual('nyancat') 9 | }) 10 | 11 | it('Has correct user agent version', async () => { 12 | const clientVersion = await page.$eval('#version', (el) => el.innerHTML) 13 | 14 | // When we make a publish run, we need to ensure that semantic-release has set a valid package version 15 | if (process.env.PUBLISH_RUN === 'true') { 16 | expect(clientVersion).toEqual(expect.not.stringContaining('semantic-release')) 17 | expect(clientVersion).toEqual(packageVersion) 18 | } else { 19 | expect(clientVersion).toEqual(packageVersion) 20 | } 21 | console.log(`Client version: ${clientVersion}`) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /lib/types/tag.ts: -------------------------------------------------------------------------------- 1 | import type { EntitySys } from './sys.js' 2 | import type { ContentfulCollection } from './collection.js' 3 | import type { UserLink } from './link.js' 4 | 5 | /** 6 | * System managed metadata for tags 7 | * @category Tag 8 | * @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/introduction/common-resource-attributes | CDA documentation on common attributes} 9 | */ 10 | export interface TagSys extends Omit { 11 | version: number 12 | visibility: string 13 | createdBy: { sys: UserLink } 14 | updatedBy: { sys: UserLink } 15 | } 16 | 17 | /** 18 | * Properties for a single content tag definition 19 | * @category Tag 20 | * @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/content-tags | CDA documentation on Content Tags} 21 | */ 22 | export type Tag = { 23 | name: string 24 | sys: TagSys 25 | } 26 | 27 | /** 28 | * Collection of tags 29 | * @category Tag 30 | */ 31 | export type TagCollection = ContentfulCollection 32 | -------------------------------------------------------------------------------- /test/output-integration/node/index.test.js: -------------------------------------------------------------------------------- 1 | const contentful = require('contentful') 2 | 3 | /** 4 | * This test project should ensure that the builds are actually functioning. 5 | * Mostly useful for changes to building/transpiling/bundling/... 6 | */ 7 | 8 | const client = contentful.createClient({ 9 | accessToken: 'QGT8WxED1nwrbCUpY6VEK6eFvZwvlC5ujlX-rzUq97U', 10 | space: 'ezs1swce23xe', 11 | }) 12 | 13 | test('Gets entry', async () => { 14 | const response = await client.getEntry('nyancat') 15 | expect(response.sys).toBeDefined() 16 | expect(response.fields).toBeDefined() 17 | }) 18 | 19 | test('Has correct user agent version', async () => { 20 | const version = require('../../../package.json').version 21 | // When we make a publish run, we need to ensure that semantic-release has set a valid package version 22 | if (process.env.PUBLISH_RUN === 'true') { 23 | expect(client.version).toEqual(expect.not.stringContaining('semantic-release')) 24 | expect(client.version).toEqual(version) 25 | } else { 26 | expect(client.version).toEqual(version) 27 | } 28 | console.log(`Client version: ${client.version}`) 29 | }) 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Contentful 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | workflow_call: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v5 16 | 17 | - name: Setup Node.js 18 | uses: actions/setup-node@v6 19 | with: 20 | node-version: '24' 21 | cache: 'npm' 22 | 23 | - name: Install latest npm 24 | run: npm install -g npm@latest 25 | 26 | - name: Install dependencies 27 | run: npm ci 28 | 29 | # required for browser output-integration tests 30 | - name: Setup Chrome 31 | uses: browser-actions/setup-chrome@v2 32 | with: 33 | install-chromedriver: true 34 | 35 | - name: Build 36 | run: npm run build 37 | env: 38 | # required for browser output-integration tests 39 | PUPPETEER_EXECUTABLE_PATH: /usr/bin/google-chrome 40 | 41 | - name: Save Build folders 42 | uses: actions/cache/save@v4 43 | with: 44 | path: | 45 | dist 46 | key: build-cache-${{ github.run_id }}-${{ github.run_attempt }} 47 | -------------------------------------------------------------------------------- /lib/types/query/existence.ts: -------------------------------------------------------------------------------- 1 | import type { EntryField, EntryFieldType } from '../entry.js' 2 | import type { ConditionalFixedQueries, FieldsType, EntrySkeletonType } from './util.js' 3 | import type { AssetDetails, AssetFile } from '../asset.js' 4 | 5 | /** 6 | * Check for existence of provided fields 7 | * @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/existence | Documentation} 8 | * @internal 9 | */ 10 | export type ExistenceFilter< 11 | Fields extends FieldsType, 12 | Prefix extends string, 13 | > = ConditionalFixedQueries< 14 | Fields, 15 | EntryField> | AssetFile | AssetDetails | undefined, 16 | boolean, 17 | Prefix, 18 | '[exists]' 19 | > 20 | 21 | /** 22 | * Check for existence of provided fields in an entry 23 | * @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/existence | Documentation} 24 | * @internal 25 | */ 26 | export type EntryFieldsExistenceFilter< 27 | Fields extends FieldsType, 28 | Prefix extends string, 29 | > = ConditionalFixedQueries< 30 | Fields, 31 | EntryFieldType | undefined, 32 | boolean, 33 | Prefix, 34 | '[exists]' 35 | > 36 | -------------------------------------------------------------------------------- /lib/create-global-options.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @category Client 3 | */ 4 | export interface GlobalOptionsParams { 5 | environment?: string 6 | space?: string 7 | spaceBaseUrl?: string 8 | environmentBaseUrl?: string 9 | } 10 | 11 | /** 12 | * @category Client 13 | */ 14 | export type GetGlobalOptions = ( 15 | globalOptions?: GlobalOptionsParams, 16 | ) => Required 17 | 18 | /** 19 | * @param globalSettings - Global library settings 20 | * @returns getGlobalSettings - Method returning client settings 21 | * @category Client 22 | */ 23 | export function createGlobalOptions( 24 | globalSettings: Required, 25 | ): GetGlobalOptions { 26 | /** 27 | * Method merging pre-configured global options and provided local parameters 28 | * @param query - regular query object used for collection endpoints 29 | * @param query.environment - optional name of the environment 30 | * @param query.space - optional space ID 31 | * @param query.spaceBaseUrl - optional base URL for the space 32 | * @param query.environmentBaseUrl - optional base URL for the environment 33 | * @returns global options 34 | */ 35 | return function getGlobalOptions(query?: GlobalOptionsParams) { 36 | return Object.assign({}, globalSettings, query) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/unit/utils/validate-search-parameters.test.ts: -------------------------------------------------------------------------------- 1 | import validateSearchParameters from '../../../lib/utils/validate-search-parameters' 2 | 3 | describe('validateSearchParameters', () => { 4 | test('does nothing if no values are objects', () => { 5 | const query = { 6 | booleanValue: true, 7 | stringValue: 'string', 8 | numberValue: 3, 9 | nullValue: null, 10 | undefinedValue: undefined, 11 | arrayValue: ['string'], 12 | } 13 | 14 | validateSearchParameters(query) 15 | }) 16 | 17 | test('throws if a value is an object', () => { 18 | const query = { 19 | booleanValue: true, 20 | stringValue: 'string', 21 | objectValue: {}, 22 | } 23 | const expectedErrorMessage = 24 | 'Objects are not supported as value for the "objectValue" query parameter' 25 | 26 | expect(() => validateSearchParameters(query)).toThrow(expectedErrorMessage) 27 | }) 28 | 29 | test('adds the affected parameter to the error', () => { 30 | const query = { 31 | affectedParameter: { key: 'value' }, 32 | } 33 | const expectedErrorMessage = 34 | 'Objects are not supported as value for the "affectedParameter" query parameter' 35 | 36 | expect(() => validateSearchParameters(query)).toThrow(expectedErrorMessage) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | registries: 3 | npm-github: 4 | type: npm-registry 5 | url: https://npm.pkg.github.com 6 | token: ${{secrets.NPM_REGISTRY_REGISTRY_GH_ORG_TOKEN}} 7 | updates: 8 | - package-ecosystem: npm 9 | versioning-strategy: lockfile-only 10 | registries: 11 | - npm-github 12 | directory: "/" 13 | schedule: 14 | interval: daily 15 | time: "00:00" 16 | timezone: UTC 17 | open-pull-requests-limit: 10 18 | ignore: 19 | - dependency-name: husky 20 | versions: 21 | - ">=5.0.0" 22 | - dependency-name: "fast-copy" 23 | versions: 24 | - ">=3.0.0" 25 | - dependency-name: "eslint-plugin-promise" 26 | versions: 27 | - ">=6.0.0" 28 | commit-message: 29 | prefix: build 30 | include: scope 31 | groups: 32 | production-dependencies: 33 | applies-to: version-updates 34 | dependency-type: production 35 | update-types: 36 | - minor 37 | - patch 38 | patterns: 39 | - '*' 40 | dev-dependencies: 41 | applies-to: version-updates 42 | dependency-type: development 43 | update-types: 44 | - minor 45 | - patch 46 | patterns: 47 | - '*' 48 | 49 | 50 | cooldown: 51 | default-days: 15 -------------------------------------------------------------------------------- /test/unit/utils/normalize-select.test.ts: -------------------------------------------------------------------------------- 1 | import normalizeSelect from '../../../lib/utils/normalize-select' 2 | 3 | describe('normalizeSelect', () => { 4 | test('normalizeSelect does nothing if sys is selected', () => { 5 | const query = { 6 | select: 'fields.foo,sys', 7 | } 8 | const normalized = normalizeSelect(query) 9 | expect(normalized.select).toBe('fields.foo,sys') 10 | }) 11 | 12 | test('normalizeSelect adds required properties if sys is not selected', () => { 13 | const query = { 14 | select: 'fields.foo', 15 | } 16 | const normalized = normalizeSelect(query) 17 | expect(normalized.select).toBe('fields.foo,sys.id,sys.type') 18 | }) 19 | 20 | test('normalizeSelect adds required properties if different sys properties are selected', () => { 21 | const query = { 22 | select: 'fields.foo,sys.createdAt', 23 | } 24 | const normalized = normalizeSelect(query) 25 | expect(normalized.select).toBe('fields.foo,sys.createdAt,sys.id,sys.type') 26 | }) 27 | 28 | test('normalizeSelect adds required properties if only some required sys properties are selected', () => { 29 | const query = { 30 | select: 'fields.foo,sys.type', 31 | } 32 | const normalized = normalizeSelect(query) 33 | expect(normalized.select).toBe('fields.foo,sys.type,sys.id') 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /lib/types/query/search.ts: -------------------------------------------------------------------------------- 1 | import type { EntryFields, EntryFieldTypes } from '../entry.js' 2 | import type { ConditionalFixedQueries, FieldsType } from './util.js' 3 | 4 | type SupportedTypes = 5 | | EntryFields.Text 6 | | EntryFields.RichText 7 | | EntryFields.Symbol 8 | | EntryFields.Symbol[] 9 | | undefined 10 | 11 | type SupportedEntryFieldTypes = 12 | | EntryFieldTypes.Text 13 | | EntryFieldTypes.RichText 14 | | EntryFieldTypes.Symbol 15 | | EntryFieldTypes.Array 16 | | undefined 17 | 18 | /** 19 | * match - full text search in provided fields 20 | * @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/full-text-search | Documentation} 21 | * @internal 22 | */ 23 | export type FullTextSearchFilters< 24 | Fields extends FieldsType, 25 | Prefix extends string, 26 | > = ConditionalFixedQueries 27 | 28 | /** 29 | * match - full text search in provided fields of an entry 30 | * @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/full-text-search | Documentation} 31 | * @internal 32 | */ 33 | export type EntryFieldsFullTextSearchFilters< 34 | Fields extends FieldsType, 35 | Prefix extends string, 36 | > = ConditionalFixedQueries 37 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import eslint from '@eslint/js' 4 | import tseslint from 'typescript-eslint' 5 | 6 | export default tseslint.config( 7 | { 8 | ignores: ['test/output-integration/*'], 9 | }, 10 | eslint.configs.recommended, 11 | tseslint.configs.recommended, 12 | // Library 13 | { 14 | files: ['lib/**/*'], 15 | rules: { 16 | // Things we probably should fix at some point 17 | '@typescript-eslint/ban-ts-comment': 'warn', 18 | '@typescript-eslint/no-empty-object-type': 'warn', 19 | '@typescript-eslint/no-explicit-any': 'warn', 20 | '@typescript-eslint/no-unsafe-function-type': 'warn', 21 | '@typescript-eslint/no-unused-vars': 'warn', 22 | '@typescript-eslint/no-namespace': 'warn', 23 | // Things we won't allow 24 | '@typescript-eslint/consistent-type-imports': 'error', 25 | '@typescript-eslint/no-this-alias': [ 26 | 'error', 27 | { 28 | allowDestructuring: true, // Allow `const { props, state } = this`; false by default 29 | allowedNames: ['self'], // Allow `const self = this`; `[]` by default 30 | }, 31 | ], 32 | }, 33 | }, 34 | // Tests 35 | { 36 | files: ['test/**/*'], 37 | rules: { 38 | '@typescript-eslint/no-unused-expressions': 'off', 39 | '@typescript-eslint/no-explicit-any': 'warn', 40 | '@typescript-eslint/ban-ts-comment': 'warn', 41 | }, 42 | }, 43 | ) 44 | -------------------------------------------------------------------------------- /lib/types/query/range.ts: -------------------------------------------------------------------------------- 1 | import type { EntryFields, EntryFieldType, EntryFieldTypes } from '../entry.js' 2 | import type { 3 | ConditionalQueries, 4 | EntryFieldsConditionalQueries, 5 | EntrySkeletonType, 6 | } from './util.js' 7 | 8 | type RangeFilterTypes = 'lt' | 'lte' | 'gt' | 'gte' 9 | 10 | type SupportedTypes = EntryFields.Date | EntryFields.Number | EntryFields.Integer | undefined 11 | 12 | type SupportedEntryFieldTypes = 13 | | EntryFieldTypes.Date 14 | | EntryFieldTypes.Number 15 | | EntryFieldTypes.Integer 16 | | undefined 17 | 18 | /** 19 | * Range operators appliable to date and number fields 20 | * @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/ranges | Documentation} 21 | * @internal 22 | */ 23 | export type RangeFilters = ConditionalQueries< 24 | Fields, 25 | SupportedTypes, 26 | Prefix, 27 | `[${RangeFilterTypes}]` 28 | > 29 | 30 | /** 31 | * Range operators appliable to date and number fields in an entry 32 | * @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/ranges | Documentation} 33 | * @internal 34 | */ 35 | export type EntryFieldsRangeFilters< 36 | Fields extends Record>, 37 | Prefix extends string, 38 | > = EntryFieldsConditionalQueries 39 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | permissions: 3 | contents: read 4 | 5 | on: 6 | push: 7 | branches: 8 | - master 9 | pull_request: 10 | 11 | jobs: 12 | build: 13 | uses: ./.github/workflows/build.yaml 14 | 15 | check: 16 | needs: build 17 | uses: ./.github/workflows/check.yaml 18 | 19 | release: 20 | if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/next') 21 | needs: [build, check] 22 | permissions: 23 | contents: write 24 | id-token: write 25 | actions: read 26 | uses: ./.github/workflows/release.yaml 27 | secrets: 28 | VAULT_URL: ${{ secrets.VAULT_URL }} 29 | 30 | notify-failure: 31 | if: | 32 | always() && 33 | (needs.build.result == 'failure' || needs.check.result == 'failure' || needs.release.result == 'failure') && 34 | (github.event_name == 'push' && github.ref == 'refs/heads/master') 35 | needs: [build, check, release] 36 | permissions: 37 | contents: read 38 | issues: write 39 | uses: ./.github/workflows/failure-notification.yaml 40 | with: 41 | workflow_name: "Main CI Pipeline" 42 | job_name: ${{ needs.build.result == 'failure' && 'build' || needs.check.result == 'failure' && 'check' || needs.release.result == 'failure' && 'release' || 'unknown' }} 43 | failure_reason: "One or more jobs in the main CI pipeline failed. Check the workflow run for detailed error information." -------------------------------------------------------------------------------- /test/types/client/getAssets.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectType } from 'tsd' 2 | import { Asset, AssetCollection, AssetCursorPaginatedCollection, createClient } from '../../../lib' 3 | 4 | const client = createClient({ 5 | accessToken: 'accessToken', 6 | space: 'spaceId', 7 | }) 8 | 9 | type Locale = 'en' 10 | 11 | expectType>(await client.getAsset('test')) 12 | 13 | expectType>(await client.getAssets()) 14 | 15 | expectType>(await client.withAllLocales.getAsset('test')) 16 | expectType>(await client.withAllLocales.getAsset('test')) 17 | 18 | expectType>(await client.withAllLocales.getAssets()) 19 | expectType>( 20 | await client.withAllLocales.getAssets(), 21 | ) 22 | 23 | expectType>(await client.getAssetsWithCursor()) 24 | expectType>( 25 | await client.getAssetsWithCursor({ limit: 20 }), 26 | ) 27 | expectType>( 28 | await client.getAssetsWithCursor({ pagePrev: 'token' }), 29 | ) 30 | 31 | expectType>( 32 | await client.withAllLocales.getAssetsWithCursor(), 33 | ) 34 | expectType>( 35 | await client.withAllLocales.getAssetsWithCursor(), 36 | ) 37 | -------------------------------------------------------------------------------- /test/unit/utils/validate-timestamp.test.ts: -------------------------------------------------------------------------------- 1 | import validateTimestamp from '../../../lib/utils/validate-timestamp' 2 | 3 | const now = () => Math.floor(Date.now() / 1000) 4 | 5 | test('validateTimestamp passes if timestamp is numeric', () => { 6 | expect(() => validateTimestamp('testTimestamp', now())).not.toThrowError() 7 | }) 8 | 9 | test('validateTimestamp fails if timestamp is not a numeric', () => { 10 | const timestamp = now().toString() 11 | const expectedErrorMessage = /only numeric values are allowed for timestamps/ 12 | 13 | // @ts-ignore 14 | expect(() => validateTimestamp('testTimestamp', timestamp)).toThrow(expectedErrorMessage) 15 | }) 16 | 17 | test('validateTimestamp fails if timestamp is greater than expected maximum', () => { 18 | const maximum = now() + 60 19 | const timestamp = now() + 120 20 | const expectedErrorMessage = new RegExp( 21 | `value \\(${timestamp}\\) cannot be further in the future than expected maximum \\(${maximum}\\)`, 22 | ) 23 | 24 | expect(() => validateTimestamp('testTimestamp', timestamp, { maximum })).toThrow( 25 | expectedErrorMessage, 26 | ) 27 | }) 28 | 29 | test('validateTimestamp fails if timestamp is in the past', () => { 30 | const current = now() 31 | const timestamp = now() - 120 32 | const expectedErrorMessage = new RegExp( 33 | `value \\(${timestamp}\\) cannot be in the past, current time was ${current}`, 34 | ) 35 | 36 | expect(() => validateTimestamp('testTimestamp', timestamp, { now: current })).toThrow( 37 | expectedErrorMessage, 38 | ) 39 | }) 40 | -------------------------------------------------------------------------------- /test/types/queries/cursor-pagination-queries.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectAssignable, expectNotAssignable } from 'tsd' 2 | import type { CursorPaginationOptions } from '../../../lib' 3 | 4 | expectAssignable({}) 5 | expectAssignable({ pagePrev: 'prev_token' }) 6 | expectAssignable({ pageNext: 'token_next' }) 7 | expectAssignable({ 8 | pageNext: 'token_next', 9 | pagePrev: undefined, 10 | }) 11 | expectAssignable({ pagePrev: 'token_prev', pageNext: undefined }) 12 | expectAssignable({ pagePrev: undefined, pageNext: undefined }) 13 | expectAssignable({ pagePrev: 'page_prev', limit: 40 }) 14 | expectAssignable({ pageNext: 'page_next', limit: 40 }) 15 | expectAssignable({ limit: 20 }) 16 | 17 | expectNotAssignable({ cursor: false }) 18 | expectNotAssignable({ cursor: undefined }) 19 | expectNotAssignable({ cursor: null }) 20 | expectNotAssignable({ pageNext: 'page_next', pagePrev: 'page_prev' }) 21 | expectNotAssignable({ pageNext: 40 }) 22 | expectNotAssignable({ pagePrev: 40 }) 23 | expectNotAssignable({ skip: 100 }) 24 | expectNotAssignable({ pagePrev: 'page_prev', skip: 20 }) 25 | expectNotAssignable({ pageNext: 'page_next', skip: 20 }) 26 | expectNotAssignable({ limit: 10, skip: 20 }) 27 | -------------------------------------------------------------------------------- /lib/types/link.ts: -------------------------------------------------------------------------------- 1 | import type { ResourceLink } from './resource-link.js' 2 | 3 | /** 4 | * @category Link 5 | */ 6 | export type LinkType = 7 | | 'Space' 8 | | 'ContentType' 9 | | 'Environment' 10 | | 'Entry' 11 | | 'Tag' 12 | | 'User' 13 | | 'Asset' 14 | | 'TaxonomyConcept' 15 | | 'TaxonomyConceptScheme' 16 | 17 | /** 18 | * Link definition of a specific link type 19 | * @category Link 20 | */ 21 | export interface Link { 22 | type: 'Link' 23 | linkType: T 24 | id: string 25 | } 26 | 27 | /** 28 | * Unresolved link field of a specific link type 29 | * @category Link 30 | */ 31 | export type UnresolvedLink = { sys: Link } 32 | 33 | /** 34 | * Space link type 35 | * @category Entity 36 | */ 37 | export type SpaceLink = Link<'Space'> 38 | 39 | /** 40 | * Content type link type 41 | * @category ContentType 42 | */ 43 | export type ContentTypeLink = Link<'ContentType'> 44 | 45 | /** 46 | * Environment link type 47 | * @category Entity 48 | */ 49 | export type EnvironmentLink = Link<'Environment'> 50 | 51 | /** 52 | * Asset link type 53 | * @category Asset 54 | */ 55 | export type AssetLink = Link<'Asset'> 56 | 57 | /** 58 | * Entry link type 59 | * @category Entry 60 | */ 61 | export type EntryLink = Link<'Entry'> | ResourceLink 62 | 63 | /** 64 | * Tag link type 65 | * @category Tag 66 | */ 67 | export type TagLink = Link<'Tag'> 68 | 69 | /** 70 | * User link type 71 | * @category Entity 72 | */ 73 | export type UserLink = Link<'User'> 74 | 75 | /** 76 | * Taxonomy Concept link type 77 | * @category Entity 78 | */ 79 | export type TaxonomyConceptLink = Link<'TaxonomyConcept'> 80 | -------------------------------------------------------------------------------- /test/unit/timeline-preview.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, vi } from 'vitest' 2 | import createContentfulApi from '../../lib/create-contentful-api' 3 | import type { AxiosInstance } from 'contentful-sdk-core' 4 | 5 | describe('Timeline Preview path and query', () => { 6 | it('changes path and query when timeline preview is enabled', async () => { 7 | const mockGet = vi.fn().mockResolvedValue({ data: { items: [] } }) 8 | const mockHttp = { 9 | get: mockGet, 10 | httpClientParams: { 11 | host: 'preview.contentful.com', 12 | 13 | timelinePreview: { 14 | release: { lte: 'black-friday' }, 15 | timestamp: { lte: '2023-11-30T23:59:59Z' }, 16 | }, 17 | }, 18 | } as unknown as AxiosInstance 19 | 20 | const getGlobalOptions = () => ({ 21 | environmentBaseUrl: 'https://preview.contentful.com/spaces/spaceid/environments/master', 22 | spaceBaseUrl: 'https://preview.contentful.com/spaces/spaceid', 23 | environment: 'master', 24 | space: 'spaceid', 25 | }) 26 | 27 | const api = createContentfulApi({ http: mockHttp, getGlobalOptions }) 28 | 29 | await api.getEntries({ 'sys.id': 'entry-id' }) 30 | 31 | // Check that the path is prefixed with "timeline/" 32 | expect(mockGet).toHaveBeenCalledWith( 33 | 'https://preview.contentful.com/spaces/spaceid/environments/master/timeline/entries', 34 | { 35 | params: { 36 | release: { 37 | lte: 'black-friday', 38 | }, 39 | 'sys.id': 'entry-id', 40 | timestamp: { 41 | lte: '2023-11-30T23:59:59Z', 42 | }, 43 | }, 44 | }, 45 | ) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /lib/types/concept.ts: -------------------------------------------------------------------------------- 1 | import type { CursorPaginatedCollection } from './collection' 2 | import type { UnresolvedLink } from './link' 3 | import type { LocaleCode } from './locale' 4 | 5 | type ISODateString = string 6 | 7 | export type ConceptSys = { 8 | id: string 9 | type: 'TaxonomyConcept' 10 | createdAt: ISODateString 11 | updatedAt: ISODateString 12 | version: number 13 | } 14 | 15 | export interface Concept { 16 | sys: ConceptSys 17 | uri?: string 18 | prefLabel: { 19 | [locale in Locales]: string 20 | } 21 | altLabels?: { 22 | [locale in Locales]: string[] 23 | } 24 | hiddenLabels?: { 25 | [locale in Locales]: string[] 26 | } 27 | note?: 28 | | { 29 | [locale in Locales]: string 30 | } 31 | | null 32 | changeNote?: 33 | | { 34 | [locale in Locales]: string 35 | } 36 | | null 37 | definition?: 38 | | { 39 | [locale in Locales]: string 40 | } 41 | | null 42 | editorialNote?: 43 | | { 44 | [locale in Locales]: string 45 | } 46 | | null 47 | example?: 48 | | { 49 | [locale in Locales]: string 50 | } 51 | | null 52 | historyNote?: 53 | | { 54 | [locale in Locales]: string 55 | } 56 | | null 57 | scopeNote?: 58 | | { 59 | [locale in Locales]: string 60 | } 61 | | null 62 | notations?: string[] 63 | broader?: UnresolvedLink<'TaxonomyConcept'>[] 64 | related?: UnresolvedLink<'TaxonomyConcept'>[] 65 | conceptSchemes?: UnresolvedLink<'TaxonomyConceptScheme'>[] 66 | } 67 | 68 | export type ConceptCollection = CursorPaginatedCollection< 69 | Concept 70 | > 71 | -------------------------------------------------------------------------------- /test/types/chain-options.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectAssignable, expectNotAssignable, expectNotType, expectType } from 'tsd' 2 | import { ChainOption, ChainOptions } from '../../lib/utils/client-helpers' 3 | import { ChainModifiers } from '../../lib' 4 | 5 | expectNotAssignable('ANY_STRING') 6 | 7 | expectAssignable({ 8 | withoutLinkResolution: true as boolean, 9 | withAllLocales: true as boolean, 10 | withoutUnresolvableLinks: true as boolean, 11 | }) 12 | 13 | expectType>({ 14 | withoutLinkResolution: false, 15 | withAllLocales: false, 16 | withoutUnresolvableLinks: false, 17 | }) 18 | 19 | expectType>({ 20 | withoutLinkResolution: false, 21 | withAllLocales: false, 22 | withoutUnresolvableLinks: true, 23 | }) 24 | 25 | expectType>({ 26 | withoutLinkResolution: true, 27 | withAllLocales: false, 28 | withoutUnresolvableLinks: false, 29 | }) 30 | 31 | expectType>({ 32 | withoutLinkResolution: false, 33 | withAllLocales: true, 34 | withoutUnresolvableLinks: false, 35 | }) 36 | 37 | expectType>({ 38 | withoutLinkResolution: false, 39 | withAllLocales: true, 40 | withoutUnresolvableLinks: true, 41 | }) 42 | 43 | expectNotType>({ 44 | withoutLinkResolution: false, 45 | withAllLocales: true, 46 | withoutUnresolvableLinks: false, 47 | }) 48 | 49 | expectType>({ 50 | withoutLinkResolution: true, 51 | withAllLocales: true, 52 | withoutUnresolvableLinks: false, 53 | }) 54 | -------------------------------------------------------------------------------- /lib/types/collection.ts: -------------------------------------------------------------------------------- 1 | export type CollectionBase = { 2 | limit: number 3 | items: Array 4 | sys?: { 5 | type: 'Array' 6 | } 7 | } 8 | 9 | export type OffsetPagination = { 10 | total: number 11 | skip: number 12 | } 13 | 14 | export type CursorPagination = { 15 | pages: { 16 | next?: string 17 | prev?: string 18 | } 19 | } 20 | 21 | /** 22 | * A wrapper object containing additional information for 23 | * an offset paginated collection of Contentful resources 24 | * @category Entity 25 | * @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/introduction/collection-resources-and-pagination | Documentation} 26 | */ 27 | export type OffsetPaginatedCollection = CollectionBase & OffsetPagination 28 | 29 | /** 30 | * A wrapper object containing additional information for 31 | * an offset paginated collection of Contentful resources 32 | * @category Entity 33 | * @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/introduction/collection-resources-and-pagination | Documentation} 34 | */ 35 | export interface ContentfulCollection extends OffsetPaginatedCollection {} 36 | 37 | /** 38 | * A wrapper object containing additional information for 39 | * a curisor paginated collection of Contentful resources 40 | * @category Entity 41 | * @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/introduction/cursor-pagination | Documentation} 42 | */ 43 | export type CursorPaginatedCollection = CollectionBase & CursorPagination 44 | 45 | export type CollectionForQuery< 46 | T = unknown, 47 | Query extends Record = Record, 48 | > = Query extends { cursor: true } ? CursorPaginatedCollection : OffsetPaginatedCollection 49 | -------------------------------------------------------------------------------- /lib/types/query/select.ts: -------------------------------------------------------------------------------- 1 | import type { FieldsType } from './util.js' 2 | import type { EntrySys } from '../entry.js' 3 | import type { AssetSys } from '../asset.js' 4 | import type { Metadata } from '../metadata.js' 5 | 6 | export type SelectFilterPaths< 7 | Fields extends FieldsType, 8 | Prefix extends string, 9 | > = `${Prefix}.${keyof Fields & string}` 10 | 11 | /** 12 | * Select fields from provided fields in an entry 13 | * @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/select-operator | Documentation} 14 | * @internal 15 | */ 16 | export type EntrySelectFilterWithFields = { 17 | select?: ( 18 | | 'sys' 19 | | 'fields' 20 | | 'metadata' 21 | | SelectFilterPaths 22 | | SelectFilterPaths 23 | | SelectFilterPaths 24 | )[] 25 | } 26 | 27 | /** 28 | * Select fields in an entry 29 | * @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/select-operator | Documentation} 30 | * @internal 31 | */ 32 | export type EntrySelectFilter = { 33 | select?: ( 34 | | 'sys' 35 | | 'fields' 36 | | 'metadata' 37 | | SelectFilterPaths 38 | | SelectFilterPaths 39 | )[] 40 | } 41 | 42 | /** 43 | * Select fields in an asset 44 | * @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/select-operator | Documentation} 45 | * @internal 46 | */ 47 | export type AssetSelectFilter = { 48 | select?: ( 49 | | 'sys' 50 | | SelectFilterPaths 51 | | 'fields' 52 | | SelectFilterPaths 53 | | 'metadata' 54 | | SelectFilterPaths 55 | )[] 56 | } 57 | -------------------------------------------------------------------------------- /lib/utils/timeline-preview-helpers.ts: -------------------------------------------------------------------------------- 1 | import type { CreateClientParams } from '../contentful' 2 | import type { TimelinePreview } from '../types/timeline-preview' 3 | import { checkEnableTimelinePreviewIsAllowed } from './validate-params' 4 | import { ValidationError } from './validation-error' 5 | 6 | function isValidRelease(release: TimelinePreview['release']): boolean { 7 | return !!(release && typeof release === 'object' && typeof release.lte === 'string') 8 | } 9 | 10 | function isValidTimestamp(timestamp: TimelinePreview['timestamp']): boolean { 11 | return !!( 12 | timestamp && 13 | typeof timestamp === 'object' && 14 | (typeof timestamp.lte === 'string' || timestamp.lte instanceof Date) 15 | ) 16 | } 17 | 18 | export const isValidTimelinePreviewConfig = (timelinePreview: TimelinePreview) => { 19 | if ( 20 | typeof timelinePreview !== 'object' || 21 | timelinePreview === null || 22 | Array.isArray(timelinePreview) 23 | ) { 24 | throw new ValidationError( 25 | 'timelinePreview', 26 | `The 'timelinePreview' parameter must be an object.`, 27 | ) 28 | } 29 | 30 | const hasRelease = isValidRelease(timelinePreview.release) 31 | const hasTimestamp = isValidTimestamp(timelinePreview.timestamp) 32 | 33 | if (!hasRelease && !hasTimestamp) { 34 | throw new ValidationError( 35 | 'timelinePreview', 36 | `The 'timelinePreview' object must have at least one of 'release' or 'timestamp' with a valid 'lte' property.`, 37 | ) 38 | } 39 | 40 | return hasRelease || hasTimestamp 41 | } 42 | 43 | export const getTimelinePreviewParams = (params: CreateClientParams) => { 44 | const host = params?.host as string 45 | const timelinePreview = 46 | params?.timelinePreview ?? (params?.alphaFeatures?.timelinePreview as TimelinePreview) 47 | const enabled = checkEnableTimelinePreviewIsAllowed(host, timelinePreview) 48 | return { enabled, timelinePreview } 49 | } 50 | -------------------------------------------------------------------------------- /lib/types/query/location.ts: -------------------------------------------------------------------------------- 1 | import type { ConditionalPick } from 'type-fest' 2 | import type { EntryFieldTypes } from '../entry.js' 3 | 4 | type Types = EntryFieldTypes.Location | undefined 5 | 6 | export type ProximitySearchFilterInput = [number, number] 7 | export type BoundingBoxSearchFilterInput = [number, number, number, number] 8 | export type BoundingCircleSearchFilterInput = [number, number, number] 9 | 10 | type BaseLocationFilter< 11 | Fields, 12 | SupportedTypes, 13 | ValueType, 14 | Prefix extends string, 15 | QueryFilter extends string = '', 16 | > = NonNullable<{ 17 | [FieldName in keyof ConditionalPick as `${Prefix}.${string & 18 | FieldName}[${QueryFilter}]`]?: ValueType 19 | }> 20 | 21 | /** 22 | * near - location proximity search in provided fields 23 | * @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/location-proximity-search | Documentation} 24 | * @internal 25 | */ 26 | export type ProximitySearchFilter = BaseLocationFilter< 27 | Fields, 28 | Types, 29 | ProximitySearchFilterInput, 30 | Prefix, 31 | 'near' 32 | > 33 | 34 | /** 35 | * within - location in a bounding object in provided fields 36 | * @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/locations-in-a-bounding-object | Documentation} 37 | * @internal 38 | */ 39 | export type BoundingObjectSearchFilter = BaseLocationFilter< 40 | Fields, 41 | Types, 42 | BoundingCircleSearchFilterInput | BoundingBoxSearchFilterInput, 43 | Prefix, 44 | 'within' 45 | > 46 | 47 | /** 48 | * Location search 49 | * @see {@link ProximitySearchFilter} 50 | * @see {@link BoundingObjectSearchFilter} 51 | * @internal 52 | */ 53 | export type LocationSearchFilters = ProximitySearchFilter< 54 | Fields, 55 | Prefix 56 | > & 57 | BoundingObjectSearchFilter 58 | -------------------------------------------------------------------------------- /test/unit/utils/validate-params.test.ts: -------------------------------------------------------------------------------- 1 | import { checkIncludeContentSourceMapsParamIsAllowed } from '../../../lib/utils/validate-params' 2 | import { ValidationError } from '../../../lib/utils/validation-error' 3 | 4 | describe('checkIncludeContentSourceMapsParamIsAllowed', () => { 5 | it('returns false if includeContentSourceMaps is not provided', () => { 6 | expect(checkIncludeContentSourceMapsParamIsAllowed('http://example.com')).toBe(false) 7 | expect(checkIncludeContentSourceMapsParamIsAllowed('http://example.com', undefined)).toBe(false) 8 | }) 9 | 10 | it('throws ValidationError if includeContentSourceMaps is not a boolean', () => { 11 | expect(() => 12 | checkIncludeContentSourceMapsParamIsAllowed('http://example.com', 'not a boolean' as any), 13 | ).toThrow(ValidationError) 14 | expect(() => 15 | checkIncludeContentSourceMapsParamIsAllowed('http://example.com', 1 as any), 16 | ).toThrow(ValidationError) 17 | }) 18 | 19 | it('throws ValidationError if includeContentSourceMaps is true but host is not preview.contentful.com', () => { 20 | expect(() => checkIncludeContentSourceMapsParamIsAllowed('cdn.contentful.com', true)).toThrow( 21 | ValidationError, 22 | ) 23 | }) 24 | 25 | it('returns true if includeContentSourceMaps is true and host is preview.contentful.com', () => { 26 | expect(checkIncludeContentSourceMapsParamIsAllowed('preview.contentful.com', true)).toBe(true) 27 | }) 28 | 29 | it('returns true if includeContentSourceMaps is true and host is preview.eu.contentful.com', () => { 30 | expect(checkIncludeContentSourceMapsParamIsAllowed('preview.eu.contentful.com', true)).toBe( 31 | true, 32 | ) 33 | }) 34 | 35 | it('returns false if includeContentSourceMaps is false, regardless of host', () => { 36 | expect(checkIncludeContentSourceMapsParamIsAllowed('preview.contentful.com', false)).toBe(false) 37 | expect(checkIncludeContentSourceMapsParamIsAllowed('cdn.contentful.com', false)).toBe(false) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /test/integration/getConceptSchemes.test.ts: -------------------------------------------------------------------------------- 1 | import * as contentful from '../../lib/contentful' 2 | import { params } from './utils' 3 | 4 | if (process.env.API_INTEGRATION_TESTS) { 5 | params.host = '127.0.0.1:5000' 6 | params.insecure = true 7 | } 8 | 9 | const client = contentful.createClient(params) 10 | 11 | describe('getConceptSchemes', () => { 12 | it('returns all concept schemes when no filters are available', async () => { 13 | const response = await client.getConceptSchemes() 14 | 15 | expect(response.items[0].sys.type).toBe('TaxonomyConceptScheme') 16 | }) 17 | 18 | describe('order', () => { 19 | it('orders the concept schemes by createdAt', async () => { 20 | const response = await client.getConceptSchemes({ order: ['sys.createdAt'] }) 21 | 22 | expect(new Date(response.items[0].sys.createdAt).getTime()).toBeLessThan( 23 | new Date(response.items[1].sys.createdAt).getTime(), 24 | ) 25 | }) 26 | 27 | it('orders the concept schemes by updatedAt', async () => { 28 | const response = await client.getConceptSchemes({ order: ['sys.updatedAt'] }) 29 | 30 | expect(new Date(response.items[0].sys.updatedAt).getTime()).toBeLessThan( 31 | new Date(response.items[1].sys.updatedAt).getTime(), 32 | ) 33 | }) 34 | 35 | it('orders the concept schemes by prefLabel', async () => { 36 | const response = await client.getConceptSchemes({ order: ['prefLabel'] }) 37 | 38 | expect( 39 | response.items[0].prefLabel['en-US'].localeCompare(response.items[1].prefLabel['en-US']), 40 | ).toBeLessThan(0) 41 | }) 42 | }) 43 | 44 | describe('pagination', () => { 45 | it('returns limit and next page cursor', async () => { 46 | const response = await client.getConceptSchemes({ limit: 1 }) 47 | 48 | expect(response.items).toHaveLength(1) 49 | expect(response.limit).toBe(1) 50 | expect(response.sys.type).toBe('Array') 51 | expect(response.pages?.next).toBeDefined() 52 | }) 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 11 | 12 | ## Expected Behavior 13 | 14 | 18 | 19 | ## Actual Behavior 20 | 21 | 25 | 26 | ## Possible Solution 27 | 28 | 32 | 33 | ## Steps to Reproduce 34 | 35 | 40 | 1. 41 | 2. 42 | 3. 43 | 4. 44 | 45 | ## Context 46 | 47 | 51 | 52 | ## Environment 53 | 54 | 57 | 58 | - **Language Version**: 59 | - **Package Manager Version**: 60 | - **Browser Version**: 61 | - **Operating System**: 62 | - **Package Version**: 63 | - **Which API are you using?**: 64 | -------------------------------------------------------------------------------- /lib/types/query/subset.ts: -------------------------------------------------------------------------------- 1 | import type { EntryFields, EntryFieldType, EntryFieldTypes } from '../index.js' 2 | import type { ConditionalListQueries, EntryFieldsConditionalListQueries } from './util.js' 3 | 4 | type SubsetFilterTypes = 'in' | 'nin' 5 | 6 | type SupportedTypes = 7 | | EntryFields.Symbol 8 | | EntryFields.Symbol[] 9 | | EntryFields.Text 10 | | EntryFields.Integer 11 | | EntryFields.Number 12 | | EntryFields.Date 13 | | EntryFields.Boolean 14 | | undefined 15 | 16 | type SupportedEntryFieldTypes = 17 | | EntryFieldTypes.Symbol 18 | | EntryFieldTypes.Array 19 | | EntryFieldTypes.Text 20 | | EntryFieldTypes.Integer 21 | | EntryFieldTypes.Number 22 | | EntryFieldTypes.Date 23 | | EntryFieldTypes.Boolean 24 | | undefined 25 | 26 | /** 27 | * Subset filters for inclusion & exclusion in provided fields 28 | * @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/inclusion | Inclusion documentation} 29 | * @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/exclusion | Exclusion documentation} 30 | * @internal 31 | */ 32 | export type SubsetFilters = ConditionalListQueries< 33 | Fields, 34 | SupportedTypes, 35 | Prefix, 36 | `[${SubsetFilterTypes}]` 37 | > 38 | 39 | /** 40 | * Subset filters for inclusion & exclusion in provided fields of an entry 41 | * @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/inclusion | Inclusion documentation} 42 | * @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/exclusion | Exclusion documentation} 43 | * @internal 44 | */ 45 | export type EntryFieldsSubsetFilters< 46 | Fields extends Record>, 47 | Prefix extends string, 48 | > = EntryFieldsConditionalListQueries< 49 | Fields, 50 | SupportedEntryFieldTypes, 51 | Prefix, 52 | `[${SubsetFilterTypes}]` 53 | > 54 | -------------------------------------------------------------------------------- /test/types/query-types/object.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectAssignable, expectNotAssignable, expectType } from 'tsd' 2 | import { 3 | EntryFieldTypes, 4 | EntryFieldsEqualityFilter, 5 | EntryFieldsInequalityFilter, 6 | EntryFieldsExistenceFilter, 7 | LocationSearchFilters, 8 | EntryFieldsRangeFilters, 9 | EntryFieldsFullTextSearchFilters, 10 | EntrySelectFilterWithFields, 11 | EntryFieldsSubsetFilters, 12 | EntryOrderFilterWithFields, 13 | EntryFieldsSetFilter, 14 | } from '../../../lib' 15 | 16 | // @ts-ignore 17 | import * as mocks from '../mocks' 18 | 19 | expectAssignable>>( 20 | {}, 21 | ) 22 | 23 | expectAssignable< 24 | Required> 25 | >({}) 26 | 27 | expectAssignable< 28 | Required> 29 | >({}) 30 | 31 | expectAssignable>({}) 32 | expectType>>({ 33 | 'fields.testField[exists]': mocks.booleanValue, 34 | }) 35 | 36 | expectAssignable>>( 37 | {}, 38 | ) 39 | 40 | expectAssignable< 41 | Required> 42 | >({}) 43 | 44 | expectAssignable< 45 | Required> 46 | >({}) 47 | 48 | expectNotAssignable>({ 49 | order: ['fields.testField'], 50 | }) 51 | 52 | expectAssignable>({}) 53 | expectAssignable>>({ 54 | select: ['fields.testField'], 55 | }) 56 | 57 | expectAssignable< 58 | Required> 59 | >({}) 60 | -------------------------------------------------------------------------------- /test/integration/getConcepts.test.ts: -------------------------------------------------------------------------------- 1 | import * as contentful from '../../lib/contentful' 2 | import { params } from './utils' 3 | 4 | if (process.env.API_INTEGRATION_TESTS) { 5 | params.host = '127.0.0.1:5000' 6 | params.insecure = true 7 | } 8 | 9 | const client = contentful.createClient(params) 10 | 11 | describe('getConcepts', () => { 12 | it('returns all concepts when no filters are available', async () => { 13 | const response = await client.getConcepts() 14 | 15 | expect(response.items[0].sys.type).toBe('TaxonomyConcept') 16 | }) 17 | 18 | describe('order', () => { 19 | it('orders the concepts by createdAt', async () => { 20 | const response = await client.getConcepts({ order: ['sys.createdAt'] }) 21 | 22 | expect(new Date(response.items[0].sys.createdAt).getTime()).toBeLessThan( 23 | new Date(response.items[1].sys.createdAt).getTime(), 24 | ) 25 | }) 26 | 27 | it('orders the concepts by updatedAt', async () => { 28 | const response = await client.getConcepts({ order: ['sys.updatedAt'] }) 29 | 30 | expect(new Date(response.items[0].sys.updatedAt).getTime()).toBeLessThan( 31 | new Date(response.items[1].sys.updatedAt).getTime(), 32 | ) 33 | }) 34 | 35 | it('orders the concepts by prefLabel', async () => { 36 | const response = await client.getConcepts({ order: ['prefLabel'] }) 37 | 38 | expect( 39 | response.items[0].prefLabel['en-US'].localeCompare(response.items[1].prefLabel['en-US']), 40 | ).toBeLessThan(0) 41 | }) 42 | }) 43 | 44 | describe('pagination', () => { 45 | it('returns limit and next page cursor', async () => { 46 | const response = await client.getConcepts({ limit: 1 }) 47 | 48 | expect(response.items).toHaveLength(1) 49 | expect(response.limit).toBe(1) 50 | expect(response.sys.type).toBe('Array') 51 | expect(response.pages?.next).toBeDefined() 52 | }) 53 | }) 54 | 55 | describe('Concept Scheme', () => { 56 | it('filters by Concept Scheme', async () => { 57 | const response = await client.getConcepts({ conceptScheme: '29lkBedZoW295B4sR7Hwrw' }) 58 | 59 | expect(response.items.length).toBeGreaterThan(0) 60 | }) 61 | }) 62 | }) 63 | -------------------------------------------------------------------------------- /test/types/query-types/richtext.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectAssignable, expectNotAssignable, expectType } from 'tsd' 2 | import { 3 | EntryFieldTypes, 4 | EntryFieldsEqualityFilter, 5 | EntryFieldsInequalityFilter, 6 | EntryFieldsExistenceFilter, 7 | LocationSearchFilters, 8 | EntryFieldsRangeFilters, 9 | EntryFieldsFullTextSearchFilters, 10 | EntrySelectFilterWithFields, 11 | EntryFieldsSubsetFilters, 12 | EntryOrderFilterWithFields, 13 | EntryFieldsSetFilter, 14 | } from '../../../lib' 15 | 16 | // @ts-ignore 17 | import * as mocks from '../mocks' 18 | 19 | expectAssignable>>( 20 | {}, 21 | ) 22 | 23 | expectAssignable< 24 | Required> 25 | >({}) 26 | 27 | expectAssignable< 28 | Required> 29 | >({}) 30 | 31 | expectAssignable>({}) 32 | expectType< 33 | Required> 34 | >({ 35 | 'fields.testField[exists]': mocks.booleanValue, 36 | }) 37 | 38 | expectAssignable< 39 | Required> 40 | >({}) 41 | 42 | expectType>>({}) 43 | 44 | expectAssignable< 45 | EntryFieldsFullTextSearchFilters<{ testField: EntryFieldTypes.RichText }, 'fields'> 46 | >({}) 47 | expectType< 48 | Required> 49 | >({ 50 | 'fields.testField[match]': mocks.stringValue, 51 | }) 52 | 53 | expectNotAssignable>({ 54 | order: ['fields.testField'], 55 | }) 56 | 57 | expectAssignable>({}) 58 | expectAssignable>>({ 59 | select: ['fields.testField'], 60 | }) 61 | 62 | expectAssignable< 63 | Required> 64 | >({}) 65 | -------------------------------------------------------------------------------- /test/unit/utils/normalize-cursor-pagination-parameters.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from 'vitest' 2 | import { normalizeCursorPaginationParameters } from '../../../lib/utils/normalize-cursor-pagination-parameters' 3 | 4 | describe('normalizeCursorPaginationParameters', () => { 5 | test('should add cursor=true param', () => { 6 | expect(normalizeCursorPaginationParameters({}).cursor).toBe(true) 7 | expect(normalizeCursorPaginationParameters({ cursor: false }).cursor).toBe(true) 8 | expect(normalizeCursorPaginationParameters({ cursor: false, pageNext: '' }).cursor).toBe(true) 9 | }) 10 | 11 | test('should omit "skip" param from the query', () => { 12 | expect(normalizeCursorPaginationParameters({ skip: 20 })).not.property('skip') 13 | }) 14 | 15 | test('should omit pagePrev and pageNext when falsy', () => { 16 | ;[ 17 | normalizeCursorPaginationParameters({ pagePrev: false, pageNext: null }), 18 | normalizeCursorPaginationParameters({ pagePrev: '' }), 19 | normalizeCursorPaginationParameters({ pagePrev: undefined }), 20 | normalizeCursorPaginationParameters({ pageNext: '' }), 21 | normalizeCursorPaginationParameters({ pageNext: undefined }), 22 | normalizeCursorPaginationParameters({ pagePrev: undefined, pageNext: '' }), 23 | ].forEach((result) => { 24 | expect(result).not.property('pagePrev') 25 | expect(result).not.property('pageNext') 26 | }) 27 | }) 28 | 29 | test('should independently pass pagePrev and pageNext when truthy', () => { 30 | expect(normalizeCursorPaginationParameters({ pagePrev: 'test' }).pagePrev).toBe('test') 31 | expect(normalizeCursorPaginationParameters({ pagePrev: 'test' })).not.property('pageNext') 32 | expect(normalizeCursorPaginationParameters({ pageNext: 'next' }).pageNext).toBe('next') 33 | expect(normalizeCursorPaginationParameters({ pageNext: 'next' })).not.property('pagePrev') 34 | expect(normalizeCursorPaginationParameters({ pageNext: 'next', pagePrev: 'prev' })).contain({ 35 | pageNext: 'next', 36 | pagePrev: 'prev', 37 | }) 38 | }) 39 | 40 | test('should pass all the other fields', () => { 41 | const params = { 42 | query: 'items', 43 | select: 'sys.id', 44 | timestamp: '2025-11-14T16:10:22.977Z', 45 | pageNext: 'next', 46 | } 47 | 48 | expect(normalizeCursorPaginationParameters(params)).deep.equal({ 49 | ...params, 50 | cursor: true, 51 | }) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /test/types/client/createClient.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectType } from 'tsd' 2 | 3 | import { ContentfulClientApi, createClient } from '../../../lib' 4 | 5 | const CLIENT_OPTIONS = { 6 | accessToken: 'accessToken', 7 | space: 'spaceId', 8 | } 9 | 10 | expectType>(createClient(CLIENT_OPTIONS)) 11 | 12 | expectType>( 13 | createClient(CLIENT_OPTIONS).withoutLinkResolution, 14 | ) 15 | expectType(createClient(CLIENT_OPTIONS).withoutLinkResolution.withoutLinkResolution) 16 | expectType(createClient(CLIENT_OPTIONS).withoutLinkResolution.withoutUnresolvableLinks) 17 | 18 | expectType>( 19 | createClient(CLIENT_OPTIONS).withoutLinkResolution.withAllLocales, 20 | ) 21 | expectType( 22 | createClient(CLIENT_OPTIONS).withoutLinkResolution.withAllLocales.withoutLinkResolution, 23 | ) 24 | expectType(createClient(CLIENT_OPTIONS).withoutLinkResolution.withAllLocales.withAllLocales) 25 | expectType( 26 | createClient(CLIENT_OPTIONS).withoutLinkResolution.withAllLocales.withoutLinkResolution, 27 | ) 28 | 29 | expectType>( 30 | createClient(CLIENT_OPTIONS).withoutUnresolvableLinks, 31 | ) 32 | expectType(createClient(CLIENT_OPTIONS).withoutUnresolvableLinks.withoutUnresolvableLinks) 33 | expectType(createClient(CLIENT_OPTIONS).withoutUnresolvableLinks.withoutLinkResolution) 34 | 35 | expectType>( 36 | createClient(CLIENT_OPTIONS).withoutUnresolvableLinks.withAllLocales, 37 | ) 38 | expectType( 39 | createClient(CLIENT_OPTIONS).withoutUnresolvableLinks.withAllLocales.withoutUnresolvableLinks, 40 | ) 41 | expectType( 42 | createClient(CLIENT_OPTIONS).withoutUnresolvableLinks.withAllLocales.withAllLocales, 43 | ) 44 | expectType( 45 | createClient(CLIENT_OPTIONS).withoutUnresolvableLinks.withAllLocales.withoutLinkResolution, 46 | ) 47 | 48 | expectType>(createClient(CLIENT_OPTIONS).withAllLocales) 49 | expectType(createClient(CLIENT_OPTIONS).withAllLocales.withAllLocales) 50 | 51 | expectType>( 52 | createClient(CLIENT_OPTIONS).withAllLocales.withoutLinkResolution, 53 | ) 54 | 55 | expectType>( 56 | createClient(CLIENT_OPTIONS).withAllLocales.withoutUnresolvableLinks, 57 | ) 58 | -------------------------------------------------------------------------------- /lib/types/query/util.ts: -------------------------------------------------------------------------------- 1 | import type { ConditionalPick } from 'type-fest' 2 | import type { BaseFieldMap, EntryFieldType, EntryFieldTypes } from '../entry.js' 3 | 4 | /** 5 | * @category Entity 6 | */ 7 | export type FieldsType = Record 8 | 9 | /** 10 | * @category Entry 11 | */ 12 | export type EntrySkeletonType = { 13 | fields: Fields 14 | contentTypeId: Id 15 | } 16 | 17 | type BaseOrArrayType = T extends Array ? U : T 18 | type EntryFieldBaseOrArrayType = T extends EntryFieldTypes.Array ? U : T 19 | 20 | export type NonEmpty = T extends Record ? never : T 21 | 22 | export type ConditionalFixedQueries< 23 | Fields extends FieldsType, 24 | SupportedTypes, 25 | ValueType, 26 | Prefix extends string, 27 | QueryFilter extends string = '', 28 | > = { 29 | [FieldName in keyof ConditionalPick as `${Prefix}.${string & 30 | FieldName}${QueryFilter}`]?: ValueType 31 | } 32 | 33 | export type ConditionalListQueries< 34 | Fields, 35 | SupportedTypes, 36 | Prefix extends string, 37 | QueryFilter extends string = '', 38 | > = { 39 | [FieldName in keyof ConditionalPick as `${Prefix}.${string & 40 | FieldName}${QueryFilter}`]?: NonNullable>[] 41 | } 42 | 43 | export type EntryFieldsConditionalListQueries< 44 | Fields extends Record>, 45 | SupportedTypes, 46 | Prefix extends string, 47 | QueryFilter extends string = '', 48 | > = { 49 | [FieldName in keyof ConditionalPick as `${Prefix}.${string & 50 | FieldName}${QueryFilter}`]?: NonNullable< 51 | BaseFieldMap> 52 | >[] 53 | } 54 | 55 | export type ConditionalQueries< 56 | Fields, 57 | SupportedTypes, 58 | Prefix extends string, 59 | QueryFilter extends string = '', 60 | > = { 61 | [FieldName in keyof ConditionalPick as `${Prefix}.${string & 62 | FieldName}${QueryFilter}`]?: Fields[FieldName] extends Array ? T : Fields[FieldName] 63 | } 64 | 65 | export type EntryFieldsConditionalQueries< 66 | Fields extends Record>, 67 | SupportedTypes extends EntryFieldType | undefined, 68 | Prefix extends string, 69 | QueryFilter extends string = '', 70 | > = { 71 | [FieldName in keyof ConditionalPick as `${Prefix}.${string & 72 | FieldName}${QueryFilter}`]?: BaseFieldMap> 73 | } 74 | -------------------------------------------------------------------------------- /lib/types/query/equality.ts: -------------------------------------------------------------------------------- 1 | import type { EntryFields, EntryFieldType, EntryFieldTypes } from '../entry.js' 2 | import type { 3 | ConditionalQueries, 4 | EntryFieldsConditionalQueries, 5 | EntrySkeletonType, 6 | } from './util.js' 7 | 8 | type SupportedTypes = 9 | | EntryFields.Symbol 10 | | EntryFields.Symbol[] 11 | | EntryFields.Text 12 | | EntryFields.Integer 13 | | EntryFields.Number 14 | | EntryFields.Date 15 | | EntryFields.Boolean 16 | | undefined 17 | 18 | type SupportedEntryFieldTypes = 19 | | EntryFieldTypes.Symbol 20 | | EntryFieldTypes.Array 21 | | EntryFieldTypes.Text 22 | | EntryFieldTypes.Integer 23 | | EntryFieldTypes.Number 24 | | EntryFieldTypes.Date 25 | | EntryFieldTypes.Boolean 26 | | undefined 27 | 28 | /** 29 | * Equality filters in provided fields - search for exact matches 30 | * @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/equality-operator | Documentation} 31 | * @internal 32 | */ 33 | export type EqualityFilter = ConditionalQueries< 34 | Fields, 35 | SupportedTypes, 36 | Prefix, 37 | '' 38 | > 39 | 40 | /** 41 | * Equality filters in provided fields of an entry - search for exact matches 42 | * @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/equality-operator | Documentation} 43 | * @internal 44 | */ 45 | export type EntryFieldsEqualityFilter< 46 | Fields extends Record>, 47 | Prefix extends string, 48 | > = EntryFieldsConditionalQueries 49 | 50 | /** 51 | * Inequality filters in provided fields - exclude matching items 52 | * @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/inequality-operator | Documentation} 53 | * @internal 54 | */ 55 | export type InequalityFilter = ConditionalQueries< 56 | Fields, 57 | SupportedTypes, 58 | Prefix, 59 | '[ne]' 60 | > 61 | 62 | /** 63 | * Inequality filters in provided fields of an entry - exclude matching items 64 | * @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/inequality-operator | Documentation} 65 | * @internal 66 | */ 67 | export type EntryFieldsInequalityFilter< 68 | Fields extends Record>, 69 | Prefix extends string, 70 | > = EntryFieldsConditionalQueries 71 | -------------------------------------------------------------------------------- /test/integration/getAsset.test.ts: -------------------------------------------------------------------------------- 1 | import * as contentful from '../../lib/contentful' 2 | import { ValidationError } from '../../lib/utils/validation-error' 3 | import { 4 | assetMappings, 5 | localisedAssetMappings, 6 | params, 7 | previewParamsWithCSM, 8 | testEncodingDecoding, 9 | } from './utils' 10 | 11 | if (process.env.API_INTEGRATION_TESTS) { 12 | params.host = '127.0.0.1:5000' 13 | params.insecure = true 14 | } 15 | 16 | const client = contentful.createClient(params) 17 | const invalidClient = contentful.createClient({ 18 | ...params, 19 | includeContentSourceMaps: true, 20 | }) 21 | const previewClient = contentful.createClient(previewParamsWithCSM) 22 | 23 | describe('getAsset', () => { 24 | const asset = '1x0xpXu4pSGS4OukSyWGUK' 25 | 26 | test('default client', async () => { 27 | const response = await client.getAsset(asset) 28 | 29 | expect(response.fields).toBeDefined() 30 | expect(typeof response.fields.title).toBe('string') 31 | }) 32 | 33 | test('client has withAllLocales modifier', async () => { 34 | const response = await client.withAllLocales.getAsset(asset) 35 | 36 | expect(response.fields).toBeDefined() 37 | expect(typeof response.fields.title).toBe('object') 38 | }) 39 | 40 | describe('has includeContentSourceMaps enabled', () => { 41 | test('cdn client', async () => { 42 | await expect(invalidClient.getAsset(asset)).rejects.toThrow( 43 | `The 'includeContentSourceMaps' parameter can only be used with the CPA. Please set host to 'preview.contentful.com' or 'preview.eu.contentful.com' to include Content Source Maps.`, 44 | ) 45 | await expect(invalidClient.getAsset(asset)).rejects.toThrow(ValidationError) 46 | }) 47 | 48 | test('preview client', async () => { 49 | const response = await previewClient.getAsset(asset) 50 | 51 | expect(response.fields).toBeDefined() 52 | expect(typeof response.fields.title).toBe('string') 53 | expect(response.sys.contentSourceMaps).toBeDefined() 54 | expect(response.sys?.contentSourceMapsLookup).toBeDefined() 55 | testEncodingDecoding(response, assetMappings) 56 | }) 57 | 58 | test('preview client withAllLocales modifier', async () => { 59 | const response = await previewClient.withAllLocales.getAsset(asset) 60 | 61 | expect(response.fields).toBeDefined() 62 | expect(typeof response.fields.title).toBe('object') 63 | expect(response.sys.contentSourceMaps).toBeDefined() 64 | expect(response.sys?.contentSourceMapsLookup).toBeDefined() 65 | testEncodingDecoding(response, localisedAssetMappings) 66 | }) 67 | }) 68 | }) 69 | -------------------------------------------------------------------------------- /test/unit/utils/normalize-cursor-pagination-response.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from 'vitest' 2 | import { normalizeCursorPaginationResponse } from '../../../lib/utils/normalize-cursor-pagination-response' 3 | 4 | const prevToken = 5 | 'wqXDgmPDrT3CqsKBw4QewrY8YcOoeFBn.W3siY3Vyc29yIjoidHJ1ZSIsImxpbWl0IjoiMiIsInNlbGVjdCI6InN5cy5pZCJ9LFsiMjAyNS0xMC0wOVQwOToxNjozMC40MzhaIiwiMmNlSDhURlkxS1IzR0VLa05heTBtWSJdXQ' 6 | const prevRaw = '/spaces/87cj9boavvn1/entries?pagePrev=' + prevToken 7 | 8 | const nextToken = 9 | 'VAbCliFbVsOvwpw9UWpewqxWw7jDjw.W3siY3Vyc29yIjoidHJ1ZSIsImxpbWl0IjoiMiIsInNlbGVjdCI6InN5cy5pZCJ9LFsiMjAyNS0wOS0xNVQxNToyMjoyNS42MDZaIiwiMTBjQXR6RWxadmRnbkJFc3hHOHlUUCJdXQ' 10 | const nextRaw = '/spaces/87cj9boavvn1/entries?pageNext=' + nextToken 11 | 12 | describe('normalizeCursorPaginationResponse', () => { 13 | test('should not update response when "pages" is empty', () => { 14 | expect(normalizeCursorPaginationResponse({ pages: {} })).deep.equal({ 15 | pages: {}, 16 | }) 17 | }) 18 | 19 | test('should normalize prev page token when presented', () => { 20 | expect( 21 | normalizeCursorPaginationResponse({ 22 | pages: { 23 | prev: prevRaw, 24 | }, 25 | }), 26 | ).deep.equal({ 27 | pages: { 28 | prev: prevToken, 29 | }, 30 | }) 31 | }) 32 | 33 | test('should normalize next page token when presented', () => { 34 | expect( 35 | normalizeCursorPaginationResponse({ 36 | pages: { 37 | next: nextRaw, 38 | }, 39 | }), 40 | ).deep.equal({ 41 | pages: { 42 | next: nextToken, 43 | }, 44 | }) 45 | }) 46 | 47 | test('should normalize prev and next pages tokens when both presented', () => { 48 | expect( 49 | normalizeCursorPaginationResponse({ 50 | pages: { 51 | prev: prevRaw, 52 | next: nextRaw, 53 | }, 54 | }), 55 | ).deep.equal({ 56 | pages: { 57 | prev: prevToken, 58 | next: nextToken, 59 | }, 60 | }) 61 | }) 62 | 63 | test('should pass all the other fields', () => { 64 | expect( 65 | normalizeCursorPaginationResponse({ 66 | sys: { 67 | type: 'Array', 68 | }, 69 | limit: 2, 70 | items: ['item', 'item'], 71 | pages: { 72 | prev: prevRaw, 73 | next: nextRaw, 74 | }, 75 | }), 76 | ).deep.equal({ 77 | sys: { 78 | type: 'Array', 79 | }, 80 | limit: 2, 81 | items: ['item', 'item'], 82 | pages: { 83 | prev: prevToken, 84 | next: nextToken, 85 | }, 86 | }) 87 | }) 88 | }) 89 | -------------------------------------------------------------------------------- /lib/types/sync.ts: -------------------------------------------------------------------------------- 1 | import type { Asset } from './asset.js' 2 | import type { Entry } from './entry.js' 3 | import type { EntitySys } from './sys.js' 4 | import type { EntrySkeletonType } from './query/index.js' 5 | import type { LocaleCode } from './locale.js' 6 | import type { ChainModifiers } from './client.js' 7 | 8 | /** 9 | * @category Sync 10 | */ 11 | export type SyncOptions = { 12 | /** 13 | * @defaultValue true 14 | */ 15 | paginate?: boolean 16 | } 17 | 18 | /** 19 | * @category Sync 20 | */ 21 | export type SyncQuery = { 22 | initial?: true 23 | limit?: number 24 | nextSyncToken?: string 25 | nextPageToken?: string 26 | } & ( 27 | | { type: 'Entry'; content_type: string } 28 | | { type?: 'Asset' | 'Entry' | 'Deletion' | 'DeletedAsset' | 'DeletedEntry' } 29 | ) 30 | 31 | /** 32 | * @category Sync 33 | */ 34 | export type SyncPageQuery = SyncQuery & { sync_token?: string } 35 | 36 | /** 37 | * @category Sync 38 | */ 39 | export type SyncResponse = { 40 | nextPageUrl?: string 41 | nextSyncUrl?: string 42 | items: SyncEntities[] 43 | } 44 | 45 | /** 46 | * @category Sync 47 | */ 48 | export type SyncPageResponse = { 49 | nextPageToken?: string 50 | nextSyncToken?: string 51 | items: SyncEntities[] 52 | } 53 | 54 | /** 55 | * System managed metadata for deleted entries 56 | * @category Sync 57 | */ 58 | export type DeletedEntry = { 59 | sys: EntitySys & { type: 'DeletedEntry' } 60 | } 61 | 62 | /** 63 | * System managed metadata for deleted assets 64 | * @category Sync 65 | */ 66 | export type DeletedAsset = { 67 | sys: EntitySys & { type: 'DeletedAsset' } 68 | } 69 | 70 | /** 71 | * @category Sync 72 | */ 73 | export type SyncEntities = Entry | Asset | DeletedEntry | DeletedAsset 74 | 75 | /** 76 | * @category Sync 77 | */ 78 | export interface SyncCollection< 79 | EntrySkeleton extends EntrySkeletonType, 80 | Modifiers extends ChainModifiers = ChainModifiers, 81 | Locales extends LocaleCode = LocaleCode, 82 | > { 83 | entries: Array< 84 | Entry< 85 | EntrySkeleton, 86 | ChainModifiers extends Modifiers 87 | ? ChainModifiers 88 | : Exclude | 'WITH_ALL_LOCALES', 89 | Locales 90 | > 91 | > 92 | assets: Array< 93 | Asset< 94 | ChainModifiers extends Modifiers 95 | ? ChainModifiers 96 | : Exclude | 'WITH_ALL_LOCALES', 97 | Locales 98 | > 99 | > 100 | deletedEntries: Array 101 | deletedAssets: Array 102 | nextSyncToken?: string 103 | nextPageToken?: string 104 | } 105 | -------------------------------------------------------------------------------- /DOCS.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Contentful Logo 4 | 5 |

6 | 7 |

contentful.js

8 | 9 |

Contentful JavaScript Content Delivery Library

10 | 11 | ## Introduction 12 | 13 | 14 | MIT License 15 | 16 | 17 | Build Status 18 | 19 | 20 | NPM 21 | 22 | 23 | jsDelivr Hits 24 | 25 | 26 | NPM downloads 27 | 28 | 29 | GZIP bundle size 30 | 31 | 32 | Contentful.js is a JavaScript and TypeScript library for the Contentful [Content Delivery API](https://www.contentful.com/developers/docs/references/content-delivery-api/) and [Content Preview API](https://www.contentful.com/developers/docs/references/content-preview-api/). 33 | 34 | The code is documented using TypeDoc and these pages are auto-generated and published with each new version. 35 | 36 | - For API and client documentation, please refer to the list below. 37 | - For all type definitions and descriptions, please refer to the list in the sidebar. 38 | 39 | ## API Overview 40 | 41 | - Contentful Namespace 42 | - {@link createClient} 43 | - {@link EntryFields} 44 | - {@link EntryFieldTypes} 45 | - {@link ContentfulClientApi} 46 | - {@link ContentfulClientApi.createAssetKey} 47 | - {@link ContentfulClientApi.getAsset} 48 | - {@link ContentfulClientApi.getAssets} 49 | - {@link ContentfulClientApi.getContentType} 50 | - {@link ContentfulClientApi.getContentTypes} 51 | - {@link ContentfulClientApi.getEntries} 52 | - {@link ContentfulClientApi.getEntry} 53 | - {@link ContentfulClientApi.getLocales} 54 | - {@link ContentfulClientApi.getSpace} 55 | - {@link ContentfulClientApi.getTag} 56 | - {@link ContentfulClientApi.getTags} 57 | - {@link ContentfulClientApi.parseEntries} 58 | - {@link ContentfulClientApi.sync} 59 | -------------------------------------------------------------------------------- /test/types/query-types/boolean.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectAssignable, expectType } from 'tsd' 2 | import { 3 | EntryFieldTypes, 4 | EntryFieldsEqualityFilter, 5 | EntryFieldsInequalityFilter, 6 | EntryFieldsExistenceFilter, 7 | LocationSearchFilters, 8 | EntryFieldsRangeFilters, 9 | EntryFieldsFullTextSearchFilters, 10 | EntrySelectFilterWithFields, 11 | EntryFieldsSubsetFilters, 12 | EntryOrderFilterWithFields, 13 | EntryFieldsSetFilter, 14 | } from '../../../lib/' 15 | 16 | // @ts-ignore 17 | import * as mocks from '../mocks' 18 | 19 | expectAssignable>>( 20 | {}, 21 | ) 22 | 23 | expectAssignable>({}) 24 | expectType>>({ 25 | 'fields.testField': mocks.booleanValue, 26 | }) 27 | 28 | expectAssignable>({}) 29 | expectType< 30 | Required> 31 | >({ 32 | 'fields.testField[ne]': mocks.booleanValue, 33 | }) 34 | 35 | expectAssignable>({}) 36 | expectType>>( 37 | { 38 | 'fields.testField[exists]': mocks.booleanValue, 39 | }, 40 | ) 41 | 42 | expectAssignable>>( 43 | {}, 44 | ) 45 | 46 | expectAssignable< 47 | Required> 48 | >({}) 49 | 50 | expectAssignable< 51 | Required> 52 | >({}) 53 | 54 | expectAssignable>({}) 55 | expectAssignable>>({ 56 | order: ['fields.testField', '-fields.testField'], 57 | }) 58 | 59 | expectAssignable>({}) 60 | expectAssignable>>({ 61 | select: ['fields.testField'], 62 | }) 63 | 64 | expectAssignable>({}) 65 | expectType>>({ 66 | 'fields.testField[in]': mocks.booleanArrayValue, 67 | 'fields.testField[nin]': mocks.booleanArrayValue, 68 | }) 69 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | workflow_call: 5 | secrets: 6 | VAULT_URL: 7 | required: true 8 | 9 | jobs: 10 | release: 11 | name: Release 12 | permissions: 13 | contents: write 14 | id-token: write # Required for OIDC trusted publishing 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: 'Retrieve Secrets from Vault' 18 | id: vault 19 | uses: hashicorp/vault-action@v3.4.0 20 | with: 21 | url: ${{ secrets.VAULT_URL }} 22 | role: ${{ github.event.repository.name }}-github-action 23 | method: jwt 24 | path: github-actions 25 | exportEnv: false 26 | secrets: | 27 | github/token/${{ github.event.repository.name }}-semantic-release token | GITHUB_TOKEN; 28 | 29 | - name: Get Automation Bot User ID 30 | id: get-user-id 31 | run: echo "user-id=$(gh api "/users/contentful-automation[bot]" --jq .id)" >> "$GITHUB_OUTPUT" 32 | env: 33 | GITHUB_TOKEN: ${{ steps.vault.outputs.GITHUB_TOKEN }} 34 | 35 | - name: Setting up Git User Credentials 36 | run: | 37 | git config --global user.name 'contentful-automation[bot]' 38 | git config --global user.email '${{ steps.get-user-id.outputs.user-id }}+contentful-automation[bot]@users.noreply.github.com' 39 | 40 | - name: Checkout code 41 | uses: actions/checkout@v5 42 | with: 43 | fetch-depth: 0 44 | 45 | - name: Setup Node.js 46 | uses: actions/setup-node@v6 47 | with: 48 | node-version: '24' 49 | registry-url: 'https://registry.npmjs.org' 50 | cache: 'npm' 51 | 52 | - name: Install latest npm 53 | run: npm install -g npm@latest 54 | 55 | - name: Install dependencies 56 | run: npm ci 57 | 58 | - name: Restore the build folders 59 | uses: actions/cache/restore@v4 60 | with: 61 | path: | 62 | dist 63 | key: build-cache-${{ github.run_id }}-${{ github.run_attempt }} 64 | 65 | - name: Setup Chrome 66 | uses: browser-actions/setup-chrome@v2 67 | with: 68 | install-chromedriver: true 69 | 70 | - name: Run semantic release 71 | run: npm run semantic-release 72 | env: 73 | GITHUB_TOKEN: ${{ steps.vault.outputs.GITHUB_TOKEN }} 74 | # required for browser output-integration tests 75 | PUPPETEER_EXECUTABLE_PATH: /usr/bin/google-chrome 76 | - name: Publish Docs 77 | if: github.ref == 'refs/heads/master' 78 | run: | 79 | echo "Publishing Documentation" 80 | npm run docs:publish 81 | env: 82 | GITHUB_TOKEN: ${{ steps.vault.outputs.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /lib/make-client.ts: -------------------------------------------------------------------------------- 1 | import type { CreateContentfulApiParams } from './create-contentful-api.js' 2 | import createContentfulApi from './create-contentful-api.js' 3 | import type { 4 | ChainOptions, 5 | DefaultChainOption, 6 | ChainOption, 7 | ModifiersFromOptions, 8 | } from './utils/client-helpers.js' 9 | import type { ContentfulClientApi } from './types/index.js' 10 | 11 | function create( 12 | { http, getGlobalOptions }: CreateContentfulApiParams, 13 | options: OptionsType, 14 | makeInnerClient: (options: OptionsType) => ContentfulClientApi>, 15 | ) { 16 | const client = createContentfulApi( 17 | { 18 | http, 19 | getGlobalOptions, 20 | }, 21 | options, 22 | ) 23 | const response: any = client ? client : {} 24 | 25 | Object.defineProperty(response, 'withAllLocales', { 26 | get: () => makeInnerClient({ ...options, withAllLocales: true }), 27 | }) 28 | Object.defineProperty(response, 'withoutLinkResolution', { 29 | get: () => makeInnerClient({ ...options, withoutLinkResolution: true }), 30 | }) 31 | Object.defineProperty(response, 'withoutUnresolvableLinks', { 32 | get: () => makeInnerClient({ ...options, withoutUnresolvableLinks: true }), 33 | }) 34 | return Object.create(response) as ContentfulClientApi> 35 | } 36 | 37 | export const makeClient = ({ 38 | http, 39 | getGlobalOptions, 40 | }: CreateContentfulApiParams): ContentfulClientApi => { 41 | function makeInnerClient( 42 | options: Options, 43 | ): ContentfulClientApi> { 44 | return create({ http, getGlobalOptions }, options, makeInnerClient) 45 | } 46 | 47 | const client = createContentfulApi( 48 | { http, getGlobalOptions }, 49 | { 50 | withoutLinkResolution: false, 51 | withAllLocales: false, 52 | withoutUnresolvableLinks: false, 53 | }, 54 | ) 55 | 56 | return { 57 | ...client, 58 | get withAllLocales() { 59 | return makeInnerClient>({ 60 | withAllLocales: true, 61 | withoutLinkResolution: false, 62 | withoutUnresolvableLinks: false, 63 | }) 64 | }, 65 | get withoutLinkResolution() { 66 | return makeInnerClient>({ 67 | withAllLocales: false, 68 | withoutLinkResolution: true, 69 | withoutUnresolvableLinks: false, 70 | }) 71 | }, 72 | get withoutUnresolvableLinks() { 73 | return makeInnerClient>({ 74 | withAllLocales: false, 75 | withoutLinkResolution: false, 76 | withoutUnresolvableLinks: true, 77 | }) 78 | }, 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /test/types/query-types/date.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectAssignable, expectType } from 'tsd' 2 | import { 3 | EntryFieldsEqualityFilter, 4 | EntryFieldsInequalityFilter, 5 | EntryFieldsExistenceFilter, 6 | LocationSearchFilters, 7 | EntryFieldsRangeFilters, 8 | EntryFieldsFullTextSearchFilters, 9 | EntrySelectFilterWithFields, 10 | EntryFieldsSubsetFilters, 11 | EntryOrderFilterWithFields, 12 | EntryFieldsSetFilter, 13 | EntryFieldTypes, 14 | } from '../../../lib' 15 | 16 | // @ts-ignore 17 | import * as mocks from '../mocks' 18 | 19 | expectAssignable>>({}) 20 | 21 | expectAssignable>({}) 22 | expectType>>({ 23 | 'fields.testField': mocks.dateValue, 24 | }) 25 | 26 | expectAssignable>({}) 27 | expectType>>({ 28 | 'fields.testField[ne]': mocks.dateValue, 29 | }) 30 | 31 | expectAssignable>({}) 32 | expectType>>({ 33 | 'fields.testField[exists]': mocks.booleanValue, 34 | }) 35 | 36 | expectAssignable>>({}) 37 | 38 | expectAssignable>({}) 39 | expectType>>({ 40 | 'fields.testField[lt]': mocks.dateValue, 41 | 'fields.testField[lte]': mocks.dateValue, 42 | 'fields.testField[gt]': mocks.dateValue, 43 | 'fields.testField[gte]': mocks.dateValue, 44 | }) 45 | 46 | expectAssignable< 47 | Required> 48 | >({}) 49 | 50 | expectAssignable>({}) 51 | expectAssignable>>({ 52 | order: ['fields.testField', '-fields.testField'], 53 | }) 54 | 55 | expectAssignable>({}) 56 | expectAssignable>>({ 57 | select: ['fields.testField'], 58 | }) 59 | 60 | expectAssignable>({}) 61 | expectType>>({ 62 | 'fields.testField[in]': mocks.dateArrayValue, 63 | 'fields.testField[nin]': mocks.dateArrayValue, 64 | }) 65 | -------------------------------------------------------------------------------- /test/types/query-types/text.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectAssignable, expectNotAssignable, expectType } from 'tsd' 2 | import { 3 | EntryFieldTypes, 4 | EntryFieldsEqualityFilter, 5 | EntryFieldsInequalityFilter, 6 | EntryFieldsExistenceFilter, 7 | LocationSearchFilters, 8 | EntryFieldsRangeFilters, 9 | EntryFieldsFullTextSearchFilters, 10 | EntrySelectFilterWithFields, 11 | EntryFieldsSubsetFilters, 12 | EntryOrderFilterWithFields, 13 | EntryFieldsSetFilter, 14 | } from '../../../lib' 15 | 16 | // @ts-ignore 17 | import * as mocks from '../mocks' 18 | 19 | expectAssignable>({}) 20 | expectType>>({ 21 | 'fields.testField[all]': mocks.stringArrayValue, 22 | }) 23 | 24 | expectAssignable>({}) 25 | expectType>>({ 26 | 'fields.testField': mocks.stringValue, 27 | }) 28 | 29 | expectAssignable>({}) 30 | expectType>>({ 31 | 'fields.testField[ne]': mocks.stringValue, 32 | }) 33 | 34 | expectAssignable>({}) 35 | expectType>>({ 36 | 'fields.testField[exists]': mocks.booleanValue, 37 | }) 38 | 39 | expectAssignable>>({}) 40 | 41 | expectAssignable>>( 42 | {}, 43 | ) 44 | 45 | expectAssignable>( 46 | {}, 47 | ) 48 | expectType< 49 | Required> 50 | >({ 51 | 'fields.testField[match]': mocks.stringValue, 52 | }) 53 | 54 | expectAssignable>({}) 55 | expectNotAssignable>({ 56 | order: ['fields.testField', '-fields.testField'], 57 | }) 58 | 59 | expectAssignable>({}) 60 | expectAssignable>>({ 61 | select: ['fields.testField'], 62 | }) 63 | 64 | expectAssignable>({}) 65 | expectType>>({ 66 | 'fields.testField[in]': mocks.stringArrayValue, 67 | 'fields.testField[nin]': mocks.stringArrayValue, 68 | }) 69 | -------------------------------------------------------------------------------- /test/types/query-types/number.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectAssignable, expectType } from 'tsd' 2 | import { 3 | EntryFieldTypes, 4 | EntryFieldsEqualityFilter, 5 | EntryFieldsInequalityFilter, 6 | EntryFieldsExistenceFilter, 7 | LocationSearchFilters, 8 | EntryFieldsRangeFilters, 9 | EntryFieldsFullTextSearchFilters, 10 | EntrySelectFilterWithFields, 11 | EntryFieldsSubsetFilters, 12 | EntryOrderFilterWithFields, 13 | EntryFieldsSetFilter, 14 | } from '../../../lib' 15 | 16 | // @ts-ignore 17 | import * as mocks from '../mocks' 18 | 19 | expectAssignable>>( 20 | {}, 21 | ) 22 | 23 | expectAssignable>({}) 24 | expectType>>({ 25 | 'fields.testField': mocks.numberValue, 26 | }) 27 | 28 | expectAssignable>({}) 29 | expectType>>( 30 | { 31 | 'fields.testField[ne]': mocks.numberValue, 32 | }, 33 | ) 34 | 35 | expectAssignable>({}) 36 | expectType>>({ 37 | 'fields.testField[exists]': mocks.booleanValue, 38 | }) 39 | 40 | expectType>>({}) 41 | 42 | expectAssignable>({}) 43 | expectType>>({ 44 | 'fields.testField[lt]': mocks.numberValue, 45 | 'fields.testField[lte]': mocks.numberValue, 46 | 'fields.testField[gt]': mocks.numberValue, 47 | 'fields.testField[gte]': mocks.numberValue, 48 | }) 49 | 50 | expectAssignable< 51 | Required> 52 | >({}) 53 | 54 | expectAssignable>({}) 55 | expectAssignable>>({ 56 | order: ['fields.testField', '-fields.testField'], 57 | }) 58 | 59 | expectAssignable>({}) 60 | expectAssignable>>({ 61 | select: ['fields.testField'], 62 | }) 63 | 64 | expectAssignable>({}) 65 | expectType>>({ 66 | 'fields.testField[in]': mocks.numberArrayValue, 67 | 'fields.testField[nin]': mocks.numberArrayValue, 68 | }) 69 | -------------------------------------------------------------------------------- /test/types/query-types/symbol.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectAssignable, expectType } from 'tsd' 2 | import { 3 | EntryFieldTypes, 4 | EntryFieldsEqualityFilter, 5 | EntryFieldsInequalityFilter, 6 | EntryFieldsExistenceFilter, 7 | LocationSearchFilters, 8 | EntryFieldsRangeFilters, 9 | EntryFieldsFullTextSearchFilters, 10 | EntrySelectFilterWithFields, 11 | EntryFieldsSubsetFilters, 12 | EntryOrderFilterWithFields, 13 | EntryFieldsSetFilter, 14 | } from '../../../lib' 15 | 16 | // @ts-ignore 17 | import * as mocks from '../mocks' 18 | 19 | expectAssignable>({}) 20 | expectType>>({ 21 | 'fields.testField[all]': mocks.stringArrayValue, 22 | }) 23 | 24 | expectAssignable>({}) 25 | expectType>>({ 26 | 'fields.testField': mocks.stringValue, 27 | }) 28 | 29 | expectAssignable>({}) 30 | expectType>>( 31 | { 32 | 'fields.testField[ne]': mocks.stringValue, 33 | }, 34 | ) 35 | 36 | expectAssignable>({}) 37 | expectType>>({ 38 | 'fields.testField[exists]': mocks.booleanValue, 39 | }) 40 | 41 | expectAssignable>>( 42 | {}, 43 | ) 44 | 45 | expectAssignable< 46 | Required> 47 | >({}) 48 | 49 | expectAssignable>( 50 | {}, 51 | ) 52 | expectType< 53 | Required> 54 | >({ 55 | 'fields.testField[match]': mocks.stringValue, 56 | }) 57 | 58 | expectAssignable>({}) 59 | expectAssignable>>({ 60 | order: ['fields.testField', '-fields.testField'], 61 | }) 62 | 63 | expectAssignable>({}) 64 | expectAssignable>>({ 65 | select: ['fields.testField'], 66 | }) 67 | 68 | expectAssignable>({}) 69 | expectType>>({ 70 | 'fields.testField[in]': mocks.stringArrayValue, 71 | 'fields.testField[nin]': mocks.stringArrayValue, 72 | }) 73 | -------------------------------------------------------------------------------- /test/types/query-types/integer.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectAssignable, expectType } from 'tsd' 2 | import { 3 | EntryFieldsEqualityFilter, 4 | EntryFieldsInequalityFilter, 5 | EntryFieldsExistenceFilter, 6 | LocationSearchFilters, 7 | EntryFieldsRangeFilters, 8 | EntryFieldsFullTextSearchFilters, 9 | EntrySelectFilterWithFields, 10 | EntryFieldsSubsetFilters, 11 | EntryOrderFilterWithFields, 12 | EntryFieldsSetFilter, 13 | EntryFieldTypes, 14 | } from '../../../lib' 15 | 16 | // @ts-ignore 17 | import * as mocks from '../mocks' 18 | 19 | expectAssignable>>( 20 | {}, 21 | ) 22 | 23 | expectAssignable>({}) 24 | expectType>>({ 25 | 'fields.testField': mocks.numberValue, 26 | }) 27 | 28 | expectAssignable>({}) 29 | expectType< 30 | Required> 31 | >({ 32 | 'fields.testField[ne]': mocks.numberValue, 33 | }) 34 | 35 | expectAssignable>({}) 36 | expectType>>( 37 | { 38 | 'fields.testField[exists]': mocks.booleanValue, 39 | }, 40 | ) 41 | 42 | expectAssignable>>( 43 | {}, 44 | ) 45 | 46 | expectAssignable>({}) 47 | expectType>>({ 48 | 'fields.testField[lt]': mocks.numberValue, 49 | 'fields.testField[lte]': mocks.numberValue, 50 | 'fields.testField[gt]': mocks.numberValue, 51 | 'fields.testField[gte]': mocks.numberValue, 52 | }) 53 | 54 | expectAssignable< 55 | Required> 56 | >({}) 57 | 58 | expectAssignable>({}) 59 | expectAssignable>>({ 60 | order: ['fields.testField', '-fields.testField'], 61 | }) 62 | 63 | expectAssignable>({}) 64 | expectAssignable>>({ 65 | select: ['fields.testField'], 66 | }) 67 | 68 | expectAssignable>({}) 69 | expectType>>({ 70 | 'fields.testField[in]': mocks.numberArrayValue, 71 | 'fields.testField[nin]': mocks.numberArrayValue, 72 | }) 73 | -------------------------------------------------------------------------------- /test/integration/getAssetsWithCursor.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest' 2 | import * as contentful from '../../lib/contentful' 3 | import { params, previewParamsWithCSM } from './utils' 4 | 5 | if (process.env.API_INTEGRATION_TESTS) { 6 | params.host = '127.0.0.1:5000' 7 | params.insecure = true 8 | } 9 | 10 | const deliveryClient = contentful.createClient(params) 11 | const previewClient = contentful.createClient(previewParamsWithCSM) 12 | const clients = [ 13 | { type: 'default', client: deliveryClient }, 14 | { type: 'preview', client: previewClient }, 15 | ] 16 | 17 | describe('getAssetsWithCursor', () => { 18 | clients.forEach(({ type, client }) => { 19 | describe(`${type} client`, () => { 20 | test('should return cursor paginated asset collection when no query provided', async () => { 21 | const response = await client.getAssetsWithCursor() 22 | 23 | expect(response.items).not.toHaveLength(0) 24 | expect(response.pages).toBeDefined() 25 | expect((response as { total?: number }).total).toBeUndefined() 26 | 27 | response.items.forEach((item) => { 28 | expect(item.sys.type).toEqual('Asset') 29 | expect(item.fields).toBeDefined() 30 | expect(typeof item.fields.title).toBe('string') 31 | }) 32 | }) 33 | 34 | test('should return [limit] number of items', async () => { 35 | const response = await client.getAssetsWithCursor({ limit: 3 }) 36 | 37 | expect(response.items).toHaveLength(3) 38 | expect(response.pages).toBeDefined() 39 | expect((response as { total?: number }).total).toBeUndefined() 40 | 41 | response.items.forEach((item) => { 42 | expect(item.sys.type).toEqual('Asset') 43 | expect(item.fields).toBeDefined() 44 | expect(typeof item.fields.title).toBe('string') 45 | }) 46 | }) 47 | 48 | test('should support forward pagination', async () => { 49 | const firstPage = await client.getAssetsWithCursor({ limit: 2 }) 50 | const secondPage = await client.getAssetsWithCursor({ 51 | limit: 2, 52 | pageNext: firstPage.pages.next, 53 | }) 54 | 55 | expect(secondPage.items).toHaveLength(2) 56 | expect(firstPage.items[0].sys.id).not.equal(secondPage.items[0].sys.id) 57 | }) 58 | 59 | test('should support backward pagination', async () => { 60 | const firstPage = await client.getAssetsWithCursor({ limit: 2, order: ['sys.createdAt'] }) 61 | const secondPage = await client.getAssetsWithCursor({ 62 | limit: 2, 63 | pageNext: firstPage.pages.next, 64 | order: ['sys.createdAt'], 65 | }) 66 | const result = await client.getAssetsWithCursor({ 67 | limit: 2, 68 | pagePrev: secondPage.pages.prev, 69 | order: ['sys.createdAt'], 70 | }) 71 | 72 | expect(result.items).toHaveLength(2) 73 | 74 | firstPage.items.forEach((item, index) => { 75 | expect(item.sys.id).equal(result.items[index].sys.id) 76 | }) 77 | }) 78 | }) 79 | }) 80 | }) 81 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | gh-pages 3 | 4 | # Esdoc dirs 5 | out 6 | gh-pages 7 | 8 | # Docker 9 | *.dockerfile 10 | *.dockerignore 11 | 12 | # Created by https://www.gitignore.io/api/node,windows,osx,linux,vim 13 | 14 | ### Linux ### 15 | *~ 16 | 17 | # temporary files which can be created if a process still has a handle open of a deleted file 18 | .fuse_hidden* 19 | 20 | # KDE directory preferences 21 | .directory 22 | 23 | # Linux trash folder which might appear on any partition or disk 24 | .Trash-* 25 | 26 | # .nfs files are created when an open file is removed but is still being accessed 27 | .nfs* 28 | 29 | ### Node ### 30 | # Logs 31 | logs 32 | *.log 33 | npm-debug.log* 34 | yarn-debug.log* 35 | yarn-error.log* 36 | 37 | # Runtime data 38 | pids 39 | *.pid 40 | *.seed 41 | *.pid.lock 42 | 43 | # Directory for instrumented libs generated by jscoverage/JSCover 44 | lib-cov 45 | 46 | # Coverage directory used by tools like istanbul 47 | coverage 48 | 49 | # nyc test coverage 50 | .nyc_output 51 | 52 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 53 | .grunt 54 | 55 | # Bower dependency directory (https://bower.io/) 56 | bower_components 57 | 58 | # node-waf configuration 59 | .lock-wscript 60 | 61 | # Compiled binary addons (http://nodejs.org/api/addons.html) 62 | build/Release 63 | 64 | # Dependency directories 65 | node_modules/ 66 | jspm_packages/ 67 | 68 | # Typescript v1 declaration files 69 | typings/ 70 | 71 | # Optional npm cache directory 72 | .npm 73 | 74 | # Optional eslint cache 75 | .eslintcache 76 | 77 | # Optional REPL history 78 | .node_repl_history 79 | 80 | # Output of 'npm pack' 81 | *.tgz 82 | 83 | # Yarn Integrity file 84 | .yarn-integrity 85 | 86 | # dotenv environment variables file 87 | .env 88 | .envrc 89 | 90 | 91 | ### OSX ### 92 | *.DS_Store 93 | .AppleDouble 94 | .LSOverride 95 | 96 | # Icon must end with two \r 97 | Icon 98 | 99 | 100 | # Thumbnails 101 | ._* 102 | 103 | # Files that might appear in the root of a volume 104 | .DocumentRevisions-V100 105 | .fseventsd 106 | .Spotlight-V100 107 | .TemporaryItems 108 | .Trashes 109 | .VolumeIcon.icns 110 | .com.apple.timemachine.donotpresent 111 | 112 | # Directories potentially created on remote AFP share 113 | .AppleDB 114 | .AppleDesktop 115 | Network Trash Folder 116 | Temporary Items 117 | .apdisk 118 | 119 | ### Vim ### 120 | # swap 121 | [._]*.s[a-v][a-z] 122 | [._]*.sw[a-p] 123 | [._]s[a-v][a-z] 124 | [._]sw[a-p] 125 | # session 126 | Session.vim 127 | # temporary 128 | .netrwhist 129 | # auto-generated tag files 130 | tags 131 | 132 | ### Windows ### 133 | # Windows thumbnail cache files 134 | Thumbs.db 135 | ehthumbs.db 136 | ehthumbs_vista.db 137 | 138 | # Folder config file 139 | Desktop.ini 140 | 141 | # Recycle Bin used on file shares 142 | $RECYCLE.BIN/ 143 | 144 | # Windows Installer files 145 | *.cab 146 | *.msi 147 | *.msm 148 | *.msp 149 | 150 | # Windows shortcuts 151 | *.lnk 152 | # es-module.patch 153 | *.patch 154 | 155 | .idea 156 | .tool-versions 157 | 158 | # End of https://www.gitignore.io/api/node,windows,osx,linux,vim 159 | /lib/temp.ts 160 | -------------------------------------------------------------------------------- /test/types/query-types/location.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectAssignable, expectNotAssignable, expectType } from 'tsd' 2 | import { 3 | EntryFieldTypes, 4 | EntryFieldsEqualityFilter, 5 | EntryFieldsInequalityFilter, 6 | EntryFieldsExistenceFilter, 7 | LocationSearchFilters, 8 | EntryFieldsRangeFilters, 9 | EntryFieldsFullTextSearchFilters, 10 | EntrySelectFilterWithFields, 11 | EntryFieldsSubsetFilters, 12 | EntryOrderFilterWithFields, 13 | EntryFieldsSetFilter, 14 | } from '../../../lib' 15 | 16 | // @ts-ignore 17 | import * as mocks from '../mocks' 18 | 19 | expectAssignable>>( 20 | {}, 21 | ) 22 | 23 | expectAssignable< 24 | Required> 25 | >({}) 26 | 27 | expectAssignable< 28 | Required> 29 | >({}) 30 | 31 | expectAssignable>({}) 32 | expectType< 33 | Required> 34 | >({ 35 | 'fields.testField[exists]': mocks.booleanValue, 36 | }) 37 | 38 | expectAssignable>({}) 39 | expectAssignable< 40 | Required> 41 | >({ 42 | 'fields.testField[near]': mocks.nearLocationValue, 43 | 'fields.testField[within]': mocks.withinCircleLocationValue, 44 | }) 45 | expectAssignable< 46 | Required> 47 | >({ 48 | 'fields.testField[near]': mocks.nearLocationValue, 49 | 'fields.testField[within]': mocks.withinBoxLocationValue, 50 | }) 51 | expectNotAssignable< 52 | LocationSearchFilters<{ testField: EntryFieldTypes.Location }, 'fields'>['fields.testField[near]'] 53 | >(mocks.withinCircleLocationValue) 54 | expectNotAssignable< 55 | LocationSearchFilters<{ testField: EntryFieldTypes.Location }, 'fields'>['fields.testField[near]'] 56 | >(mocks.withinBoxLocationValue) 57 | expectNotAssignable< 58 | LocationSearchFilters< 59 | { testField: EntryFieldTypes.Location }, 60 | 'fields' 61 | >['fields.testField[within]'] 62 | >(mocks.nearLocationValue) 63 | 64 | expectAssignable< 65 | Required> 66 | >({}) 67 | 68 | expectAssignable< 69 | Required> 70 | >({}) 71 | 72 | expectAssignable>({}) 73 | expectAssignable>>({ 74 | order: ['fields.testField', '-fields.testField'], 75 | }) 76 | 77 | expectAssignable>({}) 78 | expectAssignable>>({ 79 | select: ['fields.testField'], 80 | }) 81 | 82 | expectAssignable< 83 | Required> 84 | >({}) 85 | -------------------------------------------------------------------------------- /test/integration/getEntriesWithCursor.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest' 2 | import * as contentful from '../../lib/contentful' 3 | import { params, previewParamsWithCSM } from './utils' 4 | import { EntryFieldTypes, EntrySkeletonType } from '../../lib' 5 | 6 | if (process.env.API_INTEGRATION_TESTS) { 7 | params.host = '127.0.0.1:5000' 8 | params.insecure = true 9 | } 10 | 11 | const deliveryClient = contentful.createClient(params) 12 | const previewClient = contentful.createClient(previewParamsWithCSM) 13 | const clients = [ 14 | { type: 'default', client: deliveryClient }, 15 | { type: 'preview', client: previewClient }, 16 | ] 17 | 18 | describe('getEntriesWithCursor', () => { 19 | const entryWithResolvableLink = 'nyancat' 20 | 21 | clients.forEach(({ type, client }) => { 22 | describe(`${type} client`, () => { 23 | test('should return cursor paginated entry collection when no query provided', async () => { 24 | const response = await client.getEntriesWithCursor< 25 | EntrySkeletonType<{ 26 | bestFriend: EntryFieldTypes.EntryLink 27 | color: EntryFieldTypes.Symbol 28 | }> 29 | >({ 'sys.id': entryWithResolvableLink, include: 2 }) 30 | 31 | expect(response.items).not.toHaveLength(0) 32 | expect(response.pages).toBeDefined() 33 | expect((response as { total?: number }).total).toBeUndefined() 34 | 35 | response.items.forEach((item) => { 36 | expect(item.sys.type).toEqual('Entry') 37 | expect(item.fields).toBeDefined() 38 | }) 39 | }) 40 | 41 | test('should return [limit] number of items', async () => { 42 | const response = await client.getEntriesWithCursor({ limit: 3 }) 43 | 44 | expect(response.items).toHaveLength(3) 45 | expect(response.pages).toBeDefined() 46 | expect((response as { total?: number }).total).toBeUndefined() 47 | 48 | response.items.forEach((item) => { 49 | expect(item.sys.type).toEqual('Entry') 50 | expect(item.fields).toBeDefined() 51 | }) 52 | }) 53 | 54 | test('should support forward pagination', async () => { 55 | const firstPage = await client.getEntriesWithCursor({ limit: 2 }) 56 | const secondPage = await client.getEntriesWithCursor({ 57 | limit: 2, 58 | pageNext: firstPage.pages.next, 59 | }) 60 | 61 | expect(secondPage.items).toHaveLength(2) 62 | expect(firstPage.items[0].sys.id).not.equal(secondPage.items[0].sys.id) 63 | }) 64 | 65 | test('should support backward pagination', async () => { 66 | const firstPage = await client.getEntriesWithCursor({ limit: 2, order: ['sys.createdAt'] }) 67 | const secondPage = await client.getEntriesWithCursor({ 68 | limit: 2, 69 | pageNext: firstPage.pages.next, 70 | order: ['sys.createdAt'], 71 | }) 72 | const result = await client.getEntriesWithCursor({ 73 | limit: 2, 74 | pagePrev: secondPage.pages.prev, 75 | order: ['sys.createdAt'], 76 | }) 77 | 78 | expect(result.items).toHaveLength(2) 79 | 80 | firstPage.items.forEach((item, index) => { 81 | expect(item.sys.id).equal(result.items[index].sys.id) 82 | }) 83 | }) 84 | }) 85 | }) 86 | }) 87 | -------------------------------------------------------------------------------- /lib/types/query/order.ts: -------------------------------------------------------------------------------- 1 | import type { EntrySkeletonType, FieldsType } from './util.js' 2 | import type { EntryFields, EntryFieldType, EntryFieldTypes, EntrySys } from '../entry.js' 3 | import type { AssetSys } from '../asset.js' 4 | import type { ConditionalPick } from 'type-fest' 5 | import type { TagSys } from '../tag.js' 6 | 7 | export type SupportedTypes = 8 | | EntryFields.Symbol 9 | | EntryFields.Integer 10 | | EntryFields.Number 11 | | EntryFields.Date 12 | | EntryFields.Boolean 13 | | EntryFields.Location 14 | | undefined 15 | 16 | export type SupportedEntryFieldTypes = 17 | | EntryFieldTypes.Symbol 18 | | EntryFieldTypes.Integer 19 | | EntryFieldTypes.Number 20 | | EntryFieldTypes.Date 21 | | EntryFieldTypes.Boolean 22 | | EntryFieldTypes.Location 23 | | undefined 24 | 25 | export type SupportedLinkTypes = 26 | | EntryFieldTypes.AssetLink 27 | | EntryFieldTypes.EntryLink 28 | | undefined 29 | 30 | export type OrderFilterPaths = 31 | | `${Prefix}.${keyof ConditionalPick & string}` 32 | | `-${Prefix}.${keyof ConditionalPick & string}` 33 | 34 | /** 35 | * Order for provided fields in an entry 36 | * @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/order | Documentation} 37 | * @internal 38 | */ 39 | export type EntryOrderFilterWithFields< 40 | Fields extends Record>, 41 | > = { 42 | order?: ( 43 | | `fields.${keyof ConditionalPick & string}` 44 | | `-fields.${keyof ConditionalPick & string}` 45 | | `fields.${keyof ConditionalPick & string}.sys.id` 46 | | `-fields.${keyof ConditionalPick & string}.sys.id` 47 | | OrderFilterPaths 48 | | 'sys.contentType.sys.id' 49 | | '-sys.contentType.sys.id' 50 | )[] 51 | } 52 | 53 | /** 54 | * Order in an entry 55 | * @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/order | Documentation} 56 | * @internal 57 | */ 58 | export type EntryOrderFilter = { 59 | order?: ( 60 | | OrderFilterPaths 61 | | 'sys.contentType.sys.id' 62 | | '-sys.contentType.sys.id' 63 | )[] 64 | } 65 | 66 | /** 67 | * Order in an asset 68 | * @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/order | Documentation} 69 | * @internal 70 | */ 71 | export type AssetOrderFilter = { 72 | order?: ( 73 | | OrderFilterPaths 74 | | 'fields.file.contentType' 75 | | '-fields.file.contentType' 76 | | 'fields.file.fileName' 77 | | '-fields.file.fileName' 78 | | 'fields.file.url' 79 | | '-fields.file.url' 80 | | 'fields.file.details.size' 81 | | '-fields.file.details.size' 82 | )[] 83 | } 84 | 85 | /** 86 | * Order in a tag 87 | * @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/order | Documentation} 88 | * @internal 89 | */ 90 | export type TagOrderFilter = { 91 | order?: (OrderFilterPaths | 'name' | '-name')[] 92 | } 93 | 94 | export type TaxonomyOrderFilter = { 95 | order?: ('sys.createdAt' | 'sys.updatedAt' | 'prefLabel')[] 96 | } 97 | -------------------------------------------------------------------------------- /test/integration/utils.ts: -------------------------------------------------------------------------------- 1 | import { decode, SourceMapMetadata } from '@contentful/content-source-maps' 2 | import { get } from 'json-pointer' 3 | import { 4 | Asset, 5 | AssetCollection, 6 | ChainModifiers, 7 | CreateClientParams, 8 | Entry, 9 | EntryCollection, 10 | EntrySkeletonType, 11 | LocaleCode, 12 | } from '../../lib' 13 | 14 | export const params: CreateClientParams = { 15 | accessToken: 'QGT8WxED1nwrbCUpY6VEK6eFvZwvlC5ujlX-rzUq97U', 16 | space: 'ezs1swce23xe', 17 | } 18 | export const localeSpaceParams = { 19 | accessToken: 'p1qWlqQjma9OL_Cb-BN8YvpZ0KnRfXPjvqIWChlfL04', 20 | space: '7dh3w86is8ls', 21 | } 22 | 23 | export const previewParams = { 24 | host: 'preview.contentful.com', 25 | accessToken: 'WwNjBWmjh5DJLhrpDuoDyFX-wTz80WLalpdyFQTMGns', 26 | space: 'ezs1swce23xe', 27 | } 28 | 29 | export const previewParamsWithCSM = { 30 | ...previewParams, 31 | includeContentSourceMaps: true, 32 | } 33 | 34 | export type Mappings = Record< 35 | string, 36 | SourceMapMetadata | Record | undefined 37 | > 38 | 39 | type EncodedResponse = 40 | | Asset 41 | | Entry 42 | | AssetCollection 43 | | EntryCollection 44 | 45 | export function testEncodingDecoding(encodedResponse: EncodedResponse, mappings: Mappings) { 46 | for (const [key, expectedValue] of Object.entries(mappings)) { 47 | const encodedValue = get(encodedResponse, key) 48 | const decodedValue = decode(encodedValue) 49 | 50 | expect(decodedValue).toEqual(expectedValue) 51 | } 52 | } 53 | 54 | const mappedAsset = { 55 | origin: 'contentful.com', 56 | href: 'https://app.contentful.com/spaces/ezs1swce23xe/environments/master/assets/1x0xpXu4pSGS4OukSyWGUK/?focusedField=title&focusedLocale=en-US&source=vercel-content-link', 57 | contentful: { 58 | editorInterface: { 59 | widgetId: 'singleLine', 60 | widgetNamespace: 'builtin', 61 | }, 62 | fieldType: 'Symbol', 63 | }, 64 | } 65 | 66 | const mappedAssetCollection = { 67 | origin: 'contentful.com', 68 | href: 'https://app.contentful.com/spaces/ezs1swce23xe/environments/master/assets/38eGMAzUH5Ezv7mjU0CToA/?focusedField=title&focusedLocale=en-US&source=vercel-content-link', 69 | contentful: { 70 | editorInterface: { 71 | widgetId: 'singleLine', 72 | widgetNamespace: 'builtin', 73 | }, 74 | fieldType: 'Symbol', 75 | }, 76 | } 77 | 78 | const mappedEntryCollection = { 79 | origin: 'contentful.com', 80 | href: 'https://app.contentful.com/spaces/ezs1swce23xe/environments/master/assets/38eGMAzUH5Ezv7mjU0CToA/?focusedField=title&focusedLocale=en-US&source=vercel-content-link', 81 | contentful: { 82 | editorInterface: { 83 | widgetId: 'singleLine', 84 | widgetNamespace: 'builtin', 85 | }, 86 | fieldType: 'Symbol', 87 | }, 88 | } 89 | 90 | export const assetMappings = { 91 | '/fields/title': mappedAsset, 92 | } 93 | 94 | export const localisedAssetMappings = { 95 | '/fields/title/en-US': mappedAsset, 96 | } 97 | 98 | export const assetMappingsCollection = { 99 | '/items/0/fields/title': mappedAssetCollection, 100 | } 101 | 102 | export const entryMappingsCollection = { 103 | '/items/0/fields/name': mappedEntryCollection, 104 | } 105 | 106 | export const localisedAssetMappingsCollection = { 107 | '/items/0/fields/title/en-US': mappedAssetCollection, 108 | } 109 | -------------------------------------------------------------------------------- /lib/utils/validate-params.ts: -------------------------------------------------------------------------------- 1 | import type { TimelinePreview } from '../types/timeline-preview.js' 2 | import { isValidTimelinePreviewConfig } from './timeline-preview-helpers.js' 3 | import { ValidationError } from './validation-error.js' 4 | 5 | function checkLocaleParamIsAll(query) { 6 | if (query.locale === '*') { 7 | throw new ValidationError( 8 | 'locale', 9 | `The use of locale='*' is no longer supported.To fetch an entry in all existing locales, 10 | use client.withAllLocales instead of the locale='*' parameter.`, 11 | ) 12 | } 13 | } 14 | 15 | function checkLocaleParamExists(query) { 16 | if (query.locale) { 17 | throw new ValidationError('locale', 'The `locale` parameter is not allowed') 18 | } 19 | } 20 | 21 | export function validateLocaleParam(query, isWithAllLocalesClient: boolean) { 22 | if (isWithAllLocalesClient) { 23 | checkLocaleParamExists(query) 24 | } else { 25 | checkLocaleParamIsAll(query) 26 | } 27 | return 28 | } 29 | 30 | export function validateResolveLinksParam(query) { 31 | if ('resolveLinks' in query) { 32 | throw new ValidationError( 33 | 'resolveLinks', 34 | `The use of the 'resolveLinks' parameter is no longer supported. By default, links are resolved. 35 | If you do not want to resolve links, use client.withoutLinkResolution.`, 36 | ) 37 | } 38 | return 39 | } 40 | 41 | export function validateRemoveUnresolvedParam(query) { 42 | if ('removeUnresolved' in query) { 43 | throw new ValidationError( 44 | 'removeUnresolved', 45 | `The use of the 'removeUnresolved' parameter is no longer supported. By default, unresolved links are kept as link objects. 46 | If you do not want to include unresolved links, use client.withoutUnresolvableLinks.`, 47 | ) 48 | } 49 | return 50 | } 51 | 52 | export function checkIncludeContentSourceMapsParamIsAllowed( 53 | host?: string, 54 | includeContentSourceMaps?: boolean, 55 | ) { 56 | if (includeContentSourceMaps === undefined) { 57 | return false 58 | } 59 | 60 | if (typeof includeContentSourceMaps !== 'boolean') { 61 | throw new ValidationError( 62 | 'includeContentSourceMaps', 63 | `The 'includeContentSourceMaps' parameter must be a boolean.`, 64 | ) 65 | } 66 | 67 | const includeContentSourceMapsIsAllowed = typeof host === 'string' && host.startsWith('preview') 68 | 69 | if (includeContentSourceMaps && !includeContentSourceMapsIsAllowed) { 70 | throw new ValidationError( 71 | 'includeContentSourceMaps', 72 | `The 'includeContentSourceMaps' parameter can only be used with the CPA. Please set host to 'preview.contentful.com' or 'preview.eu.contentful.com' to include Content Source Maps. 73 | `, 74 | ) 75 | } 76 | 77 | return includeContentSourceMaps as boolean 78 | } 79 | 80 | export function checkEnableTimelinePreviewIsAllowed( 81 | host: string, 82 | timelinePreview?: TimelinePreview, 83 | ) { 84 | if (timelinePreview === undefined) { 85 | return false 86 | } 87 | 88 | const isValidConfig = isValidTimelinePreviewConfig(timelinePreview) 89 | 90 | const isValidHost = typeof host === 'string' && host.startsWith('preview') 91 | 92 | if (isValidConfig && !isValidHost) { 93 | throw new ValidationError( 94 | 'timelinePreview', 95 | `The 'timelinePreview' parameter can only be used with the CPA. Please set host to 'preview.contentful.com' or 'preview.eu.contentful.com' to enable Timeline Preview. 96 | `, 97 | ) 98 | } 99 | 100 | return true 101 | } 102 | -------------------------------------------------------------------------------- /test/integration/getTags.test.ts: -------------------------------------------------------------------------------- 1 | import * as contentful from '../../lib/contentful' 2 | import { params } from './utils' 3 | 4 | if (process.env.API_INTEGRATION_TESTS) { 5 | params.host = '127.0.0.1:5000' 6 | params.insecure = true 7 | } 8 | 9 | const client = contentful.createClient(params) 10 | 11 | describe('getTags', () => { 12 | it('returns all tags when no filters are available', async () => { 13 | const response = await client.getTags() 14 | 15 | expect(response.items[0].sys.type).toBe('Tag') 16 | }) 17 | 18 | describe('tagName filters', () => { 19 | it('gets the tag with the name equals to the provided value', async () => { 20 | const response = await client.getTags({ name: 'public tag 1' }) 21 | 22 | expect(response.items).toHaveLength(1) 23 | expect(response.items[0].name).toEqual('public tag 1') 24 | }) 25 | 26 | it('gets the tag with the name not equals to the provided value', async () => { 27 | const response = await client.getTags({ 'name[ne]': 'public tag 1' }) 28 | 29 | expect(response.items).toHaveLength(0) 30 | expect(response.items).toEqual([]) 31 | }) 32 | 33 | it('gets the tags with the name matching to the provided value', async () => { 34 | const response = await client.getTags({ 'name[match]': 'public tag' }) 35 | 36 | expect(response.items).toHaveLength(1) 37 | expect(response.items[0].name).toEqual('public tag 1') 38 | }) 39 | 40 | it('gets the tags with the name in the list of the provided value', async () => { 41 | const response = await client.getTags({ 'name[in]': ['public tag', 'public tag 1'] }) 42 | 43 | expect(response.items).toHaveLength(1) 44 | expect(response.items[0].name).toEqual('public tag 1') 45 | }) 46 | 47 | it('gets the tags with the name not in the list of the provided value', async () => { 48 | const response = await client.getTags({ 'name[nin]': ['public tag', 'public tag 1'] }) 49 | 50 | expect(response.items).toHaveLength(0) 51 | expect(response.items).toEqual([]) 52 | }) 53 | 54 | it('gets the tags with the name exists', async () => { 55 | const response = await client.getTags({ 'name[exists]': true }) 56 | 57 | expect(response.items).toHaveLength(1) 58 | expect(response.items[0].name).toEqual('public tag 1') 59 | }) 60 | }) 61 | 62 | describe('sys filters', () => { 63 | it('can filter by id', async () => { 64 | const response = await client.getTags({ 'sys.id': 'publicTag1' }) 65 | 66 | expect(response.items).toHaveLength(1) 67 | expect(response.items[0].sys.id).toEqual('publicTag1') 68 | }) 69 | 70 | it('can filter by createdAt', async () => { 71 | const response = await client.getTags({ 'sys.createdAt': '2021-02-11T14:44:48.594Z' }) 72 | 73 | expect(response.items).toHaveLength(1) 74 | expect(response.items[0].sys.id).toEqual('publicTag1') 75 | }) 76 | 77 | it('can filter by updateAt', async () => { 78 | const response = await client.getTags({ 'sys.updatedAt': '2021-02-11T14:44:48.594Z' }) 79 | 80 | expect(response.items).toHaveLength(1) 81 | expect(response.items[0].sys.id).toEqual('publicTag1') 82 | }) 83 | 84 | it('can filter by visibility', async () => { 85 | const response = await client.getTags({ 'sys.visibility': 'private' }) 86 | 87 | expect(response.items).toHaveLength(0) 88 | expect(response.items).toEqual([]) 89 | }) 90 | 91 | it('can filter by type', async () => { 92 | const response = await client.getTags({ 'sys.type': 'Tag' }) 93 | 94 | expect(response.items).toHaveLength(1) 95 | expect(response.items[0].sys.id).toEqual('publicTag1') 96 | }) 97 | }) 98 | }) 99 | -------------------------------------------------------------------------------- /.github/workflows/check.yaml: -------------------------------------------------------------------------------- 1 | name: Run Checks 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | workflow_call: 8 | 9 | jobs: 10 | lint: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v5 16 | 17 | - name: Setup Node.js 18 | uses: actions/setup-node@v6 19 | with: 20 | node-version: '24' 21 | cache: 'npm' 22 | 23 | - name: Install latest npm 24 | run: npm install -g npm@latest 25 | 26 | - name: Install dependencies 27 | run: npm ci 28 | 29 | - name: Run linter 30 | run: npm run lint 31 | 32 | prettier: 33 | runs-on: ubuntu-latest 34 | 35 | steps: 36 | - name: Checkout code 37 | uses: actions/checkout@v5 38 | 39 | - name: Setup Node.js 40 | uses: actions/setup-node@v6 41 | with: 42 | node-version: '24' 43 | cache: 'npm' 44 | 45 | - name: Install latest npm 46 | run: npm install -g npm@latest 47 | 48 | - name: Install dependencies 49 | run: npm ci 50 | 51 | - name: Check prettier formatting 52 | run: npm run prettier:check 53 | 54 | test: 55 | runs-on: ubuntu-latest 56 | 57 | steps: 58 | - name: Checkout code 59 | uses: actions/checkout@v5 60 | 61 | - name: Setup Node.js 62 | uses: actions/setup-node@v6 63 | with: 64 | node-version: '24' 65 | cache: 'npm' 66 | 67 | - name: Install latest npm 68 | run: npm install -g npm@latest 69 | 70 | - name: Install dependencies 71 | run: npm ci 72 | 73 | - name: Restore the build folders 74 | uses: actions/cache/restore@v4 75 | with: 76 | path: | 77 | dist 78 | key: build-cache-${{ github.run_id }}-${{ github.run_attempt }} 79 | 80 | - name: Run tests 81 | run: npm test 82 | 83 | - name: Run demo tests 84 | run: npm run test:demo-projects 85 | 86 | test-bundle-size: 87 | runs-on: ubuntu-latest 88 | 89 | steps: 90 | - name: Checkout code 91 | uses: actions/checkout@v5 92 | 93 | - name: Setup Node.js 94 | uses: actions/setup-node@v6 95 | with: 96 | node-version: '24' 97 | cache: 'npm' 98 | 99 | - name: Install latest npm 100 | run: npm install -g npm@latest 101 | 102 | - name: Install dependencies 103 | run: npm ci 104 | 105 | - name: Restore the build folders 106 | uses: actions/cache/restore@v4 107 | with: 108 | path: | 109 | dist 110 | key: build-cache-${{ github.run_id }}-${{ github.run_attempt }} 111 | 112 | - name: Test bundle size 113 | run: npm run test:size 114 | 115 | test-types: 116 | runs-on: ubuntu-latest 117 | 118 | steps: 119 | - name: Checkout code 120 | uses: actions/checkout@v5 121 | 122 | - name: Setup Node.js 123 | uses: actions/setup-node@v6 124 | with: 125 | node-version: '24' 126 | cache: 'npm' 127 | 128 | - name: Install latest npm 129 | run: npm install -g npm@latest 130 | 131 | - name: Install dependencies 132 | run: npm ci 133 | 134 | - name: Restore the build folders 135 | uses: actions/cache/restore@v4 136 | with: 137 | path: | 138 | dist 139 | key: build-cache-${{ github.run_id }}-${{ github.run_attempt }} 140 | 141 | - name: Test TypeScript types 142 | run: npm run test:types 143 | 144 | - name: Run checks 145 | run: npm run check 146 | -------------------------------------------------------------------------------- /lib/types/asset.ts: -------------------------------------------------------------------------------- 1 | import type { ContentfulCollection, CursorPaginatedCollection } from './collection.js' 2 | import type { LocaleCode } from './locale.js' 3 | import type { Metadata } from './metadata.js' 4 | import type { EntitySys } from './sys.js' 5 | import type { ChainModifiers } from './client.js' 6 | 7 | /** 8 | * @category Asset 9 | */ 10 | export interface AssetDetails { 11 | size: number 12 | image?: { 13 | width: number 14 | height: number 15 | } 16 | } 17 | 18 | /** 19 | * @category Asset 20 | */ 21 | export interface AssetFile { 22 | url: string 23 | details: AssetDetails 24 | fileName: string 25 | contentType: string 26 | } 27 | 28 | /** 29 | * @category Asset 30 | */ 31 | export interface AssetFields { 32 | title?: string 33 | description?: string 34 | file?: AssetFile 35 | } 36 | 37 | /** 38 | * Assets are binary files in a Contentful space 39 | * @category Asset 40 | * @typeParam Modifiers - The chain modifiers used to configure the client. They’re set automatically when using the client chain modifiers. 41 | * @typeParam Locales - If provided for a client using `allLocales` modifier, response type defines locale keys for asset field values. 42 | * @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/assets | Documentation} 43 | */ 44 | export interface Asset< 45 | Modifiers extends ChainModifiers = ChainModifiers, 46 | Locales extends LocaleCode = LocaleCode, 47 | > { 48 | sys: AssetSys 49 | fields: ChainModifiers extends Modifiers 50 | ? 51 | | { [FieldName in keyof AssetFields]: { [LocaleName in Locales]?: AssetFields[FieldName] } } 52 | | AssetFields 53 | : 'WITH_ALL_LOCALES' extends Modifiers 54 | ? { [FieldName in keyof AssetFields]: { [LocaleName in Locales]?: AssetFields[FieldName] } } 55 | : AssetFields 56 | metadata: Metadata 57 | } 58 | 59 | /** 60 | * @category Asset 61 | */ 62 | export type AssetMimeType = 63 | | 'attachment' 64 | | 'plaintext' 65 | | 'image' 66 | | 'audio' 67 | | 'video' 68 | | 'richtext' 69 | | 'presentation' 70 | | 'spreadsheet' 71 | | 'pdfdocument' 72 | | 'archive' 73 | | 'code' 74 | | 'markup' 75 | 76 | /** 77 | * A collection of assets 78 | * @category Asset 79 | * @typeParam Modifiers - The chain modifiers used to configure the client. They’re set automatically when using the client chain modifiers. 80 | * @typeParam Locales - If provided for a client using `allLocales` modifier, response type defines locale keys for asset field values. 81 | * @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/assets | Documentation} 82 | */ 83 | export type AssetCollection< 84 | Modifiers extends ChainModifiers = ChainModifiers, 85 | Locales extends LocaleCode = LocaleCode, 86 | > = ContentfulCollection> 87 | 88 | /** 89 | * A cursor paginated collection of assets 90 | * @category Asset 91 | * @typeParam Modifiers - The chain modifiers used to configure the client. They’re set automatically when using the client chain modifiers. 92 | * @typeParam Locales - If provided for a client using `allLocales` modifier, response type defines locale keys for asset field values. 93 | * @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/assets | Documentation} 94 | */ 95 | export type AssetCursorPaginatedCollection< 96 | Modifiers extends ChainModifiers = ChainModifiers, 97 | Locales extends LocaleCode = LocaleCode, 98 | > = CursorPaginatedCollection> 99 | 100 | /** 101 | * System managed metadata for assets 102 | * @category Asset 103 | */ 104 | export type AssetSys = EntitySys & { 105 | type: 'Asset' 106 | } 107 | -------------------------------------------------------------------------------- /test/integration/sync.test.ts: -------------------------------------------------------------------------------- 1 | import * as contentful from '../../lib/contentful' 2 | import { TypeCatSkeleton } from './parseEntries.test' 3 | import { params } from './utils' 4 | 5 | if (process.env.API_INTEGRATION_TESTS) { 6 | params.host = '127.0.0.1:5000' 7 | params.insecure = true 8 | } 9 | 10 | const client = contentful.createClient(params) 11 | 12 | describe('Sync API', () => { 13 | test('Sync space', async () => { 14 | const response = await client.sync({ initial: true }) 15 | expect(response.entries).toBeDefined() 16 | expect(response.assets).toBeDefined() 17 | expect(response.deletedEntries).toBeDefined() 18 | expect(response.deletedAssets).toBeDefined() 19 | expect(response.nextSyncToken).toBeDefined() 20 | }) 21 | 22 | test('Sync space asset links are resolved', async () => { 23 | const response = await client.sync({ initial: true }) 24 | expect(response.entries).toBeDefined() 25 | 26 | const entryWithImageLink = response.entries.find((entry) => entry.fields && entry.fields.image) 27 | expect( 28 | entryWithImageLink?.fields.image && entryWithImageLink?.fields.image['en-US'].sys.type, 29 | ).toBe('Asset') 30 | }) 31 | 32 | test('Sync space with token', async () => { 33 | const response = await client.sync({ 34 | nextSyncToken: 35 | 'w5ZGw6JFwqZmVcKsE8Kow4grw45QdybDsm4DWMK6OVYsSsOJwqPDksOVFXUFw54Hw65Tw6MAwqlWw5QkdcKjwqrDlsOiw4zDolvDq8KRRwUVBn3CusK6wpB3w690w6vDtMKkwrHDmsKSwobCuMKww57Cl8OGwp_Dq1QZCA', 36 | }) 37 | expect(response.entries).toBeDefined() 38 | expect(response.assets).toBeDefined() 39 | expect(response.deletedEntries).toBeDefined() 40 | expect(response.deletedAssets).toBeDefined() 41 | expect(response.nextSyncToken).toBeDefined() 42 | }) 43 | 44 | test('Sync spaces assets', async () => { 45 | const response = await client.sync({ initial: true, type: 'Asset' }) 46 | expect(response.assets).toBeDefined() 47 | expect(response.deletedAssets).toBeDefined() 48 | expect(response.nextSyncToken).toBeDefined() 49 | }) 50 | 51 | test('Sync space entries by content type', async () => { 52 | const response = await client.sync({ 53 | initial: true, 54 | type: 'Entry', 55 | content_type: 'dog', 56 | }) 57 | expect(response.entries).toBeDefined() 58 | expect(response.deletedEntries).toBeDefined() 59 | expect(response.nextSyncToken).toBeDefined() 60 | }) 61 | 62 | test('Sync has withoutUnresolvableLinks modifier', async () => { 63 | const response = await client.withoutUnresolvableLinks.sync({ 64 | initial: true, 65 | type: 'Entry', 66 | content_type: 'cat', 67 | }) 68 | 69 | expect(response.entries[0].fields).toBeDefined() 70 | expect(response.entries[0].fields.bestFriend).toEqual({}) 71 | }) 72 | 73 | test('Sync ignores withAllLocales modifier', async () => { 74 | const responseWithLocales = await client.withAllLocales.sync({ 75 | initial: true, 76 | type: 'Entry', 77 | content_type: 'cat', 78 | }) 79 | 80 | const responseWithoutLocales = await client.sync({ 81 | initial: true, 82 | type: 'Entry', 83 | content_type: 'cat', 84 | }) 85 | 86 | expect(responseWithLocales).toStrictEqual(responseWithoutLocales) 87 | }) 88 | 89 | test('Sync has withoutLinkResolution modifier', async () => { 90 | const response = await client.withoutLinkResolution.sync({ 91 | initial: true, 92 | type: 'Entry', 93 | content_type: 'cat', 94 | }) 95 | 96 | expect(response.entries[0].fields).toBeDefined() 97 | expect( 98 | response.entries[0].fields.bestFriend && 99 | response.entries[0].fields.bestFriend['en-US']?.sys.type, 100 | ).toBe('Link') 101 | }) 102 | }) 103 | -------------------------------------------------------------------------------- /test/types/query-types/symbol-array.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectAssignable, expectNotAssignable, expectType } from 'tsd' 2 | import { 3 | EntryFieldTypes, 4 | EntryFieldsEqualityFilter, 5 | EntryFieldsInequalityFilter, 6 | EntryFieldsExistenceFilter, 7 | LocationSearchFilters, 8 | EntryFieldsRangeFilters, 9 | EntryFieldsFullTextSearchFilters, 10 | EntrySelectFilterWithFields, 11 | EntryFieldsSubsetFilters, 12 | EntryOrderFilterWithFields, 13 | EntryFieldsSetFilter, 14 | } from '../../../lib' 15 | 16 | // @ts-ignore 17 | import * as mocks from '../mocks' 18 | 19 | expectAssignable< 20 | EntryFieldsSetFilter<{ testField: EntryFieldTypes.Array }, 'fields'> 21 | >({}) 22 | expectType< 23 | Required< 24 | EntryFieldsSetFilter<{ testField?: EntryFieldTypes.Array }, 'fields'> 25 | > 26 | >({ 27 | 'fields.testField[all]': mocks.stringArrayValue, 28 | }) 29 | 30 | expectAssignable< 31 | EntryFieldsEqualityFilter<{ testField: EntryFieldTypes.Array }, 'fields'> 32 | >({}) 33 | expectType< 34 | Required< 35 | EntryFieldsEqualityFilter< 36 | { testField?: EntryFieldTypes.Array }, 37 | 'fields' 38 | > 39 | > 40 | >({ 41 | 'fields.testField': mocks.stringValue, 42 | }) 43 | 44 | expectAssignable< 45 | EntryFieldsInequalityFilter< 46 | { testField: EntryFieldTypes.Array }, 47 | 'fields' 48 | > 49 | >({}) 50 | expectType< 51 | Required< 52 | EntryFieldsInequalityFilter< 53 | { testField?: EntryFieldTypes.Array }, 54 | 'fields' 55 | > 56 | > 57 | >({ 58 | 'fields.testField[ne]': mocks.stringValue, 59 | }) 60 | 61 | expectAssignable< 62 | EntryFieldsExistenceFilter<{ testField: EntryFieldTypes.Array }, 'fields'> 63 | >({}) 64 | expectType< 65 | Required< 66 | EntryFieldsExistenceFilter< 67 | { testField?: EntryFieldTypes.Array }, 68 | 'fields' 69 | > 70 | > 71 | >({ 72 | 'fields.testField[exists]': mocks.booleanValue, 73 | }) 74 | 75 | expectAssignable< 76 | Required< 77 | LocationSearchFilters<{ testField: EntryFieldTypes.Array }, 'fields'> 78 | > 79 | >({}) 80 | 81 | expectAssignable< 82 | Required< 83 | EntryFieldsRangeFilters<{ testField: EntryFieldTypes.Array }, 'fields'> 84 | > 85 | >({}) 86 | 87 | expectAssignable< 88 | EntryFieldsFullTextSearchFilters< 89 | { testField: EntryFieldTypes.Array }, 90 | 'fields' 91 | > 92 | >({}) 93 | expectType< 94 | Required< 95 | EntryFieldsFullTextSearchFilters< 96 | { testField?: EntryFieldTypes.Array }, 97 | 'fields' 98 | > 99 | > 100 | >({ 101 | 'fields.testField[match]': mocks.stringValue, 102 | }) 103 | 104 | expectNotAssignable< 105 | Required }>> 106 | >({ 107 | order: ['fields.testField'], 108 | }) 109 | 110 | expectAssignable< 111 | EntrySelectFilterWithFields<{ testField: EntryFieldTypes.Array }> 112 | >({}) 113 | expectAssignable< 114 | Required< 115 | EntrySelectFilterWithFields<{ testField: EntryFieldTypes.Array }> 116 | > 117 | >({ 118 | select: ['fields.testField'], 119 | }) 120 | 121 | expectAssignable< 122 | EntryFieldsSubsetFilters<{ testField: EntryFieldTypes.Array }, 'fields'> 123 | >({}) 124 | expectType< 125 | Required< 126 | EntryFieldsSubsetFilters< 127 | { testField?: EntryFieldTypes.Array }, 128 | 'fields' 129 | > 130 | > 131 | >({ 132 | 'fields.testField[in]': mocks.stringArrayValue, 133 | 'fields.testField[nin]': mocks.stringArrayValue, 134 | }) 135 | -------------------------------------------------------------------------------- /test/integration/getAssets.test.ts: -------------------------------------------------------------------------------- 1 | import * as contentful from '../../lib/contentful' 2 | import { ValidationError } from '../../lib/utils/validation-error' 3 | import { 4 | assetMappingsCollection, 5 | localisedAssetMappingsCollection, 6 | params, 7 | previewParamsWithCSM, 8 | testEncodingDecoding, 9 | } from './utils' 10 | 11 | if (process.env.API_INTEGRATION_TESTS) { 12 | params.host = '127.0.0.1:5000' 13 | params.insecure = true 14 | } 15 | 16 | const client = contentful.createClient(params) 17 | const invalidClient = contentful.createClient({ 18 | ...params, 19 | includeContentSourceMaps: true, 20 | }) 21 | const previewClient = contentful.createClient(previewParamsWithCSM) 22 | 23 | describe('getAssets', () => { 24 | test('default client', async () => { 25 | const response = await client.getAssets() 26 | 27 | expect(response.items).not.toHaveLength(0) 28 | 29 | response.items.forEach((item) => { 30 | expect(item.sys.type).toEqual('Asset') 31 | expect(item.fields).toBeDefined() 32 | expect(typeof item.fields.title).toBe('string') 33 | }) 34 | }) 35 | 36 | test('client has withAllLocales modifier', async () => { 37 | const response = await client.withAllLocales.getAssets() 38 | 39 | expect(response.items).not.toHaveLength(0) 40 | 41 | response.items.forEach((item) => { 42 | expect(item.sys.type).toEqual('Asset') 43 | expect(item.fields).toBeDefined() 44 | expect(typeof item.fields.title).toBe('object') 45 | }) 46 | }) 47 | 48 | describe('has includeContentSourceMaps enabled', () => { 49 | test('cdn client', async () => { 50 | await expect(invalidClient.getAssets()).rejects.toThrow( 51 | `The 'includeContentSourceMaps' parameter can only be used with the CPA. Please set host to 'preview.contentful.com' or 'preview.eu.contentful.com' to include Content Source Maps.`, 52 | ) 53 | await expect(invalidClient.getAssets()).rejects.toThrow(ValidationError) 54 | }) 55 | 56 | describe('preview client', () => { 57 | it('requests content source maps', async () => { 58 | const response = await previewClient.getAssets() 59 | 60 | expect(response.items).not.toHaveLength(0) 61 | 62 | response.items.forEach((item) => { 63 | expect(item.sys.type).toEqual('Asset') 64 | expect(item.fields).toBeDefined() 65 | expect(typeof item.fields.title).toBe('string') 66 | }) 67 | 68 | expect(response.sys?.contentSourceMapsLookup).toBeDefined() 69 | testEncodingDecoding(response, assetMappingsCollection) 70 | }) 71 | 72 | it('enforces selection of sys if query.select is present', async () => { 73 | const response = await previewClient.getAssets({ 74 | select: ['fields.title', 'sys.id', 'sys.type'], 75 | }) 76 | 77 | expect(response.items).not.toHaveLength(0) 78 | 79 | response.items.forEach((item) => { 80 | expect(item.sys.type).toEqual('Asset') 81 | expect(item.fields).toBeDefined() 82 | expect(typeof item.fields.title).toBe('string') 83 | expect(item.sys.contentSourceMaps).toBeDefined() 84 | }) 85 | 86 | expect(response.sys?.contentSourceMapsLookup).toBeDefined() 87 | testEncodingDecoding(response, assetMappingsCollection) 88 | }) 89 | 90 | it('works with withAllLocales modifier', async () => { 91 | const response = await previewClient.withAllLocales.getAssets() 92 | 93 | expect(response.items).not.toHaveLength(0) 94 | 95 | response.items.forEach((item) => { 96 | expect(item.sys.type).toEqual('Asset') 97 | expect(item.fields).toBeDefined() 98 | expect(typeof item.fields.title).toBe('object') 99 | }) 100 | 101 | expect(response.sys?.contentSourceMapsLookup).toBeDefined() 102 | testEncodingDecoding(response, localisedAssetMappingsCollection) 103 | }) 104 | }) 105 | }) 106 | }) 107 | -------------------------------------------------------------------------------- /lib/types/content-type.ts: -------------------------------------------------------------------------------- 1 | import type { ContentfulCollection } from './collection.js' 2 | import type { EntryFields } from './entry.js' 3 | import type { SpaceLink, EnvironmentLink } from './link.js' 4 | import type { BaseSys } from './sys.js' 5 | import type { BLOCKS, INLINES } from '@contentful/rich-text-types' 6 | 7 | /** 8 | * System managed metadata for content type 9 | * @category ContentType 10 | * @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/introduction/common-resource-attributes | CDA documentation on common attributes} 11 | */ 12 | export interface ContentTypeSys extends BaseSys { 13 | createdAt: EntryFields.Date 14 | updatedAt: EntryFields.Date 15 | revision: number 16 | space: { sys: SpaceLink } 17 | environment: { sys: EnvironmentLink } 18 | } 19 | 20 | /** 21 | * Definition of a content type field 22 | * @category ContentType 23 | * @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/content-types/content-type | Documentation} 24 | */ 25 | export interface ContentTypeField { 26 | disabled: boolean 27 | id: string 28 | linkType?: string 29 | localized: boolean 30 | name: string 31 | omitted: boolean 32 | required: boolean 33 | type: ContentTypeFieldType 34 | validations: ContentTypeFieldValidation[] 35 | items?: FieldItem 36 | allowedResources?: ContentTypeAllowedResources[] 37 | } 38 | 39 | export interface ContentTypeAllowedResources { 40 | type: string 41 | source: string 42 | contentTypes: string[] 43 | } 44 | 45 | /** 46 | * @category ContentType 47 | */ 48 | export type ContentTypeFieldType = 49 | | 'Symbol' 50 | | 'Text' 51 | | 'Integer' 52 | | 'Number' 53 | | 'Date' 54 | | 'Boolean' 55 | | 'Location' 56 | | 'Link' 57 | | 'Array' 58 | | 'Object' 59 | | 'RichText' 60 | | 'ResourceLink' 61 | 62 | /** 63 | * Definition of a single validation rule applied 64 | * to the related content type field 65 | * @category ContentType 66 | */ 67 | export interface ContentTypeFieldValidation { 68 | unique?: boolean 69 | size?: { 70 | min?: number 71 | max?: number 72 | } 73 | regexp?: { 74 | pattern: string 75 | } 76 | linkMimetypeGroup?: string[] 77 | in?: string[] 78 | linkContentType?: string[] 79 | message?: string | null 80 | nodes?: { 81 | [BLOCKS.EMBEDDED_ENTRY]?: Pick< 82 | ContentTypeFieldValidation, 83 | 'size' | 'linkContentType' | 'message' 84 | >[] 85 | [INLINES.EMBEDDED_ENTRY]?: Pick< 86 | ContentTypeFieldValidation, 87 | 'size' | 'linkContentType' | 'message' 88 | >[] 89 | [INLINES.ENTRY_HYPERLINK]?: Pick< 90 | ContentTypeFieldValidation, 91 | 'size' | 'linkContentType' | 'message' 92 | >[] 93 | [BLOCKS.EMBEDDED_ASSET]?: Pick[] 94 | [INLINES.ASSET_HYPERLINK]?: Pick[] 95 | [BLOCKS.EMBEDDED_RESOURCE]?: { 96 | validations: Pick[] 97 | allowedResources: ContentTypeAllowedResources[] 98 | } 99 | } 100 | enabledNodeTypes?: (`${BLOCKS}` | `${INLINES}`)[] 101 | } 102 | 103 | /** 104 | * Definition of an item belonging to the content type field 105 | * @category ContentType 106 | */ 107 | export interface FieldItem { 108 | type: 'Link' | 'Symbol' | 'ResourceLink' 109 | validations: ContentTypeFieldValidation[] 110 | linkType?: 'Entry' | 'Asset' 111 | } 112 | 113 | /** 114 | * Definition of a content type 115 | * @category ContentType 116 | * @see {@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/content-types | Documentation} 117 | */ 118 | export interface ContentType { 119 | sys: ContentTypeSys 120 | name: string 121 | description: string 122 | displayField: string 123 | fields: Array 124 | } 125 | 126 | /** 127 | * Collection of content types 128 | * @category ContentType 129 | */ 130 | export type ContentTypeCollection = ContentfulCollection 131 | -------------------------------------------------------------------------------- /test/types/asset-d.ts: -------------------------------------------------------------------------------- 1 | // As tsd does not pick up the global.d.ts located in /lib we 2 | // explicitly reference it here once. 3 | // eslint-disable-next-line @typescript-eslint/triple-slash-reference 4 | /// 5 | import { expectAssignable, expectNotAssignable } from 'tsd' 6 | 7 | import { 8 | Asset, 9 | AssetCollection, 10 | AssetDetails, 11 | AssetFields, 12 | AssetFile, 13 | ChainModifiers, 14 | } from '../../lib' 15 | 16 | // @ts-ignore 17 | import * as mocks from './mocks' 18 | 19 | type AssetLocales = 'US' | 'DE' 20 | 21 | const assetCollection = { 22 | total: mocks.numberValue, 23 | skip: mocks.numberValue, 24 | limit: mocks.numberValue, 25 | items: [mocks.asset], 26 | } 27 | 28 | const assetCollectionWithAllLocales = { 29 | total: mocks.numberValue, 30 | skip: mocks.numberValue, 31 | limit: mocks.numberValue, 32 | items: [mocks.localizedAsset], 33 | } 34 | 35 | expectAssignable(mocks.assetDetails) 36 | expectAssignable(mocks.assetFile) 37 | expectAssignable(mocks.assetFields) 38 | 39 | expectAssignable>(mocks.asset) 40 | expectAssignable>(mocks.localizedAsset) 41 | 42 | expectAssignable>(mocks.asset) 43 | expectAssignable>(mocks.localizedAsset) 44 | 45 | expectAssignable>(assetCollection) 46 | expectAssignable>(assetCollectionWithAllLocales) 47 | 48 | expectAssignable>(assetCollection) 49 | expectAssignable>(assetCollectionWithAllLocales) 50 | 51 | expectNotAssignable>(mocks.asset) 52 | expectAssignable>(mocks.localizedAsset) 53 | 54 | expectNotAssignable>( 55 | mocks.asset, 56 | ) 57 | expectAssignable>( 58 | mocks.localizedAsset, 59 | ) 60 | 61 | expectNotAssignable>( 62 | mocks.asset, 63 | ) 64 | expectAssignable>( 65 | mocks.localizedAsset, 66 | ) 67 | 68 | expectNotAssignable>(assetCollection) 69 | expectAssignable>(assetCollectionWithAllLocales) 70 | 71 | expectNotAssignable>( 72 | assetCollection, 73 | ) 74 | expectAssignable>( 75 | assetCollectionWithAllLocales, 76 | ) 77 | 78 | expectNotAssignable< 79 | AssetCollection<'WITH_ALL_LOCALES' | 'WITHOUT_UNRESOLVABLE_LINKS', AssetLocales> 80 | >(assetCollection) 81 | expectAssignable>( 82 | assetCollectionWithAllLocales, 83 | ) 84 | 85 | expectAssignable>(mocks.asset) 86 | expectNotAssignable>(mocks.localizedAsset) 87 | 88 | expectAssignable>(mocks.asset) 89 | expectNotAssignable>(mocks.localizedAsset) 90 | 91 | expectAssignable>(mocks.asset) 92 | expectNotAssignable>(mocks.localizedAsset) 93 | 94 | expectAssignable>(assetCollection) 95 | expectNotAssignable>( 96 | assetCollectionWithAllLocales, 97 | ) 98 | 99 | expectAssignable>(assetCollection) 100 | expectNotAssignable>(assetCollectionWithAllLocales) 101 | 102 | expectAssignable>(assetCollection) 103 | expectNotAssignable>( 104 | assetCollectionWithAllLocales, 105 | ) 106 | -------------------------------------------------------------------------------- /test/types/mocks.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AssetDetails, 3 | AssetFields, 4 | AssetFile, 5 | AssetLink, 6 | AssetSys, 7 | BaseEntry, 8 | EntryFields, 9 | EntryFieldTypes, 10 | EntrySys, 11 | FieldsType, 12 | Link, 13 | BoundingBoxSearchFilterInput, 14 | BoundingCircleSearchFilterInput, 15 | ProximitySearchFilterInput, 16 | ResourceLink, 17 | } from '../../lib' 18 | 19 | export const anyValue = '' as any 20 | export const stringValue = '' 21 | export const stringArrayValue = [stringValue] 22 | export const numberValue = 123 23 | export const numberArrayValue = [numberValue] 24 | 25 | export const booleanValue = true as boolean 26 | export const booleanArrayValue = [booleanValue] 27 | export const dateValue: EntryFields.Date = '2018-05-03T09:18:16.329Z' 28 | export const dateArrayValue = [dateValue] 29 | export const locationValue = { lat: 55.01496234536782, lon: 38.75813066219786 } 30 | export const jsonValue = {} 31 | 32 | export const nearLocationValue: ProximitySearchFilterInput = [1, 0] 33 | export const withinCircleLocationValue: BoundingCircleSearchFilterInput = [1, 0, 2] 34 | export const withinBoxLocationValue: BoundingBoxSearchFilterInput = [1, 0, 2, 1] 35 | 36 | export const metadataValue = { tags: [] } 37 | export const entryLink: { sys: Link<'Entry'> } = { 38 | sys: { 39 | type: 'Link', 40 | linkType: 'Entry', 41 | id: stringValue, 42 | }, 43 | } 44 | export const entryResourceLink: { sys: ResourceLink } = { 45 | sys: { 46 | type: 'ResourceLink', 47 | linkType: 'Contentful:Entry', 48 | urn: stringValue, 49 | }, 50 | } 51 | export const externalResourceLink: { sys: ResourceLink } = { 52 | sys: { 53 | type: 'ResourceLink', 54 | linkType: 'Provider1:ResourceTypeA', 55 | urn: stringValue, 56 | }, 57 | } 58 | 59 | export const entrySys: EntrySys = { 60 | contentType: { sys: { id: stringValue, type: 'Link', linkType: 'ContentType' } }, 61 | environment: { sys: { id: stringValue, type: 'Link', linkType: 'Environment' } }, 62 | revision: numberValue, 63 | space: { sys: { id: stringValue, type: 'Link', linkType: 'Space' } }, 64 | type: 'Entry', 65 | updatedAt: dateValue, 66 | id: stringValue, 67 | createdAt: dateValue, 68 | publishedVersion: numberValue, 69 | } 70 | 71 | export const entryBasics = { 72 | sys: entrySys, 73 | metadata: metadataValue, 74 | } 75 | 76 | export type SimpleEntrySkeleton = { 77 | fields: { 78 | title: EntryFieldTypes.Symbol 79 | } 80 | contentTypeId: string 81 | } 82 | 83 | export type LocalizedEntryFields = { 84 | title: { US?: string; DE?: string } 85 | } 86 | 87 | export const entry = { 88 | ...entryBasics, 89 | fields: { 90 | title: 'title', 91 | }, 92 | } 93 | 94 | export const localizedEntry = { 95 | ...entryBasics, 96 | fields: { 97 | title: { US: 'title', DE: 'titel' }, 98 | }, 99 | } 100 | 101 | export const getEntry = ( 102 | fields: Fields, 103 | ): BaseEntry & { fields: Fields } => ({ ...entryBasics, fields }) 104 | 105 | export const assetLink: { sys: AssetLink } = { 106 | sys: { 107 | type: 'Link', 108 | linkType: 'Asset', 109 | id: stringValue, 110 | }, 111 | } 112 | 113 | export const assetSys: AssetSys = { 114 | environment: { sys: { id: stringValue, type: 'Link', linkType: 'Environment' } }, 115 | revision: numberValue, 116 | space: { sys: { id: stringValue, type: 'Link', linkType: 'Space' } }, 117 | type: 'Asset', 118 | updatedAt: dateValue, 119 | id: stringValue, 120 | createdAt: dateValue, 121 | publishedVersion: numberValue, 122 | } 123 | 124 | export const assetBasics = { 125 | sys: assetSys, 126 | metadata: metadataValue, 127 | } 128 | 129 | export const assetDetails: AssetDetails = { 130 | size: numberValue, 131 | image: { 132 | width: numberValue, 133 | height: numberValue, 134 | }, 135 | } 136 | 137 | export const assetFile: AssetFile = { 138 | url: stringValue, 139 | details: assetDetails, 140 | fileName: stringValue, 141 | contentType: stringValue, 142 | } 143 | 144 | export const assetFields: AssetFields = { 145 | title: stringValue, 146 | description: stringValue, 147 | file: assetFile, 148 | } 149 | 150 | export const localizedAssetFields = { 151 | title: { US: stringValue, DE: stringValue }, 152 | description: { US: stringValue, DE: stringValue }, 153 | file: { US: assetFile, DE: assetFile }, 154 | } 155 | 156 | export const asset = { ...assetBasics, fields: assetFields } 157 | 158 | export const localizedAsset = { ...assetBasics, fields: localizedAssetFields } 159 | -------------------------------------------------------------------------------- /test/output-integration/node/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "contentful-js-node-demo", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "contentful-js-node-demo", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "contentful": "file:../../.." 13 | } 14 | }, 15 | "../../..": { 16 | "name": "contentful", 17 | "version": "0.0.0-determined-by-semantic-release", 18 | "license": "MIT", 19 | "dependencies": { 20 | "@contentful/rich-text-types": "^16.0.2", 21 | "axios": "^0.26.0", 22 | "contentful-resolve-response": "^1.3.6", 23 | "contentful-sdk-core": "^7.0.2", 24 | "json-stringify-safe": "^5.0.1", 25 | "type-fest": "^3.1.0" 26 | }, 27 | "devDependencies": { 28 | "@contentful/browserslist-config": "^3.0.0", 29 | "@semantic-release/changelog": "^6.0.1", 30 | "@types/jest": "^27.0.2", 31 | "@types/json-stringify-safe": "^5.0.0", 32 | "@typescript-eslint/eslint-plugin": "^4.9.1", 33 | "@typescript-eslint/parser": "^4.9.1", 34 | "bundlesize": "^0.18.1", 35 | "contentful-sdk-jsdoc": "^3.0.0", 36 | "core-js": "^3.2.1", 37 | "cz-conventional-changelog": "^3.3.0", 38 | "es-check": "^7.0.0", 39 | "eslint": "^7.2.0", 40 | "eslint-config-prettier": "^8.5.0", 41 | "eslint-config-standard": "^16.0.0", 42 | "eslint-plugin-import": "^2.9.0", 43 | "eslint-plugin-node": "^11.1.0", 44 | "eslint-plugin-promise": "^5.2.0", 45 | "eslint-plugin-standard": "^5.0.0", 46 | "express": "^4.15.4", 47 | "fast-copy": "^2.1.7", 48 | "husky": "^8.0.1", 49 | "in-publish": "^2.0.0", 50 | "istanbul": "^1.0.0-alpha.2", 51 | "jest": "^27.5.1", 52 | "jest-when": "^3.0.1", 53 | "json": "^11.0.0", 54 | "mkdirp": "^2.1.3", 55 | "nodemon": "^2.0.2", 56 | "prettier": "^2.2.1", 57 | "rimraf": "^4.1.1", 58 | "semantic-release": "^19.0.2", 59 | "ts-jest": "^27.1.3", 60 | "ts-loader": "^9.4.1", 61 | "tsd": "^0.28.0", 62 | "tslib": "^2.0.3", 63 | "typedoc": "^0.23.28", 64 | "typescript": "^4.8.4", 65 | "webpack": "^5.67.0", 66 | "webpack-cli": "^5.0.0" 67 | }, 68 | "engines": { 69 | "node": ">=12" 70 | } 71 | }, 72 | "node_modules/contentful": { 73 | "resolved": "../../..", 74 | "link": true 75 | } 76 | }, 77 | "dependencies": { 78 | "contentful": { 79 | "version": "file:../../..", 80 | "requires": { 81 | "@contentful/browserslist-config": "^3.0.0", 82 | "@contentful/rich-text-types": "^16.0.2", 83 | "@semantic-release/changelog": "^6.0.1", 84 | "@types/jest": "^27.0.2", 85 | "@types/json-stringify-safe": "^5.0.0", 86 | "@typescript-eslint/eslint-plugin": "^4.9.1", 87 | "@typescript-eslint/parser": "^4.9.1", 88 | "axios": "^0.26.0", 89 | "bundlesize": "^0.18.1", 90 | "contentful-resolve-response": "^1.3.6", 91 | "contentful-sdk-core": "^7.0.2", 92 | "contentful-sdk-jsdoc": "^3.0.0", 93 | "core-js": "^3.2.1", 94 | "cz-conventional-changelog": "^3.3.0", 95 | "es-check": "^7.0.0", 96 | "eslint": "^7.2.0", 97 | "eslint-config-prettier": "^8.5.0", 98 | "eslint-config-standard": "^16.0.0", 99 | "eslint-plugin-import": "^2.9.0", 100 | "eslint-plugin-node": "^11.1.0", 101 | "eslint-plugin-promise": "^5.2.0", 102 | "eslint-plugin-standard": "^5.0.0", 103 | "express": "^4.15.4", 104 | "fast-copy": "^2.1.7", 105 | "husky": "^8.0.1", 106 | "in-publish": "^2.0.0", 107 | "istanbul": "^1.0.0-alpha.2", 108 | "jest": "^27.5.1", 109 | "jest-when": "^3.0.1", 110 | "json": "^11.0.0", 111 | "json-stringify-safe": "^5.0.1", 112 | "mkdirp": "^2.1.3", 113 | "nodemon": "^2.0.2", 114 | "prettier": "^2.2.1", 115 | "rimraf": "^4.1.1", 116 | "semantic-release": "^19.0.2", 117 | "ts-jest": "^27.1.3", 118 | "ts-loader": "^9.4.1", 119 | "tsd": "^0.28.0", 120 | "tslib": "^2.0.3", 121 | "type-fest": "^3.1.0", 122 | "typedoc": "^0.23.28", 123 | "typescript": "^4.8.4", 124 | "webpack": "^5.67.0", 125 | "webpack-cli": "^5.0.0" 126 | } 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /.github/workflows/failure-notification.yaml: -------------------------------------------------------------------------------- 1 | name: Create Issue on Workflow Failure 2 | 3 | permissions: 4 | contents: read 5 | issues: write 6 | 7 | on: 8 | workflow_call: 9 | inputs: 10 | workflow_name: 11 | description: 'Name of the failed workflow' 12 | required: true 13 | type: string 14 | job_name: 15 | description: 'Name of the failed job(s)' 16 | required: false 17 | type: string 18 | default: 'Unknown' 19 | failure_reason: 20 | description: 'Reason for the failure(s)' 21 | required: false 22 | type: string 23 | default: 'Unknown failure reason' 24 | 25 | jobs: 26 | create-failure-issue: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - name: Checkout code 30 | uses: actions/checkout@v5 31 | 32 | - name: Create Issue 33 | uses: actions/github-script@v7 34 | with: 35 | script: | 36 | const workflowName = '${{ inputs.workflow_name }}'; 37 | const jobName = '${{ inputs.job_name }}'; 38 | const failureReason = '${{ inputs.failure_reason }}'; 39 | const runUrl = `${context.payload.repository.html_url}/actions/runs/${context.runId}`; 40 | const commitSha = context.sha; 41 | const commitUrl = `${context.payload.repository.html_url}/commit/${commitSha}`; 42 | const branch = context.ref.replace('refs/heads/', ''); 43 | const actor = context.actor; 44 | 45 | // Check if there's already an open issue for this workflow 46 | const existingIssues = await github.rest.issues.listForRepo({ 47 | owner: context.repo.owner, 48 | repo: context.repo.repo, 49 | state: 'open', 50 | labels: 'workflow-failure,' + workflowName.toLowerCase().replace(/\s+/g, '-') 51 | }); 52 | 53 | const title = `🚨 Workflow Failure: ${workflowName}`; 54 | const body = `## Workflow Failure Report 55 | 56 | **Workflow:** ${workflowName} 57 | **Job:** ${jobName} 58 | **Branch:** ${branch} 59 | **Commit:** [${commitSha.substring(0, 7)}](${commitUrl}) 60 | **Triggered by:** @${actor} 61 | **Run URL:** [View Failed Run](${runUrl}) 62 | 63 | ### Failure Details 64 | ${failureReason} 65 | 66 | ### Debugging Information 67 | - **Timestamp:** ${new Date().toISOString()} 68 | - **Repository:** ${context.payload.repository.full_name} 69 | - **Event:** ${context.eventName} 70 | 71 | ### Next Steps 72 | 1. Check the [workflow run logs](${runUrl}) for detailed error information 73 | 2. Review the changes in [commit ${commitSha.substring(0, 7)}](${commitUrl}) 74 | 3. Fix the issue and re-run the workflow 75 | 4. Close this issue once resolved 76 | 77 | --- 78 | *This issue was automatically created by the failure notification workflow.*`; 79 | 80 | // If no existing open issue, create a new one 81 | if (existingIssues.data.length === 0) { 82 | await github.rest.issues.create({ 83 | owner: context.repo.owner, 84 | repo: context.repo.repo, 85 | title: title, 86 | body: body, 87 | labels: [ 88 | 'workflow-failure', 89 | 'bug', 90 | workflowName.toLowerCase().replace(/\s+/g, '-'), 91 | 'automated' 92 | ] 93 | }); 94 | console.log(`Created new issue for ${workflowName} failure`); 95 | } else { 96 | console.log(`Issue already exists for ${workflowName} failure`); 97 | // Optionally add a comment to the existing issue 98 | await github.rest.issues.createComment({ 99 | owner: context.repo.owner, 100 | repo: context.repo.repo, 101 | issue_number: existingIssues.data[0].number, 102 | body: `## Additional Failure Report 103 | 104 | **New failure detected:** 105 | - **Job:** ${jobName} 106 | - **Commit:** [${commitSha.substring(0, 7)}](${commitUrl}) 107 | - **Run URL:** [View Failed Run](${runUrl}) 108 | - **Timestamp:** ${new Date().toISOString()} 109 | 110 | ${failureReason}` 111 | }); 112 | console.log(`Added comment to existing issue for ${workflowName} failure`); 113 | } -------------------------------------------------------------------------------- /test/types/client/parseEntries.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectType } from 'tsd' 2 | import { 3 | createClient, 4 | EntryCollection, 5 | EntrySys, 6 | EntrySkeletonType, 7 | EntryFieldTypes, 8 | } from '../../../lib' 9 | 10 | const client = createClient({ 11 | accessToken: 'accessToken', 12 | space: 'spaceId', 13 | }) 14 | 15 | type Fields = { 16 | title: EntryFieldTypes.Symbol 17 | link: EntryFieldTypes.EntryLink 18 | moreLinks: EntryFieldTypes.Array> 19 | } 20 | 21 | type EntrySkeleton = EntrySkeletonType 22 | 23 | const data: EntryCollection = { 24 | total: 10, 25 | skip: 0, 26 | limit: 1, 27 | items: [ 28 | { 29 | sys: { 30 | id: '0', 31 | } as EntrySys, 32 | metadata: { 33 | tags: [], 34 | }, 35 | fields: { 36 | title: 'title', 37 | link: { 38 | sys: { 39 | type: 'Link', 40 | linkType: 'Entry', 41 | id: '1', 42 | }, 43 | }, 44 | moreLinks: [ 45 | { 46 | sys: { 47 | type: 'Link', 48 | linkType: 'Entry', 49 | id: '2', 50 | }, 51 | }, 52 | ], 53 | }, 54 | }, 55 | ], 56 | } 57 | 58 | type Locales = 'en' | 'de' 59 | 60 | const dataWithAllLocales: EntryCollection< 61 | EntrySkeleton, 62 | 'WITHOUT_LINK_RESOLUTION' | 'WITH_ALL_LOCALES', 63 | Locales 64 | > = { 65 | total: 10, 66 | skip: 0, 67 | limit: 1, 68 | items: [ 69 | { 70 | sys: { 71 | id: '0', 72 | } as EntrySys, 73 | metadata: { 74 | tags: [], 75 | }, 76 | fields: { 77 | title: { 78 | en: 'title', 79 | de: 'titel', 80 | }, 81 | link: { 82 | en: { 83 | sys: { 84 | type: 'Link', 85 | linkType: 'Entry', 86 | id: '1', 87 | }, 88 | }, 89 | }, 90 | moreLinks: { 91 | en: [ 92 | { 93 | sys: { 94 | type: 'Link', 95 | linkType: 'Entry', 96 | id: '2', 97 | }, 98 | }, 99 | ], 100 | }, 101 | }, 102 | }, 103 | ], 104 | } 105 | 106 | expectType>(client.parseEntries(data)) 107 | expectType>(client.parseEntries(data)) 108 | 109 | expectType>( 110 | client.withoutUnresolvableLinks.parseEntries(data), 111 | ) 112 | expectType>( 113 | client.withoutUnresolvableLinks.parseEntries(data), 114 | ) 115 | 116 | expectType>( 117 | client.withoutLinkResolution.parseEntries(data), 118 | ) 119 | expectType>( 120 | client.withoutLinkResolution.parseEntries(data), 121 | ) 122 | 123 | expectType>( 124 | client.withAllLocales.parseEntries(dataWithAllLocales), 125 | ) 126 | expectType>( 127 | client.withAllLocales.parseEntries(dataWithAllLocales), 128 | ) 129 | expectType>( 130 | client.withAllLocales.parseEntries(dataWithAllLocales), 131 | ) 132 | 133 | expectType< 134 | EntryCollection 135 | >(client.withAllLocales.withoutUnresolvableLinks.parseEntries(dataWithAllLocales)) 136 | expectType>( 137 | client.withAllLocales.withoutUnresolvableLinks.parseEntries(dataWithAllLocales), 138 | ) 139 | expectType< 140 | EntryCollection 141 | >( 142 | client.withAllLocales.withoutUnresolvableLinks.parseEntries( 143 | dataWithAllLocales, 144 | ), 145 | ) 146 | 147 | expectType>( 148 | client.withAllLocales.withoutLinkResolution.parseEntries(dataWithAllLocales), 149 | ) 150 | expectType>( 151 | client.withAllLocales.withoutLinkResolution.parseEntries(dataWithAllLocales), 152 | ) 153 | expectType>( 154 | client.withAllLocales.withoutLinkResolution.parseEntries( 155 | dataWithAllLocales, 156 | ), 157 | ) 158 | -------------------------------------------------------------------------------- /test/unit/contentful.test.ts: -------------------------------------------------------------------------------- 1 | import { vi, test, expect, describe, MockedFunction, beforeEach, afterEach } from 'vitest' 2 | import { createClient } from '../../lib/contentful' 3 | import { createHttpClient } from 'contentful-sdk-core' 4 | import * as CreateContentfulApi from '../../lib/create-contentful-api' 5 | 6 | import pkg from '../../package.json' with { type: 'json' } 7 | 8 | vi.mock('../../lib/create-contentful-api') 9 | vi.mock('contentful-sdk-core', async (importOriginal) => { 10 | const mod: object = await importOriginal() 11 | 12 | return { ...mod, createHttpClient: vi.fn() } 13 | }) 14 | 15 | const createHttpClientMock = >(createHttpClient) 16 | const createContentfulApiMock = >( 17 | (CreateContentfulApi.default) 18 | ) 19 | 20 | describe('contentful', () => { 21 | beforeEach(() => { 22 | createHttpClientMock.mockReturnValue({ 23 | // @ts-ignore 24 | defaults: { baseURL: 'http://some-base-url.com/' }, 25 | interceptors: { 26 | // @ts-ignore 27 | response: { use: vi.fn() }, 28 | }, 29 | }) 30 | }) 31 | 32 | afterEach(() => { 33 | createHttpClientMock.mockReset() 34 | createContentfulApiMock.mockReset() 35 | }) 36 | 37 | test('Throws if no accessToken is defined', () => { 38 | // @ts-ignore 39 | expect(() => createClient({ space: 'spaceId' })).toThrow(/Expected parameter accessToken/) 40 | }) 41 | 42 | test('Throws if no space is defined', () => { 43 | // @ts-ignore 44 | expect(() => createClient({ accessToken: 'accessToken' })).toThrow(/Expected parameter space/) 45 | }) 46 | 47 | test('Generate the correct User Agent Header', () => { 48 | createClient({ 49 | accessToken: 'accessToken', 50 | space: 'spaceId', 51 | application: 'myApplication/1.1.1', 52 | integration: 'myIntegration/1.0.0', 53 | }) 54 | 55 | expect(createHttpClientMock).toHaveBeenCalledTimes(1) 56 | 57 | const callConfig = createHttpClientMock.mock.calls[0][1] 58 | if (!callConfig.headers) { 59 | throw new Error('httpClient was created without headers') 60 | } 61 | expect(callConfig.headers['Content-Type']).toBeDefined() 62 | expect(callConfig.headers['X-Contentful-User-Agent']).toBeDefined() 63 | 64 | const headerParts = callConfig.headers['X-Contentful-User-Agent'].split('; ') 65 | expect(headerParts).toHaveLength(5) 66 | expect(headerParts[0]).toEqual('app myApplication/1.1.1') 67 | expect(headerParts[1]).toEqual('integration myIntegration/1.0.0') 68 | expect(headerParts[2]).toEqual(`sdk contentful.js/${pkg.version}`) 69 | }) 70 | 71 | test('Passes along HTTP client parameters', () => { 72 | createClient({ accessToken: 'accessToken', space: 'spaceId' }) 73 | const callConfig = createHttpClientMock.mock.calls[0][1] 74 | if (!callConfig.headers) { 75 | throw new Error('httpClient was created without headers') 76 | } 77 | expect(callConfig.headers['Content-Type']).toBeDefined() 78 | expect(callConfig.headers['X-Contentful-User-Agent']).toBeDefined() 79 | }) 80 | 81 | // So what? 82 | test.skip('Returns a client instance', () => { 83 | const client = createClient({ accessToken: 'accessToken', space: 'spaceId' }) 84 | 85 | expect(client.getSpace).toBeDefined() 86 | expect(client.getEntry).toBeDefined() 87 | expect(client.getEntries).toBeDefined() 88 | expect(client.getContentType).toBeDefined() 89 | expect(client.getContentTypes).toBeDefined() 90 | expect(client.getAsset).toBeDefined() 91 | expect(client.getAssets).toBeDefined() 92 | }) 93 | 94 | test('Initializes API and attaches default environment', () => { 95 | createClient({ accessToken: 'accessToken', space: 'spaceId' }) 96 | const callConfig = createContentfulApiMock.mock.calls[0] 97 | expect(callConfig[0].http.defaults.baseURL).toEqual( 98 | 'http://some-base-url.com/environments/master', 99 | ) 100 | }) 101 | 102 | test('Initializes API and attaches custom environment', () => { 103 | createClient({ accessToken: 'accessToken', space: 'spaceId', environment: 'stage' }) 104 | const callConfig = createContentfulApiMock.mock.calls[0] 105 | expect(callConfig[0].http.defaults.baseURL).toEqual( 106 | 'http://some-base-url.com/environments/stage', 107 | ) 108 | }) 109 | 110 | test('Initializes API with includeContentSourceMaps option', () => { 111 | createClient({ accessToken: 'accessToken', space: 'spaceId', includeContentSourceMaps: true }) 112 | const callConfig = createHttpClientMock.mock.calls[0] 113 | 114 | expect(callConfig[1].includeContentSourceMaps).toBe(true) 115 | }) 116 | 117 | test('Initializes API with timelinePreview option', () => { 118 | createClient({ 119 | accessToken: 'accessToken', 120 | space: 'spaceId', 121 | timelinePreview: { release: { lte: 'black-friday' } }, 122 | }) 123 | 124 | const callConfig = createHttpClientMock.mock.calls[0] 125 | 126 | expect(callConfig[1].timelinePreview).toStrictEqual({ release: { lte: 'black-friday' } }) 127 | }) 128 | }) 129 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import { dirname, resolve } from 'path' 2 | import { fileURLToPath } from 'url' 3 | 4 | import pkg from './package.json' with { type: 'json' } 5 | 6 | import nodeResolve from '@rollup/plugin-node-resolve' 7 | import commonjs from '@rollup/plugin-commonjs' 8 | import json from '@rollup/plugin-json' 9 | import alias from '@rollup/plugin-alias' 10 | import terser from '@rollup/plugin-terser' 11 | import replace from '@rollup/plugin-replace' 12 | import { optimizeLodashImports } from '@optimize-lodash/rollup-plugin' 13 | import { visualizer } from 'rollup-plugin-visualizer' 14 | import { babel } from '@rollup/plugin-babel' 15 | 16 | const __dirname = dirname(fileURLToPath(import.meta.url)) 17 | 18 | const baseConfig = { 19 | input: 'dist/esm-raw/index.js', 20 | output: { 21 | file: 'dist/contentful.cjs', 22 | format: 'cjs', 23 | }, 24 | plugins: [ 25 | optimizeLodashImports(), 26 | replace({ 27 | preventAssignment: true, 28 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), 29 | __VERSION__: JSON.stringify(pkg.version), 30 | }), 31 | commonjs({ 32 | sourceMap: false, 33 | transformMixedEsModules: true, 34 | ignoreGlobal: true, 35 | ignoreDynamicRequires: true, 36 | requireReturnsDefault: 'auto', 37 | }), 38 | json(), 39 | ], 40 | } 41 | 42 | const esmConfig = { 43 | input: 'dist/esm-raw/index.js', 44 | output: { 45 | dir: 'dist/esm', // Output directory 46 | format: 'esm', // Output module format 47 | preserveModules: true, // Preserve module structure 48 | }, 49 | plugins: [ 50 | replace({ 51 | preventAssignment: true, 52 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), 53 | __VERSION__: JSON.stringify(pkg.version), 54 | }), 55 | ], 56 | } 57 | 58 | const cjsConfig = { 59 | ...baseConfig, 60 | output: { 61 | ...baseConfig.output, 62 | }, 63 | plugins: [ 64 | ...baseConfig.plugins, 65 | nodeResolve({ 66 | preferBuiltins: true, 67 | browser: false, 68 | }), 69 | babel({ 70 | babelHelpers: 'bundled', 71 | // exclude: 'node_modules/**', 72 | presets: [ 73 | [ 74 | '@babel/preset-env', 75 | // Please note: This is set to Node.js v8 in order to satisfy ECMA2017 requirements 76 | // However, we cannot ensure it will operate without issues on Node.js v8 77 | { targets: { node: 8 }, modules: false, bugfixes: true }, 78 | ], 79 | ], 80 | }), 81 | alias({ 82 | entries: [ 83 | { 84 | find: 'axios', 85 | replacement: resolve(__dirname, './node_modules/axios/dist/node/axios.cjs'), 86 | }, 87 | ], 88 | }), 89 | ], 90 | } 91 | 92 | const browserConfig = { 93 | ...baseConfig, 94 | output: { 95 | file: 'dist/contentful.browser.js', 96 | format: 'iife', 97 | name: 'contentful', 98 | }, 99 | plugins: [ 100 | nodeResolve({ 101 | preferBuiltins: false, 102 | browser: true, 103 | }), 104 | alias({ 105 | entries: [ 106 | { 107 | find: 'axios', 108 | replacement: resolve(__dirname, './node_modules/axios/dist/browser/axios.cjs'), 109 | }, 110 | { 111 | find: 'process', 112 | replacement: resolve(__dirname, 'node_modules', 'process/browser'), 113 | }, 114 | ], 115 | }), 116 | ...baseConfig.plugins, 117 | babel({ 118 | babelHelpers: 'runtime', 119 | presets: [ 120 | [ 121 | '@babel/preset-env', 122 | { 123 | targets: pkg.browserslist, 124 | modules: false, 125 | bugfixes: true, 126 | }, 127 | ], 128 | ], 129 | plugins: [ 130 | [ 131 | '@babel/plugin-transform-runtime', 132 | { 133 | regenerator: true, 134 | }, 135 | ], 136 | ], 137 | }), 138 | ], 139 | } 140 | 141 | const browserMinConfig = { 142 | ...browserConfig, 143 | output: { 144 | ...browserConfig.output, 145 | file: 'dist/contentful.browser.min.js', 146 | }, 147 | plugins: [ 148 | ...browserConfig.plugins, 149 | terser({ 150 | // sourceMap: { 151 | // content: 'inline', 152 | // includeSources: true, 153 | // url: 'inline' 154 | // }, 155 | compress: { 156 | passes: 5, 157 | ecma: 2018, 158 | drop_console: true, 159 | drop_debugger: true, 160 | sequences: true, 161 | booleans: true, 162 | loops: true, 163 | unused: true, 164 | evaluate: true, 165 | if_return: true, 166 | join_vars: true, 167 | collapse_vars: true, 168 | reduce_vars: true, 169 | pure_getters: true, 170 | pure_new: true, 171 | keep_classnames: false, 172 | keep_fnames: false, 173 | keep_fargs: false, 174 | keep_infinity: false, 175 | }, 176 | format: { 177 | comments: false, // Remove all comments 178 | beautify: false, // Minify output 179 | }, 180 | }), 181 | visualizer({ 182 | emitFile: true, 183 | filename: 'stats-browser-min.html', 184 | }), 185 | ], 186 | } 187 | 188 | export default [esmConfig, cjsConfig, browserConfig, browserMinConfig] 189 | -------------------------------------------------------------------------------- /test/types/query.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectAssignable } from 'tsd' 2 | import { EntriesQueries, EntryFieldTypes, EntrySkeletonType, EntryFieldsQueries } from '../../lib' 3 | 4 | // @ts-ignore 5 | import * as mocks from './mocks' 6 | 7 | expectAssignable>>({ 8 | select: ['fields.stringField'], 9 | 'fields.stringField[exists]': mocks.booleanValue, 10 | 'fields.stringField': mocks.stringValue, 11 | 'fields.stringField[ne]': mocks.stringValue, 12 | 'fields.stringField[in]': [mocks.stringValue], 13 | 'fields.stringField[nin]': [mocks.stringValue], 14 | 'fields.stringField[match]': mocks.stringValue, 15 | }) 16 | 17 | expectAssignable>>({ 18 | select: ['fields.numberField'], 19 | 'fields.numberField[exists]': mocks.booleanValue, 20 | 'fields.numberField': mocks.numberValue, 21 | 'fields.numberField[ne]': mocks.numberValue, 22 | 'fields.numberField[in]': [mocks.numberValue], 23 | 'fields.numberField[nin]': [mocks.numberValue], 24 | 'fields.numberField[lt]': mocks.numberValue, 25 | 'fields.numberField[lte]': mocks.numberValue, 26 | 'fields.numberField[gt]': mocks.numberValue, 27 | 'fields.numberField[gte]': mocks.numberValue, 28 | }) 29 | 30 | expectAssignable>>({ 31 | select: ['fields.integerField'], 32 | 'fields.integerField[exists]': mocks.booleanValue, 33 | 'fields.integerField': mocks.numberValue, 34 | 'fields.integerField[ne]': mocks.numberValue, 35 | 'fields.integerField[in]': [mocks.numberValue], 36 | 'fields.integerField[nin]': [mocks.numberValue], 37 | 'fields.integerField[lt]': mocks.numberValue, 38 | 'fields.integerField[lte]': mocks.numberValue, 39 | 'fields.integerField[gt]': mocks.numberValue, 40 | 'fields.integerField[gte]': mocks.numberValue, 41 | }) 42 | 43 | expectAssignable>>({ 44 | select: ['fields.symbolField'], 45 | 'fields.symbolField[exists]': mocks.booleanValue, 46 | 'fields.symbolField': mocks.stringValue, 47 | 'fields.symbolField[ne]': mocks.stringValue, 48 | 'fields.symbolField[in]': [mocks.stringValue], 49 | 'fields.symbolField[nin]': [mocks.stringValue], 50 | 'fields.symbolField[match]': mocks.stringValue, 51 | }) 52 | 53 | expectAssignable>>({ 54 | select: ['fields.dateField'], 55 | 'fields.dateField[exists]': mocks.booleanValue, 56 | 'fields.dateField': mocks.dateValue, 57 | 'fields.dateField[ne]': mocks.dateValue, 58 | 'fields.dateField[in]': [mocks.dateValue], 59 | 'fields.dateField[nin]': [mocks.dateValue], 60 | 'fields.dateField[match]': mocks.dateValue, // Date is a string type so Typescript will allow the match filter on it. 61 | 'fields.dateField[lt]': mocks.dateValue, 62 | 'fields.dateField[lte]': mocks.dateValue, 63 | 'fields.dateField[gt]': mocks.dateValue, 64 | 'fields.dateField[gte]': mocks.dateValue, 65 | }) 66 | 67 | expectAssignable>>({ 68 | select: ['fields.locationField'], 69 | 'fields.locationField[exists]': mocks.booleanValue, 70 | 'fields.locationField[near]': [34, 35], 71 | 'fields.locationField[within]': [34, 35, 37, 38], 72 | }) 73 | 74 | expectAssignable>>({ 75 | select: ['fields.objectField'], 76 | 'fields.objectField[exists]': mocks.booleanValue, 77 | }) 78 | 79 | expectAssignable>>({ 80 | select: ['fields.richTextField'], 81 | 'fields.richTextField[exists]': mocks.booleanValue, 82 | 'fields.richTextField[match]': mocks.stringValue, 83 | }) 84 | 85 | expectAssignable< 86 | Required }>> 87 | >({ 88 | select: ['fields.arrayStringField'], 89 | 'fields.arrayStringField[exists]': mocks.booleanValue, 90 | 'fields.arrayStringField': mocks.stringValue, 91 | 'fields.arrayStringField[ne]': mocks.stringValue, 92 | 'fields.arrayStringField[in]': [mocks.stringValue], 93 | 'fields.arrayStringField[nin]': [mocks.stringValue], 94 | 'fields.arrayStringField[match]': mocks.stringValue, 95 | }) 96 | 97 | expectAssignable< 98 | Required }>> 99 | >({ 100 | select: ['fields.referenceField'], 101 | 'fields.referenceField[exists]': mocks.booleanValue, 102 | 'fields.referenceField.sys.contentType.sys.id': mocks.stringValue, 103 | 'fields.referenceField.fields.numberField': mocks.numberValue, 104 | }) 105 | 106 | expectAssignable< 107 | EntriesQueries< 108 | EntrySkeletonType<{ 109 | stringField: EntryFieldTypes.Symbol 110 | numberField: EntryFieldTypes.Number 111 | }>, 112 | undefined 113 | > 114 | >({ 115 | content_type: 'id', 116 | 'fields.stringField[exists]': mocks.booleanValue, 117 | 'fields.stringField[match]': mocks.stringValue, 118 | 'fields.numberField[gte]': mocks.numberValue, 119 | select: ['fields.stringField', 'fields.numberField'], 120 | limit: mocks.numberValue, 121 | order: ['fields.stringField', '-fields.numberField'], 122 | links_to_asset: mocks.stringValue, 123 | links_to_entry: mocks.stringValue, 124 | }) 125 | --------------------------------------------------------------------------------