Skip to content

Commit 836f17b

Browse files
Merge pull request #358 from contentstack/fix/DX-10294
fix(tsgen): resolve the real GraphQL endpoint per region
2 parents 2d45ccc + 475a0b8 commit 836f17b

4 files changed

Lines changed: 126 additions & 75 deletions

File tree

‎.talismanrc‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,16 @@ fileignoreconfig:
22

33
- filename: pnpm-lock.yaml
44
checksum: 98236e2588d5cffcbdda3464179d91e9540fd8ddb4d4fb5251cf0f7379876f42
5+
- filename: packages/contentstack-bulk-operations/src/utils/backup-dir-asset-fetcher.ts
6+
checksum: fbf1ef882bf4ea8f06a7a28b9248a07e01f5a5d2d4da98dbb9ee52bc16ce318a
7+
- filename: packages/contentstack-bulk-operations/test/unit/utils/backup-dir-asset-fetcher.test.ts
8+
checksum: b905524c952aff0f089fa5347e0db18479b75a438cd9ae77e2dfe1cb203d7e59
9+
- filename: packages/contentstack-import/src/import/modules/assets.ts
10+
checksum: 014f612c3e8db21c891da0c17a1a3b4cd340c026c58034791d86cd5f73aa9f90
11+
- filename: packages/contentstack-bulk-operations/test/unit/utils/revert-retry-handler.test.ts
12+
checksum: 142e7b67a06bc13b6b4191b17f2ea8ae31a3bce20086ea35a92f3e3671eb603d
13+
- filename: packages/contentstack-bulk-operations/src/commands/cm/stacks/bulk-assets.ts
14+
checksum: 9392511b337c361b86c36eb2431d8fea9129a61f6b7d70dcb4a633fea339ca61
15+
- filename: packages/contentstack-bulk-operations/src/utils/batch-helper.ts
16+
checksum: ddcd8ef7c56d1122df88075883a2768a28813147e8fdef5b8e72a1ec118c045a
517
version: ""

‎packages/contentstack-cli-tsgen/src/commands/tsgen.ts‎

Lines changed: 23 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import * as path from "path";
44
import * as fs from "fs";
55
import { cliux } from "@contentstack/cli-utilities";
66
import { generateTS, graphqlTS } from "@contentstack/types-generator";
7-
import { sanitizePath, printFormattedError } from "../lib/helper";
7+
import {
8+
sanitizePath,
9+
printFormattedError,
10+
resolveGraphqlHost,
11+
} from "../lib/helper";
812
import { StackConnectionConfig } from "../types";
913

