Skip to content

Commit 57e0171

Browse files
authored
Merge pull request #1327 from Real-Dev-Squad/feature/search-task
Add filter by query for task
2 parents 6e99deb + 1051ead commit 57e0171

File tree

8 files changed

+205
-8
lines changed

8 files changed

+205
-8
lines changed

controllers/tasks.js

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const { transformQuery } = require("../utils/tasks");
1111
const { getPaginatedLink } = require("../utils/helper");
1212
const { updateUserStatusOnTaskUpdate, updateStatusOnTaskCompletion } = require("../models/userStatus");
1313
const dataAccess = require("../services/dataAccessLayer");
14+
const { parseSearchQuery } = require("../utils/tasks");
1415
/**
1516
* Creates new task
1617
*
@@ -128,7 +129,7 @@ const fetchPaginatedTasks = async (query) => {
128129

129130
const fetchTasks = async (req, res) => {
130131
try {
131-
const { dev, status, page, size, prev, next } = req.query;
132+
const { dev, status, page, size, prev, next, q: queryString } = req.query;
132133
const transformedQuery = transformQuery(dev, status, size, page);
133134

134135
if (dev) {
@@ -139,12 +140,33 @@ const fetchTasks = async (req, res) => {
139140
});
140141
}
141142

143+
if (queryString !== undefined) {
144+
const searchParams = parseSearchQuery(queryString);
145+
const filterTasks = await tasks.fetchTasks(searchParams.searchTerm);
146+
const tasksWithRdsAssigneeInfo = await fetchTasksWithRdsAssigneeInfo(filterTasks);
147+
if (tasksWithRdsAssigneeInfo.length === 0) {
148+
return res.status(404).json({
149+
message: "No tasks found.",
150+
tasks: [],
151+
});
152+
}
153+
return res.json({
154+
message: "Filter tasks returned successfully!",
155+
tasks: tasksWithRdsAssigneeInfo,
156+
});
157+
}
158+
142159
const allTasks = await tasks.fetchTasks();
143160
const tasksWithRdsAssigneeInfo = await fetchTasksWithRdsAssigneeInfo(allTasks);
144-
161+
if (tasksWithRdsAssigneeInfo.length === 0) {
162+
return res.status(404).json({
163+
message: "No tasks found",
164+
tasks: [],
165+
});
166+
}
145167
return res.json({
146168
message: "Tasks returned successfully!",
147-
tasks: tasksWithRdsAssigneeInfo.length > 0 ? tasksWithRdsAssigneeInfo : [],
169+
tasks: tasksWithRdsAssigneeInfo,
148170
});
149171
} catch (err) {
150172
logger.error(`Error while fetching tasks ${err}`);

middlewares/validators/tasks.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,19 @@ const getTasksValidator = async (req, res, next) => {
164164
})
165165
),
166166
size: joi.number().integer().positive().min(1).max(100).optional(),
167+
q: joi
168+
.string()
169+
.optional()
170+
.custom((value, helpers) => {
171+
if (value && value.includes(":")) {
172+
const [key] = value.split(":");
173+
const allowedKeywords = ["searchterm"];
174+
if (!allowedKeywords.includes(key.toLowerCase())) {
175+
return helpers.error("any.invalid");
176+
}
177+
}
178+
return value;
179+
}, "Invalid query format"),
167180
});
168181

169182
try {

models/tasks.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,14 @@ const addDependency = async (data) => {
9696
* @return {Promise<tasks|Array>}
9797
*/
9898

