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
2 changes: 1 addition & 1 deletion .talismanrc
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ fileignoreconfig:
ignore_detectors:
- filecontent
- filename: package-lock.json
checksum: 2e59256a4223df4fa670d9bedb571586daa21e59194400b8f9aa4725d378cc72
checksum: c447ed3d22eef9d2b26b9ae85370de31be04cc94da0af506ada0025bc7a9bbb6
- filename: .husky/pre-commit
checksum: 5baabd7d2c391648163f9371f0e5e9484f8fb90fa2284cfc378732ec3192c193
- filename: src/graphqlTS/index.ts
Expand Down
325 changes: 164 additions & 161 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@contentstack/types-generator",
"version": "3.6.0",
"version": "3.7.0",
"description": "Contentstack type definition generation library",
"private": false,
"author": "Contentstack",
Expand Down Expand Up @@ -40,13 +40,13 @@
"husky": "^9.1.7",
"jest": "^29.7.0",
"nock": "^13.5.6",
"rollup": "^4.46.2",
"rollup": "^4.48.0",
"ts-jest": "^29.4.0",
"tsup": "^8.5.0",
"typescript": "^5.7.3"
},
"dependencies": {
"@contentstack/cli-utilities": "^1.13.0",
"@contentstack/cli-utilities": "^1.13.1",
"@contentstack/delivery-sdk": "^4.8.0",
"@gql2ts/from-schema": "^2.0.0-4",
"async": "^3.2.6",
Expand Down
107 changes: 99 additions & 8 deletions src/generateTS/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
isNumericIdentifier,
NUMERIC_IDENTIFIER_EXCLUSION_REASON,
checkNumericIdentifierExclusion,
throwUIDValidationError,
} from "./shared/utils";

