Skip to content
This repository was archived by the owner on Nov 20, 2025. It is now read-only.

Commit 70d2cc7

Browse files
authored
create GET /ideas?filter[title][like] (#62)
* create GET /ideas?filter[title][like] * fix validator, remove .only from test * added tests and validation for query items
1 parent df4d175 commit 70d2cc7

10 files changed

Lines changed: 243 additions & 7 deletions

File tree

controllers/goto/ideas.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ module.exports = {
1111
withCreators: route(['query.filter.creators']),
1212
commentedBy: route(['query.filter.commentedBy']),
1313
highlyVoted: route(['query.filter.highlyVoted']),
14-
trending: route(['query.filter.trending'])
14+
trending: route(['query.filter.trending']),
15+
searchTitle: route(['query.filter.title.like'])
1516
},
1617
};

controllers/ideas.js

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,4 +276,27 @@ async function getIdeasTrending(req, res, next) {
276276
}
277277
}
278278

279-
module.exports = { get, getIdeasCommentedBy, getIdeasHighlyVoted, getIdeasTrending, getIdeasWithCreators, getIdeasWithMyTags, getIdeasWithTags, getNewIdeas, getRandomIdeas, patch, post };
279+
/**
280+
* Get ideas with any of specified keywords in title
281+
*/
282+
async function getIdeasSearchTitle(req, res, next) {
283+
try {
284+
// gather data
285+
const { page: { offset = 0, limit = 10 } = { } } = req.query;
286+
const { like: keywords } = req.query.filter.title;
287+
288+
// read ideas from database
289+
const foundIdeas = await models.idea.findWithTitleKeywords(keywords, { offset, limit });
290+
// serialize
291+
const serializedIdeas = serialize.idea(foundIdeas);
292+
293+
// respond
294+
return res.status(200).json(serializedIdeas);
295+
296+
} catch (e) {
297+
return next(e);
298+
}
299+
}
300+
301+
302+
module.exports = { get, getIdeasCommentedBy, getIdeasHighlyVoted, getIdeasSearchTitle, getIdeasTrending, getIdeasWithCreators, getIdeasWithMyTags, getIdeasWithTags, getNewIdeas, getRandomIdeas, patch, post };

controllers/validators/ideas.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ module.exports = {
66
get: validate('getIdea'),
77
getIdeasCommentedBy: validate('getIdeasCommentedBy'),
88
getIdeasHighlyVoted: validate('getIdeasHighlyVoted'),
9+
getIdeasSearchTitle: validate('getIdeasSearchTitle'),
910
getIdeasTrending: validate('getIdeasTrending'),
1011
getIdeasWithCreators: validate('getIdeasWithCreators'),
1112
getIdeasWithMyTags: validate('getIdeasWithMyTags'),

controllers/validators/parser.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ const parametersDictionary = {
2828
size: 'int',
2929
creators: 'array',
3030
commentedBy: 'array',
31-
highlyVoted: 'int'
31+
highlyVoted: 'int',
32+
title: {
33+
like: 'array'
34+
}
3235
},
3336
};
3437

controllers/validators/schema/definitions.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use strict';
22

3-
const { tagname, username } = require('./paths');
3+
const { tagname, title, username } = require('./paths');
44

55
module.exports = {
66
shared: {
@@ -174,6 +174,12 @@ module.exports = {
174174
items: username,
175175
maxItems: 10,
176176
minItems: 1
177+
},
178+
keywordsList: {
179+
type: 'array',
180+
items: title,
181+
maxItems: 10,
182+
minItems: 1
177183
}
178184
}
179185
};

controllers/validators/schema/ideas.js

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
'use strict';
22

