s3db.js 6.2.0 → 7.0.1

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.
Files changed (60) hide show
  1. package/PLUGINS.md +2724 -0
  2. package/README.md +372 -469
  3. package/UNLICENSE +24 -0
  4. package/dist/s3db.cjs.js +12105 -19396
  5. package/dist/s3db.cjs.min.js +1 -1
  6. package/dist/s3db.d.ts +373 -72
  7. package/dist/s3db.es.js +12090 -19393
  8. package/dist/s3db.es.min.js +1 -1
  9. package/dist/s3db.iife.js +12103 -19398
  10. package/dist/s3db.iife.min.js +1 -1
  11. package/package.json +44 -38
  12. package/src/behaviors/body-only.js +110 -0
  13. package/src/behaviors/body-overflow.js +153 -0
  14. package/src/behaviors/enforce-limits.js +195 -0
  15. package/src/behaviors/index.js +39 -0
  16. package/src/behaviors/truncate-data.js +204 -0
  17. package/src/behaviors/user-managed.js +147 -0
  18. package/src/client.class.js +515 -0
  19. package/src/concerns/base62.js +61 -0
  20. package/src/concerns/calculator.js +204 -0
  21. package/src/concerns/crypto.js +159 -0
  22. package/src/concerns/id.js +8 -0
  23. package/src/concerns/index.js +5 -0
  24. package/src/concerns/try-fn.js +151 -0
  25. package/src/connection-string.class.js +75 -0
  26. package/src/database.class.js +599 -0
  27. package/src/errors.js +261 -0
  28. package/src/index.js +17 -0
  29. package/src/plugins/audit.plugin.js +442 -0
  30. package/src/plugins/cache/cache.class.js +53 -0
  31. package/src/plugins/cache/index.js +6 -0
  32. package/src/plugins/cache/memory-cache.class.js +164 -0
  33. package/src/plugins/cache/s3-cache.class.js +189 -0
  34. package/src/plugins/cache.plugin.js +275 -0
  35. package/src/plugins/consumers/index.js +24 -0
  36. package/src/plugins/consumers/rabbitmq-consumer.js +56 -0
  37. package/src/plugins/consumers/sqs-consumer.js +102 -0
  38. package/src/plugins/costs.plugin.js +81 -0
  39. package/src/plugins/fulltext.plugin.js +473 -0
  40. package/src/plugins/index.js +12 -0
  41. package/src/plugins/metrics.plugin.js +603 -0
  42. package/src/plugins/plugin.class.js +210 -0
  43. package/src/plugins/plugin.obj.js +13 -0
  44. package/src/plugins/queue-consumer.plugin.js +134 -0
  45. package/src/plugins/replicator.plugin.js +769 -0
  46. package/src/plugins/replicators/base-replicator.class.js +85 -0
  47. package/src/plugins/replicators/bigquery-replicator.class.js +328 -0
  48. package/src/plugins/replicators/index.js +44 -0
  49. package/src/plugins/replicators/postgres-replicator.class.js +427 -0
  50. package/src/plugins/replicators/s3db-replicator.class.js +352 -0
  51. package/src/plugins/replicators/sqs-replicator.class.js +427 -0
  52. package/src/resource.class.js +2626 -0
  53. package/src/s3db.d.ts +1263 -0
  54. package/src/schema.class.js +706 -0
  55. package/src/stream/index.js +16 -0
  56. package/src/stream/resource-ids-page-reader.class.js +10 -0
  57. package/src/stream/resource-ids-reader.class.js +63 -0
  58. package/src/stream/resource-reader.class.js +81 -0
  59. package/src/stream/resource-writer.class.js +92 -0
  60. package/src/validator.class.js +97 -0
