Skip to content

Commit eaa79e2

Browse files
[typescript-nestjs-server] #22928 improve request parameter handling (#22960)
* [typescript-nestjs-server] #22928 exclude inline union strings from generating imports * [typescript-nestjs-server] #22928 add optional type hints * [typescript-nestjs-server] #22928 add/improve support for various parameter types * [typescript-nestjs-server] #22928 add docs, fix indentations and test execution * [typescript-nestjs-server] #22928 correctly parse numeric parameters, use DefaultValuePipe for default values * [typescript-nestjs-server] #22928 lowercase header access, check each import for unions * [typescript-nestjs-server] #22928 allow optional parameters for number parse pipes * [typescript-nestjs-server] #22928 updated README, additional PR feedback * [typescript-nestjs-server] #22928 updated README
1 parent 274510c commit eaa79e2

48 files changed

Lines changed: 727 additions & 53 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/samples-typescript-nestjs-server.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ on:
55
paths:
66
- samples/server/petstore/typescript-nestjs-server/**
77
- .github/workflows/samples-typescript-nestjs-server.yaml
8+
- .github/workflows/samples-typescript-nestjs-server-parameters.yaml
89
jobs:
910
build:
1011
name: Test TypeScript NestJS Server
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
generatorName: typescript-nestjs-server
2+
outputDir: samples/server/petstore/typescript-nestjs-server/builds/parameters
3+
inputSpec: modules/openapi-generator/src/test/resources/3_0/parameter-test-spec.yaml
4+
templateDir: modules/openapi-generator/src/main/resources/typescript-nestjs-server
5+
additionalProperties:
6+
"useSingleRequestParameter" : true

docs/generators/typescript-nestjs-server.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,14 @@ These options may be applied as additional-properties (cli) or configOptions (pl
4040
|nullSafeAdditionalProps|Set to make additional properties types declare that their indexer may return undefined| |false|
4141
|paramNaming|Naming convention for parameters: 'camelCase', 'PascalCase', 'snake_case' and 'original', which keeps the original name| |camelCase|
4242
|prependFormOrBodyParameters|Add form or body parameters to the beginning of the parameter list.| |false|
43-
|rxjsVersion|The version of RxJS compatible with Angular (see ngVersion option).| |null|
43+
|rxjsVersion|The version of RxJS.| |null|
4444
|snapshot|When setting this property to true, the version will be suffixed with -SNAPSHOT.yyyyMMddHHmm| |false|
4545
|sortModelPropertiesByRequiredFlag|Sort model properties to place required parameters before optional parameters.| |true|
4646
|sortParamsByRequiredFlag|Sort method arguments to place required parameters before optional parameters.| |true|
4747
|stringEnums|Generate string enums instead of objects for enum values.| |false|
4848
|supportsES6|Generate code that conforms to ES6.| |false|
4949
|taggedUnions|Use discriminators to create tagged unions instead of extending interfaces.| |false|
50-
|tsVersion|The version of typescript compatible with Angular (see ngVersion option).| |null|
50+
|tsVersion|The version of typescript.| |null|
5151
|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|
5252

5353
## IMPORT MAPPING

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptNestjsServerCodegen.java

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,8 @@ public TypeScriptNestjsServerCodegen() {
120120
this.cliOptions.add(new CliOption(FILE_NAMING, "Naming convention for the output files: 'camelCase', 'kebab-case'.").defaultValue(this.fileNaming));
121121
this.cliOptions.add(new CliOption(STRING_ENUMS, STRING_ENUMS_DESC).defaultValue(String.valueOf(this.stringEnums)));
122122
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()));
123-
this.cliOptions.add(new CliOption(TS_VERSION, "The version of typescript compatible with Angular (see ngVersion option)."));
124-
this.cliOptions.add(new CliOption(RXJS_VERSION, "The version of RxJS compatible with Angular (see ngVersion option)."));
123+
this.cliOptions.add(new CliOption(TS_VERSION, "The version of typescript."));
124+
this.cliOptions.add(new CliOption(RXJS_VERSION, "The version of RxJS."));
125125
}
126126

127127
@Override
@@ -156,6 +156,9 @@ public void processOpts() {
156156
supportingFiles.add(new SupportingFile("api-implementations.mustache", "", "api-implementations.ts"));
157157
supportingFiles.add(new SupportingFile("api.module.mustache", "", "api.module.ts"));
158158
supportingFiles.add(new SupportingFile("controllers.mustache", "controllers", "index.ts"));
159+
supportingFiles.add(new SupportingFile("cookies-decorator.mustache", "decorators", "cookies-decorator.ts"));
160+
supportingFiles.add(new SupportingFile("headers-decorator.mustache", "decorators", "headers-decorator.ts"));
161+
supportingFiles.add(new SupportingFile("decorators.mustache", "decorators", "index.ts"));
159162
supportingFiles.add(new SupportingFile("gitignore", "", ".gitignore"));
160163
supportingFiles.add(new SupportingFile("README.md", "", "README.md"));
161164
supportingFiles.add(new SupportingFile("tsconfig.mustache", "", "tsconfig.json"));
@@ -173,7 +176,7 @@ public void processOpts() {
173176
additionalProperties.put(NEST_VERSION, nestVersion);
174177

175178
if (additionalProperties.containsKey(NPM_NAME)) {
176-
if(!additionalProperties.containsKey(NPM_VERSION)) {
179+
if (!additionalProperties.containsKey(NPM_VERSION)) {
177180
additionalProperties.put(NPM_VERSION, "0.0.0");
178181
}
179182

@@ -274,7 +277,21 @@ private String applyLocalTypeMapping(String type) {
274277
}
275278

276279
private boolean isLanguagePrimitive(String type) {
277-
return languageSpecificPrimitives.contains(type);
280+
return languageSpecificPrimitives.contains(type) || isInlineUnion(type);
281+
}
282+
283+
/**
284+
* <p>
285+
* Determines if the given type is an inline union of strings, described as an enum without being an explicit component in OpenAPI spec.
286+
* </p>
287+
* Example input that matches: {@code "'A' | 'B'" }
288+
*
289+
* @param type The Typescript type to evaluate.
290+
*/
291+
private boolean isInlineUnion(String type) {
292+
return Arrays.stream(type.split("\\|"))
293+
.map(String::trim)
294+
.allMatch(value -> value.matches("([\"'].*[\"'])"));
278295
}
279296

