@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.
- package/LICENSE +21 -0
- package/README.md +167 -0
- package/dist/magazine.cjs.js +5 -0
- package/dist/magazine.cjs.js.map +1 -0
- package/dist/magazine.es.js +4149 -0
- package/dist/magazine.es.js.map +1 -0
- package/dist/magazine.umd.js +5 -0
- package/dist/magazine.umd.js.map +1 -0
- package/package.json +74 -0
- package/src/components/ConfigManager.js +202 -0
- package/src/components/ConfigSchema.js +506 -0
- package/src/components/DataCompressor.js +378 -0
- package/src/components/DataTransformer.js +1172 -0
- package/src/components/DebugUtils.js +657 -0
- package/src/components/EventFilter.js +474 -0
- package/src/components/EventMetadata.js +273 -0
- package/src/components/EventStore.js +443 -0
- package/src/components/EventSyncer.js +353 -0
- package/src/components/Logger.js +299 -0
- package/src/components/NetworkManager.js +461 -0
- package/src/components/Paginator.js +343 -0
- package/src/index.js +571 -0
|
@@ -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
|
+
}
|