99-
const getBuiltTasks = async (tasksSnapshot) => {
99+
const getBuiltTasks = async (tasksSnapshot, searchTerm) => {
100100
const tasks = buildTasks(tasksSnapshot);
101101
const promises = tasks.map(async (task) => fromFirestoreData(task));
102-
const updatedTasks = await Promise.all(promises);
102+
let updatedTasks = await Promise.all(promises);
103+
104+
if (searchTerm) {
105+
updatedTasks = updatedTasks.filter((task) => task.title.toLowerCase().includes(searchTerm.toLowerCase()));
106+
}
103107
const taskPromises = updatedTasks.map(async (task) => {
104108
task.status = TASK_STATUS[task.status.toUpperCase()] || task.status;
105109
const taskId = task.id;
@@ -159,10 +163,10 @@ const fetchPaginatedTasks = async ({ status = "", size = TASK_SIZE, page, next,
159163
}
160164
};
161165

162-
const fetchTasks = async () => {
166+
const fetchTasks = async (searchTerm) => {
163167
try {
164168
const tasksSnapshot = await tasksModel.get();
165-
const taskList = await getBuiltTasks(tasksSnapshot);
169+
const taskList = await getBuiltTasks(tasksSnapshot, searchTerm);
166170
return taskList;
167171
} catch (err) {
168172
logger.error("error getting tasks", err);
@@ -469,4 +473,5 @@ module.exports = {
469473
addDependency,
470474
fetchTaskByIssueId,
471475
fetchPaginatedTasks,
476+
getBuiltTasks,
472477
};

test/integration/tasks.test.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,46 @@ describe("Tasks", function () {
261261
expect(previousPageResponse.body).to.have.property("prev");
262262
expect(previousPageResponse.body.tasks).to.have.length(1);
263263
});
264+
it("Should get tasks filtered by search term", function (done) {
265+
const searchTerm = "task";
266+
chai
267+
.request(app)
268+
.get("/tasks?q=searchTerm:task")
269+
.end((err, res) => {
270+
if (err) {
271+
return done(err);
272+
}
273+
expect(res).to.have.status(200);
274+
expect(res.body).to.be.a("object");
275+
expect(res.body.message).to.equal("Filter tasks returned successfully!");
276+
expect(res.body.tasks).to.be.a("array");
277+
278+
const matchingTasks = res.body.tasks;
279+
matchingTasks.forEach((task) => {
280+
expect(task.title.toLowerCase()).to.include(searchTerm.toLowerCase());
281+
});
282+
expect(matchingTasks).to.have.length(3);
283+
284+
return done();
285+
});
286+
});
287+
it("Should get tasks filtered by search term and handle no tasks found", function (done) {
288+
chai
289+
.request(app)
290+
.get(`/tasks?q=searchTerm:random1`)
291+
.end((err, res) => {
292+
if (err) {
293+
return done(err);
294+
}
295+
296+
expect(res).to.have.status(404);
297+
expect(res.body).to.be.a("object");
298+
expect(res.body.message).to.equal("No tasks found.");
299+
expect(res.body.tasks).to.be.a("array");
300+
expect(res.body.tasks).to.have.lengthOf(0);
301+
return done();
302+
});
303+
});
264304
});
265305

266306
describe("GET /tasks/:id/details", function () {

test/unit/middlewares/tasks-validator.test.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,4 +347,40 @@ describe("getTasks validator", function () {
347347
await getTasksValidator(req, res, nextMiddlewareSpy);
348348
expect(nextMiddlewareSpy.callCount).to.be.equal(1);
349349
});
350+
it("should pass the request when a valid query is provided", async function () {
351+
const req = {
352+
query: {
353+
dev: "true",
354+
size: 3,
355+
page: 0,
356+
status: TASK_STATUS.ASSIGNED,
357+
q: "searchterm:apple",
358+
},
359+
};
360+
const res = {};
361+
const nextMiddlewareSpy = Sinon.spy();
362+
await getTasksValidator(req, res, nextMiddlewareSpy);
363+
expect(nextMiddlewareSpy.callCount).to.be.equal(1);
364+
});
365+
it("should fail the request when an invalid query is provided", async function () {
366+
const req = {
367+
query: {
368+
dev: "true",
369+
size: 3,
370+
page: 0,
371+
status: TASK_STATUS.ASSIGNED,
372+
q: "invalidkey:value",
373+
},
374+
};
375+
const res = {
376+
boom: {
377+
badRequest: (message) => {
378+
expect(message).to.equal('"q" contains an invalid value');
379+
},
380+
},
381+
};
382+
const nextMiddlewareSpy = Sinon.spy();
383+
await getTasksValidator(req, res, nextMiddlewareSpy);
384+
expect(nextMiddlewareSpy.callCount).to.be.equal(0);
385+
});
350386
});

test/unit/models/tasks.test.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const { expect } = chai;
99
const cleanDb = require("../../utils/cleanDb");
1010
const tasksData = require("../../fixtures/tasks/tasks")();
1111
const tasks = require("../../../models/tasks");
12-
const { addDependency, updateTask } = require("../../../models/tasks");
12+
const { addDependency, updateTask, getBuiltTasks } = require("../../../models/tasks");
1313
const firestore = require("../../../utils/firestore");
1414
const { TASK_STATUS } = require("../../../constants/tasks");
1515
const dependencyModel = firestore.collection("TaskDependencies");
@@ -91,6 +91,22 @@ describe("tasks", function () {
9191
expect(task).to.contain.all.keys(sameTask);
9292
});
9393
});
94+
it("should fetch tasks filtered by search term", async function () {
95+
const searchTerm = "task-dependency";
96+
const tasksSnapshot = await tasksModel.get();
97+
const result = await getBuiltTasks(tasksSnapshot, searchTerm);
98+
expect(result).to.have.lengthOf(1);
99+
result.forEach((task) => {
100+
expect(task.title.toLowerCase()).to.include(searchTerm.toLowerCase());
101+
});
102+
expect(tasksData[5].title.includes(searchTerm));
103+
});
104+
it("should return empty array when no search term is found", async function () {
105+
const searchTerm = "random";
106+
const tasksSnapshot = await tasksModel.get();
107+
const result = await getBuiltTasks(tasksSnapshot, searchTerm);
108+
expect(result).to.have.lengthOf(0);
109+
});
94110
});
95111

96112
describe("paginatedTasks", function () {
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
const { expect } = require("chai");
2+
const { parseSearchQuery } = require("../../../utils/tasks");
3+
4+
describe("parseSearchQuery", function () {
5+
it("should parse a valid query string", function () {
6+
const queryString = "searchterm:example+assignee:john.doe+status:in_progress";
7+
const result = parseSearchQuery(queryString);
8+
9+
expect(result).to.deep.equal({
10+
searchTerm: "example",
11+
assignee: "john.doe",
12+
status: "in_progress",
13+
});
14+
});
15+
16+
it("should handle an empty query string", function () {
17+
const queryString = "";
18+
const result = parseSearchQuery(queryString);
19+
20+
expect(result).to.deep.equal({});
21+
});
22+
23+
it("should ignore unknown keys in the query string", function () {
24+
const queryString = "searchterm:example+assignee:john.doe+category:work";
25+
const result = parseSearchQuery(queryString);
26+
27+
expect(result).to.deep.equal({
28+
searchTerm: "example",
29+
assignee: "john.doe",
30+
});
31+
});
32+
33+
it("should handle query string with duplicate keys", function () {
34+
const queryString = "searchterm:example+searchterm:test+assignee:john.doe";
35+
const result = parseSearchQuery(queryString);
36+
37+
expect(result).to.deep.equal({
38+
searchTerm: "test",
39+
assignee: "john.doe",
40+
});
41+
});
42+
});

utils/tasks.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,32 @@ const transformQuery = (dev = false, status = "", size, page) => {
8080
return { status: transformedStatus, dev: transformedDev, ...query };
8181
};
8282

83+
const parseSearchQuery = (queryString) => {
84+
const searchParams = {};
85+
const queryParts = queryString.split("+");
86+
queryParts.forEach((part) => {
87+
const [key, value] = part.split(":");
88+
switch (key.toLowerCase()) {
89+
case "searchterm":
90+
searchParams.searchTerm = value.toLowerCase();
91+
break;
92+
case "assignee":
93+
searchParams.assignee = value.toLowerCase();
94+
break;
95+
case "status":
96+
searchParams.status = value.toLowerCase();
97+
break;
98+
default:
99+
break;
100+
}
101+
});
102+
return searchParams;
103+
};
104+
83105
module.exports = {
84106
fromFirestoreData,
85107
toFirestoreData,
86108
buildTasks,
87109
transformQuery,
110+
parseSearchQuery,
88111
};

0 commit comments

Comments
 (0)