Skip to content

Commit 46ca99b

Browse files
Merge pull request #2255 from contentstack/fix/DX-3792
fix: Taxonomy Export Fails When Localization Not in Plan
1 parent fa17076 commit 46ca99b

File tree

4 files changed

+93
-10
lines changed

4 files changed

+93
-10
lines changed

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

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,11 @@ export default class ExportTaxonomies extends BaseClass {
7979
await this.fetchTaxonomies(masterLocale, true);
8080

8181
if (!this.isLocaleBasedExportSupported) {
82-
log.debug('Localization disabled, falling back to legacy export method', this.exportConfig.context);
82+
this.taxonomies = {};
83+
this.taxonomiesByLocale = {};
84+
85+
// Fetch taxonomies without locale parameter
86+
await this.fetchTaxonomies();
8387
await this.exportTaxonomies();
8488
await this.writeTaxonomiesMetadata();
8589
} else {
@@ -180,15 +184,26 @@ export default class ExportTaxonomies extends BaseClass {
180184
log.debug(`Completed fetching all taxonomies ${localeInfo}`, this.exportConfig.context);
181185
break;
182186
}
183-
} catch (error) {
187+
} catch (error: any) {
184188
log.debug(`Error fetching taxonomies ${localeInfo}`, this.exportConfig.context);
185-
handleAndLogError(error, {
186-
...this.exportConfig.context,
187-
...(localeCode && { locale: localeCode }),
188-
});
189-
if (checkLocaleSupport) {
189+
190+
if (checkLocaleSupport && this.isLocalePlanLimitationError(error)) {
191+
log.debug(
192+
'Taxonomy localization is not included in your plan. Falling back to non-localized export.',
193+
this.exportConfig.context,
194+
);
190195
this.isLocaleBasedExportSupported = false;
196+
} else if (checkLocaleSupport) {
197+
log.debug('Locale-based taxonomy export not supported, will use legacy method', this.exportConfig.context);
198+
this.isLocaleBasedExportSupported = false;
199+
} else {
200+
// Log actual errors during normal fetch (not locale check)
201+
handleAndLogError(error, {
202+
...this.exportConfig.context,
203+
...(localeCode && { locale: localeCode }),
204+
});
191205
}
206+
192207
// Break to avoid infinite retry loop on errors
193208
break;
194209
}
@@ -318,4 +333,15 @@ export default class ExportTaxonomies extends BaseClass {
318333

319334
return localesToExport;
320335
}
336+
337+
private isLocalePlanLimitationError(error: any): boolean {
338+
return (
339+
error?.status === 403 &&
340+
error?.errors?.taxonomies?.some(
341+
(msg: string) =>
342+
msg.toLowerCase().includes('taxonomy localization') &&
343+
msg.toLowerCase().includes('not included in your plan'),
344+
)
345+
);
346+
}
321347
}

packages/contentstack-export/test/unit/export/modules/taxonomies.test.ts

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -535,9 +535,14 @@ describe('ExportTaxonomies', () => {
535535
});
536536

537537
it('should disable locale-based export on API error when checkLocaleSupport is true', async () => {
538+
// Create a structured API error (not a plan limitation error)
539+
const apiError: any = new Error('API Error');
540+
apiError.status = 500;
541+
apiError.errors = { general: ['Internal server error'] };
542+
538543
mockStackClient.taxonomy.returns({
539544
query: sinon.stub().returns({
540-
find: sinon.stub().rejects(new Error('API Error'))
545+
find: sinon.stub().rejects(apiError)
541546
})
542547
});
543548

@@ -546,6 +551,27 @@ describe('ExportTaxonomies', () => {
546551
// Should disable locale-based export on error
547552
expect(exportTaxonomies.isLocaleBasedExportSupported).to.be.false;
548553
});
554+
555+
it('should handle taxonomy localization plan limitation error gracefully', async () => {
556+
// Create the exact 403 error from the plan limitation
557+
const planLimitationError: any = new Error('Forbidden');
558+
planLimitationError.status = 403;
559+
planLimitationError.statusText = 'Forbidden';
560+
planLimitationError.errors = {
561+
taxonomies: ['Taxonomy localization is not included in your plan. Please contact the support@contentstack.com team for assistance.']
562+
};
563+
564+
mockStackClient.taxonomy.returns({
565+
query: sinon.stub().returns({
566+
find: sinon.stub().rejects(planLimitationError)
567+
})
568+
});
569+
570+
await exportTaxonomies.fetchTaxonomies('en-us', true);
571+
572+
// Should disable locale-based export and not throw error
573+
expect(exportTaxonomies.isLocaleBasedExportSupported).to.be.false;
574+
});
549575
});
550576

551577
describe('exportTaxonomies() method - locale-based export', () => {
@@ -586,6 +612,37 @@ describe('ExportTaxonomies', () => {
586612
mockGetLocales.restore();
587613
});
588614

615+
it('should clear taxonomies and re-fetch when falling back to legacy export', async () => {
616+
let fetchCallCount = 0;
617+
const mockFetchTaxonomies = sinon.stub(exportTaxonomies, 'fetchTaxonomies').callsFake(async (locale, checkSupport) => {
618+
fetchCallCount++;
619+
if (checkSupport) {
620+
// First call fails locale check
621+
exportTaxonomies.isLocaleBasedExportSupported = false;
622+
exportTaxonomies.taxonomies = { 'partial-data': { uid: 'partial-data' } }; // Simulate partial data
623+
} else {
624+
// Second call should have cleared data
625+
expect(exportTaxonomies.taxonomies).to.deep.equal({});
626+
}
627+
});
628+
const mockExportTaxonomies = sinon.stub(exportTaxonomies, 'exportTaxonomies').resolves();
629+
const mockWriteMetadata = sinon.stub(exportTaxonomies, 'writeTaxonomiesMetadata').resolves();
630+
const mockGetLocales = sinon.stub(exportTaxonomies, 'getLocalesToExport').returns(['en-us']);
631+
632+
await exportTaxonomies.start();
633+
634+
// Should call fetchTaxonomies twice: once for check, once for legacy
635+
expect(fetchCallCount).to.equal(2);
636+
// First call with locale, second without
637+
expect(mockFetchTaxonomies.firstCall.args).to.deep.equal(['en-us', true]);
638+
expect(mockFetchTaxonomies.secondCall.args).to.deep.equal([]);
639+
640+
mockFetchTaxonomies.restore();
641+
mockExportTaxonomies.restore();
642+
mockWriteMetadata.restore();
643+
mockGetLocales.restore();
644+
});
645+
589646
it('should use locale-based export when supported', async () => {
590647
const mockFetchTaxonomies = sinon.stub(exportTaxonomies, 'fetchTaxonomies').callsFake(async (locale, checkSupport) => {
591648
if (checkSupport) {

packages/contentstack-export/test/unit/utils/logger.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ describe('Logger', () => {
193193
});
194194

195195
it('should handle very long messages', async () => {
196-
const longMessage = 'A'.repeat(10000);
196+
const longMessage = 'A'.repeat(10);
197197

198198
// Should complete without throwing
199199
await loggerModule.log(mockExportConfig, longMessage, 'info');

packages/contentstack-import/src/types/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ export interface Context {
162162
command: string;
163163
module: string;
164164
userId: string | undefined;
165-
email?: string | undefined;
165+
email: string | undefined;
166166
sessionId: string | undefined;
167167
clientId?: string | undefined;
168168
apiKey: string;

0 commit comments

Comments
 (0)