Skip to content

Commit 0a5ac79

Browse files
committed
refactor query to make it generic
1 parent 3437850 commit 0a5ac79

File tree

7 files changed

+166
-14
lines changed

7 files changed

+166
-14
lines changed

controllers/tasks.js

Lines changed: 20 additions & 6 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
*
@@ -130,29 +131,42 @@ const fetchTasks = async (req, res) => {
130131
try {
131132
const { dev, status, page, size, prev, next, q: queryString } = req.query;
132133
const transformedQuery = transformQuery(dev, status, size, page);
133-
const searchTerm = queryString || "";
134+
134135
if (dev) {
135136
const paginatedTasks = await fetchPaginatedTasks({ ...transformedQuery, prev, next });
136137
return res.json({
137138
message: "Tasks returned successfully!",
138139
...paginatedTasks,
139140
});
140141
}
141-
if (queryString) {
142-
const filterTasks = await tasks.fetchTasks(searchTerm);
142+
143+
if (queryString !== undefined) {
144+
const searchParams = parseSearchQuery(queryString);
145+
const filterTasks = await tasks.fetchTasks(searchParams.searchTerm);
143146
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+
}
144153
return res.json({
145154
message: "Filter tasks returned successfully!",
146-
tasks: tasksWithRdsAssigneeInfo.length > 0 ? tasksWithRdsAssigneeInfo : [],
155+
tasks: tasksWithRdsAssigneeInfo,
147156
});
148157
}
149158

150159
const allTasks = await tasks.fetchTasks();
151160
const tasksWithRdsAssigneeInfo = await fetchTasksWithRdsAssigneeInfo(allTasks);
152-
161+
if (tasksWithRdsAssigneeInfo.length === 0) {
162+
return res.status(404).json({
163+
message: "No tasks found",
164+
tasks: [],
165+
});
166+
}
153167
return res.json({
154168
message: "Tasks returned successfully!",
155-
tasks: tasksWithRdsAssigneeInfo.length > 0 ? tasksWithRdsAssigneeInfo : [],
169+
tasks: tasksWithRdsAssigneeInfo,
156170
});
157171
} catch (err) {
158172
logger.error(`Error while fetching tasks ${err}`);

middlewares/validators/tasks.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,19 @@ const getTasksValidator = async (req, res, next) => {
164164
})
165165
),
166166
size: joi.number().integer().positive().min(1).max(100).optional(),
167-
q: joi.string().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", "assignee", "status"];
174+
if (!allowedKeywords.includes(key.toLowerCase())) {
175+
return helpers.error("any.invalid");
176+
}
177+
}
178+
return value;
179+
}, "Invalid query format"),
168180
});
169181

170182
try {

test/integration/tasks.test.js

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -261,11 +261,8 @@ describe("Tasks", function () {
261261
expect(previousPageResponse.body).to.have.property("prev");
262262
expect(previousPageResponse.body.tasks).to.have.length(1);
263263
});
264-
});
265-
266-
describe("GET /tasks/:id/details", function () {
267264
it("Should get tasks filtered by search term", function (done) {
268-
const searchTerm = "search";
265+
const searchTerm = "task";
269266
chai
270267
.request(app)
271268
.get(`/tasks?q=${encodeURIComponent(searchTerm)}`)
@@ -287,6 +284,27 @@ describe("Tasks", function () {
287284
return done();
288285
});
289286
});
287+
it("Should get tasks filtered by search term and handle no tasks found", function (done) {
288+
const searchTerm = " ";
289+
chai
290+
.request(app)
291+
.get(`/tasks?q=searchTerm:"${encodeURIComponent(searchTerm)}"`)
292+
.end((err, res) => {
293+
if (err) {
294+
return done(err);
295+
}
296+
297+
expect(res).to.have.status(404);
298+
expect(res.body).to.be.a("object");
299+
expect(res.body.message).to.equal("No tasks found");
300+
expect(res.body.tasks).to.be.a("array");
301+
expect(res.body.tasks).to.have.lengthOf(0);
302+
return done();
303+
});
304+
});
305+
});
306+
307+
describe("GET /tasks/:id/details", function () {
290308
it("should return the task task with the Id that we provide in the route params", function (done) {
291309
chai
292310
.request(app)

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

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -333,19 +333,54 @@ describe("getTasks validator", function () {
333333
expect(nextMiddlewareSpy.callCount).to.be.equal(1);
334334
});
335335

336-
it("should pass the request when correct parameters are passed: page, dev, status, sixe and searchterm", async function () {
336+
it("should pass the request when correct parameters are passed: page, dev, status and size", async function () {
337337
const req = {
338338
query: {
339339
dev: "true",
340340
size: 3,
341341
page: 0,
342342
status: TASK_STATUS.ASSIGNED,
343-
q: "searchterm",
344343
},
345344
};
346345
const res = {};
347346
const nextMiddlewareSpy = Sinon.spy();
348347
await getTasksValidator(req, res, nextMiddlewareSpy);
349348
expect(nextMiddlewareSpy.callCount).to.be.equal(1);
350349
});
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+
});
351386
});

test/unit/models/tasks.test.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,12 +92,20 @@ describe("tasks", function () {
9292
});
9393
});
9494
it("should fetch tasks filtered by search term", async function () {
95-
const searchTerm = "task";
95+
const searchTerm = "task-dependency";
9696
const tasksSnapshot = await tasksModel.get();
9797
const result = await getBuiltTasks(tasksSnapshot, searchTerm);
98+
expect(result).to.have.lengthOf(1);
9899
result.forEach((task) => {
99100
expect(task.title.toLowerCase()).to.include(searchTerm.toLowerCase());
100101
});
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);
101109
});
102110
});
103111

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
@@ -73,9 +73,32 @@ const transformQuery = (dev = false, status = "", size, page) => {
7373
return { status: transformedStatus, dev: transformedDev, ...query };
7474
};
7575

76+
const parseSearchQuery = (queryString) => {
77+
const searchParams = {};
78+
const queryParts = queryString.split("+");
79+
queryParts.forEach((part) => {
80+
const [key, value] = part.split(":");
81+
switch (key.toLowerCase()) {
82+
case "searchterm":
83+
searchParams.searchTerm = value.toLowerCase();
84+
break;
85+
case "assignee":
86+
searchParams.assignee = value.toLowerCase();
87+
break;
88+
case "status":
89+
searchParams.status = value.toLowerCase();
90+
break;
91+
default:
92+
break;
93+
}
94+
});
95+
return searchParams;
96+
};
97+
7698
module.exports = {
7799
fromFirestoreData,
78100
toFirestoreData,
79101
buildTasks,
80102
transformQuery,
103+
parseSearchQuery,
81104
};

0 commit comments

Comments
 (0)