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
1 change: 1 addition & 0 deletions .github/workflows/samples-typescript-nestjs-server.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ on:
paths:
- samples/server/petstore/typescript-nestjs-server/**
- .github/workflows/samples-typescript-nestjs-server.yaml
- .github/workflows/samples-typescript-nestjs-server-parameters.yaml
jobs:
build:
name: Test TypeScript NestJS Server
Expand Down
6 changes: 6 additions & 0 deletions bin/configs/typescript-nestjs-server-parameters.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
generatorName: typescript-nestjs-server
outputDir: samples/server/petstore/typescript-nestjs-server/builds/parameters
inputSpec: modules/openapi-generator/src/test/resources/3_0/parameter-test-spec.yaml
templateDir: modules/openapi-generator/src/main/resources/typescript-nestjs-server
additionalProperties:
"useSingleRequestParameter" : true
4 changes: 2 additions & 2 deletions docs/generators/typescript-nestjs-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,14 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|nullSafeAdditionalProps|Set to make additional properties types declare that their indexer may return undefined| |false|
|paramNaming|Naming convention for parameters: 'camelCase', 'PascalCase', 'snake_case' and 'original', which keeps the original name| |camelCase|
|prependFormOrBodyParameters|Add form or body parameters to the beginning of the parameter list.| |false|
|rxjsVersion|The version of RxJS compatible with Angular (see ngVersion option).| |null|
|rxjsVersion|The version of RxJS.| |null|
|snapshot|When setting this property to true, the version will be suffixed with -SNAPSHOT.yyyyMMddHHmm| |false|
|sortModelPropertiesByRequiredFlag|Sort model properties to place required parameters before optional parameters.| |true|
|sortParamsByRequiredFlag|Sort method arguments to place required parameters before optional parameters.| |true|
|stringEnums|Generate string enums instead of objects for enum values.| |false|
|supportsES6|Generate code that conforms to ES6.| |false|
|taggedUnions|Use discriminators to create tagged unions instead of extending interfaces.| |false|
|tsVersion|The version of typescript compatible with Angular (see ngVersion option).| |null|
|tsVersion|The version of typescript.| |null|
|useSingleRequestParameter|Setting this property to true will generate functions with a single argument containing all API endpoint parameters instead of one argument per parameter.| |false|

## IMPORT MAPPING
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@ public TypeScriptNestjsServerCodegen() {
this.cliOptions.add(new CliOption(FILE_NAMING, "Naming convention for the output files: 'camelCase', 'kebab-case'.").defaultValue(this.fileNaming));
this.cliOptions.add(new CliOption(STRING_ENUMS, STRING_ENUMS_DESC).defaultValue(String.valueOf(this.stringEnums)));
this.cliOptions.add(new CliOption(USE_SINGLE_REQUEST_PARAMETER, "Setting this property to true will generate functions with a single argument containing all API endpoint parameters instead of one argument per parameter.").defaultValue(Boolean.FALSE.toString()));
this.cliOptions.add(new CliOption(TS_VERSION, "The version of typescript compatible with Angular (see ngVersion option)."));
this.cliOptions.add(new CliOption(RXJS_VERSION, "The version of RxJS compatible with Angular (see ngVersion option)."));
this.cliOptions.add(new CliOption(TS_VERSION, "The version of typescript."));
this.cliOptions.add(new CliOption(RXJS_VERSION, "The version of RxJS."));
}

@Override
Expand Down Expand Up @@ -156,6 +156,9 @@ public void processOpts() {
supportingFiles.add(new SupportingFile("api-implementations.mustache", "", "api-implementations.ts"));
supportingFiles.add(new SupportingFile("api.module.mustache", "", "api.module.ts"));
supportingFiles.add(new SupportingFile("controllers.mustache", "controllers", "index.ts"));
supportingFiles.add(new SupportingFile("cookies-decorator.mustache", "decorators", "cookies-decorator.ts"));
supportingFiles.add(new SupportingFile("headers-decorator.mustache", "decorators", "headers-decorator.ts"));
supportingFiles.add(new SupportingFile("decorators.mustache", "decorators", "index.ts"));
supportingFiles.add(new SupportingFile("gitignore", "", ".gitignore"));
supportingFiles.add(new SupportingFile("README.md", "", "README.md"));
supportingFiles.add(new SupportingFile("tsconfig.mustache", "", "tsconfig.json"));
Expand All @@ -173,7 +176,7 @@ public void processOpts() {
additionalProperties.put(NEST_VERSION, nestVersion);

if (additionalProperties.containsKey(NPM_NAME)) {
if(!additionalProperties.containsKey(NPM_VERSION)) {
if (!additionalProperties.containsKey(NPM_VERSION)) {
additionalProperties.put(NPM_VERSION, "0.0.0");
}

Expand Down Expand Up @@ -274,7 +277,21 @@ private String applyLocalTypeMapping(String type) {
}

private boolean isLanguagePrimitive(String type) {
return languageSpecificPrimitives.contains(type);
return languageSpecificPrimitives.contains(type) || isInlineUnion(type);
}

/**
* <p>
* Determines if the given type is an inline union of strings, described as an enum without being an explicit component in OpenAPI spec.
* </p>
* Example input that matches: {@code "'A' | 'B'" }
*
* @param type The Typescript type to evaluate.
*/
private boolean isInlineUnion(String type) {
return Arrays.stream(type.split("\\|"))
.map(String::trim)
.allMatch(value -> value.matches("([\"'].*[\"'])"));
}

