Skip to content

Commit 64600b3

Browse files
committed
feat(notes): implement notes management endpoints and services
- Add endpoints for retrieving notes by ID, fetching comments for a note, and searching notes with filters - Implement corresponding controller and service logic to handle business operations for notes - Enhance documentation in USAGE.md to include detailed usage examples for new notes endpoints - Introduce integration and unit tests to ensure functionality and reliability of notes management features
1 parent 08ef21b commit 64600b3

File tree

8 files changed

+1250
-7
lines changed

8 files changed

+1250
-7
lines changed

docs/USAGE.md

Lines changed: 165 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -134,27 +134,187 @@ curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
134134

135135
## Usage Examples
136136

137-
### Get User Profile
137+
### Notes Endpoints
138+
139+
#### Get Note by ID
140+
141+
Get detailed information about a specific note.
138142

139143
```bash
140144
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
141-
http://localhost:3000/api/v1/users/12345
145+
http://localhost:3000/api/v1/notes/12345
146+
```
147+
148+
**Response**:
149+
```json
150+
{
151+
"data": {
152+
"note_id": 12345,
153+
"latitude": 4.6097,
154+
"longitude": -74.0817,
155+
"status": "open",
156+
"created_at": "2024-01-15T10:30:00Z",
157+
"closed_at": null,
158+
"id_user": 67890,
159+
"id_country": 42,
160+
"comments_count": 3
161+
}
162+
}
142163
```
143164

144-
### Get Country Profile
165+
**Error Responses**:
166+
- `400 Bad Request`: Invalid note ID format
167+
- `404 Not Found`: Note does not exist
168+
- `500 Internal Server Error`: Server error
169+
170+
#### Get Note Comments
171+
172+
Get all comments for a specific note.
145173

146174
```bash
147175
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
148-
http://localhost:3000/api/v1/countries/CO
176+
http://localhost:3000/api/v1/notes/12345/comments
177+
```
178+
179+
**Response**:
180+
```json
181+
{
182+
"data": [
183+
{
184+
"comment_id": 1,
185+
"note_id": 12345,
186+
"user_id": 67890,
187+
"username": "test_user",
188+
"action": "opened",
189+
"created_at": "2024-01-15T10:30:00Z",
190+
"text": "This is a test note"
191+
},
192+
{
193+
"comment_id": 2,
194+
"note_id": 12345,
195+
"user_id": 67891,
196+
"username": "another_user",
197+
"action": "commented",
198+
"created_at": "2024-01-15T11:00:00Z",
199+
"text": "I can help with this"
200+
}
201+
],
202+
"count": 2
203+
}
149204
```
150205

151-
### Search Notes
206+
**Error Responses**:
207+
- `400 Bad Request`: Invalid note ID format
208+
- `500 Internal Server Error`: Server error
209+
210+
#### Search Notes
152211

212+
Search notes with various filters and pagination.
213+
214+
**Basic Search**:
153215
```bash
154216
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
155217
"http://localhost:3000/api/v1/notes?status=open&limit=10"
156218
```
157219