@@ -0,0 +1,603 @@
1
+ import Plugin from "./plugin.class.js";
2
+ import tryFn from "../concerns/try-fn.js";
3
+
4
+ export class MetricsPlugin extends Plugin {
5
+ constructor(options = {}) {
6
+ super();
7
+ this.config = {
8
+ collectPerformance: options.collectPerformance !== false,
9
+ collectErrors: options.collectErrors !== false,
10
+ collectUsage: options.collectUsage !== false,
11
+ retentionDays: options.retentionDays || 30,
12
+ flushInterval: options.flushInterval || 60000, // 1 minute
13
+ ...options
14
+ };
15
+
16
+ this.metrics = {
17
+ operations: {
18
+ insert: { count: 0, totalTime: 0, errors: 0 },
19
+ update: { count: 0, totalTime: 0, errors: 0 },
20
+ delete: { count: 0, totalTime: 0, errors: 0 },
21
+ get: { count: 0, totalTime: 0, errors: 0 },
22
+ list: { count: 0, totalTime: 0, errors: 0 },
23
+ count: { count: 0, totalTime: 0, errors: 0 }
24
+ },
25
+ resources: {},
26
+ errors: [],
27
+ performance: [],
28
+ startTime: new Date().toISOString()
29
+ };
30
+
31
+ this.flushTimer = null;
32
+ }
33
+
34
+ async setup(database) {
35
+ this.database = database;
36
+ if (process.env.NODE_ENV === 'test') return;
37
+
38
+ const [ok, err] = await tryFn(async () => {
39
+ const [ok1, err1, metricsResource] = await tryFn(() => database.createResource({
40
+ name: 'metrics',
41
+ attributes: {
42
+ id: 'string|required',
43
+ type: 'string|required', // 'operation', 'error', 'performance'
44
+ resourceName: 'string',
45
+ operation: 'string',
46
+ count: 'number|required',
47
+ totalTime: 'number|required',
48
+ errors: 'number|required',
49
+ avgTime: 'number|required',
50
+ timestamp: 'string|required',
51
+ metadata: 'json'
52
+ }
53
+ }));
54
+ this.metricsResource = ok1 ? metricsResource : database.resources.metrics;
55
+
56
+ const [ok2, err2, errorsResource] = await tryFn(() => database.createResource({
57
+ name: 'error_logs',
58
+ attributes: {
59
+ id: 'string|required',
60
+ resourceName: 'string|required',
61
+ operation: 'string|required',
62
+ error: 'string|required',
63
+ timestamp: 'string|required',
64
+ metadata: 'json'
65
+ }
66
+ }));
67
+ this.errorsResource = ok2 ? errorsResource : database.resources.error_logs;
68
+
69
+ const [ok3, err3, performanceResource] = await tryFn(() => database.createResource({
70
+ name: 'performance_logs',
71
+ attributes: {
72
+ id: 'string|required',
73
+ resourceName: 'string|required',
74
+ operation: 'string|required',
75
+ duration: 'number|required',
76
+ timestamp: 'string|required',
77
+ metadata: 'json'
78
+ }
79
+ }));
80
+ this.performanceResource = ok3 ? performanceResource : database.resources.performance_logs;
81
+ });
82
+ if (!ok) {
83
+ // Resources might already exist
84
+ this.metricsResource = database.resources.metrics;
85
+ this.errorsResource = database.resources.error_logs;
86
+ this.performanceResource = database.resources.performance_logs;
87
+ }
88
+
89
+ // Install hooks for all resources except metrics resources
90
+ this.installMetricsHooks();
91
+
92
+ // Disable flush timer during tests to avoid side effects
93
+ if (process.env.NODE_ENV !== 'test') {
94
+ this.startFlushTimer();
95
+ }
96
+ }
97
+
98
+ async start() {
99
+ // Plugin is ready
100
+ }
101
+
102
+ async stop() {
103
+ // Stop flush timer and flush remaining metrics
104
+ if (this.flushTimer) {
105
+ clearInterval(this.flushTimer);
106
+ this.flushTimer = null;
107
+ }
108
+
109
+ // Don't flush metrics during tests
110
+ if (process.env.NODE_ENV !== 'test') {
111
+ await this.flushMetrics();
112
+ }
113
+ }
114
+
115
+ installMetricsHooks() {
116
+ // Only hook into non-metrics resources
117
+ for (const resource of Object.values(this.database.resources)) {
118
+ if (['metrics', 'error_logs', 'performance_logs'].includes(resource.name)) {
119
+ continue; // Skip metrics resources to avoid recursion
120
+ }
121
+
122
+ this.installResourceHooks(resource);
123
+ }
124
+
125
+ // Hook into database proxy for new resources
126
+ this.database._createResource = this.database.createResource;
127
+ this.database.createResource = async function (...args) {
128
+ const resource = await this._createResource(...args);
129
+ if (this.plugins?.metrics && !['metrics', 'error_logs', 'performance_logs'].includes(resource.name)) {
130
+ this.plugins.metrics.installResourceHooks(resource);
131
+ }
132
+ return resource;
133
+ };
134
+ }
135
+
136
+ installResourceHooks(resource) {
137
+ // Store original methods
138
+ resource._insert = resource.insert;
139
+ resource._update = resource.update;
140
+ resource._delete = resource.delete;
141
+ resource._deleteMany = resource.deleteMany;
142
+ resource._get = resource.get;
143
+ resource._getMany = resource.getMany;
144
+ resource._getAll = resource.getAll;
145
+ resource._list = resource.list;
146
+ resource._listIds = resource.listIds;
147
+ resource._count = resource.count;
148
+ resource._page = resource.page;
149
+
150
+ // Hook insert operations
151
+ resource.insert = async function (...args) {
152
+ const startTime = Date.now();
153
+ const [ok, err, result] = await tryFn(() => resource._insert(...args));
154
+ this.recordOperation(resource.name, 'insert', Date.now() - startTime, !ok);
155
+ if (!ok) this.recordError(resource.name, 'insert', err);
156
+ if (!ok) throw err;
157
+ return result;
158
+ }.bind(this);
159
+
160
+ // Hook update operations
161
+ resource.update = async function (...args) {
162
+ const startTime = Date.now();
163
+ const [ok, err, result] = await tryFn(() => resource._update(...args));
164
+ this.recordOperation(resource.name, 'update', Date.now() - startTime, !ok);
165
+ if (!ok) this.recordError(resource.name, 'update', err);
166
+ if (!ok) throw err;
167
+ return result;
168
+ }.bind(this);
169
+
170
+ // Hook delete operations
171
+ resource.delete = async function (...args) {
172
+ const startTime = Date.now();
173
+ const [ok, err, result] = await tryFn(() => resource._delete(...args));
174
+ this.recordOperation(resource.name, 'delete', Date.now() - startTime, !ok);
175
+ if (!ok) this.recordError(resource.name, 'delete', err);
176
+ if (!ok) throw err;
177
+ return result;
178
+ }.bind(this);
179
+
180
+ // Hook deleteMany operations
181
+ resource.deleteMany = async function (...args) {
182
+ const startTime = Date.now();
183
+ const [ok, err, result] = await tryFn(() => resource._deleteMany(...args));
184
+ this.recordOperation(resource.name, 'delete', Date.now() - startTime, !ok);
185
+ if (!ok) this.recordError(resource.name, 'delete', err);
186
+ if (!ok) throw err;
187
+ return result;
188
+ }.bind(this);
189
+
190
+ // Hook get operations
191
+ resource.get = async function (...args) {
192
+ const startTime = Date.now();
193
+ const [ok, err, result] = await tryFn(() => resource._get(...args));
194
+ this.recordOperation(resource.name, 'get', Date.now() - startTime, !ok);
195
+ if (!ok) this.recordError(resource.name, 'get', err);
196
+ if (!ok) throw err;
197
+ return result;
198
+ }.bind(this);
199
+
200
+ // Hook getMany operations
201
+ resource.getMany = async function (...args) {
202
+ const startTime = Date.now();
203
+ const [ok, err, result] = await tryFn(() => resource._getMany(...args));
204
+ this.recordOperation(resource.name, 'get', Date.now() - startTime, !ok);
205
+ if (!ok) this.recordError(resource.name, 'get', err);
206
+ if (!ok) throw err;
207
+ return result;
208
+ }.bind(this);
209
+
210
+ // Hook getAll operations
211
+ resource.getAll = async function (...args) {
212
+ const startTime = Date.now();
213
+ const [ok, err, result] = await tryFn(() => resource._getAll(...args));
214
+ this.recordOperation(resource.name, 'list', Date.now() - startTime, !ok);
215
+ if (!ok) this.recordError(resource.name, 'list', err);
216
+ if (!ok) throw err;
217
+ return result;
218
+ }.bind(this);
219
+
220
+ // Hook list operations
221
+ resource.list = async function (...args) {
222
+ const startTime = Date.now();
223
+ const [ok, err, result] = await tryFn(() => resource._list(...args));
224
+ this.recordOperation(resource.name, 'list', Date.now() - startTime, !ok);
225
+ if (!ok) this.recordError(resource.name, 'list', err);
226
+ if (!ok) throw err;
227
+ return result;
228
+ }.bind(this);
229
+
230
+ // Hook listIds operations
231
+ resource.listIds = async function (...args) {
232
+ const startTime = Date.now();
233
+ const [ok, err, result] = await tryFn(() => resource._listIds(...args));
234
+ this.recordOperation(resource.name, 'list', Date.now() - startTime, !ok);
235
+ if (!ok) this.recordError(resource.name, 'list', err);
236
+ if (!ok) throw err;
237
+ return result;
238
+ }.bind(this);
239
+
240
+ // Hook count operations
241
+ resource.count = async function (...args) {
242
+ const startTime = Date.now();
243
+ const [ok, err, result] = await tryFn(() => resource._count(...args));
244
+ this.recordOperation(resource.name, 'count', Date.now() - startTime, !ok);
245
+ if (!ok) this.recordError(resource.name, 'count', err);
246
+ if (!ok) throw err;
247
+ return result;
248
+ }.bind(this);
249
+
250
+ // Hook page operations
251
+ resource.page = async function (...args) {
252
+ const startTime = Date.now();
253
+ const [ok, err, result] = await tryFn(() => resource._page(...args));
254
+ this.recordOperation(resource.name, 'list', Date.now() - startTime, !ok);
255
+ if (!ok) this.recordError(resource.name, 'list', err);
256
+ if (!ok) throw err;
257
+ return result;
258
+ }.bind(this);
259
+ }
260
+
261
+ recordOperation(resourceName, operation, duration, isError) {
262
+ // Update global metrics
263
+ if (this.metrics.operations[operation]) {
264
+ this.metrics.operations[operation].count++;
265
+ this.metrics.operations[operation].totalTime += duration;
266
+ if (isError) {
267
+ this.metrics.operations[operation].errors++;
268
+ }
269
+ }
270
+
271
+ // Update resource-specific metrics
272
+ if (!this.metrics.resources[resourceName]) {
273
+ this.metrics.resources[resourceName] = {
274
+ insert: { count: 0, totalTime: 0, errors: 0 },
275
+ update: { count: 0, totalTime: 0, errors: 0 },
276
+ delete: { count: 0, totalTime: 0, errors: 0 },
277
+ get: { count: 0, totalTime: 0, errors: 0 },
278
+ list: { count: 0, totalTime: 0, errors: 0 },
279
+ count: { count: 0, totalTime: 0, errors: 0 }
280
+ };
281
+ }
282
+
283
+ if (this.metrics.resources[resourceName][operation]) {
284
+ this.metrics.resources[resourceName][operation].count++;
285
+ this.metrics.resources[resourceName][operation].totalTime += duration;
286
+ if (isError) {
287
+ this.metrics.resources[resourceName][operation].errors++;
288
+ }
289
+ }
290
+
291
+ // Record performance data if enabled
292
+ if (this.config.collectPerformance) {
293
+ this.metrics.performance.push({
294
+ resourceName,
295
+ operation,
296
+ duration,
297
+ timestamp: new Date().toISOString()
298
+ });
299
+ }
300
+ }
301
+
302
+ recordError(resourceName, operation, error) {
303
+ if (!this.config.collectErrors) return;
304
+
305
+ this.metrics.errors.push({
306
+ resourceName,
307
+ operation,
308
+ error: error.message,
309
+ stack: error.stack,
310
+ timestamp: new Date().toISOString()
311
+ });
312
+ }
313
+
314
+ startFlushTimer() {
315
+ if (this.flushTimer) {
316
+ clearInterval(this.flushTimer);
317
+ }
318
+
319
+ // Only start timer if flushInterval is greater than 0
320
+ if (this.config.flushInterval > 0) {
321
+ this.flushTimer = setInterval(() => {
322
+ this.flushMetrics().catch(console.error);
323
+ }, this.config.flushInterval);
324
+ }
325
+ }
326
+
327
+ async flushMetrics() {
328
+ if (!this.metricsResource) return;
329
+
330
+ const [ok, err] = await tryFn(async () => {
331
+ // Use empty metadata during tests to avoid header issues
332
+ const metadata = process.env.NODE_ENV === 'test' ? {} : { global: 'true' };
333
+ const perfMetadata = process.env.NODE_ENV === 'test' ? {} : { perf: 'true' };
334
+ const errorMetadata = process.env.NODE_ENV === 'test' ? {} : { error: 'true' };
335
+ const resourceMetadata = process.env.NODE_ENV === 'test' ? {} : { resource: 'true' };
336
+
337
+ // Flush operation metrics
338
+ for (const [operation, data] of Object.entries(this.metrics.operations)) {
339
+ if (data.count > 0) {
340
+ await this.metricsResource.insert({
341
+ id: `metrics-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
342
+ type: 'operation',
343
+ resourceName: 'global',
344
+ operation,
345
+ count: data.count,
346
+ totalTime: data.totalTime,
347
+ errors: data.errors,
348
+ avgTime: data.count > 0 ? data.totalTime / data.count : 0,
349
+ timestamp: new Date().toISOString(),
350
+ metadata
351
+ });
352
+ }
353
+ }
354
+
355
+ // Flush resource-specific metrics
356
+ for (const [resourceName, operations] of Object.entries(this.metrics.resources)) {
357
+ for (const [operation, data] of Object.entries(operations)) {
358
+ if (data.count > 0) {
359
+ await this.metricsResource.insert({
360
+ id: `metrics-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
361
+ type: 'operation',
362
+ resourceName,
363
+ operation,
364
+ count: data.count,
365
+ totalTime: data.totalTime,
366
+ errors: data.errors,
367
+ avgTime: data.count > 0 ? data.totalTime / data.count : 0,
368
+ timestamp: new Date().toISOString(),
369
+ metadata: resourceMetadata
370
+ });
371
+ }
372
+ }
373
+ }
374
+
375
+ // Flush performance logs
376
+ if (this.config.collectPerformance && this.metrics.performance.length > 0) {
377
+ for (const perf of this.metrics.performance) {
378
+ await this.performanceResource.insert({
379
+ id: `perf-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
380
+ resourceName: perf.resourceName,
381
+ operation: perf.operation,
382
+ duration: perf.duration,
383
+ timestamp: perf.timestamp,
384
+ metadata: perfMetadata
385
+ });
386
+ }
387
+ }
388
+
389
+ // Flush error logs
390
+ if (this.config.collectErrors && this.metrics.errors.length > 0) {
391
+ for (const error of this.metrics.errors) {
392
+ await this.errorsResource.insert({
393
+ id: `error-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
394
+ resourceName: error.resourceName,
395
+ operation: error.operation,
396
+ error: error.error,
397
+ stack: error.stack,
398
+ timestamp: error.timestamp,
399
+ metadata: errorMetadata
400
+ });
401
+ }
402
+ }
403
+
404
+ // Reset metrics after flushing
405
+ this.resetMetrics();
406
+ });
407
+ if (!ok) {
408
+ console.error('Failed to flush metrics:', err);
409
+ }
410
+ }
411
+
412
+ resetMetrics() {
413
+ // Reset operation metrics
414
+ for (const operation of Object.keys(this.metrics.operations)) {
415
+ this.metrics.operations[operation] = { count: 0, totalTime: 0, errors: 0 };
416
+ }
417
+
418
+ // Reset resource metrics
419
+ for (const resourceName of Object.keys(this.metrics.resources)) {
420
+ for (const operation of Object.keys(this.metrics.resources[resourceName])) {
421
+ this.metrics.resources[resourceName][operation] = { count: 0, totalTime: 0, errors: 0 };
422
+ }
423
+ }
424
+
425
+ // Clear performance and error arrays
426
+ this.metrics.performance = [];
427
+ this.metrics.errors = [];
428
+ }
429
+
430
+ // Utility methods
431
+ async getMetrics(options = {}) {
432
+ const {
433
+ type = 'operation',
434
+ resourceName,
435
+ operation,
436
+ startDate,
437
+ endDate,
438
+ limit = 100,
439
+ offset = 0
440
+ } = options;
441
+
442
+ if (!this.metricsResource) return [];
443
+
444
+ const allMetrics = await this.metricsResource.getAll();
445
+
446
+ let filtered = allMetrics.filter(metric => {
447
+ if (type && metric.type !== type) return false;
448
+ if (resourceName && metric.resourceName !== resourceName) return false;
449
+ if (operation && metric.operation !== operation) return false;
450
+ if (startDate && new Date(metric.timestamp) < new Date(startDate)) return false;
451
+ if (endDate && new Date(metric.timestamp) > new Date(endDate)) return false;
452
+ return true;
453
+ });
454
+
455
+ // Sort by timestamp descending
456
+ filtered.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
457
+
458
+ return filtered.slice(offset, offset + limit);
459
+ }
460
+
461
+ async getErrorLogs(options = {}) {
462
+ if (!this.errorsResource) return [];
463
+
464
+ const {
465
+ resourceName,
466
+ operation,
467
+ startDate,
468
+ endDate,
469
+ limit = 100,
470
+ offset = 0
471
+ } = options;
472
+
473
+ const allErrors = await this.errorsResource.getAll();
474
+
475
+ let filtered = allErrors.filter(error => {
476
+ if (resourceName && error.resourceName !== resourceName) return false;
477
+ if (operation && error.operation !== operation) return false;
478
+ if (startDate && new Date(error.timestamp) < new Date(startDate)) return false;
479
+ if (endDate && new Date(error.timestamp) > new Date(endDate)) return false;
480
+ return true;
481
+ });
482
+
483
+ // Sort by timestamp descending
484
+ filtered.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
485
+
486
+ return filtered.slice(offset, offset + limit);
487
+ }
488
+
489
+ async getPerformanceLogs(options = {}) {
490
+ if (!this.performanceResource) return [];
491
+
492
+ const {
493
+ resourceName,
494
+ operation,
495
+ startDate,
496
+ endDate,
497
+ limit = 100,
498
+ offset = 0
499
+ } = options;
500
+
501
+ const allPerformance = await this.performanceResource.getAll();
502
+
503
+ let filtered = allPerformance.filter(perf => {
504
+ if (resourceName && perf.resourceName !== resourceName) return false;
505
+ if (operation && perf.operation !== operation) return false;
506
+ if (startDate && new Date(perf.timestamp) < new Date(startDate)) return false;
507
+ if (endDate && new Date(perf.timestamp) > new Date(endDate)) return false;
508
+ return true;
509
+ });
510
+
511
+ // Sort by timestamp descending
512
+ filtered.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
513
+
514
+ return filtered.slice(offset, offset + limit);
515
+ }
516
+
517
+ async getStats() {
518
+ const now = new Date();
519
+ const startDate = new Date(now.getTime() - (24 * 60 * 60 * 1000)); // Last 24 hours
520
+
521
+ const [metrics, errors, performance] = await Promise.all([
522
+ this.getMetrics({ startDate: startDate.toISOString() }),
523
+ this.getErrorLogs({ startDate: startDate.toISOString() }),
524
+ this.getPerformanceLogs({ startDate: startDate.toISOString() })
525
+ ]);
526
+
527
+ // Calculate summary statistics
528
+ const stats = {
529
+ period: '24h',
530
+ totalOperations: 0,
531
+ totalErrors: errors.length,
532
+ avgResponseTime: 0,
533
+ operationsByType: {},
534
+ resources: {},
535
+ uptime: {
536
+ startTime: this.metrics.startTime,
537
+ duration: now.getTime() - new Date(this.metrics.startTime).getTime()
538
+ }
539
+ };
540
+
541
+ // Aggregate metrics
542
+ for (const metric of metrics) {
543
+ if (metric.type === 'operation') {
544
+ stats.totalOperations += metric.count;
545
+
546
+ if (!stats.operationsByType[metric.operation]) {
547
+ stats.operationsByType[metric.operation] = {
548
+ count: 0,
549
+ errors: 0,
550
+ avgTime: 0
551
+ };
552
+ }
553
+
554
+ stats.operationsByType[metric.operation].count += metric.count;
555
+ stats.operationsByType[metric.operation].errors += metric.errors;
556
+
557
+ // Calculate weighted average
558
+ const current = stats.operationsByType[metric.operation];
559
+ const totalCount = current.count;
560
+ const newAvg = ((current.avgTime * (totalCount - metric.count)) + metric.totalTime) / totalCount;
561
+ current.avgTime = newAvg;
562
+ }
563
+ }
564
+
565
+ // Calculate overall average response time
566
+ const totalTime = metrics.reduce((sum, m) => sum + m.totalTime, 0);
567
+ const totalCount = metrics.reduce((sum, m) => sum + m.count, 0);
568
+ stats.avgResponseTime = totalCount > 0 ? totalTime / totalCount : 0;
569
+
570
+ return stats;
571
+ }
572
+
573
+ async cleanupOldData() {
574
+ const cutoffDate = new Date();
575
+ cutoffDate.setDate(cutoffDate.getDate() - this.config.retentionDays);
576
+
577
+ // Clean up old metrics
578
+ if (this.metricsResource) {
579
+ const oldMetrics = await this.getMetrics({ endDate: cutoffDate.toISOString() });
580
+ for (const metric of oldMetrics) {
581
+ await this.metricsResource.delete(metric.id);
582
+ }
583
+ }
584
+
585
+ // Clean up old error logs
586
+ if (this.errorsResource) {
587
+ const oldErrors = await this.getErrorLogs({ endDate: cutoffDate.toISOString() });
588
+ for (const error of oldErrors) {
589
+ await this.errorsResource.delete(error.id);
590
+ }
591
+ }
592
+
593
+ // Clean up old performance logs
594
+ if (this.performanceResource) {
595
+ const oldPerformance = await this.getPerformanceLogs({ endDate: cutoffDate.toISOString() });
596
+ for (const perf of oldPerformance) {
597
+ await this.performanceResource.delete(perf.id);
598
+ }
599
+ }
600
+ }
601
+ }
602
+
603
+ export default MetricsPlugin;