Skip to content

Commit

Permalink
Add questionToken option to transform (#1417)
Browse files Browse the repository at this point in the history
  • Loading branch information
JorrinKievit authored Nov 2, 2023
1 parent 6da7888 commit 06c04a0
Show file tree
Hide file tree
Showing 7 changed files with 220 additions and 21 deletions.
48 changes: 46 additions & 2 deletions docs/src/content/docs/node.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ That would result in the following change:

```diff
- updated_at?: string;
+ updated_at?: Date;
+ updated_at: Date | null;
```

#### Example: `Blob` types
Expand Down Expand Up @@ -158,7 +158,51 @@ Resultant diff with correctly-typed `file` property:

```diff
- file?: string;
+ file?: Blob;
+ file: Blob | null;
```

#### Example: Add "?" token to property

It is not possible to create a property with the optional "?" token with the above `transform` functions. The transform function also accepts a different return object, which allows you to add a "?" token to the property. Here's an example schema:

```yaml
Body_file_upload:
type: object;
properties:
file:
type: string;
format: binary;
required: true;
```
Here we return an object with a schema property, which is the same as the above example, but we also add a `questionToken` property, which will add the "?" token to the property.

```ts
import openapiTS from "openapi-typescript";
import ts from "typescript";
const BLOB = ts.factory.createIdentifier("Blob"); // `Blob`
const NULL = ts.factory.createLiteralTypeNode(ts.factory.createNull()); // `null`

const ast = await openapiTS(mySchema, {
transform(schemaObject, metadata) {
if (schemaObject.format === "binary") {
return {
schema: schemaObject.nullable
? ts.factory.createUnionTypeNode([BLOB, NULL])
: BLOB,
questionToken: true,
};
}
},
});
```

Resultant diff with correctly-typed `file` property and "?" token:

```diff
- file: Blob;
+ file?: Blob | null;
```

Any [Schema Object](https://spec.openapis.org/oas/latest.html#schema-object) present in your schema will be run through this formatter (even remote ones!). Also be sure to check the `metadata` parameter for additional context that may be helpful.
Expand Down
23 changes: 21 additions & 2 deletions packages/openapi-typescript/src/transform/components-object.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import ts from "typescript";
import {
NEVER,
QUESTION_TOKEN,
addJSDocComment,
tsModifiers,
tsPropertyIndex,
Expand All @@ -9,6 +10,7 @@ import { createRef, debug, getEntries } from "../lib/utils.js";
import {
ComponentsObject,
GlobalContext,
SchemaObject,
TransformNodeOptions,
} from "../types.js";
import transformHeaderObject from "./header-object.js";
Expand Down Expand Up @@ -51,14 +53,31 @@ export default function transformComponentsObject(
const items: ts.TypeElement[] = [];
if (componentsObject[key]) {
for (const [name, item] of getEntries(componentsObject[key], ctx)) {
const subType = transformers[key](item, {
let subType = transformers[key](item, {
path: createRef(["components", key, name]),
ctx,
});

let hasQuestionToken = false;
if (ctx.transform) {
const result = ctx.transform(item as SchemaObject, {
path: createRef(["components", key, name]),
ctx,
});
if (result) {
if ("schema" in result) {
subType = result.schema;
hasQuestionToken = result.questionToken;
} else {
subType = result;
}
}
}

const property = ts.factory.createPropertySignature(
/* modifiers */ tsModifiers({ readonly: ctx.immutable }),
/* name */ tsPropertyIndex(name),
/* questionToken */ undefined,
/* questionToken */ hasQuestionToken ? QUESTION_TOKEN : undefined,
/* type */ subType,
);
addJSDocComment(item as unknown as any, property); // eslint-disable-line @typescript-eslint/no-explicit-any
Expand Down
27 changes: 15 additions & 12 deletions packages/openapi-typescript/src/transform/schema-object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,16 +87,6 @@ export function transformSchemaObjectWithComposition(
return oapiRef(schemaObject.$ref);
}

/**
* transform()
*/
if (typeof options.ctx.transform === "function") {
const result = options.ctx.transform(schemaObject, options);
if (result !== undefined && result !== null) {
return result;
}
}

/**
* const (valid for any type)
*/
Expand Down Expand Up @@ -461,18 +451,31 @@ function transformSchemaObjectCore(
continue;
}
}
const optional =
let optional =
schemaObject.required?.includes(k) ||
("default" in v && options.ctx.defaultNonNullable)
? undefined
: QUESTION_TOKEN;
const type =
let type =
"$ref" in v
? oapiRef(v.$ref)
: transformSchemaObject(v, {
...options,
path: createRef([options.path ?? "", k]),
});

if (typeof options.ctx.transform === "function") {
const result = options.ctx.transform(v, options);
if (result) {
if ("schema" in result) {
type = result.schema;
optional = result.questionToken ? QUESTION_TOKEN : optional;
} else {
type = result;
}
}
}

const property = ts.factory.createPropertySignature(
/* modifiers */ tsModifiers({
readonly:
Expand Down
7 changes: 6 additions & 1 deletion packages/openapi-typescript/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,11 @@ export type SchemaObject = {
| {}
);

export interface TransformObject {
schema: ts.TypeNode;
questionToken: boolean;
}

export interface StringSubtype {
type: "string" | ["string", "null"];
enum?: (string | ReferenceObject)[];
Expand Down Expand Up @@ -646,7 +651,7 @@ export interface OpenAPITSOptions {
transform?: (
schemaObject: SchemaObject,
options: TransformNodeOptions,
) => ts.TypeNode | undefined;
) => ts.TypeNode | TransformObject | undefined;
/** Modify TypeScript types built from Schema Objects */
postTransform?: (
type: ts.TypeNode,
Expand Down
50 changes: 47 additions & 3 deletions packages/openapi-typescript/test/node-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import { TestCase } from "./test-helpers.js";

const EXAMPLES_DIR = new URL("../examples/", import.meta.url);

const DATE = ts.factory.createTypeReferenceNode(
ts.factory.createIdentifier("Date"),
);

describe("Node.js API", () => {
const tests: TestCase<any, OpenAPITSOptions>[] = [
[
Expand Down Expand Up @@ -382,9 +386,49 @@ export type operations = Record<string, never>;`,
* then use the `typescript` parser and it will tell you the desired
* AST
*/
return ts.factory.createTypeReferenceNode(
ts.factory.createIdentifier("Date"),
);
return DATE;
}
},
},
},
],
[
"options > transform with schema object",
{
given: {
openapi: "3.1",
info: { title: "Test", version: "1.0" },
components: {
schemas: {
Date: { type: "string", format: "date-time" },
},
},
},
want: `export type paths = Record<string, never>;
export type webhooks = Record<string, never>;
export interface components {
schemas: {
/** Format: date-time */
Date?: Date;
};
responses: never;
parameters: never;
requestBodies: never;
headers: never;
pathItems: never;
}
export type $defs = Record<string, never>;
export type operations = Record<string, never>;`,
options: {
transform(schemaObject) {
if (
"format" in schemaObject &&
schemaObject.format === "date-time"
) {
return {
schema: DATE,
questionToken: true,
};
}
},
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { fileURLToPath } from "node:url";
import { astToString } from "../../src/lib/ts.js";
import ts from "typescript";
import { NULL, astToString } from "../../src/lib/ts.js";
import transformComponentsObject from "../../src/transform/components-object.js";
import type { GlobalContext } from "../../src/types.js";
import { DEFAULT_CTX, TestCase } from "../test-helpers.js";

const DEFAULT_OPTIONS = DEFAULT_CTX;

const DATE = ts.factory.createTypeReferenceNode("Date");

describe("transformComponentsObject", () => {
const tests: TestCase<any, GlobalContext>[] = [
[
Expand Down Expand Up @@ -461,6 +464,45 @@ describe("transformComponentsObject", () => {
options: { ...DEFAULT_OPTIONS, excludeDeprecated: true },
},
],
[
"transform > with transform object",
{
given: {
schemas: {
Alpha: {
type: "object",
properties: {
z: { type: "string", format: "date-time" },
},
},
},
},
want: `{
schemas: {
Alpha: {
/** Format: date-time */
z?: Date | null;
};
};
responses: never;
parameters: never;
requestBodies: never;
headers: never;
pathItems: never;
}`,
options: {
...DEFAULT_OPTIONS,
transform(schemaObject) {
if (schemaObject.format === "date-time") {
return {
schema: ts.factory.createUnionTypeNode([DATE, NULL]),
questionToken: true,
};
}
},
},
},
],
];

for (const [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { fileURLToPath } from "node:url";
import ts from "typescript";
import { astToString } from "../../src/lib/ts.js";
import transformRequestBodyObject from "../../src/transform/request-body-object.js";
import { DEFAULT_CTX, TestCase } from "../test-helpers.js";
Expand All @@ -8,6 +9,8 @@ const DEFAULT_OPTIONS = {
ctx: { ...DEFAULT_CTX },
};

const BLOB = ts.factory.createTypeReferenceNode("Blob");

describe("transformRequestBodyObject", () => {
const tests: TestCase[] = [
[
Expand Down Expand Up @@ -50,6 +53,45 @@ describe("transformRequestBodyObject", () => {
// options: DEFAULT_OPTIONS,
},
],
[
"optional blob property with transform",
{
given: {
content: {
"application/json": {
schema: {
type: "object",
properties: {
blob: { type: "string", format: "binary" },
},
},
},
},
},
want: `{
content: {
"application/json": {
/** Format: binary */
blob?: Blob;
};
};
}`,
options: {
...DEFAULT_OPTIONS,
ctx: {
...DEFAULT_CTX,
transform(schemaObject) {
if (schemaObject.format === "binary") {
return {
schema: BLOB,
questionToken: true,
};
}
},
},
},
},
],
];

for (const [
Expand Down

0 comments on commit 06c04a0

Please sign in to comment.