220+
**With Filters**:
221+
```bash
222+
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
223+
"http://localhost:3000/api/v1/notes?country=42&status=open&date_from=2024-01-01&date_to=2024-12-31&page=1&limit=20"
224+
```
225+
226+
**Query Parameters**:
227+
- `country` (number): Filter by country ID
228+
- `status` (string): Filter by status (`open`, `closed`, `reopened`)
229+
- `user_id` (number): Filter by user ID
230+
- `date_from` (string): Filter notes created from this date (ISO format: `YYYY-MM-DD`)
231+
- `date_to` (string): Filter notes created until this date (ISO format: `YYYY-MM-DD`)
232+
- `bbox` (string): Filter by bounding box (format: `min_lon,min_lat,max_lon,max_lat`)
233+
- `page` (number): Page number (default: 1, minimum: 1)
234+
- `limit` (number): Results per page (default: 20, maximum: 100, minimum: 1)
235+
236+
**Response**:
237+
```json
238+
{
239+
"data": [
240+
{
241+
"note_id": 12345,
242+
"latitude": 4.6097,
243+
"longitude": -74.0817,
244+
"status": "open",
245+
"created_at": "2024-01-15T10:30:00Z",
246+
"closed_at": null,
247+
"id_user": 67890,
248+
"id_country": 42,
249+
"comments_count": 3
250+
},
251+
{
252+
"note_id": 12346,
253+
"latitude": 4.6100,
254+
"longitude": -74.0820,
255+
"status": "open",
256+
"created_at": "2024-01-16T10:30:00Z",
257+
"closed_at": null,
258+
"id_user": 67891,
259+
"id_country": 42,
260+
"comments_count": 1
261+
}
262+
],
263+
"pagination": {
264+
"page": 1,
265+
"limit": 20,
266+
"total": 250,
267+
"total_pages": 13
268+
},
269+
"filters": {
270+
"country": 42,
271+
"status": "open",
272+
"date_from": "2024-01-01",
273+
"date_to": "2024-12-31",
274+
"page": 1,
275+
"limit": 20
276+
}
277+
}
278+
```
279+
280+
**Error Responses**:
281+
- `400 Bad Request`: Invalid parameters (invalid status, invalid page/limit values)
282+
- `500 Internal Server Error`: Server error
283+
284+
**Examples**:
285+
286+
Search open notes in Colombia:
287+
```bash
288+
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
289+
"http://localhost:3000/api/v1/notes?country=42&status=open"
290+
```
291+
292+
Search notes by user:
293+
```bash
294+
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
295+
"http://localhost:3000/api/v1/notes?user_id=67890"
296+
```
297+
298+
Search notes in a bounding box:
299+
```bash
300+
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
301+
"http://localhost:3000/api/v1/notes?bbox=-74.1,4.6,-74.0,4.7"
302+
```
303+
304+
### User Profile Endpoint
305+
306+
```bash
307+
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
308+
http://localhost:3000/api/v1/users/12345
309+
```
310+
311+
### Country Profile Endpoint
312+
313+
```bash
314+
curl -H "User-Agent: MyApp/1.0 (contact@example.com)" \
315+
http://localhost:3000/api/v1/countries/CO
316+
```
317+
158318
## Error Handling
159319

160320
### HTTP Status Codes

