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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .talismanrc
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,12 @@ fileignoreconfig:
checksum: 9e7a4696561b790cb93f3be8406a70ec6fdc90a3f8bbb9739504495690158fe3
- filename: src/query/term-query.ts
checksum: 1f5b23177460d562076d93cf28b375106b19123a5ab135ffef75f4b2bb332d35
- filename: test/bundlers/run-with-report.sh
checksum: fedb0c262e3d88ad3537943e828d8ed9412a9f7d78b6406997b3955b29816f20
- filename: test/utils/assertion-tracker.ts
checksum: f02ce0af5948cd813020367c21da2cd0cd00168eeeb9e8af1858b852ae83e269
- filename: test/utils/request-capture-plugin.ts
checksum: 596fbbbf4aace2431dc165208a81f1a03c5f1d5268aceda83385debeaba79b97
- filename: test/reporting/rich-html-reporter.cjs
checksum: 1da275d7d083cc671a3888b1a045a616f79ac1fe023ee64ea34f0f23ddbc3706
version: "1.0"
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
### Version: 5.5.0
#### Date: Jul-27-2026
Enhancement: Entry variants support an optional branch name as the second argument to `variants()` on `Entry` and `Entries`. When provided, the branch is sent as the `branch` request header together with `x-cs-variant-uid`. Existing `variants(uid)` and `variants(uids)` calls remain backward compatible. Added unit and API tests for variant + branch requests.