private boolean isLanguageGenericType(String type) {
Expand All @@ -294,6 +311,9 @@ private boolean isRecordType(String type) {
public void postProcessParameter(CodegenParameter parameter) {
super.postProcessParameter(parameter);
parameter.dataType = applyLocalTypeMapping(parameter.dataType);
if ("undefined".equals(parameter.defaultValue)) {
parameter.defaultValue = null;
}
}

@Override
Expand Down Expand Up @@ -343,8 +363,8 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap operations, L
// Collect imports from parameters
if (operation.allParams != null) {
for (CodegenParameter param : operation.allParams) {
if(param.dataType != null) {
if(isLanguageGenericType(param.dataType)) {
if (param.dataType != null) {
if (isLanguageGenericType(param.dataType)) {
// Extract generic type and add to imports if its not a primitive
String genericType = extractGenericType(param.dataType);
if (genericType != null && !isLanguagePrimitive(genericType) && !isRecordType(genericType)) {
Expand All @@ -366,10 +386,10 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap operations, L
if (isLanguageGenericType(operation.returnType)) {
// Extract generic type and add to imports if it's not a primitive
String genericType = extractGenericType(operation.returnType);
if (genericType != null && !isLanguagePrimitive(genericType) && !isRecordType(genericType)) {
if (needToImport(operation.returnType) && genericType != null && !isLanguagePrimitive(genericType) && !isRecordType(genericType)) {
allImports.add(genericType);
}
} else {
} else if (needToImport(operation.returnType)) {
allImports.add(operation.returnType);
}
}
Expand Down Expand Up @@ -397,10 +417,10 @@ private String extractGenericType(String type) {
return null;
}
String genericType = type.substring(startAngleBracketIndex + 1, endAngleBracketIndex);
if(isLanguageGenericType(genericType)) {
if (isLanguageGenericType(genericType)) {
return extractGenericType(type);
}
if(genericType.contains("|")) {
if (genericType.contains("|")) {
return null;
}
return genericType;
Expand Down Expand Up @@ -429,7 +449,11 @@ private Set<String> parseImports(CodegenModel cm) {
for (String name : cm.imports) {
if (name.indexOf(" | ") >= 0) {
String[] parts = name.split(" \\| ");
Collections.addAll(newImports, parts);
for (String part : parts) {
if (needToImport(part)) {
newImports.add(part);
}
}
} else {
newImports.add(name);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';
{{#tsImports.0}}
import { {{#tsImports}}{{classname}}, {{/tsImports}} } from '../{{modelPackage}}';
{{/tsImports.0}}

{{#useSingleRequestParameter}}
{{#operations}}
{{#operation}}
export type {{#lambda.pascalcase}}{{operationId}}{{/lambda.pascalcase}}RequestParams = {
{{#allParams}}
{{paramName}}: {{{dataType}}}
{{paramName}}: {{{dataType}}}{{#isNullable}} | null{{/isNullable}}{{^required}} | undefined{{/required}}
{{/allParams}}
}
{{/operation}}
Expand All @@ -23,7 +25,7 @@ export abstract class {{classname}} {
{{/useSingleRequestParameter}}

{{^useSingleRequestParameter}}
abstract {{operationId}}({{#allParams}}{{paramName}}: {{{dataType}}}, {{/allParams}} request: Request): {{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}void{{/returnType}} | Promise<{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}void{{/returnType}}> | Observable<{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}void{{/returnType}}>;
abstract {{operationId}}({{#allParams}}{{paramName}}: {{{dataType}}}{{#isNullable}} | null{{/isNullable}}{{^required}} | undefined{{/required}}, {{/allParams}} request: Request): {{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}void{{/returnType}} | Promise<{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}void{{/returnType}}> | Observable<{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}void{{/returnType}}>;
{{/useSingleRequestParameter}}

{{/operation}}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { Body, Controller{{#httpMethods}}, {{.}}{{/httpMethods}}, Param, Query, Req } from '@nestjs/common';
import { Body, Controller, DefaultValuePipe{{#httpMethods}}, {{.}}{{/httpMethods}}, Param, ParseIntPipe, ParseFloatPipe, Query, Req } from '@nestjs/common';
import { Observable } from 'rxjs';
import { Cookies, Headers } from '../decorators';
import { {{classname}} } from '../{{apiPackage}}';
{{#tsImports.0}}
import { {{#tsImports}}{{classname}}, {{/tsImports}} } from '../{{modelPackage}}';
{{/tsImports.0}}

@Controller()
export class {{classname}}Controller {
Expand All @@ -10,7 +13,7 @@ export class {{classname}}Controller {
{{#operations}}
{{#operation}}
@{{#vendorExtensions.x-http-method}}{{.}}{{/vendorExtensions.x-http-method}}{{^vendorExtensions.x-http-method}}{{httpMethod}}{{/vendorExtensions.x-http-method}}('{{path}}')
{{operationId}}({{#allParams}}{{#isPathParam}}@Param('{{paramName}}') {{/isPathParam}}{{#isQueryParam}}@Query('{{paramName}}') {{/isQueryParam}}{{#isBodyParam}}@Body() {{/isBodyParam}}{{paramName}}: {{{dataType}}}, {{/allParams}}@Req() request: Request): {{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}void{{/returnType}} | Promise<{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}void{{/returnType}}> | Observable<{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}void{{/returnType}}> {
{{operationId}}({{#allParams}}{{#isPathParam}}@Param('{{baseName}}'{{>paramPipe}}) {{/isPathParam}}{{#isQueryParam}}@Query('{{baseName}}'{{>paramPipe}}) {{/isQueryParam}}{{#isHeaderParam}}@Headers('{{baseName}}'{{>paramPipe}}) {{/isHeaderParam}}{{#isCookieParam}}@Cookies('{{baseName}}'{{>paramPipe}}) {{/isCookieParam}}{{#isBodyParam}}@Body() {{/isBodyParam}}{{paramName}}: {{{dataType}}}{{#isNullable}} | null{{/isNullable}}{{^required}} | undefined{{/required}}, {{/allParams}}@Req() request: Request): {{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}void{{/returnType}} | Promise<{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}void{{/returnType}}> | Observable<{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}void{{/returnType}}> {
return this.{{classVarName}}.{{operationId}}({{#useSingleRequestParameter}}{ {{/useSingleRequestParameter}}{{#allParams}}{{paramName}}, {{/allParams}}{{#useSingleRequestParameter}}}, {{/useSingleRequestParameter}}request);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';

/**
* A decorator function for retrieving cookies from the request object in an HTTP context.
*
* This decorator only works, if the framework specific cookie middleware is installed and enabled.
* - For Express, you need to use the `cookie-parser` middleware.
* - For Fastify, you need to use the `@fastify/cookie` plugin.
*
* Consult https://docs.nestjs.com/techniques/cookies for further information
*
* Usage:
* ```
* @Get()
* findAll(@Cookies('name') name: string) {}
* ```
*/
export const Cookies = createParamDecorator((data: string, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
if (!data) {
return { ...request.cookies, ...request.signedCookies };
}
return request.cookies?.[data] ?? request.signedCookies?.[data];
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './cookies-decorator';
export * from './headers-decorator';
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';

/**
* A decorator function for retrieving headers from the request object in an HTTP context.
* Workaround for enabling PipeTransformers on Headers (see https://github.com/nestjs/nest/issues/356)
*
* Usage:
* ```
* @Get()
* findAll(@Headers('name') name: string) {}
* ```
*/
export const Headers = createParamDecorator((data: string, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return data ? request.headers?.[data.toLowerCase()] : request.headers;
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{#defaultValue}}, new DefaultValuePipe({{{defaultValue}}}){{/defaultValue}}{{#isNumber}}, new {{#isFloat}}ParseFloatPipe({{/isFloat}}{{^isFloat}}ParseIntPipe({{/isFloat}}{{^isRequired}}{optional: true}{{/isRequired}}{{#isNullable}}{optional: true}{{/isNullable}}){{/isNumber}}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ controllers/PetApi.controller.ts
controllers/StoreApi.controller.ts
controllers/UserApi.controller.ts
controllers/index.ts
decorators/cookies-decorator.ts
decorators/headers-decorator.ts
decorators/index.ts
index.ts
models/api-response.ts
models/category.ts
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export abstract class PetApi {
abstract addPet(pet: Pet, request: Request): Pet | Promise<Pet> | Observable<Pet>;


abstract deletePet(petId: number, apiKey: string, request: Request): void | Promise<void> | Observable<void>;
abstract deletePet(petId: number, apiKey: string | undefined, request: Request): void | Promise<void> | Observable<void>;


abstract findPetsByStatus(status: Array<'available' | 'pending' | 'sold'>, request: Request): Array<Pet> | Promise<Array<Pet>> | Observable<Array<Pet>>;
Expand All @@ -24,9 +24,9 @@ export abstract class PetApi {
abstract updatePet(pet: Pet, request: Request): Pet | Promise<Pet> | Observable<Pet>;


abstract updatePetWithForm(petId: number, name: string, status: string, request: Request): void | Promise<void> | Observable<void>;
abstract updatePetWithForm(petId: number, name: string | undefined, status: string | undefined, request: Request): void | Promise<void> | Observable<void>;


abstract uploadFile(petId: number, additionalMetadata: string, file: Blob, request: Request): ApiResponse | Promise<ApiResponse> | Observable<ApiResponse>;
abstract uploadFile(petId: number, additionalMetadata: string | undefined, file: Blob | undefined, request: Request): ApiResponse | Promise<ApiResponse> | Observable<ApiResponse>;

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Body, Controller, Delete, Get, Post, Put, Param, Query, Req } from '@nestjs/common';
import { Body, Controller, DefaultValuePipe, Delete, Get, Post, Put, Param, ParseIntPipe, ParseFloatPipe, Query, Req } from '@nestjs/common';
import { Observable } from 'rxjs';
import { Cookies, Headers } from '../decorators';
import { PetApi } from '../api';
import { ApiResponse, Pet, } from '../models';

Expand All @@ -13,7 +14,7 @@ export class PetApiController {
}

@Delete('/pet/:petId')
deletePet(@Param('petId') petId: number, apiKey: string, @Req() request: Request): void | Promise<void> | Observable<void> {
deletePet(@Param('petId') petId: number, @Headers('api_key') apiKey: string | undefined, @Req() request: Request): void | Promise<void> | Observable<void> {
return this.petApi.deletePet(petId, apiKey, request);
}

Expand All @@ -38,12 +39,12 @@ export class PetApiController {
}

@Post('/pet/:petId')
updatePetWithForm(@Param('petId') petId: number, name: string, status: string, @Req() request: Request): void | Promise<void> | Observable<void> {
updatePetWithForm(@Param('petId') petId: number, name: string | undefined, status: string | undefined, @Req() request: Request): void | Promise<void> | Observable<void> {
return this.petApi.updatePetWithForm(petId, name, status, request);
}

@Post('/pet/:petId/uploadImage')
uploadFile(@Param('petId') petId: number, additionalMetadata: string, file: Blob, @Req() request: Request): ApiResponse | Promise<ApiResponse> | Observable<ApiResponse> {
uploadFile(@Param('petId') petId: number, additionalMetadata: string | undefined, file: Blob | undefined, @Req() request: Request): ApiResponse | Promise<ApiResponse> | Observable<ApiResponse> {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Upload endpoint defines a file parameter but lacks @UseInterceptors(FileInterceptor(...)) and @UploadedFile(); NestJS won’t extract multipart file data, so file will be undefined and the upload endpoint won’t work.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/server/petstore/typescript-nestjs-server/builds/default/controllers/PetApi.controller.ts, line 47:

<comment>Upload endpoint defines a `file` parameter but lacks `@UseInterceptors(FileInterceptor(...))` and `@UploadedFile()`; NestJS won’t extract multipart file data, so `file` will be undefined and the upload endpoint won’t work.</comment>

<file context>
@@ -38,12 +39,12 @@ export class PetApiController {
 
   @Post('/pet/:petId/uploadImage')
-  uploadFile(@Param('petId') petId: number, additionalMetadata: string, file: Blob, @Req() request: Request): ApiResponse | Promise<ApiResponse> | Observable<ApiResponse> {
+  uploadFile(@Param('petId') petId: number, additionalMetadata: string | undefined, file: Blob | undefined, @Req() request: Request): ApiResponse | Promise<ApiResponse> | Observable<ApiResponse> {
     return this.petApi.uploadFile(petId, additionalMetadata, file, request);
   }
</file context>
Fix with Cubic

return this.petApi.uploadFile(petId, additionalMetadata, file, request);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Body, Controller, Delete, Get, Post, Param, Query, Req } from '@nestjs/common';
import { Body, Controller, DefaultValuePipe, Delete, Get, Post, Param, ParseIntPipe, ParseFloatPipe, Query, Req } from '@nestjs/common';
import { Observable } from 'rxjs';
import { Cookies, Headers } from '../decorators';
import { StoreApi } from '../api';
import { Order, } from '../models';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Body, Controller, Delete, Get, Post, Put, Param, Query, Req } from '@nestjs/common';
import { Body, Controller, DefaultValuePipe, Delete, Get, Post, Put, Param, ParseIntPipe, ParseFloatPipe, Query, Req } from '@nestjs/common';
import { Observable } from 'rxjs';
import { Cookies, Headers } from '../decorators';
import { UserApi } from '../api';
import { User, } from '../models';

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';

/**
* A decorator function for retrieving cookies from the request object in an HTTP context.
*
* This decorator only works, if the framework specific cookie middleware is installed and enabled.
* - For Express, you need to use the `cookie-parser` middleware.
* - For Fastify, you need to use the `@fastify/cookie` plugin.
*
* Consult https://docs.nestjs.com/techniques/cookies for further information
*
* Usage:
* ```
* @Get()
* findAll(@Cookies('name') name: string) {}
* ```
*/
export const Cookies = createParamDecorator((data: string, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
if (!data) {
return { ...request.cookies, ...request.signedCookies };
}
return request.cookies?.[data] ?? request.signedCookies?.[data];
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';

/**
* A decorator function for retrieving headers from the request object in an HTTP context.
* Workaround for enabling PipeTransformers on Headers (see https://github.com/nestjs/nest/issues/356)
*
* Usage:
* ```
* @Get()
* findAll(@Headers('name') name: string) {}
* ```
*/
export const Headers = createParamDecorator((data: string, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return data ? request.headers?.[data.toLowerCase()] : request.headers;
});
Loading
Loading