Skip to content

Commit 85fd620

Browse files
author
naman-contentstack
committed
fix: fix edge cases for AM
1 parent 78c00d5 commit 85fd620

6 files changed

Lines changed: 140 additions & 61 deletions

File tree

.talismanrc

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,4 @@
11
fileignoreconfig:
2-
- filename: pnpm-lock.yaml
3-
checksum: 069d87fc69d059bd53fa46d98916e831641ea4889cebcc9b9b30884ac67dab30
4-
- filename: packages/contentstack-branches/README.md
5-
checksum: ad32bd365db7f085cc2ea133d69748954606131ec6157a272a3471aea60011c2
6-
- filename: packages/contentstack-branches/src/branch/diff-handler.ts
7-
checksum: 3cd4d26a2142cab7cbf2094c9251e028467d17d6a1ed6daf22f21975133805f1
8-
- filename: packages/contentstack-branches/src/commands/cm/branches/merge-status.ts
9-
checksum: 6e5b959ddcc5ff68e03c066ea185fcf6c6e57b1819069730340af35aad8a93a8
10-
- filename: packages/contentstack-branches/src/utils/create-branch.ts
11-
checksum: d0613295ee26f7a77d026e40db0a4ab726fabd0a74965f729f1a66d1ef14768f
12-
- filename: packages/contentstack-branches/src/branch/merge-handler.ts
13-
checksum: 4fd8dba9b723733530b9ba12e81e1d3e5d60b73ac4c082defb10593f257bb133
2+
- filename: packages/contentstack-import/src/utils/import-config-handler.ts
3+
checksum: 3194f537cee8041f07a7ea91cdc6351c84e400766696d9c3cf80b98f99961f76
144
version: '1.0'

