@aikotools/datafilter 1.0.4 → 1.1.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 +93 -7
- package/dist/aikotools-datafilter.cjs +937 -1
- package/dist/aikotools-datafilter.cjs.map +1 -0
- package/dist/aikotools-datafilter.mjs +677 -354
- package/dist/aikotools-datafilter.mjs.map +1 -0
- package/dist/src/core/types.d.ts +46 -0
- package/dist/src/core/types.d.ts.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/matcher/Matcher.d.ts +10 -4
- package/dist/src/matcher/Matcher.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1,937 @@
|
|
|
1
|
-
"use strict";
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const luxon = require("luxon");
|
|
4
|
+
function isWildcardRule(rule) {
|
|
5
|
+
return "matchAny" in rule;
|
|
6
|
+
}
|
|
7
|
+
function isSingleMatchRule(rule) {
|
|
8
|
+
return "match" in rule && "expected" in rule;
|
|
9
|
+
}
|
|
10
|
+
function getValueFromPath(object, path) {
|
|
11
|
+
const validPath = [];
|
|
12
|
+
if (path.length === 0) {
|
|
13
|
+
return {
|
|
14
|
+
value: object,
|
|
15
|
+
found: true,
|
|
16
|
+
validPath: []
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
let current = object;
|
|
20
|
+
for (let i = 0; i < path.length; i++) {
|
|
21
|
+
const segment = path[i];
|
|
22
|
+
if (current === null || current === void 0) {
|
|
23
|
+
return {
|
|
24
|
+
value: void 0,
|
|
25
|
+
found: false,
|
|
26
|
+
error: `Cannot read property '${segment}' of ${current}`,
|
|
27
|
+
validPath
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (Array.isArray(current)) {
|
|
31
|
+
const index = typeof segment === "number" ? segment : parseInt(String(segment), 10);
|
|
32
|
+
if (isNaN(index)) {
|
|
33
|
+
return {
|
|
34
|
+
value: void 0,
|
|
35
|
+
found: false,
|
|
36
|
+
error: `Array index must be a number, got '${segment}'`,
|
|
37
|
+
validPath
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (index < 0 || index >= current.length) {
|
|
41
|
+
return {
|
|
42
|
+
value: void 0,
|
|
43
|
+
found: false,
|
|
44
|
+
error: `Array index ${index} out of bounds (length: ${current.length})`,
|
|
45
|
+
validPath
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
validPath.push(index);
|
|
49
|
+
current = current[index];
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (typeof current === "object") {
|
|
53
|
+
const key = String(segment);
|
|
54
|
+
if (!(key in current)) {
|
|
55
|
+
return {
|
|
56
|
+
value: void 0,
|
|
57
|
+
found: false,
|
|
58
|
+
error: `Property '${key}' does not exist`,
|
|
59
|
+
validPath
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
validPath.push(key);
|
|
63
|
+
current = current[key];
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
value: void 0,
|
|
68
|
+
found: false,
|
|
69
|
+
error: `Cannot access property '${segment}' of primitive type ${typeof current}`,
|
|
70
|
+
validPath
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
value: current,
|
|
75
|
+
found: true,
|
|
76
|
+
validPath
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function pathExists(object, path) {
|
|
80
|
+
const result = getValueFromPath(object, path);
|
|
81
|
+
return result.found && result.value !== void 0;
|
|
82
|
+
}
|
|
83
|
+
function getValueOr(object, path, defaultValue) {
|
|
84
|
+
const result = getValueFromPath(object, path);
|
|
85
|
+
return result.found && result.value !== void 0 ? result.value : defaultValue;
|
|
86
|
+
}
|
|
87
|
+
class FilterEngine {
|
|
88
|
+
constructor(context) {
|
|
89
|
+
this.context = context;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Evaluates a single filter criterion against a data object.
|
|
93
|
+
*
|
|
94
|
+
* @param data - The data object to check
|
|
95
|
+
* @param criterion - The filter criterion to evaluate
|
|
96
|
+
* @returns FilterCheckResult indicating success or failure
|
|
97
|
+
*/
|
|
98
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
99
|
+
evaluateCriterion(data, criterion) {
|
|
100
|
+
const check = criterion.check;
|
|
101
|
+
if ("value" in check) {
|
|
102
|
+
return this.checkValue(data, criterion.path, check);
|
|
103
|
+
}
|
|
104
|
+
if ("exists" in check) {
|
|
105
|
+
return this.checkExists(data, criterion.path, check);
|
|
106
|
+
}
|
|
107
|
+
if ("itemExists" in check && "item" in check) {
|
|
108
|
+
return this.checkArrayElement(data, criterion.path, check);
|
|
109
|
+
}
|
|
110
|
+
if ("type" in check && "size" in check) {
|
|
111
|
+
return this.checkArraySize(data, criterion.path, check);
|
|
112
|
+
}
|
|
113
|
+
if ("min" in check && "max" in check) {
|
|
114
|
+
if (typeof check.min === "number" && typeof check.max === "number") {
|
|
115
|
+
return this.checkNumericRange(data, criterion.path, check);
|
|
116
|
+
} else {
|
|
117
|
+
return this.checkTimeRange(data, criterion.path, check);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
status: false,
|
|
122
|
+
checkType: "unknown",
|
|
123
|
+
reason: `Unknown check type: ${JSON.stringify(check)}`
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Checks if a value matches an expected value using deep equality.
|
|
128
|
+
*
|
|
129
|
+
* @param data - The data object
|
|
130
|
+
* @param path - Path to the value
|
|
131
|
+
* @param check - The value check specification
|
|
132
|
+
* @returns FilterCheckResult
|
|
133
|
+
*/
|
|
134
|
+
checkValue(data, path, check) {
|
|
135
|
+
const accessResult = getValueFromPath(data, path);
|
|
136
|
+
if (!accessResult.found) {
|
|
137
|
+
return {
|
|
138
|
+
status: false,
|
|
139
|
+
checkType: "checkValue",
|
|
140
|
+
reason: {
|
|
141
|
+
message: accessResult.error || "Path not found",
|
|
142
|
+
path
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
const actual = accessResult.value;
|
|
147
|
+
const expected = check.value;
|
|
148
|
+
if (this.deepEqual(actual, expected)) {
|
|
149
|
+
return {
|
|
150
|
+
status: true,
|
|
151
|
+
checkType: "checkValue"
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
status: false,
|
|
156
|
+
checkType: "checkValue",
|
|
157
|
+
reason: {
|
|
158
|
+
message: "Value mismatch",
|
|
159
|
+
path,
|
|
160
|
+
expected,
|
|
161
|
+
actual
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Checks if a path exists in the data object.
|
|
167
|
+
*
|
|
168
|
+
* @param data - The data object
|
|
169
|
+
* @param path - Path to check
|
|
170
|
+
* @param check - The exists check specification
|
|
171
|
+
* @returns FilterCheckResult
|
|
172
|
+
*/
|
|
173
|
+
checkExists(data, path, check) {
|
|
174
|
+
const accessResult = getValueFromPath(data, path);
|
|
175
|
+
const exists = accessResult.found && accessResult.value !== void 0;
|
|
176
|
+
if (check.exists === exists) {
|
|
177
|
+
return {
|
|
178
|
+
status: true,
|
|
179
|
+
checkType: "checkExists"
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
status: false,
|
|
184
|
+
checkType: "checkExists",
|
|
185
|
+
reason: {
|
|
186
|
+
message: check.exists ? `Path should exist but doesn't: ${accessResult.error}` : "Path should not exist but does",
|
|
187
|
+
path
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Checks if an array contains (or doesn't contain) a specific element.
|
|
193
|
+
*
|
|
194
|
+
* @param data - The data object
|
|
195
|
+
* @param path - Path to the array
|
|
196
|
+
* @param check - The array element check specification
|
|
197
|
+
* @returns FilterCheckResult
|
|
198
|
+
*/
|
|
199
|
+
checkArrayElement(data, path, check) {
|
|
200
|
+
const accessResult = getValueFromPath(data, path);
|
|
201
|
+
if (!accessResult.found) {
|
|
202
|
+
return {
|
|
203
|
+
status: false,
|
|
204
|
+
checkType: "checkArrayElement",
|
|
205
|
+
reason: {
|
|
206
|
+
message: accessResult.error || "Path not found",
|
|
207
|
+
path
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
const array = accessResult.value;
|
|
212
|
+
if (!Array.isArray(array)) {
|
|
213
|
+
return {
|
|
214
|
+
status: false,
|
|
215
|
+
checkType: "checkArrayElement",
|
|
216
|
+
reason: {
|
|
217
|
+
message: "Value is not an array",
|
|
218
|
+
path,
|
|
219
|
+
actualType: typeof array
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
const found = array.some((elem) => this.deepEqual(elem, check.item));
|
|
224
|
+
if (check.itemExists === found) {
|
|
225
|
+
return {
|
|
226
|
+
status: true,
|
|
227
|
+
checkType: "checkArrayElement"
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
status: false,
|
|
232
|
+
checkType: "checkArrayElement",
|
|
233
|
+
reason: {
|
|
234
|
+
message: check.itemExists ? "Item should exist in array but doesn't" : "Item should not exist in array but does",
|
|
235
|
+
path,
|
|
236
|
+
item: check.item
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Checks if an array has the expected size.
|
|
242
|
+
*
|
|
243
|
+
* @param data - The data object
|
|
244
|
+
* @param path - Path to the array
|
|
245
|
+
* @param check - The array size check specification
|
|
246
|
+
* @returns FilterCheckResult
|
|
247
|
+
*/
|
|
248
|
+
checkArraySize(data, path, check) {
|
|
249
|
+
const accessResult = getValueFromPath(data, path);
|
|
250
|
+
if (!accessResult.found) {
|
|
251
|
+
return {
|
|
252
|
+
status: false,
|
|
253
|
+
checkType: "checkArraySize",
|
|
254
|
+
reason: {
|
|
255
|
+
message: accessResult.error || "Path not found",
|
|
256
|
+
path
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
const array = accessResult.value;
|
|
261
|
+
if (!Array.isArray(array)) {
|
|
262
|
+
return {
|
|
263
|
+
status: false,
|
|
264
|
+
checkType: "checkArraySize",
|
|
265
|
+
reason: {
|
|
266
|
+
message: "Value is not an array",
|
|
267
|
+
path,
|
|
268
|
+
actualType: typeof array
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
const actualSize = array.length;
|
|
273
|
+
const expectedSize = check.size;
|
|
274
|
+
let passes = false;
|
|
275
|
+
let message = "";
|
|
276
|
+
switch (check.type) {
|
|
277
|
+
case "equal":
|
|
278
|
+
passes = actualSize === expectedSize;
|
|
279
|
+
message = `Array length should be ${expectedSize} but is ${actualSize}`;
|
|
280
|
+
break;
|
|
281
|
+
case "lessThan":
|
|
282
|
+
passes = actualSize < expectedSize;
|
|
283
|
+
message = `Array length should be less than ${expectedSize} but is ${actualSize}`;
|
|
284
|
+
break;
|
|
285
|
+
case "greaterThan":
|
|
286
|
+
passes = actualSize > expectedSize;
|
|
287
|
+
message = `Array length should be greater than ${expectedSize} but is ${actualSize}`;
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
if (passes) {
|
|
291
|
+
return {
|
|
292
|
+
status: true,
|
|
293
|
+
checkType: "checkArraySize"
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
status: false,
|
|
298
|
+
checkType: "checkArraySize",
|
|
299
|
+
reason: {
|
|
300
|
+
message,
|
|
301
|
+
path,
|
|
302
|
+
expected: expectedSize,
|
|
303
|
+
actual: actualSize
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Checks if a timestamp is within the expected time range.
|
|
309
|
+
*
|
|
310
|
+
* @param data - The data object
|
|
311
|
+
* @param path - Path to the timestamp
|
|
312
|
+
* @param check - The time range check specification
|
|
313
|
+
* @returns FilterCheckResult
|
|
314
|
+
*/
|
|
315
|
+
checkTimeRange(data, path, check) {
|
|
316
|
+
const accessResult = getValueFromPath(data, path);
|
|
317
|
+
if (!accessResult.found) {
|
|
318
|
+
return {
|
|
319
|
+
status: false,
|
|
320
|
+
checkType: "checkTimeRange",
|
|
321
|
+
reason: {
|
|
322
|
+
message: accessResult.error || "Path not found",
|
|
323
|
+
path
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
const value = accessResult.value;
|
|
328
|
+
if (typeof value === "number") {
|
|
329
|
+
const min = parseInt(check.min);
|
|
330
|
+
const max = parseInt(check.max);
|
|
331
|
+
if (isNaN(min) || isNaN(max)) {
|
|
332
|
+
return {
|
|
333
|
+
status: false,
|
|
334
|
+
checkType: "checkTimeRange",
|
|
335
|
+
reason: {
|
|
336
|
+
message: "Min or max is not a valid number",
|
|
337
|
+
min: check.min,
|
|
338
|
+
max: check.max
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
if (value >= min && value <= max) {
|
|
343
|
+
return {
|
|
344
|
+
status: true,
|
|
345
|
+
checkType: "checkTimeRange"
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
return {
|
|
349
|
+
status: false,
|
|
350
|
+
checkType: "checkTimeRange",
|
|
351
|
+
reason: {
|
|
352
|
+
message: `Timestamp ${value} is outside range [${min}, ${max}]`,
|
|
353
|
+
path,
|
|
354
|
+
actual: value,
|
|
355
|
+
min,
|
|
356
|
+
max
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
if (typeof value === "string") {
|
|
361
|
+
const timestamp = luxon.DateTime.fromISO(value);
|
|
362
|
+
const minTime = luxon.DateTime.fromISO(check.min);
|
|
363
|
+
const maxTime = luxon.DateTime.fromISO(check.max);
|
|
364
|
+
if (!timestamp.isValid) {
|
|
365
|
+
return {
|
|
366
|
+
status: false,
|
|
367
|
+
checkType: "checkTimeRange",
|
|
368
|
+
reason: {
|
|
369
|
+
message: `Invalid timestamp: ${value}`,
|
|
370
|
+
path
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
if (!minTime.isValid || !maxTime.isValid) {
|
|
375
|
+
return {
|
|
376
|
+
status: false,
|
|
377
|
+
checkType: "checkTimeRange",
|
|
378
|
+
reason: {
|
|
379
|
+
message: "Invalid min or max time",
|
|
380
|
+
min: check.min,
|
|
381
|
+
max: check.max
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
if (timestamp >= minTime && timestamp <= maxTime) {
|
|
386
|
+
return {
|
|
387
|
+
status: true,
|
|
388
|
+
checkType: "checkTimeRange"
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
return {
|
|
392
|
+
status: false,
|
|
393
|
+
checkType: "checkTimeRange",
|
|
394
|
+
reason: {
|
|
395
|
+
message: `Timestamp ${value} is outside range [${check.min}, ${check.max}]`,
|
|
396
|
+
path,
|
|
397
|
+
actual: value,
|
|
398
|
+
min: check.min,
|
|
399
|
+
max: check.max
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
return {
|
|
404
|
+
status: false,
|
|
405
|
+
checkType: "checkTimeRange",
|
|
406
|
+
reason: {
|
|
407
|
+
message: `Timestamp must be a string or number, got ${typeof value}`,
|
|
408
|
+
path
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Checks if a numeric value is within the expected range.
|
|
414
|
+
*
|
|
415
|
+
* @param data - The data object
|
|
416
|
+
* @param path - Path to the numeric value
|
|
417
|
+
* @param check - The numeric range check specification
|
|
418
|
+
* @returns FilterCheckResult
|
|
419
|
+
*/
|
|
420
|
+
checkNumericRange(data, path, check) {
|
|
421
|
+
const accessResult = getValueFromPath(data, path);
|
|
422
|
+
if (!accessResult.found) {
|
|
423
|
+
return {
|
|
424
|
+
status: false,
|
|
425
|
+
checkType: "checkNumericRange",
|
|
426
|
+
reason: {
|
|
427
|
+
message: accessResult.error || "Path not found",
|
|
428
|
+
path
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
const value = accessResult.value;
|
|
433
|
+
if (typeof value !== "number") {
|
|
434
|
+
return {
|
|
435
|
+
status: false,
|
|
436
|
+
checkType: "checkNumericRange",
|
|
437
|
+
reason: {
|
|
438
|
+
message: `Value must be a number, got ${typeof value}`,
|
|
439
|
+
path,
|
|
440
|
+
actual: value
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
if (value >= check.min && value <= check.max) {
|
|
445
|
+
return {
|
|
446
|
+
status: true,
|
|
447
|
+
checkType: "checkNumericRange"
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
return {
|
|
451
|
+
status: false,
|
|
452
|
+
checkType: "checkNumericRange",
|
|
453
|
+
reason: {
|
|
454
|
+
message: `Value ${value} is outside range [${check.min}, ${check.max}]`,
|
|
455
|
+
path,
|
|
456
|
+
actual: value,
|
|
457
|
+
min: check.min,
|
|
458
|
+
max: check.max
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Deep equality comparison.
|
|
464
|
+
* Compares two values recursively for equality.
|
|
465
|
+
*/
|
|
466
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
467
|
+
deepEqual(a, b) {
|
|
468
|
+
if (a === b) return true;
|
|
469
|
+
if (a === null || b === null) return a === b;
|
|
470
|
+
if (a === void 0 || b === void 0) return a === b;
|
|
471
|
+
if (typeof a !== typeof b) return false;
|
|
472
|
+
if (a instanceof Date && b instanceof Date) {
|
|
473
|
+
return a.getTime() === b.getTime();
|
|
474
|
+
}
|
|
475
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
476
|
+
if (a.length !== b.length) return false;
|
|
477
|
+
return a.every((val, idx) => this.deepEqual(val, b[idx]));
|
|
478
|
+
}
|
|
479
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
480
|
+
const keysA = Object.keys(a);
|
|
481
|
+
const keysB = Object.keys(b);
|
|
482
|
+
if (keysA.length !== keysB.length) return false;
|
|
483
|
+
return keysA.every((key) => this.deepEqual(a[key], b[key]));
|
|
484
|
+
}
|
|
485
|
+
return a === b;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
class Matcher {
|
|
489
|
+
constructor(context) {
|
|
490
|
+
this.engine = new FilterEngine(context);
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Matches a single file against a rule.
|
|
494
|
+
*
|
|
495
|
+
* @param file - The file to match
|
|
496
|
+
* @param rule - The rule to match against
|
|
497
|
+
* @returns MatchResult indicating if all criteria matched
|
|
498
|
+
*/
|
|
499
|
+
matchFile(file, rule) {
|
|
500
|
+
const criteria = isWildcardRule(rule) ? rule.matchAny : rule.match;
|
|
501
|
+
const checks = criteria.map((criterion) => {
|
|
502
|
+
return this.engine.evaluateCriterion(file.data, criterion);
|
|
503
|
+
});
|
|
504
|
+
const matched = checks.every((check) => check.status);
|
|
505
|
+
return {
|
|
506
|
+
matched,
|
|
507
|
+
checks,
|
|
508
|
+
rule
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Applies pre-filter criteria to files, separating files that match from those that don't.
|
|
513
|
+
*
|
|
514
|
+
* @param files - Files to filter
|
|
515
|
+
* @param preFilter - Filter criteria that all files must match
|
|
516
|
+
* @returns Object with matched files and excluded files (with failed checks)
|
|
517
|
+
*/
|
|
518
|
+
applyPreFilter(files, preFilter) {
|
|
519
|
+
const matched = [];
|
|
520
|
+
const excluded = [];
|
|
521
|
+
for (const file of files) {
|
|
522
|
+
const checks = preFilter.map((criterion) => {
|
|
523
|
+
return this.engine.evaluateCriterion(file.data, criterion);
|
|
524
|
+
});
|
|
525
|
+
if (checks.every((check) => check.status)) {
|
|
526
|
+
matched.push(file);
|
|
527
|
+
} else {
|
|
528
|
+
excluded.push({
|
|
529
|
+
file,
|
|
530
|
+
failedChecks: checks.filter((check) => !check.status)
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return { matched, excluded };
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Main filtering function that processes files according to rules.
|
|
538
|
+
*
|
|
539
|
+
* @param files - Files to filter
|
|
540
|
+
* @param rules - Matching rules (can include arrays for flexible ordering)
|
|
541
|
+
* @param sortFn - Optional sort function for file ordering
|
|
542
|
+
* @param preFilter - Optional pre-filter criteria (files not matching are excluded)
|
|
543
|
+
* @param mode - Matching mode ('strict', 'optional', or 'strict-optional')
|
|
544
|
+
* @returns FilterResult with mapped, wildcardMatched, optionalFiles and unmapped files
|
|
545
|
+
*/
|
|
546
|
+
filterFiles(files, rules, sortFn, preFilter, mode = "strict") {
|
|
547
|
+
let filteredFiles;
|
|
548
|
+
let preFiltered = [];
|
|
549
|
+
if (preFilter) {
|
|
550
|
+
const preFilterResult = this.applyPreFilter(files, preFilter);
|
|
551
|
+
filteredFiles = preFilterResult.matched;
|
|
552
|
+
preFiltered = preFilterResult.excluded;
|
|
553
|
+
} else {
|
|
554
|
+
filteredFiles = files;
|
|
555
|
+
}
|
|
556
|
+
const sortedFiles = sortFn ? [...filteredFiles].sort(sortFn) : [...filteredFiles];
|
|
557
|
+
const mapped = [];
|
|
558
|
+
const wildcardMatched = [];
|
|
559
|
+
const unmapped = [];
|
|
560
|
+
const optionalFiles = [];
|
|
561
|
+
const matchedFileIndices = /* @__PURE__ */ new Set();
|
|
562
|
+
if (mode === "optional" || mode === "strict-optional") {
|
|
563
|
+
return this.filterFilesOptionalMode(sortedFiles, rules, preFiltered, files.length, mode);
|
|
564
|
+
}
|
|
565
|
+
const usedFlexibleRules = /* @__PURE__ */ new Map();
|
|
566
|
+
let fileIndex = 0;
|
|
567
|
+
let ruleIndex = 0;
|
|
568
|
+
while (fileIndex < sortedFiles.length) {
|
|
569
|
+
const file = sortedFiles[fileIndex];
|
|
570
|
+
if (ruleIndex >= rules.length) {
|
|
571
|
+
if (!matchedFileIndices.has(fileIndex)) {
|
|
572
|
+
unmapped.push({
|
|
573
|
+
file,
|
|
574
|
+
attemptedRules: []
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
fileIndex++;
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
const ruleOrRules = rules[ruleIndex];
|
|
581
|
+
const attemptedMatches = [];
|
|
582
|
+
let fileMatched = false;
|
|
583
|
+
if (Array.isArray(ruleOrRules)) {
|
|
584
|
+
for (let subIndex = 0; subIndex < ruleOrRules.length; subIndex++) {
|
|
585
|
+
const used = usedFlexibleRules.get(ruleIndex)?.has(subIndex);
|
|
586
|
+
if (used) {
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
const rule = ruleOrRules[subIndex];
|
|
590
|
+
const matchResult = this.matchFile(file, rule);
|
|
591
|
+
attemptedMatches.push(matchResult);
|
|
592
|
+
if (matchResult.matched && isSingleMatchRule(rule)) {
|
|
593
|
+
if (!usedFlexibleRules.has(ruleIndex)) {
|
|
594
|
+
usedFlexibleRules.set(ruleIndex, /* @__PURE__ */ new Set());
|
|
595
|
+
}
|
|
596
|
+
const usedSet = usedFlexibleRules.get(ruleIndex);
|
|
597
|
+
if (usedSet) {
|
|
598
|
+
usedSet.add(subIndex);
|
|
599
|
+
}
|
|
600
|
+
mapped.push({
|
|
601
|
+
expected: rule.expected,
|
|
602
|
+
file,
|
|
603
|
+
matchResult,
|
|
604
|
+
optional: rule.optional || false,
|
|
605
|
+
info: rule.info
|
|
606
|
+
});
|
|
607
|
+
matchedFileIndices.add(fileIndex);
|
|
608
|
+
fileMatched = true;
|
|
609
|
+
break;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
const allUsed = usedFlexibleRules.get(ruleIndex)?.size === ruleOrRules.length;
|
|
613
|
+
if (allUsed) {
|
|
614
|
+
ruleIndex++;
|
|
615
|
+
}
|
|
616
|
+
if (fileMatched) {
|
|
617
|
+
fileIndex++;
|
|
618
|
+
} else {
|
|
619
|
+
fileIndex++;
|
|
620
|
+
}
|
|
621
|
+
} else {
|
|
622
|
+
const rule = ruleOrRules;
|
|
623
|
+
const matchResult = this.matchFile(file, rule);
|
|
624
|
+
attemptedMatches.push(matchResult);
|
|
625
|
+
if (matchResult.matched) {
|
|
626
|
+
if (isSingleMatchRule(rule)) {
|
|
627
|
+
mapped.push({
|
|
628
|
+
expected: rule.expected,
|
|
629
|
+
file,
|
|
630
|
+
matchResult,
|
|
631
|
+
optional: rule.optional || false,
|
|
632
|
+
info: rule.info
|
|
633
|
+
});
|
|
634
|
+
matchedFileIndices.add(fileIndex);
|
|
635
|
+
ruleIndex++;
|
|
636
|
+
fileIndex++;
|
|
637
|
+
} else if (isWildcardRule(rule)) {
|
|
638
|
+
wildcardMatched.push({
|
|
639
|
+
file,
|
|
640
|
+
matchResult,
|
|
641
|
+
info: rule.info
|
|
642
|
+
});
|
|
643
|
+
matchedFileIndices.add(fileIndex);
|
|
644
|
+
if (rule.greedy) {
|
|
645
|
+
fileIndex++;
|
|
646
|
+
} else {
|
|
647
|
+
ruleIndex++;
|
|
648
|
+
fileIndex++;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
} else {
|
|
652
|
+
if (rule.optional) {
|
|
653
|
+
ruleIndex++;
|
|
654
|
+
} else if (isWildcardRule(rule)) {
|
|
655
|
+
ruleIndex++;
|
|
656
|
+
} else {
|
|
657
|
+
unmapped.push({
|
|
658
|
+
file,
|
|
659
|
+
attemptedRules: attemptedMatches
|
|
660
|
+
});
|
|
661
|
+
fileIndex++;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
const stats = {
|
|
667
|
+
totalFiles: files.length,
|
|
668
|
+
mappedFiles: mapped.length,
|
|
669
|
+
wildcardMatchedFiles: wildcardMatched.length,
|
|
670
|
+
unmappedFiles: unmapped.length,
|
|
671
|
+
optionalFiles: optionalFiles.length,
|
|
672
|
+
preFilteredFiles: preFiltered.length,
|
|
673
|
+
totalRules: this.countRules(rules),
|
|
674
|
+
mandatoryRules: this.countMandatoryRules(rules),
|
|
675
|
+
optionalRules: this.countOptionalRules(rules)
|
|
676
|
+
};
|
|
677
|
+
return {
|
|
678
|
+
mapped,
|
|
679
|
+
wildcardMatched,
|
|
680
|
+
optionalFiles,
|
|
681
|
+
unmapped,
|
|
682
|
+
preFiltered,
|
|
683
|
+
stats
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Filtering with optional mode - uses scan-forward algorithm
|
|
688
|
+
*/
|
|
689
|
+
filterFilesOptionalMode(sortedFiles, rules, preFiltered, totalFiles, mode) {
|
|
690
|
+
const mapped = [];
|
|
691
|
+
const wildcardMatched = [];
|
|
692
|
+
const optionalFiles = [];
|
|
693
|
+
let currentFileIndex = 0;
|
|
694
|
+
let lastMatchedIndex = -1;
|
|
695
|
+
let lastMatchedRule = null;
|
|
696
|
+
for (let ruleIndex = 0; ruleIndex < rules.length; ruleIndex++) {
|
|
697
|
+
const ruleOrRules = rules[ruleIndex];
|
|
698
|
+
let found = false;
|
|
699
|
+
for (let fileIndex = currentFileIndex; fileIndex < sortedFiles.length; fileIndex++) {
|
|
700
|
+
const file = sortedFiles[fileIndex];
|
|
701
|
+
let matchResult = null;
|
|
702
|
+
let matchedRule = null;
|
|
703
|
+
if (Array.isArray(ruleOrRules)) {
|
|
704
|
+
for (const rule of ruleOrRules) {
|
|
705
|
+
const result = this.matchFile(file, rule);
|
|
706
|
+
if (result.matched) {
|
|
707
|
+
matchResult = result;
|
|
708
|
+
matchedRule = rule;
|
|
709
|
+
break;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
} else {
|
|
713
|
+
matchResult = this.matchFile(file, ruleOrRules);
|
|
714
|
+
if (matchResult.matched) {
|
|
715
|
+
matchedRule = ruleOrRules;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
if (matchResult && matchResult.matched && matchedRule) {
|
|
719
|
+
for (let i = lastMatchedIndex + 1; i < fileIndex; i++) {
|
|
720
|
+
const optionalFile = sortedFiles[i];
|
|
721
|
+
optionalFiles.push({
|
|
722
|
+
fileName: optionalFile.fileName,
|
|
723
|
+
position: i,
|
|
724
|
+
between: {
|
|
725
|
+
afterRule: lastMatchedRule || "(start)",
|
|
726
|
+
beforeRule: isSingleMatchRule(matchedRule) ? matchedRule.expected : "(wildcard)"
|
|
727
|
+
},
|
|
728
|
+
failedMatches: []
|
|
729
|
+
// TODO: collect actual failed matches
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
if (isSingleMatchRule(matchedRule)) {
|
|
733
|
+
mapped.push({
|
|
734
|
+
expected: matchedRule.expected,
|
|
735
|
+
file,
|
|
736
|
+
matchResult,
|
|
737
|
+
optional: matchedRule.optional || false,
|
|
738
|
+
info: matchedRule.info
|
|
739
|
+
});
|
|
740
|
+
lastMatchedRule = matchedRule.expected;
|
|
741
|
+
} else if (isWildcardRule(matchedRule)) {
|
|
742
|
+
wildcardMatched.push({
|
|
743
|
+
file,
|
|
744
|
+
matchResult,
|
|
745
|
+
info: matchedRule.info
|
|
746
|
+
});
|
|
747
|
+
lastMatchedRule = "(wildcard)";
|
|
748
|
+
}
|
|
749
|
+
lastMatchedIndex = fileIndex;
|
|
750
|
+
currentFileIndex = fileIndex + 1;
|
|
751
|
+
found = true;
|
|
752
|
+
break;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
if (!found) {
|
|
756
|
+
const isMandatory = Array.isArray(ruleOrRules) ? ruleOrRules.some((r) => !r.optional && !isWildcardRule(r)) : !ruleOrRules.optional && !isWildcardRule(ruleOrRules);
|
|
757
|
+
if (isMandatory && mode === "strict-optional") {
|
|
758
|
+
break;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
for (let i = lastMatchedIndex + 1; i < sortedFiles.length; i++) {
|
|
763
|
+
const optionalFile = sortedFiles[i];
|
|
764
|
+
optionalFiles.push({
|
|
765
|
+
fileName: optionalFile.fileName,
|
|
766
|
+
position: i,
|
|
767
|
+
between: {
|
|
768
|
+
afterRule: lastMatchedRule || "(start)",
|
|
769
|
+
beforeRule: "(end)"
|
|
770
|
+
},
|
|
771
|
+
failedMatches: []
|
|
772
|
+
// TODO: collect actual failed matches
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
const stats = {
|
|
776
|
+
totalFiles,
|
|
777
|
+
mappedFiles: mapped.length,
|
|
778
|
+
wildcardMatchedFiles: wildcardMatched.length,
|
|
779
|
+
unmappedFiles: 0,
|
|
780
|
+
// Always 0 in optional modes
|
|
781
|
+
optionalFiles: optionalFiles.length,
|
|
782
|
+
preFilteredFiles: preFiltered.length,
|
|
783
|
+
totalRules: this.countRules(rules),
|
|
784
|
+
mandatoryRules: this.countMandatoryRules(rules),
|
|
785
|
+
optionalRules: this.countOptionalRules(rules)
|
|
786
|
+
};
|
|
787
|
+
return {
|
|
788
|
+
mapped,
|
|
789
|
+
wildcardMatched,
|
|
790
|
+
optionalFiles,
|
|
791
|
+
unmapped: [],
|
|
792
|
+
// Always empty in optional modes
|
|
793
|
+
preFiltered,
|
|
794
|
+
stats
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Counts total number of rules (flattening arrays)
|
|
799
|
+
*/
|
|
800
|
+
countRules(rules) {
|
|
801
|
+
return rules.reduce((count, rule) => {
|
|
802
|
+
if (Array.isArray(rule)) {
|
|
803
|
+
return count + rule.length;
|
|
804
|
+
}
|
|
805
|
+
return count + 1;
|
|
806
|
+
}, 0);
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* Counts mandatory rules
|
|
810
|
+
*/
|
|
811
|
+
countMandatoryRules(rules) {
|
|
812
|
+
return rules.reduce((count, rule) => {
|
|
813
|
+
if (Array.isArray(rule)) {
|
|
814
|
+
return count + rule.filter((r) => !r.optional && !isWildcardRule(r)).length;
|
|
815
|
+
}
|
|
816
|
+
return count + (rule.optional || isWildcardRule(rule) ? 0 : 1);
|
|
817
|
+
}, 0);
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* Counts optional rules
|
|
821
|
+
*/
|
|
822
|
+
countOptionalRules(rules) {
|
|
823
|
+
return rules.reduce((count, rule) => {
|
|
824
|
+
if (Array.isArray(rule)) {
|
|
825
|
+
return count + rule.filter((r) => r.optional || isWildcardRule(r)).length;
|
|
826
|
+
}
|
|
827
|
+
return count + (rule.optional || isWildcardRule(rule) ? 1 : 0);
|
|
828
|
+
}, 0);
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Filtering function for grouped rules with common filter criteria.
|
|
832
|
+
* Files are first filtered by preFilter (if provided), then matched against
|
|
833
|
+
* group filters, and finally processed by the group's rules.
|
|
834
|
+
*
|
|
835
|
+
* @param files - Files to filter
|
|
836
|
+
* @param groups - Filter groups with common criteria and rules
|
|
837
|
+
* @param sortFn - Optional sort function for file ordering
|
|
838
|
+
* @param preFilter - Optional pre-filter criteria (files not matching are excluded)
|
|
839
|
+
* @param mode - Matching mode ('strict', 'optional', or 'strict-optional')
|
|
840
|
+
* @returns FilterResult with mapped, wildcardMatched, optionalFiles and unmapped files
|
|
841
|
+
*/
|
|
842
|
+
filterFilesWithGroups(files, groups, sortFn, preFilter, mode = "strict") {
|
|
843
|
+
let filteredFiles;
|
|
844
|
+
let preFiltered = [];
|
|
845
|
+
if (preFilter) {
|
|
846
|
+
const preFilterResult = this.applyPreFilter(files, preFilter);
|
|
847
|
+
filteredFiles = preFilterResult.matched;
|
|
848
|
+
preFiltered = preFilterResult.excluded;
|
|
849
|
+
} else {
|
|
850
|
+
filteredFiles = files;
|
|
851
|
+
}
|
|
852
|
+
const sortedFiles = sortFn ? [...filteredFiles].sort(sortFn) : [...filteredFiles];
|
|
853
|
+
const mapped = [];
|
|
854
|
+
const wildcardMatched = [];
|
|
855
|
+
const unmapped = [];
|
|
856
|
+
const optionalFiles = [];
|
|
857
|
+
for (const group of groups) {
|
|
858
|
+
const groupFiles = sortedFiles.filter((file) => {
|
|
859
|
+
const checks = group.groupFilter.map((criterion) => {
|
|
860
|
+
return this.engine.evaluateCriterion(file.data, criterion);
|
|
861
|
+
});
|
|
862
|
+
return checks.every((check) => check.status);
|
|
863
|
+
});
|
|
864
|
+
if (groupFiles.length === 0) {
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
const rulesArray = group.rules.map((rule) => {
|
|
868
|
+
if (Array.isArray(rule)) {
|
|
869
|
+
return rule;
|
|
870
|
+
}
|
|
871
|
+
return rule;
|
|
872
|
+
});
|
|
873
|
+
const groupResult = this.filterFiles(groupFiles, rulesArray, void 0, void 0, mode);
|
|
874
|
+
mapped.push(...groupResult.mapped);
|
|
875
|
+
wildcardMatched.push(...groupResult.wildcardMatched);
|
|
876
|
+
unmapped.push(...groupResult.unmapped);
|
|
877
|
+
optionalFiles.push(...groupResult.optionalFiles);
|
|
878
|
+
}
|
|
879
|
+
const allRules = groups.flatMap((g) => g.rules);
|
|
880
|
+
const stats = {
|
|
881
|
+
totalFiles: files.length,
|
|
882
|
+
mappedFiles: mapped.length,
|
|
883
|
+
wildcardMatchedFiles: wildcardMatched.length,
|
|
884
|
+
unmappedFiles: unmapped.length,
|
|
885
|
+
optionalFiles: optionalFiles.length,
|
|
886
|
+
preFilteredFiles: preFiltered.length,
|
|
887
|
+
totalRules: this.countRules(allRules),
|
|
888
|
+
mandatoryRules: this.countMandatoryRules(allRules),
|
|
889
|
+
optionalRules: this.countOptionalRules(allRules)
|
|
890
|
+
};
|
|
891
|
+
return {
|
|
892
|
+
mapped,
|
|
893
|
+
wildcardMatched,
|
|
894
|
+
optionalFiles,
|
|
895
|
+
unmapped,
|
|
896
|
+
preFiltered,
|
|
897
|
+
stats
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
function filterFiles(request) {
|
|
902
|
+
const matcher = new Matcher(request.context);
|
|
903
|
+
if (request.rules && request.groups) {
|
|
904
|
+
throw new Error('FilterRequest: Provide either "rules" or "groups", not both');
|
|
905
|
+
}
|
|
906
|
+
if (!request.rules && !request.groups) {
|
|
907
|
+
throw new Error('FilterRequest: Must provide either "rules" or "groups"');
|
|
908
|
+
}
|
|
909
|
+
if (request.groups) {
|
|
910
|
+
return matcher.filterFilesWithGroups(
|
|
911
|
+
request.files,
|
|
912
|
+
request.groups,
|
|
913
|
+
request.sortFn,
|
|
914
|
+
request.preFilter,
|
|
915
|
+
request.mode
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
if (!request.rules) {
|
|
919
|
+
throw new Error("FilterRequest: Rules are required");
|
|
920
|
+
}
|
|
921
|
+
return matcher.filterFiles(
|
|
922
|
+
request.files,
|
|
923
|
+
request.rules,
|
|
924
|
+
request.sortFn,
|
|
925
|
+
request.preFilter,
|
|
926
|
+
request.mode
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
exports.FilterEngine = FilterEngine;
|
|
930
|
+
exports.Matcher = Matcher;
|
|
931
|
+
exports.filterFiles = filterFiles;
|
|
932
|
+
exports.getValueFromPath = getValueFromPath;
|
|
933
|
+
exports.getValueOr = getValueOr;
|
|
934
|
+
exports.isSingleMatchRule = isSingleMatchRule;
|
|
935
|
+
exports.isWildcardRule = isWildcardRule;
|
|
936
|
+
exports.pathExists = pathExists;
|
|
937
|
+
//# sourceMappingURL=aikotools-datafilter.cjs.map
|