Skip to content

Commit edeb4d5

Browse files
committed
Merge remote-tracking branch 'origin/feat/clear-unused-schemas'
2 parents f6a7597 + 557d077 commit edeb4d5

File tree

4 files changed

+307
-4
lines changed

4 files changed

+307
-4
lines changed
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
import { describe, expect, it } from "@jest/globals";
2+
import clearUnusedSchemas from "./clearUnusedSchemas";
3+
4+
describe("clearUnusedSchemas", () => {
5+
it("should return the same input if there are no schemas", () => {
6+
const output = clearUnusedSchemas({ paths: {}, components: {} });
7+
expect(output).toStrictEqual({ paths: {}, components: {} });
8+
});
9+
10+
it("should delete if a component was never used in any api routes", () => {
11+
const output = clearUnusedSchemas({
12+
paths: {
13+
"/user": {
14+
get: {
15+
summary: "Get user",
16+
responses: {
17+
200: {
18+
description: "Success",
19+
content: {
20+
"application/json": {
21+
schema: {
22+
$ref: "#/components/schemas/User",
23+
},
24+
},
25+
},
26+
},
27+
},
28+
},
29+
},
30+
},
31+
components: {
32+
schemas: {
33+
User: {
34+
type: "object",
35+
properties: {
36+
id: { type: "string" },
37+
name: { type: "string" },
38+
},
39+
},
40+
Post: {
41+
type: "object",
42+
properties: {
43+
id: { type: "string" },
44+
title: { type: "string" },
45+
},
46+
},
47+
},
48+
},
49+
});
50+
expect(output).toStrictEqual({
51+
paths: {
52+
"/user": {
53+
get: {
54+
summary: "Get user",
55+
responses: {
56+
200: {
57+
description: "Success",
58+
content: {
59+
"application/json": {
60+
schema: {
61+
$ref: "#/components/schemas/User",
62+
},
63+
},
64+
},
65+
},
66+
},
67+
},
68+
},
69+
},
70+
components: {
71+
schemas: {
72+
User: {
73+
type: "object",
74+
properties: {
75+
id: { type: "string" },
76+
name: { type: "string" },
77+
},
78+
},
79+
},
80+
},
81+
});
82+
});
83+
84+
it("should not mix the component names", () => {
85+
const output = clearUnusedSchemas({
86+
paths: {
87+
"/user/post": {
88+
get: {
89+
summary: "Get user post",
90+
responses: {
91+
200: {
92+
description: "Success",
93+
content: {
94+
"application/json": {
95+
schema: {
96+
$ref: "#/components/schemas/UserPost",
97+
},
98+
},
99+
},
100+
},
101+
},
102+
},
103+
},
104+
},
105+
components: {
106+
schemas: {
107+
User: {
108+
type: "object",
109+
properties: {
110+
id: { type: "string" },
111+
name: { type: "string" },
112+
},
113+
},
114+
UserPost: {
115+
type: "object",
116+
properties: {
117+
id: { type: "string" },
118+
title: { type: "string" },
119+
},
120+
},
121+
},
122+
},
123+
});
124+
expect(output).toStrictEqual({
125+
paths: {
126+
"/user/post": {
127+
get: {
128+
summary: "Get user post",
129+
responses: {
130+
200: {
131+
description: "Success",
132+
content: {
133+
"application/json": {
134+
schema: {
135+
$ref: "#/components/schemas/UserPost",
136+
},
137+
},
138+
},
139+
},
140+
},
141+
},
142+
},
143+
},
144+
components: {
145+
schemas: {
146+
UserPost: {
147+
type: "object",
148+
properties: {
149+
id: { type: "string" },
150+
title: { type: "string" },
151+
},
152+
},
153+
},
154+
},
155+
});
156+
});
157+
158+
it("should keep the schema if it was referenced by another schema", () => {
159+
const output = clearUnusedSchemas({
160+
paths: {
161+
"/user": {
162+
get: {
163+
summary: "Get user",
164+
responses: {
165+
200: {
166+
description: "Success",
167+
content: {
168+
"application/json": {
169+
schema: {
170+
$ref: "#/components/schemas/User",
171+
},
172+
},
173+
},
174+
},
175+
},
176+
},
177+
},
178+
},
179+
components: {
180+
schemas: {
181+
User: {
182+
type: "object",
183+
properties: {
184+
id: { type: "string" },
185+
name: { type: "string" },
186+
cars: {
187+
type: "array",
188+
items: {
189+
$ref: "#/components/schemas/Car",
190+
},
191+
},
192+
},
193+
},
194+
Post: {
195+
type: "object",
196+
properties: {
197+
id: { type: "string" },
198+
title: { type: "string" },
199+
},
200+
},
201+
Car: {
202+
type: "object",
203+
properties: {
204+
id: { type: "string" },
205+
model: { type: "string" },
206+
},
207+
},
208+
},
209+
},
210+
});
211+
expect(output).toStrictEqual({
212+
paths: {
213+
"/user": {
214+
get: {
215+
summary: "Get user",
216+
responses: {
217+
200: {
218+
description: "Success",
219+
content: {
220+
"application/json": {
221+
schema: {
222+
$ref: "#/components/schemas/User",
223+
},
224+
},
225+
},
226+
},
227+
},
228+
},
229+
},
230+
},
231+
components: {
232+
schemas: {
233+
User: {
234+
type: "object",
235+
properties: {
236+
id: { type: "string" },
237+
name: { type: "string" },
238+
cars: {
239+
type: "array",
240+
items: {
241+
$ref: "#/components/schemas/Car",
242+
},
243+
},
244+
},
245+
},
246+
Car: {
247+
type: "object",
248+
properties: {
249+
id: { type: "string" },
250+
model: { type: "string" },
251+
},
252+
},
253+
},
254+
},
255+
});
256+
});
257+
});