packages/contentstack-asset-management/src/export/spaces.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export class ExportSpaces {
5151
log.debug(`Exporting Asset Management 2.0 (${linkedWorkspaces.length} space(s))`, context);
5252
log.debug(`Spaces: ${linkedWorkspaces.map((ws) => ws.space_uid).join(', ')}`, context);
5353

54-
const spacesRootPath = pResolve(exportDir, branchName || 'main', 'spaces');
54+
const spacesRootPath = pResolve(exportDir, 'spaces');
5555
await mkdir(spacesRootPath, { recursive: true });
5656
log.debug(`Spaces root path: ${spacesRootPath}`, context);
5757

packages/contentstack-asset-management/src/utils/asset-management-api-adapter.ts

Lines changed: 103 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,41 @@ export class AssetManagementAdapter implements IAssetManagementAdapter {
5353
return '?' + parts.join('&');
5454
}
5555

56+
/**
57+
* Format response body or payload for error logging. Safely stringifies and truncates.
58+
*/
59+
private formatResponseBodyForError(data: unknown, maxLen: number = 500): string {
60+
if (data === null || data === undefined) return '';
61+
try {
62+
const str = typeof data === 'string' ? data : JSON.stringify(data);
63+
return str.length > maxLen ? str.substring(0, maxLen) + '...' : str;
64+
} catch {
65+
return '';
66+
}
67+
}
68+
69+
/**
70+
* Normalize AM API failures into a consistent error message with optional cause and body snippet.
71+
*/
72+
private normalizeAmGetFailure(details: {
73+
path: string;
74+
fullPath: string;
75+
status?: number;
76+
cause?: unknown;
77+
bodySnippet?: string;
78+
}): Error {
79+
const { path, status, cause, bodySnippet } = details;
80+
let message = `AM API GET failed: path ${path}`;
81+
if (status) message += ` (status ${status})`;
82+
if (cause && cause instanceof Error) {
83+
message += ` - ${cause.message}`;
84+
} else if (cause) {
85+
message += ` - ${String(cause)}`;
86+
}
87+
if (bodySnippet) message += `\nResponse: ${bodySnippet}`;
88+
return new Error(message);
89+
}
90+
5691
/**
5792
* GET a space-level endpoint (e.g. /api/spaces/{uid}). Builds path + query string and performs the request.
5893
*/
@@ -73,11 +108,29 @@ export class AssetManagementAdapter implements IAssetManagementAdapter {
73108
const queryString = this.buildQueryString(safeParams);
74109
const fullPath = path + queryString;
75110
log.debug(`GET ${fullPath}`, this.config.context);
76-
const response = await this.apiClient.get<T>(fullPath);
77-
if (response.status < 200 || response.status >= 300) {
78-
throw new Error(`Asset Management API error: status ${response.status}, path ${path}`);
111+
112+
try {
113+
const response = await this.apiClient.get<T>(fullPath);
114+
if (response.status < 200 || response.status >= 300) {
115+
const bodySnippet = this.formatResponseBodyForError(response.data);
116+
throw this.normalizeAmGetFailure({
117+
path,
118+
fullPath,
119+
status: response.status,
120+
bodySnippet: bodySnippet || undefined,
121+
});
122+
}
123+
return response.data as T;
124+
} catch (error) {
125+
if (error instanceof Error && error.message.includes('AM API GET failed')) {
126+
throw error;
127+
}
128+
throw this.normalizeAmGetFailure({
129+
path,
130+
fullPath,
131+
cause: error,
132+
});
79133
}
80-
return response.data as T;
81134
}
82135

83136
async init(): Promise<void> {
@@ -196,32 +249,60 @@ export class AssetManagementAdapter implements IAssetManagementAdapter {
196249
const baseUrl = this.config.baseURL?.replace(/\/$/, '') ?? '';
197250
const headers = await this.getPostHeaders({ 'Content-Type': 'application/json', ...extraHeaders });
198251
log.debug(`POST ${path}`, this.config.context);
199-
const response = await fetch(`${baseUrl}${path}`, {
200-
method: 'POST',
201-
headers,
202-
body: JSON.stringify(body),
203-
});
204-
if (!response.ok) {
205-
const text = await response.text().catch(() => '');
206-
throw new Error(`AM API POST error: status ${response.status}, path ${path}, body: ${text}`);
252+
253+
try {
254+
const response = await fetch(`${baseUrl}${path}`, {
255+
method: 'POST',
256+
headers,
257+
body: JSON.stringify(body),
258+
});
259+
if (!response.ok) {
260+
const text = await response.text().catch(() => '');
261+
const bodySnippet = this.formatResponseBodyForError(text);
262+
throw new Error(
263+
`AM API POST failed: status ${response.status} path ${path}${
264+
bodySnippet ? `\nResponse: ${bodySnippet}` : ''
265+
}`,
266+
);
267+
}
268+
return response.json() as Promise<T>;
269+
} catch (error) {
270+
if (error instanceof Error && error.message.includes('AM API POST failed')) {
271+
throw error;
272+
}
273+
throw new Error(`AM API POST failed: path ${path} - ${error instanceof Error ? error.message : String(error)}`);
207274
}
208-
return response.json() as Promise<T>;
209275
}
210276

211277
private async postMultipart<T>(path: string, form: FormData, extraHeaders: Record<string, string> = {}): Promise<T> {
212278
const baseUrl = this.config.baseURL?.replace(/\/$/, '') ?? '';
213279
const headers = await this.getPostHeaders(extraHeaders);
214280
log.debug(`POST (multipart) ${path}`, this.config.context);
215-
const response = await fetch(`${baseUrl}${path}`, {
216-
method: 'POST',
217-
headers,
218-
body: form,
219-
});
220-
if (!response.ok) {
221-
const text = await response.text().catch(() => '');
222-
throw new Error(`AM API multipart POST error: status ${response.status}, path ${path}, body: ${text}`);
281+
282+
try {
283+
const response = await fetch(`${baseUrl}${path}`, {
284+
method: 'POST',
285+
headers,
286+
body: form,
287+
});
288+
if (!response.ok) {
289+
const text = await response.text().catch(() => '');
290+
const bodySnippet = this.formatResponseBodyForError(text);
291+
throw new Error(
292+
`AM API multipart POST failed: status ${response.status} path ${path}${
293+
bodySnippet ? `\nResponse: ${bodySnippet}` : ''
294+
}`,
295+
);
296+
}
297+
return response.json() as Promise<T>;
298+
} catch (error) {
299+
if (error instanceof Error && error.message.includes('AM API multipart POST failed')) {
300+
throw error;
301+
}
302+
throw new Error(
303+
`AM API multipart POST failed: path ${path} - ${error instanceof Error ? error.message : String(error)}`,
304+
);
223305
}
224-
return response.json() as Promise<T>;
225306
}
226307

227308
// ---------------------------------------------------------------------------

packages/contentstack-export/src/export/module-exporter.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,12 +83,11 @@ class ModuleExporter {
8383
this.exportConfig.context,
8484
);
8585
} catch (error) {
86-
handleAndLogError(
87-
error,
88-
{ ...this.exportConfig.context, branch: targetBranch?.uid },
89-
messageHandler.parse('FAILED_EXPORT_CONTENT_BRANCH', { branch: targetBranch?.uid }),
90-
);
91-
throw new Error(messageHandler.parse('FAILED_EXPORT_CONTENT_BRANCH', { branch: targetBranch?.uid }));
86+
const originalMessage = (error as Error)?.message ?? '';
87+
const errorMessage =
88+
originalMessage || messageHandler.parse('FAILED_EXPORT_CONTENT_BRANCH', { branch: targetBranch?.uid });
89+
handleAndLogError(error, { ...this.exportConfig.context, branch: targetBranch?.uid }, errorMessage);
90+
throw new Error(errorMessage);
9291
}
9392
}
9493

packages/contentstack-export/src/export/modules/assets.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import map from 'lodash/map';
2-
import { getChalk } from '@contentstack/cli-utilities';
2+
import { cliux, getChalk } from '@contentstack/cli-utilities';
33
import chunk from 'lodash/chunk';
44
import first from 'lodash/first';
55
import merge from 'lodash/merge';
@@ -34,6 +34,7 @@ import {
3434
MODULE_NAMES,
3535
getOrgUid,
3636
} from '../../utils';
37+
import { handle } from '@oclif/core';
3738

3839
export default class ExportAssets extends BaseClass {
3940
private assetsRootPath: string;
@@ -61,13 +62,27 @@ export default class ExportAssets extends BaseClass {
6162
if (linkedWorkspaces.length > 0) {
6263
const assetManagementUrl = this.exportConfig.region?.assetManagementUrl;
6364
if (!assetManagementUrl) {
64-
this.completeProgress(
65-
false,
66-
'Asset Management URL is required for AM 2.0 export. Ensure your region is configured with assetManagementUrl.',
65+
handleAndLogError(
66+
new Error(
67+
'Asset Management URL is required for AM 2.0 export. Ensure your region is configured with assetManagementUrl.',
68+
),
69+
{
70+
...this.exportConfig.context,
71+
message:
72+
'Asset Management URL is required for AM 2.0 export. Ensure your region is configured with assetManagementUrl.',
73+
},
6774
);
68-
throw new Error(
75+
this.completeProgressWithMessage({
76+
moduleName: 'Asset Management 2.0',
77+
customWarningMessage:
78+
'Asset Management 2.0 export was skipped: assetManagementUrl is not configured. AM 2.0 assets will not be exported.',
79+
context: this.exportConfig.context,
80+
});
81+
cliux.print(
6982
'Asset Management URL is required for AM 2.0 export. Ensure your region is configured with assetManagementUrl.',
83+
{ color: 'yellow' },
7084
);
85+
return;
7186
}
7287
log.debug(
7388
`Exporting with AM 2.0: ${assetManagementUrl} (linked_workspaces from exportConfig)`,

packages/contentstack-import/src/utils/import-config-handler.ts

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ const setupConfig = async (importCmdFlags: any): Promise<ImportConfig> => {
127127

128128
const spacesDir = path.join(config.contentDir, 'spaces');
129129
const stackSettingsPath = path.join(config.contentDir, 'stack', 'settings.json');
130+
const stackJsonPath = path.join(config.contentDir, 'stack', 'stack.json');
130131

131132
if (existsSync(spacesDir) && existsSync(stackSettingsPath)) {
132133
try {
@@ -135,22 +136,15 @@ const setupConfig = async (importCmdFlags: any): Promise<ImportConfig> => {
135136
config.assetManagementEnabled = true;
136137
config.assetManagementUrl = configHandler.get('region')?.assetManagementUrl;
137138

138-
const branchesJsonCandidates = [
139-
path.join(config.contentDir, 'branches.json'),
140-
path.join(config.contentDir, '..', 'branches.json'),
141-
];
142-
for (const branchesJsonPath of branchesJsonCandidates) {
143-
if (existsSync(branchesJsonPath)) {
144-
try {
145-
const branches = JSON.parse(readFileSync(branchesJsonPath, 'utf8'));
146-
const apiKey = branches?.[0]?.stackHeaders?.api_key;
147-
if (apiKey) {
148-
config.source_stack = apiKey;
149-
}
150-
} catch {
151-
// branches.json unreadable — URL mapping will be skipped
139+
if (existsSync(stackJsonPath)) {
140+
try {
141+
const stackData = JSON.parse(readFileSync(stackJsonPath, 'utf8'));
142+
const apiKey = stackData?.api_key || stackData?.stackHeaders?.api_key;
143+
if (apiKey) {
144+
config.source_stack = apiKey;
152145
}
153-
break;
146+
} catch {
147+
// stack.json unreadable — source stack API key will not be set
154148
}
155149
}
156150
}

0 commit comments

Comments
 (0)