@sschepis/magazine 0.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.
@@ -0,0 +1,474 @@
1
+ /**
2
+ * EventFilter - Advanced filtering and querying for blockchain events
3
+ */
4
+ export default class EventFilter {
5
+ constructor(logger) {
6
+ this.logger = logger.child({ context: 'EventFilter' });
7
+ this.logger.info('EventFilter initialized');
8
+ }
9
+
10
+ /**
11
+ * Build filter criteria from user input
12
+ * @param {Object} options - Filter options
13
+ * @param {number} options.fromBlock - Starting block number
14
+ * @param {number} options.toBlock - Ending block number
15
+ * @param {string} options.eventName - Event name to filter
16
+ * @param {Object} options.parameters - Event parameter filters
17
+ * @param {string} options.transactionHash - Filter by transaction hash
18
+ * @param {string} options.address - Filter by contract address
19
+ * @returns {Object} Filter criteria
20
+ */
21
+ buildFilter(options = {}) {
22
+ const filter = {};
23
+
24
+ // Block range filtering
25
+ if (options.fromBlock !== undefined) {
26
+ filter.fromBlock = options.fromBlock;
27
+ }
28
+
29
+ if (options.toBlock !== undefined) {
30
+ filter.toBlock = options.toBlock;
31
+ }
32
+
33
+ // Event name filtering
34
+ if (options.eventName) {
35
+ filter.eventName = options.eventName;
36
+ }
37
+
38
+ // Parameter filtering
39
+ if (options.parameters && Object.keys(options.parameters).length > 0) {
40
+ filter.parameters = options.parameters;
41
+ }
42
+
43
+ // Transaction hash filtering
44
+ if (options.transactionHash) {
45
+ filter.transactionHash = options.transactionHash;
46
+ }
47
+
48
+ // Address filtering
49
+ if (options.address) {
50
+ filter.address = options.address.toLowerCase();
51
+ }
52
+
53
+ // Pass through logical operators
54
+ if (options.$or) filter.$or = options.$or;
55
+ if (options.$and) filter.$and = options.$and;
56
+ if (options.$nor) filter.$nor = options.$nor;
57
+
58
+ this.logger.debug('Built filter criteria', { filter });
59
+ return filter;
60
+ }
61
+
62
+ /**
63
+ * Apply filters to an array of events
64
+ * @param {Array} events - Events to filter
65
+ * @param {Object} filter - Filter criteria
66
+ * @returns {Array} Filtered events
67
+ */
68
+ applyFilter(events, filter) {
69
+ if (!Array.isArray(events)) {
70
+ this.logger.warn('Invalid events array provided to applyFilter');
71
+ return [];
72
+ }
73
+
74
+ let filtered = [...events];
75
+
76
+ // Handle top-level logical combinators
77
+ if (filter.$or) {
78
+ filtered = filtered.filter(event =>
79
+ filter.$or.some(condition => {
80
+ const subFilter = this.buildFilter(condition);
81
+ return this.applyFilter([event], subFilter).length > 0;
82
+ })
83
+ );
84
+ }
85
+
86
+ if (filter.$and) {
87
+ filtered = filtered.filter(event =>
88
+ filter.$and.every(condition => {
89
+ const subFilter = this.buildFilter(condition);
90
+ return this.applyFilter([event], subFilter).length > 0;
91
+ })
92
+ );
93
+ }
94
+
95
+ if (filter.$nor) {
96
+ filtered = filtered.filter(event =>
97
+ filter.$nor.every(condition => {
98
+ const subFilter = this.buildFilter(condition);
99
+ return this.applyFilter([event], subFilter).length === 0;
100
+ })
101
+ );
102
+ }
103
+
104
+ // Filter by block range
105
+ if (filter.fromBlock !== undefined) {
106
+ filtered = filtered.filter(event => event.blockNumber >= filter.fromBlock);
107
+ }
108
+
109
+ if (filter.toBlock !== undefined) {
110
+ filtered = filtered.filter(event => event.blockNumber <= filter.toBlock);
111
+ }
112
+
113
+ // Filter by event name
114
+ if (filter.eventName) {
115
+ filtered = filtered.filter(event => event.eventName === filter.eventName);
116
+ }
117
+
118
+ // Filter by transaction hash
119
+ if (filter.transactionHash) {
120
+ filtered = filtered.filter(event =>
121
+ event.transactionHash.toLowerCase() === filter.transactionHash.toLowerCase()
122
+ );
123
+ }
124
+
125
+ // Filter by address
126
+ if (filter.address) {
127
+ filtered = filtered.filter(event =>
128
+ event.address && event.address.toLowerCase() === filter.address
129
+ );
130
+ }
131
+
132
+ // Filter by parameters
133
+ if (filter.parameters) {
134
+ filtered = filtered.filter(event =>
135
+ this._matchesParameters(event.args || {}, filter.parameters)
136
+ );
137
+ }
138
+
139
+ this.logger.debug('Applied filters', {
140
+ originalCount: events.length,
141
+ filteredCount: filtered.length,
142
+ filter
143
+ });
144
+
145
+ return filtered;
146
+ }
147
+
148
+ /**
149
+ * Check if event parameters match filter criteria (public API)
150
+ */
151
+ matchesParameters(eventArgs, filterParams) {
152
+ return this._matchesParameters(eventArgs, filterParams);
153
+ }
154
+
155
+ /**
156
+ * Check if event parameters match filter criteria
157
+ * @private
158
+ */
159
+ _matchesParameters(eventArgs, filterParams) {
160
+ for (const [key, value] of Object.entries(filterParams)) {
161
+ // Handle different value types
162
+ if (typeof value === 'object' && value !== null) {
163
+ // 'exists' operator: check field presence
164
+ if (value.exists !== undefined) {
165
+ const fieldExists = eventArgs.hasOwnProperty(key);
166
+ if (value.exists && !fieldExists) return false;
167
+ if (!value.exists && fieldExists) return false;
168
+ // If exists is the only operator, skip remaining checks for this key
169
+ const otherKeys = Object.keys(value).filter(k => k !== 'exists');
170
+ if (otherKeys.length === 0) continue;
171
+ }
172
+
173
+ // For all non-exists operators, the field must be present
174
+ if (!eventArgs.hasOwnProperty(key)) {
175
+ return false;
176
+ }
177
+
178
+ // Range filtering for numbers
179
+ if (value.gte !== undefined && eventArgs[key] < value.gte) {
180
+ return false;
181
+ }
182
+ if (value.lte !== undefined && eventArgs[key] > value.lte) {
183
+ return false;
184
+ }
185
+ if (value.gt !== undefined && eventArgs[key] <= value.gt) {
186
+ return false;
187
+ }
188
+ if (value.lt !== undefined && eventArgs[key] >= value.lt) {
189
+ return false;
190
+ }
191
+
192
+ // 'in' operator: value must be in array
193
+ if (value.in !== undefined && !value.in.includes(eventArgs[key])) {
194
+ return false;
195
+ }
196
+
197
+ // 'nin' operator: value must NOT be in array
198
+ if (value.nin !== undefined && value.nin.includes(eventArgs[key])) {
199
+ return false;
200
+ }
201
+
202
+ // 'not' operator: negate a sub-condition
203
+ if (value.not !== undefined) {
204
+ const subMatch = this._matchesParameters(eventArgs, { [key]: value.not });
205
+ if (subMatch) return false;
206
+ }
207
+
208
+ // 'regex' operator: pattern matching
209
+ if (value.regex !== undefined) {
210
+ const pattern = new RegExp(value.regex);
211
+ if (!pattern.test(String(eventArgs[key]))) {
212
+ return false;
213
+ }
214
+ }
215
+
216
+ // 'size' operator: array length check
217
+ if (value.size !== undefined) {
218
+ const arr = eventArgs[key];
219
+ if (!Array.isArray(arr) || arr.length !== value.size) {
220
+ return false;
221
+ }
222
+ }
223
+ } else {
224
+ // Exact match - field must exist
225
+ if (!eventArgs.hasOwnProperty(key)) {
226
+ return false;
227
+ }
228
+ if (eventArgs[key] !== value) {
229
+ return false;
230
+ }
231
+ }
232
+ }
233
+
234
+ return true;
235
+ }
236
+
237
+ /**
238
+ * Sort events by specified criteria
239
+ * @param {Array} events - Events to sort
240
+ * @param {Object} options - Sort options
241
+ * @param {string} options.by - Field to sort by (blockNumber, timestamp, etc.)
242
+ * @param {string} options.order - Sort order ('asc' or 'desc')
243
+ * @returns {Array} Sorted events
244
+ */
245
+ sortEvents(events, options = {}) {
246
+ const { by = 'blockNumber', order = 'asc' } = options;
247
+
248
+ const sorted = [...events].sort((a, b) => {
249
+ let aVal = a[by];
250
+ let bVal = b[by];
251
+
252
+ // Handle nested properties (e.g., 'args.value')
253
+ if (by.includes('.')) {
254
+ const path = by.split('.');
255
+ aVal = path.reduce((obj, key) => obj?.[key], a);
256
+ bVal = path.reduce((obj, key) => obj?.[key], b);
257
+ }
258
+
259
+ if (aVal === bVal) return 0;
260
+
261
+ const comparison = aVal < bVal ? -1 : 1;
262
+ return order === 'asc' ? comparison : -comparison;
263
+ });
264
+
265
+ this.logger.debug('Sorted events', { by, order, count: sorted.length });
266
+ return sorted;
267
+ }
268
+
269
+ /**
270
+ * Group events by specified field
271
+ * @param {Array} events - Events to group
272
+ * @param {string} field - Field to group by
273
+ * @returns {Object} Grouped events
274
+ */
275
+ groupEvents(events, field) {
276
+ const grouped = {};
277
+
278
+ for (const event of events) {
279
+ let key = event[field];
280
+
281
+ // Handle nested properties
282
+ if (field.includes('.')) {
283
+ const path = field.split('.');
284
+ key = path.reduce((obj, k) => obj?.[k], event);
285
+ }
286
+
287
+ if (key === undefined) {
288
+ key = 'undefined';
289
+ }
290
+
291
+ if (!grouped[key]) {
292
+ grouped[key] = [];
293
+ }
294
+
295
+ grouped[key].push(event);
296
+ }
297
+
298
+ this.logger.debug('Grouped events', {
299
+ field,
300
+ groups: Object.keys(grouped).length,
301
+ totalEvents: events.length
302
+ });
303
+
304
+ return grouped;
305
+ }
306
+
307
+ /**
308
+ * Create a query builder for fluent API
309
+ * @param {Array} events - Events to query
310
+ * @returns {QueryBuilder} Query builder instance
311
+ */
312
+ query(events) {
313
+ return new QueryBuilder(events, this);
314
+ }
315
+ }
316
+
317
+ /**
318
+ * QueryBuilder - Fluent API for event queries
319
+ */
320
+ class QueryBuilder {
321
+ constructor(events, eventFilter) {
322
+ this.events = events;
323
+ this.eventFilter = eventFilter;
324
+ this.filters = {};
325
+ this.sortOptions = null;
326
+ this._limit = null;
327
+ this._skip = null;
328
+ }
329
+
330
+ /**
331
+ * Filter by block range
332
+ */
333
+ blockRange(from, to) {
334
+ if (from !== undefined) this.filters.fromBlock = from;
335
+ if (to !== undefined) this.filters.toBlock = to;
336
+ return this;
337
+ }
338
+
339
+ /**
340
+ * Filter by event name
341
+ */
342
+ eventName(name) {
343
+ this.filters.eventName = name;
344
+ return this;
345
+ }
346
+
347
+ /**
348
+ * Filter by transaction hash
349
+ */
350
+ transactionHash(hash) {
351
+ this.filters.transactionHash = hash;
352
+ return this;
353
+ }
354
+
355
+ /**
356
+ * Filter by contract address
357
+ */
358
+ address(addr) {
359
+ this.filters.address = addr;
360
+ return this;
361
+ }
362
+
363
+ /**
364
+ * Filter by event parameters
365
+ */
366
+ where(field, value) {
367
+ if (!this.filters.parameters) {
368
+ this.filters.parameters = {};
369
+ }
370
+ this.filters.parameters[field] = value;
371
+ return this;
372
+ }
373
+
374
+ /**
375
+ * Add an $or condition
376
+ */
377
+ orWhere(field, value) {
378
+ if (!this.filters.$or) {
379
+ this.filters.$or = [];
380
+ }
381
+ this.filters.$or.push({ parameters: { [field]: value } });
382
+ return this;
383
+ }
384
+
385
+ /**
386
+ * Shorthand for exists check on a field
387
+ */
388
+ whereExists(field) {
389
+ if (!this.filters.parameters) {
390
+ this.filters.parameters = {};
391
+ }
392
+ this.filters.parameters[field] = { exists: true };
393
+ return this;
394
+ }
395
+
396
+ /**
397
+ * Shorthand for regex match on a field
398
+ */
399
+ whereRegex(field, pattern) {
400
+ if (!this.filters.parameters) {
401
+ this.filters.parameters = {};
402
+ }
403
+ this.filters.parameters[field] = { regex: pattern };
404
+ return this;
405
+ }
406
+
407
+ /**
408
+ * Limit number of results
409
+ */
410
+ limit(n) {
411
+ this._limit = n;
412
+ return this;
413
+ }
414
+
415
+ /**
416
+ * Skip first n results
417
+ */
418
+ skip(n) {
419
+ this._skip = n;
420
+ return this;
421
+ }
422
+
423
+ /**
424
+ * Sort results
425
+ */
426
+ orderBy(field, order = 'asc') {
427
+ this.sortOptions = { by: field, order };
428
+ return this;
429
+ }
430
+
431
+ /**
432
+ * Execute query and return results
433
+ */
434
+ execute() {
435
+ let results = this.eventFilter.applyFilter(this.events, this.filters);
436
+
437
+ if (this.sortOptions) {
438
+ results = this.eventFilter.sortEvents(results, this.sortOptions);
439
+ }
440
+
441
+ if (this._skip !== null) {
442
+ results = results.slice(this._skip);
443
+ }
444
+
445
+ if (this._limit !== null) {
446
+ results = results.slice(0, this._limit);
447
+ }
448
+
449
+ return results;
450
+ }
451
+
452
+ /**
453
+ * Execute query and return first result
454
+ */
455
+ first() {
456
+ const results = this.execute();
457
+ return results.length > 0 ? results[0] : null;
458
+ }
459
+
460
+ /**
461
+ * Execute query and return count
462
+ */
463
+ count() {
464
+ return this.execute().length;
465
+ }
466
+
467
+ /**
468
+ * Execute query and group results
469
+ */
470
+ groupBy(field) {
471
+ const results = this.execute();
472
+ return this.eventFilter.groupEvents(results, field);
473
+ }
474
+ }