src/controllers/notesController.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/**
2+
* Notes controller
3+
* Handles HTTP requests for notes endpoints
4+
*/
5+
6+
import { Request, Response, NextFunction } from 'express';
7+
import * as noteService from '../services/noteService';
8+
import { logger } from '../utils/logger';
9+
import { ApiError } from '../middleware/errorHandler';
10+
import { SearchFilters } from '../types';
11+
12+
/**
13+
* Get a note by ID
14+
* GET /api/v1/notes/:note_id
15+
*/
16+
export async function getNoteById(req: Request, res: Response, next: NextFunction): Promise<void> {
17+
try {
18+
const noteId = parseInt(req.params.note_id, 10);
19+
20+
if (isNaN(noteId) || noteId <= 0) {
21+
throw new ApiError(400, 'Invalid note ID');
22+
}
23+
24+
logger.debug('Getting note by ID', { noteId });
25+
26+
const note = await noteService.getNoteById(noteId);
27+
28+
res.json({
29+
data: note,
30+
});
31+
} catch (error) {
32+
next(error);
33+
}
34+
}
35+
36+
/**
37+
* Get comments for a note
38+
* GET /api/v1/notes/:note_id/comments
39+
*/
40+
export async function getNoteComments(
41+
req: Request,
42+
res: Response,
43+
next: NextFunction
44+
): Promise<void> {
45+
try {
46+
const noteId = parseInt(req.params.note_id, 10);
47+
48+
if (isNaN(noteId) || noteId <= 0) {
49+
throw new ApiError(400, 'Invalid note ID');
50+
}
51+
52+
logger.debug('Getting note comments', { noteId });
53+
54+
const comments = await noteService.getNoteComments(noteId);
55+
56+
res.json({
57+
data: comments,
58+
count: comments.length,
59+
});
60+
} catch (error) {
61+
next(error);
62+
}
63+
}
64+
65+
/**
66+
* Search notes with filters
67+
* GET /api/v1/notes
68+
*/
69+
export async function searchNotes(req: Request, res: Response, next: NextFunction): Promise<void> {
70+
try {
71+
const filters: SearchFilters = {
72+
country: req.query.country ? parseInt(String(req.query.country), 10) : undefined,
73+
status: req.query.status as 'open' | 'closed' | 'reopened' | undefined,
74+
hashtag: req.query.hashtag ? String(req.query.hashtag) : undefined,
75+
date_from: req.query.date_from ? String(req.query.date_from) : undefined,
76+
date_to: req.query.date_to ? String(req.query.date_to) : undefined,
77+
user_id: req.query.user_id ? parseInt(String(req.query.user_id), 10) : undefined,
78+
application: req.query.application ? String(req.query.application) : undefined,
79+
bbox: req.query.bbox ? String(req.query.bbox) : undefined,
80+
page: req.query.page ? parseInt(String(req.query.page), 10) : 1,
81+
limit: req.query.limit ? parseInt(String(req.query.limit), 10) : 20,
82+
};
83+
84+
// Validate page and limit
85+
if (filters.page !== undefined && (isNaN(filters.page) || filters.page < 1)) {
86+
throw new ApiError(400, 'Invalid page number');
87+
}
88+
89+
if (
90+
filters.limit !== undefined &&
91+
(isNaN(filters.limit) || filters.limit < 1 || filters.limit > 100)
92+
) {
93+
throw new ApiError(400, 'Invalid limit (must be between 1 and 100)');
94+
}
95+
96+
// Validate status if provided
97+
if (filters.status && !['open', 'closed', 'reopened'].includes(filters.status)) {
98+
throw new ApiError(400, 'Invalid status (must be open, closed, or reopened)');
99+
}
100+
101+
logger.debug('Searching notes', { filters });
102+
103+
const result = await noteService.searchNotes(filters);
104+
105+
res.json(result);
106+
} catch (error) {
107+
next(error);
108+
}
109+
}

src/routes/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import { Router } from 'express';
66
import { getAppConfig } from '../config/app';
77
import healthRouter from './health';
8+
import notesRouter from './notes';
89

910
const router = Router();
1011
const { apiVersion } = getAppConfig();
@@ -26,4 +27,9 @@ router.get(`/api/${apiVersion}`, (_req, res) => {
2627
*/
2728
router.use('/health', healthRouter);
2829

30+
/**
31+
* Notes routes
32+
*/
33+
router.use(`/api/${apiVersion}/notes`, notesRouter);
34+
2935
export default router;

src/routes/notes.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/**
2+
* Notes routes
3+
*/
4+
5+
import { Router, Request, Response, NextFunction } from 'express';
6+
import * as notesController from '../controllers/notesController';
7+
8+
const router = Router();
9+
10+
/**
11+
* Async wrapper for route handlers
12+
*/
13+
function asyncHandler(fn: (req: Request, res: Response, next: NextFunction) => Promise<void>) {
14+
return (req: Request, res: Response, next: NextFunction): void => {
15+
void Promise.resolve(fn(req, res, next)).catch(next);
16+
};
17+
}
18+
19+
/**
20+
* @route GET /api/v1/notes
21+
* @desc Search notes with filters
22+
* @access Public
23+
*/
24+
router.get('/', asyncHandler(notesController.searchNotes));
25+
26+
/**
27+
* @route GET /api/v1/notes/:note_id
28+
* @desc Get a note by ID
29+
* @access Public
30+
*/
31+
router.get('/:note_id', asyncHandler(notesController.getNoteById));
32+
33+
/**
34+
* @route GET /api/v1/notes/:note_id/comments
35+
* @desc Get comments for a note
36+
* @access Public
37+
*/
38+
router.get('/:note_id/comments', asyncHandler(notesController.getNoteComments));
39+
40+
export default router;

0 commit comments

Comments
 (0)