Skip to content

Commit 55cd531

Browse files
authored
api-documentation & query-testing (#12)
1 parent 8570418 commit 55cd531

14 files changed

Lines changed: 1134 additions & 17 deletions

File tree

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
"pg-format": "^1.0.4",
2525
"pg-hstore": "^2.3.4",
2626
"sequelize": "^6.37.5",
27+
"swagger-jsdoc": "^6.2.8",
28+
"swagger-ui-express": "^5.0.1",
2729
"uuid": "^11.0.5"
2830
},
2931
"devDependencies": {

src/config/swagger.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { SwaggerOptions } from 'swagger-jsdoc';
2+
3+
const swaggerDefinition = {
4+
openapi: '3.0.0',
5+
info: {
6+
title: 'Backend as a Service API',
7+
version: '1.0.0',
8+
description: 'API Documentation for BaaS Project',
9+
},
10+
servers: [
11+
{
12+
url: 'http://localhost:3000/api',
13+
description: 'Local server',
14+
},
15+
],
16+
components: {
17+
securitySchemes: {
18+
BearerAuth: {
19+
type: 'http',
20+
scheme: 'bearer',
21+
bearerFormat: 'JWT',
22+
},
23+
ApiKeyAuth: {
24+
type: 'apiKey',
25+
in: 'header',
26+
name: 'x-api-key',
27+
},
28+
},
29+
},
30+
};
31+
32+
const options: SwaggerOptions = {
33+
swaggerDefinition,
34+
apis: ['src/routes/*.ts'],
35+
};
36+
37+
export default options;

src/controllers/query-controller.ts

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,12 @@
11
import { Request, Response } from 'express';
2-
import { sequelize } from '../config/database';
3-
import { QueryTypes } from 'sequelize';
42
import { errorResponse, successResponse } from '../utils/response';
3+
import { QueryExecutor } from '../operations/query';
54

65
export const executeQuery = async (req: Request, res: Response) => {
76
try {
87
const { query } = req.body;
98

10-
if (!query || typeof query !== 'string') {
11-
return errorResponse(res, 'Query is required and must be a string', 400);
12-
}
13-
14-
const forbiddenPatterns = [
15-
/DROP\s+TABLE/i,
16-
/ALTER\s+/i,
17-
/DELETE\s+FROM\s+[^\s]+(\s*;|$)/i,
18-
];
19-
if (forbiddenPatterns.some((pattern) => pattern.test(query))) {
20-
return errorResponse(res, 'Query contains forbidden operations', 403);
21-
}
22-
23-
const result = await sequelize.query(query, { type: QueryTypes.RAW });
9+
const result = await QueryExecutor.execute(query);
2410
return successResponse(res, result, 'Query executed successfully');
2511
} catch (error) {
2612
console.error(error);

src/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import express, { Request, Response } from 'express';
22
import dotenv from 'dotenv';
3+
import swaggerJsDoc from 'swagger-jsdoc';
4+
import swaggerUi from 'swagger-ui-express';
5+
import swaggerOptions from './config/swagger';
36
import userRoutes from './routes/user-routes';
47
import authRoutes from './routes/auth-routes';
58
import ddlRoutes from './routes/ddl-routes';
@@ -37,10 +40,14 @@ apiRouter.use('/apikey', apikeyRoutes);
3740

3841
app.use('/api', apiRouter);
3942

43+
const swaggerDocs = swaggerJsDoc(swaggerOptions);
44+
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocs));
45+
4046
app.get('/health', (req: Request, res: Response) => {
4147
res.send('Hello, TypeScript with Express!');
4248
});
4349

4450
app.listen(PORT, () => {
4551
console.log(`Server is running on http://localhost:${PORT}`);
52+
console.log(`Swagger Docs available at http://localhost:${PORT}/api-docs`);
4653
});

src/operations/query.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { sequelize } from '../config/database';
2+
import { QueryTypes } from 'sequelize';
3+
4+
export class QueryExecutor {
5+
static async execute(query: string) {
6+
if (!query || typeof query !== 'string') {
7+
throw new Error('Query is required and must be a string');
8+
}
9+
10+
const forbiddenPatterns = [
11+
/DROP\s+TABLE/i,
12+
/ALTER\s+/i,
13+
/DELETE\s+FROM\s+[^\s]+(\s*;|$)/i,
14+
];
15+
if (forbiddenPatterns.some((pattern) => pattern.test(query))) {
16+
throw new Error('Query contains forbidden operations');
17+
}
18+
19+
const result = await sequelize.query(query, { type: QueryTypes.RAW });
20+
return result;
21+
}
22+
}

src/routes/apikey-routes.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,93 @@ import { getApiKey, regenerateApiKey } from '../controllers/apikey-controller';
44

55
const router = Router();
66

7+
/**
8+
* @swagger
9+
* tags:
10+
* name: API Key
11+
* description: API for managing user API key
12+
*/
13+
14+
/**
15+
* @swagger
16+
* /apikey:
17+
* get:
18+
* summary: Get API key
19+
* tags: [API Key]
20+
* security:
21+
* - BearerAuth: []
22+
* responses:
23+
* 200:
24+
* description: Successfully retrieved API key
25+
* content:
26+
* application/json:
27+
* schema:
28+
* type: object
29+
* properties:
30+
* success:
31+
* type: boolean
32+
* example: true
33+
* message:
34+
* type: string
35+
* example: User details retrieved
36+
* apikey:
37+
* type: string
38+
* example: 'example-api-key'
39+
* 401:
40+
* description: Unauthorized (Invalid or missing token)
41+
* content:
42+
* application/json:
43+
* schema:
44+
* type: object
45+
* properties:
46+
* success:
47+
* type: boolean
48+
* example: false
49+
* message:
50+
* type: string
51+
* example: Unauthorized
52+
*/
753
router.get('/', authMiddleware, getApiKey);
54+
55+
/**
56+
* @swagger
57+
* /apikey:
58+
* put:
59+
* summary: Regenerate API key
60+
* tags: [API Key]
61+
* security:
62+
* - BearerAuth: []
63+
* responses:
64+
* 200:
65+
* description: Successfully regenerated API key
66+
* content:
67+
* application/json:
68+
* schema:
69+
* type: object
70+
* properties:
71+
* success:
72+
* type: boolean
73+
* example: true
74+
* message:
75+
* type: string
76+
* example: User details retrieved
77+
* apikey:
78+
* type: string
79+
* example: 'new-example-api-key'
80+
* 401:
81+
* description: Unauthorized (Invalid or missing token)
82+
* content:
83+
* application/json:
84+
* schema:
85+
* type: object
86+
* properties:
87+
* success:
88+
* type: boolean
89+
* example: false
90+
* message:
91+
* type: string
92+
* example: Unauthorized
93+
*/
894
router.put('/', authMiddleware, regenerateApiKey);
995

1096
export default router;

src/routes/auth-routes.ts

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,141 @@ import { registerUser, loginUser } from '../controllers/user-controller';
33

44
const router = Router();
55

6+
/**
7+
* @swagger
8+
* tags:
9+
* name: Authentication
10+
* description: API for user authentication
11+
*/
12+
13+
/**
14+
* @swagger
15+
* /auth/register:
16+
* post:
17+
* summary: Register a new user
18+
* tags: [Authentication]
19+
* requestBody:
20+
* required: true
21+
* content:
22+
* application/json:
23+
* schema:
24+
* type: object
25+
* required:
26+
* - name
27+
* - email
28+
* - password
29+
* properties:
30+
* name:
31+
* type: string
32+
* example: Full Name
33+
* email:
34+
* type: string
35+
* format: email
36+
* example: user@example.com
37+
* password:
38+
* type: string
39+
* format: password
40+
* example: password123
41+
* responses:
42+
* 200:
43+
* description: User registered successfully
44+
* content:
45+
* application/json:
46+
* schema:
47+
* type: object
48+
* properties:
49+
* success:
50+
* type: boolean
51+
* example: true
52+
* message:
53+
* type: string
54+
* example: User registered successfully
55+
* data:
56+
* type: object
57+
* properties:
58+
* id:
59+
* type: string
60+
* example: "123e4567-e89b-12d3-a456-426614174000"
61+
* 400:
62+
* description: Bad request (Missing fields or email already registered)
63+
* content:
64+
* application/json:
65+
* examples:
66+
* missing_fields:
67+
* summary: Missing required fields
68+
* value:
69+
* success: false
70+
* message: "Name, email, and password are required"
71+
* email_registered:
72+
* summary: Email already exists
73+
* value:
74+
* success: false
75+
* message: "Email is already registered"
76+
* 500:
77+
* description: Internal server error
78+
*/
679
router.post('/register', registerUser);
80+
81+
/**
82+
* @swagger
83+
* /auth/login:
84+
* post:
85+
* summary: Login a user
86+
* tags: [Authentication]
87+
* requestBody:
88+
* required: true
89+
* content:
90+
* application/json:
91+
* schema:
92+
* type: object
93+
* required:
94+
* - email
95+
* - password
96+
* properties:
97+
* email:
98+
* type: string
99+
* format: email
100+
* example: user@example.com
101+
* password:
102+
* type: string
103+
* format: password
104+
* example: password123
105+
* responses:
106+
* 200:
107+
* description: Login successful, returns a JWT token
108+
* content:
109+
* application/json:
110+
* schema:
111+
* type: object
112+
* properties:
113+
* success:
114+
* type: boolean
115+
* example: true
116+
* message:
117+
* type: string
118+
* example: Login successful
119+
* data:
120+
* type: object
121+
* properties:
122+
* token:
123+
* type: string
124+
* example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
125+
* 401:
126+
* description: Unauthorized
127+
* content:
128+
* application/json:
129+
* schema:
130+
* type: object
131+
* properties:
132+
* success:
133+
* type: boolean
134+
* example: false
135+
* message:
136+
* type: string
137+
* example: Unauthorized
138+
* 500:
139+
* description: Internal server error
140+
*/
7141
router.post('/login', loginUser);
8142

9143
export default router;

0 commit comments

Comments
 (0)