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
Expand Up @@ -2,6 +2,8 @@

Usage: The generated output is intended to be its own module, that can be imported into your NestJS App Module. You do not need to change generated files, just import the module and implement the API

Currently, only Express is supported.
Copy link
Member

Choose a reason for hiding this comment

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

was this already the case before this change?


Example usage (with the openapi sample `petstore.yaml`):

1. Invoke openapi-generator
Expand All @@ -28,7 +30,7 @@ Example usage (with the openapi sample `petstore.yaml`):

...
```
3. Import the generated `ApiModule` with `ApiModule.forRoot` and provide a instance of `ApiImplementations` with a reference to your implementation
3. Import the generated `ApiModule` with `ApiModule.forRoot` and provide an instance of `ApiImplementations` with a reference to your implementation
`app.module.ts`
```typescript
import { Module } from "@nestjs/common";
Expand All @@ -45,12 +47,35 @@ Example usage (with the openapi sample `petstore.yaml`):

@Module({
imports: [
ApiModule.forRoot(apiImplementations),
ApiModule.forRoot({
apiImplementations: apiImplementations,
providers: [
// additional providers for services injected into apiImplementations
]
}),
],
controllers: [],
providers: [],
})
export class AppModule {}
```

You now can regenerate the API module as often as you want without overriding your implementation.
You now can regenerate the API module as often as you want without overriding your implementation.

## Using Cookie parameters

In order for cookie parameters to work, the the framework specific cookie middleware must be enabled.

For Express, the `cookie-parser` middleware must be installed and enabled.
Copy link
Member

Choose a reason for hiding this comment

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

please link the package/npm page here


```
npm install cookie-parser
```

in `main.ts`

```
import * as cookieParser from 'cookie-parser';

app.use(cookieParser());
```
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((cookieName: string, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
if (!cookieName) {
return { ...request.cookies, ...request.signedCookies };
}
return request.cookies?.[cookieName] ?? request.signedCookies?.[cookieName];
});
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((headerName: string, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return headerName ? request.headers?.[headerName.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 @@ -2,6 +2,8 @@

Usage: The generated output is intended to be its own module, that can be imported into your NestJS App Module. You do not need to change generated files, just import the module and implement the API

Currently, only Express is supported.

Example usage (with the openapi sample `petstore.yaml`):

1. Invoke openapi-generator
Expand All @@ -28,7 +30,7 @@ Example usage (with the openapi sample `petstore.yaml`):

...
```
3. Import the generated `ApiModule` with `ApiModule.forRoot` and provide a instance of `ApiImplementations` with a reference to your implementation
3. Import the generated `ApiModule` with `ApiModule.forRoot` and provide an instance of `ApiImplementations` with a reference to your implementation
`app.module.ts`
```typescript
import { Module } from "@nestjs/common";
Expand All @@ -45,12 +47,35 @@ Example usage (with the openapi sample `petstore.yaml`):

@Module({
imports: [
ApiModule.forRoot(apiImplementations),
ApiModule.forRoot({
apiImplementations: apiImplementations,
providers: [
// additional providers for services injected into apiImplementations
]
}),
],
controllers: [],
providers: [],
})
export class AppModule {}
```

You now can regenerate the API module as often as you want without overriding your implementation.
You now can regenerate the API module as often as you want without overriding your implementation.

## Using Cookie parameters

In order for cookie parameters to work, the the framework specific cookie middleware must be enabled.

For Express, the `cookie-parser` middleware must be installed and enabled.

```
npm install cookie-parser
```

in `main.ts`

```
import * as cookieParser from 'cookie-parser';

app.use(cookieParser());
```
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>;

}
Loading
Loading