Skip to content

Commit 85ab047

Browse files
committed
winning submission for challenge 30083089 - Topcoder Connect - Filter project table by columns, fix for issue #245
1 parent fb45942 commit 85ab047

File tree

6 files changed

+425
-26
lines changed

6 files changed

+425
-26
lines changed

migrations/elasticsearch_sync.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,8 @@ function getRequestBody(indexName) {
209209
},
210210
},
211211
id: {
212-
type: 'long',
212+
type: 'string',
213+
index: 'not_analyzed',
213214
},
214215
members: {
215216
type: 'nested',

postman.json

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1485,6 +1485,120 @@
14851485
},
14861486
"response": []
14871487
},
1488+
{
1489+
"name": "List projects with filters - name, code, customer, manager",
1490+
"request": {
1491+
"url": {
1492+
"raw": "{{api-url}}/v4/projects?filter=id%3D1*%26name%3Dtes*%26code=test*%26customer%3DDiya*%26manager=first*",
1493+
"host": [
1494+
"{{api-url}}"
1495+
],
1496+
"path": [
1497+
"v4",
1498+
"projects"
1499+
],
1500+
"query": [
1501+
{
1502+
"key": "filter",
1503+
"value": "id%3D1*%26name%3Dtes*%26code=test*%26customer%3DDiya*%26manager=first*",
1504+
"equals": true,
1505+
"description": ""
1506+
}
1507+
],
1508+
"variable": []
1509+
},
1510+
"method": "GET",
1511+
"header": [
1512+
{
1513+
"key": "Authorization",
1514+
"value": "Bearer {{jwt-token}}",
1515+
"description": ""
1516+
}
1517+
],
1518+
"body": {
1519+
"mode": "raw",
1520+
"raw": ""
1521+
},
1522+
"description": "List all the project with filters applied. The filter string should be url encoded. Default limit and offset is applicable"
1523+
},
1524+
"response": []
1525+
},
1526+
{
1527+
"name": "List projects with filters - id",
1528+
"request": {
1529+
"url": {
1530+
"raw": "{{api-url}}/v4/projects?filter=id%3D2",
1531+
"host": [
1532+
"{{api-url}}"
1533+
],
1534+
"path": [
1535+
"v4",
1536+
"projects"
1537+
],
1538+
"query": [
1539+
{
1540+
"key": "filter",
1541+
"value": "id%3D2",
1542+
"equals": true,
1543+
"description": ""
1544+
}
1545+
],
1546+
"variable": []
1547+
},
1548+
"method": "GET",
1549+
"header": [
1550+
{
1551+
"key": "Authorization",
1552+
"value": "Bearer {{jwt-token}}",
1553+
"description": ""
1554+
}
1555+
],
1556+
"body": {
1557+
"mode": "raw",
1558+
"raw": ""
1559+
},
1560+
"description": "List all the project with filters applied. The filter string should be url encoded. Default limit and offset is applicable"
1561+
},
1562+
"response": []
1563+
},
1564+
{
1565+
"name": "List projects with filters - code",
1566+
"request": {
1567+
"url": {
1568+
"raw": "{{api-url}}/v4/projects?filter=code%3Dtest*",
1569+
"host": [
1570+
"{{api-url}}"
1571+
],
1572+
"path": [
1573+
"v4",
1574+
"projects"
1575+
],
1576+
"query": [
1577+
{
1578+
"key": "filter",
1579+
"value": "code%3Dtest*",
1580+
"equals": true,
1581+
"description": ""
1582+
}
1583+
],
1584+
"variable": []
1585+
},
1586+
"method": "GET",
1587+
"header": [
1588+
{
1589+
"key": "Authorization",
1590+
"value": "Bearer {{jwt-token}}",
1591+
"description": ""
1592+
}
1593+
],
1594+
"body": {
1595+
"mode": "raw",
1596+
"raw": ""
1597+
},
1598+
"description": "List all the project with filters applied. The filter string should be url encoded. Default limit and offset is applicable"
1599+
},
1600+
"response": []
1601+
},
14881602
{
14891603
"name": "List projects with sort applied",
14901604
"request": {
@@ -5395,4 +5509,4 @@
53955509
]
53965510
}
53975511
]
5398-
}
5512+
}

