@e22m4u/js-repository 0.6.1 → 0.6.3
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 +299 -70
- package/dist/cjs/index.cjs +188 -139
- package/package.json +2 -2
- package/src/adapter/builtin/memory-adapter.spec.js +2 -2
- package/src/filter/filter-clause.d.ts +4 -4
- package/src/filter/operator-clause-tool.d.ts +2 -1
- package/src/filter/operator-clause-tool.js +92 -104
- package/src/filter/operator-clause-tool.spec.js +560 -315
- package/src/filter/where-clause-tool.js +39 -41
- package/src/filter/where-clause-tool.spec.js +123 -13
- package/src/utils/index.d.ts +1 -0
- package/src/utils/index.js +1 -0
- package/src/utils/like-to-regexp.d.ts +14 -0
- package/src/utils/like-to-regexp.js +57 -0
- package/src/utils/like-to-regexp.spec.js +143 -0
- package/src/utils/string-to-regexp.js +1 -10
- package/src/utils/string-to-regexp.spec.js +10 -7
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {Service} from '@e22m4u/js-service';
|
|
2
|
-
import {getValueByPath} from '../utils/index.js';
|
|
3
2
|
import {InvalidArgumentError} from '../errors/index.js';
|
|
4
3
|
import {OperatorClauseTool} from './operator-clause-tool.js';
|
|
4
|
+
import {getValueByPath, isDeepEqual, isPureObject} from '../utils/index.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Where clause tool.
|
|
@@ -75,8 +75,9 @@ export class WhereClauseTool extends Service {
|
|
|
75
75
|
const andClause = whereClause[key];
|
|
76
76
|
if (Array.isArray(andClause))
|
|
77
77
|
return andClause.every(clause => this._createFilter(clause)(data));
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
}
|
|
79
|
+
// OrClause (recursion)
|
|
80
|
+
else if (key === 'or' && key in whereClause) {
|
|
80
81
|
const orClause = whereClause[key];
|
|
81
82
|
if (Array.isArray(orClause))
|
|
82
83
|
return orClause.some(clause => this._createFilter(clause)(data));
|
|
@@ -84,34 +85,6 @@ export class WhereClauseTool extends Service {
|
|
|
84
85
|
// PropertiesClause (properties)
|
|
85
86
|
const value = getValueByPath(data, key);
|
|
86
87
|
const matcher = whereClause[key];
|
|
87
|
-
// Property value is an array.
|
|
88
|
-
if (Array.isArray(value)) {
|
|
89
|
-
// {neq: ...}
|
|
90
|
-
if (
|
|
91
|
-
typeof matcher === 'object' &&
|
|
92
|
-
matcher !== null &&
|
|
93
|
-
'neq' in matcher &&
|
|
94
|
-
matcher.neq !== undefined
|
|
95
|
-
) {
|
|
96
|
-
// The following condition is for the case where
|
|
97
|
-
// we are querying with a neq filter, and when
|
|
98
|
-
// the value is an empty array ([]).
|
|
99
|
-
if (value.length === 0) return true;
|
|
100
|
-
// The neq operator requires each element
|
|
101
|
-
// of the array to be excluded.
|
|
102
|
-
return value.every((el, index) => {
|
|
103
|
-
const where = {};
|
|
104
|
-
where[index] = matcher;
|
|
105
|
-
return this._createFilter(where)({...value});
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
// Requires one of an array elements to be match.
|
|
109
|
-
return value.some((el, index) => {
|
|
110
|
-
const where = {};
|
|
111
|
-
where[index] = matcher;
|
|
112
|
-
return this._createFilter(where)({...value});
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
88
|
// Test property value.
|
|
116
89
|
if (this._test(matcher, value)) return true;
|
|
117
90
|
});
|
|
@@ -126,30 +99,55 @@ export class WhereClauseTool extends Service {
|
|
|
126
99
|
* @returns {boolean}
|
|
127
100
|
*/
|
|
128
101
|
_test(example, value) {
|
|
129
|
-
//
|
|
102
|
+
// прямое сравнение
|
|
103
|
+
if (example === value) {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
// условием является null
|
|
130
107
|
if (example === null) {
|
|
131
108
|
return value === null;
|
|
132
109
|
}
|
|
133
|
-
//
|
|
110
|
+
// условием является undefined
|
|
134
111
|
if (example === undefined) {
|
|
135
112
|
return value === undefined;
|
|
136
113
|
}
|
|
137
|
-
//
|
|
138
|
-
// noinspection ALL
|
|
114
|
+
// условием является регулярное выражение
|
|
139
115
|
if (example instanceof RegExp) {
|
|
140
|
-
if (typeof value === 'string')
|
|
116
|
+
if (typeof value === 'string') {
|
|
117
|
+
return example.test(value);
|
|
118
|
+
}
|
|
119
|
+
// если значением является массив,
|
|
120
|
+
// то проверяется каждый элемент
|
|
121
|
+
if (Array.isArray(value)) {
|
|
122
|
+
return value.some(el => typeof el === 'string' && example.test(el));
|
|
123
|
+
}
|
|
141
124
|
return false;
|
|
142
125
|
}
|
|
143
|
-
//
|
|
144
|
-
if (
|
|
126
|
+
// условием является простой объект
|
|
127
|
+
if (isPureObject(example)) {
|
|
145
128
|
const operatorsTest = this.getService(OperatorClauseTool).testAll(
|
|
146
129
|
example,
|
|
147
130
|
value,
|
|
148
131
|
);
|
|
149
|
-
if (operatorsTest !== undefined)
|
|
132
|
+
if (operatorsTest !== undefined) {
|
|
133
|
+
// особая логика для neq с массивами
|
|
134
|
+
// {hobbies: {neq: 'yoga'}}
|
|
135
|
+
// должно вернуть true для
|
|
136
|
+
// ['bicycle', 'meditation']
|
|
137
|
+
if ('neq' in example && Array.isArray(value)) {
|
|
138
|
+
return !value.some(el => isDeepEqual(el, example.neq));
|
|
139
|
+
}
|
|
140
|
+
return operatorsTest;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// значением является массив
|
|
144
|
+
if (Array.isArray(value)) {
|
|
145
|
+
// если один из элементов массива соответствует
|
|
146
|
+
// поиску, то возвращается true
|
|
147
|
+
const isElementMatched = value.some(el => isDeepEqual(el, example));
|
|
148
|
+
if (isElementMatched) return true;
|
|
150
149
|
}
|
|
151
|
-
|
|
152
|
-
return example == value;
|
|
150
|
+
return isDeepEqual(example, value);
|
|
153
151
|
}
|
|
154
152
|
|
|
155
153
|
/**
|
|
@@ -13,6 +13,7 @@ const OBJECTS = [
|
|
|
13
13
|
hobbies: ['bicycle', 'yoga'],
|
|
14
14
|
nickname: 'Spear',
|
|
15
15
|
birthdate: '2002-04-14',
|
|
16
|
+
address: {city: 'New York', street: '5th Avenue'},
|
|
16
17
|
},
|
|
17
18
|
{
|
|
18
19
|
id: 2,
|
|
@@ -22,6 +23,7 @@ const OBJECTS = [
|
|
|
22
23
|
hobbies: ['yoga', 'meditation'],
|
|
23
24
|
nickname: 'Flower',
|
|
24
25
|
birthdate: '2002-01-12',
|
|
26
|
+
address: {city: 'London', street: 'Baker Street'},
|
|
25
27
|
},
|
|
26
28
|
{
|
|
27
29
|
id: 3,
|
|
@@ -31,6 +33,7 @@ const OBJECTS = [
|
|
|
31
33
|
hobbies: [],
|
|
32
34
|
nickname: null,
|
|
33
35
|
birthdate: '2002-03-01',
|
|
36
|
+
address: {city: 'Paris', street: 'Champs-Élysées'},
|
|
34
37
|
},
|
|
35
38
|
{
|
|
36
39
|
id: 4,
|
|
@@ -39,6 +42,18 @@ const OBJECTS = [
|
|
|
39
42
|
age: 32,
|
|
40
43
|
hobbies: ['bicycle'],
|
|
41
44
|
birthdate: '1991-06-24',
|
|
45
|
+
// нет nickname
|
|
46
|
+
address: {city: 'New York', street: 'Wall Street'},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: 5,
|
|
50
|
+
name: 'Peter',
|
|
51
|
+
surname: 'Jones',
|
|
52
|
+
age: 45,
|
|
53
|
+
hobbies: ['fishing'],
|
|
54
|
+
birthdate: '1978-11-05',
|
|
55
|
+
// нет nickname
|
|
56
|
+
address: {city: 'New York', street: '5th Avenue'},
|
|
42
57
|
},
|
|
43
58
|
];
|
|
44
59
|
|
|
@@ -154,32 +169,36 @@ describe('WhereClauseTool', function () {
|
|
|
154
169
|
|
|
155
170
|
it('uses the "neq" operator to match non-equality', function () {
|
|
156
171
|
const result = S.filter(OBJECTS, {name: {neq: 'John'}});
|
|
157
|
-
expect(result).to.have.length(
|
|
172
|
+
expect(result).to.have.length(4);
|
|
158
173
|
expect(result[0]).to.be.eql(OBJECTS[1]);
|
|
159
174
|
expect(result[1]).to.be.eql(OBJECTS[2]);
|
|
160
175
|
expect(result[2]).to.be.eql(OBJECTS[3]);
|
|
176
|
+
expect(result[3]).to.be.eql(OBJECTS[4]);
|
|
161
177
|
});
|
|
162
178
|
|
|
163
179
|
it('uses the "neq" operator to match an empty array', function () {
|
|
164
180
|
const result = S.filter(OBJECTS, {hobbies: {neq: 'bicycle'}});
|
|
165
|
-
expect(result).to.have.length(
|
|
181
|
+
expect(result).to.have.length(3);
|
|
166
182
|
expect(result[0]).to.be.eql(OBJECTS[1]);
|
|
167
183
|
expect(result[1]).to.be.eql(OBJECTS[2]);
|
|
184
|
+
expect(result[2]).to.be.eql(OBJECTS[4]);
|
|
168
185
|
});
|
|
169
186
|
|
|
170
187
|
it('uses the "gt" operator to compare values', function () {
|
|
171
188
|
const result = S.filter(OBJECTS, {id: {gt: 2}});
|
|
172
|
-
expect(result).to.have.length(
|
|
189
|
+
expect(result).to.have.length(3);
|
|
173
190
|
expect(result[0]).to.be.eql(OBJECTS[2]);
|
|
174
191
|
expect(result[1]).to.be.eql(OBJECTS[3]);
|
|
192
|
+
expect(result[2]).to.be.eql(OBJECTS[4]);
|
|
175
193
|
});
|
|
176
194
|
|
|
177
195
|
it('uses the "gte" operator to compare values', function () {
|
|
178
196
|
const result = S.filter(OBJECTS, {id: {gte: 2}});
|
|
179
|
-
expect(result).to.have.length(
|
|
197
|
+
expect(result).to.have.length(4);
|
|
180
198
|
expect(result[0]).to.be.eql(OBJECTS[1]);
|
|
181
199
|
expect(result[1]).to.be.eql(OBJECTS[2]);
|
|
182
200
|
expect(result[2]).to.be.eql(OBJECTS[3]);
|
|
201
|
+
expect(result[3]).to.be.eql(OBJECTS[4]);
|
|
183
202
|
});
|
|
184
203
|
|
|
185
204
|
it('uses the "lt" operator to compare values', function () {
|
|
@@ -206,9 +225,10 @@ describe('WhereClauseTool', function () {
|
|
|
206
225
|
|
|
207
226
|
it('uses the "nin" operator to compare values', function () {
|
|
208
227
|
const result = S.filter(OBJECTS, {id: {nin: [2, 3]}});
|
|
209
|
-
expect(result).to.have.length(
|
|
228
|
+
expect(result).to.have.length(3);
|
|
210
229
|
expect(result[0]).to.be.eql(OBJECTS[0]);
|
|
211
230
|
expect(result[1]).to.be.eql(OBJECTS[3]);
|
|
231
|
+
expect(result[2]).to.be.eql(OBJECTS[4]);
|
|
212
232
|
});
|
|
213
233
|
|
|
214
234
|
it('uses the "between" operator to compare values', function () {
|
|
@@ -228,36 +248,39 @@ describe('WhereClauseTool', function () {
|
|
|
228
248
|
|
|
229
249
|
it('uses the "exists" operator to check non-existence', function () {
|
|
230
250
|
const result = S.filter(OBJECTS, {nickname: {exists: false}});
|
|
231
|
-
expect(result).to.have.length(
|
|
251
|
+
expect(result).to.have.length(2);
|
|
232
252
|
expect(result[0]).to.be.eql(OBJECTS[3]);
|
|
253
|
+
expect(result[1]).to.be.eql(OBJECTS[4]);
|
|
233
254
|
});
|
|
234
255
|
|
|
235
256
|
it('uses the "like" operator to match by a substring', function () {
|
|
236
|
-
const result = S.filter(OBJECTS, {name: {like: 'liv'}});
|
|
257
|
+
const result = S.filter(OBJECTS, {name: {like: '%liv%'}});
|
|
237
258
|
expect(result).to.have.length(1);
|
|
238
259
|
expect(result[0]).to.be.eql(OBJECTS[3]);
|
|
239
260
|
});
|
|
240
261
|
|
|
241
262
|
it('uses the "nlike" operator to exclude by a substring', function () {
|
|
242
|
-
const result = S.filter(OBJECTS, {name: {nlike: 'liv'}});
|
|
243
|
-
expect(result).to.have.length(
|
|
263
|
+
const result = S.filter(OBJECTS, {name: {nlike: '%liv%'}});
|
|
264
|
+
expect(result).to.have.length(4);
|
|
244
265
|
expect(result[0]).to.be.eql(OBJECTS[0]);
|
|
245
266
|
expect(result[1]).to.be.eql(OBJECTS[1]);
|
|
246
267
|
expect(result[2]).to.be.eql(OBJECTS[2]);
|
|
268
|
+
expect(result[3]).to.be.eql(OBJECTS[4]);
|
|
247
269
|
});
|
|
248
270
|
|
|
249
271
|
it('uses the "ilike" operator to case-insensitively matching by a substring', function () {
|
|
250
|
-
const result = S.filter(OBJECTS, {name: {ilike: 'LIV'}});
|
|
272
|
+
const result = S.filter(OBJECTS, {name: {ilike: '%LIV%'}});
|
|
251
273
|
expect(result).to.have.length(1);
|
|
252
274
|
expect(result[0]).to.be.eql(OBJECTS[3]);
|
|
253
275
|
});
|
|
254
276
|
|
|
255
277
|
it('uses the "nilike" operator to exclude case-insensitively by a substring', function () {
|
|
256
|
-
const result = S.filter(OBJECTS, {name: {nilike: 'LIV'}});
|
|
257
|
-
expect(result).to.have.length(
|
|
278
|
+
const result = S.filter(OBJECTS, {name: {nilike: '%LIV%'}});
|
|
279
|
+
expect(result).to.have.length(4);
|
|
258
280
|
expect(result[0]).to.be.eql(OBJECTS[0]);
|
|
259
281
|
expect(result[1]).to.be.eql(OBJECTS[1]);
|
|
260
282
|
expect(result[2]).to.be.eql(OBJECTS[2]);
|
|
283
|
+
expect(result[3]).to.be.eql(OBJECTS[4]);
|
|
261
284
|
});
|
|
262
285
|
|
|
263
286
|
it('uses the "regexp" operator to compare values', function () {
|
|
@@ -274,8 +297,95 @@ describe('WhereClauseTool', function () {
|
|
|
274
297
|
|
|
275
298
|
it('does not use undefined to match a null value', function () {
|
|
276
299
|
const result = S.filter(OBJECTS, {nickname: undefined});
|
|
277
|
-
expect(result).to.have.length(
|
|
300
|
+
expect(result).to.have.length(2);
|
|
278
301
|
expect(result[0]).to.be.eql(OBJECTS[3]);
|
|
302
|
+
expect(result[1]).to.be.eql(OBJECTS[4]);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
describe('advanced matching', function () {
|
|
306
|
+
it('combines multiple operators for one field using "and"', function () {
|
|
307
|
+
const result = S.filter(OBJECTS, {
|
|
308
|
+
and: [{age: {gt: 20}}, {age: {lt: 30}}],
|
|
309
|
+
});
|
|
310
|
+
expect(result).to.have.length(3);
|
|
311
|
+
expect(result.map(o => o.id)).to.eql([1, 2, 3]);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('combines multiple operators for one field implicitly', function () {
|
|
315
|
+
const result = S.filter(OBJECTS, {age: {gt: 20, lt: 30}});
|
|
316
|
+
expect(result).to.have.length(3);
|
|
317
|
+
expect(result.map(o => o.id)).to.eql([1, 2, 3]);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('uses dot notation to query nested objects', function () {
|
|
321
|
+
const result = S.filter(OBJECTS, {'address.city': 'New York'});
|
|
322
|
+
expect(result).to.have.length(3);
|
|
323
|
+
expect(result.map(o => o.id)).to.eql([1, 4, 5]);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('uses dot notation combined with operators', function () {
|
|
327
|
+
const result = S.filter(OBJECTS, {
|
|
328
|
+
'address.street': {like: '%Avenue%'},
|
|
329
|
+
});
|
|
330
|
+
expect(result).to.have.length(2);
|
|
331
|
+
expect(result.map(o => o.id)).to.eql([1, 5]);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('matches an object by exact deep equality', function () {
|
|
335
|
+
const result = S.filter(OBJECTS, {
|
|
336
|
+
address: {city: 'New York', street: '5th Avenue'},
|
|
337
|
+
});
|
|
338
|
+
expect(result).to.have.length(2);
|
|
339
|
+
expect(result.map(o => o.id)).to.eql([1, 5]);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('does not match an object if it has extra properties', function () {
|
|
343
|
+
const result = S.filter(OBJECTS, {
|
|
344
|
+
address: {city: 'New York'},
|
|
345
|
+
});
|
|
346
|
+
expect(result).to.have.length(0);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('does match an object if property order is different', function () {
|
|
350
|
+
const result = S.filter(OBJECTS, {
|
|
351
|
+
address: {street: '5th Avenue', city: 'New York'},
|
|
352
|
+
});
|
|
353
|
+
expect(result).to.have.length(2);
|
|
354
|
+
expect(result[0].id).to.equal(1);
|
|
355
|
+
expect(result[1].id).to.equal(5);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('matches an array by exact deep equality', function () {
|
|
359
|
+
const result = S.filter(OBJECTS, {
|
|
360
|
+
hobbies: ['bicycle', 'yoga'],
|
|
361
|
+
});
|
|
362
|
+
expect(result).to.have.length(1);
|
|
363
|
+
expect(result[0].id).to.equal(1);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('does not match an array if order is different', function () {
|
|
367
|
+
const result = S.filter(OBJECTS, {
|
|
368
|
+
hobbies: ['yoga', 'bicycle'],
|
|
369
|
+
});
|
|
370
|
+
expect(result).to.have.length(0);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('does not match an array if it contains extra items', function () {
|
|
374
|
+
const result = S.filter(OBJECTS, {
|
|
375
|
+
hobbies: ['bicycle'],
|
|
376
|
+
});
|
|
377
|
+
// Найдет только объект с id: 4, так как у него hobbies: ['bicycle']
|
|
378
|
+
expect(result).to.have.length(1);
|
|
379
|
+
expect(result[0].id).to.equal(4);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('correctly combines multiple operators with dot notation in an "and" clause', function () {
|
|
383
|
+
const result = S.filter(OBJECTS, {
|
|
384
|
+
and: [{'address.city': 'New York'}, {age: {gt: 30}}],
|
|
385
|
+
});
|
|
386
|
+
expect(result).to.have.length(2);
|
|
387
|
+
expect(result.map(o => o.id)).to.eql([4, 5]);
|
|
388
|
+
});
|
|
279
389
|
});
|
|
280
390
|
});
|
|
281
391
|
|
package/src/utils/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export * from './singularize.js';
|
|
|
6
6
|
export * from './is-deep-equal.js';
|
|
7
7
|
export * from './get-ctor-name.js';
|
|
8
8
|
export * from './is-pure-object.js';
|
|
9
|
+
export * from './like-to-regexp.js';
|
|
9
10
|
export * from './string-to-regexp.js';
|
|
10
11
|
export * from './get-value-by-path.js';
|
|
11
12
|
export * from './transform-promise.js';
|
package/src/utils/index.js
CHANGED
|
@@ -6,6 +6,7 @@ export * from './singularize.js';
|
|
|
6
6
|
export * from './is-deep-equal.js';
|
|
7
7
|
export * from './get-ctor-name.js';
|
|
8
8
|
export * from './is-pure-object.js';
|
|
9
|
+
export * from './like-to-regexp.js';
|
|
9
10
|
export * from './string-to-regexp.js';
|
|
10
11
|
export * from './get-value-by-path.js';
|
|
11
12
|
export * from './transform-promise.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Преобразует SQL LIKE-шаблон в объект RegExp.
|
|
3
|
+
*
|
|
4
|
+
* Экранирует специальные символы регулярных выражений,
|
|
5
|
+
* чтобы они обрабатывались как обычные символы, и преобразует
|
|
6
|
+
* SQL wildcards (% и _) в их эквиваленты в регулярных выражениях.
|
|
7
|
+
*
|
|
8
|
+
* @param pattern
|
|
9
|
+
* @param isCaseInsensitive
|
|
10
|
+
*/
|
|
11
|
+
export function likeToRegexp(
|
|
12
|
+
pattern: string,
|
|
13
|
+
isCaseInsensitive?: boolean,
|
|
14
|
+
): RegExp;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import {InvalidArgumentError} from '../errors/index.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Преобразует SQL LIKE-шаблон в объект RegExp.
|
|
5
|
+
*
|
|
6
|
+
* Экранирует специальные символы регулярных выражений,
|
|
7
|
+
* чтобы они обрабатывались как обычные символы, и преобразует
|
|
8
|
+
* SQL wildcards (% и _) в их эквиваленты в регулярных выражениях.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} pattern
|
|
11
|
+
* @param {boolean} isCaseInsensitive
|
|
12
|
+
* @returns {RegExp}
|
|
13
|
+
*/
|
|
14
|
+
export function likeToRegexp(pattern, isCaseInsensitive = false) {
|
|
15
|
+
if (typeof pattern !== 'string') {
|
|
16
|
+
throw new InvalidArgumentError(
|
|
17
|
+
'The first argument of `likeToRegexp` ' +
|
|
18
|
+
'should be a String, but %v was given.',
|
|
19
|
+
pattern,
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
// символы, которые имеют специальное значение
|
|
23
|
+
// в RegExp и должны быть экранированы
|
|
24
|
+
const regexSpecials = '-[]{}()*+?.\\^$|';
|
|
25
|
+
let regexString = '';
|
|
26
|
+
let isEscaping = false;
|
|
27
|
+
// экранирование
|
|
28
|
+
for (const char of pattern) {
|
|
29
|
+
if (isEscaping) {
|
|
30
|
+
// предыдущий символ был '\', значит текущий символ - литерал
|
|
31
|
+
regexString += regexSpecials.includes(char) ? `\\${char}` : char;
|
|
32
|
+
isEscaping = false;
|
|
33
|
+
} else if (char === '\\') {
|
|
34
|
+
// символ экранирования, следующий символ будет литералом
|
|
35
|
+
isEscaping = true;
|
|
36
|
+
} else if (char === '%') {
|
|
37
|
+
// SQL wildcard: любое количество любых символов
|
|
38
|
+
regexString += '.*';
|
|
39
|
+
} else if (char === '_') {
|
|
40
|
+
// SQL wildcard: ровно один любой символ
|
|
41
|
+
regexString += '.';
|
|
42
|
+
} else if (regexSpecials.includes(char)) {
|
|
43
|
+
// экранирование других специальных символов RegExp
|
|
44
|
+
regexString += `\\${char}`;
|
|
45
|
+
} else {
|
|
46
|
+
// обычный символ
|
|
47
|
+
regexString += char;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// если строка заканчивается на экранирующий символ,
|
|
51
|
+
// считаем его литералом.
|
|
52
|
+
if (isEscaping) {
|
|
53
|
+
regexString += '\\\\';
|
|
54
|
+
}
|
|
55
|
+
const flags = isCaseInsensitive ? 'i' : '';
|
|
56
|
+
return new RegExp(`^${regexString}$`, flags);
|
|
57
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import {expect} from 'chai';
|
|
2
|
+
import {likeToRegexp} from './like-to-regexp.js';
|
|
3
|
+
|
|
4
|
+
describe('likeToRegexp', function () {
|
|
5
|
+
it('throws an error if the pattern is not a string', function () {
|
|
6
|
+
const error = v =>
|
|
7
|
+
'The first argument of `likeToRegexp` ' +
|
|
8
|
+
`should be a String, but ${v} was given.`;
|
|
9
|
+
expect(() => likeToRegexp(123)).to.throw(error('123'));
|
|
10
|
+
expect(() => likeToRegexp(null)).to.throw(error('null'));
|
|
11
|
+
expect(() => likeToRegexp({})).to.throw(error('Object'));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe('basic wildcards', function () {
|
|
15
|
+
it('should handle "%" as zero or more characters', function () {
|
|
16
|
+
const re = likeToRegexp('he%o');
|
|
17
|
+
expect(re.test('hello')).to.be.true;
|
|
18
|
+
expect(re.test('heo')).to.be.true;
|
|
19
|
+
expect(re.test('hexxxxo')).to.be.true;
|
|
20
|
+
expect(re.test('ahello')).to.be.false;
|
|
21
|
+
expect(re.test('hellob')).to.be.false;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should handle "_" as exactly one character', function () {
|
|
25
|
+
const re = likeToRegexp('he_lo');
|
|
26
|
+
expect(re.test('hello')).to.be.true;
|
|
27
|
+
expect(re.test('healo')).to.be.true;
|
|
28
|
+
expect(re.test('he_lo')).to.be.true;
|
|
29
|
+
expect(re.test('helo')).to.be.false;
|
|
30
|
+
expect(re.test('helllo')).to.be.false;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should handle multiple wildcards', function () {
|
|
34
|
+
const re = likeToRegexp('%_world%');
|
|
35
|
+
expect(re.test('hello_world_today')).to.be.true;
|
|
36
|
+
expect(re.test('a_world')).to.be.true;
|
|
37
|
+
expect(re.test('no_match')).to.be.false;
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('case sensitivity', function () {
|
|
42
|
+
it('should be case-sensitive by default', function () {
|
|
43
|
+
const re = likeToRegexp('Hello%');
|
|
44
|
+
expect(re.test('Hello World')).to.be.true;
|
|
45
|
+
expect(re.test('hello World')).to.be.false;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should be case-insensitive when specified', function () {
|
|
49
|
+
const re = likeToRegexp('Hello%', true);
|
|
50
|
+
expect(re.test('Hello World')).to.be.true;
|
|
51
|
+
expect(re.test('hello World')).to.be.true;
|
|
52
|
+
expect(re.test('HELLO WORLD')).to.be.true;
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('escaping', function () {
|
|
57
|
+
it('should handle escaped "%" as a literal character', function () {
|
|
58
|
+
const re = likeToRegexp('100\\%');
|
|
59
|
+
expect(re.test('100%')).to.be.true;
|
|
60
|
+
expect(re.test('100_')).to.be.false;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should handle escaped "_" as a literal character', function () {
|
|
64
|
+
const re = likeToRegexp('file\\_name');
|
|
65
|
+
expect(re.test('file_name')).to.be.true;
|
|
66
|
+
expect(re.test('filename')).to.be.false;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should handle escaped backslash as a literal backslash', function () {
|
|
70
|
+
const re = likeToRegexp('path\\\\to\\\\file');
|
|
71
|
+
expect(re.test('path\\to\\file')).to.be.true;
|
|
72
|
+
expect(re.test('pathtofile')).to.be.false;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should handle a trailing backslash as a literal backslash', function () {
|
|
76
|
+
const re = likeToRegexp('path\\');
|
|
77
|
+
expect(re.test('path\\')).to.be.true;
|
|
78
|
+
expect(re.test('path')).to.be.false;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should handle mixed escaping and wildcards', function () {
|
|
82
|
+
const re = likeToRegexp('%file\\_%.docx');
|
|
83
|
+
expect(re.test('my_awesome_file_v1.docx')).to.be.true;
|
|
84
|
+
expect(re.test('my_awesome_file-v1.docx')).to.be.false;
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('escaping RegExp special character', function () {
|
|
89
|
+
it('should escape dots "." as literal dots', function () {
|
|
90
|
+
const re = likeToRegexp('v1.0');
|
|
91
|
+
expect(re.test('v1.0')).to.be.true;
|
|
92
|
+
expect(re.test('v1_0')).to.be.false;
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should escape parentheses "()" as literals', function () {
|
|
96
|
+
const re = likeToRegexp('file(1)');
|
|
97
|
+
expect(re.test('file(1)')).to.be.true;
|
|
98
|
+
expect(re.test('file1')).to.be.false;
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should escape plus signs "+"', function () {
|
|
102
|
+
const re = likeToRegexp('C++');
|
|
103
|
+
expect(re.test('C++')).to.be.true;
|
|
104
|
+
expect(re.test('C+')).to.be.false;
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should escape question marks "?"', function () {
|
|
108
|
+
const re = likeToRegexp('Are you sure?');
|
|
109
|
+
expect(re.test('Are you sure?')).to.be.true;
|
|
110
|
+
expect(re.test('Are you sure')).to.be.false;
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should escape curly braces "{}"', function () {
|
|
114
|
+
const re = likeToRegexp('{id}');
|
|
115
|
+
expect(re.test('{id}')).to.be.true;
|
|
116
|
+
expect(re.test('id')).to.be.false;
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should handle a complex string with multiple special characters', function () {
|
|
120
|
+
const pattern = 'docs\\(v1.0\\)/%.txt+';
|
|
121
|
+
const re = likeToRegexp(pattern);
|
|
122
|
+
expect(re.test('docs(v1.0)/document_v2.txt+')).to.be.true;
|
|
123
|
+
expect(re.test('docs(v1.0)/document_v2.txt')).to.be.false;
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('full pattern matching', function () {
|
|
128
|
+
it('should match the entire string, not just a part of it', function () {
|
|
129
|
+
const re = likeToRegexp('world');
|
|
130
|
+
expect(re.test('hello world')).to.be.false;
|
|
131
|
+
expect(re.test('world today')).to.be.false;
|
|
132
|
+
expect(re.test('world')).to.be.true;
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should match the entire string when using wildcards at boundaries', function () {
|
|
136
|
+
const re = likeToRegexp('%world%');
|
|
137
|
+
expect(re.test('hello world')).to.be.true;
|
|
138
|
+
expect(re.test('world today')).to.be.true;
|
|
139
|
+
expect(re.test('hello world today')).to.be.true;
|
|
140
|
+
expect(re.test('world')).to.be.true;
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -9,14 +9,5 @@ export function stringToRegexp(pattern, flags = undefined) {
|
|
|
9
9
|
if (pattern instanceof RegExp) {
|
|
10
10
|
return new RegExp(pattern, flags);
|
|
11
11
|
}
|
|
12
|
-
|
|
13
|
-
for (let i = 0, n = pattern.length; i < n; i++) {
|
|
14
|
-
const char = pattern.charAt(i);
|
|
15
|
-
if (char === '%') {
|
|
16
|
-
regex += '.*';
|
|
17
|
-
} else {
|
|
18
|
-
regex += char;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
return new RegExp(regex, flags);
|
|
12
|
+
return new RegExp(pattern, flags);
|
|
22
13
|
}
|
|
@@ -2,34 +2,37 @@ import {expect} from 'chai';
|
|
|
2
2
|
import {stringToRegexp} from './string-to-regexp.js';
|
|
3
3
|
|
|
4
4
|
describe('stringToRegexp', function () {
|
|
5
|
-
it('
|
|
5
|
+
it('should return RegExp from a given string', function () {
|
|
6
6
|
expect(stringToRegexp('value').test('value')).to.be.true;
|
|
7
7
|
expect(stringToRegexp('val.+').test('value')).to.be.true;
|
|
8
|
-
expect(stringToRegexp('%alu%').test('value')).to.be.true;
|
|
9
8
|
expect(stringToRegexp('val*').test('value')).to.be.true;
|
|
10
9
|
});
|
|
11
10
|
|
|
12
|
-
it('uses case-sensitive mode by default', function () {
|
|
11
|
+
it('should uses case-sensitive mode by default', function () {
|
|
13
12
|
expect(stringToRegexp('value').test('VALUE')).to.be.false;
|
|
14
13
|
expect(stringToRegexp('val.+').test('VALUE')).to.be.false;
|
|
15
14
|
expect(stringToRegexp('%alu%').test('VALUE')).to.be.false;
|
|
16
15
|
expect(stringToRegexp('val*').test('VALUE')).to.be.false;
|
|
17
16
|
});
|
|
18
17
|
|
|
19
|
-
it('uses given flags in a new RegExp', function () {
|
|
18
|
+
it('should uses given flags in a new RegExp', function () {
|
|
20
19
|
expect(stringToRegexp('value', 'i').test('VALUE')).to.be.true;
|
|
21
20
|
expect(stringToRegexp('val.+', 'i').test('VALUE')).to.be.true;
|
|
22
|
-
expect(stringToRegexp('%alu%', 'i').test('VALUE')).to.be.true;
|
|
23
21
|
expect(stringToRegexp('val*', 'i').test('VALUE')).to.be.true;
|
|
24
22
|
});
|
|
25
23
|
|
|
26
|
-
it('
|
|
24
|
+
it('should return RegExp from a given RegExp', function () {
|
|
27
25
|
const regExp = new RegExp('value');
|
|
28
26
|
expect(stringToRegexp(regExp).test('value')).to.be.true;
|
|
29
27
|
});
|
|
30
28
|
|
|
31
|
-
it('overrides flags of a given RegExp', function () {
|
|
29
|
+
it('should overrides flags of a given RegExp', function () {
|
|
32
30
|
const regExp = new RegExp('value');
|
|
33
31
|
expect(stringToRegexp(regExp, 'i').test('VALUE')).to.be.true;
|
|
34
32
|
});
|
|
33
|
+
|
|
34
|
+
it('should not replace "%" and "_" symbols as SQL-like wildcard', function () {
|
|
35
|
+
const res = stringToRegexp('%alu_');
|
|
36
|
+
expect(res).to.be.eql(/%alu_/);
|
|
37
|
+
});
|
|
35
38
|
});
|