@nymphjs/query-parser 1.0.0-beta.13 → 1.0.0-beta.131
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/CHANGELOG.md +499 -0
- package/README.md +64 -50
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/queryParser.js +953 -0
- package/dist/queryParser.js.map +1 -0
- package/dist/queryParser.test.js +1686 -0
- package/dist/queryParser.test.js.map +1 -0
- package/jest.config.js +31 -5
- package/package.json +35 -34
- package/src/index.ts +2 -2
- package/src/queryParser.test.ts +1106 -164
- package/src/queryParser.ts +425 -140
- package/tsconfig.json +7 -4
- package/typedoc.json +4 -0
- package/lib/index.d.ts +0 -4
- package/lib/index.js +0 -25
- package/lib/index.js.map +0 -1
- package/lib/queryParser.js +0 -672
- package/lib/queryParser.js.map +0 -1
- package/lib/queryParser.test.js +0 -856
- package/lib/queryParser.test.js.map +0 -1
- package/webpack.config.js +0 -28
- /package/{lib → dist}/queryParser.d.ts +0 -0
- /package/{lib → dist}/queryParser.test.d.ts +0 -0
package/src/queryParser.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { EntityConstructor, Options, Selector } from '@nymphjs/client';
|
|
2
|
-
import splitn from '@sciactive/splitn';
|
|
2
|
+
import { splitn } from '@sciactive/splitn';
|
|
3
3
|
|
|
4
4
|
export type BareQueryHandler = (
|
|
5
5
|
input: string,
|
|
6
6
|
entityClass?: EntityConstructor,
|
|
7
|
-
defaultFields?: string[]
|
|
7
|
+
defaultFields?: string[],
|
|
8
8
|
) => Partial<Selector>;
|
|
9
9
|
|
|
10
10
|
export type QRefMap = {
|
|
@@ -12,7 +12,7 @@ export type QRefMap = {
|
|
|
12
12
|
};
|
|
13
13
|
|
|
14
14
|
export default function queryParser<
|
|
15
|
-
T extends EntityConstructor = EntityConstructor
|
|
15
|
+
T extends EntityConstructor = EntityConstructor,
|
|
16
16
|
>({
|
|
17
17
|
query,
|
|
18
18
|
entityClass,
|
|
@@ -27,7 +27,7 @@ export default function queryParser<
|
|
|
27
27
|
type: '|',
|
|
28
28
|
ilike: defaultFields.map((field) => [field, input]) as [
|
|
29
29
|
string,
|
|
30
|
-
string
|
|
30
|
+
string,
|
|
31
31
|
][],
|
|
32
32
|
};
|
|
33
33
|
}
|
|
@@ -90,7 +90,8 @@ function selectorsParser({
|
|
|
90
90
|
} else if (inQuote) {
|
|
91
91
|
continue;
|
|
92
92
|
} else if (curQuery[i] === '(') {
|
|
93
|
-
|
|
93
|
+
const searchClause = i !== 0 && !curQuery[i - 1].match(/\s/);
|
|
94
|
+
if (currentStart == null && !searchClause) {
|
|
94
95
|
currentStart = i;
|
|
95
96
|
} else {
|
|
96
97
|
nesting++;
|
|
@@ -144,7 +145,7 @@ function selectorsParser({
|
|
|
144
145
|
defaultFields,
|
|
145
146
|
qrefMap,
|
|
146
147
|
bareHandler,
|
|
147
|
-
})
|
|
148
|
+
}),
|
|
148
149
|
);
|
|
149
150
|
}
|
|
150
151
|
}
|
|
@@ -157,21 +158,31 @@ function selectorsParser({
|
|
|
157
158
|
});
|
|
158
159
|
|
|
159
160
|
if (options) {
|
|
160
|
-
const limitRegex = /(
|
|
161
|
+
const limitRegex = /(?:\s|^)limit:(\d+)(?=\s|$)/;
|
|
161
162
|
const limitMatch = curQuery.match(limitRegex);
|
|
162
163
|
if (limitMatch) {
|
|
163
164
|
options.limit = Number(limitMatch[1]);
|
|
164
165
|
}
|
|
165
166
|
curQuery = curQuery.replace(limitRegex, '');
|
|
166
167
|
|
|
167
|
-
const offsetRegex = /(
|
|
168
|
+
const offsetRegex = /(?:\s|^)offset:(\d+)(?=\s|$)/;
|
|
168
169
|
const offsetMatch = curQuery.match(offsetRegex);
|
|
169
170
|
if (offsetMatch) {
|
|
170
171
|
options.offset = Number(offsetMatch[1]);
|
|
171
172
|
}
|
|
172
173
|
curQuery = curQuery.replace(offsetRegex, '');
|
|
173
174
|
|
|
174
|
-
|
|
175
|
+
// JavaScript variable names are ridiculously infeasable to check
|
|
176
|
+
// thoroughly, so this is a "best attempt".
|
|
177
|
+
const sortRegex =
|
|
178
|
+
/(?:\s|^)sort:(-|[_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*)(?=\s|$)/;
|
|
179
|
+
const sortMatch = curQuery.match(sortRegex);
|
|
180
|
+
if (sortMatch) {
|
|
181
|
+
options.sort = sortMatch[1] === '-' ? null : sortMatch[1];
|
|
182
|
+
}
|
|
183
|
+
curQuery = curQuery.replace(sortRegex, '');
|
|
184
|
+
|
|
185
|
+
const reverseRegex = /(?:\s|^)reverse:(true|false|1|0)(?=\s|$)/;
|
|
175
186
|
const reverseMatch = curQuery.match(reverseRegex);
|
|
176
187
|
if (reverseMatch) {
|
|
177
188
|
options.reverse = reverseMatch[1] === 'true' || reverseMatch[1] === '1';
|
|
@@ -222,92 +233,270 @@ function selectorParser({
|
|
|
222
233
|
}): string {
|
|
223
234
|
let curQuery = query;
|
|
224
235
|
|
|
225
|
-
//
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
236
|
+
// This one needs to happen first, because it has entire queries in it.
|
|
237
|
+
// eg. user<{User name="Hunter"}> or -user<{User name="Hunter"}>
|
|
238
|
+
const qrefRegex =
|
|
239
|
+
/(?:\s|^)-?([^-\s=\[\]<>{}()][^\s=\[\]<>{}()]*?)<\{(\w+)(?=\s)/;
|
|
240
|
+
// This is used to bypass badly formatted things that match the above regex.
|
|
241
|
+
let offsetIndex = 0;
|
|
242
|
+
let qrefMatch = curQuery.match(qrefRegex);
|
|
243
|
+
while (qrefMatch) {
|
|
244
|
+
let endIndex = (qrefMatch.index ?? 0) + offsetIndex + qrefMatch[0].length;
|
|
245
|
+
let curOpen = 1;
|
|
246
|
+
while (curOpen) {
|
|
247
|
+
const indexOfNextOpen = curQuery.indexOf('<{', endIndex);
|
|
248
|
+
const indexOfNextClose = curQuery.slice(endIndex).search(/(?<!\\)\}>/);
|
|
249
|
+
|
|
250
|
+
if (indexOfNextClose < 0) {
|
|
251
|
+
endIndex = curQuery.length;
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (
|
|
256
|
+
indexOfNextOpen > -1 &&
|
|
257
|
+
indexOfNextOpen < endIndex + indexOfNextClose
|
|
258
|
+
) {
|
|
259
|
+
endIndex = indexOfNextOpen + 2;
|
|
260
|
+
curOpen++;
|
|
261
|
+
} else {
|
|
262
|
+
endIndex += indexOfNextClose + 2;
|
|
263
|
+
curOpen--;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (!('qref' in selector)) {
|
|
268
|
+
selector.qref = [];
|
|
269
|
+
}
|
|
270
|
+
if (!('!qref' in selector)) {
|
|
271
|
+
selector['!qref'] = [];
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const match = curQuery.slice(
|
|
275
|
+
(qrefMatch.index ?? 0) + offsetIndex,
|
|
276
|
+
endIndex,
|
|
277
|
+
);
|
|
278
|
+
try {
|
|
279
|
+
let [name, value] = splitn(match.trim().slice(0, -1), '<', 2);
|
|
280
|
+
value = unQuoteCurlies(value.slice(1, -1));
|
|
281
|
+
let [className, qrefQuery] = splitn(value, ' ', 2);
|
|
282
|
+
const EntityClass = qrefMap[className].class;
|
|
283
|
+
if (EntityClass == null) {
|
|
284
|
+
throw new Error();
|
|
285
|
+
}
|
|
286
|
+
const qref = queryParser({
|
|
287
|
+
query: qrefQuery,
|
|
288
|
+
entityClass: EntityClass,
|
|
289
|
+
defaultFields: qrefMap[className].defaultFields,
|
|
290
|
+
qrefMap,
|
|
291
|
+
bareHandler,
|
|
292
|
+
});
|
|
293
|
+
if (name.startsWith('-')) {
|
|
294
|
+
(selector['!qref'] as [string, any][]).push([name.slice(1), qref]);
|
|
295
|
+
} else {
|
|
296
|
+
(selector.qref as [string, any][]).push([name, qref]);
|
|
297
|
+
}
|
|
298
|
+
} catch (e: any) {
|
|
299
|
+
offsetIndex = endIndex;
|
|
300
|
+
qrefMatch = curQuery.slice(offsetIndex).match(qrefRegex);
|
|
301
|
+
|
|
302
|
+
if (!selector.qref?.length) {
|
|
303
|
+
delete selector.qref;
|
|
304
|
+
}
|
|
305
|
+
if (!selector['!qref']?.length) {
|
|
306
|
+
delete selector['!qref'];
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
if (!selector.qref?.length) {
|
|
312
|
+
delete selector.qref;
|
|
313
|
+
}
|
|
314
|
+
if (!selector['!qref']?.length) {
|
|
315
|
+
delete selector['!qref'];
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
curQuery =
|
|
319
|
+
curQuery.slice(0, (qrefMatch.index ?? 0) + offsetIndex) +
|
|
320
|
+
' ' +
|
|
321
|
+
curQuery.slice(endIndex);
|
|
322
|
+
qrefMatch = curQuery.slice(offsetIndex).match(qrefRegex);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// eg. prop(some search string) or -prop(some search string)
|
|
326
|
+
const searchRegex =
|
|
327
|
+
/(?:\s|^)-?([^-\s=\[\]<>{}()][^\s=\[\]<>{}()]*?)\([^\)]+\)(?=\s|\)|\}>|$)/g;
|
|
328
|
+
const searchMatch = curQuery.match(searchRegex);
|
|
329
|
+
if (searchMatch) {
|
|
330
|
+
selector.search = [];
|
|
331
|
+
selector['!search'] = [];
|
|
332
|
+
for (let match of searchMatch) {
|
|
232
333
|
try {
|
|
233
|
-
let [name, value] = splitn(match.trim().slice(0, -1), '
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
continue;
|
|
334
|
+
let [name, value] = splitn(match.trim().slice(0, -1), '(', 2);
|
|
335
|
+
if (name.startsWith('-')) {
|
|
336
|
+
selector['!search'].push([name.slice(1), value]);
|
|
337
|
+
} else {
|
|
338
|
+
selector.search.push([name, value]);
|
|
239
339
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
340
|
+
} catch (e: any) {
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (!selector.search.length) {
|
|
345
|
+
delete selector.search;
|
|
346
|
+
}
|
|
347
|
+
if (!selector['!search'].length) {
|
|
348
|
+
delete selector['!search'];
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
curQuery = curQuery.replace(searchRegex, '');
|
|
352
|
+
|
|
353
|
+
// eg. name:Marty or name:"Marty McFly" or -name:'Marty McFly'
|
|
354
|
+
const searchColonRegex =
|
|
355
|
+
/(?:\s|^)(?!(limit|offset|sort|reverse):)-?([^-\s=\[\]<>{}()][^\s=\[\]<>{}()]*?):("[^"]*?"|'[^']*?'|[^\s]+)(?=\s|\)|\}>|$)/g;
|
|
356
|
+
const searchColonMatch = curQuery.match(searchColonRegex);
|
|
357
|
+
if (searchColonMatch) {
|
|
358
|
+
if (!('search' in selector)) {
|
|
359
|
+
selector.search = [];
|
|
360
|
+
}
|
|
361
|
+
if (!('!search' in selector)) {
|
|
362
|
+
selector['!search'] = [];
|
|
363
|
+
}
|
|
364
|
+
for (let match of searchColonMatch) {
|
|
365
|
+
try {
|
|
366
|
+
let [name, value] = splitn(match.trim(), ':', 2);
|
|
367
|
+
if (name.startsWith('-')) {
|
|
368
|
+
(selector['!search'] as [string, string][]).push([
|
|
369
|
+
name.slice(1),
|
|
370
|
+
value,
|
|
371
|
+
]);
|
|
249
372
|
} else {
|
|
250
|
-
selector.
|
|
373
|
+
(selector.search as [string, string][]).push([name, value]);
|
|
251
374
|
}
|
|
252
375
|
} catch (e: any) {
|
|
253
376
|
continue;
|
|
254
377
|
}
|
|
255
378
|
}
|
|
256
|
-
if (!selector.
|
|
257
|
-
delete selector.
|
|
379
|
+
if (selector.search == null || !selector.search.length) {
|
|
380
|
+
delete selector.search;
|
|
258
381
|
}
|
|
259
|
-
if (!selector['!
|
|
260
|
-
delete selector['!
|
|
382
|
+
if (selector['!search'] == null || !selector['!search'].length) {
|
|
383
|
+
delete selector['!search'];
|
|
261
384
|
}
|
|
262
385
|
}
|
|
263
|
-
curQuery = curQuery.replace(
|
|
386
|
+
curQuery = curQuery.replace(searchColonRegex, '');
|
|
264
387
|
|
|
265
|
-
// eg.
|
|
266
|
-
const
|
|
388
|
+
// eg. someArray=[1,2] or someObject={"prop":"some value"}
|
|
389
|
+
const equalJsonRegex =
|
|
390
|
+
/(?:\s|^)-?([^-\s=\[\]<>{}()][^\s=\[\]<>{}()]*?)=(\{|\[)/g;
|
|
391
|
+
const equalJsonMatch = [...curQuery.matchAll(equalJsonRegex)];
|
|
392
|
+
if (equalJsonMatch) {
|
|
393
|
+
if (!('equal' in selector)) {
|
|
394
|
+
selector.equal = [];
|
|
395
|
+
}
|
|
396
|
+
if (!('!equal' in selector)) {
|
|
397
|
+
selector['!equal'] = [];
|
|
398
|
+
}
|
|
399
|
+
// Work backward to find all long JSON values.
|
|
400
|
+
for (let i = equalJsonMatch.length - 1; i >= 0; i--) {
|
|
401
|
+
const match = equalJsonMatch[i];
|
|
402
|
+
let [name, opener] = splitn(match[0].trim(), '=', 2);
|
|
403
|
+
let start = match.index + match[0].length - 1;
|
|
404
|
+
let nextEndToken = curQuery.indexOf(opener === '{' ? '}' : ']', start);
|
|
405
|
+
while (nextEndToken !== -1) {
|
|
406
|
+
try {
|
|
407
|
+
if (name.startsWith('-')) {
|
|
408
|
+
(selector['!equal'] as [string, any][]).unshift([
|
|
409
|
+
name.slice(1),
|
|
410
|
+
JSON.parse(curQuery.substring(start, nextEndToken + 1)),
|
|
411
|
+
]);
|
|
412
|
+
} else {
|
|
413
|
+
(selector.equal as [string, any][]).unshift([
|
|
414
|
+
name,
|
|
415
|
+
JSON.parse(curQuery.substring(start, nextEndToken + 1)),
|
|
416
|
+
]);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
curQuery =
|
|
420
|
+
curQuery.substring(0, match.index) +
|
|
421
|
+
curQuery.substring(nextEndToken + 1);
|
|
422
|
+
break;
|
|
423
|
+
} catch (e: any) {
|
|
424
|
+
nextEndToken = curQuery.indexOf(
|
|
425
|
+
opener === '{' ? '}' : ']',
|
|
426
|
+
nextEndToken + 1,
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (selector.equal == null || !selector.equal.length) {
|
|
432
|
+
delete selector.equal;
|
|
433
|
+
}
|
|
434
|
+
if (selector['!equal'] == null || !selector['!equal'].length) {
|
|
435
|
+
delete selector['!equal'];
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// eg. name=Marty or name="Marty McFly" or enabled=true
|
|
440
|
+
const equalRegex =
|
|
441
|
+
/(?:\s|^)-?([^-\s=\[\]<>{}()][^\s=\[\]<>{}()]*?)=(""|".*?[^\\]"|[^\s]+)(?=\s|\)|\}>|$)/g;
|
|
267
442
|
const equalMatch = curQuery.match(equalRegex);
|
|
268
443
|
if (equalMatch) {
|
|
269
|
-
|
|
270
|
-
|
|
444
|
+
if (!('equal' in selector)) {
|
|
445
|
+
selector.equal = [];
|
|
446
|
+
}
|
|
447
|
+
if (!('!equal' in selector)) {
|
|
448
|
+
selector['!equal'] = [];
|
|
449
|
+
}
|
|
271
450
|
for (let match of equalMatch) {
|
|
272
451
|
try {
|
|
273
|
-
let [name, value] = match.trim()
|
|
452
|
+
let [name, value] = splitn(match.trim(), '=', 2);
|
|
274
453
|
try {
|
|
275
|
-
if (name.
|
|
276
|
-
selector['!equal']
|
|
454
|
+
if (name.startsWith('-')) {
|
|
455
|
+
(selector['!equal'] as [string, any][]).push([
|
|
456
|
+
name.slice(1),
|
|
457
|
+
JSON.parse(value),
|
|
458
|
+
]);
|
|
277
459
|
} else {
|
|
278
|
-
selector.equal.push([name, JSON.parse(value)]);
|
|
460
|
+
(selector.equal as [string, any][]).push([name, JSON.parse(value)]);
|
|
279
461
|
}
|
|
280
462
|
} catch (e: any) {
|
|
281
|
-
if (name.
|
|
282
|
-
selector['!equal']
|
|
463
|
+
if (name.startsWith('-')) {
|
|
464
|
+
(selector['!equal'] as [string, any][]).push([
|
|
465
|
+
name.slice(1),
|
|
466
|
+
unQuoteString(value),
|
|
467
|
+
]);
|
|
283
468
|
} else {
|
|
284
|
-
selector.equal
|
|
469
|
+
(selector.equal as [string, any][]).push([
|
|
470
|
+
name,
|
|
471
|
+
unQuoteString(value),
|
|
472
|
+
]);
|
|
285
473
|
}
|
|
286
474
|
}
|
|
287
475
|
} catch (e: any) {
|
|
288
476
|
continue;
|
|
289
477
|
}
|
|
290
478
|
}
|
|
291
|
-
if (!selector.equal.length) {
|
|
479
|
+
if (selector.equal == null || !selector.equal.length) {
|
|
292
480
|
delete selector.equal;
|
|
293
481
|
}
|
|
294
|
-
if (!selector['!equal'].length) {
|
|
482
|
+
if (selector['!equal'] == null || !selector['!equal'].length) {
|
|
295
483
|
delete selector['!equal'];
|
|
296
484
|
}
|
|
297
485
|
}
|
|
298
486
|
curQuery = curQuery.replace(equalRegex, '');
|
|
299
487
|
|
|
300
|
-
// eg. user<{790274347f9b3a018c2cedee}> or user
|
|
301
|
-
const refRegex =
|
|
488
|
+
// eg. user<{790274347f9b3a018c2cedee}> or -user<{790274347f9b3a018c2cedee}>
|
|
489
|
+
const refRegex =
|
|
490
|
+
/(?:\s|^)-?([^-\s=\[\]<>{}()][^\s=\[\]<>{}()]*?)<\{([0-9a-f]{24})\}>(?=\s|\)|\}>|$)/g;
|
|
302
491
|
const refMatch = curQuery.match(refRegex);
|
|
303
492
|
if (refMatch) {
|
|
304
493
|
selector.ref = [];
|
|
305
494
|
selector['!ref'] = [];
|
|
306
495
|
for (let match of refMatch) {
|
|
307
496
|
try {
|
|
308
|
-
let [name, value] = match.trim().slice(0, -1)
|
|
309
|
-
if (name.
|
|
310
|
-
selector['!ref'].push([name.slice(
|
|
497
|
+
let [name, value] = splitn(match.trim().slice(0, -1), '<', 2);
|
|
498
|
+
if (name.startsWith('-')) {
|
|
499
|
+
selector['!ref'].push([name.slice(1), value.slice(1, -1)]);
|
|
311
500
|
} else {
|
|
312
501
|
selector.ref.push([name, value.slice(1, -1)]);
|
|
313
502
|
}
|
|
@@ -324,32 +513,30 @@ function selectorParser({
|
|
|
324
513
|
}
|
|
325
514
|
curQuery = curQuery.replace(refRegex, '');
|
|
326
515
|
|
|
327
|
-
// eg. someArrayOfNumbers<10> or someObject
|
|
328
|
-
const containRegex =
|
|
516
|
+
// eg. someArrayOfNumbers<10> or -someObject<"some string">
|
|
517
|
+
const containRegex =
|
|
518
|
+
/(?:\s|^)-?([^-\s=\[\]<>{}()][^\s=\[\]<>{}()]*?)(<(?:[^"][^>]*?|".*?[^\\]"))>(?=\s|\)|\}>|$)/g;
|
|
329
519
|
const containMatch = curQuery.match(containRegex);
|
|
330
520
|
if (containMatch) {
|
|
331
521
|
selector.contain = [];
|
|
332
522
|
selector['!contain'] = [];
|
|
333
523
|
for (let match of containMatch) {
|
|
334
524
|
try {
|
|
335
|
-
let [name, value] = match.trim().slice(0, -1)
|
|
525
|
+
let [name, value] = splitn(match.trim().slice(0, -1), '<', 2);
|
|
336
526
|
try {
|
|
337
|
-
if (name.
|
|
527
|
+
if (name.startsWith('-')) {
|
|
338
528
|
selector['!contain'].push([
|
|
339
|
-
name.slice(
|
|
340
|
-
JSON.parse(
|
|
529
|
+
name.slice(1),
|
|
530
|
+
JSON.parse(unQuoteString(value)),
|
|
341
531
|
]);
|
|
342
532
|
} else {
|
|
343
|
-
selector.contain.push([name, JSON.parse(
|
|
533
|
+
selector.contain.push([name, JSON.parse(unQuoteString(value))]);
|
|
344
534
|
}
|
|
345
535
|
} catch (e: any) {
|
|
346
|
-
if (name.
|
|
347
|
-
selector['!contain'].push([
|
|
348
|
-
name.slice(0, -1),
|
|
349
|
-
unQuoteAngles(value),
|
|
350
|
-
]);
|
|
536
|
+
if (name.startsWith('-')) {
|
|
537
|
+
selector['!contain'].push([name.slice(1), unQuoteString(value)]);
|
|
351
538
|
} else {
|
|
352
|
-
selector.contain.push([name,
|
|
539
|
+
selector.contain.push([name, unQuoteString(value)]);
|
|
353
540
|
}
|
|
354
541
|
}
|
|
355
542
|
} catch (e: any) {
|
|
@@ -365,8 +552,9 @@ function selectorParser({
|
|
|
365
552
|
}
|
|
366
553
|
curQuery = curQuery.replace(containRegex, '');
|
|
367
554
|
|
|
368
|
-
// eg. name~/Hunter/ or name
|
|
369
|
-
const posixRegex =
|
|
555
|
+
// eg. name~/Hunter/ or -name~/hunter/i
|
|
556
|
+
const posixRegex =
|
|
557
|
+
/(?:\s|^)-?([^-\s=\[\]<>{}()][^\s=\[\]<>{}()]*?)~(\/\/|\/.*?[^\\]\/)i?(?=\s|\)|\}>|$)/g;
|
|
370
558
|
const posixMatch = curQuery.match(posixRegex);
|
|
371
559
|
if (posixMatch) {
|
|
372
560
|
selector.match = [];
|
|
@@ -375,16 +563,16 @@ function selectorParser({
|
|
|
375
563
|
selector['!imatch'] = [];
|
|
376
564
|
for (let match of posixMatch) {
|
|
377
565
|
try {
|
|
378
|
-
let [name, value] = match.trim()
|
|
379
|
-
if (name.
|
|
566
|
+
let [name, value] = splitn(match.trim(), '~', 2);
|
|
567
|
+
if (name.startsWith('-')) {
|
|
380
568
|
if (value.endsWith('i')) {
|
|
381
569
|
selector['!imatch'].push([
|
|
382
|
-
name.slice(
|
|
570
|
+
name.slice(1),
|
|
383
571
|
value.replace(/^\/|\/i$/g, ''),
|
|
384
572
|
]);
|
|
385
573
|
} else {
|
|
386
574
|
selector['!match'].push([
|
|
387
|
-
name.slice(
|
|
575
|
+
name.slice(1),
|
|
388
576
|
value.replace(/^\/|\/$/g, ''),
|
|
389
577
|
]);
|
|
390
578
|
}
|
|
@@ -414,8 +602,9 @@ function selectorParser({
|
|
|
414
602
|
}
|
|
415
603
|
curQuery = curQuery.replace(posixRegex, '');
|
|
416
604
|
|
|
417
|
-
// eg. name~Hunter or name
|
|
418
|
-
const likeRegex =
|
|
605
|
+
// eg. name~Hunter or -name~"hunter"i
|
|
606
|
+
const likeRegex =
|
|
607
|
+
/(?:\s|^)-?([^-\s=\[\]<>{}()][^\s=\[\]<>{}()]*?)~(""i?|".*?[^\\]"i?|[^\s]+)(?=\s|\)|\}>|$)/g;
|
|
419
608
|
const likeMatch = curQuery.match(likeRegex);
|
|
420
609
|
if (likeMatch) {
|
|
421
610
|
selector.like = [];
|
|
@@ -424,15 +613,15 @@ function selectorParser({
|
|
|
424
613
|
selector['!ilike'] = [];
|
|
425
614
|
for (let match of likeMatch) {
|
|
426
615
|
try {
|
|
427
|
-
let [name, value] = match.trim()
|
|
428
|
-
if (name.
|
|
616
|
+
let [name, value] = splitn(match.trim(), '~', 2);
|
|
617
|
+
if (name.startsWith('-')) {
|
|
429
618
|
if (value.endsWith('"i')) {
|
|
430
619
|
selector['!ilike'].push([
|
|
431
|
-
name.slice(
|
|
620
|
+
name.slice(1),
|
|
432
621
|
unQuoteString(value.slice(0, -1)),
|
|
433
622
|
]);
|
|
434
623
|
} else {
|
|
435
|
-
selector['!like'].push([name.slice(
|
|
624
|
+
selector['!like'].push([name.slice(1), unQuoteString(value)]);
|
|
436
625
|
}
|
|
437
626
|
} else {
|
|
438
627
|
if (value.endsWith('"i')) {
|
|
@@ -460,16 +649,16 @@ function selectorParser({
|
|
|
460
649
|
}
|
|
461
650
|
curQuery = curQuery.replace(likeRegex, '');
|
|
462
651
|
|
|
463
|
-
// eg. {790274347f9b3a018c2cedee} or {
|
|
464
|
-
const guidRegex = /(
|
|
652
|
+
// eg. {790274347f9b3a018c2cedee} or -{790274347f9b3a018c2cedee}
|
|
653
|
+
const guidRegex = /(?:\s|^)-?\{([0-9a-f]{24})\}(?=\s|\)|\}>|$)/g;
|
|
465
654
|
const guidMatch = curQuery.match(guidRegex);
|
|
466
655
|
if (guidMatch) {
|
|
467
656
|
selector.guid = [];
|
|
468
657
|
selector['!guid'] = [];
|
|
469
658
|
for (let match of guidMatch) {
|
|
470
659
|
try {
|
|
471
|
-
let guid = match.trim().replace(
|
|
472
|
-
if (guid.startsWith('
|
|
660
|
+
let guid = match.trim().replace(/^(-?)\{|\}$/g, '$1');
|
|
661
|
+
if (guid.startsWith('-')) {
|
|
473
662
|
selector['!guid'].push(guid.slice(1));
|
|
474
663
|
} else {
|
|
475
664
|
selector.guid.push(guid);
|
|
@@ -487,16 +676,16 @@ function selectorParser({
|
|
|
487
676
|
}
|
|
488
677
|
curQuery = curQuery.replace(guidRegex, '');
|
|
489
678
|
|
|
490
|
-
// eg. [enabled] or [
|
|
491
|
-
const truthyRegex = /(
|
|
679
|
+
// eg. [enabled] or -[defaultPrimaryGroup]
|
|
680
|
+
const truthyRegex = /(?:\s|^)-?\[([^\s=\[\]<>{}]+?)\](?=\s|\)|\}>|$)/g;
|
|
492
681
|
const truthyMatch = curQuery.match(truthyRegex);
|
|
493
682
|
if (truthyMatch) {
|
|
494
683
|
selector.truthy = [];
|
|
495
684
|
selector['!truthy'] = [];
|
|
496
685
|
for (let match of truthyMatch) {
|
|
497
686
|
try {
|
|
498
|
-
let name = match.trim().replace(
|
|
499
|
-
if (name.startsWith('
|
|
687
|
+
let name = match.trim().replace(/^(-?)\[|\]$/g, '$1');
|
|
688
|
+
if (name.startsWith('-')) {
|
|
500
689
|
selector['!truthy'].push(name.slice(1));
|
|
501
690
|
} else {
|
|
502
691
|
selector.truthy.push(name);
|
|
@@ -514,16 +703,16 @@ function selectorParser({
|
|
|
514
703
|
}
|
|
515
704
|
curQuery = curQuery.replace(truthyRegex, '');
|
|
516
705
|
|
|
517
|
-
// eg. <archived> or
|
|
518
|
-
const tagRegex = /(
|
|
706
|
+
// eg. <archived> or -<archived>
|
|
707
|
+
const tagRegex = /(?:\s|^)-?<(\w+)>(?=\s|\)|\}>|$)/g;
|
|
519
708
|
const tagMatch = curQuery.match(tagRegex);
|
|
520
709
|
if (tagMatch) {
|
|
521
710
|
selector.tag = [];
|
|
522
711
|
selector['!tag'] = [];
|
|
523
712
|
for (let match of tagMatch) {
|
|
524
713
|
try {
|
|
525
|
-
let name = match.trim().replace(
|
|
526
|
-
if (name.startsWith('
|
|
714
|
+
let name = match.trim().replace(/^(-?)<|>$/g, '$1');
|
|
715
|
+
if (name.startsWith('-')) {
|
|
527
716
|
selector['!tag'].push(name.slice(1));
|
|
528
717
|
} else {
|
|
529
718
|
selector.tag.push(name);
|
|
@@ -541,15 +730,21 @@ function selectorParser({
|
|
|
541
730
|
}
|
|
542
731
|
curQuery = curQuery.replace(tagRegex, '');
|
|
543
732
|
|
|
544
|
-
// eg. cdate>15
|
|
545
|
-
const gtRegex =
|
|
733
|
+
// eg. cdate>15 or -cdate>15
|
|
734
|
+
const gtRegex =
|
|
735
|
+
/(?:\s|^)-?([^\s=\[\]<>{}]+?)>(-?\d+(?:\.\d+)?)(?=\s|\)|\}>|$)/g;
|
|
546
736
|
const gtMatch = curQuery.match(gtRegex);
|
|
547
737
|
if (gtMatch) {
|
|
548
738
|
selector.gt = [];
|
|
739
|
+
selector['!gt'] = [];
|
|
549
740
|
for (let match of gtMatch) {
|
|
550
741
|
try {
|
|
551
|
-
let [name, value] = match.trim()
|
|
552
|
-
|
|
742
|
+
let [name, value] = splitn(match.trim(), '>', 2);
|
|
743
|
+
if (name.startsWith('-')) {
|
|
744
|
+
selector['!gt'].push([name.slice(1), Number(value)]);
|
|
745
|
+
} else {
|
|
746
|
+
selector.gt.push([name, Number(value)]);
|
|
747
|
+
}
|
|
553
748
|
} catch (e: any) {
|
|
554
749
|
continue;
|
|
555
750
|
}
|
|
@@ -557,24 +752,39 @@ function selectorParser({
|
|
|
557
752
|
if (!selector.gt.length) {
|
|
558
753
|
delete selector.gt;
|
|
559
754
|
}
|
|
755
|
+
if (!selector['!gt'].length) {
|
|
756
|
+
delete selector['!gt'];
|
|
757
|
+
}
|
|
560
758
|
}
|
|
561
759
|
curQuery = curQuery.replace(gtRegex, '');
|
|
562
760
|
|
|
563
|
-
// eg. cdate>yesterday or cdate>"2 days ago"
|
|
564
|
-
const gtRelativeRegex =
|
|
761
|
+
// eg. cdate>yesterday or -cdate>"2 days ago"
|
|
762
|
+
const gtRelativeRegex =
|
|
763
|
+
/(?:\s|^)-?([^\s=\[\]<>{}]+?)>(\w+|"[^"]+")(?=\s|\)|\}>|$)/g;
|
|
565
764
|
const gtRelativeMatch = curQuery.match(gtRelativeRegex);
|
|
566
765
|
if (gtRelativeMatch) {
|
|
567
766
|
if (selector.gt == null) {
|
|
568
767
|
selector.gt = [];
|
|
569
768
|
}
|
|
769
|
+
if (selector['!gt'] == null) {
|
|
770
|
+
selector['!gt'] = [];
|
|
771
|
+
}
|
|
570
772
|
for (let match of gtRelativeMatch) {
|
|
571
773
|
try {
|
|
572
|
-
let [name, value] = match.trim()
|
|
573
|
-
(
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
774
|
+
let [name, value] = splitn(match.trim(), '>', 2);
|
|
775
|
+
if (name.startsWith('-')) {
|
|
776
|
+
(selector['!gt'] as [string, null, string][]).push([
|
|
777
|
+
name.slice(1),
|
|
778
|
+
null,
|
|
779
|
+
value.replace(/"/g, ''),
|
|
780
|
+
]);
|
|
781
|
+
} else {
|
|
782
|
+
(selector.gt as [string, null, string][]).push([
|
|
783
|
+
name,
|
|
784
|
+
null,
|
|
785
|
+
value.replace(/"/g, ''),
|
|
786
|
+
]);
|
|
787
|
+
}
|
|
578
788
|
} catch (e: any) {
|
|
579
789
|
continue;
|
|
580
790
|
}
|
|
@@ -582,18 +792,27 @@ function selectorParser({
|
|
|
582
792
|
if (!selector.gt.length) {
|
|
583
793
|
delete selector.gt;
|
|
584
794
|
}
|
|
795
|
+
if (!selector['!gt'].length) {
|
|
796
|
+
delete selector['!gt'];
|
|
797
|
+
}
|
|
585
798
|
}
|
|
586
799
|
curQuery = curQuery.replace(gtRelativeRegex, '');
|
|
587
800
|
|
|
588
|
-
// eg. cdate>=15
|
|
589
|
-
const gteRegex =
|
|
801
|
+
// eg. cdate>=15 or -cdate>=15
|
|
802
|
+
const gteRegex =
|
|
803
|
+
/(?:\s|^)-?([^\s=\[\]<>{}]+?)>=(-?\d+(?:\.\d+)?)(?=\s|\)|\}>|$)/g;
|
|
590
804
|
const gteMatch = curQuery.match(gteRegex);
|
|
591
805
|
if (gteMatch) {
|
|
592
806
|
selector.gte = [];
|
|
807
|
+
selector['!gte'] = [];
|
|
593
808
|
for (let match of gteMatch) {
|
|
594
809
|
try {
|
|
595
|
-
let [name, value] = match.trim()
|
|
596
|
-
|
|
810
|
+
let [name, value] = splitn(match.trim(), '>=', 2);
|
|
811
|
+
if (name.startsWith('-')) {
|
|
812
|
+
selector['!gte'].push([name.slice(1), Number(value)]);
|
|
813
|
+
} else {
|
|
814
|
+
selector.gte.push([name, Number(value)]);
|
|
815
|
+
}
|
|
597
816
|
} catch (e: any) {
|
|
598
817
|
continue;
|
|
599
818
|
}
|
|
@@ -601,24 +820,39 @@ function selectorParser({
|
|
|
601
820
|
if (!selector.gte.length) {
|
|
602
821
|
delete selector.gte;
|
|
603
822
|
}
|
|
823
|
+
if (!selector['!gte'].length) {
|
|
824
|
+
delete selector['!gte'];
|
|
825
|
+
}
|
|
604
826
|
}
|
|
605
827
|
curQuery = curQuery.replace(gteRegex, '');
|
|
606
828
|
|
|
607
|
-
// eg. cdate>=yesterday or cdate>="2 days ago"
|
|
608
|
-
const gteRelativeRegex =
|
|
829
|
+
// eg. cdate>=yesterday or -cdate>="2 days ago"
|
|
830
|
+
const gteRelativeRegex =
|
|
831
|
+
/(?:\s|^)-?([^\s=\[\]<>{}]+?)>=(\w+|"[^"]+")(?=\s|\)|\}>|$)/g;
|
|
609
832
|
const gteRelativeMatch = curQuery.match(gteRelativeRegex);
|
|
610
833
|
if (gteRelativeMatch) {
|
|
611
834
|
if (selector.gte == null) {
|
|
612
835
|
selector.gte = [];
|
|
613
836
|
}
|
|
837
|
+
if (selector['!gte'] == null) {
|
|
838
|
+
selector['!gte'] = [];
|
|
839
|
+
}
|
|
614
840
|
for (let match of gteRelativeMatch) {
|
|
615
841
|
try {
|
|
616
|
-
let [name, value] = match.trim()
|
|
617
|
-
(
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
842
|
+
let [name, value] = splitn(match.trim(), '>=', 2);
|
|
843
|
+
if (name.startsWith('-')) {
|
|
844
|
+
(selector['!gte'] as [string, null, string][]).push([
|
|
845
|
+
name.slice(1),
|
|
846
|
+
null,
|
|
847
|
+
value.replace(/"/g, ''),
|
|
848
|
+
]);
|
|
849
|
+
} else {
|
|
850
|
+
(selector.gte as [string, null, string][]).push([
|
|
851
|
+
name,
|
|
852
|
+
null,
|
|
853
|
+
value.replace(/"/g, ''),
|
|
854
|
+
]);
|
|
855
|
+
}
|
|
622
856
|
} catch (e: any) {
|
|
623
857
|
continue;
|
|
624
858
|
}
|
|
@@ -626,18 +860,27 @@ function selectorParser({
|
|
|
626
860
|
if (!selector.gte.length) {
|
|
627
861
|
delete selector.gte;
|
|
628
862
|
}
|
|
863
|
+
if (!selector['!gte'].length) {
|
|
864
|
+
delete selector['!gte'];
|
|
865
|
+
}
|
|
629
866
|
}
|
|
630
867
|
curQuery = curQuery.replace(gteRelativeRegex, '');
|
|
631
868
|
|
|
632
|
-
// eg. cdate<15
|
|
633
|
-
const ltRegex =
|
|
869
|
+
// eg. cdate<15 or -cdate<15
|
|
870
|
+
const ltRegex =
|
|
871
|
+
/(?:\s|^)-?([^\s=\[\]<>{}]+?)<(-?\d+(?:\.\d+)?)(?=\s|\)|\}>|$)/g;
|
|
634
872
|
const ltMatch = curQuery.match(ltRegex);
|
|
635
873
|
if (ltMatch) {
|
|
636
874
|
selector.lt = [];
|
|
875
|
+
selector['!lt'] = [];
|
|
637
876
|
for (let match of ltMatch) {
|
|
638
877
|
try {
|
|
639
|
-
let [name, value] = match.trim()
|
|
640
|
-
|
|
878
|
+
let [name, value] = splitn(match.trim(), '<', 2);
|
|
879
|
+
if (name.startsWith('-')) {
|
|
880
|
+
selector['!lt'].push([name.slice(1), Number(value)]);
|
|
881
|
+
} else {
|
|
882
|
+
selector.lt.push([name, Number(value)]);
|
|
883
|
+
}
|
|
641
884
|
} catch (e: any) {
|
|
642
885
|
continue;
|
|
643
886
|
}
|
|
@@ -645,24 +888,39 @@ function selectorParser({
|
|
|
645
888
|
if (!selector.lt.length) {
|
|
646
889
|
delete selector.lt;
|
|
647
890
|
}
|
|
891
|
+
if (!selector['!lt'].length) {
|
|
892
|
+
delete selector['!lt'];
|
|
893
|
+
}
|
|
648
894
|
}
|
|
649
895
|
curQuery = curQuery.replace(ltRegex, '');
|
|
650
896
|
|
|
651
|
-
// eg. cdate<yesterday or cdate<"2 days ago"
|
|
652
|
-
const ltRelativeRegex =
|
|
897
|
+
// eg. cdate<yesterday or -cdate<"2 days ago"
|
|
898
|
+
const ltRelativeRegex =
|
|
899
|
+
/(?:\s|^)-?([^\s=\[\]<>{}]+?)<(\w+|"[^"]+")(?=\s|\)|\}>|$)/g;
|
|
653
900
|
const ltRelativeMatch = curQuery.match(ltRelativeRegex);
|
|
654
901
|
if (ltRelativeMatch) {
|
|
655
902
|
if (selector.lt == null) {
|
|
656
903
|
selector.lt = [];
|
|
657
904
|
}
|
|
905
|
+
if (selector['!lt'] == null) {
|
|
906
|
+
selector['!lt'] = [];
|
|
907
|
+
}
|
|
658
908
|
for (let match of ltRelativeMatch) {
|
|
659
909
|
try {
|
|
660
|
-
let [name, value] = match.trim()
|
|
661
|
-
(
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
910
|
+
let [name, value] = splitn(match.trim(), '<', 2);
|
|
911
|
+
if (name.startsWith('-')) {
|
|
912
|
+
(selector['!lt'] as [string, null, string][]).push([
|
|
913
|
+
name.slice(1),
|
|
914
|
+
null,
|
|
915
|
+
value.replace(/"/g, ''),
|
|
916
|
+
]);
|
|
917
|
+
} else {
|
|
918
|
+
(selector.lt as [string, null, string][]).push([
|
|
919
|
+
name,
|
|
920
|
+
null,
|
|
921
|
+
value.replace(/"/g, ''),
|
|
922
|
+
]);
|
|
923
|
+
}
|
|
666
924
|
} catch (e: any) {
|
|
667
925
|
continue;
|
|
668
926
|
}
|
|
@@ -670,18 +928,27 @@ function selectorParser({
|
|
|
670
928
|
if (!selector.lt.length) {
|
|
671
929
|
delete selector.lt;
|
|
672
930
|
}
|
|
931
|
+
if (!selector['!lt'].length) {
|
|
932
|
+
delete selector['!lt'];
|
|
933
|
+
}
|
|
673
934
|
}
|
|
674
935
|
curQuery = curQuery.replace(ltRelativeRegex, '');
|
|
675
936
|
|
|
676
|
-
// eg. cdate<=15
|
|
677
|
-
const lteRegex =
|
|
937
|
+
// eg. cdate<=15 or -cdate<=15
|
|
938
|
+
const lteRegex =
|
|
939
|
+
/(?:\s|^)-?([^\s=\[\]<>{}]+?)<=(-?\d+(?:\.\d+)?)(?=\s|\)|\}>|$)/g;
|
|
678
940
|
const lteMatch = curQuery.match(lteRegex);
|
|
679
941
|
if (lteMatch) {
|
|
680
942
|
selector.lte = [];
|
|
943
|
+
selector['!lte'] = [];
|
|
681
944
|
for (let match of lteMatch) {
|
|
682
945
|
try {
|
|
683
|
-
let [name, value] = match.trim()
|
|
684
|
-
|
|
946
|
+
let [name, value] = splitn(match.trim(), '<=', 2);
|
|
947
|
+
if (name.startsWith('-')) {
|
|
948
|
+
selector['!lte'].push([name.slice(1), Number(value)]);
|
|
949
|
+
} else {
|
|
950
|
+
selector.lte.push([name, Number(value)]);
|
|
951
|
+
}
|
|
685
952
|
} catch (e: any) {
|
|
686
953
|
continue;
|
|
687
954
|
}
|
|
@@ -689,24 +956,39 @@ function selectorParser({
|
|
|
689
956
|
if (!selector.lte.length) {
|
|
690
957
|
delete selector.lte;
|
|
691
958
|
}
|
|
959
|
+
if (!selector['!lte'].length) {
|
|
960
|
+
delete selector['!lte'];
|
|
961
|
+
}
|
|
692
962
|
}
|
|
693
963
|
curQuery = curQuery.replace(lteRegex, '');
|
|
694
964
|
|
|
695
|
-
// eg. cdate<=yesterday or cdate<="2 days ago"
|
|
696
|
-
const lteRelativeRegex =
|
|
965
|
+
// eg. cdate<=yesterday or -cdate<="2 days ago"
|
|
966
|
+
const lteRelativeRegex =
|
|
967
|
+
/(?:\s|^)-?([^\s=\[\]<>{}]+?)<=(\w+|"[^"]+")(?=\s|\)|\}>|$)/g;
|
|
697
968
|
const lteRelativeMatch = curQuery.match(lteRelativeRegex);
|
|
698
969
|
if (lteRelativeMatch) {
|
|
699
970
|
if (selector.lte == null) {
|
|
700
971
|
selector.lte = [];
|
|
701
972
|
}
|
|
973
|
+
if (selector['!lte'] == null) {
|
|
974
|
+
selector['!lte'] = [];
|
|
975
|
+
}
|
|
702
976
|
for (let match of lteRelativeMatch) {
|
|
703
977
|
try {
|
|
704
|
-
let [name, value] = match.trim()
|
|
705
|
-
(
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
978
|
+
let [name, value] = splitn(match.trim(), '<=', 2);
|
|
979
|
+
if (name.startsWith('-')) {
|
|
980
|
+
(selector['!lte'] as [string, null, string][]).push([
|
|
981
|
+
name.slice(1),
|
|
982
|
+
null,
|
|
983
|
+
value.replace(/"/g, ''),
|
|
984
|
+
]);
|
|
985
|
+
} else {
|
|
986
|
+
(selector.lte as [string, null, string][]).push([
|
|
987
|
+
name,
|
|
988
|
+
null,
|
|
989
|
+
value.replace(/"/g, ''),
|
|
990
|
+
]);
|
|
991
|
+
}
|
|
710
992
|
} catch (e: any) {
|
|
711
993
|
continue;
|
|
712
994
|
}
|
|
@@ -714,6 +996,9 @@ function selectorParser({
|
|
|
714
996
|
if (!selector.lte.length) {
|
|
715
997
|
delete selector.lte;
|
|
716
998
|
}
|
|
999
|
+
if (!selector['!lte'].length) {
|
|
1000
|
+
delete selector['!lte'];
|
|
1001
|
+
}
|
|
717
1002
|
}
|
|
718
1003
|
curQuery = curQuery.replace(lteRelativeRegex, '');
|
|
719
1004
|
|