src/routes/projects/list.js

Lines changed: 121 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,87 @@ const buildEsFullTextQuery = (keyword, matchType, singleFieldName) => {
102102
};
103103
};
104104

105+
/**
106+
* Build ES query search request body based on value, keyword, matchType and fieldName
107+
*
108+
* @param {String} value the value to build request body for
109+
* @param {String} keyword the keyword to query
110+
* @param {String} matchType wildcard match or exact match
111+
* @param {Array} fieldName the fieldName
112+
* @return {Object} search request body that can be passed to .search api call
113+
*/
114+
const buildEsQueryWithFilter = (value, keyword, matchType, fieldName) => {
115+
let should = [];
116+
if (value !== 'details' && value !== 'customer' && value !== 'manager') {
117+
should = _.concat(should, {
118+
query_string: {
119+
query: keyword,
120+
analyze_wildcard: (matchType === MATCH_TYPE_WILDCARD),
121+
fields: fieldName,
122+
},
123+
});
124+
}
125+
126+
if (value === 'details') {
127+
should = _.concat(should, {
128+
nested: {
129+
path: 'details',
130+
query: {
131+
nested: {
132+
path: 'details.utm',
133+
query: {
134+
query_string: {
135+
query: keyword,
136+
analyze_wildcard: (matchType === MATCH_TYPE_WILDCARD),
137+
fields: fieldName,
138+
},
139+
},
140+
},
141+
},
142+
},
143+
});
144+
}
145+
146+
if (value === 'customer' || value === 'manager') {
147+
should = _.concat(should, {
148+
nested: {
149+
path: 'members',
150+
query: {
151+
bool: {
152+
must: [
153+
{ match: { 'members.role': value } },
154+
{
155+
query_string: {
156+
query: keyword,
157+
analyze_wildcard: (matchType === MATCH_TYPE_WILDCARD),
158+
fields: fieldName,
159+
},
160+
},
161+
],
162+
},
163+
},
164+
},
165+
});
166+
}
167+
168+
return should;
169+
};
170+
171+
/**
172+
* Prepare search request body based on wildcard query
173+
*
174+
* @param {String} value the value to build request body for
175+
* @param {String} keyword the keyword to query
176+
* @param {Array} fieldName the fieldName
177+
* @return {Object} search request body that can be passed to .search api call
178+
*/
179+
const setFilter = (value, keyword, fieldName) => {
180+
if (keyword.indexOf('*') > -1) {
181+
return buildEsQueryWithFilter(value, keyword, MATCH_TYPE_WILDCARD, fieldName);
182+
}
183+
return buildEsQueryWithFilter(value, keyword, MATCH_TYPE_EXACT_PHRASE, fieldName);
184+
};
185+
105186
/**
106187
* Parse the ES search criteria and prepare search request body
107188
*
@@ -152,13 +233,42 @@ const parseElasticSearchCriteria = (criteria, fields, order) => {
152233
}
153234
// prepare the elasticsearch filter criteria
154235
const boolQuery = [];
236+
let mustQuery = [];
155237
let fullTextQuery;
156238
if (_.has(criteria, 'filters.id.$in')) {
157239
boolQuery.push({
158240
ids: {
159241
values: criteria.filters.id.$in,
160242
},
161243
});
244+
} else if (_.has(criteria, 'filters.id') && criteria.filters.id.indexOf('*') > -1) {
245+
mustQuery = _.concat(mustQuery, buildEsQueryWithFilter('id', criteria.filters.id, MATCH_TYPE_WILDCARD, ['id']));
246+
} else if (_.has(criteria, 'filters.id')) {
247+
boolQuery.push({
248+
term: {
249+
id: criteria.filters.id,
250+
},
251+
});
252+
}
253+
254+
if (_.has(criteria, 'filters.name')) {
255+
mustQuery = _.concat(mustQuery, setFilter('name', criteria.filters.name, ['name']));
256+
}
257+
258+
if (_.has(criteria, 'filters.code')) {
259+
mustQuery = _.concat(mustQuery, setFilter('details', criteria.filters.code, ['details.utm.code']));
260+
}
261+
262+
if (_.has(criteria, 'filters.customer')) {
263+
mustQuery = _.concat(mustQuery, setFilter('customer',
264+
criteria.filters.customer,
265+
['members.firstName', 'members.lastName']));
266+
}
267+
268+
if (_.has(criteria, 'filters.manager')) {
269+
mustQuery = _.concat(mustQuery, setFilter('manager',
270+
criteria.filters.manager,
271+
['members.firstName', 'members.lastName']));
162272
}
163273

164274
if (_.has(criteria, 'filters.status.$in')) {
@@ -177,21 +287,6 @@ const parseElasticSearchCriteria = (criteria, fields, order) => {
177287
});
178288
}
179289

180-
if (_.has(criteria, 'filters.type.$in')) {
181-
// type is an array
182-
boolQuery.push({
183-
terms: {
184-
type: criteria.filters.type.$in,
185-
},
186-
});
187-
} else if (_.has(criteria, 'filters.type')) {
188-
// type is simple string
189-
boolQuery.push({
190-
term: {
191-
type: criteria.filters.type,
192-
},
193-
});
194-
}
195290
if (_.has(criteria, 'filters.keyword')) {
196291
// keyword is a full text search
197292
// escape special fields from keyword search
@@ -222,7 +317,7 @@ const parseElasticSearchCriteria = (criteria, fields, order) => {
222317

223318
if (!keyword) {
224319
// Not a specific field search nor an exact phrase search, do a wildcard match
225-
keyword = escapeEsKeyword(criteria.filters.keyword);
320+
keyword = criteria.filters.keyword;
226321
matchType = MATCH_TYPE_WILDCARD;
227322
}
228323

@@ -234,17 +329,22 @@ const parseElasticSearchCriteria = (criteria, fields, order) => {
234329
filter: boolQuery,
235330
};
236331
}
332+
333+
if (mustQuery.length > 0) {
334+
body.query.bool = _.merge(body.query.bool, {
335+
must: mustQuery,
336+
});
337+
}
237338
if (fullTextQuery) {
238339
body.query = _.merge(body.query, fullTextQuery);
239340
if (body.query.bool) {
240341
body.query.bool.minimum_should_match = 1;
241342
}
242343
}
243344

244-
if (fullTextQuery || boolQuery.length > 0) {
345+
if (fullTextQuery || boolQuery.length > 0 || mustQuery.length > 0) {
245346
searchCriteria.body = body;
246347
}
247-
248348
return searchCriteria;
249349
};
250350

@@ -267,8 +367,7 @@ const retrieveProjects = (req, criteria, sort, ffields) => {
267367
fields.projects.push('id');
268368
}
269369

270-
const searchCriteria = parseElasticSearchCriteria(criteria, fields, order);
271-
370+
const searchCriteria = parseElasticSearchCriteria(criteria, fields, order) || {};
272371
return new Promise((accept, reject) => {
273372
const es = util.getElasticSearchClient();
274373
es.search(searchCriteria).then((docs) => {
@@ -300,7 +399,8 @@ module.exports = [
300399
'name', 'name asc', 'name desc',
301400
'type', 'type asc', 'type desc',
302401
];
303-
if (!util.isValidFilter(filters, ['id', 'status', 'type', 'memberOnly', 'keyword']) ||
402+
if (!util.isValidFilter(filters,
403+
['id', 'status', 'memberOnly', 'keyword', 'name', 'code', 'customer', 'manager']) ||
304404
(sort && _.indexOf(sortableProps, sort) < 0)) {
305405
return util.handleError('Invalid filters or sort', null, req, next);
306406
}

0 commit comments

Comments
 (0)