Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,5 +71,6 @@
"packages/*",
"tests",
"tests/esm"
]
],
"packageManager": "[email protected]+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
47 changes: 47 additions & 0 deletions packages/cli/src/metadataGeneration/parameterGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,54 @@ export class ParameterGenerator {
const parameterName = (parameter.name as ts.Identifier).text;
const type = this.getValidatedType(parameter);

// Handle cases where TypeResolver doesn't properly resolve complex types
// like Zod's z.infer types to refObject or nestedObjectLiteral
if (type.dataType !== 'refObject' && type.dataType !== 'nestedObjectLiteral') {
// Try to resolve the type more aggressively for complex types
let typeNode = parameter.type;
if (!typeNode) {
const typeFromChecker = this.current.typeChecker.getTypeAtLocation(parameter);
typeNode = this.current.typeChecker.typeToTypeNode(typeFromChecker, undefined, ts.NodeBuilderFlags.NoTruncation) as ts.TypeNode;
}

// If it's a TypeReferenceNode (like z.infer), try to resolve it differently
if (ts.isTypeReferenceNode(typeNode)) {
try {
// Try to get the actual type from the type checker
const actualType = this.current.typeChecker.getTypeAtLocation(typeNode);
const typeNodeFromType = this.current.typeChecker.typeToTypeNode(actualType, undefined, ts.NodeBuilderFlags.NoTruncation) as ts.TypeNode;
const resolvedType = new TypeResolver(typeNodeFromType, this.current, parameter).resolve();

// Check if the resolved type is now acceptable
if (resolvedType.dataType === 'refObject' || resolvedType.dataType === 'nestedObjectLiteral') {
// Use the resolved type instead
for (const property of resolvedType.properties) {
this.validateQueriesProperties(property, parameterName);
}

const { examples: example, exampleLabels } = this.getParameterExample(parameter, parameterName);

return {
description: this.getParameterDescription(parameter),
in: 'queries',
name: parameterName,
example,
exampleLabels,
parameterName,
required: !parameter.questionToken && !parameter.initializer,
type: resolvedType,
validators: getParameterValidators(this.parameter, parameterName),
deprecated: this.getParameterDeprecation(parameter),
};
}
} catch (error) {
// If resolution fails, log the error for debugging but continue with the original error
// This helps developers understand why the type resolution failed
console.warn(`Failed to resolve complex type for @Queries('${parameterName}'):`, error);
// Continue with the original error below
}
}

throw new GenerateMetadataError(`@Queries('${parameterName}') only support 'refObject' or 'nestedObjectLiteral' types. If you want only one query parameter, please use the '@Query' decorator.`);
}

Expand Down
70 changes: 64 additions & 6 deletions packages/cli/src/metadataGeneration/typeResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,44 @@ export class TypeResolver {
} else {
const declarations = this.getModelTypeDeclarations(type);

// Handle cases where declarations is empty (e.g., inline object types in generics)
if (!declarations || declarations.length === 0) {
// Check if this is a simple identifier (like Date, String, etc.)
if (ts.isIdentifier(type)) {
// For simple identifiers, just return the name
return type.text;
}

// For inline object types, we should use the resolve method to get the proper type
// This will handle TypeLiteralNode, UnionTypeNode, etc. properly
// Note: We need to cast to TypeNode since EntityName can be Identifier or QualifiedName
const typeNode = type as unknown as ts.TypeNode;
const resolvedType = new TypeResolver(typeNode, this.current, this.parentNode, this.context).resolve();

// Generate a deterministic name for this inline type based on its structure
const typeName = this.calcTypeName(typeNode);
const sanitizedName = typeName
.replace(/[^A-Za-z0-9]/g, '_')
.replace(/_+/g, '_')
.replace(/^_|_$/g, '');

const uniqueName = `Inline_${sanitizedName}`;

// Add to reference types so it can be properly serialized
// We need to create a proper ReferenceType object
const referenceType: Tsoa.ReferenceType = {
dataType: 'refAlias',
refName: uniqueName,
type: resolvedType,
validators: {},
deprecated: false,
};

this.current.AddReferenceType(referenceType);

return uniqueName;
}

//Two possible solutions for recognizing different types:
// - Add declaration positions into type names (In an order).
// - It accepts multiple types with same name, if the code compiles, there would be no conflicts in the type names
Expand All @@ -589,7 +627,7 @@ export class TypeResolver {

const oneDeclaration = declarations[0]; //Every declarations should be in the same namespace hierarchy
const identifiers = name.split('.');
if (ts.isEnumMember(oneDeclaration)) {
if (oneDeclaration && ts.isEnumMember(oneDeclaration)) {
name = identifiers.slice(identifiers.length - 2).join('.');
} else {
name = identifiers.slice(identifiers.length - 1).join('.');
Expand Down Expand Up @@ -934,9 +972,16 @@ export class TypeResolver {
const fullEnumSymbol = this.getSymbolAtLocation(type.left);
symbol = fullEnumSymbol.exports?.get(typeName as any);
}
const declarations = symbol?.getDeclarations();

throwUnless(symbol && declarations, new GenerateMetadataError(`No declarations found for referenced type ${typeName}.`));
// Handle built-in types that don't have declarations in user code
if (!symbol || !symbol.getDeclarations) {
return [];
}

const declarations = symbol.getDeclarations();
if (!declarations || declarations.length === 0) {
return [];
}

if ((symbol.escapedName as string) !== typeName && (symbol.escapedName as string) !== 'default') {
typeName = symbol.escapedName as string;
Expand All @@ -946,7 +991,10 @@ export class TypeResolver {
return this.nodeIsUsable(node) && node.name?.getText() === typeName;
});

throwUnless(modelTypes.length, new GenerateMetadataError(`No matching model found for referenced type ${typeName}.`));
// If no usable model types found, return empty array instead of throwing
if (modelTypes.length === 0) {
return [];
}

if (modelTypes.length > 1) {
// remove types that are from typescript e.g. 'Account'
Expand Down Expand Up @@ -987,8 +1035,18 @@ export class TypeResolver {
private typeArgumentsToContext(type: ts.TypeReferenceNode | ts.ExpressionWithTypeArguments, targetEntity: ts.EntityName): Context {
let newContext: Context = {};

const declaration = this.getModelTypeDeclarations(targetEntity);
const typeParameters = 'typeParameters' in declaration[0] ? declaration[0].typeParameters : undefined;
// Handle cases where targetEntity might be an inline object type
// Inline object types don't have declarations, so we need to handle them differently
let declarations;
try {
declarations = this.getModelTypeDeclarations(targetEntity);
} catch (error) {
// If we can't get declarations (e.g., inline object type),
// we can't process type parameters, so return empty context
return newContext;
}

const typeParameters = 'typeParameters' in declarations[0] ? declarations[0].typeParameters : undefined;

if (typeParameters) {
for (let index = 0; index < typeParameters.length; index++) {
Expand Down