@aikotools/datafilter 1.0.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 ADDED
@@ -0,0 +1,396 @@
1
+ # @aikotools/datafilter
2
+
3
+ Advanced data filtering engine for JSON file matching in E2E testing.
4
+
5
+ [![npm version](https://badge.fury.io/js/@aikotools%2Fdatafilter.svg)](https://www.npmjs.com/package/@aikotools/datafilter)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
7
+
8
+ ## Overview
9
+
10
+ `@aikotools/datafilter` provides sophisticated file filtering and matching capabilities specifically designed for E2E testing scenarios where you need to map actual result files to expected files.
11
+
12
+ ### Key Features
13
+
14
+ - **Flexible Matching**: Match files using various filter criteria (value, exists, array checks, time ranges)
15
+ - **Order Handling**: Support for both strict and flexible ordering of expected files
16
+ - **🆕 Wildcard Optionals**: Match arbitrary number of optional files without explicit specification
17
+ - **Greedy vs. Non-Greedy**: Control whether wildcards match once or multiple times
18
+ - **Deep Object Access**: Path-based navigation through nested JSON structures
19
+ - **Sort Integration**: Custom sort functions for file ordering before matching
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ npm install @aikotools/datafilter
25
+ ```
26
+
27
+ ## Quick Start
28
+
29
+ ```typescript
30
+ import { filterFiles } from '@aikotools/datafilter';
31
+
32
+ const result = filterFiles({
33
+ files: [
34
+ { fileName: 'event1.json', data: { type: 'event1', value: 42 } },
35
+ { fileName: 'optional1.json', data: { type: 'optional' } },
36
+ { fileName: 'optional2.json', data: { type: 'optional' } },
37
+ { fileName: 'event2.json', data: { type: 'event2', value: 100 } },
38
+ ],
39
+ rules: [
40
+ // Match specific file
41
+ { match: [{ path: ['type'], check: { value: 'event1' } }], expected: 'event1' },
42
+
43
+ // 🆕 NEW: Match arbitrary optionals with wildcard
44
+ {
45
+ matchAny: [{ path: ['type'], check: { value: 'optional' } }],
46
+ optional: true,
47
+ greedy: true, // Match all optionals
48
+ },
49
+
50
+ // Match another specific file
51
+ { match: [{ path: ['type'], check: { value: 'event2' } }], expected: 'event2' },
52
+ ],
53
+ });
54
+
55
+ console.log(result.mapped.length); // 2 (event1, event2)
56
+ console.log(result.wildcardMatched.length); // 2 (both optionals)
57
+ console.log(result.unmapped.length); // 0
58
+ ```
59
+
60
+ ## Core Concepts
61
+
62
+ ### Filter Criteria
63
+
64
+ All filter criteria use path-based access and various check types:
65
+
66
+ ```typescript
67
+ {
68
+ path: ['data', 'user', 'name'], // Navigate through object
69
+ check: { value: 'John' } // Check type
70
+ }
71
+ ```
72
+
73
+ ### Match Rules
74
+
75
+ #### Single Match Rule
76
+
77
+ Maps exactly one file to an expected identifier:
78
+
79
+ ```typescript
80
+ {
81
+ match: [
82
+ { path: ['type'], check: { value: 'event1' } },
83
+ { path: ['status'], check: { value: 'active' } }
84
+ ],
85
+ expected: 'event1',
86
+ optional: false // Default: false
87
+ }
88
+ ```
89
+
90
+ #### 🆕 Wildcard Match Rule (NEW)
91
+
92
+ Matches arbitrary number of files without explicit specification:
93
+
94
+ ```typescript
95
+ {
96
+ matchAny: [
97
+ { path: ['category'], check: { value: 'optional' } }
98
+ ],
99
+ optional: true, // Always true for wildcards
100
+ greedy: true // Match all (true) or just one (false)
101
+ }
102
+ ```
103
+
104
+ #### Flexible Ordering
105
+
106
+ Use arrays of rules when order is not deterministic:
107
+
108
+ ```typescript
109
+ [
110
+ { match: [{ path: ['type'], check: { value: 'event3' } }], expected: 'event3' },
111
+ { match: [{ path: ['type'], check: { value: 'event4' } }], expected: 'event4' }
112
+ ]
113
+ // Either event3 or event4 can come first
114
+ ```
115
+
116
+ ## Filter Check Types
117
+
118
+ ### 1. Value Check
119
+
120
+ Deep equality comparison:
121
+
122
+ ```typescript
123
+ { path: ['user', 'name'], check: { value: 'John' } }
124
+ { path: ['user', 'address'], check: { value: { city: 'Berlin', zip: '10115' } } }
125
+ ```
126
+
127
+ ### 2. Exists Check
128
+
129
+ Check if a path exists:
130
+
131
+ ```typescript
132
+ { path: ['optional', 'field'], check: { exists: true } }
133
+ { path: ['deprecated'], check: { exists: false } }
134
+ ```
135
+
136
+ ### 3. Array Element Check
137
+
138
+ Check if array contains (or doesn't contain) an element:
139
+
140
+ ```typescript
141
+ { path: ['tags'], check: { itemExists: true, item: 'typescript' } }
142
+ { path: ['status'], check: { itemExists: false, item: 'banned' } }
143
+ ```
144
+
145
+ ### 4. Array Size Check
146
+
147
+ Validate array length:
148
+
149
+ ```typescript
150
+ { path: ['items'], check: { type: 'equal', size: 3 } }
151
+ { path: ['errors'], check: { type: 'lessThan', size: 5 } }
152
+ { path: ['records'], check: { type: 'greaterThan', size: 10 } }
153
+ ```
154
+
155
+ ### 5. Time Range Check
156
+
157
+ Validate timestamps (ISO strings or numeric):
158
+
159
+ ```typescript
160
+ // ISO timestamp
161
+ {
162
+ path: ['timestamp'],
163
+ check: {
164
+ min: '2023-11-08T15:00:00+01:00',
165
+ max: '2023-11-08T16:00:00+01:00'
166
+ }
167
+ }
168
+
169
+ // Numeric timestamp (milliseconds)
170
+ {
171
+ path: ['timestamp'],
172
+ check: {
173
+ min: '1699452000000',
174
+ max: '1699455600000'
175
+ }
176
+ }
177
+ ```
178
+
179
+ ## Complete Example
180
+
181
+ ### Scenario from Requirements
182
+
183
+ ```
184
+ event1
185
+ - event1.1 (optional)
186
+ event2
187
+ - event3, event4 (flexible order)
188
+ event5
189
+ ```
190
+
191
+ ### Without Wildcard (Old Way)
192
+
193
+ Every optional file must be explicitly specified:
194
+
195
+ ```typescript
196
+ const result = filterFiles({
197
+ files: actualFiles,
198
+ rules: [
199
+ { match: [{ path: ['type'], check: { value: 'event1' } }], expected: 'event1' },
200
+ { match: [{ path: ['type'], check: { value: 'event1.1' } }], expected: 'event1.1', optional: true },
201
+ { match: [{ path: ['type'], check: { value: 'event2' } }], expected: 'event2' },
202
+ [
203
+ { match: [{ path: ['type'], check: { value: 'event3' } }], expected: 'event3' },
204
+ { match: [{ path: ['type'], check: { value: 'event4' } }], expected: 'event4' }
205
+ ],
206
+ { match: [{ path: ['type'], check: { value: 'event5' } }], expected: 'event5' },
207
+ ],
208
+ });
209
+ ```
210
+
211
+ ### With Wildcard (New Way) 🆕
212
+
213
+ Arbitrary number of optional files without explicit specification:
214
+
215
+ ```typescript
216
+ const result = filterFiles({
217
+ files: actualFiles,
218
+ rules: [
219
+ { match: [{ path: ['type'], check: { value: 'event1' } }], expected: 'event1' },
220
+
221
+ // 🆕 NEW: Match ANY number of optional files
222
+ {
223
+ matchAny: [{ path: ['category'], check: { value: 'optional' } }],
224
+ optional: true,
225
+ greedy: true
226
+ },
227
+
228
+ { match: [{ path: ['type'], check: { value: 'event2' } }], expected: 'event2' },
229
+ [
230
+ { match: [{ path: ['type'], check: { value: 'event3' } }], expected: 'event3' },
231
+ { match: [{ path: ['type'], check: { value: 'event4' } }], expected: 'event4' }
232
+ ],
233
+ { match: [{ path: ['type'], check: { value: 'event5' } }], expected: 'event5' },
234
+ ],
235
+ sortFn: (a, b) => a.data.timestamp - b.data.timestamp
236
+ });
237
+
238
+ console.log(result.mapped); // event1, event2, event3, event4, event5
239
+ console.log(result.wildcardMatched); // All files matching optional criteria
240
+ console.log(result.unmapped); // Files that didn't match any rule
241
+ ```
242
+
243
+ ## API Reference
244
+
245
+ ### filterFiles(request: FilterRequest): FilterResult
246
+
247
+ Main filtering function.
248
+
249
+ **FilterRequest:**
250
+ - `files: JsonFile[]` - Files to filter
251
+ - `rules: (MatchRule | MatchRule[])[]` - Matching rules
252
+ - `sortFn?: (a, b) => number` - Optional sort function
253
+ - `context?: { startTimeScript?, startTimeTest?, pathTime? }` - Optional context
254
+
255
+ **FilterResult:**
256
+ - `mapped: MappedFile[]` - Successfully mapped files
257
+ - `wildcardMatched: WildcardMappedFile[]` - Files matched by wildcards
258
+ - `unmapped: UnmappedFile[]` - Files that couldn't be matched
259
+ - `stats: { totalFiles, mappedFiles, wildcardMatchedFiles, unmappedFiles, ... }`
260
+
261
+ ### Matcher Class
262
+
263
+ For advanced usage with multiple filter operations:
264
+
265
+ ```typescript
266
+ import { Matcher } from '@aikotools/datafilter';
267
+
268
+ const matcher = new Matcher({ startTimeScript, startTimeTest });
269
+ const result = matcher.filterFiles(files, rules, sortFn);
270
+ ```
271
+
272
+ ### FilterEngine Class
273
+
274
+ For custom filter criterion evaluation:
275
+
276
+ ```typescript
277
+ import { FilterEngine } from '@aikotools/datafilter';
278
+
279
+ const engine = new FilterEngine();
280
+ const checkResult = engine.evaluateCriterion(data, criterion);
281
+ ```
282
+
283
+ ### Utility Functions
284
+
285
+ ```typescript
286
+ import { getValueFromPath, pathExists, getValueOr } from '@aikotools/datafilter';
287
+
288
+ // Get value from path
289
+ const result = getValueFromPath(obj, ['data', 'user', 'name']);
290
+ // result: { value: 'John', found: true, validPath: ['data', 'user', 'name'] }
291
+
292
+ // Check if path exists
293
+ const exists = pathExists(obj, ['data', 'user']);
294
+ // exists: true
295
+
296
+ // Get value with default fallback
297
+ const value = getValueOr(obj, ['data', 'missing'], 'default');
298
+ // value: 'default'
299
+ ```
300
+
301
+ ## Migration from e2e-tool-util-result-mapper
302
+
303
+ ### Key Differences
304
+
305
+ 1. **Wildcard Support**: New wildcard rules eliminate need to specify every optional file
306
+ 2. **Simpler API**: Focused on filtering, not entire pipeline (dedupe, sort, etc.)
307
+ 3. **Type Safety**: Full TypeScript support with comprehensive types
308
+ 4. **Modern Structure**: ESM modules, Vite build, Vitest testing
309
+
310
+ ### Migration Example
311
+
312
+ **Old (result-mapper):**
313
+ ```typescript
314
+ // Every optional must be explicitly specified
315
+ {
316
+ "mappingCriteria": [
317
+ { "expected": "event1.json", "optional": false, "filter": {...} },
318
+ { "expected": "optional1.json", "optional": true, "filter": {...} },
319
+ { "expected": "optional2.json", "optional": true, "filter": {...} },
320
+ { "expected": "optional3.json", "optional": true, "filter": {...} },
321
+ { "expected": "event2.json", "optional": false, "filter": {...} }
322
+ ]
323
+ }
324
+ ```
325
+
326
+ **New (datafilter):**
327
+ ```typescript
328
+ // Wildcard matches all optionals
329
+ {
330
+ rules: [
331
+ { match: [...], expected: 'event1' },
332
+ { matchAny: [...], optional: true, greedy: true }, // 🆕 All optionals
333
+ { match: [...], expected: 'event2' }
334
+ ]
335
+ }
336
+ ```
337
+
338
+ ## Test Results
339
+
340
+ ✅ **21/21 tests passing**
341
+ - ✅ Basic filtering with single matches
342
+ - ✅ Flexible ordering (array of rules)
343
+ - ✅ Optional rules
344
+ - ✅ 🆕 Wildcard matches (greedy & non-greedy)
345
+ - ✅ All filter check types (value, exists, array, time)
346
+ - ✅ Complex real-world scenarios
347
+ - ✅ Sort function integration
348
+
349
+ ## Development
350
+
351
+ ```bash
352
+ # Install dependencies
353
+ npm install
354
+
355
+ # Build
356
+ npm run build
357
+
358
+ # Test
359
+ npm test
360
+
361
+ # Lint
362
+ npm run lint
363
+
364
+ # Format
365
+ npm run format
366
+ ```
367
+
368
+ ## Architecture
369
+
370
+ ```
371
+ FilterEngine
372
+ ├── Evaluates individual filter criteria
373
+ ├── Supports: value, exists, array checks, time ranges
374
+ └── Deep equality comparison
375
+
376
+ Matcher
377
+ ├── Orchestrates file-to-rule matching
378
+ ├── Handles: single rules, arrays, wildcards
379
+ ├── Tracks: matched files, exhausted rules
380
+ └── Produces detailed results
381
+
382
+ Utilities
383
+ └── Path-based object access (getValueFromPath, pathExists)
384
+ ```
385
+
386
+ ## License
387
+
388
+ MIT - See [LICENSE](LICENSE) file for details.
389
+
390
+ ## Contributing
391
+
392
+ This is part of the @aikotools ecosystem. For issues and contributions, please see the repository.
393
+
394
+ ---
395
+
396
+ **Built for E2E Testing** 🚀