Skip to content
Open
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
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