Skip to content

Commit 7e8ac67

Browse files
authored
Merge pull request #179 from codelitdev/issue-178
OpenAPI documentation
2 parents 0165ba0 + b2e74f6 commit 7e8ac67

File tree

23 files changed

+1936
-129
lines changed

23 files changed

+1936
-129
lines changed

AGENTS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Dev Environment Tips
2+
3+
- Use `pnpm` as the package manager.
4+
- This is a monorepo, so use `pnpm --filter <package-name> <command>` to run commands in specific packages.

apps/api/AGENTS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
## Development Tips
2+
3+
- Always stick to industry standards and best practices for maintaining the REST API documentation using swagger and openapi.
4+
- Stick to OpenAPI >=3.0.3 specification for the API documentation.

apps/api/__tests__/media/handlers.test.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,17 @@ describe("Media handlers", () => {
9494
);
9595

9696
mock.method(mediaService, "getMediaDetails").mock.mockImplementation(
97-
async () => ({ id: "test-media-id" }),
97+
async () => ({
98+
mediaId: "test-media-id",
99+
originalFileName: "test.jpg",
100+
mimeType: "image/jpeg",
101+
size: 1024,
102+
access: "private",
103+
file: "http://example.com/file.jpg",
104+
thumbnail: "http://example.com/thumb.jpg",
105+
caption: "test caption",
106+
group: "default",
107+
}),
98108
);
99109

100110
const response = await uploadMedia(req, res, () => {});

apps/api/package.json

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@
2525
"url": "https://github.com/codelitdev/medialit/issues"
2626
},
2727
"scripts": {
28-
"build": "tsc",
29-
"dev": "nodemon src/index.ts",
28+
"build": "pnpm run swagger:generate && tsc",
29+
"swagger:generate": "node --import tsx src/swagger-generator.ts",
30+
"dev": "pnpm run swagger:generate && nodemon --exec 'node --env-file=.env --import tsx' src/index.ts",
3031
"start": "node dist/src/index.js",
31-
"test": "node --import tsx --test '**/*.test.ts'"
32+
"test": "node --import tsx --test '**/*.test.ts'",
33+
"test:fuzz": "uvx schemathesis run http://127.0.0.1:8000/openapi.json"
3234
},
3335
"dependencies": {
3436
"@aws-sdk/client-s3": "^3.922.0",
@@ -46,26 +48,29 @@
4648
"express": "^4.2.0",
4749
"express-fileupload": "^1.5.2",
4850
"joi": "^17.6.0",
51+
"joi-to-swagger": "^6.2.0",
4952
"mongoose": "^8.19.3",
5053
"passport": "^0.7.0",
5154
"passport-jwt": "^4.0.1",
52-
"pino": "^10.1.0"
55+
"pino": "^10.1.0",
56+
"swagger-ui-express": "^5.0.1"
5357
},
5458
"devDependencies": {
5559
"@types/cors": "^2.8.12",
5660
"@types/express": "^4.17.20",
5761
"@types/express-fileupload": "^1.2.2",
62+
"@types/joi": "^17.2.3",
5863
"@types/mongoose": "^5.11.97",
5964
"@types/node": "^22.14.1",
6065
"@types/passport": "^1.0.7",
6166
"@types/passport-jwt": "^3.0.6",
67+
"@types/swagger-ui-express": "^4.1.8",
6268
"@typescript-eslint/eslint-plugin": "^5.17.0",
6369
"@typescript-eslint/parser": "^5.17.0",
6470
"eslint": "^8.12.0",
6571
"nodemon": "^3.1.10",
66-
"ts-node": "^10.9.2",
67-
"tsconfig-paths": "^4.2.0",
72+
"swagger-autogen": "^2.23.7",
6873
"tsx": "^4.20.6",
6974
"typescript": "^5.9.3"
7075
}
71-
}
76+
}