src/core/clearUnusedSchemas.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { omit } from "../utils/object";
2+
import type { OpenApiDocument } from "@omer-x/openapi-types";
3+
4+
function countReferences(schemaName: string, source: string) {
5+
return (source.match(new RegExp(`"#/components/schemas/${schemaName}"`, "g")) ?? []).length;
6+
}
7+
8+
export default function clearUnusedSchemas({
9+
paths,
10+
components,
11+
}: Required<Pick<OpenApiDocument, "paths" | "components">>) {
12+
if (!components.schemas) return { paths, components };
13+
const stringifiedPaths = JSON.stringify(paths);
14+
const stringifiedSchemas = Object.fromEntries(Object.entries(components.schemas).map(([schemaName, schema]) => {
15+
return [schemaName, JSON.stringify(schema)];
16+
}));
17+
return {
18+
paths,
19+
components: {
20+
...components,
21+
schemas: Object.fromEntries(Object.entries(components.schemas).filter(([schemaName]) => {
22+
const otherSchemas = omit(stringifiedSchemas, schemaName);
23+
return (
24+
countReferences(schemaName, stringifiedPaths) > 0 ||
25+
countReferences(schemaName, Object.values(otherSchemas).join("")) > 0
26+
);
27+
})),
28+
},
29+
};
30+
}

src/core/generateOpenApiSpec.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import getPackageMetadata from "@omer-x/package-metadata";
2+
import clearUnusedSchemas from "./clearUnusedSchemas";
23
import { filterDirectoryItems, getDirectoryItems } from "./dir";
34
import isDocumentedRoute from "./isDocumentedRoute";
45
import { findAppFolderPath, getRouteExports } from "./next";
@@ -41,16 +42,20 @@ export default async function generateOpenApiSpec(schemas: Record<string, ZodTyp
4142
}
4243
const metadata = getPackageMetadata();
4344

45+
const pathsAndComponents = {
46+
paths: bundlePaths(validRoutes, schemas),
47+
components: {
48+
schemas: bundleSchemas(schemas),
49+
},
50+
};
51+
4452
return {
4553
openapi: "3.1.0",
4654
info: {
4755
title: metadata.serviceName,
4856
version: metadata.version,
4957
},
50-
paths: bundlePaths(validRoutes, schemas),
51-
components: {
52-
schemas: bundleSchemas(schemas),
53-
},
58+
...clearUnusedSchemas(pathsAndComponents),
5459
tags: [],
5560
} as Omit<OpenApiDocument, "components"> & Required<Pick<OpenApiDocument, "components">>;
5661
}

src/utils/object.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/* export function pick<T extends object, K extends keyof T>(object: T, ...keys: K[]) {
2+
return Object.fromEntries(Object.entries(object).filter(([key]) => keys.includes(key as K))) as Pick<T, K>;
3+
} */
4+
5+
export function omit<T extends object, K extends keyof T>(object: T, ...keys: K[]) {
6+
return Object.fromEntries(Object.entries(object).filter(([key]) => !keys.includes(key as K))) as Omit<T, K>;
7+
}
8+
9+
/* export function pluck<T, K extends keyof T>(collection: T[], key: K) {
10+
return collection.map(item => item[key]) as T[K][];
11+
} */

0 commit comments

Comments
 (0)