3-
const { title, detail, id, page, pageOffset0, random, tagsList, usersList } = require('./paths');
4-
3+
const { title, detail, id, keywordsList, page, pageOffset0, random, tagsList, usersList } = require('./paths');
54
const postIdeas = {
65
properties: {
76
body: {
@@ -218,4 +217,30 @@ const getIdeasTrending = {
218217
required: ['query']
219218
};
220219

221-
module.exports = { getIdea, getIdeasCommentedBy, getIdeasHighlyVoted, getIdeasTrending, getIdeasWithCreators, getIdeasWithMyTags, getIdeasWithTags, getNewIdeas, getRandomIdeas, patchIdea, postIdeas };
220+
const getIdeasSearchTitle = {
221+
properties: {
222+
query: {
223+
properties: {
224+
filter: {
225+
properties: {
226+
title: {
227+
properties: {
228+
like: keywordsList
229+
},
230+
required: ['like'],
231+
additionalProperties: false
232+
}
233+
},
234+
required: ['title'],
235+
additionalProperties: false
236+
},
237+
page
238+
},
239+
required: ['filter'],
240+
additionalProperties: false
241+
},
242+
},
243+
required: ['query']
244+
};
245+
246+
module.exports = { getIdea, getIdeasCommentedBy, getIdeasHighlyVoted, getIdeasSearchTitle, getIdeasTrending, getIdeasWithCreators, getIdeasWithMyTags, getIdeasWithTags, getNewIdeas, getRandomIdeas, patchIdea, postIdeas };

controllers/validators/schema/paths.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ module.exports = {
2121
random: { $ref: 'sch#/definitions/query/random' },
2222
tagsList: { $ref: 'sch#/definitions/query/tagsList' },
2323
usersList: { $ref: 'sch#/definitions/query/usersList' },
24+
keywordsList: { $ref: 'sch#/definitions/query/keywordsList' },
2425
ideaId: { $ref : 'sch#/definitions/idea/ideaId' },
2526
title: { $ref: 'sch#/definitions/idea/titl' },
2627
detail: { $ref: 'sch#/definitions/idea/detail' },

models/idea/index.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,38 @@ class Idea extends Model {
352352
const cursor = await this.db.query(query, params);
353353
return await cursor.all();
354354
}
355+
356+
357+
/**
358+
* Read ideas with any of specified keywords in the title
359+
* @param {string[]} keywords - list of keywords to search with
360+
* @param {integer} offset - pagination offset
361+
* @param {integer} limit - pagination limit
362+
* @returns {Promise<Idea[]>} - list of found ideas
363+
*/
364+
static async findWithTitleKeywords(keywords, { offset, limit }) {
365+
const query = `
366+
FOR idea IN ideas
367+
LET search = ( FOR keyword in @keywords
368+
RETURN TO_NUMBER(CONTAINS(idea.title, keyword)))
369+
LET fit = SUM(search)
370+
FILTER fit > 0
371+
// find creator
372+
LET c = (DOCUMENT(idea.creator))
373+
// format for output
374+
LET creator = MERGE(KEEP(c, 'username'), c.profile)
375+
LET ideaOut = MERGE(KEEP(idea, 'title', 'detail', 'created'), { id: idea._key}, { creator }, {fit})
376+
// sort from newest
377+
SORT fit DESC, ideaOut.title
378+
// limit
379+
LIMIT @offset, @limit
380+
// respond
381+
RETURN ideaOut`;
382+
383+
const params = { 'keywords': keywords, offset, limit };
384+
const cursor = await this.db.query(query, params);
385+
return await cursor.all();
386+
}
355387
}
356388

357389

routes/ideas.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ router.route('/')
4848
router.route('/')
4949
.get(go.get.trending, authorize.onlyLogged, parse, ideaValidators.getIdeasTrending, ideaControllers.getIdeasTrending);
5050

51+
// get ideas with keywords
52+
router.route('/')
53+
.get(go.get.searchTitle, authorize.onlyLogged, parse, ideaValidators.getIdeasSearchTitle, ideaControllers.getIdeasSearchTitle);
54+
55+
5156
router.route('/:id')
5257
// read idea by id
5358
.get(authorize.onlyLogged, ideaValidators.get, ideaControllers.get)

test/ideas.list.js

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1006,4 +1006,143 @@ describe('read lists of ideas', () => {
10061006
});
10071007
});
10081008
});
1009+
1010+
describe('GET /ideas?filter[title][like]=string1,string2,string3', () => {
1011+
let user0;
1012+
// create and save testing data
1013+
beforeEach(async () => {
1014+
const data = {
1015+
users: 2,
1016+
verifiedUsers: [0],
1017+
ideas: [ [{title:'idea-title1'}, 0], [{title:'idea-title2-keyword1'}, 0], [{title:'idea-title3-keyword2'}, 0], [{title:'idea-title4-keyword3'}, 0], [{title:'idea-title5-keyword2-keyword3'}, 0], [{title:'idea-title6-keyword1'}, 0], [{title:'idea-title7-keyword1-keyword4'}, 0] ]
1018+
};
1019+
1020+
dbData = await dbHandle.fill(data);
1021+
1022+
[user0, ] = dbData.users;
1023+
});
1024+
1025+
context('logged in', () => {
1026+
1027+
beforeEach(() => {
1028+
agent = agentFactory.logged(user0);
1029+
});
1030+
1031+
context('valid data', () => {
1032+
1033+
it('[find ideas with one word] 200 and return array of matched ideas', async () => {
1034+
1035+
// request
1036+
const response = await agent
1037+
.get('/ideas?filter[title][like]=keyword1')
1038+
.expect(200);
1039+
1040+
// we should find 2 ideas...
1041+
should(response.body).have.property('data').Array().length(3);
1042+
1043+
// sorted by creation date desc
1044+
should(response.body.data.map(idea => idea.attributes.title))
1045+
.eql(['idea-title2-keyword1','idea-title6-keyword1', 'idea-title7-keyword1-keyword4']);
1046+
1047+
});
1048+
1049+
1050+
it('[find ideas with two words] 200 and return array of matched ideas', async () => {
1051+
1052+
// request
1053+
const response = await agent
1054+
.get('/ideas?filter[title][like]=keyword2,keyword3')
1055+
.expect(200);
1056+
1057+
// we should find 4 ideas...
1058+
should(response.body).have.property('data').Array().length(3);
1059+
1060+
// sorted by creation date desc
1061+
should(response.body.data.map(idea => idea.attributes.title))
1062+
.eql(['idea-title5-keyword2-keyword3', 'idea-title3-keyword2', 'idea-title4-keyword3']);
1063+
});
1064+
1065+
it('[find ideas with word not present in any] 200 and return array of matched ideas', async () => {
1066+
1067+
// request
1068+
const response = await agent
1069+
.get('/ideas?filter[title][like]=keyword10')
1070+
.expect(200);
1071+
1072+
// we should find 0 ideas...
1073+
should(response.body).have.property('data').Array().length(0);
1074+
1075+
});
1076+
1077+
it('[pagination] offset and limit the results', async () => {
1078+
const response = await agent
1079+
.get('/ideas?filter[title][like]=keyword1&page[offset]=1&page[limit]=2')
1080+
.expect(200);
1081+
1082+
// we should find 3 ideas
1083+
should(response.body).have.property('data').Array().length(2);
1084+
1085+
// sorted by creation date desc
1086+
should(response.body.data.map(idea => idea.attributes.title))
1087+
.eql(['idea-title6-keyword1', 'idea-title7-keyword1-keyword4']);
1088+
});
1089+
1090+
it('should be fine to provide a keyword which includes empty spaces and/or special characters', async () => {
1091+
// request
1092+
await agent
1093+
.get('/ideas?filter[title][like]=keyword , aa,1-i')
1094+
.expect(200);
1095+
});
1096+
1097+
});
1098+
1099+
context('invalid data', () => {
1100+
1101+
it('[too many keywords] 400', async () => {
1102+
await agent
1103+
.get('/ideas?filter[title][like]=keyword1,keyword2,keyword3,keyword4,keyword5,keyword6,keyword7,keyword8,keyword9,keyword10,keyword11')
1104+
.expect(400);
1105+
});
1106+
1107+
it('[empty keywords] 400', async () => {
1108+
await agent
1109+
.get('/ideas?filter[title][like]=keyword1,')
1110+
.expect(400);
1111+
});
1112+
1113+
it('[too long keywords] 400', async () => {
1114+
await agent
1115+
.get(`/ideas?filter[title][like]=keyword1,${'a'.repeat(257)}`)
1116+
.expect(400);
1117+
});
1118+
1119+
it('[keywords spaces only] 400', async () => {
1120+
await agent
1121+
.get('/ideas?filter[title][like]= ,keyword2')
1122+
.expect(400);
1123+
});
1124+
1125+
it('[invalid pagination] 400', async () => {
1126+
await agent
1127+
.get('/ideas?filter[title][like]=keyword1&page[offset]=1&page[limit]=21')
1128+
.expect(400);
1129+
});
1130+
1131+
it('[unexpected query params] 400', async () => {
1132+
await agent
1133+
.get('/ideas?filter[title][like]=keyword1&additional[param]=3&page[offset]=1&page[limit]=3')
1134+
.expect(400);
1135+
});
1136+
});
1137+
});
1138+
1139+
context('not logged in', () => {
1140+
it('403', async () => {
1141+
await agent
1142+
.get('/ideas?filter[title][like]=keyword1')
1143+
.expect(403);
1144+
});
1145+
});
1146+
});
1147+
10091148
});

0 commit comments

Comments
 (0)