Skip to content

Commit f5a8a58

Browse files
Merge pull request #117 from contentstack/enhancement/DX-3404
2 parents 9807923 + 26dbcd3 commit f5a8a58

9 files changed

Lines changed: 413 additions & 225 deletions

File tree

.talismanrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ fileignoreconfig:
33
ignore_detectors:
44
- filecontent
55
- filename: package-lock.json
6-
checksum: 2e59256a4223df4fa670d9bedb571586daa21e59194400b8f9aa4725d378cc72
6+
checksum: c447ed3d22eef9d2b26b9ae85370de31be04cc94da0af506ada0025bc7a9bbb6
77
- filename: .husky/pre-commit
88
checksum: 5baabd7d2c391648163f9371f0e5e9484f8fb90fa2284cfc378732ec3192c193
99
- filename: src/graphqlTS/index.ts

package-lock.json

Lines changed: 164 additions & 161 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@contentstack/types-generator",
3-
"version": "3.6.0",
3+
"version": "3.7.0",
44
"description": "Contentstack type definition generation library",
55
"private": false,
66
"author": "Contentstack",
@@ -40,13 +40,13 @@
4040
"husky": "^9.1.7",
4141
"jest": "^29.7.0",
4242
"nock": "^13.5.6",
43-
"rollup": "^4.46.2",
43+
"rollup": "^4.48.0",
4444
"ts-jest": "^29.4.0",
4545
"tsup": "^8.5.0",
4646
"typescript": "^5.7.3"
4747
},
4848
"dependencies": {
49-
"@contentstack/cli-utilities": "^1.13.0",
49+
"@contentstack/cli-utilities": "^1.13.1",
5050
"@contentstack/delivery-sdk": "^4.8.0",
5151
"@gql2ts/from-schema": "^2.0.0-4",
5252
"async": "^3.2.6",

src/generateTS/factory.ts

Lines changed: 99 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
isNumericIdentifier,
99
NUMERIC_IDENTIFIER_EXCLUSION_REASON,
1010
checkNumericIdentifierExclusion,
11+
throwUIDValidationError,
1112
} from "./shared/utils";
1213

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

