Skip to content

Commit 46e23c0

Browse files
author
Rajat
committed
swagger doc fixes
1 parent 2ef42a7 commit 46e23c0

File tree

10 files changed

+572
-154
lines changed

10 files changed

+572
-154
lines changed

apps/api/AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
## Development Tips
22

33
- Always stick to industry standards and best practices for maintaining the REST API documentation using swagger and openapi.
4-
- Stick to OpenAPI 3.0.0 specification for the API documentation.
4+
- Stick to OpenAPI >=3.0.3 specification for the API documentation.

apps/api/src/index.ts

Lines changed: 16 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,20 @@ app.get(
6969
},
7070
);
7171

72-
app.use("/docs", swaggerUi.serve, swaggerUi.setup(swaggerOutput));
72+
app.use(
73+
"/docs",
74+
swaggerUi.serve,
75+
swaggerUi.setup(swaggerOutput, {
76+
explorer: true,
77+
swaggerOptions: {
78+
persistAuthorization: true,
79+
displayRequestDuration: true,
80+
docExpansion: "none",
81+
defaultModelsExpandDepth: -1,
82+
validatorUrl: null,
83+
},
84+
}),
85+
);
7386

7487
app.use("/settings/media", mediaSettingsRoutes(passport));
7588
app.use("/media/signature", signatureRoutes);
@@ -78,27 +91,7 @@ app.use("/media", mediaRoutes);
7891