apps/api/src/apikey/middleware.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export default async function apikey(
1313

1414
if (!reqKey) {
1515
logger.error({}, "API key is missing");
16-
return res.status(400).json({ error: BAD_REQUEST });
16+
return res.status(401).json({ error: UNAUTHORISED });
1717
}
1818

1919
const apiKey: Apikey | null = await getApiKeyUsingKeyId(reqKey);

apps/api/src/index.ts

Lines changed: 79 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ import logger from "./services/log";
1212
import { createUser, findByEmail } from "./user/queries";
1313
import { Apikey, User } from "@medialit/models";
1414
import { createApiKey } from "./apikey/queries";
15+
import swaggerUi from "swagger-ui-express";
16+
import swaggerOutput from "./swagger_output.json";
17+
1518
import { spawn } from "child_process";
1619
import { cleanupTUSUploads } from "./tus/cleanup";
1720
import { cleanupExpiredTempUploads } from "./media/cleanup";
@@ -24,30 +27,88 @@ app.set("trust proxy", process.env.ENABLE_TRUST_PROXY === "true");
2427

2528
app.use(express.json());
2629

27-
app.get("/health", (req, res) => {
28-
res.status(200).json({
29-
status: "ok",
30-
uptime: process.uptime(),
31-
});
32-
});
30+
app.get(
31+
"/health",
32+
/*
33+
#swagger.summary = 'Status of the server',
34+
#swagger.description = 'Returns the status of the server and uptime'
35+
#swagger.responses[200] = {
36+
description: "OK",
37+
content: {
38+
"application/json": {
39+
schema: {
40+
type: "object",
41+
properties: {
42+
status: {
43+
type: "string",
44+
example: "ok",
45+
},
46+
uptime: {
47+
type: "number",
48+
example: 12.345,
49+
},
50+
},
51+
},
52+
},
53+
},
54+
}
55+
*/
56+
(req, res) => {
57+
res.status(200).json({
58+
status: "ok",
59+
uptime: process.uptime(),
60+
});
61+
},
62+
);
63+
64+
app.get(
65+
"/openapi.json",
66+
/* #swagger.ignore = true */
67+
(req, res) => {
68+
res.json(swaggerOutput);
69+
},
70+
);
71+
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+
);
3386

3487
app.use("/settings/media", mediaSettingsRoutes(passport));
3588
app.use("/media/signature", signatureRoutes);
3689
app.use("/media", tusRoutes);
3790
app.use("/media", mediaRoutes);
3891

39-
app.get("/cleanup/temp", async (req, res) => {
40-
await cleanupExpiredTempUploads();
41-
res.status(200).json({
42-
message: "Expired temp uploads cleaned up",
43-
});
44-
});
45-
app.get("/cleanup/tus", async (req, res) => {
46-
await cleanupTUSUploads();
47-
res.status(200).json({
48-
message: "Expired tus uploads cleaned up",
49-
});
50-
});
92+
app.get(
93+
"/cleanup/temp",
94+
/* #swagger.ignore = true */
95+
async (req, res) => {
96+
await cleanupExpiredTempUploads();
97+
res.status(200).json({
98+
message: "Expired temp uploads cleaned up",
99+
});
100+
},
101+
);
102+
app.get(
103+
"/cleanup/tus",
104+
/* #swagger.ignore = true */
105+
async (req, res) => {
106+
await cleanupTUSUploads();
107+
res.status(200).json({
108+
message: "Expired tus uploads cleaned up",
109+
});
110+
},
111+
);
51112

52113
const port = process.env.PORT || 80;
53114

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

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,15 @@
1-
import Joi from "joi";
21
import { SUCCESS } from "../config/strings";
32
import logger from "../services/log";
43
import { updateMediaSettings } from "./queries";
54
import * as mediaSettingsService from "./service";
65