98+
// Collect numeric identifier errors instead of throwing immediately
99+
const numericIdentifierErrors: Array<{
100+
uid: string;
101+
referenceTo?: string;
102+
type: "content_type" | "global_field";
103+
}> = [];
104+
97105
const typeMap: TypeMap = {
98106
text: { func: type_text, track: true, flag: TypeFlags.BuiltinJS },
99107
number: { func: type_number, track: true, flag: TypeFlags.BuiltinJS },
@@ -152,6 +160,12 @@ export default function (userOptions: TSGenOptions) {
152160
}
153161

154162
function name_type(uid: string) {
163+
// Check if the UID starts with a number, which would create invalid TypeScript
164+
if (isNumericIdentifier(uid)) {
165+
// Return a fallback name to continue processing
166+
return `InvalidInterface_${uid}`;
167+
}
168+
155169
return [options?.naming?.prefix, _.upperFirst(_.camelCase(uid))]
156170
.filter((v) => v)
157171
.join("");
@@ -161,14 +175,41 @@ export default function (userOptions: TSGenOptions) {
161175
contentType: ContentstackTypes.ContentType | ContentstackTypes.GlobalField,
162176
systemFields = false
163177
) {
164-
const interface_declaration = [
165-
"export interface",
166-
name_type(
167-
contentType.data_type === "global_field"
168-
? (contentType.reference_to as string)
169-
: contentType.uid
170-
),
171-
];
178+
// Validate the interface name before creating it
179+
let interfaceName: string;
180+
181+
const isGlobalField = contentType.data_type === "global_field";
182+
183+
// Check if the content type's own UID starts with a number
184+
if (isNumericIdentifier(contentType.uid)) {
185+
numericIdentifierErrors.push({
186+
uid: contentType.uid,
187+
type: "content_type",
188+
});
189+
// Return a fallback interface declaration to continue processing
190+
interfaceName = `InvalidInterface_${contentType.uid}`;
191+
} else if (
192+
isGlobalField &&
193+
contentType.reference_to &&
194+
isNumericIdentifier(contentType.reference_to as string)
195+
) {
196+
// For global fields, check if the referenced content type has a numeric identifier
197+
// This is a global field error because it references an invalid content type
198+
numericIdentifierErrors.push({
199+
uid: contentType.uid, // The global field's UID
200+
type: "global_field",
201+
referenceTo: contentType.reference_to as string, // The referenced content type's UID
202+
});
203+
// Return a fallback interface declaration to continue processing
204+
interfaceName = `InvalidInterface_${contentType.reference_to}`;
205+
} else {
206+
// No numeric identifier issues, generate normal interface name
207+
interfaceName = name_type(
208+
isGlobalField ? (contentType.reference_to as string) : contentType.uid
209+
);
210+
}
211+
212+
const interface_declaration = ["export interface", interfaceName];
172213
if (systemFields && contentType.schema_type !== "global_field") {
173214
interface_declaration.push("extends", name_type("SystemFields"));
174215
}
@@ -570,6 +611,56 @@ export default function (userOptions: TSGenOptions) {
570611

571612
const definition = visit_content_type(contentType);
572613

614+
// Check for numeric identifier errors and throw them immediately
615+
if (numericIdentifierErrors.length > 0) {
616+
// Group errors by type for better organization
617+
const contentTypeErrors = numericIdentifierErrors.filter(
618+
(err) => err.type === "content_type"
619+
);
620+
const globalFieldErrors = numericIdentifierErrors.filter(
621+
(err) => err.type === "global_field"
622+
);
623+
624+
// Build the detailed error message
625+
let errorDetails = "";
626+
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`;
627+
628+
if (contentTypeErrors.length > 0) {
629+
errorDetails += "Content Types and Global Fields with Numeric UIDs\n";
630+
errorDetails +=
631+
"Note: Global Fields are also Content Types. If their UID begins with a number, they are listed here.\n\n";
632+
633+
contentTypeErrors.forEach((error, index) => {
634+
errorDetails += `${index + 1}. UID: "${error.uid}"\n`;
635+
errorDetails += `TypeScript constraint: Object keys cannot start with a number.\n`;
636+
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`;
637+
});
638+
}
639+
640+
if (globalFieldErrors.length > 0) {
641+
errorDetails += "Global Fields Referencing Invalid Content Types:\n\n";
642+
643+
globalFieldErrors.forEach((error, index) => {
644+
errorDetails += `${index + 1}. Global Field: "${error.uid}"\n`;
645+
errorDetails += ` References: "${error.referenceTo || "Unknown"}"\n`;
646+
errorDetails += `TypeScript constraint: Object keys cannot start with a number.\n`;
647+
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`;
648+
});
649+
}
650+
651+
errorDetails += "To resolve these issues:\n";
652+
errorDetails +=
653+
"• Use the --prefix flag to add a valid prefix to all interface names.\n";
654+
errorDetails += '• Example: --prefix "ContentType"\n';
655+
656+
// Throw a comprehensive error with all the details
657+
throw {
658+
type: "validation",
659+
error_code: "VALIDATION_ERROR",
660+
error_message: errorDetails,
661+
};
662+
}
663+
573664
// Log summary table of skipped fields and blocks
574665
if (skippedFields.length > 0 || skippedBlocks.length > 0) {
575666
cliux.print("");

src/generateTS/index.ts

Lines changed: 33 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { defaultInterfaces } from "./stack/builtins";
1111
import { format } from "../format/index";
1212
import { ContentType } from "../types/schema";
1313
import { cliux } from "@contentstack/cli-utilities";
14+
import { createValidationError, createErrorDetails } from "./shared/utils";
1415

1516
export const generateTS = async ({
1617
token,
@@ -28,11 +29,9 @@ export const generateTS = async ({
2829
}: GenerateTS) => {
2930
try {
3031
if (!token || !tokenType || !apiKey || !environment || !region) {
31-
throw {
32-
type: "validation",
33-
error_message:
34-
"Please provide all the required params (token, tokenType, apiKey, environment, region)",
35-
};
32+
throw createValidationError(
33+
"Please provide all the required params (token, tokenType, apiKey, environment, region)"
34+
);
3635
}
3736

3837
if (tokenType === TOKEN_TYPE.DELIVERY) {
@@ -62,11 +61,9 @@ export const generateTS = async ({
6261
"Please create Content Models to generate type definitions",
6362
{ color: "yellow" }
6463
);
65-
throw {
66-
type: "validation",
67-
error_message:
68-
"There are no Content Types in the Stack, please create Content Models to generate type definitions",
69-
};
64+
throw createValidationError(
65+
"There are no Content Types in the Stack, please create Content Models to generate type definitions"
66+
);
7067
}
7168

7269
let schemas: ContentType[] = [];
@@ -95,43 +92,40 @@ export const generateTS = async ({
9592
}
9693
} catch (error: any) {
9794
if (error.type === "validation") {
98-
throw { error_message: error.error_message };
95+
// Handle validation errors with proper error codes
96+
throw {
97+
error_message: error.error_message,
98+
error_code: error.error_code || "VALIDATION_ERROR",
99+
};
99100
} else {
100101
const errorObj = JSON.parse(error.message.replace("Error: ", ""));
101102
let errorMessage = "Something went wrong";
103+
let errorCode = "API_ERROR";
104+
102105
if (errorObj.status) {
103106
switch (errorObj.status) {
104107
case 401:
105-
cliux.print("Authentication failed", {
106-
color: "red",
107-
bold: true,
108-
});
109-
cliux.print("Please check your apiKey, token, and region", {
110-
color: "yellow",
111-
});
112108
errorMessage =
113109
"Unauthorized: The apiKey, token or region is not valid.";
110+
errorCode = "AUTHENTICATION_FAILED";
114111
break;
115112
case 412:
116-
cliux.print("Invalid credentials", { color: "red", bold: true });
117-
cliux.print("Please verify your apiKey, token, and region", {
118-
color: "yellow",
119-
});
120113
errorMessage =
121114
"Invalid Credentials: Please check the provided apiKey, token and region.";
115+
errorCode = "INVALID_CREDENTIALS";
122116
break;
123117
default:
124-
cliux.print(`API Error (${errorObj.status})`, {
125-
color: "red",
126-
bold: true,
127-
});
128118
errorMessage = `${errorMessage}, ${errorObj.error_message}`;
119+
errorCode = `API_ERROR_${errorObj.status}`;
129120
}
130121
}
131122
if (errorObj.error_message && !errorObj.status) {
132123
errorMessage = `${errorMessage}, ${errorObj.error_message}`;
133124
}
134-
throw { error_message: errorMessage };
125+
throw {
126+
error_message: errorMessage,
127+
error_code: errorCode,
128+
};
135129
}
136130
}
137131
};
@@ -193,21 +187,19 @@ export const generateTSFromContentTypes = async ({
193187

194188
return output;
195189
} catch (err: any) {
196-
// Enhanced error logging with more context
197-
const errorMessage = err.message || "Unknown error occurred";
198-
const errorDetails = {
199-
error_message: `Type generation failed: ${errorMessage}`,
200-
context: "generateTSFromContentTypes",
201-
timestamp: new Date().toISOString(),
202-
error_type: err.constructor.name,
203-
};
204-
205-
// Log detailed error information for debugging
206-
cliux.print(`Type generation failed: ${errorMessage}`, {
207-
color: "red",
208-
bold: true,
209-
});
190+
// Handle numeric identifier errors specially to preserve their detailed format
191+
if (
192+
err.type === "validation" &&
193+
err.error_code === "VALIDATION_ERROR" &&
194+
err.error_message &&
195+
err.error_message.includes("numeric identifiers")
196+
) {
197+
// Pass through the detailed error as-is
198+
throw err;
199+
}
210200

201+
// Use common function to create detailed error information for other errors
202+
const errorDetails = createErrorDetails(err, "generateTSFromContentTypes");
211203
throw errorDetails;
212204
}
213205
};

src/generateTS/shared/utils.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,90 @@ export function checkNumericIdentifierExclusion(
5757
}
5858
return { shouldExclude: false };
5959
}
60+
61+
/**
62+
* Throws a UID validation error with standardized structure
63+
* @param params - Object containing error parameters
64+
* @param params.uid - The UID that caused the validation error
65+
* @param params.errorCode - The error code for the validation error
66+
* @param params.reason - The reason for the validation error
67+
* @param params.suggestion - The suggestion to resolve the issue
68+
* @param params.context - The context where the error occurred
69+
* @param params.referenceTo - Optional reference UID for global field errors
70+
* @throws A validation error object
71+
*/
72+
export function throwUIDValidationError({
73+
uid,
74+
errorCode,
75+
reason,
76+
suggestion,
77+
context,
78+
referenceTo,
79+
}: {
80+
uid: string;
81+
errorCode: string;
82+
reason: string;
83+
suggestion: string;
84+
context: string;
85+
referenceTo?: string;
86+
}): never {
87+
const errorMessage =
88+
errorCode === "INVALID_GLOBAL_FIELD_REFERENCE"
89+
? `Global field "${uid}" references content type "${referenceTo}" which starts with a number, creating invalid TypeScript interface names.`
90+
: `Content type UID "${uid}" starts with a number, which creates invalid TypeScript interface names.`;
91+
92+
throw {
93+
type: "validation",
94+
error_code: errorCode,
95+
error_message: errorMessage,
96+
details: {
97+
uid,
98+
...(referenceTo ? { reference_to: referenceTo } : {}),
99+
reason,
100+
suggestion,
101+
},
102+
context,
103+
timestamp: new Date().toISOString(),
104+
};
105+
}
106+
107+
/**
108+
* Creates a validation error in the exact format expected by tests
109+
* This maintains backward compatibility while reducing code duplication
110+
* @param errorMessage - The error message to display
111+
* @returns A validation error object with the expected structure
112+
*/
113+
export function createValidationError(errorMessage: string) {
114+
return {
115+
type: "validation",
116+
error_message: errorMessage,
117+
};
118+
}
119+
120+
/**
121+
* Creates standardized error details for different types of errors
122+
* @param err - The error object to process
123+
* @param context - The context where the error occurred
124+
* @returns Standardized error details object
125+
*/
126+
export function createErrorDetails(
127+
err: any,
128+
context: string = "generateTSFromContentTypes"
129+
) {
130+
if (err.type === "validation") {
131+
// Handle validation errors with proper error codes
132+
return {
133+
error_message: err.error_message || "Validation error occurred", // Keep for backwards compatibility
134+
error_code: err.error_code || "VALIDATION_ERROR", // New property
135+
details: err.details || {},
136+
};
137+
} else {
138+
// Handle other types of errors
139+
const errorMessage = err.message || "Unknown error occurred";
140+
return {
141+
error_message: `Type generation failed: ${errorMessage}`, // Keep for backwards compatibility
142+
error_code: "TYPE_GENERATION_FAILED", // New property
143+
details: {},
144+
};
145+
}
146+
}

0 commit comments

Comments
 (0)