export type TSGenOptions = {
Expand Down Expand Up @@ -94,6 +95,13 @@ export default function (userOptions: TSGenOptions) {
const skippedBlocks: Array<{ uid: string; path: string; reason: string }> =
[];

// Collect numeric identifier errors instead of throwing immediately
const numericIdentifierErrors: Array<{
uid: string;
referenceTo?: string;
type: "content_type" | "global_field";
}> = [];

const typeMap: TypeMap = {
text: { func: type_text, track: true, flag: TypeFlags.BuiltinJS },
number: { func: type_number, track: true, flag: TypeFlags.BuiltinJS },
Expand Down Expand Up @@ -152,6 +160,12 @@ export default function (userOptions: TSGenOptions) {
}

function name_type(uid: string) {
// Check if the UID starts with a number, which would create invalid TypeScript
if (isNumericIdentifier(uid)) {
// Return a fallback name to continue processing
return `InvalidInterface_${uid}`;
}

return [options?.naming?.prefix, _.upperFirst(_.camelCase(uid))]
.filter((v) => v)
.join("");
Expand All @@ -161,14 +175,41 @@ export default function (userOptions: TSGenOptions) {
contentType: ContentstackTypes.ContentType | ContentstackTypes.GlobalField,
systemFields = false
) {
const interface_declaration = [
"export interface",
name_type(
contentType.data_type === "global_field"
? (contentType.reference_to as string)
: contentType.uid
),
];
// Validate the interface name before creating it
let interfaceName: string;

const isGlobalField = contentType.data_type === "global_field";

// Check if the content type's own UID starts with a number
if (isNumericIdentifier(contentType.uid)) {
numericIdentifierErrors.push({
uid: contentType.uid,
type: "content_type",
});
// Return a fallback interface declaration to continue processing
interfaceName = `InvalidInterface_${contentType.uid}`;
} else if (
isGlobalField &&
contentType.reference_to &&
isNumericIdentifier(contentType.reference_to as string)
) {
// For global fields, check if the referenced content type has a numeric identifier
// This is a global field error because it references an invalid content type
numericIdentifierErrors.push({
uid: contentType.uid, // The global field's UID
type: "global_field",
referenceTo: contentType.reference_to as string, // The referenced content type's UID
});
// Return a fallback interface declaration to continue processing
interfaceName = `InvalidInterface_${contentType.reference_to}`;
} else {
// No numeric identifier issues, generate normal interface name
interfaceName = name_type(
isGlobalField ? (contentType.reference_to as string) : contentType.uid
);
}

const interface_declaration = ["export interface", interfaceName];
if (systemFields && contentType.schema_type !== "global_field") {
interface_declaration.push("extends", name_type("SystemFields"));
}
Expand Down Expand Up @@ -570,6 +611,56 @@ export default function (userOptions: TSGenOptions) {

const definition = visit_content_type(contentType);

// Check for numeric identifier errors and throw them immediately
if (numericIdentifierErrors.length > 0) {
// Group errors by type for better organization
const contentTypeErrors = numericIdentifierErrors.filter(
(err) => err.type === "content_type"
);
const globalFieldErrors = numericIdentifierErrors.filter(
(err) => err.type === "global_field"
);

// Build the detailed error message
let errorDetails = "";
errorDetails += `Type generation failed: ${numericIdentifierErrors.length} items use numeric identifiers, which result in invalid TypeScript interface names. Use the --prefix flag to resolve this issue.\n\n`;

if (contentTypeErrors.length > 0) {
errorDetails += "Content Types and Global Fields with Numeric UIDs\n";
errorDetails +=
"Note: Global Fields are also Content Types. If their UID begins with a number, they are listed here.\n\n";

contentTypeErrors.forEach((error, index) => {
errorDetails += `${index + 1}. UID: "${error.uid}"\n`;
errorDetails += `TypeScript constraint: Object keys cannot start with a number.\n`;
errorDetails += `Suggestion: Since UIDs cannot be changed, use the --prefix flag to add a valid prefix to all interface names (e.g., --prefix "ContentType").\n\n`;
});
}

if (globalFieldErrors.length > 0) {
errorDetails += "Global Fields Referencing Invalid Content Types:\n\n";

globalFieldErrors.forEach((error, index) => {
errorDetails += `${index + 1}. Global Field: "${error.uid}"\n`;
errorDetails += ` References: "${error.referenceTo || "Unknown"}"\n`;
errorDetails += `TypeScript constraint: Object keys cannot start with a number.\n`;
errorDetails += `Suggestion: Since UIDs cannot be changed, use the --prefix flag to add a valid prefix to all interface names (e.g., --prefix "ContentType").\n\n`;
});
}

errorDetails += "To resolve these issues:\n";
errorDetails +=
"• Use the --prefix flag to add a valid prefix to all interface names.\n";
errorDetails += '• Example: --prefix "ContentType"\n';

// Throw a comprehensive error with all the details
throw {
type: "validation",
error_code: "VALIDATION_ERROR",
error_message: errorDetails,
};
}

// Log summary table of skipped fields and blocks
if (skippedFields.length > 0 || skippedBlocks.length > 0) {
cliux.print("");
Expand Down
74 changes: 33 additions & 41 deletions src/generateTS/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { defaultInterfaces } from "./stack/builtins";
import { format } from "../format/index";
import { ContentType } from "../types/schema";
import { cliux } from "@contentstack/cli-utilities";
import { createValidationError, createErrorDetails } from "./shared/utils";

export const generateTS = async ({
token,
Expand All @@ -28,11 +29,9 @@ export const generateTS = async ({
}: GenerateTS) => {
try {
if (!token || !tokenType || !apiKey || !environment || !region) {
throw {
type: "validation",
error_message:
"Please provide all the required params (token, tokenType, apiKey, environment, region)",
};
throw createValidationError(
"Please provide all the required params (token, tokenType, apiKey, environment, region)"
);
}

if (tokenType === TOKEN_TYPE.DELIVERY) {
Expand Down Expand Up @@ -62,11 +61,9 @@ export const generateTS = async ({
"Please create Content Models to generate type definitions",
{ color: "yellow" }
);
throw {
type: "validation",
error_message:
"There are no Content Types in the Stack, please create Content Models to generate type definitions",
};
throw createValidationError(
"There are no Content Types in the Stack, please create Content Models to generate type definitions"
);
}

let schemas: ContentType[] = [];
Expand Down Expand Up @@ -95,43 +92,40 @@ export const generateTS = async ({
}
} catch (error: any) {
if (error.type === "validation") {
throw { error_message: error.error_message };
// Handle validation errors with proper error codes
throw {
error_message: error.error_message,
error_code: error.error_code || "VALIDATION_ERROR",
};
} else {
const errorObj = JSON.parse(error.message.replace("Error: ", ""));
let errorMessage = "Something went wrong";
let errorCode = "API_ERROR";

if (errorObj.status) {
switch (errorObj.status) {
case 401:
cliux.print("Authentication failed", {
color: "red",
bold: true,
});
cliux.print("Please check your apiKey, token, and region", {
color: "yellow",
});
errorMessage =
"Unauthorized: The apiKey, token or region is not valid.";
errorCode = "AUTHENTICATION_FAILED";
break;
case 412:
cliux.print("Invalid credentials", { color: "red", bold: true });
cliux.print("Please verify your apiKey, token, and region", {
color: "yellow",
});
errorMessage =
"Invalid Credentials: Please check the provided apiKey, token and region.";
errorCode = "INVALID_CREDENTIALS";
break;
default:
cliux.print(`API Error (${errorObj.status})`, {
color: "red",
bold: true,
});
errorMessage = `${errorMessage}, ${errorObj.error_message}`;
errorCode = `API_ERROR_${errorObj.status}`;
}
}
if (errorObj.error_message && !errorObj.status) {
errorMessage = `${errorMessage}, ${errorObj.error_message}`;
}
throw { error_message: errorMessage };
throw {
error_message: errorMessage,
error_code: errorCode,
};
}
}
};
Expand Down Expand Up @@ -193,21 +187,19 @@ export const generateTSFromContentTypes = async ({

return output;
} catch (err: any) {
// Enhanced error logging with more context
const errorMessage = err.message || "Unknown error occurred";
const errorDetails = {
error_message: `Type generation failed: ${errorMessage}`,
context: "generateTSFromContentTypes",
timestamp: new Date().toISOString(),
error_type: err.constructor.name,
};

// Log detailed error information for debugging
cliux.print(`Type generation failed: ${errorMessage}`, {
color: "red",
bold: true,
});
// Handle numeric identifier errors specially to preserve their detailed format
if (
err.type === "validation" &&
err.error_code === "VALIDATION_ERROR" &&
err.error_message &&
err.error_message.includes("numeric identifiers")
) {
// Pass through the detailed error as-is
throw err;
}

// Use common function to create detailed error information for other errors
const errorDetails = createErrorDetails(err, "generateTSFromContentTypes");
throw errorDetails;
}
};
Expand Down
87 changes: 87 additions & 0 deletions src/generateTS/shared/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,90 @@ export function checkNumericIdentifierExclusion(
}
return { shouldExclude: false };
}

/**
* Throws a UID validation error with standardized structure
* @param params - Object containing error parameters
* @param params.uid - The UID that caused the validation error
* @param params.errorCode - The error code for the validation error
* @param params.reason - The reason for the validation error
* @param params.suggestion - The suggestion to resolve the issue
* @param params.context - The context where the error occurred
* @param params.referenceTo - Optional reference UID for global field errors
* @throws A validation error object
*/
export function throwUIDValidationError({
uid,
errorCode,
reason,
suggestion,
context,
referenceTo,
}: {
uid: string;
errorCode: string;
reason: string;
suggestion: string;
context: string;
referenceTo?: string;
}): never {
const errorMessage =
errorCode === "INVALID_GLOBAL_FIELD_REFERENCE"
? `Global field "${uid}" references content type "${referenceTo}" which starts with a number, creating invalid TypeScript interface names.`
: `Content type UID "${uid}" starts with a number, which creates invalid TypeScript interface names.`;

throw {
type: "validation",
error_code: errorCode,
error_message: errorMessage,
details: {
uid,
...(referenceTo ? { reference_to: referenceTo } : {}),
reason,
suggestion,
},
context,
timestamp: new Date().toISOString(),
};
}

/**
* Creates a validation error in the exact format expected by tests
* This maintains backward compatibility while reducing code duplication
* @param errorMessage - The error message to display
* @returns A validation error object with the expected structure
*/
export function createValidationError(errorMessage: string) {
return {
type: "validation",
error_message: errorMessage,
};
}

/**
* Creates standardized error details for different types of errors
* @param err - The error object to process
* @param context - The context where the error occurred
* @returns Standardized error details object
*/
export function createErrorDetails(
err: any,
context: string = "generateTSFromContentTypes"
) {
if (err.type === "validation") {
// Handle validation errors with proper error codes
return {
error_message: err.error_message || "Validation error occurred", // Keep for backwards compatibility
error_code: err.error_code || "VALIDATION_ERROR", // New property
details: err.details || {},
};
} else {
// Handle other types of errors
const errorMessage = err.message || "Unknown error occurred";
return {
error_message: `Type generation failed: ${errorMessage}`, // Keep for backwards compatibility
error_code: "TYPE_GENERATION_FAILED", // New property
details: {},
};
}
}
Loading