6+
import { mediaSettingsSchema } from "./schemas";
7+
78
export async function updateMediaSettingsHandler(
89
req: any,
910
res: any,
1011
next: (...args: any[]) => void,
1112
) {
12-
const mediaSettingsSchema = Joi.object({
13-
useWebP: Joi.boolean(),
14-
webpOutputQuality: Joi.number().min(0).max(100),
15-
thumbnailWidth: Joi.number().positive(),
16-
thumbnailHeight: Joi.number().positive(),
17-
});
18-
1913
const { useWebP, webpOutputQuality, thumbnailWidth, thumbnailHeight } =
2014
req.body;
2115

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

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,63 @@ import apikey from "../apikey/middleware";
88
export default (passport: any) => {
99
const router = express.Router();
1010

11-
router.post("/create", apikey, updateMediaSettingsHandler);
11+
router.post(
12+
"/create",
13+
/*
14+
#swagger.tags = ['Settings']
15+
#swagger.summary = 'Update Media Settings'
16+
#swagger.description = 'Update configuration for media processing.'
17+
#swagger.security = [{ "apiKeyAuth": [] }]
18+
#swagger.requestBody = {
19+
required: true,
20+
content: {
21+
"application/json": {
22+
schema: { $ref: "#/components/schemas/MediaSettingsPayload" }
23+
}
24+
}
25+
}
26+
#swagger.responses[200] = {
27+
description: 'Settings updated successfully',
28+
content: {
29+
"application/json": {
30+
schema: {
31+
type: "object",
32+
properties: {
33+
message: { type: "string", example: "success" }
34+
}
35+
}
36+
}
37+
}
38+
}
39+
#swagger.responses[400] = { description: 'Bad Request' }
40+
#swagger.responses[401] = { description: 'Unauthorized' }
41+
#swagger.responses[500] = { description: 'Internal Server Error' }
42+
*/
43+
apikey,
44+
updateMediaSettingsHandler,
45+
);
1246

13-
router.post("/get", apikey, getMediaSettingsHandler);
47+
router.post(
48+
"/get",
49+
/*
50+
#swagger.tags = ['Settings']
51+
#swagger.summary = 'Get Media Settings'
52+
#swagger.description = 'Retrieve current media processing configuration.'
53+
#swagger.security = [{ "apiKeyAuth": [] }]
54+
#swagger.responses[200] = {
55+
description: 'Settings retrieved successfully',
56+
content: {
57+
"application/json": {
58+
schema: { $ref: '#/components/schemas/MediaSettings' }
59+
}
60+
}
61+
}
62+
#swagger.responses[401] = { description: 'Unauthorized' }
63+
#swagger.responses[500] = { description: 'Internal Server Error' }
64+
*/
65+
apikey,
66+
getMediaSettingsHandler,
67+
);
1468

1569
return router;
1670
};
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import Joi from "joi";
2+
3+
export interface MediaSettingsResponse {
4+
useWebP: boolean;
5+
webpOutputQuality: number;
6+
thumbnailHeight: number;
7+
thumbnailWidth: number;
8+
}
9+
10+
export const mediaSettingsSchema = Joi.object<MediaSettingsResponse>({
11+
useWebP: Joi.boolean(),
12+
webpOutputQuality: Joi.number().min(0).max(100),
13+
thumbnailWidth: Joi.number().positive(),
14+
thumbnailHeight: Joi.number().positive(),
15+
});
16+
17+
export const mediaSettingsResponseSchema = mediaSettingsSchema;

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

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,31 @@
1-
import { MediaSettings } from "./model";
21
import * as queries from "./queries";
2+
import { MediaSettingsResponse } from "./schemas";
3+
import {
4+
thumbnailHeight as defaultThumbnailHeight,
5+
thumbnailWidth as defaultThumbnailWidth,
6+
} from "../config/constants";
37

48
export async function getMediaSettings(
59
userId: string,
610
apikey: string,
7-
): Promise<Omit<MediaSettings, "userId" | "apikey"> | null> {
11+
): Promise<MediaSettingsResponse> {
812
const mediaSettings = await queries.getMediaSettings(userId, apikey);
913

1014
if (!mediaSettings) {
11-
return {};
15+
return {
16+
useWebP: false,
17+
webpOutputQuality: 0,
18+
thumbnailHeight: defaultThumbnailHeight,
19+
thumbnailWidth: defaultThumbnailWidth,
20+
};
1221
}
1322

1423
return {
15-
useWebP: mediaSettings.useWebP,
16-
webpOutputQuality: mediaSettings.webpOutputQuality,
17-
thumbnailHeight: mediaSettings.thumbnailHeight,
18-
thumbnailWidth: mediaSettings.thumbnailWidth,
24+
useWebP: mediaSettings.useWebP || false,
25+
webpOutputQuality: mediaSettings.webpOutputQuality || 0,
26+
thumbnailHeight:
27+
mediaSettings.thumbnailHeight || defaultThumbnailHeight,
28+
thumbnailWidth: mediaSettings.thumbnailWidth || defaultThumbnailWidth,
1929
};
2030
}
2131

0 commit comments

Comments
 (0)