Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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) {
Comment thread
aryobenholzner marked this conversation as resolved.
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,9 @@ private Set<String> parseImports(CodegenModel cm) {
for (String name : cm.imports) {
if (name.indexOf(" | ") >= 0) {
String[] parts = name.split(" \\| ");
Collections.addAll(newImports, parts);
if (needToImport(parts[0])) {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
Collections.addAll(newImports, parts);
}
} 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) => {
Comment thread
aryobenholzner marked this conversation as resolved.
Outdated
Comment thread
aryobenholzner marked this conversation as resolved.
Outdated
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) => {
Comment thread
aryobenholzner marked this conversation as resolved.
Outdated
const request = ctx.switchToHttp().getRequest();
Comment thread
aryobenholzner marked this conversation as resolved.
Outdated
return data ? request.headers?.[data] : request.headers;
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{#defaultValue}}, new DefaultValuePipe({{{defaultValue}}}){{/defaultValue}}{{#isNumber}}, {{#isFloat}}ParseFloatPipe{{/isFloat}}{{^isFloat}}ParseIntPipe{{/isFloat}}{{/isNumber}}
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
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
Copy Markdown
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] : request.headers;
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './cookies-decorator';
export * from './headers-decorator';
Loading
Loading