@aikotools/datafilter 1.0.5 → 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.
@@ -1 +1,937 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const E=require("luxon");function x(i){return"matchAny"in i}function b(i){return"match"in i&&"expected"in i}function y(i,s){const t=[];if(s.length===0)return{value:i,found:!0,validPath:[]};let e=i;for(let n=0;n<s.length;n++){const r=s[n];if(e==null)return{value:void 0,found:!1,error:`Cannot read property '${r}' of ${e}`,validPath:t};if(Array.isArray(e)){const a=typeof r=="number"?r:parseInt(String(r),10);if(isNaN(a))return{value:void 0,found:!1,error:`Array index must be a number, got '${r}'`,validPath:t};if(a<0||a>=e.length)return{value:void 0,found:!1,error:`Array index ${a} out of bounds (length: ${e.length})`,validPath:t};t.push(a),e=e[a];continue}if(typeof e=="object"){const a=String(r);if(!(a in e))return{value:void 0,found:!1,error:`Property '${a}' does not exist`,validPath:t};t.push(a),e=e[a];continue}return{value:void 0,found:!1,error:`Cannot access property '${r}' of primitive type ${typeof e}`,validPath:t}}return{value:e,found:!0,validPath:t}}function P(i,s){const t=y(i,s);return t.found&&t.value!==void 0}function V(i,s,t){const e=y(i,s);return e.found&&e.value!==void 0?e.value:t}class w{constructor(s){this.context=s}evaluateCriterion(s,t){const e=t.check;return"value"in e?this.checkValue(s,t.path,e):"exists"in e?this.checkExists(s,t.path,e):"itemExists"in e&&"item"in e?this.checkArrayElement(s,t.path,e):"type"in e&&"size"in e?this.checkArraySize(s,t.path,e):"min"in e&&"max"in e?typeof e.min=="number"&&typeof e.max=="number"?this.checkNumericRange(s,t.path,e):this.checkTimeRange(s,t.path,e):{status:!1,checkType:"unknown",reason:`Unknown check type: ${JSON.stringify(e)}`}}checkValue(s,t,e){const n=y(s,t);if(!n.found)return{status:!1,checkType:"checkValue",reason:{message:n.error||"Path not found",path:t}};const r=n.value,a=e.value;return this.deepEqual(r,a)?{status:!0,checkType:"checkValue"}:{status:!1,checkType:"checkValue",reason:{message:"Value mismatch",path:t,expected:a,actual:r}}}checkExists(s,t,e){const n=y(s,t),r=n.found&&n.value!==void 0;return e.exists===r?{status:!0,checkType:"checkExists"}:{status:!1,checkType:"checkExists",reason:{message:e.exists?`Path should exist but doesn't: ${n.error}`:"Path should not exist but does",path:t}}}checkArrayElement(s,t,e){const n=y(s,t);if(!n.found)return{status:!1,checkType:"checkArrayElement",reason:{message:n.error||"Path not found",path:t}};const r=n.value;if(!Array.isArray(r))return{status:!1,checkType:"checkArrayElement",reason:{message:"Value is not an array",path:t,actualType:typeof r}};const a=r.some(u=>this.deepEqual(u,e.item));return e.itemExists===a?{status:!0,checkType:"checkArrayElement"}:{status:!1,checkType:"checkArrayElement",reason:{message:e.itemExists?"Item should exist in array but doesn't":"Item should not exist in array but does",path:t,item:e.item}}}checkArraySize(s,t,e){const n=y(s,t);if(!n.found)return{status:!1,checkType:"checkArraySize",reason:{message:n.error||"Path not found",path:t}};const r=n.value;if(!Array.isArray(r))return{status:!1,checkType:"checkArraySize",reason:{message:"Value is not an array",path:t,actualType:typeof r}};const a=r.length,u=e.size;let o=!1,h="";switch(e.type){case"equal":o=a===u,h=`Array length should be ${u} but is ${a}`;break;case"lessThan":o=a<u,h=`Array length should be less than ${u} but is ${a}`;break;case"greaterThan":o=a>u,h=`Array length should be greater than ${u} but is ${a}`;break}return o?{status:!0,checkType:"checkArraySize"}:{status:!1,checkType:"checkArraySize",reason:{message:h,path:t,expected:u,actual:a}}}checkTimeRange(s,t,e){const n=y(s,t);if(!n.found)return{status:!1,checkType:"checkTimeRange",reason:{message:n.error||"Path not found",path:t}};const r=n.value;if(typeof r=="number"){const a=parseInt(e.min),u=parseInt(e.max);return isNaN(a)||isNaN(u)?{status:!1,checkType:"checkTimeRange",reason:{message:"Min or max is not a valid number",min:e.min,max:e.max}}:r>=a&&r<=u?{status:!0,checkType:"checkTimeRange"}:{status:!1,checkType:"checkTimeRange",reason:{message:`Timestamp ${r} is outside range [${a}, ${u}]`,path:t,actual:r,min:a,max:u}}}if(typeof r=="string"){const a=E.DateTime.fromISO(r),u=E.DateTime.fromISO(e.min),o=E.DateTime.fromISO(e.max);return a.isValid?!u.isValid||!o.isValid?{status:!1,checkType:"checkTimeRange",reason:{message:"Invalid min or max time",min:e.min,max:e.max}}:a>=u&&a<=o?{status:!0,checkType:"checkTimeRange"}:{status:!1,checkType:"checkTimeRange",reason:{message:`Timestamp ${r} is outside range [${e.min}, ${e.max}]`,path:t,actual:r,min:e.min,max:e.max}}:{status:!1,checkType:"checkTimeRange",reason:{message:`Invalid timestamp: ${r}`,path:t}}}return{status:!1,checkType:"checkTimeRange",reason:{message:`Timestamp must be a string or number, got ${typeof r}`,path:t}}}checkNumericRange(s,t,e){const n=y(s,t);if(!n.found)return{status:!1,checkType:"checkNumericRange",reason:{message:n.error||"Path not found",path:t}};const r=n.value;return typeof r!="number"?{status:!1,checkType:"checkNumericRange",reason:{message:`Value must be a number, got ${typeof r}`,path:t,actual:r}}:r>=e.min&&r<=e.max?{status:!0,checkType:"checkNumericRange"}:{status:!1,checkType:"checkNumericRange",reason:{message:`Value ${r} is outside range [${e.min}, ${e.max}]`,path:t,actual:r,min:e.min,max:e.max}}}deepEqual(s,t){if(s===t)return!0;if(s===null||t===null||s===void 0||t===void 0)return s===t;if(typeof s!=typeof t)return!1;if(s instanceof Date&&t instanceof Date)return s.getTime()===t.getTime();if(Array.isArray(s)&&Array.isArray(t))return s.length!==t.length?!1:s.every((e,n)=>this.deepEqual(e,t[n]));if(typeof s=="object"&&typeof t=="object"){const e=Object.keys(s),n=Object.keys(t);return e.length!==n.length?!1:e.every(r=>this.deepEqual(s[r],t[r]))}return s===t}}class M{constructor(s){this.engine=new w(s)}matchFile(s,t){const n=(x(t)?t.matchAny:t.match).map(a=>this.engine.evaluateCriterion(s.data,a));return{matched:n.every(a=>a.status),checks:n,rule:t}}applyPreFilter(s,t){const e=[],n=[];for(const r of s){const a=t.map(u=>this.engine.evaluateCriterion(r.data,u));a.every(u=>u.status)?e.push(r):n.push({file:r,failedChecks:a.filter(u=>!u.status)})}return{matched:e,excluded:n}}filterFiles(s,t,e,n){let r,a=[];if(n){const f=this.applyPreFilter(s,n);r=f.matched,a=f.excluded}else r=s;const u=e?[...r].sort(e):[...r],o=[],h=[],g=[],k=new Set,R=new Map;let l=0,c=0;for(;l<u.length;){const f=u[l];if(c>=t.length){k.has(l)||g.push({file:f,attemptedRules:[]}),l++;continue}const m=t[c],F=[];let T=!1;if(Array.isArray(m)){for(let p=0;p<m.length;p++){if(R.get(c)?.has(p))continue;const v=m[p],$=this.matchFile(f,v);if(F.push($),$.matched&&b(v)){R.has(c)||R.set(c,new Set);const S=R.get(c);S&&S.add(p),o.push({expected:v.expected,file:f,matchResult:$,optional:v.optional||!1,info:v.info}),k.add(l),T=!0;break}}R.get(c)?.size===m.length&&c++,l++}else{const d=m,p=this.matchFile(f,d);F.push(p),p.matched?b(d)?(o.push({expected:d.expected,file:f,matchResult:p,optional:d.optional||!1,info:d.info}),k.add(l),c++,l++):x(d)&&(h.push({file:f,matchResult:p,info:d.info}),k.add(l),d.greedy||c++,l++):d.optional||x(d)?c++:(g.push({file:f,attemptedRules:F}),l++)}}const A={totalFiles:s.length,mappedFiles:o.length,wildcardMatchedFiles:h.length,unmappedFiles:g.length,preFilteredFiles:a.length,totalRules:this.countRules(t),mandatoryRules:this.countMandatoryRules(t),optionalRules:this.countOptionalRules(t)};return{mapped:o,wildcardMatched:h,unmapped:g,preFiltered:a,stats:A}}countRules(s){return s.reduce((t,e)=>Array.isArray(e)?t+e.length:t+1,0)}countMandatoryRules(s){return s.reduce((t,e)=>Array.isArray(e)?t+e.filter(n=>!n.optional&&!x(n)).length:t+(e.optional||x(e)?0:1),0)}countOptionalRules(s){return s.reduce((t,e)=>Array.isArray(e)?t+e.filter(n=>n.optional||x(n)).length:t+(e.optional||x(e)?1:0),0)}filterFilesWithGroups(s,t,e,n){let r,a=[];if(n){const l=this.applyPreFilter(s,n);r=l.matched,a=l.excluded}else r=s;const u=e?[...r].sort(e):[...r],o=[],h=[],g=[];for(const l of t){const c=u.filter(m=>l.groupFilter.map(T=>this.engine.evaluateCriterion(m.data,T)).every(T=>T.status));if(c.length===0)continue;const A=l.rules.map(m=>(Array.isArray(m),m)),f=this.filterFiles(c,A);o.push(...f.mapped),h.push(...f.wildcardMatched),g.push(...f.unmapped)}const k=t.flatMap(l=>l.rules),R={totalFiles:s.length,mappedFiles:o.length,wildcardMatchedFiles:h.length,unmappedFiles:g.length,preFilteredFiles:a.length,totalRules:this.countRules(k),mandatoryRules:this.countMandatoryRules(k),optionalRules:this.countOptionalRules(k)};return{mapped:o,wildcardMatched:h,unmapped:g,preFiltered:a,stats:R}}}function I(i){const s=new M(i.context);if(i.rules&&i.groups)throw new Error('FilterRequest: Provide either "rules" or "groups", not both');if(!i.rules&&!i.groups)throw new Error('FilterRequest: Must provide either "rules" or "groups"');if(i.groups)return s.filterFilesWithGroups(i.files,i.groups,i.sortFn,i.preFilter);if(!i.rules)throw new Error("FilterRequest: Rules are required");return s.filterFiles(i.files,i.rules,i.sortFn,i.preFilter)}exports.FilterEngine=w;exports.Matcher=M;exports.filterFiles=I;exports.getValueFromPath=y;exports.getValueOr=V;exports.isSingleMatchRule=b;exports.isWildcardRule=x;exports.pathExists=P;
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