├── .nvmrc ├── .husky └── pre-commit ├── .npmrc ├── vitest.setup.browser.ts ├── types.d.ts ├── test ├── output-integration │ ├── browser │ │ ├── .gitignore │ │ ├── vitest.config.ts │ │ ├── README.md │ │ ├── public │ │ │ ├── index.html │ │ │ └── index.js │ │ ├── package.json │ │ ├── vitest.setup.ts │ │ ├── scripts │ │ │ └── inject-env.js │ │ └── test │ │ │ └── index.test.js │ └── node │ │ ├── README.md │ │ ├── vitest.config.ts │ │ ├── package.json │ │ └── index.test.ts ├── integration │ ├── fixtures │ │ ├── example.svg │ │ ├── build.zip │ │ └── shiba-stuck-bush.jpg │ ├── preview-api-key-integration.test.ts │ ├── space-integration.test.ts │ ├── space-team-integration.test.ts │ ├── space-user-integration.test.ts │ ├── user-integration.test.ts │ ├── space-member-integration.test.ts │ ├── organization-space-membership-integration.test.ts │ ├── environment-alias-integration.test.ts │ ├── upload-credential.test.ts │ ├── organization-invitation.test.ts │ ├── org-team-space-membership-integration.test.ts │ ├── app-upload-integration.test.ts │ ├── organization-membership-integration.test.ts │ └── team-integration.test.ts ├── unit │ ├── adapters │ │ └── REST │ │ │ ├── endpoints │ │ │ ├── extension.test.ts │ │ │ ├── utils.test.ts │ │ │ ├── organization-membership.test.ts │ │ │ ├── environment-template.test.ts │ │ │ ├── ui-config.test.ts │ │ │ └── user-ui-config.test.ts │ │ │ ├── helpers │ │ │ └── setupRestAdapter.ts │ │ │ ├── rest-adapter.test.ts │ │ │ └── reusable-tests │ │ │ └── update.ts │ ├── mocks │ │ ├── makeRequest.ts │ │ └── http.ts │ ├── entities │ │ ├── asset-key.test.ts │ │ ├── ui-config.test.ts │ │ ├── semantic-search.test.ts │ │ ├── user-ui-config.test.ts │ │ ├── app-access-token.test.ts │ │ ├── usage.test.ts │ │ ├── app-signed-request.test.ts │ │ ├── semantic-duplicates.test.ts │ │ ├── upload-credential.test.ts │ │ ├── vectorization-status.test.ts │ │ ├── ai-action-invocation.test.ts │ │ ├── invitation.test.ts │ │ ├── semantic-recommendations.test.ts │ │ ├── semantic-reference-suggestions.test.ts │ │ ├── bulk-action.test.ts │ │ ├── release-action.test.ts │ │ ├── entry.test.ts │ │ ├── user.test.ts │ │ ├── access-token.test.ts │ │ ├── space-member.test.ts │ │ ├── preview-api-key.test.ts │ │ ├── personal-access-token.test.ts │ │ ├── upload.test.ts │ │ ├── app-details.test.ts │ │ ├── app-bundle.test.ts │ │ ├── workflows-changelog-entry.test.ts │ │ ├── space.test.ts │ │ ├── resource-type.test.ts │ │ ├── app-signing-secret.test.ts │ │ ├── tag.test.ts │ │ ├── app-event-subscription.test.ts │ │ ├── organization.test.ts │ │ ├── release.test.ts │ │ ├── app-upload.test.ts │ │ ├── environment.test.ts │ │ ├── app-key.test.ts │ │ ├── extension.test.ts │ │ ├── environment-alias.test.ts │ │ ├── team.test.ts │ │ ├── role.test.ts │ │ ├── task.test.ts │ │ ├── locale.test.ts │ │ ├── api-key.test.ts │ │ ├── comment.test.ts │ │ ├── webhook.test.ts │ │ └── organization-membership.test.ts │ ├── create-adapter.test.ts │ ├── create-ui-config-api.test.ts │ └── create-user-ui-config-api.test.ts ├── utils.ts └── defaults.ts ├── vitest.setup.ts ├── .github ├── CODEOWNERS ├── workflows │ ├── dependabot-approve-and-request-merge.yaml │ ├── codeql.yaml │ ├── build.yaml │ ├── main.yaml │ ├── check.yaml │ ├── test-integration.yaml │ └── test-demo-projects.yaml ├── ISSUE_TEMPLATE │ ├── config.yml │ └── bug_report.md ├── dependabot.yml └── PULL_REQUEST_TEMPLATE.md ├── .prettierignore ├── images └── contentful-icon.png ├── .prettierrc ├── .contentful └── vault-secrets.yaml ├── lib ├── entities │ ├── utils.ts │ ├── field-type.ts │ ├── widget-parameters.ts │ ├── concept-scheme.ts │ ├── asset-key.ts │ ├── semantic-search.ts │ ├── space-member.ts │ ├── semantic-recommendations.ts │ ├── semantic-duplicates.ts │ ├── semantic-reference-suggestions.ts │ ├── organization-invitation.ts │ ├── concept.ts │ ├── preview-api-key.ts │ ├── vectorization-status.ts │ ├── app-access-token.ts │ ├── snapshot.ts │ ├── space.ts │ ├── user-ui-config.ts │ ├── resource.ts │ ├── app-signed-request.ts │ └── user.ts ├── methods │ ├── utils.ts │ └── release-action.ts ├── constants │ └── editor-interface-defaults │ │ ├── index.ts │ │ ├── types.ts │ │ └── editors-defaults.ts ├── create-adapter.ts ├── adapters │ └── REST │ │ ├── endpoints │ │ ├── utils.ts │ │ ├── ai-action-invocation.ts │ │ ├── app-signed-request.ts │ │ ├── upload-credentials.ts │ │ ├── semantic-search.ts │ │ ├── resource.ts │ │ ├── app-access-token.ts │ │ ├── semantic-duplicates.ts │ │ ├── space-member.ts │ │ ├── semantic-recommendations.ts │ │ ├── semantic-reference-suggestions.ts │ │ ├── workflows-changelog.ts │ │ ├── preview-api-key.ts │ │ ├── usage.ts │ │ ├── ui-config.ts │ │ ├── user-ui-config.ts │ │ ├── vectorization-status.ts │ │ ├── app-details.ts │ │ ├── resource-provider.ts │ │ ├── app-signing-secret.ts │ │ ├── app-event-subscription.ts │ │ ├── organization.ts │ │ ├── environment-template-installation.ts │ │ ├── function-log.ts │ │ ├── release-action.ts │ │ ├── app-key.ts │ │ ├── function.ts │ │ ├── app-upload.ts │ │ ├── organization-invitation.ts │ │ ├── http.ts │ │ └── user.ts │ │ ├── types.ts │ │ └── make-request.ts ├── plain │ ├── checks.ts │ ├── entities │ │ ├── semantic-search.ts │ │ ├── semantic-duplicates.ts │ │ ├── semantic-recommendations.ts │ │ ├── ai-action-invocation.ts │ │ ├── semantic-reference-suggestions.ts │ │ ├── upload-credential.ts │ │ ├── app-access-token.ts │ │ ├── space-member.ts │ │ ├── organization.ts │ │ ├── workflows-changelog.ts │ │ ├── vectorization-status.ts │ │ ├── app-signed-request.ts │ │ ├── resource.ts │ │ ├── ui-config.ts │ │ └── user-ui-config.ts │ ├── wrappers │ │ └── wrap.test-d.ts │ └── as-iterator.ts ├── upload-http-client.ts └── enhance-with-methods.ts ├── vitest.setup.unit.ts ├── vitest.config.ts ├── catalog-info.yaml ├── tsconfig.json ├── LICENSE ├── CHANGELOG.md └── eslint.config.mjs /.nvmrc: -------------------------------------------------------------------------------- 1 | v18 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | lint-staged 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-scripts=true 2 | -------------------------------------------------------------------------------- /vitest.setup.browser.ts: -------------------------------------------------------------------------------- 1 | window.global ||= window 2 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | export * from './dist/typings/export-types' 2 | -------------------------------------------------------------------------------- /test/output-integration/browser/.gitignore: -------------------------------------------------------------------------------- 1 | public/contentful*.js 2 | public/env.js -------------------------------------------------------------------------------- /test/integration/fixtures/example.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | out 5 | typings 6 | test/output-integration/browser/public 7 | -------------------------------------------------------------------------------- /images/contentful-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/contentful-management.js/master/images/contentful-icon.png -------------------------------------------------------------------------------- /test/integration/fixtures/build.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/contentful-management.js/master/test/integration/fixtures/build.zip -------------------------------------------------------------------------------- /test/integration/fixtures/shiba-stuck-bush.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/contentful-management.js/master/test/integration/fixtures/shiba-stuck-bush.jpg -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "singleQuote": true, 6 | "bracketSameLine": true, 7 | "semi": false 8 | } 9 | -------------------------------------------------------------------------------- /.contentful/vault-secrets.yaml: -------------------------------------------------------------------------------- 1 | version: 1 2 | services: 3 | github-action: 4 | policies: 5 | - dependabot 6 | - semantic-release 7 | - packages-read 8 | -------------------------------------------------------------------------------- /lib/entities/utils.ts: -------------------------------------------------------------------------------- 1 | export type LocalizedEntity< 2 | Entity, 3 | LocalizedFields extends keyof Entity, 4 | Locales extends string, 5 | > = { 6 | [K in keyof Entity]: K extends LocalizedFields ? { [Locale in Locales]: Entity[K] } : Entity[K] 7 | } 8 | -------------------------------------------------------------------------------- /test/output-integration/node/README.md: -------------------------------------------------------------------------------- 1 | ## Node demo 2 | 3 | This small demo application shows how the contentful-management.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 | -------------------------------------------------------------------------------- /test/unit/adapters/REST/endpoints/extension.test.ts: -------------------------------------------------------------------------------- 1 | import { describe } from 'vitest' 2 | import { reusableEntityUpdateTest } from '../reusable-tests/update' 3 | 4 | describe('Rest Extension', () => { 5 | reusableEntityUpdateTest('Extension', 'extension') 6 | }) 7 | -------------------------------------------------------------------------------- /lib/methods/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Helper function that resolves a Promise after the specified duration (in milliseconds) 3 | * @private 4 | */ 5 | export function sleep(durationMs: number): Promise { 6 | return new Promise((resolve) => setTimeout(resolve, durationMs)) 7 | } 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /vitest.setup.unit.ts: -------------------------------------------------------------------------------- 1 | import { vi } from 'vitest' 2 | import type contentfulSdkCore from 'contentful-sdk-core' 3 | 4 | vi.mock('contentful-sdk-core', async (importOriginal) => { 5 | const orig = await importOriginal() 6 | return { 7 | ...orig, 8 | createHttpClient: vi.fn(), 9 | } 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 | "scripts": { 7 | "test": "vitest --run" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "contentful": "file:../../.." 13 | } 14 | } -------------------------------------------------------------------------------- /test/unit/mocks/makeRequest.ts: -------------------------------------------------------------------------------- 1 | import { vi } from 'vitest' 2 | import type { Mock } from 'vitest' 3 | import type { MakeRequest } from '../../../lib/common-types' 4 | 5 | export default function setupMakeRequest( 6 | promise: Promise, 7 | ): Mock<{ payload: T }[], T> & MakeRequest { 8 | return vi.fn().mockReturnValue(promise) 9 | } 10 | -------------------------------------------------------------------------------- /test/output-integration/browser/README.md: -------------------------------------------------------------------------------- 1 | ## Browser demo 2 | 3 | This small demo application shows how the contentful-management.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 | -------------------------------------------------------------------------------- /lib/constants/editor-interface-defaults/index.ts: -------------------------------------------------------------------------------- 1 | import { SidebarAssetConfiguration, SidebarEntryConfiguration } from './sidebar-defaults' 2 | import { EntryConfiguration } from './editors-defaults' 3 | import getDefaultControlOfField from './controls-defaults' 4 | 5 | export default { 6 | SidebarEntryConfiguration, 7 | SidebarAssetConfiguration, 8 | EntryConfiguration, 9 | getDefaultControlOfField, 10 | } 11 | -------------------------------------------------------------------------------- /.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/constants/editor-interface-defaults/types.ts: -------------------------------------------------------------------------------- 1 | export enum WidgetNamespace { 2 | BUILTIN = 'builtin', 3 | EXTENSION = 'extension', 4 | SIDEBAR_BUILTIN = 'sidebar-builtin', 5 | APP = 'app', 6 | EDITOR_BUILTIN = 'editor-builtin', 7 | } 8 | 9 | export const DEFAULT_EDITOR_ID = 'default-editor' 10 | 11 | /** 12 | * @private 13 | */ 14 | export const in_ = (key: K, object: O): key is K & keyof O => 15 | key in object 16 | -------------------------------------------------------------------------------- /test/utils.ts: -------------------------------------------------------------------------------- 1 | import type { Link, VersionedLink } from '../lib/common-types' 2 | 3 | export function makeLink(type: T, id: string): Link { 4 | return { 5 | sys: { id, linkType: type, type: 'Link' }, 6 | } 7 | } 8 | 9 | export function makeVersionedLink( 10 | type: T, 11 | id: string, 12 | version: number, 13 | ): VersionedLink { 14 | return { 15 | sys: { id, linkType: type, type: 'Link', version }, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /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 | // @todo In a future version of Vitest, we hope to be able to set these options to our integration tests through Vitest workspaces. 11 | // Currently, we’re specifying them in the package.json CLI parameters. 12 | // maxWorkers: 3, 13 | // minWorkers: 1, 14 | // fileParallelism: false, 15 | }, 16 | }) 17 | -------------------------------------------------------------------------------- /lib/create-adapter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @packageDocumentation 3 | * @hidden 4 | */ 5 | 6 | import type { Adapter } from './common-types' 7 | import type { RestAdapterParams } from './adapters/REST/rest-adapter' 8 | import { RestAdapter } from './adapters/REST/rest-adapter' 9 | 10 | export type AdapterParams = { 11 | apiAdapter: Adapter 12 | } 13 | 14 | /** 15 | * @private 16 | */ 17 | export function createAdapter(params: RestAdapterParams | AdapterParams): Adapter { 18 | if ('apiAdapter' in params) { 19 | return params.apiAdapter 20 | } else { 21 | return new RestAdapter(params) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Question or Discussion 4 | url: https://github.com/contentful/contentful-management/discussions 5 | about: Ask questions or discuss ideas with the community 6 | - name: Documentation 7 | url: https://www.contentful.com/developers/docs/references/content-management-api/ 8 | about: Check out our documentation for help and guides 9 | - name: Security Vulnerability 10 | url: https://github.com/contentful/contentful-management/security/advisories/new 11 | about: Report a security vulnerability privately 12 | -------------------------------------------------------------------------------- /catalog-info.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: backstage.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: contentful-management.js 5 | description: | 6 | JavaScript library for Contentful's Management API (node & browser) 7 | annotations: 8 | circleci.com/project-slug: github/contentful/contentful-management.js 9 | github.com/project-slug: contentful/contentful-management.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/adapters/REST/endpoints/utils.ts: -------------------------------------------------------------------------------- 1 | import type { QueryOptions } from '../../../common-types' 2 | 3 | export function normalizeSelect(query?: QueryOptions): QueryOptions | undefined { 4 | if (query && query.select && !/sys/i.test(query.select)) { 5 | return { 6 | ...query, 7 | select: query.select + ',sys', 8 | } 9 | } 10 | return query 11 | } 12 | 13 | export function normalizeSpaceId(query?: QueryOptions): QueryOptions | undefined { 14 | if (query && query.spaceId) { 15 | const { spaceId, ...rest } = query 16 | return { 17 | ...rest, 18 | 'sys.space.sys.id[in]': spaceId, 19 | } 20 | } 21 | return query 22 | } 23 | -------------------------------------------------------------------------------- /test/integration/preview-api-key-integration.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, beforeAll, describe, test, afterAll } from 'vitest' 2 | import { getDefaultSpace, timeoutToCalmRateLimiting } from '../helpers' 3 | import type { Space } from '../../lib/export-types' 4 | 5 | describe('PreviewApiKeys Api', () => { 6 | let space: Space 7 | 8 | beforeAll(async () => { 9 | space = await getDefaultSpace() 10 | }) 11 | 12 | afterAll(timeoutToCalmRateLimiting) 13 | 14 | test('Gets previewApiKeys', async () => { 15 | const response = await space.getPreviewApiKeys() 16 | expect(response.sys).toBeDefined() 17 | expect(response.items).toBeDefined() 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /test/unit/entities/asset-key.test.ts: -------------------------------------------------------------------------------- 1 | import { cloneMock } from '../mocks/entities' 2 | import { wrapAssetKey } from '../../../lib/entities/asset-key' 3 | import { entityWrappedTest } from '../test-creators/instance-entity-methods' 4 | import { describe, test } from 'vitest' 5 | import setupMakeRequest from '../mocks/makeRequest' 6 | 7 | function setup(promise) { 8 | return { 9 | makeRequest: setupMakeRequest(promise), 10 | entityMock: cloneMock('assetKey'), 11 | } 12 | } 13 | 14 | describe('Entity AssetKey', () => { 15 | test('AssetKey is wrapped', async () => { 16 | return entityWrappedTest(setup, { 17 | wrapperMethod: wrapAssetKey, 18 | }) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /test/unit/entities/ui-config.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test } from 'vitest' 2 | import { wrapUIConfig } from '../../../lib/entities/ui-config' 3 | import { cloneMock } from '../mocks/entities' 4 | import setupMakeRequest from '../mocks/makeRequest' 5 | import { entityWrappedTest } from '../test-creators/instance-entity-methods' 6 | 7 | function setup(promise) { 8 | return { 9 | makeRequest: setupMakeRequest(promise), 10 | entityMock: cloneMock('uiConfig'), 11 | } 12 | } 13 | 14 | describe('Entity UIConfig', () => { 15 | test('UIConfig is wrapped', async () => { 16 | return entityWrappedTest(setup, { 17 | wrapperMethod: wrapUIConfig, 18 | }) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /lib/plain/checks.ts: -------------------------------------------------------------------------------- 1 | import type { MetaSysProps } from '../common-types' 2 | 3 | export const isPublished = (data: { sys: MetaSysProps }) => !!data.sys.publishedVersion 4 | 5 | export const isUpdated = (data: { sys: MetaSysProps }) => { 6 | // The act of publishing an entity increases its version by 1, so any entry which has 7 | // 2 versions higher or more than the publishedVersion has unpublished changes. 8 | return !!(data.sys.publishedVersion && data.sys.version > data.sys.publishedVersion + 1) 9 | } 10 | 11 | export const isDraft = (data: { sys: MetaSysProps }) => !data.sys.publishedVersion 12 | 13 | export const isArchived = (data: { sys: MetaSysProps }) => !!data.sys.archivedVersion 14 | -------------------------------------------------------------------------------- /lib/entities/field-type.ts: -------------------------------------------------------------------------------- 1 | export type FieldType = 2 | | { type: 'Symbol' } 3 | | { type: 'Text' } 4 | | { type: 'RichText' } 5 | | { type: 'Integer' } 6 | | { type: 'Number' } 7 | | { type: 'Date' } 8 | | { type: 'Boolean' } 9 | | { type: 'Object' } 10 | | { type: 'Location' } 11 | | { type: 'Link'; linkType: 'Asset' } 12 | | { type: 'Link'; linkType: 'Entry' } 13 | | { type: 'ResourceLink'; linkType: string } 14 | | { type: 'Array'; items: { type: 'Symbol' } } 15 | | { type: 'Array'; items: { type: 'Link'; linkType: 'Entry' } } 16 | | { type: 'Array'; items: { type: 'ResourceLink'; linkType: string } } 17 | | { type: 'Array'; items: { type: 'Link'; linkType: 'Asset' } } 18 | -------------------------------------------------------------------------------- /test/output-integration/browser/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | contentful-management.js Test Page 8 | 9 | 10 | 11 | 12 | 13 |
This text should be replaced by the response
14 |
This text should be replaced by the client version
15 | 16 | 17 | -------------------------------------------------------------------------------- /test/unit/entities/semantic-search.test.ts: -------------------------------------------------------------------------------- 1 | import { cloneMock } from '../mocks/entities' 2 | import setupMakeRequest from '../mocks/makeRequest' 3 | import { wrapSemanticSearch } from '../../../lib/entities/semantic-search' 4 | import { entityWrappedTest } from '../test-creators/instance-entity-methods' 5 | import { describe, test } from 'vitest' 6 | 7 | function setup(promise) { 8 | return { 9 | makeRequest: setupMakeRequest(promise), 10 | entityMock: cloneMock('semanticSearch'), 11 | } 12 | } 13 | 14 | describe('Entity SemanticSearch', () => { 15 | test('SemanticSearch is wrapped', async () => { 16 | return entityWrappedTest(setup, { wrapperMethod: wrapSemanticSearch }) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /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 | "dev": "npx serve public", 10 | "setup-test-env": "npm run setup-puppeteer && npm run copy-bundle && npm run setup-env", 11 | "setup-puppeteer": "node ./node_modules/puppeteer/install.mjs", 12 | "copy-bundle": "cp ../../../dist/contentful-management.browser.min.js ./public/.", 13 | "setup-env": "node scripts/inject-env.js" 14 | }, 15 | "author": "", 16 | "license": "ISC", 17 | "devDependencies": { 18 | "puppeteer": "^22.15.0" 19 | } 20 | } -------------------------------------------------------------------------------- /test/integration/space-integration.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, beforeAll, afterAll } from 'vitest' 2 | import { expect } from 'vitest' 3 | import { getTestOrganization, timeoutToCalmRateLimiting } from '../helpers' 4 | import type { Organization } from '../../lib/export-types' 5 | 6 | describe('Space API', () => { 7 | let organization: Organization 8 | 9 | beforeAll(async () => { 10 | organization = await getTestOrganization() 11 | }) 12 | 13 | afterAll(timeoutToCalmRateLimiting) 14 | 15 | it('Gets organization spaces', async () => { 16 | const response = await organization.getUsers() 17 | 18 | expect(response.sys).toBeTruthy() 19 | expect(response.items).toBeTruthy() 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /test/unit/entities/user-ui-config.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test } from 'vitest' 2 | import { wrapUserUIConfig } from '../../../lib/entities/user-ui-config' 3 | import { cloneMock } from '../mocks/entities' 4 | import setupMakeRequest from '../mocks/makeRequest' 5 | import { entityWrappedTest } from '../test-creators/instance-entity-methods' 6 | 7 | function setup(promise) { 8 | return { 9 | makeRequest: setupMakeRequest(promise), 10 | entityMock: cloneMock('userUIConfig'), 11 | } 12 | } 13 | 14 | describe('Entity UserUIConfig', () => { 15 | test('UserUIConfig is wrapped', async () => { 16 | return entityWrappedTest(setup, { 17 | wrapperMethod: wrapUserUIConfig, 18 | }) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /test/unit/entities/app-access-token.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test } from 'vitest' 2 | import { wrapAppAccessToken } from '../../../lib/entities/app-access-token' 3 | import { appAccessTokenMock } from '../mocks/entities' 4 | import setupMakeRequest from '../mocks/makeRequest' 5 | import { entityWrappedTest } from '../test-creators/instance-entity-methods' 6 | 7 | function setup(promise) { 8 | return { 9 | makeRequest: setupMakeRequest(promise), 10 | entityMock: appAccessTokenMock, 11 | } 12 | } 13 | 14 | describe('AppAccessToken', () => { 15 | test('AppAccessToken is wrapped', async () => { 16 | return entityWrappedTest(setup, { 17 | wrapperMethod: wrapAppAccessToken, 18 | }) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /test/unit/entities/usage.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test } from 'vitest' 2 | import { cloneMock } from '../mocks/entities' 3 | import setupMakeRequest from '../mocks/makeRequest' 4 | import { wrapUsageCollection } from '../../../lib/entities/usage' 5 | import { entityCollectionWrappedTest } from '../test-creators/instance-entity-methods' 6 | 7 | function setup(promise) { 8 | return { 9 | makeRequest: setupMakeRequest(promise), 10 | entityMock: cloneMock('usage'), 11 | } 12 | } 13 | 14 | describe('Entity Usage', () => { 15 | test('Usage period collection is wrapped', async () => { 16 | return entityCollectionWrappedTest(setup, { 17 | wrapperMethod: wrapUsageCollection, 18 | }) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /lib/upload-http-client.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosInstance } from 'contentful-sdk-core' 2 | 3 | type UploadHttpClientOpts = { 4 | uploadTimeout?: number 5 | } 6 | 7 | /** 8 | * @private 9 | */ 10 | export function getUploadHttpClient( 11 | http: AxiosInstance, 12 | options?: UploadHttpClientOpts, 13 | ): AxiosInstance { 14 | const { hostUpload, defaultHostnameUpload, timeout } = http.httpClientParams as Record< 15 | string, 16 | any 17 | > 18 | const uploadHttp = http.cloneWithNewParams({ 19 | host: hostUpload || defaultHostnameUpload, 20 | // Using client presets, options or 5 minute default timeout 21 | timeout: timeout ?? options?.uploadTimeout ?? 300000, 22 | }) 23 | return uploadHttp 24 | } 25 | -------------------------------------------------------------------------------- /test/output-integration/browser/public/index.js: -------------------------------------------------------------------------------- 1 | async function run() { 2 | if (!createClient) { 3 | throw 'contentful-management.js could not be loaded. Please check the build output.' 4 | } 5 | 6 | const client = createClient({ 7 | accessToken: process.env.CONTENTFUL_INTEGRATION_TEST_CMA_TOKEN, 8 | }) 9 | 10 | const response = await client.getSpace('segpl12szpe6') 11 | 12 | const loadedDiv = document.createElement('div') 13 | loadedDiv.id = 'contentful-management-loaded' 14 | document.querySelector('body').appendChild(loadedDiv) 15 | 16 | document.querySelector('#content').innerHTML = response.sys.id 17 | 18 | document.querySelector('#version').innerHTML = client.version 19 | } 20 | 21 | run() 22 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "CodeQL Scan for GitHub Actions Workflows" 3 | 4 | on: 5 | push: 6 | branches: [main] 7 | paths: [".github/workflows/**"] 8 | 9 | jobs: 10 | analyze: 11 | name: Analyze GitHub Actions workflows 12 | runs-on: ubuntu-latest 13 | permissions: 14 | actions: read 15 | contents: read 16 | security-events: write 17 | 18 | steps: 19 | - uses: actions/checkout@v5 20 | 21 | - name: Initialize CodeQL 22 | uses: github/codeql-action/init@v4 23 | with: 24 | languages: actions 25 | 26 | - name: Run CodeQL Analysis 27 | uses: github/codeql-action/analyze@v4 28 | with: 29 | category: actions 30 | -------------------------------------------------------------------------------- /test/unit/entities/app-signed-request.test.ts: -------------------------------------------------------------------------------- 1 | import { wrapAppSignedRequest } from '../../../lib/entities/app-signed-request' 2 | import { entityWrappedTest } from '../test-creators/instance-entity-methods' 3 | import { appSignedRequestMock } from '../mocks/entities' 4 | import { describe, test } from 'vitest' 5 | import setupMakeRequest from '../mocks/makeRequest' 6 | 7 | function setup(promise) { 8 | return { 9 | makeRequest: setupMakeRequest(promise), 10 | entityMock: appSignedRequestMock, 11 | } 12 | } 13 | 14 | describe('App SignedRequest', () => { 15 | test('SignedRequest is wrapped', async () => { 16 | return entityWrappedTest(setup, { 17 | wrapperMethod: wrapAppSignedRequest, 18 | }) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /test/unit/entities/semantic-duplicates.test.ts: -------------------------------------------------------------------------------- 1 | import { cloneMock } from '../mocks/entities' 2 | import setupMakeRequest from '../mocks/makeRequest' 3 | import { wrapSemanticDuplicates } from '../../../lib/entities/semantic-duplicates' 4 | import { entityWrappedTest } from '../test-creators/instance-entity-methods' 5 | import { describe, test } from 'vitest' 6 | 7 | function setup(promise) { 8 | return { 9 | makeRequest: setupMakeRequest(promise), 10 | entityMock: cloneMock('semanticDuplicates'), 11 | } 12 | } 13 | 14 | describe('Entity SemanticDuplicates', () => { 15 | test('SemanticDuplicates is wrapped', async () => { 16 | return entityWrappedTest(setup, { wrapperMethod: wrapSemanticDuplicates }) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Enable version updates for npm 4 | - package-ecosystem: 'npm' 5 | directory: '/' 6 | schedule: 7 | interval: 'weekly' 8 | day: 'monday' 9 | open-pull-requests-limit: 10 10 | reviewers: 11 | - 'contentful/team-developer-experience' 12 | labels: 13 | - 'dependencies' 14 | commit-message: 15 | prefix: 'chore' 16 | include: 'scope' 17 | ignore: 18 | - dependency-name: husky 19 | versions: 20 | - ">=5.0.0" 21 | - dependency-name: typedoc 22 | versions: 23 | - '>= 0' 24 | - dependency-name: webpack 25 | versions: 26 | - '>= 5.0.0' 27 | cooldown: 28 | default-days: 15 29 | -------------------------------------------------------------------------------- /test/unit/entities/upload-credential.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test } from 'vitest' 2 | import { wrapUploadCredential } from '../../../lib/entities/upload-credential' 3 | import { cloneMock } from '../mocks/entities' 4 | import setupMakeRequest from '../mocks/makeRequest' 5 | import { entityWrappedTest } from '../test-creators/instance-entity-methods' 6 | 7 | function setup(promise) { 8 | return { 9 | makeRequest: setupMakeRequest(promise), 10 | entityMock: cloneMock('uploadCredential'), 11 | } 12 | } 13 | 14 | describe('Entity Upload credential', () => { 15 | test('UploadCredential is wrapped', async () => { 16 | return entityWrappedTest(setup, { 17 | wrapperMethod: wrapUploadCredential, 18 | }) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /test/unit/entities/vectorization-status.test.ts: -------------------------------------------------------------------------------- 1 | import { cloneMock } from '../mocks/entities' 2 | import setupMakeRequest from '../mocks/makeRequest' 3 | import { wrapVectorizationStatus } from '../../../lib/entities/vectorization-status' 4 | import { entityWrappedTest } from '../test-creators/instance-entity-methods' 5 | import { describe, test } from 'vitest' 6 | 7 | function setup(promise) { 8 | return { 9 | makeRequest: setupMakeRequest(promise), 10 | entityMock: cloneMock('vectorizationStatus'), 11 | } 12 | } 13 | 14 | describe('Entity VectorizationStatus', () => { 15 | test('VectorizationStatus is wrapped', async () => { 16 | return entityWrappedTest(setup, { wrapperMethod: wrapVectorizationStatus }) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /test/unit/entities/ai-action-invocation.test.ts: -------------------------------------------------------------------------------- 1 | import { cloneMock } from '../mocks/entities' 2 | import setupMakeRequest from '../mocks/makeRequest' 3 | import { wrapAiActionInvocation } from '../../../lib/entities/ai-action-invocation' 4 | import { entityWrappedTest } from '../test-creators/instance-entity-methods' 5 | import { describe, test } from 'vitest' 6 | 7 | function setup(promise) { 8 | return { 9 | makeRequest: setupMakeRequest(promise), 10 | entityMock: cloneMock('aiActionInvocation'), 11 | } 12 | } 13 | 14 | describe('Entity AiActionInvocation', () => { 15 | test('AiActionInvocation is wrapped', async () => { 16 | return entityWrappedTest(setup, { 17 | wrapperMethod: wrapAiActionInvocation, 18 | }) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /test/unit/entities/invitation.test.ts: -------------------------------------------------------------------------------- 1 | import { cloneMock } from '../mocks/entities' 2 | import setupMakeRequest from '../mocks/makeRequest' 3 | import { entityWrappedTest } from '../test-creators/instance-entity-methods' 4 | import { wrapOrganizationInvitation } from '../../../lib/entities/organization-invitation' 5 | import { describe, test } from 'vitest' 6 | 7 | function setup(promise) { 8 | return { 9 | makeRequest: setupMakeRequest(promise), 10 | entityMock: cloneMock('organizationInvitation'), 11 | } 12 | } 13 | 14 | describe('Entity OrganizationInvitation', () => { 15 | test('Organization invitation is wrapped', () => { 16 | entityWrappedTest(setup, { 17 | wrapperMethod: wrapOrganizationInvitation, 18 | }) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /lib/entities/widget-parameters.ts: -------------------------------------------------------------------------------- 1 | export type ParameterType = 'Boolean' | 'Symbol' | 'Number' | 'Enum' 2 | export type InstallationParameterType = ParameterType | 'Secret' 3 | export type ParameterOption = string | { [key: string]: string } 4 | 5 | export interface ParameterDefinition { 6 | name: string 7 | id: string 8 | description?: string 9 | type: T 10 | required?: boolean 11 | default?: boolean | string | number 12 | options?: ParameterOption[] 13 | labels?: { 14 | empty?: string 15 | true?: string 16 | false?: string 17 | } 18 | } 19 | 20 | export type DefinedParameters = Record 21 | export type FreeFormParameters = Record | Array | number | string | boolean 22 | -------------------------------------------------------------------------------- /test/output-integration/node/index.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from 'vitest' 2 | import * as contentfulManagement from 'contentful-management' 3 | 4 | /** 5 | * This test project should ensure that the builds are actually functioning. 6 | * Mostly useful for changes to building/transpiling/bundling/... 7 | */ 8 | 9 | const client = contentfulManagement.createClient({ 10 | accessToken: process.env.CONTENTFUL_INTEGRATION_TEST_CMA_TOKEN || '', 11 | }) 12 | 13 | const PERMANENT_SPACE_ID = 'segpl12szpe6' 14 | 15 | test('Gets entry', async () => { 16 | const response = await client.getSpace(PERMANENT_SPACE_ID) 17 | expect(response.sys).toBeDefined() 18 | expect(response.name).toBeDefined() 19 | expect(response.sys.id).toBe(PERMANENT_SPACE_ID) 20 | }) 21 | -------------------------------------------------------------------------------- /lib/adapters/REST/endpoints/ai-action-invocation.ts: -------------------------------------------------------------------------------- 1 | import type { RawAxiosRequestHeaders } from 'axios' 2 | import type { AxiosInstance } from 'contentful-sdk-core' 3 | import type { GetSpaceEnvironmentParams } from '../../../common-types' 4 | import type { RestEndpoint } from '../types' 5 | import * as raw from './raw' 6 | 7 | export const get: RestEndpoint<'AiActionInvocation', 'get'> = ( 8 | http: AxiosInstance, 9 | params: GetSpaceEnvironmentParams & { aiActionId: string; invocationId: string }, 10 | headers?: RawAxiosRequestHeaders, 11 | ) => { 12 | return raw.get( 13 | http, 14 | `/spaces/${params.spaceId}/environments/${params.environmentId}/ai/actions/${params.aiActionId}/invocations/${params.invocationId}`, 15 | { headers }, 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /test/unit/entities/semantic-recommendations.test.ts: -------------------------------------------------------------------------------- 1 | import { cloneMock } from '../mocks/entities' 2 | import setupMakeRequest from '../mocks/makeRequest' 3 | import { wrapSemanticRecommendations } from '../../../lib/entities/semantic-recommendations' 4 | import { entityWrappedTest } from '../test-creators/instance-entity-methods' 5 | import { describe, test } from 'vitest' 6 | 7 | function setup(promise) { 8 | return { 9 | makeRequest: setupMakeRequest(promise), 10 | entityMock: cloneMock('semanticRecommendations'), 11 | } 12 | } 13 | 14 | describe('Entity SemanticRecommendations', () => { 15 | test('SemanticRecommendations is wrapped', async () => { 16 | return entityWrappedTest(setup, { wrapperMethod: wrapSemanticRecommendations }) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /test/integration/space-team-integration.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, beforeAll, afterAll } from 'vitest' 2 | import { expect } from 'vitest' 3 | import { getDefaultSpace, timeoutToCalmRateLimiting } from '../helpers' 4 | import type { Space } from '../../lib/export-types' 5 | 6 | describe('SpaceTeam API', () => { 7 | let space: Space 8 | 9 | beforeAll(async () => { 10 | space = await getDefaultSpace() 11 | }) 12 | 13 | afterAll(timeoutToCalmRateLimiting) 14 | 15 | it('Gets teams for space', async () => { 16 | const response = await space.getTeams() 17 | 18 | expect(response.sys).toBeTruthy() 19 | expect(response.sys.type).toBe('Array') 20 | expect(response.items).toBeTruthy() 21 | expect(response.items[0].sys.type).toBe('Team') 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "moduleResolution": "node", 5 | "allowJs": true, 6 | "noEmit": true, 7 | "outDir": "./dist/typings", 8 | "strict": true, 9 | "isolatedModules": true, 10 | "esModuleInterop": true, 11 | "noImplicitThis": false, 12 | "typeRoots": ["node_modules/@types"], 13 | "skipLibCheck": true 14 | }, 15 | "include": ["lib", "global-types"], 16 | "typedocOptions": { 17 | "out": "./out", 18 | "entryPoints": ["./lib/contentful-management.ts"], 19 | "readme": "README.md", 20 | "name": "contentful-management.js", 21 | "exclude": ["./lib/adapters/REST/endpoints/*"], 22 | "includeVersion": true, 23 | "hideGenerator": true, 24 | "excludePrivate": true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/unit/entities/semantic-reference-suggestions.test.ts: -------------------------------------------------------------------------------- 1 | import { cloneMock } from '../mocks/entities' 2 | import setupMakeRequest from '../mocks/makeRequest' 3 | import { wrapSemanticReferenceSuggestions } from '../../../lib/entities/semantic-reference-suggestions' 4 | import { entityWrappedTest } from '../test-creators/instance-entity-methods' 5 | import { describe, test } from 'vitest' 6 | 7 | function setup(promise) { 8 | return { 9 | makeRequest: setupMakeRequest(promise), 10 | entityMock: cloneMock('semanticReferenceSuggestions'), 11 | } 12 | } 13 | 14 | describe('Entity SemanticReferenceSuggestions', () => { 15 | test('SemanticReferenceSuggestions is wrapped', async () => { 16 | return entityWrappedTest(setup, { wrapperMethod: wrapSemanticReferenceSuggestions }) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /lib/adapters/REST/endpoints/app-signed-request.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosInstance } from 'contentful-sdk-core' 2 | import type { 3 | CreateAppSignedRequestProps, 4 | AppSignedRequestProps, 5 | } from '../../../entities/app-signed-request' 6 | import * as raw from './raw' 7 | import type { RestEndpoint } from '../types' 8 | import type { GetAppInstallationParams } from '../../../common-types' 9 | 10 | export const create: RestEndpoint<'AppSignedRequest', 'create'> = ( 11 | http: AxiosInstance, 12 | params: GetAppInstallationParams, 13 | data: CreateAppSignedRequestProps, 14 | ) => { 15 | return raw.post( 16 | http, 17 | `/spaces/${params.spaceId}/environments/${params.environmentId}/app_installations/${params.appDefinitionId}/signed_requests`, 18 | data, 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /lib/adapters/REST/endpoints/upload-credentials.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosInstance } from 'contentful-sdk-core' 2 | import type { GetSpaceEnvironmentParams } from '../../../common-types' 3 | import { getUploadHttpClient } from '../../../upload-http-client' 4 | import type { RestEndpoint } from '../types' 5 | import * as raw from './raw' 6 | 7 | const getBaseUrl = (params: GetSpaceEnvironmentParams) => { 8 | return `/spaces/${params.spaceId}/environments/${ 9 | params.environmentId ?? 'master' 10 | }/upload_credentials` 11 | } 12 | 13 | export const create: RestEndpoint<'UploadCredential', 'create'> = ( 14 | http: AxiosInstance, 15 | params: GetSpaceEnvironmentParams, 16 | ) => { 17 | const httpUpload = getUploadHttpClient(http) 18 | 19 | const path = getBaseUrl(params) 20 | return raw.post(httpUpload, path) 21 | } 22 | -------------------------------------------------------------------------------- /test/unit/adapters/REST/helpers/setupRestAdapter.ts: -------------------------------------------------------------------------------- 1 | import { RestAdapter } from '../../../../../lib/adapters/REST/rest-adapter' 2 | import setupHttpMock from '../../../mocks/http' 3 | import { createHttpClient } from 'contentful-sdk-core' 4 | 5 | import type { AxiosInstance } from 'contentful-sdk-core' 6 | import type { MockedFunction } from 'vitest' 7 | 8 | const createHttpClientMock = >(createHttpClient) 9 | 10 | export default function setupRestAdapter(httpPromise, params = {}) { 11 | const httpMock = setupHttpMock(httpPromise) 12 | 13 | createHttpClientMock.mockReturnValue(httpMock as unknown as AxiosInstance) 14 | 15 | return { 16 | adapterMock: new RestAdapter({ 17 | accessToken: 'token', 18 | ...params, 19 | }), 20 | httpMock, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/adapters/REST/endpoints/semantic-search.ts: -------------------------------------------------------------------------------- 1 | import type { RawAxiosRequestHeaders } from 'axios' 2 | import type { AxiosInstance } from 'contentful-sdk-core' 3 | import type { GetSpaceEnvironmentParams } from '../../../common-types' 4 | import type { RestEndpoint } from '../types' 5 | import * as raw from './raw' 6 | import type { GetSemanticSearchProps, SemanticSearchProps } from '../../../entities/semantic-search' 7 | 8 | export const get: RestEndpoint<'SemanticSearch', 'get'> = ( 9 | http: AxiosInstance, 10 | params: GetSpaceEnvironmentParams, 11 | data: GetSemanticSearchProps, 12 | headers?: RawAxiosRequestHeaders, 13 | ) => { 14 | return raw.post( 15 | http, 16 | `/spaces/${params.spaceId}/environments/${params.environmentId}/semantic/search`, 17 | data, 18 | { headers }, 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /lib/adapters/REST/endpoints/resource.ts: -------------------------------------------------------------------------------- 1 | import type { CursorPaginatedCollectionProp, GetResourceParams } from '../../../common-types' 2 | import type { RestEndpoint } from '../types' 3 | import type { AxiosInstance } from 'contentful-sdk-core' 4 | import * as raw from './raw' 5 | import type { ResourceProps, ResourceQueryOptions } from '../../../entities/resource' 6 | 7 | const getBaseUrl = (params: GetResourceParams) => 8 | `/spaces/${params.spaceId}/environments/${params.environmentId}/resource_types/${params.resourceTypeId}/resources` 9 | 10 | export const getMany: RestEndpoint<'Resource', 'getMany'> = ( 11 | http: AxiosInstance, 12 | params: GetResourceParams & { query?: ResourceQueryOptions }, 13 | ) => 14 | raw.get>(http, getBaseUrl(params), { 15 | params: params.query, 16 | }) 17 | -------------------------------------------------------------------------------- /test/unit/entities/bulk-action.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'vitest' 2 | import { cloneMock } from '../mocks/entities' 3 | import setupMakeRequest from '../mocks/makeRequest' 4 | 5 | import { wrapBulkAction } from '../../../lib/entities/bulk-action' 6 | import { entityWrappedTest, entityActionTest } from '../test-creators/instance-entity-methods' 7 | 8 | function setup(promise) { 9 | return { 10 | makeRequest: setupMakeRequest(promise), 11 | entityMock: cloneMock('bulkAction'), 12 | } 13 | } 14 | 15 | describe('Entity BulkAction', () => { 16 | it('BulkAction is wrapped', async () => { 17 | return entityWrappedTest(setup, { wrapperMethod: wrapBulkAction }) 18 | }) 19 | 20 | it('BulkAction get', async () => { 21 | return entityActionTest(setup, { wrapperMethod: wrapBulkAction, actionMethod: 'get' }) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /lib/adapters/REST/endpoints/app-access-token.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosInstance } from 'contentful-sdk-core' 2 | import type { 3 | AppAccessTokenProps, 4 | CreateAppAccessTokenProps, 5 | } from '../../../entities/app-access-token' 6 | import * as raw from './raw' 7 | import type { RestEndpoint } from '../types' 8 | import type { GetAppInstallationParams } from '../../../common-types' 9 | 10 | export const create: RestEndpoint<'AppAccessToken', 'create'> = ( 11 | http: AxiosInstance, 12 | params: GetAppInstallationParams, 13 | data: CreateAppAccessTokenProps, 14 | ) => { 15 | return raw.post( 16 | http, 17 | `/spaces/${params.spaceId}/environments/${params.environmentId}/app_installations/${params.appDefinitionId}/access_tokens`, 18 | undefined, 19 | { headers: { Authorization: `Bearer ${data.jwt}` } }, 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /test/unit/entities/release-action.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'vitest' 2 | import { cloneMock } from '../mocks/entities' 3 | import setupMakeRequest from '../mocks/makeRequest' 4 | import { entityWrappedTest, entityActionTest } from '../test-creators/instance-entity-methods' 5 | import { wrapReleaseAction } from '../../../lib/entities/release-action' 6 | 7 | function setup(promise) { 8 | return { 9 | makeRequest: setupMakeRequest(promise), 10 | entityMock: cloneMock('releaseAction'), 11 | } 12 | } 13 | 14 | describe('Entity ReleaseAction', () => { 15 | it('ReleaseAction is wrapped', async () => { 16 | return entityWrappedTest(setup, { wrapperMethod: wrapReleaseAction }) 17 | }) 18 | 19 | it('ReleaseAction get', async () => { 20 | return entityActionTest(setup, { wrapperMethod: wrapReleaseAction, actionMethod: 'get' }) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /test/unit/create-adapter.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest' 2 | import type { RestAdapterParams } from '../../lib/adapters/REST/rest-adapter' 3 | import { RestAdapter } from '../../lib/adapters/REST/rest-adapter' 4 | import { createAdapter } from '../../lib/create-adapter' 5 | 6 | describe('createAdapter', () => { 7 | it('returns adapter if provided', () => { 8 | const apiAdapter = new RestAdapter({ accessToken: 'token' } as RestAdapterParams) 9 | 10 | const createdAdapter = createAdapter({ 11 | apiAdapter, 12 | }) 13 | 14 | expect(createdAdapter).toBe(apiAdapter) 15 | }) 16 | 17 | describe('creates RestAdapter', () => { 18 | it('if no apiAdapter is provided', () => { 19 | const createdAdapter = createAdapter({ accessToken: 'token' }) 20 | expect(createdAdapter).toBeInstanceOf(RestAdapter) 21 | }) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /.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/unit/adapters/REST/endpoints/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest' 2 | import { normalizeSpaceId } from '../../../../../lib/adapters/REST/endpoints/utils' 3 | 4 | describe('normalizeSpaceId', () => { 5 | it('replaces the `spaceId` property of a query', () => { 6 | const query = { 7 | spaceId: 'some-space-id', 8 | } 9 | 10 | const expected = { 11 | 'sys.space.sys.id[in]': 'some-space-id', 12 | } 13 | 14 | expect(normalizeSpaceId(query)).to.deep.equal(expected) 15 | }) 16 | 17 | it('does not replace other properties', () => { 18 | const query = { 19 | limit: 10, 20 | spaceId: 'some-space-id', 21 | } 22 | 23 | const expected = { 24 | limit: 10, 25 | 'sys.space.sys.id[in]': 'some-space-id', 26 | } 27 | 28 | expect(normalizeSpaceId(query)).to.deep.equal(expected) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /lib/adapters/REST/endpoints/semantic-duplicates.ts: -------------------------------------------------------------------------------- 1 | import type { RawAxiosRequestHeaders } from 'axios' 2 | import type { AxiosInstance } from 'contentful-sdk-core' 3 | import type { GetSpaceEnvironmentParams } from '../../../common-types' 4 | import type { RestEndpoint } from '../types' 5 | import * as raw from './raw' 6 | import type { 7 | GetSemanticDuplicatesProps, 8 | SemanticDuplicatesProps, 9 | } from '../../../entities/semantic-duplicates' 10 | 11 | export const get: RestEndpoint<'SemanticDuplicates', 'get'> = ( 12 | http: AxiosInstance, 13 | params: GetSpaceEnvironmentParams, 14 | data: GetSemanticDuplicatesProps, 15 | headers?: RawAxiosRequestHeaders, 16 | ) => { 17 | return raw.post( 18 | http, 19 | `/spaces/${params.spaceId}/environments/${params.environmentId}/semantic/duplicates`, 20 | data, 21 | { headers }, 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | workflow_call: 5 | 6 | jobs: 7 | 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | permissions: 12 | contents: read 13 | 14 | steps: 15 | 16 | - name: Checkout code 17 | uses: actions/checkout@v5 18 | 19 | - name: Setup Node.js 20 | uses: actions/setup-node@v6 21 | with: 22 | node-version: '22' 23 | cache: 'npm' 24 | 25 | - name: Install dependencies 26 | run: npm ci 27 | 28 | - name: Build 29 | run: npm run build 30 | 31 | - name: Check build artifacts 32 | run: ls -la dist/ 33 | 34 | - name: Save Build folders 35 | uses: actions/cache/save@v4 36 | with: 37 | path: | 38 | dist 39 | key: build-cache-${{ github.run_id }}-${{ github.run_attempt }} 40 | -------------------------------------------------------------------------------- /lib/adapters/REST/endpoints/space-member.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosInstance } from 'contentful-sdk-core' 2 | import type { CollectionProp, GetSpaceParams, QueryParams } from '../../../common-types' 3 | import type { SpaceMemberProps } from '../../../entities/space-member' 4 | import type { RestEndpoint } from '../types' 5 | import * as raw from './raw' 6 | 7 | export const get: RestEndpoint<'SpaceMember', 'get'> = ( 8 | http: AxiosInstance, 9 | params: GetSpaceParams & { spaceMemberId: string }, 10 | ) => 11 | raw.get(http, `/spaces/${params.spaceId}/space_members/${params.spaceMemberId}`) 12 | 13 | export const getMany: RestEndpoint<'SpaceMember', 'getMany'> = ( 14 | http: AxiosInstance, 15 | params: GetSpaceParams & QueryParams, 16 | ) => 17 | raw.get>(http, `/spaces/${params.spaceId}/space_members`, { 18 | params: params.query, 19 | }) 20 | -------------------------------------------------------------------------------- /test/unit/entities/entry.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test } from 'vitest' 2 | import { wrapEntry, wrapEntryCollection } from '../../../lib/entities/entry' 3 | import { cloneMock } from '../mocks/entities' 4 | import setupMakeRequest from '../mocks/makeRequest' 5 | import { 6 | entityCollectionWrappedTest, 7 | entityWrappedTest, 8 | } from '../test-creators/instance-entity-methods' 9 | 10 | function setup(promise) { 11 | return { 12 | makeRequest: setupMakeRequest(promise), 13 | entityMock: cloneMock('entry'), 14 | } 15 | } 16 | 17 | describe('Entity Entry', () => { 18 | test('Entry is wrapped', async () => { 19 | return entityWrappedTest(setup, { 20 | wrapperMethod: wrapEntry, 21 | }) 22 | }) 23 | 24 | test('Entry collection is wrapped', async () => { 25 | return entityCollectionWrappedTest(setup, { 26 | wrapperMethod: wrapEntryCollection, 27 | }) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /test/unit/entities/user.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test } from 'vitest' 2 | import { cloneMock } from '../mocks/entities' 3 | import setupMakeRequest from '../mocks/makeRequest' 4 | import { wrapUser, wrapUserCollection } from '../../../lib/entities/user' 5 | import { 6 | entityCollectionWrappedTest, 7 | entityWrappedTest, 8 | } from '../test-creators/instance-entity-methods' 9 | 10 | function setup(promise) { 11 | return { 12 | makeRequest: setupMakeRequest(promise), 13 | entityMock: cloneMock('user'), 14 | } 15 | } 16 | 17 | describe('Entity TeamSpaceMembership', () => { 18 | test('User is wrapped', async () => { 19 | return entityWrappedTest(setup, { 20 | wrapperMethod: wrapUser, 21 | }) 22 | }) 23 | 24 | test('User collection is wrapped', async () => { 25 | return entityCollectionWrappedTest(setup, { 26 | wrapperMethod: wrapUserCollection, 27 | }) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /test/output-integration/browser/vitest.setup.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, beforeEach, afterAll } from 'vitest' 2 | 3 | import puppeteer, { Browser, Page } 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-management-loaded', { timeout: 5_000 }) 25 | }) 26 | 27 | afterAll(async () => { 28 | await browser.close() 29 | }) 30 | 31 | export { page } 32 | -------------------------------------------------------------------------------- /lib/adapters/REST/endpoints/semantic-recommendations.ts: -------------------------------------------------------------------------------- 1 | import type { RawAxiosRequestHeaders } from 'axios' 2 | import type { AxiosInstance } from 'contentful-sdk-core' 3 | import type { GetSpaceEnvironmentParams } from '../../../common-types' 4 | import type { RestEndpoint } from '../types' 5 | import * as raw from './raw' 6 | import type { 7 | GetSemanticRecommendationsProps, 8 | SemanticRecommendationsProps, 9 | } from '../../../entities/semantic-recommendations' 10 | 11 | export const get: RestEndpoint<'SemanticRecommendations', 'get'> = ( 12 | http: AxiosInstance, 13 | params: GetSpaceEnvironmentParams, 14 | data: GetSemanticRecommendationsProps, 15 | headers?: RawAxiosRequestHeaders, 16 | ) => { 17 | return raw.post( 18 | http, 19 | `/spaces/${params.spaceId}/environments/${params.environmentId}/semantic/recommendations`, 20 | data, 21 | { headers }, 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /lib/entities/concept-scheme.ts: -------------------------------------------------------------------------------- 1 | import type { Link } from '../common-types' 2 | import type { TaxonomyConceptLink } from './concept' 3 | import type { LocalizedEntity } from './utils' 4 | 5 | export type ConceptScheme = { 6 | uri: string | null 7 | prefLabel: string 8 | definition: string | null 9 | topConcepts: TaxonomyConceptLink[] 10 | concepts: TaxonomyConceptLink[] 11 | totalConcepts: number 12 | sys: { 13 | type: 'TaxonomyConceptScheme' 14 | createdAt: string 15 | updatedAt: string 16 | id: string 17 | version: number 18 | createdBy: Link<'User'> 19 | updatedBy: Link<'User'> 20 | } 21 | } 22 | 23 | export type ConceptSchemeProps = LocalizedEntity< 24 | ConceptScheme, 25 | 'prefLabel' | 'definition', 26 | Locales 27 | > 28 | 29 | export type CreateConceptSchemeProps = Partial> & 30 | Pick 31 | -------------------------------------------------------------------------------- /lib/plain/entities/semantic-search.ts: -------------------------------------------------------------------------------- 1 | import type { GetSemanticSearchProps, SemanticSearchProps } from '../../entities/semantic-search' 2 | import type { OptionalDefaults } from '../wrappers/wrap' 3 | import type { GetSpaceEnvironmentParams } from '../../common-types' 4 | import type { RawAxiosRequestHeaders } from 'axios' 5 | 6 | export type SemanticSearchPlainClientAPI = { 7 | /** 8 | * Retrieves Semantic Search results for the given query. 9 | * @param params Parameters for getting the space and environment IDs. 10 | * @param payload Payload containing query and optional filters. 11 | * @param headers Optional headers for the request. 12 | * @returns A promise that resolves to Semantic Search results. 13 | */ 14 | get( 15 | params: OptionalDefaults, 16 | payload: GetSemanticSearchProps, 17 | headers?: Partial, 18 | ): Promise 19 | } 20 | -------------------------------------------------------------------------------- /test/output-integration/browser/scripts/inject-env.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import { resolve } from 'path' 3 | import * as url from 'url' 4 | 5 | // Define the path for the output file 6 | const outputPath = resolve( 7 | url.fileURLToPath(new URL('.', import.meta.url)), 8 | '..', 9 | 'public', 10 | 'env.js', 11 | ) 12 | 13 | // Convert process.env into a JS object with JSON.stringify 14 | const envVariables = Object.keys(process.env).reduce((acc, key) => { 15 | acc[key] = process.env[key] 16 | return acc 17 | }, {}) 18 | 19 | // Write the JS file that exports the environment variables 20 | const fileContent = ` 21 | // Auto-generated file for exposing environment variables (testing purposes only - do not do this in production) 22 | const process = { env: ${JSON.stringify(envVariables, null, 2)} }; 23 | ` 24 | 25 | fs.writeFileSync(outputPath, fileContent, 'utf8') 26 | 27 | console.log(`Environment variables written to ${outputPath}`) 28 | -------------------------------------------------------------------------------- /lib/adapters/REST/endpoints/semantic-reference-suggestions.ts: -------------------------------------------------------------------------------- 1 | import type { RawAxiosRequestHeaders } from 'axios' 2 | import type { AxiosInstance } from 'contentful-sdk-core' 3 | import type { GetSpaceEnvironmentParams } from '../../../common-types' 4 | import type { RestEndpoint } from '../types' 5 | import * as raw from './raw' 6 | import type { 7 | GetSemanticReferenceSuggestionsProps, 8 | SemanticReferenceSuggestionsProps, 9 | } from '../../../entities/semantic-reference-suggestions' 10 | 11 | export const get: RestEndpoint<'SemanticReferenceSuggestions', 'get'> = ( 12 | http: AxiosInstance, 13 | params: GetSpaceEnvironmentParams, 14 | data: GetSemanticReferenceSuggestionsProps, 15 | headers?: RawAxiosRequestHeaders, 16 | ) => { 17 | return raw.post( 18 | http, 19 | `/spaces/${params.spaceId}/environments/${params.environmentId}/semantic/reference-suggestions`, 20 | data, 21 | { headers }, 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /test/unit/entities/access-token.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test } from 'vitest' 2 | import { cloneMock } from '../mocks/entities' 3 | import setupMakeRequest from '../mocks/makeRequest' 4 | import { wrapAccessToken, wrapAccessTokenCollection } from '../../../lib/entities/access-token' 5 | import { 6 | entityCollectionWrappedTest, 7 | entityWrappedTest, 8 | } from '../test-creators/instance-entity-methods' 9 | 10 | function setup(promise) { 11 | return { 12 | makeRequest: setupMakeRequest(promise), 13 | entityMock: cloneMock('accessToken'), 14 | } 15 | } 16 | 17 | describe('Entity AccessToken', () => { 18 | test('AccessToken is wrapped', async () => { 19 | return entityWrappedTest(setup, { 20 | wrapperMethod: wrapAccessToken, 21 | }) 22 | }) 23 | 24 | test('AccessToken collection is wrapped', async () => { 25 | return entityCollectionWrappedTest(setup, { 26 | wrapperMethod: wrapAccessTokenCollection, 27 | }) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /test/unit/entities/space-member.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test } from 'vitest' 2 | import { cloneMock } from '../mocks/entities' 3 | import setupMakeRequest from '../mocks/makeRequest' 4 | import { wrapSpaceMember, wrapSpaceMemberCollection } from '../../../lib/entities/space-member' 5 | import { 6 | entityCollectionWrappedTest, 7 | entityWrappedTest, 8 | } from '../test-creators/instance-entity-methods' 9 | 10 | function setup(promise) { 11 | return { 12 | makeRequest: setupMakeRequest(promise), 13 | entityMock: cloneMock('spaceMember'), 14 | } 15 | } 16 | 17 | describe('Entity SpaceMember', () => { 18 | test('SpaceMember is wrapped', async () => { 19 | return entityWrappedTest(setup, { 20 | wrapperMethod: wrapSpaceMember, 21 | }) 22 | }) 23 | 24 | test('SpaceMember collection is wrapped', async () => { 25 | return entityCollectionWrappedTest(setup, { 26 | wrapperMethod: wrapSpaceMemberCollection, 27 | }) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /lib/adapters/REST/endpoints/workflows-changelog.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosInstance, RawAxiosRequestHeaders } from 'axios' 2 | import type { CollectionProp, GetSpaceEnvironmentParams } from '../../../common-types' 3 | import type { 4 | WorkflowsChangelogQueryOptions, 5 | WorkflowsChangelogEntryProps, 6 | } from '../../../entities/workflows-changelog-entry' 7 | import type { RestEndpoint } from '../types' 8 | import * as raw from './raw' 9 | 10 | const getBaseUrl = (params: GetSpaceEnvironmentParams) => 11 | `/spaces/${params.spaceId}/environments/${params.environmentId}/workflows_changelog` 12 | 13 | export const getMany: RestEndpoint<'WorkflowsChangelog', 'getMany'> = ( 14 | http: AxiosInstance, 15 | params: GetSpaceEnvironmentParams & { query: WorkflowsChangelogQueryOptions }, 16 | headers?: RawAxiosRequestHeaders, 17 | ) => 18 | raw.get>(http, getBaseUrl(params), { 19 | headers, 20 | params: params.query, 21 | }) 22 | -------------------------------------------------------------------------------- /lib/entities/asset-key.ts: -------------------------------------------------------------------------------- 1 | import copy from 'fast-copy' 2 | import { toPlainObject } from 'contentful-sdk-core' 3 | import type { DefaultElements, MakeRequest } from '../common-types' 4 | 5 | export type AssetKeyProps = { 6 | /** A JWT describing a policy; needs to be attached to signed URLs */ 7 | policy: string 8 | /** A secret key to be used for signing URLs */ 9 | secret: string 10 | } 11 | 12 | export type CreateAssetKeyProps = { 13 | /** (required) UNIX timestamp in the future (but not more than 48 hours from now) */ 14 | expiresAt: number 15 | } 16 | 17 | export interface AssetKey extends AssetKeyProps, DefaultElements {} 18 | 19 | /** 20 | * @private 21 | * @param http - HTTP client instance 22 | * @param data - Raw asset key data 23 | * @return Wrapped asset key data 24 | */ 25 | export function wrapAssetKey(_makeRequest: MakeRequest, data: AssetKeyProps): AssetKey { 26 | const assetKey = toPlainObject(copy(data)) 27 | return assetKey 28 | } 29 | -------------------------------------------------------------------------------- /lib/plain/entities/semantic-duplicates.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | GetSemanticDuplicatesProps, 3 | SemanticDuplicatesProps, 4 | } from '../../entities/semantic-duplicates' 5 | import type { OptionalDefaults } from '../wrappers/wrap' 6 | import type { GetSpaceEnvironmentParams } from '../../common-types' 7 | import type { RawAxiosRequestHeaders } from 'axios' 8 | 9 | export type SemanticDuplicatesPlainClientAPI = { 10 | /** 11 | * Retrieves Semantic Duplicates for the given entity ID. 12 | * @param params Parameters for getting the space and environment IDs. 13 | * @param payload Payload containing entity ID and optional filters. 14 | * @param headers Optional headers for the request. 15 | * @returns A promise that resolves to Semantic Duplicates. 16 | */ 17 | get( 18 | params: OptionalDefaults, 19 | payload: GetSemanticDuplicatesProps, 20 | headers?: Partial, 21 | ): Promise 22 | } 23 | -------------------------------------------------------------------------------- /test/unit/entities/preview-api-key.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test } from 'vitest' 2 | import { cloneMock } from '../mocks/entities' 3 | import setupMakeRequest from '../mocks/makeRequest' 4 | import { 5 | wrapPreviewApiKey, 6 | wrapPreviewApiKeyCollection, 7 | } from '../../../lib/entities/preview-api-key' 8 | import { 9 | entityCollectionWrappedTest, 10 | entityWrappedTest, 11 | } from '../test-creators/instance-entity-methods' 12 | 13 | function setup(promise) { 14 | return { 15 | makeRequest: setupMakeRequest(promise), 16 | entityMock: cloneMock('previewApiKey'), 17 | } 18 | } 19 | 20 | describe('PreviewApiKey', () => { 21 | test('PreviewApiKey is wrapped', async () => { 22 | return entityWrappedTest(setup, { 23 | wrapperMethod: wrapPreviewApiKey, 24 | }) 25 | }) 26 | 27 | test('PreviewApiKey collection is wrapped', async () => { 28 | return entityCollectionWrappedTest(setup, { 29 | wrapperMethod: wrapPreviewApiKeyCollection, 30 | }) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /test/unit/adapters/REST/rest-adapter.test.ts: -------------------------------------------------------------------------------- 1 | import { vi, expect, describe, it } from 'vitest' 2 | import { RestAdapter } from '../../../../lib/adapters/REST/rest-adapter' 3 | import setupRestAdapter from './helpers/setupRestAdapter' 4 | 5 | vi.mock('contentful-sdk-core') 6 | 7 | describe('Rest Adapter', () => { 8 | it('throws if no accessToken is defined', () => { 9 | // @ts-expect-error we skip accessToken parameter to trigger error 10 | expect(() => new RestAdapter({})).toThrowError('Expected parameter accessToken') 11 | }) 12 | 13 | it('throws if unknown endpoint is called', async () => { 14 | const { adapterMock } = setupRestAdapter(Promise.resolve) 15 | 16 | await expect(() => 17 | adapterMock.makeRequest({ 18 | // @ts-expect-error we pass nothing to trigger unkown endpoint rejection 19 | entityType: 'nothing', 20 | action: 'nothing', 21 | userAgent: 'test-runner', 22 | }), 23 | ).rejects.toThrowError('Unknown endpoint') 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /lib/adapters/REST/endpoints/preview-api-key.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosInstance } from 'contentful-sdk-core' 2 | import type { CollectionProp, GetSpaceParams, QueryParams } from '../../../common-types' 3 | import type { PreviewApiKeyProps } from '../../../entities/preview-api-key' 4 | import type { RestEndpoint } from '../types' 5 | import * as raw from './raw' 6 | 7 | export const get: RestEndpoint<'PreviewApiKey', 'get'> = ( 8 | http: AxiosInstance, 9 | params: GetSpaceParams & { previewApiKeyId: string }, 10 | ) => { 11 | return raw.get( 12 | http, 13 | `/spaces/${params.spaceId}/preview_api_keys/${params.previewApiKeyId}`, 14 | ) 15 | } 16 | 17 | export const getMany: RestEndpoint<'PreviewApiKey', 'getMany'> = ( 18 | http: AxiosInstance, 19 | params: GetSpaceParams & QueryParams, 20 | ) => { 21 | return raw.get>( 22 | http, 23 | `/spaces/${params.spaceId}/preview_api_keys`, 24 | { 25 | params: params.query, 26 | }, 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /lib/plain/entities/semantic-recommendations.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | GetSemanticRecommendationsProps, 3 | SemanticRecommendationsProps, 4 | } from '../../entities/semantic-recommendations' 5 | import type { OptionalDefaults } from '../wrappers/wrap' 6 | import type { GetSpaceEnvironmentParams } from '../../common-types' 7 | import type { RawAxiosRequestHeaders } from 'axios' 8 | 9 | export type SemanticRecommendationsPlainClientAPI = { 10 | /** 11 | * Retrieves Semantic Recommendations for the given entity ID. 12 | * @param params Parameters for getting the space and environment IDs. 13 | * @param payload Payload containing entity ID and optional filters. 14 | * @param headers Optional headers for the request. 15 | * @returns A promise that resolves to Semantic Recommendations. 16 | */ 17 | get( 18 | params: OptionalDefaults, 19 | payload: GetSemanticRecommendationsProps, 20 | headers?: Partial, 21 | ): Promise 22 | } 23 | -------------------------------------------------------------------------------- /lib/plain/entities/ai-action-invocation.ts: -------------------------------------------------------------------------------- 1 | import type { GetSpaceEnvironmentParams } from '../../common-types' 2 | import type { AiActionInvocationProps } from '../../entities/ai-action-invocation' 3 | import type { OptionalDefaults } from '../wrappers/wrap' 4 | import type { RawAxiosRequestHeaders } from 'axios' 5 | 6 | export type AiActionInvocationPlainClientAPI = { 7 | /** 8 | * Fetches an AI Action Invocation. 9 | * @param params Entity IDs to identify the AI Action Invocation. 10 | * Must include spaceId, environmentId, aiActionId, and invocationId. 11 | * @param headers Optional headers for the request. 12 | * @returns A promise resolving with the AI Action Invocation. 13 | * @throws if the request fails or the AI Action Invocation is not found. 14 | */ 15 | get( 16 | params: OptionalDefaults< 17 | GetSpaceEnvironmentParams & { aiActionId: string; invocationId: string } 18 | >, 19 | headers?: Partial, 20 | ): Promise 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | permissions: 3 | contents: read 4 | 5 | on: 6 | push: 7 | branches: ['**'] 8 | 9 | jobs: 10 | build: 11 | uses: ./.github/workflows/build.yaml 12 | 13 | check: 14 | needs: build 15 | uses: ./.github/workflows/check.yaml 16 | 17 | test-demo-projects: 18 | needs: [build, check] 19 | uses: ./.github/workflows/test-demo-projects.yaml 20 | secrets: inherit 21 | 22 | test-integration: 23 | needs: [build, check, test-demo-projects] 24 | uses: ./.github/workflows/test-integration.yaml 25 | secrets: inherit 26 | 27 | release: 28 | if: github.event_name == 'push' && contains(fromJSON('["refs/heads/master", "refs/heads/beta", "refs/heads/canary", "refs/heads/dev"]'), github.ref) 29 | needs: [build, check, test-demo-projects, test-integration] 30 | permissions: 31 | contents: write 32 | id-token: write 33 | actions: read 34 | uses: ./.github/workflows/release.yaml 35 | secrets: 36 | VAULT_URL: ${{ secrets.VAULT_URL }} 37 | -------------------------------------------------------------------------------- /lib/entities/semantic-search.ts: -------------------------------------------------------------------------------- 1 | import { freezeSys, toPlainObject } from 'contentful-sdk-core' 2 | import copy from 'fast-copy' 3 | import type { DefaultElements, Link, MakeRequest, SemanticRequestFilter } from '../common-types' 4 | 5 | export type GetSemanticSearchProps = { 6 | query: string 7 | filter?: SemanticRequestFilter 8 | } 9 | 10 | export type SemanticSearchResult = { 11 | sys: { 12 | type: 'SemanticSearchResult' 13 | entity: Link<'Entry'> 14 | space: Link<'Space'> 15 | environment: Link<'Environment'> 16 | } 17 | } 18 | 19 | export type SemanticSearchProps = { 20 | sys: { 21 | type: 'Array' 22 | correlationId?: string 23 | } 24 | items: SemanticSearchResult[] 25 | } 26 | 27 | export interface SemanticSearch extends SemanticSearchProps, DefaultElements {} 28 | 29 | export function wrapSemanticSearch( 30 | _makeRequest: MakeRequest, 31 | data: SemanticSearchProps, 32 | ): SemanticSearch { 33 | const result = toPlainObject(copy(data)) 34 | return freezeSys(result) 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/check.yaml: -------------------------------------------------------------------------------- 1 | name: Run Checks 2 | 3 | on: 4 | workflow_call: 5 | 6 | jobs: 7 | lint: 8 | runs-on: ubuntu-latest 9 | 10 | permissions: 11 | contents: read 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: '22' 21 | cache: 'npm' 22 | 23 | - name: Install dependencies 24 | run: npm ci 25 | 26 | - name: Restore the build folders 27 | uses: actions/cache/restore@v4 28 | with: 29 | path: | 30 | dist 31 | key: build-cache-${{ github.run_id }}-${{ github.run_attempt }} 32 | 33 | - name: Run linter 34 | run: npm run lint 35 | 36 | - name: Check formatting 37 | run: npm run format:check 38 | 39 | - name: Run unit tests 40 | run: npm run test:unit:cover 41 | 42 | - name: Test package size 43 | run: npm run test:size 44 | -------------------------------------------------------------------------------- /test/integration/space-user-integration.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, beforeAll, afterAll } from 'vitest' 2 | import { expect } from 'vitest' 3 | import { getDefaultSpace, timeoutToCalmRateLimiting } from '../helpers' 4 | import { TestDefaults } from '../defaults' 5 | import type { Space } from '../../lib/export-types' 6 | 7 | const { userId } = TestDefaults 8 | 9 | describe('SpaceUser API', () => { 10 | let space: Space 11 | 12 | beforeAll(async () => { 13 | space = await getDefaultSpace() 14 | }) 15 | 16 | afterAll(timeoutToCalmRateLimiting) 17 | 18 | it('Gets users', async () => { 19 | const response = await space.getSpaceUsers() 20 | 21 | expect(response.sys).toBeTruthy() 22 | expect(response.items).toBeTruthy() 23 | expect(response.items[0].sys.type).toBe('User') 24 | }) 25 | 26 | it('Gets user by id', async () => { 27 | const response = await space.getSpaceUser(userId) 28 | 29 | expect(response.sys).toBeTruthy() 30 | expect(response.sys.id).toBe(userId) 31 | expect(response.sys.type).toBe('User') 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /test/unit/entities/personal-access-token.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test } from 'vitest' 2 | import { cloneMock } from '../mocks/entities' 3 | import setupMakeRequest from '../mocks/makeRequest' 4 | import { 5 | wrapPersonalAccessToken, 6 | wrapPersonalAccessTokenCollection, 7 | } from '../../../lib/entities/personal-access-token' 8 | import { 9 | entityCollectionWrappedTest, 10 | entityWrappedTest, 11 | } from '../test-creators/instance-entity-methods' 12 | 13 | function setup(promise) { 14 | return { 15 | makeRequest: setupMakeRequest(promise), 16 | entityMock: cloneMock('personalAccessToken'), 17 | } 18 | } 19 | 20 | describe('Entity PersonalAccessToken', () => { 21 | test('personalAccessToken is wrapped', async () => { 22 | return entityWrappedTest(setup, { 23 | wrapperMethod: wrapPersonalAccessToken, 24 | }) 25 | }) 26 | 27 | test('personalAccessToken collection is wrapped', async () => { 28 | return entityCollectionWrappedTest(setup, { 29 | wrapperMethod: wrapPersonalAccessTokenCollection, 30 | }) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /test/unit/entities/upload.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test } from 'vitest' 2 | import { cloneMock } from '../mocks/entities' 3 | import setupMakeRequest from '../mocks/makeRequest' 4 | import { wrapUpload } from '../../../lib/entities/upload' 5 | import { 6 | entityDeleteTest, 7 | entityWrappedTest, 8 | failingActionTest, 9 | } from '../test-creators/instance-entity-methods' 10 | 11 | function setup(promise) { 12 | return { 13 | makeRequest: setupMakeRequest(promise), 14 | entityMock: cloneMock('upload'), 15 | } 16 | } 17 | 18 | describe('Entity Uploads', () => { 19 | test('Upload is wrapped', async () => { 20 | return entityWrappedTest(setup, { 21 | wrapperMethod: wrapUpload, 22 | }) 23 | }) 24 | 25 | test('Upload delete', async () => { 26 | return entityDeleteTest(setup, { 27 | wrapperMethod: wrapUpload, 28 | }) 29 | }) 30 | 31 | test('Upload delete fails', async () => { 32 | return failingActionTest(setup, { 33 | wrapperMethod: wrapUpload, 34 | actionMethod: 'delete', 35 | }) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /lib/entities/space-member.ts: -------------------------------------------------------------------------------- 1 | import { freezeSys, toPlainObject } from 'contentful-sdk-core' 2 | import copy from 'fast-copy' 3 | import type { DefaultElements, MakeRequest, MetaLinkProps, MetaSysProps } from '../common-types' 4 | import { wrapCollection } from '../common-utils' 5 | 6 | export type SpaceMemberProps = { 7 | sys: MetaSysProps 8 | /** 9 | * User is an admin 10 | */ 11 | admin: boolean 12 | /** 13 | * Array of Role Links 14 | */ 15 | roles: { sys: MetaLinkProps }[] 16 | } 17 | 18 | export interface SpaceMember extends SpaceMemberProps, DefaultElements {} 19 | 20 | /** 21 | * @private 22 | * @param makeRequest - function to make requests via an adapter 23 | * @param data - Raw space member data 24 | * @return Wrapped space member data 25 | */ 26 | export function wrapSpaceMember(_makeRequest: MakeRequest, data: SpaceMemberProps) { 27 | const spaceMember = toPlainObject(copy(data)) 28 | return freezeSys(spaceMember) 29 | } 30 | 31 | /** 32 | * @private 33 | */ 34 | export const wrapSpaceMemberCollection = wrapCollection(wrapSpaceMember) 35 | -------------------------------------------------------------------------------- /test/unit/entities/app-details.test.ts: -------------------------------------------------------------------------------- 1 | import { wrapAppDetails } from '../../../lib/entities/app-details' 2 | import { 3 | entityWrappedTest, 4 | entityDeleteTest, 5 | failingActionTest, 6 | } from '../test-creators/instance-entity-methods' 7 | import { appDetailsMock } from '../mocks/entities' 8 | import { describe, test } from 'vitest' 9 | import setupMakeRequest from '../mocks/makeRequest' 10 | 11 | function setup(promise) { 12 | return { 13 | makeRequest: setupMakeRequest(promise), 14 | entityMock: appDetailsMock, 15 | } 16 | } 17 | 18 | describe('App Details', () => { 19 | test('Details is wrapped', async () => { 20 | return entityWrappedTest(setup, { 21 | wrapperMethod: wrapAppDetails, 22 | }) 23 | }) 24 | 25 | test('Details delete', async () => { 26 | return entityDeleteTest(setup, { 27 | wrapperMethod: wrapAppDetails, 28 | }) 29 | }) 30 | 31 | test('Details delete fails', async () => { 32 | return failingActionTest(setup, { 33 | wrapperMethod: wrapAppDetails, 34 | actionMethod: 'delete', 35 | }) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /lib/adapters/REST/endpoints/usage.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosInstance } from 'contentful-sdk-core' 2 | import type { CollectionProp, QueryParams } from '../../../common-types' 3 | import type { UsageProps } from '../../../entities/usage' 4 | import type { RestEndpoint } from '../types' 5 | import * as raw from './raw' 6 | 7 | export const getManyForSpace: RestEndpoint<'Usage', 'getManyForSpace'> = ( 8 | http: AxiosInstance, 9 | params: { organizationId: string } & QueryParams, 10 | ) => { 11 | return raw.get>( 12 | http, 13 | `/organizations/${params.organizationId}/space_periodic_usages`, 14 | { 15 | params: params.query, 16 | }, 17 | ) 18 | } 19 | 20 | export const getManyForOrganization: RestEndpoint<'Usage', 'getManyForOrganization'> = ( 21 | http: AxiosInstance, 22 | params: { organizationId: string } & QueryParams, 23 | ) => { 24 | return raw.get>( 25 | http, 26 | `/organizations/${params.organizationId}/organization_periodic_usages`, 27 | { 28 | params: params.query, 29 | }, 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /test/integration/user-integration.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, beforeAll, expect, afterAll } from 'vitest' 2 | import { getTestOrganization, timeoutToCalmRateLimiting } from '../helpers' 3 | import { TestDefaults } from '../defaults' 4 | import type { Organization } from '../../lib/export-types' 5 | 6 | const { userId } = TestDefaults 7 | 8 | describe('User API', () => { 9 | let organization: Organization 10 | 11 | beforeAll(async () => { 12 | organization = await getTestOrganization() 13 | }) 14 | 15 | afterAll(timeoutToCalmRateLimiting) 16 | 17 | it('Gets organization users', async () => { 18 | const response = await organization.getUsers() 19 | 20 | expect(response.sys).toBeTruthy() 21 | expect(response.items).toBeTruthy() 22 | expect(response.items[0].sys.type).toBe('User') 23 | }) 24 | 25 | it('Gets organization user by ID', async () => { 26 | const response = await organization.getUser(userId) 27 | 28 | expect(response.sys).toBeTruthy() 29 | expect(response.sys.id).toBe(userId) 30 | expect(response.sys.type).toBe('User') 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /lib/adapters/REST/types.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosInstance } from 'contentful-sdk-core' 2 | import type { MRActions, MROpts, MRReturn } from '../../common-types' 3 | 4 | /** 5 | * @private 6 | */ 7 | export type RestEndpoint< 8 | ET extends keyof MRActions, 9 | Action extends keyof MRActions[ET], 10 | Params = 'params' extends keyof MROpts 11 | ? MROpts['params'] 12 | : undefined, 13 | Payload = 'payload' extends keyof MROpts 14 | ? MROpts['payload'] 15 | : undefined, 16 | Headers = 'headers' extends keyof MROpts 17 | ? MROpts['headers'] 18 | : undefined, 19 | Return = MRReturn, 20 | > = Params extends undefined 21 | ? (http: AxiosInstance) => Return 22 | : Payload extends undefined 23 | ? (http: AxiosInstance, params: Params) => Return 24 | : Headers extends undefined 25 | ? (http: AxiosInstance, params: Params, payload: Payload) => Return 26 | : (http: AxiosInstance, params: Params, payload: Payload, headers: Headers) => Return 27 | -------------------------------------------------------------------------------- /test/unit/entities/app-bundle.test.ts: -------------------------------------------------------------------------------- 1 | import { cloneMock } from '../mocks/entities' 2 | import setupMakeRequest from '../mocks/makeRequest' 3 | import { wrapAppBundle, wrapAppBundleCollection } from '../../../lib/entities/app-bundle' 4 | import { 5 | entityCollectionWrappedTest, 6 | entityWrappedTest, 7 | entityDeleteTest, 8 | } from '../test-creators/instance-entity-methods' 9 | import { describe, test } from 'vitest' 10 | 11 | function setup(promise) { 12 | return { 13 | makeRequest: setupMakeRequest(promise), 14 | entityMock: cloneMock('appBundle'), 15 | } 16 | } 17 | 18 | describe('Entity AppBundle', () => { 19 | test('AppBundle is wrapped', async () => { 20 | return entityWrappedTest(setup, { wrapperMethod: wrapAppBundle }) 21 | }) 22 | 23 | test('AppBundle collection is wrapped', async () => { 24 | return entityCollectionWrappedTest(setup, { 25 | wrapperMethod: wrapAppBundleCollection, 26 | }) 27 | }) 28 | 29 | test('AppBundle delete', async () => { 30 | return entityDeleteTest(setup, { 31 | wrapperMethod: wrapAppBundle, 32 | }) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /test/unit/entities/workflows-changelog-entry.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test } from 'vitest' 2 | import { 3 | wrapWorkflowsChangelogEntry, 4 | wrapWorkflowsChangelogEntryCollection, 5 | } from '../../../lib/entities/workflows-changelog-entry' 6 | import { cloneMock } from '../mocks/entities' 7 | import setupMakeRequest from '../mocks/makeRequest' 8 | import { 9 | entityCollectionWrappedTest, 10 | entityWrappedTest, 11 | } from '../test-creators/instance-entity-methods' 12 | 13 | function setup(promise) { 14 | return { 15 | makeRequest: setupMakeRequest(promise), 16 | entityMock: cloneMock('workflowsChangelogEntry'), 17 | } 18 | } 19 | 20 | describe('Entity WorkflowsChangelogEntry', () => { 21 | test('WorkflowsChangelogEntry is wrapped', async () => { 22 | return entityWrappedTest(setup, { 23 | wrapperMethod: wrapWorkflowsChangelogEntry, 24 | }) 25 | }) 26 | 27 | test('WorkflowsChangelogEntry collection is wrapped', async () => { 28 | return entityCollectionWrappedTest(setup, { 29 | wrapperMethod: wrapWorkflowsChangelogEntryCollection, 30 | }) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /test/unit/entities/space.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from 'vitest' 2 | import { cloneMock, mockCollection, spaceMock } from '../mocks/entities' 3 | import type { Space } from '../../../lib/entities/space' 4 | import { wrapSpace, wrapSpaceCollection } from '../../../lib/entities/space' 5 | import setupMakeRequest from '../mocks/makeRequest' 6 | 7 | function setup(promise) { 8 | return { 9 | makeRequest: setupMakeRequest(promise), 10 | entityMock: cloneMock('space'), 11 | } 12 | } 13 | describe('Entity Space', () => { 14 | test('Space is wrapped', async () => { 15 | const { makeRequest } = setup(Promise.resolve) 16 | const wrappedSpace = wrapSpace(makeRequest, spaceMock) 17 | expect(wrappedSpace.toPlainObject()).eql(spaceMock) 18 | }) 19 | 20 | test('Space collection is wrapped', async () => { 21 | const { makeRequest } = setup(Promise.resolve) 22 | const spaceCollection = mockCollection(spaceMock) 23 | const wrappedSpace = wrapSpaceCollection(makeRequest, spaceCollection) 24 | expect(wrappedSpace.toPlainObject()).eql(spaceCollection) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /lib/plain/entities/semantic-reference-suggestions.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | GetSemanticReferenceSuggestionsProps, 3 | SemanticReferenceSuggestionsProps, 4 | } from '../../entities/semantic-reference-suggestions' 5 | import type { OptionalDefaults } from '../wrappers/wrap' 6 | import type { GetSpaceEnvironmentParams } from '../../common-types' 7 | import type { RawAxiosRequestHeaders } from 'axios' 8 | 9 | export type SemanticReferenceSuggestionsPlainClientAPI = { 10 | /** 11 | * Retrieves Semantic Reference Suggestions for the given entity ID and its reference field ID. 12 | * @param params Parameters for getting the space and environment IDs. 13 | * @param payload Payload containing entity ID, reference field ID and optional filters. 14 | * @param headers Optional headers for the request. 15 | * @returns A promise that resolves to Semantic Reference Suggestions. 16 | */ 17 | get( 18 | params: OptionalDefaults, 19 | payload: GetSemanticReferenceSuggestionsProps, 20 | headers?: Partial, 21 | ): Promise 22 | } 23 | -------------------------------------------------------------------------------- /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-management.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('segpl12szpe6') 9 | }) 10 | 11 | // @todo reenable as soon version is injected properly again 12 | it.skip('Has correct user agent version', async () => { 13 | const clientVersion = await page.$eval('#version', (el) => el.innerHTML) 14 | 15 | // When we make a publish run, we need to ensure that semantic-release has set a valid package version 16 | if (process.env.PUBLISH_RUN === 'true') { 17 | expect(clientVersion).toEqual(expect.not.stringContaining('semantic-release')) 18 | expect(clientVersion).toEqual(packageVersion) 19 | } else { 20 | expect(clientVersion).toEqual(packageVersion) 21 | } 22 | console.log(`Client version: ${clientVersion}`) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /test/unit/entities/resource-type.test.ts: -------------------------------------------------------------------------------- 1 | import { cloneMock } from '../mocks/entities' 2 | import setupMakeRequest from '../mocks/makeRequest' 3 | import { 4 | entityActionTest, 5 | entityWrappedTest, 6 | entityDeleteTest, 7 | } from '../test-creators/instance-entity-methods' 8 | import { describe, it } from 'vitest' 9 | import { wrapResourceType, type ResourceTypeProps } from '../../../lib/entities/resource-type' 10 | 11 | function setup(promise: Promise) { 12 | return { 13 | makeRequest: setupMakeRequest(promise), 14 | entityMock: cloneMock('resourceType'), 15 | } 16 | } 17 | 18 | describe('Entity ResourceType', () => { 19 | it('ResourceType is wrapped', async () => { 20 | await entityWrappedTest(setup, { wrapperMethod: wrapResourceType }) 21 | }) 22 | 23 | it('ResourceType upsert', async () => { 24 | await entityActionTest(setup, { 25 | wrapperMethod: wrapResourceType, 26 | actionMethod: 'upsert', 27 | }) 28 | }) 29 | 30 | it('ResourceType delete', async () => { 31 | await entityDeleteTest(setup, { 32 | wrapperMethod: wrapResourceType, 33 | }) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/adapters/REST/endpoints/ui-config.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosInstance } from 'contentful-sdk-core' 2 | import type { SetOptional } from 'type-fest' 3 | import type { GetUIConfigParams } from '../../../common-types' 4 | import type { UIConfigProps } from '../../../entities/ui-config' 5 | import type { RestEndpoint } from '../types' 6 | import * as raw from './raw' 7 | import copy from 'fast-copy' 8 | 9 | const getUrl = (params: GetUIConfigParams) => 10 | `/spaces/${params.spaceId}/environments/${params.environmentId}/ui_config` 11 | 12 | export const get: RestEndpoint<'UIConfig', 'get'> = ( 13 | http: AxiosInstance, 14 | params: GetUIConfigParams, 15 | ) => { 16 | return raw.get(http, getUrl(params)) 17 | } 18 | 19 | export const update: RestEndpoint<'UIConfig', 'update'> = ( 20 | http: AxiosInstance, 21 | params: GetUIConfigParams, 22 | rawData: UIConfigProps, 23 | ) => { 24 | const data: SetOptional = copy(rawData) 25 | delete data.sys 26 | return raw.put(http, getUrl(params), data, { 27 | headers: { 28 | 'X-Contentful-Version': rawData.sys.version ?? 0, 29 | }, 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /lib/entities/semantic-recommendations.ts: -------------------------------------------------------------------------------- 1 | import { freezeSys, toPlainObject } from 'contentful-sdk-core' 2 | import copy from 'fast-copy' 3 | import type { DefaultElements, Link, MakeRequest, SemanticRequestFilter } from '../common-types' 4 | 5 | export type GetSemanticRecommendationsProps = { 6 | entityId: string 7 | filter?: SemanticRequestFilter 8 | } 9 | 10 | export type SemanticRecommendationsResult = { 11 | sys: { 12 | type: 'SemanticRecommendationsResult' 13 | entity: Link<'Entry'> 14 | space: Link<'Space'> 15 | environment: Link<'Environment'> 16 | } 17 | } 18 | 19 | export type SemanticRecommendationsProps = { 20 | sys: { 21 | type: 'Array' 22 | correlationId?: string 23 | } 24 | items: SemanticRecommendationsResult[] 25 | } 26 | 27 | export interface SemanticRecommendations 28 | extends SemanticRecommendationsProps, 29 | DefaultElements {} 30 | 31 | export function wrapSemanticRecommendations( 32 | _makeRequest: MakeRequest, 33 | data: SemanticRecommendationsProps, 34 | ): SemanticRecommendations { 35 | const result = toPlainObject(copy(data)) 36 | return freezeSys(result) 37 | } 38 | -------------------------------------------------------------------------------- /test/defaults.ts: -------------------------------------------------------------------------------- 1 | export const TestDefaults = { 2 | spaceId: 'segpl12szpe6', 3 | spaceWithAliasesAndEmbargoedAssetsId: '6mqcevu5a50r', 4 | environmentId: 'master', 5 | contentType: { 6 | withCrossSpaceReferenceId: 'test-content-type33324244', 7 | }, 8 | entry: { 9 | testEntryId: '3Z76Riu91MIwPWIMG93LNb', 10 | /** Used in the entry references specs */ 11 | testEntryReferenceId: '3ZgkmNQJxGjO9TUcnDgNQC', 12 | /** Used in Release specs */ 13 | testEntryReleasesId: '7pw3xjgbpE41JJrenBessW', 14 | /** Used in BulkAction specs */ 15 | testEntryBulkActionId: '6THqkC8vpDqAFtomRQazPu', 16 | }, 17 | userId: '0DdCYLZI33Oe1uZ0FLpCzw', 18 | userEmail: 'for_tests_contentful-management.js@contentful.com', 19 | teamId: '4XQ2AGpitUAECQf7EPP392', 20 | teamName: '[FOR TESTS] contentful-management.js', 21 | teamMembershipId: '6vuRqhFc875EEIJ50lSMYG', 22 | spaceMemberId: '0PCYk22mt1xD7gTKZhHycN', 23 | teamSpaceMembershipId: '3cf4vmSEj6zQ0aw78EOCqj', 24 | organizationSpaceMembershipId: '1nYAKCEkzkKmioitxMb1Vd', 25 | organizationMembershipId: '0DeAVut1GiI2T1oj1nje4i', 26 | organizationMembershipId2: '5EpPPjxx4v23D5J92uaI8m', 27 | } 28 | -------------------------------------------------------------------------------- /test/unit/entities/app-signing-secret.test.ts: -------------------------------------------------------------------------------- 1 | import { wrapAppSigningSecret } from '../../../lib/entities/app-signing-secret' 2 | import { 3 | entityWrappedTest, 4 | entityDeleteTest, 5 | failingActionTest, 6 | } from '../test-creators/instance-entity-methods' 7 | import { appSigningSecretMock } from '../mocks/entities' 8 | import { describe, test } from 'vitest' 9 | import setupMakeRequest from '../mocks/makeRequest' 10 | 11 | function setup(promise) { 12 | return { 13 | makeRequest: setupMakeRequest(promise), 14 | entityMock: appSigningSecretMock, 15 | } 16 | } 17 | 18 | describe('App SigningSecret', () => { 19 | test('SigningSecret is wrapped', async () => { 20 | return entityWrappedTest(setup, { 21 | wrapperMethod: wrapAppSigningSecret, 22 | }) 23 | }) 24 | 25 | test('SigningSecret delete', async () => { 26 | return entityDeleteTest(setup, { 27 | wrapperMethod: wrapAppSigningSecret, 28 | }) 29 | }) 30 | 31 | test('SigningSecret delete fails', async () => { 32 | return failingActionTest(setup, { 33 | wrapperMethod: wrapAppSigningSecret, 34 | actionMethod: 'delete', 35 | }) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /lib/methods/release-action.ts: -------------------------------------------------------------------------------- 1 | import type { ReleaseActionProps, ReleaseActionTypes } from '../entities/release-action' 2 | import type { PlainClientAPI } from '../plain/common-types' 3 | import type { AsyncActionProcessingOptions } from './action' 4 | import { pollAsyncActionStatus } from './action' 5 | 6 | type PlainOptions = { 7 | /** Used by the PlainClient to perform a poll for the BulkAction status */ 8 | plainClient: PlainClientAPI 9 | spaceId: string 10 | environmentId: string 11 | releaseId: string 12 | actionId: string 13 | } 14 | 15 | /** Waits for a ReleaseAction status to be either succeeded or failed. 16 | * Used by the Plain client */ 17 | export async function waitForReleaseActionProcessing( 18 | { plainClient, spaceId, environmentId, releaseId, actionId }: PlainOptions, 19 | options?: AsyncActionProcessingOptions, 20 | ): Promise> { 21 | return pollAsyncActionStatus( 22 | async () => 23 | plainClient.releaseAction.get({ 24 | releaseId, 25 | spaceId, 26 | environmentId, 27 | actionId, 28 | }), 29 | options, 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | 5 | Contentful Logo 6 | 7 |

8 | 9 |

Content Management API

10 | 11 |

JavaScript

12 | 13 |

14 | Readme · 15 | Setup · 16 | Migration · 17 | Changelog · 18 | Contributing 19 |

20 | 21 |

22 | 23 | Join Contentful Community Slack 24 | 25 |

26 | 27 | 28 | 29 | # CHANGELOG 30 | 31 | The changelog is automatically updated using 32 | [semantic-release](https://github.com/semantic-release/semantic-release). You 33 | can see it on the [releases page](https://github.com/contentful/contentful-management.js/releases). 34 | -------------------------------------------------------------------------------- /test/integration/space-member-integration.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, beforeAll, afterAll } from 'vitest' 2 | import { expect } from 'vitest' 3 | import { getDefaultSpace, timeoutToCalmRateLimiting } from '../helpers' 4 | import { TestDefaults } from '../defaults' 5 | import type { Space } from '../../lib/export-types' 6 | 7 | const { spaceId, userId } = TestDefaults 8 | 9 | describe('SpaceMember API', () => { 10 | let space: Space 11 | 12 | beforeAll(async () => { 13 | space = await getDefaultSpace() 14 | }) 15 | 16 | afterAll(timeoutToCalmRateLimiting) 17 | 18 | it('Gets space members', async () => { 19 | const response = await space.getSpaceMembers() 20 | 21 | expect(response.sys).toBeTruthy() 22 | expect(response.sys.type).toBe('Array') 23 | expect(response.items).toBeTruthy() 24 | expect(response.items[0].sys.type).toBe('SpaceMember') 25 | }) 26 | 27 | it('Gets space member by userId', async () => { 28 | const response = await space.getSpaceMember(userId) 29 | 30 | expect(response.sys).toBeTruthy() 31 | expect(response.sys.type).toBe('SpaceMember') 32 | expect(response.sys.id).toBe(`${spaceId}-${userId}`) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /lib/entities/semantic-duplicates.ts: -------------------------------------------------------------------------------- 1 | import { freezeSys, toPlainObject } from 'contentful-sdk-core' 2 | import copy from 'fast-copy' 3 | import type { DefaultElements, Link, MakeRequest, SemanticRequestFilter } from '../common-types' 4 | 5 | export type DuplicateLabel = 'high' | 'medium' | 'low' 6 | 7 | export type GetSemanticDuplicatesProps = { 8 | entityId: string 9 | filter?: SemanticRequestFilter 10 | } 11 | 12 | export type SemanticDuplicatesResult = { 13 | sys: { 14 | type: 'SemanticDuplicatesResult' 15 | entity: Link<'Entry'> 16 | space: Link<'Space'> 17 | environment: Link<'Environment'> 18 | } 19 | label: DuplicateLabel 20 | } 21 | 22 | export type SemanticDuplicatesProps = { 23 | sys: { 24 | type: 'Array' 25 | correlationId?: string 26 | } 27 | items: SemanticDuplicatesResult[] 28 | } 29 | 30 | export interface SemanticDuplicates 31 | extends SemanticDuplicatesProps, 32 | DefaultElements {} 33 | 34 | export function wrapSemanticDuplicates( 35 | _makeRequest: MakeRequest, 36 | data: SemanticDuplicatesProps, 37 | ): SemanticDuplicates { 38 | const result = toPlainObject(copy(data)) 39 | return freezeSys(result) 40 | } 41 | -------------------------------------------------------------------------------- /lib/adapters/REST/endpoints/user-ui-config.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosInstance } from 'contentful-sdk-core' 2 | import copy from 'fast-copy' 3 | import type { SetOptional } from 'type-fest' 4 | import type { GetUserUIConfigParams } from '../../../common-types' 5 | import type { UserUIConfigProps } from '../../../entities/user-ui-config' 6 | import type { RestEndpoint } from '../types' 7 | import * as raw from './raw' 8 | 9 | const getUrl = (params: GetUserUIConfigParams) => 10 | `/spaces/${params.spaceId}/environments/${params.environmentId}/ui_config/me` 11 | 12 | export const get: RestEndpoint<'UserUIConfig', 'get'> = ( 13 | http: AxiosInstance, 14 | params: GetUserUIConfigParams, 15 | ) => { 16 | return raw.get(http, getUrl(params)) 17 | } 18 | 19 | export const update: RestEndpoint<'UserUIConfig', 'update'> = ( 20 | http: AxiosInstance, 21 | params: GetUserUIConfigParams, 22 | rawData: UserUIConfigProps, 23 | ) => { 24 | const data: SetOptional = copy(rawData) 25 | delete data.sys 26 | return raw.put(http, getUrl(params), data, { 27 | headers: { 28 | 'X-Contentful-Version': rawData.sys.version ?? 0, 29 | }, 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /test/unit/entities/tag.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test } from 'vitest' 2 | import { wrapTag } from '../../../lib/entities/tag' 3 | import setupMakeRequest from '../mocks/makeRequest' 4 | import { cloneMock } from '../mocks/entities' 5 | import { 6 | entityDeleteTest, 7 | entityUpdateTest, 8 | failingActionTest, 9 | } from '../test-creators/instance-entity-methods' 10 | 11 | function setup(promise) { 12 | return { 13 | makeRequest: setupMakeRequest(promise), 14 | entityMock: cloneMock('tag'), 15 | } 16 | } 17 | 18 | describe('Entity Tag', () => { 19 | test('Tag update', async () => { 20 | return entityUpdateTest(setup, { 21 | wrapperMethod: wrapTag, 22 | }) 23 | }) 24 | 25 | test('Tag update fails', async () => { 26 | return failingActionTest(setup, { 27 | wrapperMethod: wrapTag, 28 | actionMethod: 'update', 29 | }) 30 | }) 31 | 32 | test('Tag delete', async () => { 33 | return entityDeleteTest(setup, { 34 | wrapperMethod: wrapTag, 35 | }) 36 | }) 37 | 38 | test('Tag delete fails', async () => { 39 | return failingActionTest(setup, { 40 | wrapperMethod: wrapTag, 41 | actionMethod: 'delete', 42 | }) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /.github/workflows/test-integration.yaml: -------------------------------------------------------------------------------- 1 | name: Run Checks 2 | 3 | on: 4 | workflow_call: 5 | secrets: 6 | CONTENTFUL_INTEGRATION_TEST_CMA_TOKEN: 7 | required: true 8 | CONTENTFUL_ORGANIZATION_ID: 9 | required: true 10 | 11 | jobs: 12 | test-integration: 13 | runs-on: ubuntu-latest 14 | 15 | permissions: 16 | contents: read 17 | 18 | steps: 19 | - name: Checkout code 20 | uses: actions/checkout@v5 21 | 22 | - name: Setup Node.js 23 | uses: actions/setup-node@v6 24 | with: 25 | node-version: '22' 26 | cache: 'npm' 27 | 28 | - name: Install dependencies 29 | run: npm ci 30 | 31 | - name: Restore the build folders 32 | uses: actions/cache/restore@v4 33 | with: 34 | path: | 35 | dist 36 | key: build-cache-${{ github.run_id }}-${{ github.run_attempt }} 37 | 38 | - name: Run integration tests 39 | run: npm run test:integration 40 | env: 41 | CONTENTFUL_INTEGRATION_TEST_CMA_TOKEN: ${{ secrets.CONTENTFUL_INTEGRATION_TEST_CMA_TOKEN }} 42 | CONTENTFUL_ORGANIZATION_ID: ${{ secrets.CONTENTFUL_ORGANIZATION_ID }} 43 | -------------------------------------------------------------------------------- /test/unit/entities/app-event-subscription.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test } from 'vitest' 2 | import { wrapAppEventSubscription } from '../../../lib/entities/app-event-subscription' 3 | import { appEventSubscriptionMock } from '../mocks/entities' 4 | import setupMakeRequest from '../mocks/makeRequest' 5 | import { 6 | entityDeleteTest, 7 | entityWrappedTest, 8 | failingActionTest, 9 | } from '../test-creators/instance-entity-methods' 10 | 11 | function setup(promise) { 12 | return { 13 | makeRequest: setupMakeRequest(promise), 14 | entityMock: appEventSubscriptionMock, 15 | } 16 | } 17 | 18 | describe('AppEventSubscription', () => { 19 | test('EventSubscription is wrapped', async () => { 20 | return entityWrappedTest(setup, { 21 | wrapperMethod: wrapAppEventSubscription, 22 | }) 23 | }) 24 | 25 | test('AppEventSubscription delete', async () => { 26 | return entityDeleteTest(setup, { 27 | wrapperMethod: wrapAppEventSubscription, 28 | }) 29 | }) 30 | 31 | test('AppEventSubscription delete fails', async () => { 32 | return failingActionTest(setup, { 33 | wrapperMethod: wrapAppEventSubscription, 34 | actionMethod: 'delete', 35 | }) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /test/unit/entities/organization.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from 'vitest' 2 | import { cloneMock, mockCollection, organizationMock } from '../mocks/entities' 3 | import type { Organization } from '../../../lib/entities/organization' 4 | import { wrapOrganization, wrapOrganizationCollection } from '../../../lib/entities/organization' 5 | import setupMakeRequest from '../mocks/makeRequest' 6 | 7 | function setup(promise) { 8 | return { 9 | makeRequest: setupMakeRequest(promise), 10 | entityMock: cloneMock('organization'), 11 | } 12 | } 13 | 14 | describe('Entity Organization', () => { 15 | test('Organization is wrapped', async () => { 16 | const { makeRequest } = setup(Promise.resolve) 17 | const wrappedOrg = wrapOrganization(makeRequest, organizationMock) 18 | expect(wrappedOrg.toPlainObject()).eql(organizationMock) 19 | }) 20 | 21 | test('Organization collection is wrapped', async () => { 22 | const { makeRequest } = setup(Promise.resolve) 23 | const orgCollection = mockCollection(organizationMock) 24 | const wrappedOrg = wrapOrganizationCollection(makeRequest, orgCollection) 25 | expect(wrappedOrg.toPlainObject()).eql(orgCollection) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /.github/workflows/test-demo-projects.yaml: -------------------------------------------------------------------------------- 1 | name: Run Checks 2 | 3 | on: 4 | workflow_call: 5 | secrets: 6 | CONTENTFUL_INTEGRATION_TEST_CMA_TOKEN: 7 | required: true 8 | CONTENTFUL_ORGANIZATION_ID: 9 | required: true 10 | 11 | jobs: 12 | test-demo-projects: 13 | runs-on: ubuntu-latest 14 | 15 | permissions: 16 | contents: read 17 | 18 | steps: 19 | - name: Checkout code 20 | uses: actions/checkout@v5 21 | 22 | - name: Setup Node.js 23 | uses: actions/setup-node@v6 24 | with: 25 | node-version: '22' 26 | cache: 'npm' 27 | 28 | - name: Install dependencies 29 | run: npm ci 30 | 31 | - name: Restore the build folders 32 | uses: actions/cache/restore@v4 33 | with: 34 | path: | 35 | dist 36 | key: build-cache-${{ github.run_id }}-${{ github.run_attempt }} 37 | 38 | - name: Run integration tests 39 | run: npm run test:demo-projects 40 | env: 41 | CONTENTFUL_INTEGRATION_TEST_CMA_TOKEN: ${{ secrets.CONTENTFUL_INTEGRATION_TEST_CMA_TOKEN }} 42 | CONTENTFUL_ORGANIZATION_ID: ${{ secrets.CONTENTFUL_ORGANIZATION_ID }} 43 | -------------------------------------------------------------------------------- /lib/constants/editor-interface-defaults/editors-defaults.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT_EDITOR_ID, WidgetNamespace } from './types' 2 | 3 | export const EntryEditorWidgetTypes = { 4 | DEFAULT_EDITOR: { 5 | name: 'Editor', 6 | id: DEFAULT_EDITOR_ID, 7 | icon: 'Entry', 8 | }, 9 | REFERENCE_TREE: { 10 | name: 'References', 11 | id: 'reference-tree', 12 | icon: 'References', 13 | }, 14 | TAGS_EDITOR: { 15 | name: 'Tags', 16 | id: 'tags-editor', 17 | icon: 'Tags', 18 | }, 19 | } 20 | 21 | const DefaultEntryEditor = { 22 | widgetId: EntryEditorWidgetTypes.DEFAULT_EDITOR.id, 23 | widgetNamespace: WidgetNamespace.EDITOR_BUILTIN, 24 | name: EntryEditorWidgetTypes.DEFAULT_EDITOR.name, 25 | } 26 | 27 | const ReferencesEntryEditor = { 28 | widgetId: EntryEditorWidgetTypes.REFERENCE_TREE.id, 29 | widgetNamespace: WidgetNamespace.EDITOR_BUILTIN, 30 | name: EntryEditorWidgetTypes.REFERENCE_TREE.name, 31 | } 32 | 33 | const TagsEditor = { 34 | widgetId: EntryEditorWidgetTypes.TAGS_EDITOR.id, 35 | widgetNamespace: WidgetNamespace.EDITOR_BUILTIN, 36 | name: EntryEditorWidgetTypes.TAGS_EDITOR.name, 37 | } 38 | 39 | export const EntryConfiguration = [DefaultEntryEditor, ReferencesEntryEditor, TagsEditor] 40 | -------------------------------------------------------------------------------- /lib/enhance-with-methods.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This method enhances a base object which would normally contain data, with 3 | * methods from another object that might work on manipulating that data. 4 | * All the added methods are set as non enumerable, non configurable, and non 5 | * writable properties. This ensures that if we try to clone or stringify the 6 | * base object, we don't have to worry about these additional methods. 7 | * @private 8 | * @param {object} baseObject - Base object with data 9 | * @param {object} methodsObject - Object with methods as properties. The key 10 | * values used here will be the same that will be defined on the baseObject. 11 | */ 12 | export default function enhanceWithMethods< 13 | B extends Record, 14 | M extends Record, 15 | >(baseObject: B, methodsObject: M): M & B { 16 | return Object.keys(methodsObject).reduce( 17 | (enhancedObject, methodName) => { 18 | Object.defineProperty(enhancedObject, methodName, { 19 | enumerable: false, 20 | configurable: true, 21 | writable: false, 22 | value: methodsObject[methodName], 23 | }) 24 | return enhancedObject 25 | }, 26 | baseObject as M & B, 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /test/unit/adapters/REST/reusable-tests/update.ts: -------------------------------------------------------------------------------- 1 | import setupRestAdapter from '../helpers/setupRestAdapter' 2 | import { cloneMock } from '../../../mocks/entities' 3 | import { expect, it } from 'vitest' 4 | 5 | export function reusableEntityUpdateTest(entityType, mockName) { 6 | it(`emits valid update request`, async () => { 7 | const entityMock = cloneMock(mockName) 8 | entityMock.name = 'updated name' 9 | entityMock.sys.version = 2 10 | 11 | const { adapterMock, httpMock } = setupRestAdapter(Promise.resolve({ data: entityMock })) 12 | 13 | await adapterMock.makeRequest({ 14 | entityType, 15 | action: 'update', 16 | params: {}, 17 | payload: entityMock, 18 | headers: { 'X-Test': 'test header' }, 19 | }) 20 | 21 | expect(httpMock.put.mock.calls[0][1].name).equals('updated name', 'data is sent') 22 | expect(httpMock.put.mock.calls[0][1].sys).equals(undefined, 'sys is removed') 23 | expect(httpMock.put.mock.calls[0][2].headers['X-Contentful-Version']).equals( 24 | 2, 25 | 'version header is sent', 26 | ) 27 | expect(httpMock.put.mock.calls[0][2].headers['X-Test']).equals( 28 | 'test header', 29 | 'custom header is set', 30 | ) 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /test/unit/entities/release.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'vitest' 2 | import { cloneMock } from '../mocks/entities' 3 | import setupMakeRequest from '../mocks/makeRequest' 4 | 5 | import { wrapRelease, wrapReleaseCollection } from '../../../lib/entities/release' 6 | import { 7 | entityWrappedTest, 8 | entityCollectionWrappedTest, 9 | entityDeleteTest, 10 | entityUpdateTest, 11 | } from '../test-creators/instance-entity-methods' 12 | 13 | function setup(promise) { 14 | return { 15 | makeRequest: setupMakeRequest(promise), 16 | entityMock: cloneMock('release'), 17 | } 18 | } 19 | 20 | describe('Entity Release', () => { 21 | it('Release is wrapped', async () => { 22 | return entityWrappedTest(setup, { wrapperMethod: wrapRelease }) 23 | }) 24 | 25 | it('Release collection is wrapped', async () => { 26 | return entityCollectionWrappedTest(setup, { 27 | wrapperMethod: wrapReleaseCollection, 28 | }) 29 | }) 30 | 31 | it('Release update', async () => { 32 | return entityUpdateTest(setup, { 33 | wrapperMethod: wrapRelease, 34 | }) 35 | }) 36 | 37 | it('Release delete', async () => { 38 | return entityDeleteTest(setup, { 39 | wrapperMethod: wrapRelease, 40 | }) 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /lib/entities/semantic-reference-suggestions.ts: -------------------------------------------------------------------------------- 1 | import { freezeSys, toPlainObject } from 'contentful-sdk-core' 2 | import copy from 'fast-copy' 3 | import type { DefaultElements, Link, MakeRequest, SemanticRequestFilter } from '../common-types' 4 | 5 | export type GetSemanticReferenceSuggestionsProps = { 6 | entityId: string 7 | referenceFieldId: string 8 | filter?: SemanticRequestFilter 9 | } 10 | 11 | export type SemanticReferenceSuggestionsResult = { 12 | sys: { 13 | type: 'SemanticReferenceSuggestionsResult' 14 | entity: Link<'Entry'> 15 | space: Link<'Space'> 16 | environment: Link<'Environment'> 17 | } 18 | } 19 | 20 | export type SemanticReferenceSuggestionsProps = { 21 | sys: { 22 | type: 'Array' 23 | correlationId?: string 24 | } 25 | items: SemanticReferenceSuggestionsResult[] 26 | } 27 | 28 | export interface SemanticReferenceSuggestions 29 | extends SemanticReferenceSuggestionsProps, 30 | DefaultElements {} 31 | 32 | export function wrapSemanticReferenceSuggestions( 33 | _makeRequest: MakeRequest, 34 | data: SemanticReferenceSuggestionsProps, 35 | ): SemanticReferenceSuggestions { 36 | const result = toPlainObject(copy(data)) 37 | return freezeSys(result) 38 | } 39 | -------------------------------------------------------------------------------- /test/unit/create-ui-config-api.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, afterEach, vi } from 'vitest' 2 | import createUIConfigApi from '../../lib/create-ui-config-api' 3 | import { wrapUIConfig } from '../../lib/entities/ui-config' 4 | import { cloneMock } from './mocks/entities' 5 | import setupMakeRequest from './mocks/makeRequest' 6 | import { entityUpdateTest, failingVersionActionTest } from './test-creators/instance-entity-methods' 7 | 8 | function setup(promise) { 9 | const makeRequest = setupMakeRequest(promise) 10 | const uiConfigMock = cloneMock('uiConfig') 11 | const api = createUIConfigApi(makeRequest) 12 | 13 | return { 14 | api: { 15 | ...api, 16 | toPlainObject: () => uiConfigMock, 17 | }, 18 | makeRequest, 19 | entityMock: uiConfigMock, 20 | } 21 | } 22 | 23 | describe('createUIConfigApi', () => { 24 | afterEach(() => { 25 | vi.restoreAllMocks() 26 | }) 27 | 28 | test('UIConfig update', async () => { 29 | await entityUpdateTest(setup, { 30 | wrapperMethod: wrapUIConfig, 31 | }) 32 | }) 33 | 34 | test('UIConfig update fails', async () => { 35 | await failingVersionActionTest(setup, { 36 | wrapperMethod: wrapUIConfig, 37 | actionMethod: 'update', 38 | }) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /test/unit/adapters/REST/endpoints/organization-membership.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from 'vitest' 2 | import { cloneMock } from '../../../mocks/entities' 3 | import { wrapOrganizationMembership } from '../../../../../lib/entities/organization-membership' 4 | import setupRestAdapter from '../helpers/setupRestAdapter' 5 | 6 | function setup(promise, params = {}) { 7 | return { 8 | ...setupRestAdapter(promise, params), 9 | entityMock: cloneMock('organizationMembership'), 10 | } 11 | } 12 | 13 | describe('Rest Organization Membership', () => { 14 | test('OrganizationMembership delete', async () => { 15 | const { httpMock, entityMock, adapterMock } = setup(Promise.resolve({})) 16 | entityMock.sys.version = 2 17 | const entity = wrapOrganizationMembership( 18 | (...args) => adapterMock.makeRequest(...args), 19 | entityMock, 20 | 'org-id', 21 | ) 22 | return entity.delete().then((response) => { 23 | expect(httpMock.delete.mock.calls[0][0]).equals( 24 | `/organizations/org-id/organization_memberships/${entityMock.sys.id}`, 25 | 'url is correct', 26 | ) 27 | return { 28 | httpMock, 29 | entityMock, 30 | response, 31 | } 32 | }) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a report to help us improve 4 | title: '[BUG] ' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | ## Bug Description 10 | 11 | A clear and concise description of what the bug is. 12 | 13 | ## Steps to Reproduce 14 | 15 | 1. Go to '...' 16 | 2. Execute '...' 17 | 3. See error 18 | 19 | ## Expected Behavior 20 | 21 | A clear and concise description of what you expected to happen. 22 | 23 | ## Actual Behavior 24 | 25 | A clear and concise description of what actually happened. 26 | 27 | ## Code Sample 28 | 29 | ```javascript 30 | // Minimal code to reproduce the issue 31 | ``` 32 | 33 | ## Environment 34 | 35 | - OS: [e.g. macOS 13.0, Windows 11, Ubuntu 22.04] 36 | - Package Version: [e.g. 1.2.3] 37 | - Node Version: [e.g. 18.0.0] 38 | - Package Manager: [e.g. npm 9.0.0, yarn 1.22.0] 39 | 40 | ## Error Messages/Logs 41 | 42 | ``` 43 | Paste any error messages or relevant logs here 44 | ``` 45 | 46 | ## Screenshots 47 | 48 | If applicable, add screenshots to help explain your problem. 49 | 50 | ## Additional Context 51 | 52 | Add any other context about the problem here. 53 | 54 | ## Possible Solution 55 | 56 | If you have suggestions on how to fix the bug, please describe them here. 57 | -------------------------------------------------------------------------------- /lib/entities/organization-invitation.ts: -------------------------------------------------------------------------------- 1 | import { freezeSys, toPlainObject } from 'contentful-sdk-core' 2 | import copy from 'fast-copy' 3 | import type { DefaultElements, MakeRequest, MetaLinkProps, MetaSysProps } from '../common-types' 4 | 5 | export type OrganizationInvitationProps = { 6 | sys: MetaSysProps & { 7 | organizationMembership: { sys: MetaLinkProps } 8 | user: Record | null 9 | invitationUrl: string 10 | status: string 11 | } 12 | firstName: string 13 | lastName: string 14 | email: string 15 | role: string 16 | } 17 | 18 | export type CreateOrganizationInvitationProps = Omit 19 | 20 | export interface OrganizationInvitation 21 | extends OrganizationInvitationProps, 22 | DefaultElements {} 23 | 24 | /** 25 | * @private 26 | * @param makeRequest - function to make requests via an adapter 27 | * @param data - Raw invitation data 28 | * @return {OrganizationInvitation} Wrapped Inviation data 29 | */ 30 | export function wrapOrganizationInvitation( 31 | _makeRequest: MakeRequest, 32 | data: OrganizationInvitationProps, 33 | ): OrganizationInvitation { 34 | const invitation = toPlainObject(copy(data)) 35 | return freezeSys(invitation) 36 | } 37 | -------------------------------------------------------------------------------- /test/unit/entities/app-upload.test.ts: -------------------------------------------------------------------------------- 1 | import { wrapAppUpload, wrapAppUploadCollection } from '../../../lib/entities/app-upload' 2 | import setupMakeRequest from '../mocks/makeRequest' 3 | import { describe, test } from 'vitest' 4 | import { 5 | entityCollectionWrappedTest, 6 | entityWrappedTest, 7 | failingActionTest, 8 | entityDeleteTest, 9 | } from '../test-creators/instance-entity-methods' 10 | import { appUploadMock } from '../mocks/entities' 11 | 12 | function setup(promise) { 13 | return { 14 | makeRequest: setupMakeRequest(promise), 15 | entityMock: appUploadMock, 16 | } 17 | } 18 | 19 | describe('Entity AppUpload', () => { 20 | test('AppUpload is wrapped', async () => { 21 | return entityWrappedTest(setup, { wrapperMethod: wrapAppUpload }) 22 | }) 23 | 24 | test('AppUpload collection is wrapped', async () => { 25 | return entityCollectionWrappedTest(setup, { wrapperMethod: wrapAppUploadCollection }) 26 | }) 27 | 28 | test('AppUpload delete', async () => { 29 | return entityDeleteTest(setup, { 30 | wrapperMethod: wrapAppUpload, 31 | }) 32 | }) 33 | 34 | test('AppUpload delete fails', async () => { 35 | return failingActionTest(setup, { wrapperMethod: wrapAppUpload, actionMethod: 'delete' }) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /test/unit/entities/environment.test.ts: -------------------------------------------------------------------------------- 1 | import { cloneMock, environmentMock, mockCollection } from '../mocks/entities' 2 | import type { EnvironmentProps } from '../../../lib/entities/environment' 3 | import { wrapEnvironment, wrapEnvironmentCollection } from '../../../lib/entities/environment' 4 | import { describe, test, expect } from 'vitest' 5 | import setupMakeRequest from '../mocks/makeRequest' 6 | 7 | function setup(promise) { 8 | return { 9 | makeRequest: setupMakeRequest(promise), 10 | entityMock: cloneMock('environment'), 11 | } 12 | } 13 | 14 | describe('Entity Environment', () => { 15 | test('Environment is wrapped', async () => { 16 | const { makeRequest } = setup(Promise.resolve()) 17 | const wrappedEnvironment = wrapEnvironment(makeRequest, environmentMock) 18 | expect(wrappedEnvironment.toPlainObject()).eql(environmentMock) 19 | }) 20 | 21 | test('Environment collection is wrapped', async () => { 22 | const { makeRequest } = setup(Promise.resolve()) 23 | const environmentCollection = mockCollection(environmentMock) 24 | const wrappedEnvironment = wrapEnvironmentCollection(makeRequest, environmentCollection) 25 | expect(wrappedEnvironment.toPlainObject()).eql(environmentCollection) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /lib/entities/concept.ts: -------------------------------------------------------------------------------- 1 | import type { Link } from '../common-types' 2 | import type { LocalizedEntity } from './utils' 3 | 4 | export type TaxonomyConceptLink = Link<'TaxonomyConcept'> 5 | 6 | type Concept = { 7 | uri: string | null 8 | prefLabel: string 9 | altLabels: string[] 10 | hiddenLabels: string[] 11 | definition: string | null 12 | editorialNote: string | null 13 | historyNote: string | null 14 | example: string | null 15 | note: string | null 16 | scopeNote: string | null 17 | notations: string[] 18 | broader: TaxonomyConceptLink[] 19 | related: TaxonomyConceptLink[] 20 | sys: { 21 | type: 'TaxonomyConcept' 22 | createdAt: string 23 | updatedAt: string 24 | id: string 25 | version: number 26 | createdBy: Link<'User'> 27 | updatedBy: Link<'User'> 28 | } 29 | } 30 | 31 | export type ConceptProps = LocalizedEntity< 32 | Omit, 33 | | 'prefLabel' 34 | | 'altLabels' 35 | | 'hiddenLabels' 36 | | 'definition' 37 | | 'historyNote' 38 | | 'editorialNote' 39 | | 'example' 40 | | 'note' 41 | | 'scopeNote', 42 | Locales 43 | > 44 | 45 | export type CreateConceptProps = Partial> & 46 | Pick 47 | -------------------------------------------------------------------------------- /lib/plain/entities/upload-credential.ts: -------------------------------------------------------------------------------- 1 | import type { GetSpaceEnvironmentParams, MetaSysProps } from '../../common-types' 2 | import type { OptionalDefaults } from '../wrappers/wrap' 3 | 4 | export type UploadCredential = { 5 | /** 6 | * System metadata 7 | */ 8 | sys: MetaSysProps & { 9 | type: 'UploadCredential' 10 | } 11 | 12 | /** 13 | * upload credentials 14 | */ 15 | uploadCredentials: { 16 | policy: string 17 | signature: string 18 | expiresAt: string 19 | createdAt: string 20 | } 21 | } 22 | 23 | export type UploadCredentialAPI = { 24 | /** Creates a Space Environment UploadCredential for Filestack Upload 25 | * 26 | * @param params Space Id and Environment Id to identify the Space Environment 27 | * @param data the Space Environment Upload 28 | * @returns the Space Environment Upload 29 | * @throws if the request fails, or the Space Environment is not found 30 | * @example 31 | * ```javascript 32 | * const credential = await client.uploadCredential.create( 33 | * { 34 | * spaceId: '', 35 | * environmentId: '', 36 | * } 37 | * ); 38 | * ``` 39 | */ 40 | create(params: OptionalDefaults): Promise 41 | } 42 | -------------------------------------------------------------------------------- /test/unit/entities/app-key.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test } from 'vitest' 2 | import { wrapAppKey, wrapAppKeyCollection } from '../../../lib/entities/app-key' 3 | import { appKeyMock } from '../mocks/entities' 4 | import setupMakeRequest from '../mocks/makeRequest' 5 | import { 6 | entityCollectionWrappedTest, 7 | entityDeleteTest, 8 | entityWrappedTest, 9 | failingActionTest, 10 | } from '../test-creators/instance-entity-methods' 11 | 12 | function setup(promise) { 13 | return { 14 | makeRequest: setupMakeRequest(promise), 15 | entityMock: appKeyMock, 16 | } 17 | } 18 | 19 | describe('AppKey', () => { 20 | test('Key is wrapped', async () => { 21 | return entityWrappedTest(setup, { 22 | wrapperMethod: wrapAppKey, 23 | }) 24 | }) 25 | 26 | test('AppKey collection is wrapped', async () => { 27 | return entityCollectionWrappedTest(setup, { 28 | wrapperMethod: wrapAppKeyCollection, 29 | }) 30 | }) 31 | 32 | test('AppKey delete', async () => { 33 | return entityDeleteTest(setup, { 34 | wrapperMethod: wrapAppKey, 35 | }) 36 | }) 37 | 38 | test('AppKey delete fails', async () => { 39 | return failingActionTest(setup, { 40 | wrapperMethod: wrapAppKey, 41 | actionMethod: 'delete', 42 | }) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /lib/adapters/REST/endpoints/vectorization-status.ts: -------------------------------------------------------------------------------- 1 | import type { RawAxiosRequestHeaders } from 'axios' 2 | import type { AxiosInstance } from 'contentful-sdk-core' 3 | import type { GetOrganizationParams } from '../../../common-types' 4 | import type { RestEndpoint } from '../types' 5 | import * as raw from './raw' 6 | import type { 7 | UpdateVectorizationStatusProps, 8 | VectorizationStatusProps, 9 | } from '../../../entities/vectorization-status' 10 | 11 | export const get: RestEndpoint<'VectorizationStatus', 'get'> = ( 12 | http: AxiosInstance, 13 | params: GetOrganizationParams, 14 | headers?: RawAxiosRequestHeaders, 15 | ) => { 16 | return raw.get( 17 | http, 18 | `/organizations/${params.organizationId}/semantic/vectorization-status`, 19 | { 20 | headers, 21 | }, 22 | ) 23 | } 24 | 25 | export const update: RestEndpoint<'VectorizationStatus', 'update'> = ( 26 | http: AxiosInstance, 27 | params: GetOrganizationParams, 28 | data: UpdateVectorizationStatusProps, 29 | headers?: RawAxiosRequestHeaders, 30 | ) => { 31 | return raw.post( 32 | http, 33 | `/organizations/${params.organizationId}/semantic/vectorization-status`, 34 | data, 35 | { 36 | headers, 37 | }, 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /test/unit/create-user-ui-config-api.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, afterEach, vi } from 'vitest' 2 | import createUIConfigApi from '../../lib/create-ui-config-api' 3 | import { wrapUIConfig } from '../../lib/entities/ui-config' 4 | import { cloneMock } from './mocks/entities' 5 | import setupMakeRequest from './mocks/makeRequest' 6 | import { entityUpdateTest, failingVersionActionTest } from './test-creators/instance-entity-methods' 7 | 8 | function setup(promise) { 9 | const makeRequest = setupMakeRequest(promise) 10 | const userUIConfigMock = cloneMock('userUIConfig') 11 | const api = createUIConfigApi(makeRequest) 12 | 13 | return { 14 | api: { 15 | ...api, 16 | toPlainObject: () => userUIConfigMock, 17 | }, 18 | makeRequest, 19 | entityMock: userUIConfigMock, 20 | } 21 | } 22 | 23 | describe('createUserUIConfigApi', () => { 24 | afterEach(() => { 25 | vi.restoreAllMocks() 26 | }) 27 | 28 | test('UserUIConfig update', async () => { 29 | await entityUpdateTest(setup, { 30 | wrapperMethod: wrapUIConfig, 31 | }) 32 | }) 33 | 34 | test('UserUIConfig update fails', async () => { 35 | await failingVersionActionTest(setup, { 36 | wrapperMethod: wrapUIConfig, 37 | actionMethod: 'update', 38 | }) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /test/unit/adapters/REST/endpoints/environment-template.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect } from 'vitest' 2 | import { cloneMock } from '../../../mocks/entities' 3 | import setupRestAdapter from '../helpers/setupRestAdapter' 4 | 5 | describe('Environment Template', async () => { 6 | const mockName = 'environmentTemplate' 7 | const entityType = 'EnvironmentTemplate' 8 | 9 | const actions = [ 10 | { name: 'create', httpMethod: 'post' }, 11 | { name: 'update', httpMethod: 'put' }, 12 | { name: 'versionUpdate', httpMethod: 'patch' }, 13 | { name: 'validate', httpMethod: 'put' }, 14 | { name: 'install', httpMethod: 'post' }, 15 | ] 16 | 17 | actions.forEach((action) => { 18 | it(`propagates custom headers for ${action.name} request`, async () => { 19 | const { adapterMock, httpMock } = setupRestAdapter(Promise.resolve({})) 20 | 21 | const entityMock = cloneMock(mockName) 22 | 23 | await adapterMock.makeRequest({ 24 | entityType, 25 | action: action.name, 26 | params: {}, 27 | payload: entityMock, 28 | headers: { 'X-Test': 'test header' }, 29 | userAgent: 'mockedAgent', 30 | }) 31 | 32 | expect(httpMock[action.httpMethod].mock.calls[0][2].headers['X-Test']).equals('test header') 33 | }) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /lib/adapters/REST/endpoints/app-details.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosInstance } from 'contentful-sdk-core' 2 | import type { AppDetailsProps, CreateAppDetailsProps } from '../../../entities/app-details' 3 | import * as raw from './raw' 4 | import type { RestEndpoint } from '../types' 5 | import type { GetAppDefinitionParams } from '../../../common-types' 6 | 7 | export const get: RestEndpoint<'AppDetails', 'get'> = ( 8 | http: AxiosInstance, 9 | params: GetAppDefinitionParams, 10 | ) => { 11 | return raw.get( 12 | http, 13 | `/organizations/${params.organizationId}/app_definitions/${params.appDefinitionId}/details`, 14 | ) 15 | } 16 | 17 | export const upsert: RestEndpoint<'AppDetails', 'upsert'> = ( 18 | http: AxiosInstance, 19 | params: GetAppDefinitionParams, 20 | data: CreateAppDetailsProps, 21 | ) => { 22 | return raw.put( 23 | http, 24 | `/organizations/${params.organizationId}/app_definitions/${params.appDefinitionId}/details`, 25 | data, 26 | ) 27 | } 28 | 29 | export const del: RestEndpoint<'AppDetails', 'delete'> = ( 30 | http: AxiosInstance, 31 | params: GetAppDefinitionParams, 32 | ) => { 33 | return raw.del( 34 | http, 35 | `/organizations/${params.organizationId}/app_definitions/${params.appDefinitionId}/details`, 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /lib/entities/preview-api-key.ts: -------------------------------------------------------------------------------- 1 | import { freezeSys, toPlainObject } from 'contentful-sdk-core' 2 | import copy from 'fast-copy' 3 | import type { DefaultElements, MakeRequest, MetaSysProps } from '../common-types' 4 | import { wrapCollection } from '../common-utils' 5 | import enhanceWithMethods from '../enhance-with-methods' 6 | 7 | export type PreviewApiKeyProps = { 8 | sys: MetaSysProps 9 | name: string 10 | description: string 11 | accessToken: string 12 | } 13 | 14 | export interface PreviewApiKey extends PreviewApiKeyProps, DefaultElements {} 15 | 16 | /** 17 | * @private 18 | */ 19 | function createPreviewApiKeyApi() { 20 | return {} 21 | } 22 | 23 | /** 24 | * @private 25 | * @param makeRequest - function to make requests via an adapter 26 | * @param data - Raw api key data 27 | * @return Wrapped preview api key data 28 | */ 29 | export function wrapPreviewApiKey( 30 | _makeRequest: MakeRequest, 31 | data: PreviewApiKeyProps, 32 | ): PreviewApiKey { 33 | const previewApiKey = toPlainObject(copy(data)) 34 | const previewApiKeyWithMethods = enhanceWithMethods(previewApiKey, createPreviewApiKeyApi()) 35 | return freezeSys(previewApiKeyWithMethods) 36 | } 37 | 38 | /** 39 | * @private 40 | */ 41 | export const wrapPreviewApiKeyCollection = wrapCollection(wrapPreviewApiKey) 42 | -------------------------------------------------------------------------------- /lib/entities/vectorization-status.ts: -------------------------------------------------------------------------------- 1 | import { freezeSys, toPlainObject } from 'contentful-sdk-core' 2 | import copy from 'fast-copy' 3 | import type { DefaultElements, Link, MakeRequest } from '../common-types' 4 | 5 | export enum EmbeddingSetStatus { 6 | ACTIVE = 'ACTIVE', 7 | PENDING = 'PENDING', 8 | ERROR = 'ERROR', 9 | DISABLED = 'DISABLED', 10 | DELETING = 'DELETING', 11 | } 12 | 13 | export type SpaceVectorizationStatus = { 14 | sys: { 15 | space: Link<'Space'> 16 | status: EmbeddingSetStatus 17 | type: 'VectorizationStatus' 18 | createdAt: string 19 | updatedAt: string 20 | disabledAt?: string 21 | } 22 | } 23 | 24 | export type VectorizationStatusProps = { 25 | sys: { 26 | type: 'Array' 27 | correlationId?: string 28 | } 29 | items: SpaceVectorizationStatus[] 30 | } 31 | 32 | export type UpdateVectorizationStatusProps = { 33 | spaceId: string 34 | enabled: boolean 35 | }[] 36 | 37 | export interface VectorizationStatus 38 | extends VectorizationStatusProps, 39 | DefaultElements {} 40 | 41 | export function wrapVectorizationStatus( 42 | _makeRequest: MakeRequest, 43 | data: VectorizationStatusProps, 44 | ): VectorizationStatus { 45 | const vectorizationStatus = toPlainObject(copy(data)) 46 | return freezeSys(vectorizationStatus) 47 | } 48 | -------------------------------------------------------------------------------- /test/unit/entities/extension.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test } from 'vitest' 2 | import { wrapExtension, wrapExtensionCollection } from '../../../lib/entities/extension' 3 | import { cloneMock } from '../mocks/entities' 4 | import setupMakeRequest from '../mocks/makeRequest' 5 | import { 6 | entityActionTest, 7 | entityCollectionWrappedTest, 8 | entityDeleteTest, 9 | entityWrappedTest, 10 | } from '../test-creators/instance-entity-methods' 11 | 12 | function setup(promise) { 13 | return { 14 | makeRequest: setupMakeRequest(promise), 15 | entityMock: cloneMock('extension'), 16 | } 17 | } 18 | 19 | describe('Entity Extension', () => { 20 | test('Extension is wrapped', async () => { 21 | return entityWrappedTest(setup, { 22 | wrapperMethod: wrapExtension, 23 | }) 24 | }) 25 | 26 | test('Extension collection is wrapped', async () => { 27 | return entityCollectionWrappedTest(setup, { 28 | wrapperMethod: wrapExtensionCollection, 29 | }) 30 | }) 31 | 32 | test('Extension update', async () => { 33 | return entityActionTest(setup, { 34 | wrapperMethod: wrapExtension, 35 | actionMethod: 'update', 36 | }) 37 | }) 38 | 39 | test('Extension delete', async () => { 40 | return entityDeleteTest(setup, { 41 | wrapperMethod: wrapExtension, 42 | }) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /lib/plain/entities/app-access-token.ts: -------------------------------------------------------------------------------- 1 | import type { GetAppInstallationParams } from '../../common-types' 2 | import type { 3 | AppAccessTokenProps, 4 | CreateAppAccessTokenProps, 5 | } from '../../entities/app-access-token' 6 | import type { OptionalDefaults } from '../wrappers/wrap' 7 | 8 | export type AppAccessTokenPlainClientAPI = { 9 | /** 10 | * Issue a token for an app installation in a space environment 11 | * @param params space, environment, and app definition IDs 12 | * @param payload the JWT to be used to issue the token 13 | * @returns the issued token, which can be cached until it expires 14 | * @throws if the request fails 15 | * @example 16 | * ```javascript 17 | * import { sign } from 'jsonwebtoken' 18 | * 19 | * const signOptions = { algorithm: 'RS256', issuer: '', expiresIn: '10m' } 20 | * 21 | * const { token } = await client.appAccessToken.create( 22 | * { 23 | * spaceId: '', 24 | * environmentId: '', 25 | * appDefinitionId: '', 26 | * }, { 27 | * jwt: sign({}, '', signOptions) 28 | * } 29 | * ); 30 | * ``` 31 | */ 32 | create( 33 | params: OptionalDefaults, 34 | payload: CreateAppAccessTokenProps, 35 | ): Promise 36 | } 37 | -------------------------------------------------------------------------------- /lib/entities/app-access-token.ts: -------------------------------------------------------------------------------- 1 | import copy from 'fast-copy' 2 | import { freezeSys, toPlainObject } from 'contentful-sdk-core' 3 | import type { Except } from 'type-fest' 4 | import type { BasicMetaSysProps, DefaultElements, MakeRequest, SysLink } from '../common-types' 5 | 6 | type AppAccessTokenSys = Except & { 7 | space: SysLink 8 | environment: SysLink 9 | appDefinition: SysLink 10 | expiresAt: string 11 | } 12 | 13 | export type AppAccessTokenProps = { 14 | /** 15 | * System metadata 16 | */ 17 | sys: AppAccessTokenSys 18 | /** 19 | * Token for an app installation in a space environment 20 | */ 21 | token: string 22 | } 23 | 24 | export type CreateAppAccessTokenProps = { 25 | /** 26 | * JSON Web Token 27 | */ 28 | jwt: string 29 | } 30 | 31 | export interface AppAccessToken extends AppAccessTokenProps, DefaultElements {} 32 | 33 | /** 34 | * @private 35 | * @param makeRequest - function to make requests via an adapter 36 | * @param data - Raw app access token data 37 | * @return {AppAccessToken} Wrapped AppAccessToken data 38 | */ 39 | export function wrapAppAccessToken( 40 | _makeRequest: MakeRequest, 41 | data: AppAccessTokenProps, 42 | ): AppAccessToken { 43 | const appAccessToken = toPlainObject(copy(data)) 44 | return freezeSys(appAccessToken) 45 | } 46 | -------------------------------------------------------------------------------- /test/integration/organization-space-membership-integration.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, test, expect, afterAll } from 'vitest' 2 | import type { Organization } from '../../lib/export-types' 3 | import { getTestOrganization, timeoutToCalmRateLimiting } from '../helpers' 4 | import { TestDefaults } from '../defaults' 5 | 6 | const { organizationSpaceMembershipId } = TestDefaults 7 | 8 | describe('OrganizationSpaceMembership Api', function () { 9 | let organization: Organization 10 | 11 | beforeAll(async () => { 12 | organization = await getTestOrganization() 13 | }) 14 | 15 | afterAll(timeoutToCalmRateLimiting) 16 | 17 | test('Gets organizationSpaceMemberships', async () => { 18 | return organization.getOrganizationSpaceMemberships().then((response) => { 19 | expect(response.sys, 'sys') 20 | expect(response.items, 'fields') 21 | }) 22 | }) 23 | 24 | test('Gets organizationSpaceMembership', async () => { 25 | return organization 26 | .getOrganizationSpaceMembership(organizationSpaceMembershipId) 27 | .then((response) => { 28 | expect(response.sys, 'sys').ok 29 | expect(response.sys.id).equal(organizationSpaceMembershipId, 'id') 30 | expect(response.sys.type).equal('SpaceMembership', 'type') 31 | expect(response.user.sys.linkType).equal('User', 'user') 32 | }) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /lib/adapters/REST/endpoints/resource-provider.ts: -------------------------------------------------------------------------------- 1 | import type { RawAxiosRequestHeaders } from 'axios' 2 | import type { AxiosInstance } from 'contentful-sdk-core' 3 | import * as raw from './raw' 4 | import type { GetResourceProviderParams } from '../../../common-types' 5 | import type { RestEndpoint } from '../types' 6 | import type { 7 | ResourceProviderProps, 8 | UpsertResourceProviderProps, 9 | } from '../../../entities/resource-provider' 10 | 11 | const getBaseUrl = (params: GetResourceProviderParams) => 12 | `/organizations/${params.organizationId}/app_definitions/${params.appDefinitionId}/resource_provider` 13 | 14 | export const get: RestEndpoint<'ResourceProvider', 'get'> = ( 15 | http: AxiosInstance, 16 | params: GetResourceProviderParams, 17 | ) => { 18 | return raw.get(http, getBaseUrl(params)) 19 | } 20 | 21 | export const upsert: RestEndpoint<'ResourceProvider', 'upsert'> = ( 22 | http: AxiosInstance, 23 | params: GetResourceProviderParams, 24 | rawData: UpsertResourceProviderProps, 25 | headers?: RawAxiosRequestHeaders, 26 | ) => { 27 | return raw.put(http, getBaseUrl(params), rawData, { headers }) 28 | } 29 | 30 | export const del: RestEndpoint<'ResourceProvider', 'delete'> = ( 31 | http: AxiosInstance, 32 | params: GetResourceProviderParams, 33 | ) => { 34 | return raw.del(http, getBaseUrl(params)) 35 | } 36 | -------------------------------------------------------------------------------- /test/unit/adapters/REST/endpoints/ui-config.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from 'vitest' 2 | import { wrapUIConfig } from '../../../../../lib/entities/ui-config' 3 | import { cloneMock } from '../../../mocks/entities' 4 | import setupRestAdapter from '../helpers/setupRestAdapter' 5 | 6 | function setup(promise, params = {}) { 7 | return { 8 | ...setupRestAdapter(promise, params), 9 | entityMock: cloneMock('uiConfig'), 10 | } 11 | } 12 | 13 | describe('Rest UIConfig', () => { 14 | test('UIConfig update works', async () => { 15 | const { httpMock, adapterMock } = setup(Promise.resolve({ data: {} })) 16 | const entityMock = cloneMock('uiConfig') 17 | entityMock.sys.version = 2 18 | const entity = wrapUIConfig((...args) => adapterMock.makeRequest(...args), entityMock) 19 | entity.entryListViews = [{ id: 'view', title: 'View', views: [] }] 20 | 21 | return entity.update().then((response) => { 22 | expect(response.toPlainObject, 'response is wrapped').to.be.ok 23 | expect(httpMock.put.mock.calls[0][1].entryListViews[0].id).equals('view', 'metadata is sent') 24 | expect(httpMock.put.mock.calls[0][2].headers['X-Contentful-Version']).equals( 25 | 2, 26 | 'version header is sent', 27 | ) 28 | return { 29 | httpMock, 30 | entityMock, 31 | response, 32 | } 33 | }) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /lib/plain/entities/space-member.ts: -------------------------------------------------------------------------------- 1 | import type { CollectionProp, GetSpaceParams, QueryParams } from '../../common-types' 2 | import type { SpaceMemberProps } from '../../entities/space-member' 3 | import type { OptionalDefaults } from '../wrappers/wrap' 4 | 5 | export type SpaceMemberPlainClientAPI = { 6 | /** 7 | * Fetch the space member 8 | * @param params the space and member IDs 9 | * @returns the space member 10 | * @throws if the request fails, or the space member is not found 11 | * @example ```javascript 12 | * const spaceMember = await client.spaceMember.get({ 13 | * spaceId: '', 14 | * spaceMemberId: '', 15 | * }); 16 | * ``` 17 | */ 18 | get( 19 | params: OptionalDefaults, 20 | ): Promise 21 | /** 22 | * Fetches all the space members for a given space 23 | * @param params a space ID and query parameters 24 | * @returns a collection of space members 25 | * @throws if the request fails, the space is not found, or the query parameters are malformed 26 | * @example ```javascript 27 | * const spaceMember = await client.spaceMember.getMany({ 28 | * spaceId: '', 29 | * }); 30 | * ``` 31 | */ 32 | getMany( 33 | params: OptionalDefaults, 34 | ): Promise> 35 | } 36 | -------------------------------------------------------------------------------- /lib/plain/entities/organization.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | PaginationQueryParams, 3 | CollectionProp, 4 | GetOrganizationParams, 5 | } from '../../common-types' 6 | import type { OrganizationProps } from '../../entities/organization' 7 | import type { OptionalDefaults } from '../wrappers/wrap' 8 | 9 | export type OrganizationPlainClientAPI = { 10 | /** 11 | * Fetch all organizations the user has access to 12 | * @param params Optional pagination query parameters 13 | * @returns A collection of organizations 14 | * @throws if the request fails, or no organizations are found 15 | * @example 16 | * ```javascript 17 | * const organizations = await client.organization.getAll({ 18 | * query: { 19 | * limit: 10, 20 | * } 21 | * }) 22 | * ``` 23 | */ 24 | getAll( 25 | params?: OptionalDefaults, 26 | ): Promise> 27 | /** 28 | * Fetch a single organization by its ID 29 | * @param params the organization ID 30 | * @returns the requested organization 31 | * @throws if the request fails, or the organization is not found 32 | * @example 33 | * ```javascript 34 | * const organization = await client.organization.get({ 35 | * organizationId: '' 36 | * }) 37 | * ``` 38 | */ 39 | get(params: OptionalDefaults): Promise 40 | } 41 | -------------------------------------------------------------------------------- /test/unit/adapters/REST/endpoints/user-ui-config.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from 'vitest' 2 | import { wrapUIConfig } from '../../../../../lib/entities/ui-config' 3 | import { cloneMock } from '../../../mocks/entities' 4 | import setupRestAdapter from '../helpers/setupRestAdapter' 5 | 6 | function setup(promise, params = {}) { 7 | return { 8 | ...setupRestAdapter(promise, params), 9 | entityMock: cloneMock('userUIConfig'), 10 | } 11 | } 12 | 13 | describe('Rest UserUIConfig', () => { 14 | test('UIConfig update works', async () => { 15 | const { httpMock, adapterMock } = setup(Promise.resolve({ data: {} })) 16 | const entityMock = cloneMock('userUIConfig') 17 | entityMock.sys.version = 2 18 | const entity = wrapUIConfig((...args) => adapterMock.makeRequest(...args), entityMock) 19 | entity.entryListViews = [{ id: 'view', title: 'View', views: [] }] 20 | 21 | return entity.update().then((response) => { 22 | expect(response.toPlainObject, 'response is wrapped').to.be.ok 23 | expect(httpMock.put.mock.calls[0][1].entryListViews[0].id).equals('view', 'metadata is sent') 24 | expect(httpMock.put.mock.calls[0][2].headers['X-Contentful-Version']).equals( 25 | 2, 26 | 'version header is sent', 27 | ) 28 | return { 29 | httpMock, 30 | entityMock, 31 | response, 32 | } 33 | }) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /lib/adapters/REST/endpoints/app-signing-secret.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosInstance } from 'contentful-sdk-core' 2 | import type { 3 | CreateAppSigningSecretProps, 4 | AppSigningSecretProps, 5 | } from '../../../entities/app-signing-secret' 6 | import * as raw from './raw' 7 | import type { RestEndpoint } from '../types' 8 | import type { GetAppDefinitionParams } from '../../../common-types' 9 | 10 | export const get: RestEndpoint<'AppSigningSecret', 'get'> = ( 11 | http: AxiosInstance, 12 | params: GetAppDefinitionParams, 13 | ) => { 14 | return raw.get( 15 | http, 16 | `/organizations/${params.organizationId}/app_definitions/${params.appDefinitionId}/signing_secret`, 17 | ) 18 | } 19 | 20 | export const upsert: RestEndpoint<'AppSigningSecret', 'upsert'> = ( 21 | http: AxiosInstance, 22 | params: GetAppDefinitionParams, 23 | data: CreateAppSigningSecretProps, 24 | ) => { 25 | return raw.put( 26 | http, 27 | `/organizations/${params.organizationId}/app_definitions/${params.appDefinitionId}/signing_secret`, 28 | data, 29 | ) 30 | } 31 | 32 | export const del: RestEndpoint<'AppSigningSecret', 'delete'> = ( 33 | http: AxiosInstance, 34 | params: GetAppDefinitionParams, 35 | ) => { 36 | return raw.del( 37 | http, 38 | `/organizations/${params.organizationId}/app_definitions/${params.appDefinitionId}/signing_secret`, 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /lib/plain/wrappers/wrap.test-d.ts: -------------------------------------------------------------------------------- 1 | import { describe, expectTypeOf, it } from 'vitest' 2 | import type { OptionalDefaults } from './wrap' 3 | 4 | describe('OptionalDefaults', () => { 5 | it('does not add props', () => { 6 | type Result = OptionalDefaults<{ 7 | foo: string 8 | }> 9 | 10 | type Expected = { 11 | foo: string 12 | } 13 | 14 | expectTypeOf().toMatchTypeOf() 15 | }) 16 | 17 | it('adds default props if available', () => { 18 | type Result = OptionalDefaults<{ 19 | foo: string 20 | environmentId: string 21 | }> 22 | 23 | type Expected = { 24 | foo: string 25 | environmentId?: string 26 | } 27 | 28 | expectTypeOf().toMatchTypeOf() 29 | }) 30 | 31 | it('handles intersection types', () => { 32 | type Result = OptionalDefaults<{ foo1: 'bar1' } | { foo2: 'bar2' }> 33 | 34 | type Expected = { foo1: 'bar1' } | { foo2: 'bar2' } 35 | 36 | expectTypeOf().toMatchTypeOf() 37 | }) 38 | 39 | it('handles union with intersection types', () => { 40 | type Result = OptionalDefaults<{ spaceId: string } & ({ foo1: 'bar1' } | { foo2: 'bar2' })> 41 | 42 | type Expected = 43 | | { spaceId?: string | undefined; foo1: 'bar1' } 44 | | { spaceId?: string | undefined; foo2: 'bar2' } 45 | 46 | expectTypeOf().toMatchTypeOf() 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /lib/plain/entities/workflows-changelog.ts: -------------------------------------------------------------------------------- 1 | import type { RawAxiosRequestHeaders } from 'axios' 2 | import type { GetSpaceEnvironmentParams, CollectionProp } from '../../common-types' 3 | import type { OptionalDefaults } from '../wrappers/wrap' 4 | import type { 5 | WorkflowsChangelogEntryProps, 6 | WorkflowsChangelogQueryOptions, 7 | } from '../../entities/workflows-changelog-entry' 8 | 9 | export type WorkflowsChangelogPlainClientAPI = { 10 | /** 11 | * Query records in the Workflows Changelog with certain filters 12 | * @param params entity IDs to identify the Space/Environment, query options to identify the entry for which the Workflow was executed, and optional filtering and pagination parameters 13 | * @returns an object containing the array of Workflow Changelogs 14 | * @throws if the request fails, or the Space/Environment is not found 15 | * @example 16 | * ```javascript 17 | * const records = await client.workflowsChangelog.getMany({ 18 | * spaceId: '', 19 | * environmentId: '', 20 | * query: { 21 | * 'entity.sys.linkType': 'entry', 22 | * 'entity.sys.id': '', 23 | * } 24 | * }); 25 | * ``` 26 | */ 27 | getMany( 28 | params: OptionalDefaults, 29 | headers?: RawAxiosRequestHeaders, 30 | ): Promise> 31 | } 32 | -------------------------------------------------------------------------------- /test/integration/environment-alias-integration.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, beforeAll, afterAll, expect } from 'vitest' 2 | import { getSpecialSpace, timeoutToCalmRateLimiting } from '../helpers' 3 | import type { Space } from '../../lib/export-types' 4 | 5 | describe('EnvironmentAlias API', () => { 6 | describe('read', () => { 7 | let space: Space 8 | 9 | beforeAll(async () => { 10 | space = await getSpecialSpace('alias') 11 | }) 12 | 13 | afterAll(async () => { 14 | const alias = await space.getEnvironmentAlias('master') 15 | alias.environment.sys.id = 'previously-master-env' 16 | await alias.update() 17 | await timeoutToCalmRateLimiting() 18 | }) 19 | 20 | it('Gets aliases', async () => { 21 | const response = await space.getEnvironmentAliases() 22 | expect(response.items[0].sys.id).toBe('master') 23 | expect(response.items[0].environment.sys.id).toBe('previously-master-env') 24 | }) 25 | 26 | it('Updates alias', async () => { 27 | const alias = await space.getEnvironmentAlias('master') 28 | expect(alias.sys.id).toBe('master') 29 | expect(alias.environment.sys.id).toBe('previously-master-env') 30 | alias.environment.sys.id = 'feature-env' 31 | const updatedAlias = await alias.update() 32 | expect(updatedAlias.sys.id).toBe('master') 33 | expect(updatedAlias.environment.sys.id).toBe('feature-env') 34 | }) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /lib/plain/entities/vectorization-status.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | UpdateVectorizationStatusProps, 3 | VectorizationStatusProps, 4 | } from '../../entities/vectorization-status' 5 | import type { OptionalDefaults } from '../wrappers/wrap' 6 | import type { GetOrganizationParams } from '../../common-types' 7 | import type { RawAxiosRequestHeaders } from 'axios' 8 | 9 | export type VectorizationStatusPlainClientAPI = { 10 | /** 11 | * Fetches the vectorization status for all spaces in an organization. 12 | * @param params Parameters for getting the organization. 13 | * @param headers Optional headers for the request. 14 | * @returns A promise that resolves to the vectorization status of spaces. 15 | */ 16 | get( 17 | params: OptionalDefaults, 18 | headers?: Partial, 19 | ): Promise 20 | 21 | /** 22 | * Updates the vectorization status for spaces within an organization. 23 | * @param params Parameters for getting the organization. 24 | * @param payload Payload containing the update information. 25 | * @param headers Optional headers for the request. 26 | * @returns A promise that resolves to the vectorization status of spaces. 27 | */ 28 | update( 29 | params: OptionalDefaults, 30 | payload: UpdateVectorizationStatusProps, 31 | headers?: Partial, 32 | ): Promise 33 | } 34 | -------------------------------------------------------------------------------- /lib/adapters/REST/endpoints/app-event-subscription.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosInstance } from 'contentful-sdk-core' 2 | import type { 3 | CreateAppEventSubscriptionProps, 4 | AppEventSubscriptionProps, 5 | } from '../../../entities/app-event-subscription' 6 | import * as raw from './raw' 7 | import type { RestEndpoint } from '../types' 8 | import type { GetAppDefinitionParams } from '../../../common-types' 9 | 10 | export const get: RestEndpoint<'AppEventSubscription', 'get'> = ( 11 | http: AxiosInstance, 12 | params: GetAppDefinitionParams, 13 | ) => { 14 | return raw.get( 15 | http, 16 | `/organizations/${params.organizationId}/app_definitions/${params.appDefinitionId}/event_subscription`, 17 | ) 18 | } 19 | 20 | export const upsert: RestEndpoint<'AppEventSubscription', 'upsert'> = ( 21 | http: AxiosInstance, 22 | params: GetAppDefinitionParams, 23 | data: CreateAppEventSubscriptionProps, 24 | ) => { 25 | return raw.put( 26 | http, 27 | `/organizations/${params.organizationId}/app_definitions/${params.appDefinitionId}/event_subscription`, 28 | data, 29 | ) 30 | } 31 | 32 | export const del: RestEndpoint<'AppEventSubscription', 'delete'> = ( 33 | http: AxiosInstance, 34 | params: GetAppDefinitionParams, 35 | ) => { 36 | return raw.del( 37 | http, 38 | `/organizations/${params.organizationId}/app_definitions/${params.appDefinitionId}/event_subscription`, 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /lib/entities/snapshot.ts: -------------------------------------------------------------------------------- 1 | import copy from 'fast-copy' 2 | import { freezeSys, toPlainObject } from 'contentful-sdk-core' 3 | import enhanceWithMethods from '../enhance-with-methods' 4 | import { wrapCollection } from '../common-utils' 5 | import type { MetaSysProps, DefaultElements, MakeRequest } from '../common-types' 6 | 7 | export type SnapshotProps = { 8 | sys: MetaSysProps & { 9 | snapshotType: string 10 | snapshotEntityType: string 11 | } 12 | snapshot: T 13 | } 14 | 15 | export interface Snapshot extends SnapshotProps, DefaultElements> {} 16 | 17 | /** 18 | * @private 19 | */ 20 | function createSnapshotApi() { 21 | return { 22 | /* In case the snapshot object evolve later */ 23 | } 24 | } 25 | /** 26 | * @private 27 | * @param makeRequest - function to make requests via an adapter 28 | * @param data - Raw snapshot data 29 | * @return Wrapped snapshot data 30 | */ 31 | export function wrapSnapshot(_makeRequest: MakeRequest, data: SnapshotProps): Snapshot { 32 | const snapshot = toPlainObject(copy(data)) 33 | const snapshotWithMethods = enhanceWithMethods(snapshot, createSnapshotApi()) 34 | return freezeSys(snapshotWithMethods) 35 | } 36 | 37 | /** 38 | * @private 39 | * @param makeRequest - function to make requests via an adapter 40 | * @param data - Raw snapshot collection data 41 | * @return Wrapped snapshot collection data 42 | */ 43 | export const wrapSnapshotCollection = wrapCollection(wrapSnapshot) 44 | -------------------------------------------------------------------------------- /test/unit/entities/environment-alias.test.ts: -------------------------------------------------------------------------------- 1 | import { cloneMock } from '../mocks/entities' 2 | import setupMakeRequest from '../mocks/makeRequest' 3 | import { 4 | wrapEnvironmentAlias, 5 | wrapEnvironmentAliasCollection, 6 | } from '../../../lib/entities/environment-alias' 7 | import { 8 | entityCollectionWrappedTest, 9 | entityUpdateTest, 10 | entityWrappedTest, 11 | failingVersionActionTest, 12 | } from '../test-creators/instance-entity-methods' 13 | import { describe, test } from 'vitest' 14 | 15 | function setup(promise) { 16 | return { 17 | makeRequest: setupMakeRequest(promise), 18 | entityMock: cloneMock('environmentAlias'), 19 | } 20 | } 21 | 22 | describe('Entity EnvironmentAlias', () => { 23 | test('Environment alias is wrapped', async () => { 24 | return entityWrappedTest(setup, { 25 | wrapperMethod: wrapEnvironmentAlias, 26 | }) 27 | }) 28 | 29 | test('Environment alias collection is wrapped', async () => { 30 | return entityCollectionWrappedTest(setup, { 31 | wrapperMethod: wrapEnvironmentAliasCollection, 32 | }) 33 | }) 34 | 35 | test('Environment alias update', async () => { 36 | return entityUpdateTest(setup, { 37 | wrapperMethod: wrapEnvironmentAlias, 38 | }) 39 | }) 40 | 41 | test('Environment alias update fails', async () => { 42 | return failingVersionActionTest(setup, { 43 | wrapperMethod: wrapEnvironmentAlias, 44 | actionMethod: 'update', 45 | }) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /lib/adapters/REST/make-request.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosInstance } from 'contentful-sdk-core' 2 | import type { MakeRequestOptions, MakeRequestPayload } from '../../common-types' 3 | import type { OpPatch } from 'json-patch' 4 | import type { RawAxiosRequestHeaders } from 'axios' 5 | import endpoints from './endpoints' 6 | 7 | type makeAxiosRequest = MakeRequestOptions & { 8 | axiosInstance: AxiosInstance 9 | } 10 | export const makeRequest = async ({ 11 | axiosInstance, 12 | entityType, 13 | action: actionInput, 14 | params, 15 | payload, 16 | headers, 17 | userAgent, 18 | }: makeAxiosRequest): Promise => { 19 | // `delete` is a reserved keyword. Therefore, the methods are called `del`. 20 | const action = actionInput === 'delete' ? 'del' : actionInput 21 | 22 | const endpoint: ( 23 | http: AxiosInstance, 24 | params?: Record, 25 | payload?: Record | OpPatch[] | MakeRequestPayload, 26 | headers?: RawAxiosRequestHeaders, 27 | ) => Promise = 28 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 29 | // @ts-ignore 30 | endpoints[entityType]?.[action] 31 | 32 | if (endpoint === undefined) { 33 | throw new Error('Unknown endpoint') 34 | } 35 | 36 | return await endpoint(axiosInstance, params, payload, { 37 | ...headers, 38 | // overwrite the userAgent with the one passed in the request 39 | ...(userAgent ? { 'X-Contentful-User-Agent': userAgent } : {}), 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /test/integration/upload-credential.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, describe, test, beforeAll, afterAll } from 'vitest' 2 | import type { Environment, PlainClientAPI, Space } from '../../lib/export-types' 3 | import { 4 | createTestEnvironment, 5 | createTestSpace, 6 | initClient, 7 | initPlainClient, 8 | waitForEnvironmentToBeReady, 9 | } from '../helpers' 10 | 11 | describe('Upload Credential Integration', () => { 12 | let space: Space 13 | let environment: Environment 14 | let client: PlainClientAPI 15 | 16 | beforeAll(async () => { 17 | space = (await createTestSpace(initClient(), 'Entry')) as Space 18 | environment = (await createTestEnvironment(space, 'Testing Environment')) as Environment 19 | client = initPlainClient() 20 | await waitForEnvironmentToBeReady(space, environment) 21 | }) 22 | 23 | afterAll(() => { 24 | if (space) { 25 | return space.delete() 26 | } 27 | }) 28 | 29 | test('create a upload credential', async () => { 30 | const result = await client.uploadCredential.create({ 31 | spaceId: space.sys.id, 32 | environmentId: environment.sys.id, 33 | }) 34 | 35 | expect(result.sys.type).equals('UploadCredential') 36 | expect(result.uploadCredentials.policy).to.be.a('string') 37 | expect(result.uploadCredentials.signature).to.be.a('string') 38 | expect(result.uploadCredentials.expiresAt).to.be.a('string') 39 | expect(result.uploadCredentials.createdAt).to.be.a('string') 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /lib/plain/entities/app-signed-request.ts: -------------------------------------------------------------------------------- 1 | import type { GetAppInstallationParams } from '../../common-types' 2 | import type { 3 | AppSignedRequestProps, 4 | CreateAppSignedRequestProps, 5 | } from '../../entities/app-signed-request' 6 | import type { OptionalDefaults } from '../wrappers/wrap' 7 | 8 | export type AppSignedRequestPlainClientAPI = { 9 | /** 10 | * Creates a Signed Request for an App 11 | * @param params entity IDs to identify the App to make a signed request to 12 | * @param payload the Signed Request payload 13 | * @returns metadata about the Signed Request 14 | * @throws if the request fails, the App is not found, or the payload is malformed 15 | * @example 16 | * ```javascript 17 | * const signedRequest = await client.appSignedRequest.create( 18 | * { 19 | * spaceId: '', 20 | * organizationId: '', 21 | * appDefinitionId: '', 22 | * }, 23 | * { 24 | * method: 'POST', 25 | * path: 'https://your-app-backend.com/event-handler', 26 | * headers: { 27 | * 'Content-Type': 'application/json', 28 | * 'X-some-header': 'some-value', 29 | * }, 30 | * body: JSON.stringify({ 31 | * // ... 32 | * }), 33 | * } 34 | * ); 35 | * ``` 36 | */ 37 | create( 38 | params: OptionalDefaults, 39 | payload: CreateAppSignedRequestProps, 40 | ): Promise 41 | } 42 | -------------------------------------------------------------------------------- /lib/plain/as-iterator.ts: -------------------------------------------------------------------------------- 1 | import copy from 'fast-copy' 2 | import type { CollectionProp, QueryParams } from '../common-types' 3 | 4 | type IterableFn

= (params: P) => Promise> 5 | type ParamsType = T extends (params: infer P) => any ? P : never 6 | 7 | export const asIterator =

>( 8 | fn: F, 9 | params: ParamsType, 10 | ): AsyncIterable => { 11 | return { 12 | [Symbol.asyncIterator]() { 13 | let options = copy(params) 14 | const get = () => fn(copy(options)) 15 | let currentResult = get() 16 | 17 | return { 18 | current: 0, 19 | async next() { 20 | const { total = 0, items = [], skip = 0, limit = 100 } = await currentResult 21 | 22 | if (total === this.current) { 23 | return { done: true, value: null } 24 | } 25 | 26 | const value = items[this.current++ - skip] 27 | const endOfPage = this.current % limit === 0 28 | const endOfList = this.current === total 29 | 30 | if (endOfPage && !endOfList) { 31 | options = { 32 | ...options, 33 | query: { 34 | ...options.query, 35 | skip: skip + limit, 36 | }, 37 | } 38 | currentResult = get() 39 | } 40 | 41 | return { done: false, value } 42 | }, 43 | } 44 | }, 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/adapters/REST/endpoints/organization.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosInstance } from 'contentful-sdk-core' 2 | import type { 3 | CollectionProp, 4 | GetOrganizationParams, 5 | PaginationQueryParams, 6 | } from '../../../common-types' 7 | import type { OrganizationProps } from '../../../entities/organization' 8 | import type { RestEndpoint } from '../types' 9 | import * as raw from './raw' 10 | 11 | export const getMany: RestEndpoint<'Organization', 'getMany'> = ( 12 | http: AxiosInstance, 13 | params?: PaginationQueryParams, 14 | ) => { 15 | return raw.get>(http, `/organizations`, { 16 | params: params?.query, 17 | }) 18 | } 19 | 20 | export const get: RestEndpoint<'Organization', 'get'> = ( 21 | http: AxiosInstance, 22 | params: GetOrganizationParams, 23 | ) => { 24 | return getMany(http, { query: { limit: 100 } }).then((data) => { 25 | const org = data.items.find((org) => org.sys.id === params.organizationId) 26 | if (!org) { 27 | const error = new Error( 28 | `No organization was found with the ID ${ 29 | params.organizationId 30 | } instead got ${JSON.stringify(data)}`, 31 | ) 32 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 33 | // @ts-ignore 34 | error.status = 404 35 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 36 | // @ts-ignore 37 | error.statusText = 'Not Found' 38 | return Promise.reject(error) 39 | } 40 | return org 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /lib/adapters/REST/endpoints/environment-template-installation.ts: -------------------------------------------------------------------------------- 1 | import type { RawAxiosRequestHeaders } from 'axios' 2 | import type { RestEndpoint } from '../types' 3 | import * as raw from './raw' 4 | 5 | const apiPath = (organizationId: string, ...pathSegments: (number | string)[]) => 6 | `/organizations/${organizationId}/environment_templates/` + pathSegments.join('/') 7 | 8 | export const getMany: RestEndpoint<'EnvironmentTemplateInstallation', 'getMany'> = ( 9 | http, 10 | { organizationId, environmentTemplateId, spaceId, environmentId, ...otherProps }, 11 | headers?: RawAxiosRequestHeaders, 12 | ) => 13 | raw.get(http, apiPath(organizationId, environmentTemplateId, 'template_installations'), { 14 | params: { 15 | ...otherProps, 16 | ...(environmentId && { 'environment.sys.id': environmentId }), 17 | ...(spaceId && { 'space.sys.id': spaceId }), 18 | }, 19 | headers, 20 | }) 21 | 22 | export const getForEnvironment: RestEndpoint< 23 | 'EnvironmentTemplateInstallation', 24 | 'getForEnvironment' 25 | > = ( 26 | http, 27 | { spaceId, environmentId, environmentTemplateId, installationId, ...paginationProps }, 28 | headers?: RawAxiosRequestHeaders, 29 | ) => 30 | raw.get( 31 | http, 32 | `/spaces/${spaceId}/environments/${environmentId}/template_installations/${environmentTemplateId}`, 33 | { 34 | params: { 35 | ...(installationId && { 'sys.id': installationId }), 36 | ...paginationProps, 37 | }, 38 | headers, 39 | }, 40 | ) 41 | -------------------------------------------------------------------------------- /test/integration/organization-invitation.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, test, expect, afterAll } from 'vitest' 2 | import { getTestOrganization, timeoutToCalmRateLimiting } from '../helpers' 3 | import type { Organization } from '../../lib/export-types' 4 | 5 | describe('OrganizationMembership Invitation API', () => { 6 | let organization: Organization 7 | 8 | beforeAll(async () => { 9 | organization = await getTestOrganization() 10 | }) 11 | 12 | afterAll(timeoutToCalmRateLimiting) 13 | 14 | test('Creates, gets an invitation in the organization and removes membership after test', async () => { 15 | const response = await organization.createOrganizationInvitation({ 16 | email: 'test.user@contentful.com', 17 | firstName: 'Test', 18 | lastName: 'User', 19 | role: 'developer', 20 | }) 21 | 22 | const invitation = await organization.getOrganizationInvitation(response.sys.id) 23 | 24 | expect(invitation.sys.type).toBe('Invitation') 25 | expect(invitation.sys.status).toBe('open') 26 | expect(invitation.sys.user).toBe(null) 27 | expect(invitation.sys.organizationMembership.sys.type).toBe('Link') 28 | expect(invitation.sys.organizationMembership.sys.linkType).toBe('OrganizationMembership') 29 | 30 | const membership = await organization.getOrganizationMembership( 31 | invitation.sys.organizationMembership.sys.id, 32 | ) 33 | 34 | // Delete membership, which also deletes the invitation for this user 35 | await membership.delete() 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import eslint from '@eslint/js' 4 | import tseslint from 'typescript-eslint' 5 | import globals from 'globals' 6 | 7 | export default tseslint.config( 8 | eslint.configs.recommended, 9 | tseslint.configs.recommended, 10 | { 11 | languageOptions: { 12 | globals: { 13 | ...globals.node, 14 | ...globals.browser, 15 | createClient: true, 16 | }, 17 | }, 18 | }, 19 | // Library 20 | { 21 | files: ['lib/**/*'], 22 | rules: { 23 | // Things we probably should fix at some point 24 | '@typescript-eslint/ban-ts-comment': 'warn', 25 | '@typescript-eslint/no-empty-object-type': 'warn', 26 | '@typescript-eslint/no-explicit-any': 'warn', 27 | '@typescript-eslint/no-unsafe-function-type': 'warn', 28 | '@typescript-eslint/no-unused-vars': 'warn', 29 | // Things we won't allow 30 | '@typescript-eslint/consistent-type-imports': 'error', 31 | '@typescript-eslint/no-this-alias': [ 32 | 'error', 33 | { 34 | allowDestructuring: true, // Allow `const { props, state } = this`; false by default 35 | allowedNames: ['self'], // Allow `const self = this`; `[]` by default 36 | }, 37 | ], 38 | }, 39 | }, 40 | // Tests 41 | { 42 | files: ['test/**/*'], 43 | rules: { 44 | '@typescript-eslint/no-unused-expressions': 'off', 45 | '@typescript-eslint/no-explicit-any': 'warn', 46 | '@typescript-eslint/ban-ts-comment': 'warn', 47 | }, 48 | } 49 | ) 50 | -------------------------------------------------------------------------------- /test/integration/org-team-space-membership-integration.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, it, expect, afterAll } from 'vitest' 2 | import { getTestOrganization, timeoutToCalmRateLimiting } from '../helpers' 3 | import { TestDefaults } from '../defaults' 4 | import type { Organization } from '../../lib/export-types' 5 | 6 | const { teamId, teamSpaceMembershipId } = TestDefaults 7 | 8 | describe('TeamSpaceMembership API', () => { 9 | let organization: Organization 10 | 11 | beforeAll(async () => { 12 | organization = await getTestOrganization() 13 | }) 14 | 15 | afterAll(timeoutToCalmRateLimiting) 16 | 17 | it('Gets a single Team Space Membership', async () => { 18 | const response = await organization.getTeamSpaceMembership(teamSpaceMembershipId) 19 | expect(response.sys.type).toBe('TeamSpaceMembership') 20 | expect(response.sys.team).toBeTruthy() 21 | expect(response.roles).toBeTruthy() 22 | }) 23 | 24 | it('Gets all Team Space Memberships in organization', async () => { 25 | const response = await organization.getTeamSpaceMemberships() 26 | expect(response.sys).toBeTruthy() 27 | expect(response.items).toBeTruthy() 28 | expect(response.items[0].sys.type).toBe('TeamSpaceMembership') 29 | }) 30 | 31 | it('Gets all Team Space Memberships in a team', async () => { 32 | const response = await organization.getTeamSpaceMemberships({ teamId }) 33 | expect(response.sys).toBeTruthy() 34 | expect(response.items).toBeTruthy() 35 | expect(response.items[0].sys.type).toBe('TeamSpaceMembership') 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /lib/adapters/REST/endpoints/function-log.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosInstance } from 'contentful-sdk-core' 2 | import * as raw from './raw' 3 | import type { 4 | CollectionProp, 5 | GetFunctionLogParams, 6 | GetManyFunctionLogParams, 7 | } from '../../../common-types' 8 | import type { RestEndpoint } from '../types' 9 | import type { FunctionLogProps } from '../../../entities/function-log' 10 | 11 | const FunctionLogAlphaHeaders = { 12 | 'x-contentful-enable-alpha-feature': 'function-logs', 13 | } 14 | 15 | const baseURL = (params: GetManyFunctionLogParams) => 16 | `/spaces/${params.spaceId}/environments/${params.environmentId}/app_installations/${params.appInstallationId}/functions/${params.functionId}/logs` 17 | 18 | const getURL = (params: GetFunctionLogParams) => 19 | `/spaces/${params.spaceId}/environments/${params.environmentId}/app_installations/${params.appInstallationId}/functions/${params.functionId}/logs/${params.logId}` 20 | 21 | export const get: RestEndpoint<'FunctionLog', 'get'> = ( 22 | http: AxiosInstance, 23 | params: GetFunctionLogParams, 24 | ) => { 25 | return raw.get(http, getURL(params), { 26 | headers: { 27 | ...FunctionLogAlphaHeaders, 28 | }, 29 | }) 30 | } 31 | 32 | export const getMany: RestEndpoint<'FunctionLog', 'getMany'> = ( 33 | http: AxiosInstance, 34 | params: GetManyFunctionLogParams, 35 | ) => { 36 | return raw.get>(http, baseURL(params), { 37 | params: params.query, 38 | headers: { 39 | ...FunctionLogAlphaHeaders, 40 | }, 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /lib/adapters/REST/endpoints/release-action.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosInstance } from 'contentful-sdk-core' 2 | import type { GetReleaseParams, GetSpaceEnvironmentParams } from '../../../common-types' 3 | import type { ReleaseActionQueryOptions } from '../../../entities/release-action' 4 | import type { RestEndpoint } from '../types' 5 | import * as raw from './raw' 6 | 7 | export const get: RestEndpoint<'ReleaseAction', 'get'> = ( 8 | http: AxiosInstance, 9 | params: GetReleaseParams & { actionId: string }, 10 | ) => { 11 | return raw.get( 12 | http, 13 | `/spaces/${params.spaceId}/environments/${params.environmentId}/releases/${params.releaseId}/actions/${params.actionId}`, 14 | ) 15 | } 16 | 17 | export const getMany: RestEndpoint<'ReleaseAction', 'getMany'> = ( 18 | http: AxiosInstance, 19 | params: GetSpaceEnvironmentParams & { query?: ReleaseActionQueryOptions }, 20 | ) => { 21 | return raw.get( 22 | http, 23 | `/spaces/${params.spaceId}/environments/${params.environmentId}/release_actions`, 24 | { 25 | params: params.query, 26 | }, 27 | ) 28 | } 29 | 30 | export const queryForRelease: RestEndpoint<'ReleaseAction', 'queryForRelease'> = ( 31 | http: AxiosInstance, 32 | params: GetReleaseParams & { query?: ReleaseActionQueryOptions }, 33 | ) => { 34 | return raw.get( 35 | http, 36 | `/spaces/${params.spaceId}/environments/${params.environmentId}/release_actions`, 37 | { 38 | params: { 39 | 'sys.release.sys.id[in]': params.releaseId, 40 | ...params.query, 41 | }, 42 | }, 43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /test/integration/app-upload-integration.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, describe, test, beforeAll, afterAll } from 'vitest' 2 | import { readFileSync } from 'fs' 3 | import { getTestOrganization, timeoutToCalmRateLimiting } from '../helpers' 4 | import type { Organization } from '../../lib/contentful-management' 5 | 6 | describe('AppUpload api', { sequential: true }, () => { 7 | let organization: Organization 8 | 9 | beforeAll(async () => { 10 | organization = await getTestOrganization() 11 | }) 12 | 13 | afterAll(timeoutToCalmRateLimiting) 14 | 15 | test('createAppUpload', async () => { 16 | const appUpload = await organization.createAppUpload( 17 | readFileSync(`${__dirname}/fixtures/build.zip`), 18 | ) 19 | 20 | expect(appUpload.sys.type).toBe('AppUpload') 21 | 22 | await appUpload.delete() 23 | }) 24 | 25 | test('getAppUpload', async () => { 26 | const appUpload = await organization.createAppUpload( 27 | readFileSync(`${__dirname}/fixtures/build.zip`), 28 | ) 29 | 30 | const fetchedAppUpload = await organization.getAppUpload(appUpload.sys.id) 31 | 32 | expect(appUpload.sys.id).toBe(fetchedAppUpload.sys.id) 33 | 34 | await appUpload.delete() 35 | }) 36 | 37 | test('delete', async () => { 38 | const appUpload = await organization.createAppUpload( 39 | readFileSync(`${__dirname}/fixtures/build.zip`), 40 | ) 41 | 42 | await appUpload.delete() 43 | 44 | await expect(organization.getAppUpload(appUpload.sys.id)).rejects.toThrow( 45 | 'The resource could not be found', 46 | ) 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /lib/entities/space.ts: -------------------------------------------------------------------------------- 1 | import { freezeSys, toPlainObject } from 'contentful-sdk-core' 2 | import copy from 'fast-copy' 3 | import type { BasicMetaSysProps, DefaultElements, MakeRequest } from '../common-types' 4 | import { wrapCollection } from '../common-utils' 5 | import type { ContentfulSpaceAPI } from '../create-space-api' 6 | import createSpaceApi from '../create-space-api' 7 | import enhanceWithMethods from '../enhance-with-methods' 8 | 9 | export type SpaceProps = { 10 | sys: BasicMetaSysProps & { organization: { sys: { id: string } }; archivedAt?: string } 11 | name: string 12 | } 13 | 14 | export type Space = SpaceProps & DefaultElements & ContentfulSpaceAPI 15 | 16 | /** 17 | * This method creates the API for the given space with all the methods for 18 | * reading and creating other entities. It also passes down a clone of the 19 | * http client with a space id, so the base path for requests now has the 20 | * space id already set. 21 | * @private 22 | * @param makeRequest - function to make requests via an adapter 23 | * @param data - API response for a Space 24 | * @return {Space} 25 | */ 26 | export function wrapSpace(makeRequest: MakeRequest, data: SpaceProps): Space { 27 | const space = toPlainObject(copy(data)) 28 | const spaceApi = createSpaceApi(makeRequest) 29 | const enhancedSpace = enhanceWithMethods(space, spaceApi) 30 | return freezeSys(enhancedSpace) 31 | } 32 | 33 | /** 34 | * This method wraps each space in a collection with the space API. See wrapSpace 35 | * above for more details. 36 | * @private 37 | */ 38 | export const wrapSpaceCollection = wrapCollection(wrapSpace) 39 | -------------------------------------------------------------------------------- /lib/entities/user-ui-config.ts: -------------------------------------------------------------------------------- 1 | import { freezeSys, toPlainObject } from 'contentful-sdk-core' 2 | import copy from 'fast-copy' 3 | import type { BasicMetaSysProps, DefaultElements, MakeRequest, SysLink } from '../common-types' 4 | import createUserUIConfigApi from '../create-user-ui-config-api' 5 | import enhanceWithMethods from '../enhance-with-methods' 6 | 7 | export type UserUIConfigProps = { 8 | /** 9 | * System metadata 10 | */ 11 | sys: UserUIConfigSysProps 12 | 13 | assetListViews: ViewFolder[] 14 | entryListViews: ViewFolder[] 15 | } 16 | 17 | export interface UserUIConfigSysProps extends BasicMetaSysProps { 18 | space: SysLink 19 | environment: SysLink 20 | } 21 | 22 | interface ViewFolder { 23 | id: string 24 | title: string 25 | views: View[] 26 | } 27 | 28 | interface View { 29 | id: string 30 | title: string 31 | order?: { 32 | fieldId: string 33 | direction: 'ascending' | 'descending' 34 | } 35 | displayedFieldIds?: string[] 36 | contentTypeId: string | null 37 | searchText?: string 38 | searchFilters?: [string, string, string][] 39 | } 40 | 41 | export interface UserUIConfig extends UserUIConfigProps, DefaultElements {} 42 | 43 | /** 44 | * @private 45 | * @param makeRequest - function to make requests via an adapter 46 | * @param data - Raw data 47 | * @return Wrapped UserUIConfig 48 | */ 49 | export function wrapUserUIConfig(makeRequest: MakeRequest, data: UserUIConfigProps) { 50 | const user = toPlainObject(copy(data)) 51 | const userWithMethods = enhanceWithMethods(user, createUserUIConfigApi(makeRequest)) 52 | return freezeSys(userWithMethods) 53 | } 54 | -------------------------------------------------------------------------------- /lib/plain/entities/resource.ts: -------------------------------------------------------------------------------- 1 | import type { OptionalDefaults } from '../wrappers/wrap' 2 | import type { CursorPaginatedCollectionProp, GetResourceParams } from '../../common-types' 3 | import type { ResourceProps, ResourceQueryOptions } from '../../entities/resource' 4 | 5 | export type ResourcePlainAPI = { 6 | /** 7 | * Fetches all Resources. 8 | * Supports fetching specific Resources by URNs or searching by a text query. 9 | * @param params entity IDs to identify the Resources 10 | * @params optional query params for search or lookup events 11 | * @returns the Resources collection 12 | * @throws if the request fails or the Resource Type is not found 13 | * @example 14 | * ```javascript 15 | * // Lookup example 16 | * const resources = await client.resource.getMany({ 17 | * spaceId: '', 18 | * environmentId: '', 19 | * resourceTypeId: ':', 20 | * query: { 21 | * 'sys.urn[in]': ',', 22 | * limit': , 23 | * } 24 | * }); 25 | * 26 | * // Search example 27 | * const resources = await client.resource.getMany({ 28 | * spaceId: '', 29 | * environmentId: '', 30 | * resourceTypeId: ':', 31 | * query: { 32 | * 'query': 'text', 33 | * 'limit': , 34 | * } 35 | * }); 36 | * ``` 37 | */ 38 | getMany( 39 | params: OptionalDefaults & { query?: ResourceQueryOptions }, 40 | ): Promise> 41 | } 42 | -------------------------------------------------------------------------------- /test/unit/entities/team.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test } from 'vitest' 2 | import { cloneMock } from '../mocks/entities' 3 | import setupMakeRequest from '../mocks/makeRequest' 4 | import { wrapTeam, wrapTeamCollection } from '../../../lib/entities/team' 5 | import { 6 | entityWrappedTest, 7 | entityCollectionWrappedTest, 8 | failingActionTest, 9 | entityUpdateTest, 10 | entityDeleteTest, 11 | } from '../test-creators/instance-entity-methods' 12 | 13 | function setup(promise) { 14 | return { 15 | makeRequest: setupMakeRequest(promise), 16 | entityMock: cloneMock('team'), 17 | } 18 | } 19 | 20 | describe('Entity TeamSpaceMembership', () => { 21 | test('Team is wrapped', async () => { 22 | return entityWrappedTest(setup, { 23 | wrapperMethod: wrapTeam, 24 | }) 25 | }) 26 | 27 | test('Team collection is wrapped', async () => { 28 | return entityCollectionWrappedTest(setup, { 29 | wrapperMethod: wrapTeamCollection, 30 | }) 31 | }) 32 | 33 | test('Team update', async () => { 34 | return entityUpdateTest(setup, { 35 | wrapperMethod: wrapTeam, 36 | }) 37 | }) 38 | 39 | test('Team update fails', async () => { 40 | return failingActionTest(setup, { 41 | wrapperMethod: wrapTeam, 42 | actionMethod: 'update', 43 | }) 44 | }) 45 | 46 | test('Team delete', async () => { 47 | return entityDeleteTest(setup, { 48 | wrapperMethod: wrapTeam, 49 | }) 50 | }) 51 | 52 | test('Team delete fails', async () => { 53 | return failingActionTest(setup, { 54 | wrapperMethod: wrapTeam, 55 | actionMethod: 'delete', 56 | }) 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /lib/entities/resource.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | BasicCursorPaginationOptions, 3 | CursorPaginatedCollectionProp, 4 | MakeRequest, 5 | SysLink, 6 | } from '../common-types' 7 | import { wrapCursorPaginatedCollection } from '../common-utils' 8 | import { freezeSys, toPlainObject } from 'contentful-sdk-core' 9 | 10 | export type ResourceQueryOptions = LookupQueryOptions | SearchQueryOptions 11 | 12 | type LookupQueryOptions = { 13 | 'sys.urn[in]': string 14 | locale?: string 15 | referencingEntryId?: string 16 | } & BasicCursorPaginationOptions 17 | 18 | type SearchQueryOptions = { 19 | query: string 20 | locale?: string 21 | referencingEntryId?: string 22 | } & BasicCursorPaginationOptions 23 | 24 | export type ResourceProps = { 25 | sys: { 26 | type: 'Resource' 27 | urn: string 28 | resourceType: SysLink 29 | resourceProvider: SysLink 30 | appDefinition: SysLink 31 | } 32 | fields: { 33 | title: string 34 | subtitle?: string 35 | description?: string 36 | externalUrl?: string 37 | image?: { 38 | url: string 39 | altText?: string 40 | } 41 | badge?: { 42 | label: string 43 | variant: 'primary' | 'negative' | 'positive' | 'warning' | 'secondary' 44 | } 45 | } 46 | } 47 | export function wrapResource(makeRequest: MakeRequest, data: ResourceProps) { 48 | const resource = toPlainObject(data) 49 | return freezeSys(resource) 50 | } 51 | export const wrapResourceCollection: ( 52 | makeRequest: MakeRequest, 53 | data: CursorPaginatedCollectionProp, 54 | ) => CursorPaginatedCollectionProp = wrapCursorPaginatedCollection(wrapResource) 55 | -------------------------------------------------------------------------------- /lib/adapters/REST/endpoints/app-key.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosInstance } from 'contentful-sdk-core' 2 | import type { CreateAppKeyProps, AppKeyProps } from '../../../entities/app-key' 3 | import * as raw from './raw' 4 | import type { RestEndpoint } from '../types' 5 | import type { CollectionProp, GetAppDefinitionParams, GetAppKeyParams } from '../../../common-types' 6 | 7 | export const get: RestEndpoint<'AppKey', 'get'> = ( 8 | http: AxiosInstance, 9 | params: GetAppKeyParams, 10 | ) => { 11 | return raw.get( 12 | http, 13 | `/organizations/${params.organizationId}/app_definitions/${params.appDefinitionId}/keys/${params.fingerprint}`, 14 | ) 15 | } 16 | 17 | export const getMany: RestEndpoint<'AppKey', 'getMany'> = ( 18 | http: AxiosInstance, 19 | params: GetAppDefinitionParams, 20 | ) => { 21 | return raw.get>( 22 | http, 23 | `/organizations/${params.organizationId}/app_definitions/${params.appDefinitionId}/keys`, 24 | ) 25 | } 26 | 27 | export const create: RestEndpoint<'AppKey', 'create'> = ( 28 | http: AxiosInstance, 29 | params: GetAppDefinitionParams, 30 | data: CreateAppKeyProps, 31 | ) => { 32 | return raw.post( 33 | http, 34 | `/organizations/${params.organizationId}/app_definitions/${params.appDefinitionId}/keys`, 35 | data, 36 | ) 37 | } 38 | 39 | export const del: RestEndpoint<'AppKey', 'delete'> = ( 40 | http: AxiosInstance, 41 | params: GetAppKeyParams, 42 | ) => { 43 | return raw.del( 44 | http, 45 | `/organizations/${params.organizationId}/app_definitions/${params.appDefinitionId}/keys/${params.fingerprint}`, 46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /test/unit/entities/role.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test } from 'vitest' 2 | import { cloneMock } from '../mocks/entities' 3 | import setupMakeRequest from '../mocks/makeRequest' 4 | import { wrapRole, wrapRoleCollection } from '../../../lib/entities/role' 5 | import { 6 | entityCollectionWrappedTest, 7 | entityDeleteTest, 8 | entityUpdateTest, 9 | entityWrappedTest, 10 | failingActionTest, 11 | failingVersionActionTest, 12 | } from '../test-creators/instance-entity-methods' 13 | 14 | function setup(promise) { 15 | return { 16 | makeRequest: setupMakeRequest(promise), 17 | entityMock: cloneMock('role'), 18 | } 19 | } 20 | 21 | describe('Entity Role', () => { 22 | test('Role is wrapped', async () => { 23 | return entityWrappedTest(setup, { 24 | wrapperMethod: wrapRole, 25 | }) 26 | }) 27 | 28 | test('Role collection is wrapped', async () => { 29 | return entityCollectionWrappedTest(setup, { 30 | wrapperMethod: wrapRoleCollection, 31 | }) 32 | }) 33 | 34 | test('Role update', async () => { 35 | return entityUpdateTest(setup, { 36 | wrapperMethod: wrapRole, 37 | }) 38 | }) 39 | 40 | test('Role update fails', async () => { 41 | return failingVersionActionTest(setup, { 42 | wrapperMethod: wrapRole, 43 | actionMethod: 'update', 44 | }) 45 | }) 46 | 47 | test('Role delete', async () => { 48 | return entityDeleteTest(setup, { 49 | wrapperMethod: wrapRole, 50 | }) 51 | }) 52 | 53 | test('Role delete fails', async () => { 54 | return failingActionTest(setup, { 55 | wrapperMethod: wrapRole, 56 | actionMethod: 'delete', 57 | }) 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /test/unit/entities/task.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test } from 'vitest' 2 | import { wrapTask, wrapTaskCollection } from '../../../lib/entities/task' 3 | import { cloneMock } from '../mocks/entities' 4 | import setupMakeRequest from '../mocks/makeRequest' 5 | import { 6 | entityCollectionWrappedTest, 7 | entityDeleteTest, 8 | entityUpdateTest, 9 | entityWrappedTest, 10 | failingActionTest, 11 | failingVersionActionTest, 12 | } from '../test-creators/instance-entity-methods' 13 | 14 | function setup(promise) { 15 | return { 16 | makeRequest: setupMakeRequest(promise), 17 | entityMock: cloneMock('task'), 18 | } 19 | } 20 | 21 | describe('Entity Task', () => { 22 | test('Task is wrapped', async () => { 23 | return entityWrappedTest(setup, { 24 | wrapperMethod: wrapTask, 25 | }) 26 | }) 27 | 28 | test('Task collection is wrapped', async () => { 29 | return entityCollectionWrappedTest(setup, { 30 | wrapperMethod: wrapTaskCollection, 31 | }) 32 | }) 33 | 34 | test('Task update', async () => { 35 | return entityUpdateTest(setup, { 36 | wrapperMethod: wrapTask, 37 | }) 38 | }) 39 | 40 | test('Task update fails', async () => { 41 | return failingVersionActionTest(setup, { 42 | wrapperMethod: wrapTask, 43 | actionMethod: 'update', 44 | }) 45 | }) 46 | 47 | test('Task delete', async () => { 48 | return entityDeleteTest(setup, { 49 | wrapperMethod: wrapTask, 50 | }) 51 | }) 52 | 53 | test('Task delete fails', async () => { 54 | return failingActionTest(setup, { 55 | wrapperMethod: wrapTask, 56 | actionMethod: 'delete', 57 | }) 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /lib/adapters/REST/endpoints/function.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosInstance } from 'contentful-sdk-core' 2 | import * as raw from './raw' 3 | import type { 4 | CollectionProp, 5 | GetFunctionForEnvParams, 6 | GetFunctionParams, 7 | GetManyFunctionParams, 8 | } from '../../../common-types' 9 | import type { RestEndpoint } from '../types' 10 | import type { FunctionProps } from '../../../entities/function' 11 | 12 | // Base URL 13 | const getManyUrl = (params: GetManyFunctionParams) => 14 | `/organizations/${params.organizationId}/app_definitions/${params.appDefinitionId}/functions` 15 | 16 | const getFunctionUrl = (params: GetFunctionParams) => `${getManyUrl(params)}/${params.functionId}` 17 | 18 | const getFunctionsEnvURL = (params: GetFunctionForEnvParams) => { 19 | return `/spaces/${params.spaceId}/environments/${params.environmentId}/app_installations/${params.appInstallationId}/functions` 20 | } 21 | 22 | export const get: RestEndpoint<'Function', 'get'> = ( 23 | http: AxiosInstance, 24 | params: GetFunctionParams, 25 | ) => { 26 | return raw.get(http, getFunctionUrl(params)) 27 | } 28 | 29 | export const getMany: RestEndpoint<'Function', 'getMany'> = ( 30 | http: AxiosInstance, 31 | params: GetManyFunctionParams, 32 | ) => { 33 | return raw.get>(http, getManyUrl(params), { params: params.query }) 34 | } 35 | 36 | export const getManyForEnvironment: RestEndpoint<'Function', 'getManyForEnvironment'> = ( 37 | http: AxiosInstance, 38 | params: GetFunctionForEnvParams, 39 | ) => { 40 | return raw.get>(http, getFunctionsEnvURL(params), { 41 | params: params.query, 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /test/integration/organization-membership-integration.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, test, expect } from 'vitest' 2 | import { getTestOrganization, timeoutToCalmRateLimiting } from '../helpers' 3 | import type { Organization } from '../../lib/export-types' 4 | import { TestDefaults } from '../defaults' 5 | 6 | const { organizationMembershipId } = TestDefaults 7 | 8 | describe('OrganizationMembership Api', function () { 9 | let organization: Organization 10 | 11 | beforeAll(async () => { 12 | organization = await getTestOrganization() 13 | }) 14 | 15 | afterAll(timeoutToCalmRateLimiting) 16 | 17 | test('Gets organizationMemberships', async () => { 18 | return organization.getOrganizationMemberships().then((response) => { 19 | expect(response.sys, 'sys').ok 20 | expect(response.items, 'fields').ok 21 | }) 22 | }) 23 | 24 | test('Gets organizationMembership', async () => { 25 | return organization.getOrganizationMembership(organizationMembershipId).then((response) => { 26 | expect(response.sys, 'sys').ok 27 | expect(response.sys.id).equals(organizationMembershipId) 28 | expect(response.sys.type).equals('OrganizationMembership') 29 | }) 30 | }) 31 | 32 | test('Gets organizationMemberships paged', async () => { 33 | return organization 34 | .getOrganizationMemberships({ 35 | query: { 36 | limit: 1, 37 | skip: 1, 38 | }, 39 | }) 40 | .then((response) => { 41 | expect(response.sys, 'sys').ok 42 | expect(response.limit).equals(1) 43 | expect(response.skip).equals(1) 44 | expect(response.items.length).equals(1) 45 | }) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /lib/adapters/REST/endpoints/app-upload.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosInstance } from 'contentful-sdk-core' 2 | import type { Stream } from 'stream' 3 | import * as raw from './raw' 4 | import type { GetAppUploadParams, GetOrganizationParams } from '../../../common-types' 5 | import type { RestEndpoint } from '../types' 6 | import type { AppUploadProps } from '../../../entities/app-upload' 7 | import { getUploadHttpClient } from '../../../upload-http-client' 8 | 9 | const getBaseUrl = (params: GetOrganizationParams) => 10 | `/organizations/${params.organizationId}/app_uploads` 11 | 12 | const getAppUploadUrl = (params: GetAppUploadParams) => 13 | `${getBaseUrl(params)}/${params.appUploadId}` 14 | 15 | export const get: RestEndpoint<'AppUpload', 'get'> = ( 16 | http: AxiosInstance, 17 | params: GetAppUploadParams, 18 | ) => { 19 | const httpUpload = getUploadHttpClient(http) 20 | 21 | return raw.get(httpUpload, getAppUploadUrl(params)) 22 | } 23 | 24 | export const del: RestEndpoint<'AppUpload', 'delete'> = ( 25 | http: AxiosInstance, 26 | params: GetAppUploadParams, 27 | ) => { 28 | const httpUpload = getUploadHttpClient(http) 29 | 30 | return raw.del(httpUpload, getAppUploadUrl(params)) 31 | } 32 | 33 | export const create: RestEndpoint<'AppUpload', 'create'> = ( 34 | http: AxiosInstance, 35 | params: GetOrganizationParams, 36 | payload: { file: string | ArrayBuffer | Stream }, 37 | ) => { 38 | const httpUpload = getUploadHttpClient(http) 39 | 40 | const { file } = payload 41 | 42 | return raw.post(httpUpload, getBaseUrl(params), file, { 43 | headers: { 44 | 'Content-Type': 'application/octet-stream', 45 | }, 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /lib/adapters/REST/endpoints/organization-invitation.ts: -------------------------------------------------------------------------------- 1 | import type { RawAxiosRequestHeaders } from 'axios' 2 | import type { AxiosInstance } from 'contentful-sdk-core' 3 | import type { 4 | CreateOrganizationInvitationProps, 5 | OrganizationInvitationProps, 6 | } from '../../../entities/organization-invitation' 7 | import type { RestEndpoint } from '../types' 8 | import * as raw from './raw' 9 | 10 | const OrganizationUserManagementAlphaHeaders = { 11 | 'x-contentful-enable-alpha-feature': 'organization-user-management-api', 12 | } 13 | 14 | const InvitationAlphaHeaders = { 15 | 'x-contentful-enable-alpha-feature': 'pending-org-membership', 16 | } 17 | 18 | export const create: RestEndpoint<'OrganizationInvitation', 'create'> = ( 19 | http: AxiosInstance, 20 | params: { organizationId: string }, 21 | data: CreateOrganizationInvitationProps, 22 | headers?: RawAxiosRequestHeaders, 23 | ) => { 24 | return raw.post( 25 | http, 26 | `/organizations/${params.organizationId}/invitations`, 27 | data, 28 | { 29 | headers: { 30 | ...InvitationAlphaHeaders, 31 | ...headers, 32 | }, 33 | }, 34 | ) 35 | } 36 | 37 | export const get: RestEndpoint<'OrganizationInvitation', 'get'> = ( 38 | http: AxiosInstance, 39 | params: { organizationId: string; invitationId: string }, 40 | headers?: RawAxiosRequestHeaders, 41 | ) => { 42 | return raw.get( 43 | http, 44 | `/organizations/${params.organizationId}/invitations/${params.invitationId}`, 45 | { 46 | headers: { 47 | ...OrganizationUserManagementAlphaHeaders, 48 | ...headers, 49 | }, 50 | }, 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /lib/plain/entities/ui-config.ts: -------------------------------------------------------------------------------- 1 | import type { GetUIConfigParams } from '../../common-types' 2 | import type { UIConfigProps } from '../../entities/ui-config' 3 | import type { OptionalDefaults } from '../wrappers/wrap' 4 | 5 | export type UIConfigPlainClientAPI = { 6 | /** 7 | * Fetch the UI Config for a given Space and Environment 8 | * @param params entity IDs to identify the UI Config 9 | * @returns the UI Config 10 | * @throws if the request fails, or the UI Config is not found 11 | * @example 12 | * ```javascript 13 | * const uiConfig = await client.uiConfig.get({ 14 | * spaceId: "", 15 | * environmentId: "", 16 | * }); 17 | * ``` 18 | */ 19 | get(params: OptionalDefaults): Promise 20 | /** 21 | * Update the UI Config for a given Space and Environment 22 | * @param params entity IDs to identify the UI Config 23 | * @param rawData the UI Config update 24 | * @returns the updated UI Config 25 | * @throws if the request fails, the UI Config is not found, or the update payload is malformed 26 | * @example 27 | * ```javascript 28 | * await client.uiConfig.update({ 29 | * spaceId: "", 30 | * environmentId: "", 31 | * }, { 32 | * ...currentUIConfig, 33 | * entryListViews: [ 34 | * ...currentUIConfig.entryListViews, 35 | * { 36 | * id: 'newFolder', 37 | * title: 'New Folder', 38 | * views: [] 39 | * } 40 | * ], 41 | * }); 42 | * ``` 43 | */ 44 | update( 45 | params: OptionalDefaults, 46 | rawData: UIConfigProps, 47 | ): Promise 48 | } 49 | -------------------------------------------------------------------------------- /test/unit/entities/locale.test.ts: -------------------------------------------------------------------------------- 1 | import { cloneMock } from '../mocks/entities' 2 | import setupMakeRequest from '../mocks/makeRequest' 3 | import { wrapLocale, wrapLocaleCollection } from '../../../lib/entities/locale' 4 | import { 5 | entityCollectionWrappedTest, 6 | entityDeleteTest, 7 | entityUpdateTest, 8 | entityWrappedTest, 9 | failingActionTest, 10 | failingVersionActionTest, 11 | } from '../test-creators/instance-entity-methods' 12 | import { describe, test } from 'vitest' 13 | 14 | function setup(promise) { 15 | return { 16 | makeRequest: setupMakeRequest(promise), 17 | entityMock: cloneMock('locale'), 18 | } 19 | } 20 | 21 | describe('Entity Locale', () => { 22 | test('Locale is wrapped', async () => { 23 | return entityWrappedTest(setup, { 24 | wrapperMethod: wrapLocale, 25 | }) 26 | }) 27 | 28 | test('Locale collection is wrapped', async () => { 29 | return entityCollectionWrappedTest(setup, { 30 | wrapperMethod: wrapLocaleCollection, 31 | }) 32 | }) 33 | 34 | test('Locale update', async () => { 35 | return entityUpdateTest(setup, { 36 | wrapperMethod: wrapLocale, 37 | }) 38 | }) 39 | 40 | test('Locale update fails', async () => { 41 | return failingVersionActionTest(setup, { 42 | wrapperMethod: wrapLocale, 43 | actionMethod: 'update', 44 | }) 45 | }) 46 | 47 | test('Locale delete', async () => { 48 | return entityDeleteTest(setup, { 49 | wrapperMethod: wrapLocale, 50 | }) 51 | }) 52 | 53 | test('Locale delete fails', async () => { 54 | return failingActionTest(setup, { 55 | wrapperMethod: wrapLocale, 56 | actionMethod: 'delete', 57 | }) 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /test/unit/entities/api-key.test.ts: -------------------------------------------------------------------------------- 1 | import { cloneMock } from '../mocks/entities' 2 | import setupMakeRequest from '../mocks/makeRequest' 3 | import { wrapApiKey, wrapApiKeyCollection } from '../../../lib/entities/api-key' 4 | import { 5 | entityCollectionWrappedTest, 6 | entityDeleteTest, 7 | entityUpdateTest, 8 | entityWrappedTest, 9 | failingActionTest, 10 | failingVersionActionTest, 11 | } from '../test-creators/instance-entity-methods' 12 | import { describe, test } from 'vitest' 13 | 14 | function setup(promise) { 15 | return { 16 | makeRequest: setupMakeRequest(promise), 17 | entityMock: cloneMock('apiKey'), 18 | } 19 | } 20 | 21 | describe('Entity ApiKey', () => { 22 | test('ApiKey is wrapped', async () => { 23 | return entityWrappedTest(setup, { 24 | wrapperMethod: wrapApiKey, 25 | }) 26 | }) 27 | 28 | test('ApiKey collection is wrapped', async () => { 29 | return entityCollectionWrappedTest(setup, { 30 | wrapperMethod: wrapApiKeyCollection, 31 | }) 32 | }) 33 | 34 | test('ApiKey update', async () => { 35 | return entityUpdateTest(setup, { 36 | wrapperMethod: wrapApiKey, 37 | }) 38 | }) 39 | 40 | test('ApiKey update fails', async () => { 41 | return failingVersionActionTest(setup, { 42 | wrapperMethod: wrapApiKey, 43 | actionMethod: 'update', 44 | }) 45 | }) 46 | 47 | test('ApiKey delete', async () => { 48 | return entityDeleteTest(setup, { 49 | wrapperMethod: wrapApiKey, 50 | }) 51 | }) 52 | 53 | test('ApiKey delete fails', async () => { 54 | return failingActionTest(setup, { 55 | wrapperMethod: wrapApiKey, 56 | actionMethod: 'delete', 57 | }) 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /lib/adapters/REST/endpoints/http.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import type { AxiosInstance, RawAxiosRequestConfig } from 'axios' 3 | import type { RestEndpoint } from '../types' 4 | import * as raw from './raw' 5 | 6 | export const get: RestEndpoint<'Http', 'get'> = ( 7 | http: AxiosInstance, 8 | { url, config }: { url: string; config?: RawAxiosRequestConfig }, 9 | ) => { 10 | return raw.get(http, url, config) 11 | } 12 | 13 | export const post: RestEndpoint<'Http', 'post'> = ( 14 | http: AxiosInstance, 15 | { url, config }: { url: string; config?: RawAxiosRequestConfig }, 16 | payload?: any, 17 | ) => { 18 | return raw.post(http, url, payload, config) 19 | } 20 | 21 | export const put: RestEndpoint<'Http', 'put'> = ( 22 | http: AxiosInstance, 23 | { url, config }: { url: string; config?: RawAxiosRequestConfig }, 24 | payload?: any, 25 | ) => { 26 | return raw.put(http, url, payload, config) 27 | } 28 | 29 | export const patch: RestEndpoint<'Http', 'patch'> = ( 30 | http: AxiosInstance, 31 | { url, config }: { url: string; config?: RawAxiosRequestConfig }, 32 | payload?: any, 33 | ) => { 34 | return raw.patch(http, url, payload, config) 35 | } 36 | 37 | export const del: RestEndpoint<'Http', 'delete'> = ( 38 | http: AxiosInstance, 39 | { url, config }: { url: string; config?: RawAxiosRequestConfig }, 40 | ) => { 41 | return raw.del(http, url, config) 42 | } 43 | 44 | export const request: RestEndpoint<'Http', 'request'> = ( 45 | http: AxiosInstance, 46 | { url, config }: { url: string; config?: RawAxiosRequestConfig }, 47 | ) => { 48 | return raw.http(http, url, config) 49 | } 50 | -------------------------------------------------------------------------------- /test/unit/entities/comment.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test } from 'vitest' 2 | import { wrapComment, wrapCommentCollection } from '../../../lib/entities/comment' 3 | import { cloneMock } from '../mocks/entities' 4 | import setupMakeRequest from '../mocks/makeRequest' 5 | import { 6 | entityCollectionWrappedTest, 7 | entityDeleteTest, 8 | entityUpdateTest, 9 | entityWrappedTest, 10 | failingActionTest, 11 | failingVersionActionTest, 12 | } from '../test-creators/instance-entity-methods' 13 | 14 | function setup(promise) { 15 | return { 16 | makeRequest: setupMakeRequest(promise), 17 | entityMock: cloneMock('comment'), 18 | } 19 | } 20 | 21 | describe('Entity Comment', () => { 22 | test('Comment is wrapped', async () => { 23 | return entityWrappedTest(setup, { 24 | wrapperMethod: wrapComment, 25 | }) 26 | }) 27 | 28 | test('Comment collection is wrapped', async () => { 29 | return entityCollectionWrappedTest(setup, { 30 | wrapperMethod: wrapCommentCollection, 31 | }) 32 | }) 33 | 34 | test('Comment update', async () => { 35 | return entityUpdateTest(setup, { 36 | wrapperMethod: wrapComment, 37 | }) 38 | }) 39 | 40 | test('Comment update fails', async () => { 41 | return failingVersionActionTest(setup, { 42 | wrapperMethod: wrapComment, 43 | actionMethod: 'update', 44 | }) 45 | }) 46 | 47 | test('Comment delete', async () => { 48 | return entityDeleteTest(setup, { 49 | wrapperMethod: wrapComment, 50 | }) 51 | }) 52 | 53 | test('Comment delete fails', async () => { 54 | return failingActionTest(setup, { 55 | wrapperMethod: wrapComment, 56 | actionMethod: 'delete', 57 | }) 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /test/unit/entities/webhook.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test } from 'vitest' 2 | import { wrapWebhook, wrapWebhookCollection } from '../../../lib/entities/webhook' 3 | import { cloneMock } from '../mocks/entities' 4 | import setupMakeRequest from '../mocks/makeRequest' 5 | import { 6 | entityCollectionWrappedTest, 7 | entityDeleteTest, 8 | entityUpdateTest, 9 | entityWrappedTest, 10 | failingActionTest, 11 | failingVersionActionTest, 12 | } from '../test-creators/instance-entity-methods' 13 | 14 | function setup(promise) { 15 | return { 16 | makeRequest: setupMakeRequest(promise), 17 | entityMock: cloneMock('webhook'), 18 | } 19 | } 20 | 21 | describe('Entity Webhook', () => { 22 | test('Webhook is wrapped', async () => { 23 | return entityWrappedTest(setup, { 24 | wrapperMethod: wrapWebhook, 25 | }) 26 | }) 27 | 28 | test('Webhook collection is wrapped', async () => { 29 | return entityCollectionWrappedTest(setup, { 30 | wrapperMethod: wrapWebhookCollection, 31 | }) 32 | }) 33 | 34 | test('Webhook update', async () => { 35 | return entityUpdateTest(setup, { 36 | wrapperMethod: wrapWebhook, 37 | }) 38 | }) 39 | 40 | test('Webhook update fails', async () => { 41 | return failingVersionActionTest(setup, { 42 | wrapperMethod: wrapWebhook, 43 | actionMethod: 'update', 44 | }) 45 | }) 46 | 47 | test('Webhook delete', async () => { 48 | return entityDeleteTest(setup, { 49 | wrapperMethod: wrapWebhook, 50 | }) 51 | }) 52 | 53 | test('Webhook delete fails', async () => { 54 | return failingActionTest(setup, { 55 | wrapperMethod: wrapWebhook, 56 | actionMethod: 'delete', 57 | }) 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /test/unit/mocks/http.ts: -------------------------------------------------------------------------------- 1 | import type { Mock } from 'vitest' 2 | import { vi } from 'vitest' 3 | 4 | interface MockedHttp extends Mock<[T], R> { 5 | get?: Mock<[T], R> 6 | post?: Mock<[T], R> 7 | put?: Mock<[T], R> 8 | patch?: Mock<[T], R> 9 | delete?: Mock<[T], R> 10 | defaults?: { baseURL: string } 11 | httpClientParams?: { hostUpload: string } 12 | cloneWithNewParams?: () => MockedHttp 13 | } 14 | 15 | export default function setupHttpMock(promise = Promise.resolve({ data: {} })) { 16 | const mock: MockedHttp = vi.fn().mockImplementation(() => { 17 | console.log('Mock: Returning promise via direct call') 18 | return promise 19 | }) 20 | 21 | mock.get = vi.fn().mockImplementation(() => { 22 | console.log('Mock: Returning promise via get call') 23 | return promise 24 | }) 25 | mock.post = vi.fn().mockImplementation(() => { 26 | console.log('Mock: Returning promise via post call') 27 | return promise 28 | }) 29 | mock.put = vi.fn().mockImplementation(() => { 30 | console.log('Mock: Returning promise via put call') 31 | return promise 32 | }) 33 | mock.patch = vi.fn().mockImplementation(() => { 34 | console.log('Mock: Returning promise via patch call') 35 | return promise 36 | }) 37 | mock.delete = vi.fn().mockImplementation(() => { 38 | console.log('Mock: Returning promise via delete call') 39 | return promise 40 | }) 41 | 42 | mock.defaults = { 43 | baseURL: 'https://api.contentful.com/spaces/', 44 | } 45 | mock.httpClientParams = { 46 | hostUpload: 'upload.contentful.com', 47 | } 48 | 49 | mock.cloneWithNewParams = () => mock 50 | 51 | return mock as Required> 52 | } 53 | -------------------------------------------------------------------------------- /test/unit/entities/organization-membership.test.ts: -------------------------------------------------------------------------------- 1 | import { cloneMock } from '../mocks/entities' 2 | import setupMakeRequest from '../mocks/makeRequest' 3 | import { 4 | wrapOrganizationMembership, 5 | wrapOrganizationMembershipCollection, 6 | } from '../../../lib/entities/organization-membership' 7 | import { 8 | entityCollectionWrappedTest, 9 | entityWrappedTest, 10 | entityUpdateTest, 11 | failingActionTest, 12 | } from '../test-creators/instance-entity-methods' 13 | import { describe, test } from 'vitest' 14 | 15 | function setup(promise) { 16 | return { 17 | makeRequest: setupMakeRequest(promise), 18 | entityMock: cloneMock('organizationMembership'), 19 | } 20 | } 21 | 22 | describe('Entity OrganizationMembership', () => { 23 | test('OrganizationMembership is wrapped', async () => { 24 | return entityWrappedTest(setup, { 25 | wrapperMethod: wrapOrganizationMembership, 26 | }) 27 | }) 28 | 29 | test('OrganizationMembership collection is wrapped', async () => { 30 | return entityCollectionWrappedTest(setup, { 31 | wrapperMethod: wrapOrganizationMembershipCollection, 32 | }) 33 | }) 34 | 35 | test('OrganizationMembership update', async () => { 36 | return entityUpdateTest(setup, { 37 | wrapperMethod: wrapOrganizationMembership, 38 | }) 39 | }) 40 | 41 | test('OrganizationMembership update fails', async () => { 42 | return failingActionTest(setup, { 43 | wrapperMethod: wrapOrganizationMembership, 44 | actionMethod: 'update', 45 | }) 46 | }) 47 | 48 | test('OrganizationMembership delete fails', async () => { 49 | return failingActionTest(setup, { 50 | wrapperMethod: wrapOrganizationMembership, 51 | actionMethod: 'delete', 52 | }) 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /lib/adapters/REST/endpoints/user.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosInstance } from 'contentful-sdk-core' 2 | import type { 3 | CollectionProp, 4 | GetOrganizationParams, 5 | GetSpaceParams, 6 | QueryParams, 7 | } from '../../../common-types' 8 | import type { UserProps } from '../../../entities/user' 9 | import type { RestEndpoint } from '../types' 10 | import * as raw from './raw' 11 | 12 | export const getForSpace: RestEndpoint<'User', 'getForSpace'> = ( 13 | http: AxiosInstance, 14 | params: GetSpaceParams & { userId: string }, 15 | ) => { 16 | return raw.get(http, `/spaces/${params.spaceId}/users/${params.userId}`) 17 | } 18 | 19 | export const getCurrent: RestEndpoint<'User', 'getCurrent'> = ( 20 | http: AxiosInstance, 21 | params?: QueryParams, 22 | ) => raw.get(http, `/users/me`, { params: params?.query }) 23 | 24 | export const getManyForSpace: RestEndpoint<'User', 'getManyForSpace'> = ( 25 | http: AxiosInstance, 26 | params: GetSpaceParams & QueryParams, 27 | ) => { 28 | return raw.get>(http, `/spaces/${params.spaceId}/users`, { 29 | params: params.query, 30 | }) 31 | } 32 | 33 | export const getForOrganization: RestEndpoint<'User', 'getForOrganization'> = ( 34 | http: AxiosInstance, 35 | params: GetOrganizationParams & { userId: string }, 36 | ) => { 37 | return raw.get(http, `/organizations/${params.organizationId}/users/${params.userId}`) 38 | } 39 | 40 | export const getManyForOrganization: RestEndpoint<'User', 'getManyForOrganization'> = ( 41 | http: AxiosInstance, 42 | params: GetOrganizationParams & QueryParams, 43 | ) => { 44 | return raw.get>(http, `/organizations/${params.organizationId}/users`, { 45 | params: params.query, 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /lib/plain/entities/user-ui-config.ts: -------------------------------------------------------------------------------- 1 | import type { GetUserUIConfigParams } from '../../common-types' 2 | import type { UserUIConfigProps } from '../../entities/user-ui-config' 3 | import type { OptionalDefaults } from '../wrappers/wrap' 4 | 5 | export type UserUIConfigPlainClientAPI = { 6 | /** 7 | * Fetch the UI Config for the current user in a given Space and Environment 8 | * @param params entity IDs to identify the UI Config 9 | * @returns the UI Config 10 | * @throws if the request fails, or the UI Config is not found 11 | * @example 12 | * ```javascript 13 | * const uiConfig = await client.userUIConfig.get({ 14 | * spaceId: "", 15 | * environmentId: "", 16 | * }); 17 | * ``` 18 | */ 19 | get(params: OptionalDefaults): Promise 20 | /** 21 | * Update the UI Config for for the current user in a given Space and Environment 22 | * @param params entity IDs to identify the UI Config 23 | * @param rawData the UI Config update 24 | * @returns the updated UI Config 25 | * @throws if the request fails, the UI Config is not found, or the update payload is malformed 26 | * @example 27 | * ```javascript 28 | * await client.userUIConfig.update({ 29 | * spaceId: "", 30 | * environmentId: "", 31 | * }, { 32 | * ...currentUIConfig, 33 | * entryListViews: [ 34 | * ...currentUIConfig.entryListViews, 35 | * { 36 | * id: 'newFolder', 37 | * title: 'New Folder', 38 | * views: [] 39 | * } 40 | * ], 41 | * }); 42 | * ``` 43 | */ 44 | update( 45 | params: OptionalDefaults, 46 | rawData: UserUIConfigProps, 47 | ): Promise 48 | } 49 | -------------------------------------------------------------------------------- /lib/entities/app-signed-request.ts: -------------------------------------------------------------------------------- 1 | import copy from 'fast-copy' 2 | import { toPlainObject } from 'contentful-sdk-core' 3 | import type { Except } from 'type-fest' 4 | import type { BasicMetaSysProps, DefaultElements, MakeRequest, SysLink } from '../common-types' 5 | 6 | type AppSignedRequestSys = Except & { 7 | appDefinition: SysLink 8 | space: SysLink 9 | environment: SysLink 10 | } 11 | 12 | export type AppSignedRequestProps = { 13 | /** 14 | * System metadata 15 | */ 16 | sys: AppSignedRequestSys 17 | /** new headers to be included in the request */ 18 | additionalHeaders: { 19 | 'x-contentful-signature': string 20 | 'x-contentful-signed-headers': string 21 | 'x-contentful-timestamp': string 22 | 'x-contentful-space-id': string 23 | 'x-contentful-environment-id': string 24 | 'x-contentful-user-id': string 25 | } 26 | } 27 | 28 | export type CreateAppSignedRequestProps = { 29 | /** the request method */ 30 | method: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'PATCH' | 'HEAD' 31 | /** the path of the request method */ 32 | path: string 33 | /** optional stringified body of the request */ 34 | body?: string 35 | /** optional headers of the request */ 36 | headers?: Record 37 | } 38 | 39 | export interface AppSignedRequest 40 | extends AppSignedRequestProps, 41 | DefaultElements {} 42 | 43 | /** 44 | * @private 45 | * @param http - HTTP client instance 46 | * @param data - Raw AppSignedRequest data 47 | * @return Wrapped AppSignedRequest data 48 | */ 49 | export function wrapAppSignedRequest( 50 | _makeRequest: MakeRequest, 51 | data: AppSignedRequestProps, 52 | ): AppSignedRequest { 53 | const signedRequest = toPlainObject(copy(data)) 54 | return signedRequest 55 | } 56 | -------------------------------------------------------------------------------- /lib/entities/user.ts: -------------------------------------------------------------------------------- 1 | import { freezeSys, toPlainObject } from 'contentful-sdk-core' 2 | import copy from 'fast-copy' 3 | import enhanceWithMethods from '../enhance-with-methods' 4 | import { wrapCollection } from '../common-utils' 5 | import type { DefaultElements, BasicMetaSysProps, MakeRequest } from '../common-types' 6 | 7 | export type UserProps = { 8 | /** 9 | * System metadata 10 | */ 11 | sys: BasicMetaSysProps 12 | 13 | /** 14 | * First name of the user 15 | */ 16 | firstName: string 17 | 18 | /** 19 | * Last name of the user 20 | */ 21 | lastName: string 22 | 23 | /** 24 | * Url to the users avatar 25 | */ 26 | avatarUrl: string 27 | 28 | /** 29 | * Email address of the user 30 | */ 31 | email: string 32 | 33 | /** 34 | * Activation flag 35 | */ 36 | activated: boolean 37 | 38 | /** 39 | * Number of sign ins 40 | */ 41 | signInCount: number 42 | 43 | /** 44 | * User confirmation flag 45 | */ 46 | confirmed: boolean 47 | 48 | '2faEnabled': boolean 49 | cookieConsentData: string 50 | } 51 | 52 | export interface User extends UserProps, DefaultElements {} 53 | 54 | /** 55 | * @private 56 | * @param makeRequest - function to make requests via an adapter 57 | * @param data - Raw data 58 | * @return Normalized user 59 | */ 60 | export function wrapUser(_makeRequest: MakeRequest, data: T) { 61 | const user = toPlainObject(copy(data)) 62 | const userWithMethods = enhanceWithMethods(user, {}) 63 | return freezeSys(userWithMethods) 64 | } 65 | 66 | /** 67 | * @private 68 | * @param makeRequest - function to make requests via an adapter 69 | * @param data - Raw data collection 70 | * @return Normalized user collection 71 | */ 72 | export const wrapUserCollection = wrapCollection(wrapUser) 73 | -------------------------------------------------------------------------------- /test/integration/team-integration.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, beforeAll, expect, afterAll } from 'vitest' 2 | import { getTestOrganization, timeoutToCalmRateLimiting } from '../helpers' 3 | import { TestDefaults } from '../defaults' 4 | import type { Organization } from '../../lib/export-types' 5 | 6 | const { teamId, teamName } = TestDefaults 7 | 8 | describe('Team API', () => { 9 | let organization: Organization 10 | 11 | beforeAll(async () => { 12 | organization = await getTestOrganization() 13 | }) 14 | 15 | afterAll(timeoutToCalmRateLimiting) 16 | 17 | it('Gets teams', async () => { 18 | const response = await organization.getTeams() 19 | 20 | expect(response.sys).toBeTruthy() 21 | expect(response.items).toBeTruthy() 22 | expect(response.items[0].sys.type).toBe('Team') 23 | }) 24 | 25 | it('Gets team by ID', async () => { 26 | const response = await organization.getTeam(teamId) 27 | 28 | expect(response.sys).toBeTruthy() 29 | expect(response.sys.id).toBe(teamId) 30 | expect(response.sys.type).toBe('Team') 31 | expect(response.name).toBe(teamName) 32 | }) 33 | 34 | it('Creates, updates, and deletes a team', async () => { 35 | const team = await organization.createTeam({ 36 | name: 'test team', 37 | description: 'mocked', 38 | }) 39 | 40 | expect(team.sys).toBeTruthy() 41 | expect(team.name).toBe('test team') 42 | expect(team.sys.type).toBe('Team') 43 | 44 | team.description = 'test description' 45 | const updatedTeam = await team.update() 46 | 47 | expect(updatedTeam.sys).toBeTruthy() 48 | expect(updatedTeam.name).toBe('test team') 49 | expect(updatedTeam.sys.type).toBe('Team') 50 | expect(updatedTeam.description).toBe('test description') 51 | 52 | await updatedTeam.delete() 53 | }) 54 | }) 55 | --------------------------------------------------------------------------------