@momsfriendlydevco/sift-sieve 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -0
- package/index.js +282 -19
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -41,3 +41,38 @@ Options are passed directly to Sift with the following exceptions:
|
|
|
41
41
|
|--------------|-----------|---------|----------------------------------------------------------------------------------------|
|
|
42
42
|
| `remapArray` | `boolean` | `true` | Whether to transform simple `{key: Array}` keys into `{key: {$in: Array}}` |
|
|
43
43
|
| `count` | `boolean` | `false` | Whether to return a single Number of the matching elements rather than the found items |
|
|
44
|
+
| `applyWhere` | `boolean` | `false` | Whether to allow `$where` conditions and function values (at any depth) - unless exactly `true` any query containing either throws an error |
|
|
45
|
+
| `applyUnsafeRegex` | `boolean` | `false` | Whether to allow regular expressions prone to catastrophic backtracking (ReDoS), detected via [safe-regex2](https://github.com/fastify/safe-regex2) plus a check for alternation inside large repetitions such as `(a\|a)*` - unless exactly `true` any query containing one throws an error |
|
|
46
|
+
| `allowFields` | `Array<string>` | `null` | If set, only these (dotted) field paths and their sub-paths may be filtered or sorted on. Containers of allowed fields may be queried via `$elemMatch`. Does not restrict `select` |
|
|
47
|
+
| `literalFields` | `Array<string>` | `[]` | Field paths which only accept literal values (strings, numbers, booleans, null or arrays of these) - operators, objects and RegExps are rejected, as are sub-paths of these fields |
|
|
48
|
+
| `maxLimit` | `number` | `0` | If set, the maximum number of results returned, also applied when no `limit` is given. Not applied when `count` is set |
|
|
49
|
+
| `maxDepth` | `number` | `16` | Maximum nesting depth of the query, `0` to disable |
|
|
50
|
+
| `maxKeys` | `number` | `500` | Maximum total number of object keys within the query, `0` to disable |
|
|
51
|
+
| `maxArrayLength` | `number` | `500` | Maximum length of any array within the query, `0` to disable |
|
|
52
|
+
| `maxRegexLength` | `number` | `256` | Maximum length of any regular expression source, applied even if `applyUnsafeRegex` is set, `0` to disable |
|
|
53
|
+
|
|
54
|
+
Queries must be plain objects (or empty), any other type throws an error.
|
|
55
|
+
|
|
56
|
+
The following are also validated:
|
|
57
|
+
|
|
58
|
+
* `limit` / `skip` must be non-negative integers or numeric strings
|
|
59
|
+
* `select` / `sort` must be strings (CSV) or arrays of strings
|
|
60
|
+
* Field paths may never contain the `__proto__`, `constructor` or `prototype` segments
|
|
61
|
+
|
|
62
|
+
Note that `limit` / `skip` are applied before `sort`.
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
Errors
|
|
66
|
+
------
|
|
67
|
+
Any invalid, unsafe or disallowed query throws a `SieveQueryError` (exported as `sieve.SieveQueryError`) which has `status` and `statusCode` set to `400` so it can be handed directly to an ExpressJS style error handler. Error messages only ever contain truncated user input and the underlying error (if any) is available as `err.cause`.
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
Security
|
|
71
|
+
--------
|
|
72
|
+
When feeding user input such as `req.query` or `req.body` directly into sieve:
|
|
73
|
+
|
|
74
|
+
* **Always** set `allowFields` - otherwise any field can be filtered on, letting an attacker infer hidden values (e.g. `?passwordHash[$regex]=^a` with the result count acting as an oracle) even if the field is removed via `select`
|
|
75
|
+
* Set `literalFields` for any field used for lookups or authentication (e.g. tokens) - otherwise `?token[$ne]=x` matches every record
|
|
76
|
+
* Set `maxLimit` to stop bulk extraction
|
|
77
|
+
* Query string values are always strings, `?age=22` will not match the number `22`
|
|
78
|
+
* Repeated keys (`?name=a&name=b`) become arrays which are remapped to `$in` unless `remapArray` is disabled
|
package/index.js
CHANGED
|
@@ -1,52 +1,313 @@
|
|
|
1
1
|
var _ = require('lodash');
|
|
2
|
+
var ret = require('ret');
|
|
3
|
+
var safeRegex = require('safe-regex2');
|
|
2
4
|
var sift = require('sift');
|
|
3
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Error thrown for any invalid, unsafe or disallowed query
|
|
8
|
+
* Carries `status` / `statusCode` of 400 so it can be passed directly to an ExpressJS style error handler
|
|
9
|
+
*/
|
|
10
|
+
class SieveQueryError extends Error {
|
|
11
|
+
/**
|
|
12
|
+
* @param {string} message The error message, this should not contain unescaped or untruncated user input
|
|
13
|
+
* @param {Object} [options] Additional Error options
|
|
14
|
+
* @param {Error} [options.cause] The underlying error, if any
|
|
15
|
+
*/
|
|
16
|
+
constructor(message, options) {
|
|
17
|
+
super(message, options);
|
|
18
|
+
this.name = 'SieveQueryError';
|
|
19
|
+
this.status = 400;
|
|
20
|
+
this.statusCode = 400;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Option keys consumed by sieve which should not be passed onto Sift
|
|
27
|
+
* @type {Array<string>}
|
|
28
|
+
*/
|
|
29
|
+
var sieveOptions = ['remapArray', 'count', 'applyWhere', 'applyUnsafeRegex', 'allowFields', 'literalFields', 'maxLimit', 'maxDepth', 'maxKeys', 'maxArrayLength', 'maxRegexLength'];
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Path segments which are never permitted within a field path as they reach into the prototype chain
|
|
34
|
+
* @type {Array<string>}
|
|
35
|
+
*/
|
|
36
|
+
var blockedSegments = ['__proto__', 'constructor', 'prototype'];
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Logical operators whose values are arrays of sub-queries
|
|
41
|
+
* @type {Array<string>}
|
|
42
|
+
*/
|
|
43
|
+
var logicalOperators = ['$and', '$or', '$nor'];
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Safely quote user input for inclusion within an error message
|
|
48
|
+
* @param {*} value The value to quote
|
|
49
|
+
* @returns {string} The JSON quoted and truncated value
|
|
50
|
+
*/
|
|
51
|
+
var quote = value => JSON.stringify(_.truncate(String(value), {length: 64}));
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Determine if a numeric limit setting is active
|
|
56
|
+
* @param {*} limit The limit setting
|
|
57
|
+
* @returns {boolean} True if the limit should be enforced
|
|
58
|
+
*/
|
|
59
|
+
var isLimited = limit => _.isFinite(limit) && limit > 0;
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Determine if a parsed regular expression token tree contains an alternation group within an unbounded (or large) repetition
|
|
64
|
+
* This catches overlapping alternation such as `(a|a)*` which safe-regex2 does not detect
|
|
65
|
+
* @param {Object} token The `ret` token to examine
|
|
66
|
+
* @param {boolean} [inRepeat=false] Whether the token is already within a large repetition
|
|
67
|
+
* @returns {boolean} True if a quantified alternation was found
|
|
68
|
+
*/
|
|
69
|
+
var hasQuantifiedAlternation = (token, inRepeat = false) =>
|
|
70
|
+
!_.isObject(token) ? false
|
|
71
|
+
: token.type == ret.types.REPETITION ? hasQuantifiedAlternation(token.value, inRepeat || token.max > 10)
|
|
72
|
+
: token.options ? (inRepeat && token.options.length > 1) || token.options.some(option => option.some(t => hasQuantifiedAlternation(t, inRepeat)))
|
|
73
|
+
: token.stack ? token.stack.some(t => hasQuantifiedAlternation(t, inRepeat))
|
|
74
|
+
: false;
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Validate a regular expression (as a string or RegExp) against the settings
|
|
79
|
+
* @param {string|RegExp} pattern The regular expression to examine
|
|
80
|
+
* @param {Object} settings The sieve settings
|
|
81
|
+
* @returns {void}
|
|
82
|
+
* @throws {SieveQueryError} If the expression is too long, unparsable or unsafe
|
|
83
|
+
*/
|
|
84
|
+
var validateRegex = (pattern, settings) => {
|
|
85
|
+
var source = _.isRegExp(pattern) ? pattern.source : pattern;
|
|
86
|
+
if (isLimited(settings.maxRegexLength) && source.length > settings.maxRegexLength) throw new SieveQueryError(`Regular expression exceeds the maximum length of ${settings.maxRegexLength} (\`maxRegexLength\` option)`);
|
|
87
|
+
if (settings.applyUnsafeRegex === true) return;
|
|
88
|
+
|
|
89
|
+
var tokens = _.attempt(()=> ret(source));
|
|
90
|
+
if (_.isError(tokens)) throw new SieveQueryError('Invalid regular expression', {cause: tokens});
|
|
91
|
+
if (!safeRegex(source) || hasQuantifiedAlternation(tokens)) throw new SieveQueryError('Unsafe regular expression is not permitted unless the `applyUnsafeRegex` option is enabled');
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Recursively validate the structure of a query at any depth, throwing if it contains anything not permitted by the settings
|
|
97
|
+
* Rejects `$where` operators and function values unless `applyWhere` is true, unsafe (ReDoS prone) regular expressions unless `applyUnsafeRegex` is true and anything exceeding the size limits
|
|
98
|
+
* @param {*} query The query (or sub-query) to examine
|
|
99
|
+
* @param {Object} settings The sieve settings
|
|
100
|
+
* @param {Object} [state] Running totals across the whole query
|
|
101
|
+
* @param {number} [state.keys=0] Number of object keys seen so far
|
|
102
|
+
* @param {number} [depth=1] The current nesting depth
|
|
103
|
+
* @returns {void}
|
|
104
|
+
* @throws {SieveQueryError} If the query contains a disallowed condition
|
|
105
|
+
*/
|
|
106
|
+
var validateQuery = (query, settings, state = {keys: 0}, depth = 1) => {
|
|
107
|
+
if (isLimited(settings.maxDepth) && depth > settings.maxDepth) throw new SieveQueryError(`Query exceeds the maximum nesting depth of ${settings.maxDepth} (\`maxDepth\` option)`);
|
|
108
|
+
|
|
109
|
+
if (_.isFunction(query)) {
|
|
110
|
+
if (settings.applyWhere !== true) throw new SieveQueryError('Function values are not permitted unless the `applyWhere` option is enabled');
|
|
111
|
+
} else if (_.isRegExp(query)) {
|
|
112
|
+
validateRegex(query, settings);
|
|
113
|
+
} else if (Array.isArray(query)) {
|
|
114
|
+
if (isLimited(settings.maxArrayLength) && query.length > settings.maxArrayLength) throw new SieveQueryError(`Query array exceeds the maximum length of ${settings.maxArrayLength} (\`maxArrayLength\` option)`);
|
|
115
|
+
query.forEach(q => validateQuery(q, settings, state, depth + 1));
|
|
116
|
+
} else if (_.isPlainObject(query)) {
|
|
117
|
+
var keys = Object.keys(query);
|
|
118
|
+
state.keys += keys.length;
|
|
119
|
+
if (isLimited(settings.maxKeys) && state.keys > settings.maxKeys) throw new SieveQueryError(`Query exceeds the maximum number of keys of ${settings.maxKeys} (\`maxKeys\` option)`);
|
|
120
|
+
|
|
121
|
+
keys.forEach(k => {
|
|
122
|
+
if (k == '$where' && settings.applyWhere !== true) throw new SieveQueryError('$where conditions are not permitted unless the `applyWhere` option is enabled');
|
|
123
|
+
if (k == '$regex' && _.isString(query[k])) validateRegex(query[k], settings);
|
|
124
|
+
validateQuery(query[k], settings, state, depth + 1);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Validate a single field path against the blocked segments and (optionally) the allowed fields
|
|
132
|
+
* @param {string} path The dotted field path
|
|
133
|
+
* @param {Object} settings The sieve settings
|
|
134
|
+
* @param {boolean} [checkAllowed=true] Whether to also check the path against `settings.allowFields`
|
|
135
|
+
* @returns {void}
|
|
136
|
+
* @throws {SieveQueryError} If the field is not permitted
|
|
137
|
+
*/
|
|
138
|
+
var validateField = (path, settings, checkAllowed = true) => {
|
|
139
|
+
if (path.split('.').some(segment => blockedSegments.includes(segment))) throw new SieveQueryError(`Field ${quote(path)} is not permitted`);
|
|
140
|
+
if (
|
|
141
|
+
checkAllowed
|
|
142
|
+
&& settings.allowFields
|
|
143
|
+
&& !settings.allowFields.some(field => path == field || path.startsWith(field + '.'))
|
|
144
|
+
) throw new SieveQueryError(`Field ${quote(path)} is not permitted (\`allowFields\` option)`);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Determine if a value is a primitive literal or an array of primitive literals
|
|
150
|
+
* @param {*} value The value to examine
|
|
151
|
+
* @returns {boolean} True if the value contains no operators, objects or expressions
|
|
152
|
+
*/
|
|
153
|
+
var isLiteral = value =>
|
|
154
|
+
Array.isArray(value) ? value.every(v => v === null || ['string', 'number', 'boolean'].includes(typeof v))
|
|
155
|
+
: value === null || ['string', 'number', 'boolean'].includes(typeof value);
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Validate the value given against a field path, recursing into any sub-queries
|
|
160
|
+
* @param {string} path The dotted field path
|
|
161
|
+
* @param {*} value The value or operator object given for the field
|
|
162
|
+
* @param {Object} settings The sieve settings
|
|
163
|
+
* @returns {void}
|
|
164
|
+
* @throws {SieveQueryError} If the field or its value is not permitted
|
|
165
|
+
*/
|
|
166
|
+
var validateFieldValue = (path, value, settings) => {
|
|
167
|
+
var isElemMatchContainer = _.isPlainObject(value)
|
|
168
|
+
&& _.isEqual(Object.keys(value), ['$elemMatch'])
|
|
169
|
+
&& settings.allowFields
|
|
170
|
+
&& settings.allowFields.some(field => field.startsWith(path + '.'));
|
|
171
|
+
|
|
172
|
+
validateField(path, settings, !isElemMatchContainer); // Containers of allowed fields may use $elemMatch, sub-fields are checked below
|
|
173
|
+
|
|
174
|
+
(settings.literalFields || []).forEach(field => {
|
|
175
|
+
if (path.startsWith(field + '.')) throw new SieveQueryError(`Field ${quote(path)} is not permitted (\`literalFields\` option)`);
|
|
176
|
+
if (
|
|
177
|
+
(path == field || field.startsWith(path + '.'))
|
|
178
|
+
&& !isLiteral(value)
|
|
179
|
+
) throw new SieveQueryError(`Field ${quote(path)} only accepts literal values (\`literalFields\` option)`);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
if (_.isPlainObject(value))
|
|
183
|
+
Object.keys(value).forEach(op => {
|
|
184
|
+
if (op == '$elemMatch' && _.isPlainObject(value[op])) {
|
|
185
|
+
validateFields(value[op], settings, path + '.');
|
|
186
|
+
} else if (op == '$not') {
|
|
187
|
+
validateFieldValue(path, value[op], settings);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Recursively validate all field paths used within a query
|
|
195
|
+
* @param {Object} query The query (or sub-query) to examine
|
|
196
|
+
* @param {Object} settings The sieve settings
|
|
197
|
+
* @param {string} [prefix=''] Prefix to prepend to all field paths, used within `$elemMatch`
|
|
198
|
+
* @returns {void}
|
|
199
|
+
* @throws {SieveQueryError} If any field or field value is not permitted
|
|
200
|
+
*/
|
|
201
|
+
var validateFields = (query, settings, prefix = '') =>
|
|
202
|
+
Object.keys(query).forEach(k => {
|
|
203
|
+
if (logicalOperators.includes(k)) {
|
|
204
|
+
_.values(query[k])
|
|
205
|
+
.filter(_.isPlainObject)
|
|
206
|
+
.forEach(subQuery => validateFields(subQuery, settings, prefix));
|
|
207
|
+
} else if (!k.startsWith('$')) {
|
|
208
|
+
validateFieldValue(prefix + k, query[k], settings);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Parse a limit / skip value into a non-negative integer
|
|
215
|
+
* @param {*} value The raw value, which may be a number or numeric string
|
|
216
|
+
* @param {string} name The name of the parameter, used in error messages
|
|
217
|
+
* @returns {number} The parsed integer, zero if not specified
|
|
218
|
+
* @throws {SieveQueryError} If the value is not a non-negative integer
|
|
219
|
+
*/
|
|
220
|
+
var parseCount = (value, name) =>
|
|
221
|
+
_.isNil(value) || value === false || value === '' ? 0
|
|
222
|
+
: Number.isSafeInteger(value) && value >= 0 ? value
|
|
223
|
+
: _.isString(value) && /^\d{1,15}$/.test(value) ? Number.parseInt(value, 10)
|
|
224
|
+
: (()=> { throw new SieveQueryError(`Invalid ${name}, must be a non-negative integer`) })();
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Parse a select / sort value into an array of field strings
|
|
229
|
+
* @param {*} value The raw value, which may be a CSV string or an array of strings
|
|
230
|
+
* @param {string} name The name of the parameter, used in error messages
|
|
231
|
+
* @returns {Array<string>} The parsed fields, empty if not specified
|
|
232
|
+
* @throws {SieveQueryError} If the value is not a string or array of strings
|
|
233
|
+
*/
|
|
234
|
+
var parseFieldList = (value, name) =>
|
|
235
|
+
_.isNil(value) || value === false ? []
|
|
236
|
+
: _.isString(value) ? value.split(/\s*,\s*/).filter(Boolean)
|
|
237
|
+
: Array.isArray(value) && value.every(_.isString) ? value.filter(Boolean)
|
|
238
|
+
: (()=> { throw new SieveQueryError(`Invalid ${name}, must be a string or array of strings`) })();
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Filter, sort, page and select an array of data using a Sift / Mongo style query
|
|
243
|
+
* @param {Array} data The data to filter
|
|
244
|
+
* @param {Object} [query] The query to apply, may include the reserved `select`, `sort`, `limit` and `skip` keys
|
|
245
|
+
* @param {Object} [options] Options, see the README for the full list. Any unrecognised options are passed onto Sift
|
|
246
|
+
* @returns {Array|number} The filtered data or, if `options.count` is set, the number of matching items
|
|
247
|
+
* @throws {SieveQueryError} If the query is invalid, unsafe or disallowed
|
|
248
|
+
*/
|
|
4
249
|
module.exports = (data, query, options) => {
|
|
5
250
|
var settings = {
|
|
6
251
|
remapArray: true,
|
|
7
252
|
count: false,
|
|
253
|
+
applyWhere: false,
|
|
254
|
+
applyUnsafeRegex: false,
|
|
255
|
+
allowFields: null,
|
|
256
|
+
literalFields: [],
|
|
257
|
+
maxLimit: 0,
|
|
258
|
+
maxDepth: 16,
|
|
259
|
+
maxKeys: 500,
|
|
260
|
+
maxArrayLength: 500,
|
|
261
|
+
maxRegexLength: 256,
|
|
8
262
|
...options,
|
|
9
263
|
};
|
|
10
264
|
|
|
11
265
|
if (!_.isArray(data)) throw new Error('Input to sieve must be an array');
|
|
266
|
+
if (query && !_.isPlainObject(query)) throw new SieveQueryError('Query to sieve must be an object');
|
|
267
|
+
if (!query) query = {};
|
|
12
268
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
269
|
+
var limit = parseCount(query.limit, 'limit');
|
|
270
|
+
var skip = parseCount(query.skip, 'skip');
|
|
271
|
+
if (!settings.count && isLimited(settings.maxLimit) && (!limit || limit > settings.maxLimit)) limit = settings.maxLimit;
|
|
272
|
+
|
|
273
|
+
var sortFields = parseFieldList(query.sort, 'sort');
|
|
274
|
+
sortFields.forEach(field => validateField(field.replace(/^[+-]/, ''), settings));
|
|
275
|
+
|
|
276
|
+
var selectFields = parseFieldList(query.select, 'select');
|
|
277
|
+
selectFields.forEach(field => validateField(field.replace(/^[!-]/, ''), settings, false));
|
|
22
278
|
|
|
23
279
|
var siftFilter = _.omit(query, ['select', 'sort', 'limit', 'skip']);
|
|
24
280
|
|
|
281
|
+
validateQuery(siftFilter, settings);
|
|
282
|
+
validateFields(siftFilter, settings);
|
|
283
|
+
|
|
25
284
|
// Remap {id: ARRAY} into {id: {$in: ARRAY}} {{{
|
|
26
285
|
if (settings.remapArray)
|
|
27
286
|
Object.keys(siftFilter).forEach(k => {
|
|
28
|
-
if (Array.isArray(siftFilter[k]))
|
|
287
|
+
if (Array.isArray(siftFilter[k]) && !k.startsWith('$'))
|
|
29
288
|
siftFilter[k] = {$in: siftFilter[k]};
|
|
30
289
|
});
|
|
31
290
|
// }}}
|
|
32
291
|
|
|
33
|
-
var siftQuery = sift(siftFilter, options);
|
|
292
|
+
var siftQuery = _.attempt(()=> sift(siftFilter, _.omit(options, sieveOptions)));
|
|
293
|
+
if (_.isError(siftQuery)) throw new SieveQueryError(`Invalid query: ${quote(siftQuery.message)}`, {cause: siftQuery});
|
|
34
294
|
|
|
35
295
|
var count = 0; // Number of items that test positive in the output so far
|
|
36
296
|
var skipped = 0;
|
|
37
297
|
var res = data.filter(item => {
|
|
38
298
|
// Apply limit {{{
|
|
39
|
-
if (
|
|
299
|
+
if (limit && count >= limit) { // Gone over limit?
|
|
40
300
|
return false;
|
|
41
301
|
}
|
|
42
302
|
// }}}
|
|
43
303
|
|
|
44
304
|
// Apply Sift (filters) {{{
|
|
45
|
-
var siftResult = siftQuery
|
|
305
|
+
var siftResult = _.attempt(siftQuery, item);
|
|
306
|
+
if (_.isError(siftResult)) throw new SieveQueryError(`Invalid query: ${quote(siftResult.message)}`, {cause: siftResult});
|
|
46
307
|
// }}}
|
|
47
308
|
|
|
48
309
|
// Apply skip {{{
|
|
49
|
-
if (siftResult &&
|
|
310
|
+
if (siftResult && skip && ++skipped <= skip) {
|
|
50
311
|
return false; // Still within skip range
|
|
51
312
|
} else if (siftResult) {
|
|
52
313
|
count++;
|
|
@@ -60,8 +321,8 @@ module.exports = (data, query, options) => {
|
|
|
60
321
|
if (settings.count) return res.length;
|
|
61
322
|
|
|
62
323
|
// Apply sort {{{
|
|
63
|
-
if (
|
|
64
|
-
res = _.orderBy(res, ..._(
|
|
324
|
+
if (sortFields.length) {
|
|
325
|
+
res = _.orderBy(res, ..._(sortFields)
|
|
65
326
|
.map(o =>
|
|
66
327
|
o.startsWith('-') ? [o.substr(1), 'desc']
|
|
67
328
|
: o.startsWith('+') ? [o.substr(1), 'asc']
|
|
@@ -74,15 +335,15 @@ module.exports = (data, query, options) => {
|
|
|
74
335
|
// }}}
|
|
75
336
|
|
|
76
337
|
// Apply select {{{
|
|
77
|
-
if (
|
|
78
|
-
var fields =
|
|
338
|
+
if (selectFields.length) {
|
|
339
|
+
var fields = selectFields;
|
|
79
340
|
|
|
80
341
|
// Act as an exclusion selection?
|
|
81
342
|
var isExclude = fields.some(field => field.startsWith('!') || field.startsWith('-'));
|
|
82
343
|
|
|
83
344
|
if (isExclude) {
|
|
84
345
|
fields = fields.map(field => {
|
|
85
|
-
if (!field.startsWith('!') && !field.startsWith('-')) throw new
|
|
346
|
+
if (!field.startsWith('!') && !field.startsWith('-')) throw new SieveQueryError('Invalid selection criteria, if using a select exclusion all fields must be exclusions');
|
|
86
347
|
return field.substr(1);
|
|
87
348
|
});
|
|
88
349
|
|
|
@@ -95,3 +356,5 @@ module.exports = (data, query, options) => {
|
|
|
95
356
|
|
|
96
357
|
return res;
|
|
97
358
|
};
|
|
359
|
+
|
|
360
|
+
module.exports.SieveQueryError = SieveQueryError;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@momsfriendlydevco/sift-sieve",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Wrapper around Sift which provides other ReST functionality",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -22,10 +22,13 @@
|
|
|
22
22
|
"homepage": "https://github.com/MomsFriendlyDevCo/sift-sieve#readme",
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"chai": "^4.2.0",
|
|
25
|
-
"mocha": "^8.2.1"
|
|
25
|
+
"mocha": "^8.2.1",
|
|
26
|
+
"qs": "6.16.1"
|
|
26
27
|
},
|
|
27
28
|
"dependencies": {
|
|
28
29
|
"lodash": "^4.18.1",
|
|
30
|
+
"ret": "^0.5.1",
|
|
31
|
+
"safe-regex2": "^5.1.2",
|
|
29
32
|
"sift": "^17.1.4"
|
|
30
33
|
}
|
|
31
34
|
}
|