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
5 changes: 5 additions & 0 deletions .changeset/dirty-apes-itch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"openapi-typescript": minor
---

Add option to handle parameters with default as non-optional
9 changes: 9 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,3 +234,12 @@ export enum ApiPaths {
This option is useful for generating path params optimistically when the schema has flaky path parameter definitions.
Checks the path for opening and closing brackets and extracts them as path parameters.
Does not override already defined by schema path parameters.

### makeParametersWithDefaultNotUndefined

Enabling `--make-parameters-with-default-not-undefined` will result in generated types where operation parameters that
have default values defined are non-optional.

This allows generating types for certain server implementation use cases where the default value is injected
automatically by the framework for the handling function to consume and the signature on the handler should
indicate that the value can not be undefined.
4 changes: 4 additions & 0 deletions packages/openapi-typescript/bin/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ Options
--root-types-no-schema-prefix (optional)
Do not add "Schema" prefix to types at the root level (should only be used with --root-types)
--make-paths-enum Generate ApiPaths enum for all paths
--make-parameters-with-default-not-undefined
Generate non-optional parameter types if the parameter has a default
`;

const OUTPUT_FILE = "FILE";
Expand Down Expand Up @@ -86,6 +88,7 @@ const flags = parser(args, {
"rootTypesNoSchemaPrefix",
"makePathsEnum",
"generatePathParams",
"makeParametersWithDefaultNotUndefined",
],
string: ["output", "redocly"],
alias: {
Expand Down Expand Up @@ -149,6 +152,7 @@ async function generateSchema(schema, { redocly, silent = false }) {
rootTypesNoSchemaPrefix: flags.rootTypesNoSchemaPrefix,
makePathsEnum: flags.makePathsEnum,
generatePathParams: flags.generatePathParams,
makeParametersWithDefaultNotUndefined: flags.makeParametersWithDefaultNotUndefined,
redocly,
silent,
}),
Expand Down
2 changes: 1 addition & 1 deletion packages/openapi-typescript/examples/simple-example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export interface paths {
path?: never;
cookie?: never;
};
/** Checks if unevaluetedProperties work */
/** Checks if unevaluatedProperties work */
get: operations["getUnevaluatedProperties"];
put?: never;
post?: never;
Expand Down
2 changes: 1 addition & 1 deletion packages/openapi-typescript/examples/simple-example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ paths:
/unevaluated-properties:
get:
operationId: getUnevaluatedProperties
summary: Checks if unevaluetedProperties work
summary: Checks if unevaluatedProperties work
responses:
200:
description: 'OK'
Expand Down
1 change: 1 addition & 0 deletions packages/openapi-typescript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export default async function openapiTS(
resolve($ref) {
return resolveRef(schema, $ref, { silent: options.silent ?? false });
},
makeParametersWithDefaultNotUndefined: options.makeParametersWithDefaultNotUndefined ?? false,
};

const transformT = performance.now();
Expand Down
11 changes: 9 additions & 2 deletions packages/openapi-typescript/src/transform/parameters-array.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import ts from "typescript";
import { addJSDocComment, NEVER, oapiRef, QUESTION_TOKEN, tsModifiers, tsPropertyIndex } from "../lib/ts.js";
import { createRef } from "../lib/utils.js";
import type { ParameterObject, ReferenceObject, TransformNodeOptions } from "../types.js";
import type { ParameterObject, ReferenceObject, SchemaObject, TransformNodeOptions } from "../types.js";
import transformParameterObject from "./parameter-object.js";

// Regex to match path parameters in URL
Expand Down Expand Up @@ -86,8 +86,15 @@ export function transformParametersArray(
if (resolved?.in !== paramIn) {
continue;
}
const resolvedSchema =
resolved.schema && "$ref" in resolved.schema
? options.ctx.resolve<SchemaObject>(resolved.schema.$ref as string)
: resolved.schema;
let optional: ts.QuestionToken | undefined = undefined;
if (paramIn !== "path" && !(resolved as ParameterObject).required) {
const isNonOptional =
(resolved as ParameterObject).required ||
(options.ctx.makeParametersWithDefaultNotUndefined && resolvedSchema?.default !== undefined);
if (paramIn !== "path" && !isNonOptional) {
optional = QUESTION_TOKEN;
}
const subType =
Expand Down
5 changes: 5 additions & 0 deletions packages/openapi-typescript/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,10 @@ export interface OpenAPITSOptions {
makePathsEnum?: boolean;
/** Generate path params based on path even if they are not defiend in the open api schema */
generatePathParams?: boolean;
/** Generate non-optional parameter types if the parameter has a default. This is useful
* for server implementations that automatically set the default from the spec for the user.
*/
makeParametersWithDefaultNotUndefined?: boolean;
}

/** Context passed to all submodules */
Expand Down Expand Up @@ -714,6 +718,7 @@ export interface GlobalContext {
inject?: string;
makePathsEnum: boolean;
generatePathParams: boolean;
makeParametersWithDefaultNotUndefined: boolean;
}

export type $defs = Record<string, SchemaObject>;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
openapi: "3.0"
info:
title: test
version: "1.0"
paths:
/endpoint:
get:
description: OK
parameters:
- in: query
name: optional_nodefault
schema:
type: string
required: false
- in: query
name: optional_default
schema:
type: string
required: false
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ paths:
/unevaluated-properties:
get:
operationId: getUnevaluatedProperties
summary: Checks if unevaluetedProperties work
summary: Checks if unevaluatedProperties work
responses:
200:
description: 'OK'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ paths:
/unevaluated-properties:
get:
operationId: getUnevaluatedProperties
summary: Checks if unevaluetedProperties work
summary: Checks if unevaluatedProperties work
responses:
200:
description: 'OK'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ paths:
/unevaluated-properties:
get:
operationId: getUnevaluatedProperties
summary: Checks if unevaluetedProperties work
summary: Checks if unevaluatedProperties work
responses:
200:
description: 'OK'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ paths:
/unevaluated-properties:
get:
operationId: getUnevaluatedProperties
summary: Checks if unevaluetedProperties work
summary: Checks if unevaluatedProperties work
responses:
200:
description: 'OK'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ paths:
/unevaluated-properties:
get:
operationId: getUnevaluatedProperties
summary: Checks if unevaluetedProperties work
summary: Checks if unevaluatedProperties work
responses:
200:
description: 'OK'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ paths:
/unevaluated-properties:
get:
operationId: getUnevaluatedProperties
summary: Checks if unevaluetedProperties work
summary: Checks if unevaluatedProperties work
responses:
200:
description: 'OK'
Expand Down
4 changes: 2 additions & 2 deletions packages/openapi-typescript/test/node-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,15 @@ export type operations = Record<string, never>;`,
[
"input > string > URL",
{
given: "https://raw.githubusercontent.com/Redocly/redocly-cli/main/__tests__/lint/oas3.1/openapi.yaml",
given: "https://raw.githubusercontent.com/Redocly/redocly-cli/main/tests/e2e/lint/oas3.1/openapi.yaml",
want: new URL("simple-example.ts", EXAMPLES_DIR),
// options: DEFAULT_OPTIONS,
},
],
[
"input > URL > remote",
{
given: new URL("https://raw.githubusercontent.com/Redocly/redocly-cli/main/__tests__/lint/oas3.1/openapi.yaml"),
given: new URL("https://raw.githubusercontent.com/Redocly/redocly-cli/main/tests/e2e/lint/oas3.1/openapi.yaml"),
want: new URL("simple-example.ts", EXAMPLES_DIR),
// options: DEFAULT_OPTIONS,
},
Expand Down
1 change: 1 addition & 0 deletions packages/openapi-typescript/test/test-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export const DEFAULT_CTX: GlobalContext = {
transformProperty: undefined,
makePathsEnum: false,
generatePathParams: false,
makeParametersWithDefaultNotUndefined: false,
};

/** Generic test case */
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { fileURLToPath } from "node:url";
import { astToString } from "../../src/lib/ts.js";
import transformPathsObject from "../../src/transform/paths-object.js";
import type { GlobalContext } from "../../src/types.js";
import { DEFAULT_CTX, type TestCase } from "../test-helpers.js";

const DEFAULT_OPTIONS = DEFAULT_CTX;

describe("parametersWithDefaults", () => {
const tests: TestCase<any, GlobalContext>[] = [
[
"options > default",
{
given: {
"/api/{version}/user/{user_id}": {
parameters: [
{ in: "query", name: "no_default", required: false, schema: { type: "number" } },
{ in: "query", name: "with_default", required: false, schema: { type: "number", default: 1337 } },
{
in: "query",
name: "ref_without_default",
required: false,
schema: { $ref: "#/components/schemas/NoDefault" },
},
{
in: "query",
name: "ref_with_default",
required: false,
schema: { $ref: "#/components/schemas/WithDefault" },
},
],
},
},
want: `{
"/api/{version}/user/{user_id}": {
parameters: {
query?: {
no_default?: number;
with_default?: number;
ref_without_default?: components["schemas"]["NoDefault"];
ref_with_default?: components["schemas"]["WithDefault"];
};
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
}`,
options: {
...DEFAULT_OPTIONS,
},
},
],
[
"options > makeParametersWithDefaultNotUndefined: true",
{
given: {
"/api/{version}/user/{user_id}": {
parameters: [
{ in: "query", name: "no_default", required: false, schema: { type: "number" } },
{ in: "query", name: "with_default", required: false, schema: { type: "number", default: 1337 } },
{
in: "query",
name: "ref_without_default",
required: false,
schema: { $ref: "#/components/schemas/NoDefault" },
},
{
in: "query",
name: "ref_with_default",
required: false,
schema: { $ref: "#/components/schemas/WithDefault" },
},
],
},
},
want: `{
"/api/{version}/user/{user_id}": {
parameters: {
query: {
no_default?: number;
with_default: number;
ref_without_default?: components["schemas"]["NoDefault"];
ref_with_default: components["schemas"]["WithDefault"];
};
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
}`,
options: {
...DEFAULT_OPTIONS,
makeParametersWithDefaultNotUndefined: true,
},
},
],
];

for (const [testName, { given, want, options = DEFAULT_OPTIONS, ci }] of tests) {
test.skipIf(ci?.skipIf)(
testName,
async () => {
const result = astToString(
transformPathsObject(given, {
...options,
resolve($ref) {
switch ($ref) {
case "#/components/schemas/NoDefault": {
return {
type: "number",
};
}
case "#/components/schemas/WithDefault": {
return {
type: "number",
default: 1338,
};
}
default: {
return undefined as any;
}
}
},
}),
);
if (want instanceof URL) {
await expect(result).toMatchFileSnapshot(fileURLToPath(want));
} else {
expect(result).toBe(`${want}\n`);
}
},
ci?.timeout,
);
}
});