diff --git a/.talismanrc b/.talismanrc index 7efa0e61f..5fe2447d3 100644 --- a/.talismanrc +++ b/.talismanrc @@ -1,4 +1,6 @@ fileignoreconfig: +- filename: packages/contentstack-cli-tsgen/src/lib/helper.ts + checksum: cc2f88294ca026c29ca44ee7f9994ebc64e56d9a7a015c64070aaf271f1c3ba2 - filename: packages/contentstack-audit/test/unit/mock/am-contents-no-stack/environments/environments.json checksum: fe82c708f5f4d0b05694a601855fb7950661048eef8d7f9f794713e3a665064e - filename: packages/contentstack-audit/test/unit/mock/am-contents/environments/environments.json diff --git a/packages/contentstack-cli-tsgen/src/commands/tsgen.ts b/packages/contentstack-cli-tsgen/src/commands/tsgen.ts index d6ab5251c..c43b5a67b 100644 --- a/packages/contentstack-cli-tsgen/src/commands/tsgen.ts +++ b/packages/contentstack-cli-tsgen/src/commands/tsgen.ts @@ -4,7 +4,11 @@ import * as path from "path"; import * as fs from "fs"; import { cliux } from "@contentstack/cli-utilities"; import { generateTS, graphqlTS } from "@contentstack/types-generator"; -import { sanitizePath, printFormattedError } from "../lib/helper"; +import { + sanitizePath, + printFormattedError, + resolveGraphqlHost, +} from "../lib/helper"; import { StackConnectionConfig } from "../types"; function createOutputPath(outputFile: string) { @@ -31,20 +35,6 @@ export default class TypeScriptCodeGeneratorCommand extends Command { '$ csdx tsgen -a "delivery token alias" --output "contentstack/generated.d.ts" --api-type graphql --namespace "GraphQL" ', ]; - // Check if a region is a default Contentstack region - private isDefaultRegion(region: string): boolean { - const defaultRegions = [ - "US", - "EU", - "AU", - "AZURE_NA", - "AZURE_EU", - "GCP_NA", - "GCP_EU", - ]; - return defaultRegions.includes(region.toUpperCase()); - } - static flags: FlagInput = { alias: flags.string({ char: "a", @@ -150,10 +140,6 @@ export default class TypeScriptCodeGeneratorCommand extends Command { // Generate the GraphQL schema TypeScript definitions if (flags["api-type"] === "graphql") { try { - if (config.region === "us") { - config.region = "US"; - } - // Check if token has delivery type (required for GraphQL) if (token.type !== "delivery") { throw new Error( @@ -161,25 +147,30 @@ export default class TypeScriptCodeGeneratorCommand extends Command { ); } - // Prepare GraphQL config - only include host for custom regions - const graphqlConfig: any = { + // GraphQL has its own host per region; config.host is the CDA (REST) + // host and answers a GraphQL query with a 403. + const graphqlHost = resolveGraphqlHost(this.region); + + if (!graphqlHost) { + throw new Error( + `No GraphQL delivery endpoint is configured for the '${this.region.name}' region.`, + ); + } + + const graphqlConfig: StackConnectionConfig & { + namespace?: string; + logger?: any; + } = { apiKey: config.apiKey, token: config.token, environment: config.environment, - namespace: namespace, + region: this.region.name, + host: graphqlHost, + branch: config.branch, + namespace, logger: log, }; - // Add region or host based on whether it's a custom region - if (config.host && !this.isDefaultRegion(config.region)) { - // Custom region - include both region and host - graphqlConfig.region = config.region; - graphqlConfig.host = config.host; - } else { - // Default region - only include region - graphqlConfig.region = config.region; - } - const result = await graphqlTS(graphqlConfig); if (!result) { diff --git a/packages/contentstack-cli-tsgen/src/lib/helper.ts b/packages/contentstack-cli-tsgen/src/lib/helper.ts index 5ea7a7adb..187287dad 100644 --- a/packages/contentstack-cli-tsgen/src/lib/helper.ts +++ b/packages/contentstack-cli-tsgen/src/lib/helper.ts @@ -1,10 +1,23 @@ -import { log } from "@contentstack/cli-utilities"; +import { log, resolveCanonicalEndpoints } from '@contentstack/cli-utilities'; + +/** + * Resolve the GraphQL delivery host for a region. + */ +export const resolveGraphqlHost = (region: { + name: string; + endpoints?: Record; +}): string | undefined => { + const graphqlDelivery = + region?.endpoints?.graphqlDelivery || resolveCanonicalEndpoints(region?.name)?.graphqlDelivery; + + return graphqlDelivery?.replace(/^https?:\/\//, '').replace(/\/+$/, ''); +}; export const sanitizePath = (str: string) => { return str - ?.replace(/^([\/\\]){2,}/, "./") // Normalize leading slashes/backslashes to '' - .replace(/[\/\\]+/g, "/") // Replace multiple slashes/backslashes with a single '/' - .replace(/(\.\.(\/|\\|$))+/g, ""); // Remove directory traversal (../ or ..\) + ?.replace(/^([\/\\]){2,}/, './') // Normalize leading slashes/backslashes to '' + .replace(/[\/\\]+/g, '/') // Replace multiple slashes/backslashes with a single '/' + .replace(/(\.\.(\/|\\|$))+/g, ''); // Remove directory traversal (../ or ..\) }; /** @@ -24,59 +37,46 @@ export interface FormattedError { * @param context - The context where the error occurred (e.g., "tsgen", "graphql") */ export const printFormattedError = (error: FormattedError, context: string) => { - const errorCode = error?.error_code || "UNKNOWN_ERROR"; + const errorCode = error?.error_code || 'UNKNOWN_ERROR'; // Special handling for our numeric identifier validation errors - if ( - errorCode === "VALIDATION_ERROR" && - error?.error_message && - error.error_message.includes("numeric identifiers") - ) { + if (errorCode === 'VALIDATION_ERROR' && error?.error_message && error.error_message.includes('numeric identifiers')) { // Just print our detailed message as-is, no extra formatting log.error(error.error_message); return; } - let errorMessage = "An unexpected error occurred. Try again."; - let hint = ""; + let errorMessage = 'An unexpected error occurred. Try again.'; + let hint = ''; switch (errorCode) { - case "AUTHENTICATION_FAILED": - errorMessage = "Authentication failed. Check your credentials and try again."; - hint = "Please check your API key, token, and region."; + case 'AUTHENTICATION_FAILED': + errorMessage = 'Authentication failed. Check your credentials and try again.'; + hint = 'Please check your API key, token, and region.'; break; - case "INVALID_CREDENTIALS": - errorMessage = "Invalid credentials. Please verify and re-enter your login details."; - hint = "Please verify your API key, token, and region."; + case 'INVALID_CREDENTIALS': + errorMessage = 'Invalid credentials. Please verify and re-enter your login details.'; + hint = 'Please verify your API key, token, and region.'; break; - case "INVALID_INTERFACE_NAME": - case "INVALID_CONTENT_TYPE_UID": - errorMessage = "Generated types contain a TypeScript syntax error."; - hint = - "Use a prefix to ensure all interface names are valid TypeScript identifiers."; + case 'INVALID_INTERFACE_NAME': + case 'INVALID_CONTENT_TYPE_UID': + errorMessage = 'Generated types contain a TypeScript syntax error.'; + hint = 'Use a prefix to ensure all interface names are valid TypeScript identifiers.'; break; - case "INVALID_GLOBAL_FIELD_REFERENCE": - errorMessage = "Generated types contain a TypeScript syntax error."; - hint = - "Use a prefix to ensure all interface names are valid TypeScript identifiers."; + case 'INVALID_GLOBAL_FIELD_REFERENCE': + errorMessage = 'Generated types contain a TypeScript syntax error.'; + hint = 'Use a prefix to ensure all interface names are valid TypeScript identifiers.'; break; - case "VALIDATION_ERROR": - errorMessage = "Type generation failed due to a validation error."; - hint = - error?.error_message || - "Type generation failed due to a validation error."; + case 'VALIDATION_ERROR': + errorMessage = 'Type generation failed due to a validation error.'; + hint = error?.error_message || 'Type generation failed due to a validation error.'; break; - case "TYPE_GENERATION_FAILED": - errorMessage = "Type generation failed due to a system error. Try again."; - hint = - error?.error_message || - "Unexpected error during type generation. Try again."; + case 'TYPE_GENERATION_FAILED': + errorMessage = 'Type generation failed due to a system error. Try again.'; + hint = error?.error_message || 'Unexpected error during type generation. Try again.'; break; default: - errorMessage = - error?.error_message || - error?.message || - "An unexpected error occurred. Try again."; - hint = "Check the error details and try again."; + errorMessage = error?.error_message || error?.message || 'An unexpected error occurred. Try again.'; + hint = 'Check the error details and try again.'; } // Print formatted error output diff --git a/packages/contentstack-cli-tsgen/test/unit/helper.test.ts b/packages/contentstack-cli-tsgen/test/unit/helper.test.ts index 9d4058c85..5b6a9cb9b 100644 --- a/packages/contentstack-cli-tsgen/test/unit/helper.test.ts +++ b/packages/contentstack-cli-tsgen/test/unit/helper.test.ts @@ -2,10 +2,18 @@ // printFormattedError writes to spies instead of the real logger. jest.mock("@contentstack/cli-utilities", () => ({ log: { error: jest.fn(), warn: jest.fn(), info: jest.fn() }, + // Real lookup - the region-name-to-endpoint mapping is what these tests check. + resolveCanonicalEndpoints: jest.requireActual( + "@contentstack/cli-utilities/lib/region-endpoints", + ).resolveCanonicalEndpoints, })); import { log } from "@contentstack/cli-utilities"; -import { sanitizePath, printFormattedError } from "../../src/lib/helper"; +import { + sanitizePath, + printFormattedError, + resolveGraphqlHost, +} from "../../src/lib/helper"; const errorMock = log.error as jest.Mock; const warnMock = log.warn as jest.Mock; @@ -209,3 +217,43 @@ describe("helper", () => { }); }); }); + +describe("resolveGraphqlHost", () => { + // The regression: region names are stored as "AWS-NA"/"AZURE-EU"/..., so a + // lookup keyed on "US"/"AZURE_NA" misses and the CDA host gets used instead, + // which answers a GraphQL query with a 403. + it.each([ + ["AWS-NA", "graphql.contentstack.com"], + ["AWS-EU", "eu-graphql.contentstack.com"], + ["AWS-AU", "au-graphql.contentstack.com"], + ["AZURE-NA", "azure-na-graphql.contentstack.com"], + ["AZURE-EU", "azure-eu-graphql.contentstack.com"], + ["GCP-NA", "gcp-na-graphql.contentstack.com"], + ["GCP-EU", "gcp-eu-graphql.contentstack.com"], + ])("resolves %s by name to %s", (name, expected) => { + expect(resolveGraphqlHost({ name })).toBe(expected); + }); + + it.each(["AWS-NA", "AWS-EU", "AZURE-EU", "GCP-NA"])( + "never returns a CDA host for %s", + (name) => { + const host = resolveGraphqlHost({ name }); + + expect(host).toContain("graphql"); + expect(host).not.toContain("cdn."); + }, + ); + + it("prefers the endpoints already on the region object", () => { + expect( + resolveGraphqlHost({ + name: "AWS-NA", + endpoints: { graphqlDelivery: "https://custom-graphql.example.com/" }, + }), + ).toBe("custom-graphql.example.com"); + }); + + it("returns undefined for a custom region with no GraphQL endpoint", () => { + expect(resolveGraphqlHost({ name: "my-pilot-region" })).toBeUndefined(); + }); +});