-
Notifications
You must be signed in to change notification settings - Fork 13
/
parseSearchQuery.js
134 lines (110 loc) · 5.15 KB
/
parseSearchQuery.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
'use strict';
const Joi = require('joi');
const normalizeNumber = require('normalize-number');
const searchQueryParser = require('search-query-parser');
const deepCompact = require('deep-compact');
const mapKeys = require('lodash/mapKeys');
const unset = require('lodash/unset');
const camelCase = require('lodash/camelCase');
const uniq = require('lodash/uniq');
const assignWith = require('lodash/assignWith');
const qualifiers = [
'author', 'maintainer', 'scope', 'keywords', 'not', 'is', 'boost-exact',
'score-effect', 'popularity-weight', 'quality-weight', 'maintenance-weight',
];
const paramsDefaults = {
text: null,
author: null,
maintainer: null,
scope: null,
keywords: [],
not: [],
is: [],
boostExact: true,
scoreEffect: 13.5,
qualityWeight: 1.9, // ~0.27
popularityWeight: 3.3, // ~0.45
maintenanceWeight: 2.1, // ~0.28
};
/* eslint-disable newline-per-chained-call */
const paramsSchema = Joi.object({
text: Joi.string().trim().lowercase().min(1).max(250), // Trim + lowercase so that the exact boost works correctly
author: Joi.string().trim().lowercase().min(1).max(250), // Trim + lowercase to mimic the raw analyzer
scope: Joi.string().trim().lowercase().min(1).max(250), // Trim + lowercase to mimic the raw analyzer
maintainer: Joi.string().trim().lowercase().min(1).max(250), // Trim + lowercase to mimic the raw analyzer
keywords: Joi.array().items(Joi.string().trim().lowercase().min(1).max(50)).min(1).max(10).single(), // Trim + lowercase to mimic the raw analyzer
not: Joi.array().items(Joi.string().trim().valid(['deprecated', 'unstable', 'insecure'])).single(),
is: Joi.array().items(Joi.string().trim().valid(['deprecated', 'unstable', 'insecure'])).single(),
boostExact: Joi.boolean().truthy('yes', '1', 1, 'on').falsy('no', '0', 0, 'off'),
scoreEffect: Joi.number().min(0).max(25),
qualityWeight: Joi.number().min(0).max(100),
popularityWeight: Joi.number().min(0).max(100),
maintenanceWeight: Joi.number().min(0).max(100),
}).required();
/* eslint-enable newline-per-chained-call */
const mandatoryParams = ['text', 'author', 'maintainer', 'scope', 'keywords', 'not', 'is'];
function normalizeWeights(params) {
const minMax = [0, params.qualityWeight + params.popularityWeight + params.maintenanceWeight];
params.qualityWeight = normalizeNumber(minMax, params.qualityWeight);
params.popularityWeight = normalizeNumber(minMax, params.popularityWeight);
params.maintenanceWeight = normalizeNumber(minMax, params.maintenanceWeight);
}
function parseKeywords(params) {
const include = [];
const exclude = [];
params.keywords.forEach((keyword) => {
if (keyword.indexOf('-') === 0) {
exclude.push(keyword.substr(1));
} else {
include.push(keyword);
}
});
params.keywords = { include: uniq(include), exclude: uniq(exclude) };
params.keywords.exclude = params.keywords.exclude.filter((keyword) => params.keywords.include.indexOf(keyword) === -1);
}
function uniquifyFlags(params) {
params.not = uniq(params.not);
params.is = uniq(params.is).filter((flag) => params.not.indexOf(flag) === -1);
}
function hasMandatoryParams(params) {
params = deepCompact(params);
return mandatoryParams.some((paramName) => params[paramName] != null);
}
// -------------------------------------------------
function parseSearchQuery(query, options) {
options = Object.assign({ throwOnInvalid: false }, options);
// Parse query into object
query = searchQueryParser.parse(query, { keywords: qualifiers });
query = typeof query === 'string' ? { text: query } : query;
query = mapKeys(query, (value, key) => camelCase(key)); // CamelCase keys
delete query.exclude; // We do not use the exclusion feature from search-query-parser
delete query.offsets; // Remove `offsets` otherwise validation will fail
// Convert & validate
const validation = Joi.validate(query, paramsSchema, { abortEarly: options.throwOnInvalid });
let params = validation.value;
if (validation.error) {
// Throw if there's an error and `options.throwOnInvalid` is enabled
if (options.throwOnInvalid && validation.error) {
throw validation.error;
}
// If `options.throwOnInvalid` is enabled, remove all failed validation props and fill it with defaults
validation.error.details
.reverse()
.forEach((detail) => unset(params, detail.path));
params = deepCompact(params);
}
// Fill in defaults
params = assignWith(params, paramsDefaults, (tartgetValue, srcValue) => tartgetValue == null ? srcValue : tartgetValue);
// Post process
normalizeWeights(params);
parseKeywords(params);
uniquifyFlags(params);
// If no mandatory params were specified (that actually filter results) then we return null
return hasMandatoryParams(params) ? params : null;
}
function discardQualifiers(query) {
query = searchQueryParser.parse(query, { keywords: qualifiers });
return (typeof query === 'string' ? query : query.text || '').trim();
}
module.exports = parseSearchQuery;
module.exports.discardQualifiers = discardQualifiers;