Skip to content

Commit 2bbf80a

Browse files
authored
fix(type-safe-api): address compilation issues for models with reserved words and array params (#834)
Address an issue where models would not compile when reserved words (such as `if`) were used in request parameters. Also address an issue where arrays of enum request parameters would cause operation config to not compile as it did not render some booleans.
1 parent f2ca308 commit 2bbf80a

File tree

7 files changed

+1634
-57
lines changed

7 files changed

+1634
-57
lines changed

packages/type-safe-api/scripts/type-safe-api/generators/generate.ts

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,16 @@ import SwaggerParser from "@apidevtools/swagger-parser";
66
import { parse } from "ts-command-line-args";
77
import * as ejs from "ejs";
88
import * as path from "path";
9-
import * as _ from "lodash";
9+
import _get from "lodash/get";
10+
import _set from "lodash/set";
11+
import _trim from "lodash/trim";
12+
import _upperFirst from "lodash/upperFirst";
13+
import _camelCase from "lodash/camelCase";
14+
import _snakeCase from "lodash/snakeCase";
15+
import _kebabCase from "lodash/kebabCase";
16+
import _orderBy from "lodash/orderBy";
17+
import _uniq from "lodash/uniq";
18+
import _uniqBy from "lodash/uniqBy";
1019
import { OpenAPIV3 } from "openapi-types";
1120
import * as parseOpenapi from "parse-openapi";
1221
import { getOperationResponses } from "parse-openapi/dist/parser/getOperationResponses";
@@ -79,7 +88,7 @@ const splitRef = (ref: string): string[] =>
7988
*/
8089
const resolveRef = (spec: OpenAPIV3.Document, ref: string): any => {
8190
const refParts = splitRef(ref);
82-
const resolved = _.get(spec, refParts);
91+
const resolved = _get(spec, refParts);
8392
if (!resolved) {
8493
throw new Error(`Unable to resolve ref ${ref} in spec`);
8594
}
@@ -236,12 +245,12 @@ const toTypeScriptType = (property: parseOpenapi.Model): string => {
236245
* Mutates the given model to add language specific types and names
237246
*/
238247
const mutateModelWithAdditionalTypes = (model: parseOpenapi.Model) => {
239-
(model as any).typescriptName = _.camelCase(model.name);
248+
(model as any).typescriptName = model.name;
240249
(model as any).typescriptType = toTypeScriptType(model);
241250
(model as any).isPrimitive = PRIMITIVE_TYPES.has(model.type);
242251

243252
// Trim any surrounding quotes from name
244-
model.name = _.trim(model.name, `"'`);
253+
model.name = _trim(model.name, `"'`);
245254
};
246255

247256
const mutateWithOpenapiSchemaProperties = (spec: OpenAPIV3.Document, model: parseOpenapi.Model, schema: OpenAPIV3.SchemaObject, visited: Set<parseOpenapi.Model> = new Set()) => {
@@ -292,7 +301,7 @@ const hoistInlineObjectSubSchemas = (nameParts: string[], schema: OpenAPIV3.Sche
292301

293302
// Clone the object subschemas to build the refs
294303
const refs = inlineSubSchemas.filter(s => s.schema.type === "object" && ["items", "additionalProperties"].includes(s.prop)).map(s => {
295-
const name = s.nameParts.map(_.upperFirst).join('');
304+
const name = s.nameParts.map(_upperFirst).join('');
296305
const $ref = `#/components/schemas/${name}`;
297306
const ref = {
298307
$ref,
@@ -301,7 +310,7 @@ const hoistInlineObjectSubSchemas = (nameParts: string[], schema: OpenAPIV3.Sche
301310
};
302311

303312
// Replace each subschema with a ref in the spec
304-
_.set(schema, s.prop, { $ref });
313+
_set(schema, s.prop, { $ref });
305314

306315
return ref;
307316
});
@@ -341,7 +350,7 @@ const buildData = (inSpec: OpenAPIV3.Document, metadata: any) => {
341350
const response = resolveIfRef(spec, res);
342351
const jsonResponseSchema = response?.content?.['application/json']?.schema;
343352
if (jsonResponseSchema && !isRef(jsonResponseSchema) && ["object", "array"].includes(jsonResponseSchema.type!)) {
344-
const schemaName = `${_.upperFirst(_.camelCase(operation.operationId ?? `${path}-${method}`))}${code}Response`;
353+
const schemaName = `${_upperFirst(_camelCase(operation.operationId ?? `${path}-${method}`))}${code}Response`;
345354
spec.components!.schemas![schemaName] = jsonResponseSchema;
346355
response!.content!['application/json'].schema = {
347356
$ref: `#/components/schemas/${schemaName}`,
@@ -413,7 +422,7 @@ const buildData = (inSpec: OpenAPIV3.Document, metadata: any) => {
413422
// If the operation didn't specify an operationId, we need to generate one in a backwards compatible way
414423
// which matches openapi generator
415424
if (!specOp.operationId) {
416-
(op as any).name = _.camelCase(`${op.path.replace(/{(.*?)}/g, 'by-$1').replace(/[/:]/g, '-')}-${op.method}`);
425+
(op as any).name = _camelCase(`${op.path.replace(/{(.*?)}/g, 'by-$1').replace(/[/:]/g, '-')}-${op.method}`);
417426
}
418427
}
419428

@@ -434,7 +443,7 @@ const buildData = (inSpec: OpenAPIV3.Document, metadata: any) => {
434443

435444
if (parameter.in === "body") {
436445
// Parameter name for the body is it's type in camelCase
437-
parameter.name = parameter.export === "reference" ? _.camelCase(parameter.type) : "body";
446+
parameter.name = parameter.export === "reference" ? _camelCase(parameter.type) : "body";
438447

439448
// The request body is not in the "parameters" section of the openapi spec so we won't have added the schema
440449
// properties above. Find it here.
@@ -469,16 +478,16 @@ const buildData = (inSpec: OpenAPIV3.Document, metadata: any) => {
469478
[...((op as any).responses ?? []), ...op.results].forEach(mutateModelWithAdditionalTypes);
470479

471480
// Add variants of operation name
472-
(op as any).operationIdPascalCase = _.upperFirst(op.name);
473-
(op as any).operationIdKebabCase = _.kebabCase(op.name);
474-
(op as any).operationIdSnakeCase = _.snakeCase(op.name);
481+
(op as any).operationIdPascalCase = _upperFirst(op.name);
482+
(op as any).operationIdKebabCase = _kebabCase(op.name);
483+
(op as any).operationIdSnakeCase = _snakeCase(op.name);
475484
});
476485

477486
// Lexicographical ordering of operations to match openapi generator
478-
service.operations = _.orderBy(service.operations, (op) => op.name);
487+
service.operations = _orderBy(service.operations, (op) => op.name);
479488

480489
// Add the models to import
481-
(service as any).modelImports = _.orderBy(_.uniq([...service.imports, ...responseModelImports]));
490+
(service as any).modelImports = _orderBy(_uniq([...service.imports, ...responseModelImports]));
482491

483492
// Add the service class name
484493
(service as any).className = `${service.name}Api`;
@@ -492,7 +501,7 @@ const buildData = (inSpec: OpenAPIV3.Document, metadata: any) => {
492501
const specModel = isRef(matchingSpecModel) ? resolveRef(spec, matchingSpecModel.$ref) as OpenAPIV3.SchemaObject : matchingSpecModel;
493502

494503
// Add unique imports
495-
(model as any).uniqueImports = _.orderBy(_.uniq(model.imports));
504+
(model as any).uniqueImports = _orderBy(_uniq(model.imports));
496505

497506
// Add deprecated flag if present
498507
(model as any).deprecated = specModel.deprecated || false;
@@ -521,13 +530,13 @@ const buildData = (inSpec: OpenAPIV3.Document, metadata: any) => {
521530
});
522531

523532
// Order models lexicographically by name
524-
data.models = _.orderBy(data.models, d => d.name);
533+
data.models = _orderBy(data.models, d => d.name);
525534

526535
// Order services so default appears first, then otherwise by name
527-
data.services = _.orderBy(data.services, (s => s.name === "Default" ? "" : s.name));
536+
data.services = _orderBy(data.services, (s => s.name === "Default" ? "" : s.name));
528537

529538
// All operations across all services
530-
const allOperations = _.uniqBy(data.services.flatMap(s => s.operations), o => o.name);
539+
const allOperations = _uniqBy(data.services.flatMap(s => s.operations), o => o.name);
531540

532541
return {
533542
...data,

packages/type-safe-api/scripts/type-safe-api/generators/typescript/templates/client/apis/apis.ejs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ import {
3939
<%_ if (operation.parameters.length > 0) { _%>
4040
export interface <%- operation.operationIdPascalCase %>Request {
4141
<%_ operation.parameters.forEach((parameter) => { _%>
42-
<%- parameter.name %><%- parameter.isRequired ? '' : '?' %>: <%- parameter.typescriptType %><%= (parameter.isNullable || parameter.type === "any") ? ' | null' : '' %>;
42+
<%- parameter.typescriptName %><%- parameter.isRequired ? '' : '?' %>: <%- parameter.typescriptType %><%= (parameter.isNullable || parameter.type === "any") ? ' | null' : '' %>;
4343
<%_ }); _%>
4444
}
4545
<%_ } _%>

packages/type-safe-api/scripts/type-safe-api/generators/typescript/templates/client/models/models.ejs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export interface <%= model.name %> {
5555
<%_ } _%>
5656
<%_ model.resolvedProperties.forEach((property) => { _%>
5757
/**
58-
* <%= property.description || '' %>
58+
* <%- property.description || '' %>
5959
* @type {<%- property.typescriptType %>}
6060
* @memberof <%= model.name %>
6161
<%_ if (property.deprecated) { _%>

packages/type-safe-api/scripts/type-safe-api/generators/typescript/templates/server/operationConfig.ejs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -318,9 +318,9 @@ export const <%- operation.name %>Handler = (
318318
try {
319319
requestParameters = {
320320
<%_ operation.parameters.filter(p => p.in !== "body").forEach((parameter) => { _%>
321-
<%_ let isInteger = parameter.isInteger || (parameter.export === "array" && parameter.link && parameter.link.isInteger); _%>
322-
<%_ let isShort = parameter.isShort || (parameter.export === "array" && parameter.link && parameter.link.isShort); _%>
323-
<%_ let isLong = parameter.isLong || (parameter.export === "array" && parameter.link && parameter.link.isLong); _%>
321+
<%_ let isInteger = parameter.isInteger || !!(parameter.export === "array" && parameter.link && parameter.link.isInteger); _%>
322+
<%_ let isShort = parameter.isShort || !!(parameter.export === "array" && parameter.link && parameter.link.isShort); _%>
323+
<%_ let isLong = parameter.isLong || !!(parameter.export === "array" && parameter.link && parameter.link.isLong); _%>
324324
<%- parameter.typescriptName %>: coerceParameter("<%- parameter.prop %>", "<%- parameter.typescriptType %>", <%- isInteger %> || <%- isLong %> || <%- isShort %>, rawSingleValueParameters, rawMultiValueParameters, <%- parameter.isRequired %>) as <%- parameter.typescriptType %><% if (!parameter.isRequired) { %> | undefined<% } %>,
325325
<%_ }); %>
326326
};
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
openapi: 3.0.3
2+
info:
3+
version: 1.0.0
4+
title: Edge Cases
5+
paths:
6+
/reserved-keywords:
7+
get:
8+
operationId: reservedKeywords
9+
parameters:
10+
- in: query
11+
name: with
12+
schema:
13+
type: string
14+
- in: query
15+
name: if
16+
schema:
17+
type: string
18+
- in: query
19+
name: class
20+
schema:
21+
type: string
22+
/array-request-parameters:
23+
get:
24+
operationId: arrayRequestParameters
25+
parameters:
26+
- in: query
27+
name: my-string-array-request-params
28+
schema:
29+
type: array
30+
items:
31+
type: string
32+
- in: query
33+
name: my-enum-array-request-params
34+
schema:
35+
type: array
36+
items:
37+
$ref: "#/components/schemas/MyEnum"
38+
- in: query
39+
name: my-integer-array-request-params
40+
schema:
41+
type: array
42+
items:
43+
type: integer
44+
- in: query
45+
name: my-long-array-request-params
46+
schema:
47+
type: array
48+
items:
49+
type: integer
50+
format: int64
51+
- in: query
52+
name: my-int32-array-request-params
53+
schema:
54+
type: array
55+
items:
56+
type: integer
57+
format: int32
58+
- in: query
59+
name: my-number-array-request-params
60+
schema:
61+
type: array
62+
items:
63+
type: number
64+
- in: query
65+
name: my-float-array-request-params
66+
schema:
67+
type: array
68+
items:
69+
type: number
70+
format: float
71+
- in: query
72+
name: my-double-array-request-params
73+
schema:
74+
type: array
75+
items:
76+
type: number
77+
format: double
78+
components:
79+
schemas:
80+
MyEnum:
81+
type: string
82+
enum:
83+
- one
84+
- two
85+
- three

0 commit comments

Comments
 (0)