Skip to content

Commit 84b4437

Browse files
authored
Merge pull request #130 from upskill-team/feature/appeal-search-endpoint
Feature: Appeal search with filters endpoint
2 parents f3f604f + b3631cd commit 84b4437

4 files changed

Lines changed: 84 additions & 15 deletions

File tree

pnpm-lock.yaml

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/models/appeal/appeal.controller.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { NextFunction, Request, Response } from 'express'
77
import { orm } from '../../shared/db/orm.js'
88
import { AppealService } from './appeal.service.js'
99
import { HttpResponse } from '../../shared/response/http.response.js'
10+
import * as v from 'valibot'
11+
import { SearchAppealsSchema } from './appeal.schemas.js'
1012

1113
/**
1214
* Handles the creation of a new appeal.
@@ -39,18 +41,32 @@ async function add(req: Request, res: Response, next: NextFunction) {
3941
}
4042

4143
/**
42-
* Handles the retrieval of all appeals.
43-
* @param {Request} req The Express request object.
44+
* Handles the retrieval of all appeals with filtering, sorting, and pagination.
45+
* @param {Request} req The Express request object, containing query parameters.
4446
* @param {Response} res The Express response object.
4547
* @param {NextFunction} next The next middleware function.
46-
* @returns {Promise<Response>} A list of all appeals.
48+
* @returns {Promise<Response>} A paginated list of appeals.
4749
*/
4850
async function findAll(req: Request, res: Response, next: NextFunction) {
4951
try {
52+
const validatedQuery = v.parse(SearchAppealsSchema, req.query);
53+
5054
const appealService = new AppealService(orm.em.fork(), req.log)
51-
const appeals = await appealService.findAll()
52-
return HttpResponse.Ok(res, appeals)
55+
const result = await appealService.findAll(validatedQuery)
56+
57+
return HttpResponse.Ok(res, result)
58+
5359
} catch (error) {
60+
if (error instanceof v.ValiError) {
61+
const errorDetails = error.issues.map(issue => ({
62+
field: issue.path?.map((p: { key: any; }) => p.key).join('.'),
63+
message: issue.message,
64+
}));
65+
return HttpResponse.BadRequest(res, {
66+
message: 'Parámetros de consulta inválidos.',
67+
errors: errorDetails,
68+
});
69+
}
5470
next(error)
5571
}
5672
}

src/models/appeal/appeal.schemas.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55

66
import * as v from 'valibot';
77

8+
const NumericString = v.pipe(
9+
v.string('El valor debe ser un string.'),
10+
v.regex(/^\d+$/, 'Debe contener solo dígitos.'),
11+
v.transform(Number)
12+
);
13+
814
/**
915
* Schema for creating a new appeal. Validates the required fields for submitting a professor application.
1016
*/
@@ -36,5 +42,29 @@ export const UpdateAppealSchema = v.object({
3642
),
3743
});
3844

45+
/**
46+
* Schema for searching/filtering appeals via query parameters.
47+
* Valida y transforma los parámetros de la URL para la búsqueda.
48+
*/
49+
export const SearchAppealsSchema = v.object({
50+
51+
status: v.optional(v.picklist(['pending', 'accepted', 'rejected'])),
52+
q: v.optional(v.string()),
53+
54+
limit: v.optional(NumericString, '10'),
55+
offset: v.optional(NumericString, '0'),
56+
57+
sortBy: v.optional(v.string(), 'date'),
58+
sortOrder: v.optional(
59+
v.pipe(
60+
v.string(),
61+
v.regex(/^(ASC|DESC)$/i, 'sortOrder debe ser "ASC" o "DESC".'),
62+
v.transform(val => val.toUpperCase() as 'ASC' | 'DESC')
63+
),
64+
'DESC'
65+
),
66+
});
67+
3968
export type CreateAppealType = v.InferOutput<typeof CreateAppealSchema>;
4069
export type UpdateAppealType = v.InferOutput<typeof UpdateAppealSchema>;
70+
export type SearchAppealsQuery = v.InferOutput<typeof SearchAppealsSchema>;

src/models/appeal/appeal.service.ts

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
* @remarks Encapsulates the business logic for managing professor appeals.
44
*/
55

6-
import { EntityManager } from '@mikro-orm/core'
6+
import { EntityManager, FilterQuery } from '@mikro-orm/core'
77
import { Appeal } from './appeal.entity.js'
8-
import { CreateAppealType, UpdateAppealSchema, UpdateAppealType } from './appeal.schemas.js'
8+
import { CreateAppealType, SearchAppealsQuery, UpdateAppealSchema, UpdateAppealType } from './appeal.schemas.js'
99
import { User } from '../user/user.entity.js'
1010
import { ProfessorService } from '../professor/professor.services.js'
1111
import { ObjectId } from '@mikro-orm/mongodb'
@@ -58,13 +58,37 @@ export class AppealService {
5858
}
5959

6060
/**
61-
* Retrieves all appeals, populating the associated user information.
62-
* @returns {Promise<Appeal[]>} A promise that resolves to an array of all appeals.
61+
* Retrieves all appeals based on filter, sort, and pagination parameters.
62+
* @param {SearchAppealsQuery} query - The validated query parameters.
63+
* @returns {Promise<{appeals: Appeal[], total: number}>} An object with the list of appeals and the total count.
6364
*/
64-
public async findAll(): Promise<Appeal[]> {
65-
this.logger.info('Fetching all appeals.')
65+
public async findAll(query: SearchAppealsQuery): Promise<{ appeals: Appeal[], total: number }> {
66+
this.logger.info({ query }, 'Fetching all appeals with filters.')
6667

67-
return this.em.find(Appeal, {}, { populate: ['user'] })
68+
const where: FilterQuery<Appeal> = {};
69+
70+
if (query.status) {
71+
where.state = query.status;
72+
}
73+
74+
if (query.q) {
75+
const searchQuery = { $ilike: `%${query.q}%` };
76+
where.user = {
77+
$or: [
78+
{ name: searchQuery },
79+
{ surname: searchQuery }
80+
]
81+
};
82+
}
83+
84+
const [appeals, total] = await this.em.findAndCount(Appeal, where, {
85+
populate: ['user'],
86+
orderBy: { [query.sortBy]: query.sortOrder },
87+
limit: query.limit,
88+
offset: query.offset,
89+
});
90+
91+
return { appeals, total };
6892
}
6993

7094
/**
@@ -74,7 +98,6 @@ export class AppealService {
7498
*/
7599
public async findOne(id: string): Promise<Appeal | null> {
76100
this.logger.info({ appealId: id }, 'Fetching appeal.')
77-
78101
const objectId = new ObjectId(id);
79102
return this.em.findOne(Appeal, { _id: objectId }, { populate: ['user'] })
80103
}

0 commit comments

Comments
 (0)