1014
function createOutputPath(outputFile: string) {
@@ -31,20 +35,6 @@ export default class TypeScriptCodeGeneratorCommand extends Command {
3135
'$ csdx tsgen -a "delivery token alias" --output "contentstack/generated.d.ts" --api-type graphql --namespace "GraphQL" ',
3236
];
3337

34-
// Check if a region is a default Contentstack region
35-
private isDefaultRegion(region: string): boolean {
36-
const defaultRegions = [
37-
"US",
38-
"EU",
39-
"AU",
40-
"AZURE_NA",
41-
"AZURE_EU",
42-
"GCP_NA",
43-
"GCP_EU",
44-
];
45-
return defaultRegions.includes(region.toUpperCase());
46-
}
47-
4838
static flags: FlagInput = {
4939
alias: flags.string({
5040
char: "a",
@@ -150,36 +140,37 @@ export default class TypeScriptCodeGeneratorCommand extends Command {
150140
// Generate the GraphQL schema TypeScript definitions
151141
if (flags["api-type"] === "graphql") {
152142
try {
153-
if (config.region === "us") {
154-
config.region = "US";
155-
}
156-
157143
// Check if token has delivery type (required for GraphQL)
158144
if (token.type !== "delivery") {
159145
throw new Error(
160146
"GraphQL API requires a delivery token. Management tokens aren't supported.",
161147
);
162148
}
163149

164-
// Prepare GraphQL config - only include host for custom regions
165-
const graphqlConfig: any = {
150+
// GraphQL has its own host per region; config.host is the CDA (REST)
151+
// host and answers a GraphQL query with a 403.
152+
const graphqlHost = resolveGraphqlHost(this.region);
153+
154+
if (!graphqlHost) {
155+
throw new Error(
156+
`No GraphQL delivery endpoint is configured for the '${this.region.name}' region.`,
157+
);
158+
}
159+
160+
const graphqlConfig: StackConnectionConfig & {
161+
namespace?: string;
162+
logger?: any;
163+
} = {
166164
apiKey: config.apiKey,
167165
token: config.token,
168166
environment: config.environment,
169-
namespace: namespace,
167+
region: this.region.name,
168+
host: graphqlHost,
169+
branch: config.branch,
170+
namespace,
170171
logger: log,
171172
};
172173

173-
// Add region or host based on whether it's a custom region
174-
if (config.host && !this.isDefaultRegion(config.region)) {
175-
// Custom region - include both region and host
176-
graphqlConfig.region = config.region;
177-
graphqlConfig.host = config.host;
178-
} else {
179-
// Default region - only include region
180-
graphqlConfig.region = config.region;
181-
}
182-
183174
const result = await graphqlTS(graphqlConfig);
184175

185176
if (!result) {

‎packages/contentstack-cli-tsgen/src/lib/helper.ts‎

Lines changed: 42 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,23 @@
1-
import { log } from "@contentstack/cli-utilities";
1+
import { log, resolveCanonicalEndpoints } from '@contentstack/cli-utilities';
2+
3+
/**
4+
* Resolve the GraphQL delivery host for a region.
5+
*/
6+
export const resolveGraphqlHost = (region: {
7+
name: string;
8+
endpoints?: Record<string, string>;
9+
}): string | undefined => {
10+
const graphqlDelivery =
11+
region?.endpoints?.graphqlDelivery || resolveCanonicalEndpoints(region?.name)?.graphqlDelivery;
12+
13+
return graphqlDelivery?.replace(/^https?:\/\//, '').replace(/\/+$/, '');
14+
};
215

316
export const sanitizePath = (str: string) => {
417
return str
5-
?.replace(/^([\/\\]){2,}/, "./") // Normalize leading slashes/backslashes to ''
6-
.replace(/[\/\\]+/g, "/") // Replace multiple slashes/backslashes with a single '/'
7-
.replace(/(\.\.(\/|\\|$))+/g, ""); // Remove directory traversal (../ or ..\)
18+
?.replace(/^([\/\\]){2,}/, './') // Normalize leading slashes/backslashes to ''
19+
.replace(/[\/\\]+/g, '/') // Replace multiple slashes/backslashes with a single '/'
20+
.replace(/(\.\.(\/|\\|$))+/g, ''); // Remove directory traversal (../ or ..\)
821
};
922

1023
/**
@@ -24,59 +37,46 @@ export interface FormattedError {
2437
* @param context - The context where the error occurred (e.g., "tsgen", "graphql")
2538
*/
2639
export const printFormattedError = (error: FormattedError, context: string) => {
27-
const errorCode = error?.error_code || "UNKNOWN_ERROR";
40+
const errorCode = error?.error_code || 'UNKNOWN_ERROR';
2841
// Special handling for our numeric identifier validation errors
29-
if (
30-
errorCode === "VALIDATION_ERROR" &&
31-
error?.error_message &&
32-
error.error_message.includes("numeric identifiers")
33-
) {
42+
if (errorCode === 'VALIDATION_ERROR' && error?.error_message && error.error_message.includes('numeric identifiers')) {
3443
// Just print our detailed message as-is, no extra formatting
3544
log.error(error.error_message);
3645
return;
3746
}
3847

39-
let errorMessage = "An unexpected error occurred. Try again.";
40-
let hint = "";
48+
let errorMessage = 'An unexpected error occurred. Try again.';
49+
let hint = '';
4150

4251
switch (errorCode) {
43-
case "AUTHENTICATION_FAILED":
44-
errorMessage = "Authentication failed. Check your credentials and try again.";
45-
hint = "Please check your API key, token, and region.";
52+
case 'AUTHENTICATION_FAILED':
53+
errorMessage = 'Authentication failed. Check your credentials and try again.';
54+
hint = 'Please check your API key, token, and region.';
4655
break;
47-
case "INVALID_CREDENTIALS":
48-
errorMessage = "Invalid credentials. Please verify and re-enter your login details.";
49-
hint = "Please verify your API key, token, and region.";
56+
case 'INVALID_CREDENTIALS':
57+
errorMessage = 'Invalid credentials. Please verify and re-enter your login details.';
58+
hint = 'Please verify your API key, token, and region.';
5059
break;
51-
case "INVALID_INTERFACE_NAME":
52-
case "INVALID_CONTENT_TYPE_UID":
53-
errorMessage = "Generated types contain a TypeScript syntax error.";
54-
hint =
55-
"Use a prefix to ensure all interface names are valid TypeScript identifiers.";
60+
case 'INVALID_INTERFACE_NAME':
61+
case 'INVALID_CONTENT_TYPE_UID':
62+
errorMessage = 'Generated types contain a TypeScript syntax error.';
63+
hint = 'Use a prefix to ensure all interface names are valid TypeScript identifiers.';
5664
break;
57-
case "INVALID_GLOBAL_FIELD_REFERENCE":
58-
errorMessage = "Generated types contain a TypeScript syntax error.";
59-
hint =
60-
"Use a prefix to ensure all interface names are valid TypeScript identifiers.";
65+
case 'INVALID_GLOBAL_FIELD_REFERENCE':
66+
errorMessage = 'Generated types contain a TypeScript syntax error.';
67+
hint = 'Use a prefix to ensure all interface names are valid TypeScript identifiers.';
6168
break;
62-
case "VALIDATION_ERROR":
63-
errorMessage = "Type generation failed due to a validation error.";
64-
hint =
65-
error?.error_message ||
66-
"Type generation failed due to a validation error.";
69+
case 'VALIDATION_ERROR':
70+
errorMessage = 'Type generation failed due to a validation error.';
71+
hint = error?.error_message || 'Type generation failed due to a validation error.';
6772
break;
68-
case "TYPE_GENERATION_FAILED":
69-
errorMessage = "Type generation failed due to a system error. Try again.";
70-
hint =
71-
error?.error_message ||
72-
"Unexpected error during type generation. Try again.";
73+
case 'TYPE_GENERATION_FAILED':
74+
errorMessage = 'Type generation failed due to a system error. Try again.';
75+
hint = error?.error_message || 'Unexpected error during type generation. Try again.';
7376
break;
7477
default:
75-
errorMessage =
76-
error?.error_message ||
77-
error?.message ||
78-
"An unexpected error occurred. Try again.";
79-
hint = "Check the error details and try again.";
78+
errorMessage = error?.error_message || error?.message || 'An unexpected error occurred. Try again.';
79+
hint = 'Check the error details and try again.';
8080
}
8181

8282
// Print formatted error output

‎packages/contentstack-cli-tsgen/test/unit/helper.test.ts‎

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,18 @@
22
// printFormattedError writes to spies instead of the real logger.
33
jest.mock("@contentstack/cli-utilities", () => ({
44
log: { error: jest.fn(), warn: jest.fn(), info: jest.fn() },
5+
// Real lookup - the region-name-to-endpoint mapping is what these tests check.
6+
resolveCanonicalEndpoints: jest.requireActual(
7+
"@contentstack/cli-utilities/lib/region-endpoints",
8+
).resolveCanonicalEndpoints,
59
}));
610

711
import { log } from "@contentstack/cli-utilities";
8-
import { sanitizePath, printFormattedError } from "../../src/lib/helper";
12+
import {
13+
sanitizePath,
14+
printFormattedError,
15+
resolveGraphqlHost,
16+
} from "../../src/lib/helper";
917

1018
const errorMock = log.error as jest.Mock;
1119
const warnMock = log.warn as jest.Mock;
@@ -209,3 +217,43 @@ describe("helper", () => {
209217
});
210218
});
211219
});
220+
221+
describe("resolveGraphqlHost", () => {
222+
// The regression: region names are stored as "AWS-NA"/"AZURE-EU"/..., so a
223+
// lookup keyed on "US"/"AZURE_NA" misses and the CDA host gets used instead,
224+
// which answers a GraphQL query with a 403.
225+
it.each([
226+
["AWS-NA", "graphql.contentstack.com"],
227+
["AWS-EU", "eu-graphql.contentstack.com"],
228+
["AWS-AU", "au-graphql.contentstack.com"],
229+
["AZURE-NA", "azure-na-graphql.contentstack.com"],
230+
["AZURE-EU", "azure-eu-graphql.contentstack.com"],
231+
["GCP-NA", "gcp-na-graphql.contentstack.com"],
232+
["GCP-EU", "gcp-eu-graphql.contentstack.com"],
233+
])("resolves %s by name to %s", (name, expected) => {
234+
expect(resolveGraphqlHost({ name })).toBe(expected);
235+
});
236+
237+
it.each(["AWS-NA", "AWS-EU", "AZURE-EU", "GCP-NA"])(
238+
"never returns a CDA host for %s",
239+
(name) => {
240+
const host = resolveGraphqlHost({ name });
241+
242+
expect(host).toContain("graphql");
243+
expect(host).not.toContain("cdn.");
244+
},
245+
);
246+
247+
it("prefers the endpoints already on the region object", () => {
248+
expect(
249+
resolveGraphqlHost({
250+
name: "AWS-NA",
251+
endpoints: { graphqlDelivery: "https://custom-graphql.example.com/" },
252+
}),
253+
).toBe("custom-graphql.example.com");
254+
});
255+
256+
it("returns undefined for a custom region with no GraphQL endpoint", () => {
257+
expect(resolveGraphqlHost({ name: "my-pilot-region" })).toBeUndefined();
258+
});
259+
});

0 commit comments

Comments
 (0)