7992
app.get(
8093
"/cleanup/temp",
81-
/*
82-
#swagger.tags = ['Cleanup']
83-
#swagger.summary = 'Cleanup expired temp uploads'
84-
#swagger.description = 'Cleanup expired temp uploads'
85-
#swagger.responses[200] = {
86-
description: "OK",
87-
content: {
88-
"application/json": {
89-
schema: {
90-
type: "object",
91-
properties: {
92-
message: {
93-
type: "string",
94-
example: "Expired temp uploads cleaned up",
95-
},
96-
},
97-
},
98-
},
99-
},
100-
}
101-
*/
94+
/* #swagger.ignore = true */
10295
async (req, res) => {
10396
await cleanupExpiredTempUploads();
10497
res.status(200).json({
@@ -108,27 +101,7 @@ app.get(
108101
);
109102
app.get(
110103
"/cleanup/tus",
111-
/*
112-
#swagger.tags = ['Cleanup']
113-
#swagger.summary = 'Cleanup expired tus uploads'
114-
#swagger.description = 'Cleanup expired tus uploads'
115-
#swagger.responses[200] = {
116-
description: "OK",
117-
content: {
118-
"application/json": {
119-
schema: {
120-
type: "object",
121-
properties: {
122-
message: {
123-
type: "string",
124-
example: "Expired tus uploads cleaned up",
125-
},
126-
},
127-
},
128-
},
129-
},
130-
}
131-
*/
104+
/* #swagger.ignore = true */
132105
async (req, res) => {
133106
await cleanupTUSUploads();
134107
res.status(200).json({

apps/api/src/media-settings/routes.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ export default (passport: any) => {
3636
}
3737
}
3838
}
39+
#swagger.responses[400] = { description: 'Bad Request' }
3940
#swagger.responses[401] = { description: 'Unauthorized' }
41+
#swagger.responses[500] = { description: 'Internal Server Error' }
4042
*/
4143
apikey,
4244
updateMediaSettingsHandler,
@@ -58,6 +60,7 @@ export default (passport: any) => {
5860
}
5961
}
6062
#swagger.responses[401] = { description: 'Unauthorized' }
63+
#swagger.responses[500] = { description: 'Internal Server Error' }
6164
*/
6265
apikey,
6366
getMediaSettingsHandler,

apps/api/src/media/handlers.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,9 @@ export async function getMedia(
8080
res: any,
8181
next: (...args: any[]) => void,
8282
) {
83-
const { page, limit, access, group } = req.query;
83+
const filters =
84+
req.body && Object.keys(req.body).length > 0 ? req.body : req.query;
85+
const { page, limit, access, group } = filters;
8486

8587
const { error } = getMediaSchema.validate({ page, limit, access, group });
8688

apps/api/src/media/routes.ts

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ router.post(
2626
/*
2727
#swagger.tags = ['Media']
2828
#swagger.summary = 'Upload Media'
29-
#swagger.description = 'Upload a new media file. Requires an API key in the `x-medialit-apikey` header or a signature in the `x-medialit-signature` header.'
29+
#swagger.description = 'Upload a new media file. Use API key auth from Authorize (`x-medialit-apikey`) or pass `x-medialit-signature` for this endpoint only.'
3030
#swagger.security = [{ "apiKeyAuth": [] }]
3131
#swagger.parameters['x-medialit-signature'] = {
3232
in: 'header',
@@ -35,6 +35,7 @@ router.post(
3535
required: false
3636
}
3737
#swagger.requestBody = {
38+
required: true,
3839
content: {
3940
"multipart/form-data": {
4041
schema: {
@@ -44,7 +45,8 @@ router.post(
4445
caption: { type: "string" },
4546
access: { type: "string", enum: ["public", "private"] },
4647
group: { type: "string" }
47-
}
48+
},
49+
required: ["file"]
4850
}
4951
}
5052
}
@@ -59,6 +61,16 @@ router.post(
5961
}
6062
#swagger.responses[400] = { description: 'Bad Request' }
6163
#swagger.responses[401] = { description: 'Unauthorized' }
64+
#swagger.responses[404] = {
65+
description: 'Invalid signature',
66+
content: {
67+
"application/json": {
68+
schema: { $ref: '#/components/schemas/ErrorResponse' },
69+
example: { error: 'Invalid signature' }
70+
}
71+
}
72+
}
73+
#swagger.responses[500] = { description: 'Internal Server Error' }
6274
*/
6375
cors(),
6476
fileUpload({
@@ -150,6 +162,7 @@ router.post(
150162
}
151163
#swagger.responses[401] = { description: 'Unauthorized' }
152164
#swagger.responses[404] = { description: 'Media not found' }
165+
#swagger.responses[500] = { description: 'Internal Server Error' }
153166
*/
154167
apikey,
155168
getMediaDetails,
@@ -159,12 +172,15 @@ router.post(
159172
/*
160173
#swagger.tags = ['Media']
161174
#swagger.summary = 'List Media'
162-
#swagger.description = 'List all media files. POST is used to support authenticated requests and complex filters.'
175+
#swagger.description = 'List media files filtered by optional query payload.'
163176
#swagger.security = [{ "apiKeyAuth": [] }]
164-
#swagger.parameters['query'] = {
165-
in: 'query',
166-
name: 'filters',
167-
schema: { $ref: '#/components/schemas/GetMediaQuery' }
177+
#swagger.requestBody = {
178+
required: false,
179+
content: {
180+
"application/json": {
181+
schema: { $ref: '#/components/schemas/GetMediaQuery' }
182+
}
183+
}
168184
}
169185
#swagger.responses[200] = {
170186
description: 'List retrieved successfully',
@@ -177,7 +193,9 @@ router.post(
177193
}
178194
}
179195
}
196+
#swagger.responses[400] = { description: 'Bad Request' }
180197
#swagger.responses[401] = { description: 'Unauthorized' }
198+
#swagger.responses[500] = { description: 'Internal Server Error' }
181199
*/
182200
apikey,
183201
getMedia,
@@ -204,6 +222,8 @@ router.post(
204222
}
205223
}
206224
#swagger.responses[401] = { description: 'Unauthorized' }
225+
#swagger.responses[404] = { description: 'Media not found' }
226+
#swagger.responses[500] = { description: 'Internal Server Error' }
207227
*/
208228
apikey,
209229
sealMedia,
@@ -235,7 +255,7 @@ router.delete(
235255
}
236256
}
237257
#swagger.responses[401] = { description: 'Unauthorized' }
238-
#swagger.responses[404] = { description: 'Media not found' }
258+
#swagger.responses[500] = { description: 'Internal Server Error' }
239259
*/
240260
apikey,
241261
deleteMedia,

apps/api/src/signature/handlers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,6 @@ export async function getSignature(
3030
return res.status(200).json({ signature });
3131
} catch (err: any) {
3232
logger.error({ err }, err.message);
33-
return res.status(500).json(err.message);
33+
return res.status(500).json({ error: err.message });
3434
}
3535
}

apps/api/src/signature/routes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ router.post(
2020
}
2121
}
2222
}
23+
#swagger.responses[400] = { description: 'Bad Request' }
2324
#swagger.responses[401] = { description: 'Unauthorized' }
25+
#swagger.responses[500] = { description: 'Internal Server Error' }
2426
*/
2527
apikey,
2628
getSignature,

apps/api/src/swagger-generator.ts

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,20 @@ import {
1717
import { signatureResponseSchema } from "./signature/schemas";
1818

1919
const doc = {
20-
openapi: "3.0.0",
20+
openapi: "3.0.3",
2121
info: {
2222
title: "Medialit API",
2323
description: "Easy file uploads for serverless apps",
2424
version: "0.3.0",
25+
termsOfService: "https://medialit.cloud/p/terms",
26+
contact: {
27+
name: "Medialit Support",
28+
url: "https://medialit.cloud",
29+
},
30+
license: {
31+
name: "AGPL-3.0",
32+
url: "https://www.gnu.org/licenses/agpl-3.0.en.html",
33+
},
2534
},
2635
servers: [
2736
{
@@ -38,6 +47,16 @@ const doc = {
3847
},
3948
},
4049
],
50+
tags: [
51+
{
52+
name: "Media",
53+
description: "Upload, list, and manage media resources.",
54+
},
55+
{
56+
name: "Settings",
57+
description: "Manage media processing configuration.",
58+
},
59+
],
4160
components: {
4261
securitySchemes: {
4362
apiKeyAuth: {
@@ -57,6 +76,28 @@ swaggerAutogen()(outputFile, routes, doc).then(() => {
5776
// Post-process to inject Joi schemas directly (avoiding autogen inference)
5877
const content = JSON.parse(fs.readFileSync(outputFile, "utf8"));
5978

79+
const operationIdByMethodAndPath: Record<string, string> = {
80+
"get /health": "getHealth",
81+
"post /settings/media/create": "updateMediaSettings",
82+
"post /settings/media/get": "getMediaSettings",
83+
"post /media/signature/create": "createUploadSignature",
84+
"post /media/create": "uploadMedia",
85+
"post /media/get/count": "getMediaCount",
86+
"post /media/get/size": "getMediaSize",
87+
"post /media/get/{mediaId}": "getMediaDetails",
88+
"post /media/get": "listMedia",
89+
"post /media/seal/{mediaId}": "sealMedia",
90+
"delete /media/delete/{mediaId}": "deleteMedia",
91+
};
92+
93+
const errorDescriptionByStatus: Record<string, string> = {
94+
"400": "Bad Request",
95+
"401": "Unauthorized",
96+
"404": "Not Found",
97+
"409": "Conflict",
98+
"500": "Internal Server Error",
99+
};
100+
60101
// Inject components.schemas manually
61102
content.components.schemas = {
62103
...content.components.schemas,
@@ -68,8 +109,93 @@ swaggerAutogen()(outputFile, routes, doc).then(() => {
68109
SignatureResponse: joiToSwagger(signatureResponseSchema).swagger,
69110
MediaCountResponse: joiToSwagger(mediaCountResponseSchema).swagger,
70111
MediaSizeResponse: joiToSwagger(mediaSizeResponseSchema).swagger,
112+
ErrorResponse: {
113+
type: "object",
114+
properties: {
115+
error: {
116+
type: "string",
117+
example: "Unauthorized",
118+
},
119+
},
120+
required: ["error"],
121+
additionalProperties: false,
122+
},
71123
};
72124

125+
if (content.paths) {
126+
delete content.paths["/cleanup/temp"];
127+
delete content.paths["/cleanup/tus"];
128+
}
129+
130+
Object.entries(content.paths || {}).forEach(([apiPath, pathItem]: any) => {
131+
Object.entries(pathItem || {}).forEach(([method, operation]: any) => {
132+
if (!operation || typeof operation !== "object") {
133+
return;
134+
}
135+
136+
const operationId =
137+
operationIdByMethodAndPath[`${method} ${apiPath}`];
138+
if (operationId) {
139+
operation.operationId = operationId;
140+
}
141+
142+
if (Array.isArray(operation.parameters)) {
143+
operation.parameters.forEach((parameter: any) => {
144+
if (parameter && typeof parameter === "object") {
145+
delete parameter.type;
146+
}
147+
});
148+
}
149+
150+
if (apiPath === "/media/create" && method === "post") {
151+
operation.security = [{ apiKeyAuth: [] }];
152+
}
153+
154+
if (apiPath === "/health" && method === "get") {
155+
operation.security = [];
156+
}
157+
158+
if (apiPath === "/media/get" && method === "post") {
159+
delete operation.parameters;
160+
operation.requestBody = {
161+
required: false,
162+
content: {
163+
"application/json": {
164+
schema: {
165+
$ref: "#/components/schemas/GetMediaQuery",
166+
},
167+
},
168+
},
169+
};
170+
}
171+
172+
operation.responses = operation.responses || {};
173+
["400", "401", "404", "409", "500"].forEach((statusCode) => {
174+
const existingResponse = operation.responses[statusCode];
175+
if (!existingResponse || existingResponse.$ref) {
176+
return;
177+
}
178+
179+
operation.responses[statusCode] = {
180+
...existingResponse,
181+
description:
182+
existingResponse.description ||
183+
errorDescriptionByStatus[statusCode],
184+
content: existingResponse.content || {
185+
"application/json": {
186+
schema: {
187+
$ref: "#/components/schemas/ErrorResponse",
188+
},
189+
example: {
190+
error: errorDescriptionByStatus[statusCode],
191+
},
192+
},
193+
},
194+
};
195+
});
196+
});
197+
});
198+
73199
if (content.openapi && content.swagger) {
74200
delete content.swagger;
75201
}

0 commit comments

Comments
 (0)