### Version: 5.4.0
#### Date: Jul-16-2026
Enhancement: Removed `locale?` parameter from `Taxonomy.fetch()` and `Term.fetch()` — locale is now set via the chainable `.param('locale', value)` API, consistent with other query modifiers.
Expand Down
14 changes: 5 additions & 9 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,12 @@ export default {
includeConsoleLog: true,
},
],
// Rich single-file HTML report with inline per-test HTTP context (cURL,
// SDK method, request/response). Fixed path (the one the GoCD pipelines link to);
// prints the absolute path at run end.
[
"jest-html-reporters",
{
publicPath: "./reports/contentstack-delivery/html",
filename: "index.html",
expand: true,
// Enable console log capture in reports
enableMergeData: true,
dataMergeLevel: 2,
},
"./test/reporting/rich-html-reporter.cjs",
{ outputPath: "reports/contentstack-delivery/html/index.html" },
],
[
"jest-junit",
Expand Down
52 changes: 51 additions & 1 deletion jest.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@
*/
import * as fs from 'fs';
import * as path from 'path';
import {
getLastCapturedRequest,
clearCapturedRequests,
} from './test/utils/request-capture-plugin';
import {
installAssertionTracker,
clearAssertions,
getAssertions,
} from './test/utils/assertion-tracker';

// Store captured console logs
interface ConsoleLog {
Expand Down Expand Up @@ -37,7 +46,7 @@ const originalConsole = {
const expectedErrors = [
'Invalid key:', // From query.search() validation
'Invalid value (expected string or number):', // From query.equalTo() validation
'Argument should be a String or an Array.', // From entry/entries.includeReference() validation
'Invalid argument. Provide a string or an array', // From entry/entries.includeReference() validation (ErrorMessages.INVALID_ARGUMENT_STRING_OR_ARRAY)
'Invalid fieldUid:', // From asset query validation
];

Expand Down Expand Up @@ -76,6 +85,47 @@ console.error = captureConsole('error');
console.info = captureConsole('info');
console.debug = captureConsole('debug');

// ---------------------------------------------------------------------------
// Rich per-test HTTP context (cURL / SDK method / request+response / status).
// Active only when ENABLE_HTTP_CAPTURE=true (the request-capture plugin is
// attached to the stack instance under the same flag). Each test's last
// captured HTTP call is appended to a JSONL sidecar that the custom
// rich-html-reporter reads at run-end to build the single-file HTML report.
// ---------------------------------------------------------------------------
const HTTP_CAPTURE_ENABLED = process.env.ENABLE_HTTP_CAPTURE === 'true';
const CAPTURES_FILE = path.resolve(__dirname, 'test-results', 'http-captures.jsonl');

if (HTTP_CAPTURE_ENABLED) {
beforeEach(() => {
// Install inside beforeEach so it runs AFTER the spec's `import { expect } from
// '@jest/globals'` has resolved the shared module object (idempotent via a guard).
// Records every assertion (expected/actual/pass) without changing any test.
installAssertionTracker();
clearCapturedRequests();
clearAssertions();
});

afterEach(() => {
try {
const cap = getLastCapturedRequest();
const assertions = getAssertions();
if (!cap && assertions.length === 0) return;
const state: any = (expect as any).getState();
const rec = {
testPath: state.testPath,
testName: state.currentTestName,
capture: cap || null,
assertions,
};
const dir = path.dirname(CAPTURES_FILE);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
fs.appendFileSync(CAPTURES_FILE, JSON.stringify(rec) + '\n');
} catch {
// never let reporting break a test
}
});
}

// After all tests complete, write logs to file
afterAll(() => {
const logsPath = path.resolve(__dirname, 'test-results', 'console-logs.json');
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@contentstack/delivery-sdk",
"version": "5.4.0",
"version": "5.5.0",
"type": "module",
"license": "MIT",
"engines": {
Expand All @@ -26,11 +26,11 @@
"prepare": "npm run build",
"test": "jest ./test/unit",
"test:unit": "jest ./test/unit",
"test:api": "jest ./test/api",
"test:api": "ENABLE_HTTP_CAPTURE=true jest ./test/api",
"test:browser": "jest --config jest.config.browser.ts",
"test:e2e": "node test/e2e/build-browser-bundle.js && playwright test",
"test:e2e:ui": "node test/e2e/build-browser-bundle.js && playwright test --ui",
"test:api:report": "jest ./test/api --json --outputFile=test-results/jest-results.json",
"test:api:report": "ENABLE_HTTP_CAPTURE=true jest ./test/api --json --outputFile=test-results/jest-results.json",
"test:bundlers:report": "cd test/bundlers && ./run-with-report.sh",
"test:cicd": "mkdir -p test-results && npm run test:api:report && npm run test:bundlers:report && npm run test:e2e && node test/reporting/generate-unified-report.js",
"test:cicd:no-browser": "mkdir -p test-results && npm run test:api:report && npm run test:bundlers:report && node test/reporting/generate-unified-report.js",
Expand Down
21 changes: 21 additions & 0 deletions src/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,24 @@ export function encodeQueryParams(params: params): params {

return encodedParams;
}

/**
* Builds request headers for entry variant requests.
* @param variants - Comma-separated variant UID(s)
* @param branch - Optional branch name to scope the variant request
*/
export function buildVariantRequestHeaders(
variants: string,
branch?: string
): Record<string, string> | undefined {
const headers: Record<string, string> = {};

if (variants) {
headers['x-cs-variant-uid'] = variants;
}
if (branch) {
headers.branch = branch;
}

return Object.keys(headers).length > 0 ? headers : undefined;
}
24 changes: 17 additions & 7 deletions src/entries/entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { AxiosInstance, getData } from '@contentstack/core';
import { Query } from '../query';
import { BaseQuery } from '../query';
import { FindResponse } from '../common/types';
import { encodeQueryParams } from '../common/utils';
import { buildVariantRequestHeaders, encodeQueryParams } from '../common/utils';
import { ErrorMessages } from '../common/error-messages';

export class Entries extends BaseQuery {
Expand All @@ -14,6 +14,7 @@ export class Entries extends BaseQuery {
this._contentTypeUid = contentTypeUid;
this._urlPath = `/content_types/${this._contentTypeUid}/entries`;
this._variants = '';
this._variantsBranch = '';
}

/**
Expand Down Expand Up @@ -252,28 +253,36 @@ export class Entries extends BaseQuery {
* const query = stack.contentType("contentTypeUid").entry().query();
*/
query(queryObj?: { [key: string]: any }) {
if (queryObj) return new Query(this._client, this._parameters, this._queryParams, this._variants, this._contentTypeUid, queryObj);
if (queryObj) {
return new Query(this._client, this._parameters, this._queryParams, this._variants, this._contentTypeUid, this._variantsBranch, queryObj);
}

return new Query(this._client, this._parameters, this._queryParams, this._variants, this._contentTypeUid);
return new Query(this._client, this._parameters, this._queryParams, this._variants, this._contentTypeUid, this._variantsBranch);
}

/**
* @method variants
* @memberof Entries
* @description The variant header will be added to axios client
* @description Stores the variant UID(s) and optional branch name, which are sent as the `x-cs-variant-uid` and `branch` headers on the request when find() is called.
* @param {string | string[]} variants - Variant UID or UIDs
* @param {string} [branchName] - Optional branch name sent as the `branch` header
* @returns {Entries}
* @example
* import contentstack from '@contentstack/delivery-sdk'
*
* const stack = contentstack.stack({ apiKey: "apiKey", deliveryToken: "deliveryToken", environment: "environment" });
* const result = await stack.contentType('abc').entry().variants('xyz').find();
* const resultWithBranch = await stack.contentType('abc').entry().variants('xyz', 'branch_name').find();
*/
variants(variants: string | string[]): Entries {
variants(variants: string | string[], branchName?: string): Entries {
if (Array.isArray(variants) && variants.length > 0) {
this._variants = variants.join(',');
} else if (typeof variants == 'string' && variants.length > 0) {
this._variants = variants;
}
if (typeof branchName === 'string' && branchName.length > 0) {
this._variantsBranch = branchName;
}
return this;
}

Expand Down Expand Up @@ -320,10 +329,11 @@ export class Entries extends BaseQuery {
contentTypeUid: this._contentTypeUid
};

if (this._variants) {
const variantHeaders = buildVariantRequestHeaders(this._variants, this._variantsBranch);
if (variantHeaders) {
getRequestOptions.headers = {
...getRequestOptions.headers,
'x-cs-variant-uid': this._variants
...variantHeaders
};
}
const response = await getData(this._client, this._urlPath, getRequestOptions);
Expand Down
18 changes: 14 additions & 4 deletions src/entries/entry.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { AxiosInstance, getData } from '@contentstack/core';
import { ErrorMessages } from '../common/error-messages';
import { buildVariantRequestHeaders } from '../common/utils';

interface EntryResponse<T> {
entry: T;
Expand All @@ -10,13 +11,15 @@ export class Entry {
private _entryUid: string;
private _urlPath: string;
protected _variants: string;
protected _variantsBranch: string;
_queryParams: { [key: string]: string | number | string[] } = {};
constructor(client: AxiosInstance, contentTypeUid: string, entryUid: string) {
this._client = client;
this._contentTypeUid = contentTypeUid;
this._entryUid = entryUid;
this._urlPath = `/content_types/${this._contentTypeUid}/entries/${this._entryUid}`;
this._variants = '';
this._variantsBranch = '';
}

/**
Expand All @@ -39,20 +42,26 @@ export class Entry {
/**
* @method variants
* @memberof Entry
* @description The variant header will be added to axios client
* @description Stores the variant UID(s) and optional branch name, which are sent as the `x-cs-variant-uid` and `branch` headers on the request when fetch() is called.
* @param {string | string[]} variants - Variant UID or UIDs
* @param {string} [branchName] - Optional branch name sent as the `branch` header
* @returns {Entry}
* @example
* import contentstack from '@contentstack/delivery-sdk'
*
* const stack = contentstack.stack({ apiKey: "apiKey", deliveryToken: "deliveryToken", environment: "environment" });
* const result = await stack.contentType('abc').entry('entry_uid').variants('xyz').fetch();
* const resultWithBranch = await stack.contentType('abc').entry('entry_uid').variants('xyz', 'branch_name').fetch();
*/
variants(variants: string | string[]): this {
variants(variants: string | string[], branchName?: string): this {
if (Array.isArray(variants) && variants.length > 0) {
this._variants = variants.join(',');
} else if (typeof variants == 'string' && variants.length > 0) {
this._variants = variants;
}
if (typeof branchName === 'string' && branchName.length > 0) {
this._variantsBranch = branchName;
}

return this;
}
Expand Down Expand Up @@ -189,10 +198,11 @@ export class Entry {
contentTypeUid: this._contentTypeUid,
entryUid: this._entryUid
};
if (this._variants) {
const variantHeaders = buildVariantRequestHeaders(this._variants, this._variantsBranch);
if (variantHeaders) {
getRequestOptions.headers = {
...getRequestOptions.headers,
'x-cs-variant-uid': this._variants
...variantHeaders
};
}

Expand Down
8 changes: 5 additions & 3 deletions src/query/base-query.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { AxiosInstance, getData } from '@contentstack/core';
import { Pagination } from '../common/pagination';
import { FindResponse, params } from '../common/types';
import { encodeQueryParams } from '../common/utils';
import { buildVariantRequestHeaders, encodeQueryParams } from '../common/utils';
import type { Query } from './query';

export class BaseQuery extends Pagination {
Expand All @@ -10,6 +10,7 @@ export class BaseQuery extends Pagination {
protected _client!: AxiosInstance;
protected _urlPath!: string;
protected _variants!: string;
protected _variantsBranch!: string;

/**
* Helper method to cast this instance to Query type
Expand Down Expand Up @@ -231,10 +232,11 @@ export class BaseQuery extends Pagination {
contentTypeUid: this.extractContentTypeUidFromUrl()
};

if (this._variants) {
const variantHeaders = buildVariantRequestHeaders(this._variants, this._variantsBranch);
if (variantHeaders) {
getRequestOptions.headers = {
...getRequestOptions.headers,
'x-cs-variant-uid': this._variants
...variantHeaders
};
}
const response = await getData(this._client, this._urlPath, getRequestOptions);
Expand Down
Loading
Loading