Skip to content

Commit 5fc7342

Browse files
committed
feat(metadata): merge properly operations together
1 parent 1b0095a commit 5fc7342

File tree

12 files changed

+323
-16
lines changed

12 files changed

+323
-16
lines changed

packages/openapi-metadata/src/generators/operation-response.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ export async function generateOperationResponse(
77
context: Context,
88
metadata: OperationResponseMetadata,
99
): Promise<OpenAPIV3.ResponseObject> {
10-
const { type, schema: s, enum: e, ...response } = metadata as any;
10+
const { type, schema: s, enum: e, mediaType, status, ...response } = metadata;
1111

1212
return {
1313
description: "",
1414
...response,
1515
content: {
16-
[metadata.mediaType]: {
16+
[mediaType]: {
1717
schema: await loadType(context, metadata),
1818
},
1919
},

packages/openapi-metadata/src/generators/operation.ts

+4-9
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ import { generateOperationParameters } from "./operation-parameters.js";
88
import { OperationResponseMetadataStorage } from "../metadata/operation-response.js";
99
import { generateOperationResponse } from "./operation-response.js";
1010
import { OperationSecurityMetadataStorage } from "../metadata/operation-security.js";
11-
import { ExtraModelsMetadataStorage } from "../metadata/extra-models.js";
12-
import { loadType } from "../loaders/type.js";
1311

1412
export async function generateOperation(
1513
context: Context,
@@ -21,29 +19,26 @@ export async function generateOperation(
2119

2220
const target = controller.prototype;
2321

24-
const extraModels = ExtraModelsMetadataStorage.getMetadata(target);
25-
26-
await Promise.all(extraModels.map((m) => loadType(context, { type: m })));
27-
2822
const body = OperationBodyMetadataStorage.getMetadata(target, propertyKey);
2923
if (body) {
3024
operation.requestBody = await generateOperationBody(context, body);
3125
}
3226

33-
const parameters = OperationParameterMetadataStorage.getMetadata(target, propertyKey);
27+
const parameters = OperationParameterMetadataStorage.getMetadata(target, propertyKey, true);
3428
operation.parameters = [];
3529
for (const parameter of parameters) {
3630
operation.parameters.push(await generateOperationParameters(context, parameter));
3731
}
3832

39-
const responses = OperationResponseMetadataStorage.getMetadata(target, propertyKey);
33+
const responses = OperationResponseMetadataStorage.getMetadata(target, propertyKey, true);
4034
for (const [status, response] of Object.entries(responses)) {
4135
operation.responses[status] = await generateOperationResponse(context, response);
4236
}
4337

4438
const security = OperationSecurityMetadataStorage.getMetadata(target, propertyKey, true);
4539

46-
operation.security = [security];
40+
// TODO: Check what the difference between `[{ auth1: {} }, {auth2: {} }]` and `[{ auth1: {}, auth2: {}}]`
41+
operation.security = Object.keys(security).length > 0 ? [security] : [];
4742

4843
return operation;
4944
}

packages/openapi-metadata/src/generators/paths.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
import type { OpenAPIV3 } from "openapi-types";
22
import type { Context } from "../context.js";
33
import { generateOperation } from "./operation.js";
4-
import { ExcludeMetadataStorage, OperationMetadataStorage } from "../metadata/index.js";
4+
import { ExcludeMetadataStorage, ExtraModelsMetadataStorage, OperationMetadataStorage } from "../metadata/index.js";
5+
import { loadType } from "../loaders/type.js";
56

67
export async function generatePaths(context: Context, controllers: Function[]): Promise<OpenAPIV3.PathsObject> {
78
const paths: OpenAPIV3.PathsObject = {};
89

910
for (const controller of controllers) {
1011
const target = controller.prototype;
1112
const keys = Object.getOwnPropertyNames(target);
13+
14+
// Loads extra models defined on this controller
15+
const extraModels = ExtraModelsMetadataStorage.getMetadata(target);
16+
await Promise.all(extraModels.map((m) => loadType(context, { type: m })));
17+
1218
for (const key of keys) {
13-
const metadata = OperationMetadataStorage.getMetadata(target, key);
19+
const metadata = OperationMetadataStorage.getMetadata(target, key, true);
1420
if (!metadata) {
1521
continue;
1622
}

packages/openapi-metadata/src/loaders/type.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ export async function loadType(
137137
const thunk = isThunk(options.type);
138138
const value = thunk ? (options.type as Function)(context) : options.type;
139139

140-
for (const loader of [PrimitiveTypeLoader, ...context.typeLoaders, ClassTypeLoader]) {
140+
for (const loader of [PrimitiveTypeLoader, ArrayTypeLoader, ...context.typeLoaders, ClassTypeLoader]) {
141141
const result = await loader(context, value, options.type);
142142
if (result) {
143143
return result;

packages/openapi-metadata/src/metadata/operation-security.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export type OperationSecurityMetadata = OpenAPIV3.SecurityRequirementObject;
55

66
export const OperationSecurityMetadataKey = Symbol("OperationSecurity");
77

8-
export const OperationSecurityMetadataStorage = createMetadataStorage<OpenAPIV3.SecurityRequirementObject>(
8+
export const OperationSecurityMetadataStorage = createMetadataStorage<OperationSecurityMetadata>(
99
OperationSecurityMetadataKey,
1010
{},
1111
);

packages/openapi-metadata/test/decorators.test.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -134,12 +134,13 @@ test("@ApiTags", () => {
134134
@ApiTags("Root")
135135
class MyController {
136136
@ApiTags("Hello", "World")
137+
@ApiTags("Foo", "Bar")
137138
operation() {}
138139
}
139140

140141
const metadata = OperationMetadataStorage.getMetadata(MyController.prototype, "operation", true);
141142

142-
expect(metadata.tags).toEqual(["Root", "Hello", "World"]);
143+
expect(metadata.tags).toEqual(["Root", "Foo", "Bar", "Hello", "World"]);
143144
});
144145

145146
test("@ApiSecurity", () => {
+165
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import "reflect-metadata";
2+
import { generateDocument } from "../src/index.js";
3+
import UsersController from "./fixtures/controllers/users_controller.js";
4+
import type { OpenAPIV3 } from "openapi-types";
5+
6+
test("simple schema", async () => {
7+
const document = await generateDocument({
8+
controllers: [UsersController],
9+
document: {
10+
info: {
11+
title: "Test API",
12+
version: "1.0.0",
13+
},
14+
},
15+
});
16+
17+
expect(document).toEqual({
18+
openapi: "3.0.0",
19+
info: {
20+
title: "Test API",
21+
version: "1.0.0",
22+
},
23+
paths: {
24+
"/users": {
25+
get: {
26+
summary: "List users",
27+
parameters: [
28+
{
29+
in: "query",
30+
name: "page",
31+
schema: {
32+
type: "string",
33+
},
34+
},
35+
],
36+
responses: {
37+
default: {
38+
description: "",
39+
content: {
40+
"application/json": {
41+
schema: {
42+
type: "array",
43+
items: {
44+
$ref: "#/components/schemas/User",
45+
},
46+
},
47+
},
48+
},
49+
},
50+
"404": {
51+
description: "Not found",
52+
content: {
53+
"application/json": {
54+
schema: {
55+
type: "object",
56+
},
57+
},
58+
},
59+
},
60+
},
61+
security: [],
62+
tags: ["Users", "List"],
63+
},
64+
post: {
65+
summary: "Create user",
66+
parameters: [],
67+
requestBody: {
68+
content: {
69+
"application/json": {
70+
schema: {
71+
$ref: "#/components/schemas/User",
72+
},
73+
},
74+
},
75+
},
76+
responses: {
77+
default: {
78+
description: "",
79+
content: {
80+
"application/json": {
81+
schema: {
82+
$ref: "#/components/schemas/User",
83+
},
84+
},
85+
},
86+
},
87+
"404": {
88+
description: "Not found",
89+
content: {
90+
"application/json": {
91+
schema: {
92+
type: "object",
93+
},
94+
},
95+
},
96+
},
97+
},
98+
security: [],
99+
tags: ["Users", "Create"],
100+
},
101+
},
102+
"/users/{id}": {
103+
get: {
104+
summary: "Show user",
105+
parameters: [],
106+
responses: {
107+
default: {
108+
description: "",
109+
content: {
110+
"application/json": {
111+
schema: {
112+
$ref: "#/components/schemas/User",
113+
},
114+
},
115+
},
116+
},
117+
"404": {
118+
description: "Not found",
119+
content: {
120+
"application/json": {
121+
schema: {
122+
type: "object",
123+
},
124+
},
125+
},
126+
},
127+
},
128+
security: [],
129+
tags: ["Users", "Show"],
130+
},
131+
},
132+
},
133+
components: {
134+
schemas: {
135+
User: {
136+
type: "object",
137+
properties: {
138+
id: {
139+
type: "number",
140+
},
141+
posts: {
142+
type: "array",
143+
items: {
144+
$ref: "#/components/schemas/Post",
145+
},
146+
},
147+
},
148+
required: ["id", "posts"],
149+
},
150+
Post: {
151+
type: "object",
152+
properties: {
153+
id: {
154+
type: "number",
155+
},
156+
author: {
157+
$ref: "#/components/schemas/User",
158+
},
159+
},
160+
required: ["id", "author"],
161+
},
162+
},
163+
},
164+
} satisfies OpenAPIV3.Document);
165+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { ApiBody } from "../../../src/decorators/api-body.js";
2+
import { ApiOperation } from "../../../src/decorators/api-operation.js";
3+
import { ApiQuery } from "../../../src/decorators/api-query.js";
4+
import { ApiResponse } from "../../../src/decorators/api-response.js";
5+
import { ApiTags } from "../../../src/decorators/api-tags.js";
6+
import User from "../schemas/user.js";
7+
8+
@ApiTags("Users")
9+
@ApiResponse({
10+
status: 404,
11+
description: "Not found",
12+
type: "object",
13+
})
14+
export default class UsersController {
15+
@ApiTags("List")
16+
@ApiOperation({
17+
summary: "List users",
18+
methods: ["get"],
19+
path: "/users",
20+
})
21+
@ApiResponse({
22+
type: [User],
23+
})
24+
@ApiQuery({ name: "page" })
25+
index() {}
26+
27+
@ApiTags("Show")
28+
@ApiOperation({
29+
summary: "Show user",
30+
methods: ["get"],
31+
path: "/users/{id}",
32+
})
33+
@ApiResponse({
34+
type: User,
35+
})
36+
show() {}
37+
38+
@ApiTags("Create")
39+
@ApiOperation({
40+
summary: "Create user",
41+
methods: ["post"],
42+
path: "/users",
43+
})
44+
@ApiBody({
45+
type: User,
46+
})
47+
@ApiResponse({
48+
type: User,
49+
})
50+
create() {}
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { ApiProperty } from "../../../src/decorators/api-property.js";
2+
import User from "./user.js";
3+
4+
export default class Post {
5+
@ApiProperty()
6+
declare id: number;
7+
8+
@ApiProperty({ type: () => User })
9+
declare author: User;
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { ApiProperty } from "../../../src/decorators/api-property.js";
2+
import Post from "./post.js";
3+
4+
export default class User {
5+
@ApiProperty()
6+
declare id: number;
7+
8+
@ApiProperty({ type: () => [Post] })
9+
declare posts: Post[];
10+
}

packages/openapi-metadata/test/loaders/array.test.ts

+16
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import "reflect-metadata";
12
import type { Context } from "../../src/context.js";
3+
import { ApiProperty } from "../../src/decorators/api-property.js";
24
import { ArrayTypeLoader } from "../../src/loaders/type.js";
35

46
let error: string | undefined = undefined;
@@ -21,6 +23,20 @@ test("simple array", async () => {
2123
});
2224
});
2325

26+
test("model", async () => {
27+
class User {
28+
@ApiProperty()
29+
declare id: string;
30+
}
31+
32+
expect(await ArrayTypeLoader(context, [User])).toEqual({
33+
type: "array",
34+
items: {
35+
$ref: "#/components/schemas/User",
36+
},
37+
});
38+
});
39+
2440
test("empty array should warn", async () => {
2541
// @ts-expect-error
2642
expect(await ArrayTypeLoader(context, [])).toEqual(undefined);

0 commit comments

Comments
 (0)