280297
private boolean isLanguageGenericType(String type) {
@@ -294,6 +311,9 @@ private boolean isRecordType(String type) {
294311
public void postProcessParameter(CodegenParameter parameter) {
295312
super.postProcessParameter(parameter);
296313
parameter.dataType = applyLocalTypeMapping(parameter.dataType);
314+
if ("undefined".equals(parameter.defaultValue)) {
315+
parameter.defaultValue = null;
316+
}
297317
}
298318

299319
@Override
@@ -343,8 +363,8 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap operations, L
343363
// Collect imports from parameters
344364
if (operation.allParams != null) {
345365
for (CodegenParameter param : operation.allParams) {
346-
if(param.dataType != null) {
347-
if(isLanguageGenericType(param.dataType)) {
366+
if (param.dataType != null) {
367+
if (isLanguageGenericType(param.dataType)) {
348368
// Extract generic type and add to imports if its not a primitive
349369
String genericType = extractGenericType(param.dataType);
350370
if (genericType != null && !isLanguagePrimitive(genericType) && !isRecordType(genericType)) {
@@ -366,10 +386,10 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap operations, L
366386
if (isLanguageGenericType(operation.returnType)) {
367387
// Extract generic type and add to imports if it's not a primitive
368388
String genericType = extractGenericType(operation.returnType);
369-
if (genericType != null && !isLanguagePrimitive(genericType) && !isRecordType(genericType)) {
389+
if (needToImport(operation.returnType) && genericType != null && !isLanguagePrimitive(genericType) && !isRecordType(genericType)) {
370390
allImports.add(genericType);
371391
}
372-
} else {
392+
} else if (needToImport(operation.returnType)) {
373393
allImports.add(operation.returnType);
374394
}
375395
}
@@ -397,10 +417,10 @@ private String extractGenericType(String type) {
397417
return null;
398418
}
399419
String genericType = type.substring(startAngleBracketIndex + 1, endAngleBracketIndex);
400-
if(isLanguageGenericType(genericType)) {
420+
if (isLanguageGenericType(genericType)) {
401421
return extractGenericType(type);
402422
}
403-
if(genericType.contains("|")) {
423+
if (genericType.contains("|")) {
404424
return null;
405425
}
406426
return genericType;
@@ -429,7 +449,11 @@ private Set<String> parseImports(CodegenModel cm) {
429449
for (String name : cm.imports) {
430450
if (name.indexOf(" | ") >= 0) {
431451
String[] parts = name.split(" \\| ");
432-
Collections.addAll(newImports, parts);
452+
for (String part : parts) {
453+
if (needToImport(part)) {
454+
newImports.add(part);
455+
}
456+
}
433457
} else {
434458
newImports.add(name);
435459
}

modules/openapi-generator/src/main/resources/typescript-nestjs-server/README.md

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
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
44

5+
Currently, only Express is supported.
6+
57
Example usage (with the openapi sample `petstore.yaml`):
68

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

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

4648
@Module({
4749
imports: [
48-
ApiModule.forRoot(apiImplementations),
50+
ApiModule.forRoot({
51+
apiImplementations: apiImplementations,
52+
providers: [
53+
// additional providers for services injected into apiImplementations
54+
]
55+
}),
4956
],
5057
controllers: [],
5158
providers: [],
5259
})
5360
export class AppModule {}
5461
```
5562
56-
You now can regenerate the API module as often as you want without overriding your implementation.
63+
You now can regenerate the API module as often as you want without overriding your implementation.
64+
65+
## Using Cookie parameters
66+
67+
In order for cookie parameters to work, the framework specific cookie middleware must be enabled.
68+
69+
For Express, the [cookie-parser](https://www.npmjs.com/package/cookie-parser) middleware must be installed and enabled.
70+
71+
```
72+
npm install cookie-parser
73+
```
74+
75+
in `main.ts`
76+
77+
```
78+
import * as cookieParser from 'cookie-parser';
79+
80+
app.use(cookieParser());
81+
```

modules/openapi-generator/src/main/resources/typescript-nestjs-server/api.mustache

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import { Injectable } from '@nestjs/common';
22
import { Observable } from 'rxjs';
3+
{{#tsImports.0}}
34
import { {{#tsImports}}{{classname}}, {{/tsImports}} } from '../{{modelPackage}}';
5+
{{/tsImports.0}}
46

57
{{#useSingleRequestParameter}}
68
{{#operations}}
79
{{#operation}}
810
export type {{#lambda.pascalcase}}{{operationId}}{{/lambda.pascalcase}}RequestParams = {
911
{{#allParams}}
10-
{{paramName}}: {{{dataType}}}
12+
{{paramName}}: {{{dataType}}}{{#isNullable}} | null{{/isNullable}}{{^required}} | undefined{{/required}}
1113
{{/allParams}}
1214
}
1315
{{/operation}}
@@ -23,7 +25,7 @@ export abstract class {{classname}} {
2325
{{/useSingleRequestParameter}}
2426

2527
{{^useSingleRequestParameter}}
26-
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}}>;
28+
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}}>;
2729
{{/useSingleRequestParameter}}
2830

2931
{{/operation}}

modules/openapi-generator/src/main/resources/typescript-nestjs-server/controller.mustache

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
import { Body, Controller{{#httpMethods}}, {{.}}{{/httpMethods}}, Param, Query, Req } from '@nestjs/common';
1+
import { Body, Controller, DefaultValuePipe{{#httpMethods}}, {{.}}{{/httpMethods}}, Param, ParseIntPipe, ParseFloatPipe, Query, Req } from '@nestjs/common';
22
import { Observable } from 'rxjs';
3+
import { Cookies, Headers } from '../decorators';
34
import { {{classname}} } from '../{{apiPackage}}';
5+
{{#tsImports.0}}
46
import { {{#tsImports}}{{classname}}, {{/tsImports}} } from '../{{modelPackage}}';
7+
{{/tsImports.0}}
58

69
@Controller()
710
export class {{classname}}Controller {
@@ -10,7 +13,7 @@ export class {{classname}}Controller {
1013
{{#operations}}
1114
{{#operation}}
1215
@{{#vendorExtensions.x-http-method}}{{.}}{{/vendorExtensions.x-http-method}}{{^vendorExtensions.x-http-method}}{{httpMethod}}{{/vendorExtensions.x-http-method}}('{{path}}')
13-
{{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}}> {
16+
{{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}}> {
1417
return this.{{classVarName}}.{{operationId}}({{#useSingleRequestParameter}}{ {{/useSingleRequestParameter}}{{#allParams}}{{paramName}}, {{/allParams}}{{#useSingleRequestParameter}}}, {{/useSingleRequestParameter}}request);
1518
}
1619

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
2+
3+
/**
4+
* A decorator function for retrieving cookies from the request object in an HTTP context.
5+
*
6+
* This decorator only works, if the framework specific cookie middleware is installed and enabled.
7+
* - For Express, you need to use the `cookie-parser` middleware.
8+
* - For Fastify, you need to use the `@fastify/cookie` plugin.
9+
*
10+
* Consult https://docs.nestjs.com/techniques/cookies for further information
11+
*
12+
* Usage:
13+
* ```
14+
* @Get()
15+
* findAll(@Cookies('name') name: string) {}
16+
* ```
17+
*/
18+
export const Cookies = createParamDecorator((cookieName: string, ctx: ExecutionContext) => {
19+
const request = ctx.switchToHttp().getRequest();
20+
if (!cookieName) {
21+
return { ...request.cookies, ...request.signedCookies };
22+
}
23+
return request.cookies?.[cookieName] ?? request.signedCookies?.[cookieName];
24+
});
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './cookies-decorator';
2+
export * from './headers-decorator';
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
2+
3+
/**
4+
* A decorator function for retrieving headers from the request object in an HTTP context.
5+
* Workaround for enabling PipeTransformers on Headers (see https://github.com/nestjs/nest/issues/356)
6+
*
7+
* Usage:
8+
* ```
9+
* @Get()
10+
* findAll(@Headers('name') name: string) {}
11+
* ```
12+
*/
13+
export const Headers = createParamDecorator((headerName: string, ctx: ExecutionContext) => {
14+
const request = ctx.switchToHttp().getRequest();
15+
return headerName ? request.headers?.[headerName.toLowerCase()] : request.headers;
16+
});

0 commit comments

Comments
 (0)