s3db.js 10.0.4 → 10.0.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "s3db.js",
3
- "version": "10.0.4",
3
+ "version": "10.0.6",
4
4
  "description": "Use AWS S3, the world's most reliable document storage, as a database with this ORM.",
5
5
  "main": "dist/s3db.cjs.js",
6
6
  "module": "dist/s3db.es.js",
@@ -58,16 +58,16 @@
58
58
  "UNLICENSE"
59
59
  ],
60
60
  "dependencies": {
61
- "@aws-sdk/client-s3": "^3.873.0",
62
- "@modelcontextprotocol/sdk": "^1.17.4",
63
- "@smithy/node-http-handler": "^4.1.1",
61
+ "@aws-sdk/client-s3": "^3.906.0",
62
+ "@modelcontextprotocol/sdk": "^1.19.1",
63
+ "@smithy/node-http-handler": "^4.3.0",
64
64
  "@supercharge/promise-pool": "^3.2.0",
65
- "dotenv": "^17.2.1",
65
+ "dotenv": "^17.2.3",
66
66
  "fastest-validator": "^1.19.1",
67
67
  "flat": "^6.0.1",
68
68
  "json-stable-stringify": "^1.3.0",
69
69
  "lodash-es": "^4.17.21",
70
- "nanoid": "5.1.5"
70
+ "nanoid": "5.1.6"
71
71
  },
72
72
  "peerDependencies": {
73
73
  "@aws-sdk/client-sqs": "^3.0.0",
@@ -94,32 +94,32 @@
94
94
  }
95
95
  },
96
96
  "devDependencies": {
97
- "@babel/core": "^7.28.3",
97
+ "@babel/core": "^7.28.4",
98
98
  "@babel/preset-env": "^7.28.3",
99
99
  "@rollup/plugin-commonjs": "^28.0.6",
100
100
  "@rollup/plugin-json": "^6.1.0",
101
- "@rollup/plugin-node-resolve": "^16.0.1",
101
+ "@rollup/plugin-node-resolve": "^16.0.2",
102
102
  "@rollup/plugin-replace": "^6.0.2",
103
103
  "@rollup/plugin-terser": "^0.4.4",
104
- "@types/node": "24.3.0",
104
+ "@types/node": "24.7.0",
105
105
  "babel-loader": "^10.0.0",
106
- "chalk": "^5.6.0",
106
+ "chalk": "^5.6.2",
107
107
  "cli-table3": "^0.6.5",
108
- "commander": "^14.0.0",
109
- "esbuild": "^0.25.9",
110
- "inquirer": "^12.9.3",
111
- "jest": "^30.0.5",
108
+ "commander": "^14.0.1",
109
+ "esbuild": "^0.25.10",
110
+ "inquirer": "^12.9.6",
111
+ "jest": "^30.2.0",
112
112
  "node-loader": "^2.1.0",
113
- "ora": "^8.2.0",
113
+ "ora": "^9.0.0",
114
114
  "pkg": "^5.8.1",
115
- "rollup": "^4.48.0",
115
+ "rollup": "^4.52.4",
116
116
  "rollup-plugin-copy": "^3.5.0",
117
117
  "rollup-plugin-esbuild": "^6.2.1",
118
118
  "rollup-plugin-polyfill-node": "^0.13.0",
119
119
  "rollup-plugin-shebang-bin": "^0.1.0",
120
120
  "rollup-plugin-terser": "^7.0.2",
121
- "typescript": "5.9.2",
122
- "webpack": "^5.101.3",
121
+ "typescript": "5.9.3",
122
+ "webpack": "^5.102.1",
123
123
  "webpack-cli": "^6.0.1"
124
124
  },
125
125
  "funding": [
@@ -186,10 +186,33 @@ export class EventualConsistencyPlugin extends Plugin {
186
186
  // Setup consolidation if enabled
187
187
  if (this.config.autoConsolidate) {
188
188
  this.startConsolidationTimer();
189
+ if (this.config.verbose) {
190
+ console.log(
191
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
192
+ `Auto-consolidation ENABLED (interval: ${this.config.consolidationInterval}s, ` +
193
+ `window: ${this.config.consolidationWindow}h, mode: ${this.config.mode})`
194
+ );
195
+ }
196
+ } else {
197
+ if (this.config.verbose) {
198
+ console.log(
199
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
200
+ `Auto-consolidation DISABLED (manual consolidation only)`
201
+ );
202
+ }
189
203
  }
190
204
 
191
205
  // Setup garbage collection timer
192
206
  this.startGarbageCollectionTimer();
207
+
208
+ if (this.config.verbose) {
209
+ console.log(
210
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
211
+ `Setup complete. Resources: ${this.config.resource}_transactions_${this.config.field}, ` +
212
+ `${this.config.resource}_consolidation_locks_${this.config.field}` +
213
+ `${this.config.enableAnalytics ? `, ${this.config.resource}_analytics_${this.config.field}` : ''}`
214
+ );
215
+ }
193
216
  }
194
217
 
195
218
  async onStart() {
@@ -539,15 +562,31 @@ export class EventualConsistencyPlugin extends Plugin {
539
562
  // Batch transactions if configured
540
563
  if (this.config.batchTransactions) {
541
564
  this.pendingTransactions.set(transaction.id, transaction);
542
-
565
+
566
+ if (this.config.verbose) {
567
+ console.log(
568
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
569
+ `Transaction batched: ${data.operation} ${data.value} for ${data.originalId} ` +
570
+ `(batch: ${this.pendingTransactions.size}/${this.config.batchSize})`
571
+ );
572
+ }
573
+
543
574
  // Flush if batch size reached
544
575
  if (this.pendingTransactions.size >= this.config.batchSize) {
545
576
  await this.flushPendingTransactions();
546
577
  }
547
578
  } else {
548
579
  await this.transactionResource.insert(transaction);
580
+
581
+ if (this.config.verbose) {
582
+ console.log(
583
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
584
+ `Transaction created: ${data.operation} ${data.value} for ${data.originalId} ` +
585
+ `(cohort: ${cohortInfo.hour}, applied: false)`
586
+ );
587
+ }
549
588
  }
550
-
589
+
551
590
  return transaction;
552
591
  }
553
592
 
@@ -636,12 +675,30 @@ export class EventualConsistencyPlugin extends Plugin {
636
675
  startConsolidationTimer() {
637
676
  const intervalMs = this.config.consolidationInterval * 1000; // Convert seconds to ms
638
677
 
678
+ if (this.config.verbose) {
679
+ const nextRun = new Date(Date.now() + intervalMs);
680
+ console.log(
681
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
682
+ `Consolidation timer started. Next run at ${nextRun.toISOString()} ` +
683
+ `(every ${this.config.consolidationInterval}s)`
684
+ );
685
+ }
686
+
639
687
  this.consolidationTimer = setInterval(async () => {
640
688
  await this.runConsolidation();
641
689
  }, intervalMs);
642
690
  }
643
691
 
644
692
  async runConsolidation() {
693
+ const startTime = Date.now();
694
+
695
+ if (this.config.verbose) {
696
+ console.log(
697
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
698
+ `Starting consolidation run at ${new Date().toISOString()}`
699
+ );
700
+ }
701
+
645
702
  try {
646
703
  // Query unapplied transactions from recent cohorts (last 24 hours by default)
647
704
  // This uses hourly partition for O(1) performance instead of full scan
@@ -655,6 +712,13 @@ export class EventualConsistencyPlugin extends Plugin {
655
712
  cohortHours.push(cohortInfo.hour);
656
713
  }
657
714
 
715
+ if (this.config.verbose) {
716
+ console.log(
717
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
718
+ `Querying ${hoursToCheck} hour partitions for pending transactions...`
719
+ );
720
+ }
721
+
658
722
  // Query transactions by partition for each hour (parallel for speed)
659
723
  const transactionsByHour = await Promise.all(
660
724
  cohortHours.map(async (cohortHour) => {
@@ -673,7 +737,10 @@ export class EventualConsistencyPlugin extends Plugin {
673
737
 
674
738
  if (transactions.length === 0) {
675
739
  if (this.config.verbose) {
676
- console.log(`[EventualConsistency] No pending transactions to consolidate`);
740
+ console.log(
741
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
742
+ `No pending transactions found. Next run in ${this.config.consolidationInterval}s`
743
+ );
677
744
  }
678
745
  return;
679
746
  }
@@ -681,6 +748,14 @@ export class EventualConsistencyPlugin extends Plugin {
681
748
  // Get unique originalIds
682
749
  const uniqueIds = [...new Set(transactions.map(t => t.originalId))];
683
750
 
751
+ if (this.config.verbose) {
752
+ console.log(
753
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
754
+ `Found ${transactions.length} pending transactions for ${uniqueIds.length} records. ` +
755
+ `Consolidating with concurrency=${this.config.consolidationConcurrency}...`
756
+ );
757
+ }
758
+
684
759
  // Consolidate each record in parallel with concurrency limit
685
760
  const { results, errors } = await PromisePool
686
761
  .for(uniqueIds)
@@ -689,8 +764,22 @@ export class EventualConsistencyPlugin extends Plugin {
689
764
  return await this.consolidateRecord(id);
690
765
  });
691
766
 
767
+ const duration = Date.now() - startTime;
768
+
692
769
  if (errors && errors.length > 0) {
693
- console.error(`Consolidation completed with ${errors.length} errors:`, errors);
770
+ console.error(
771
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
772
+ `Consolidation completed with ${errors.length} errors in ${duration}ms:`,
773
+ errors
774
+ );
775
+ }
776
+
777
+ if (this.config.verbose) {
778
+ console.log(
779
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
780
+ `Consolidation complete: ${results.length} records consolidated in ${duration}ms ` +
781
+ `(${errors.length} errors). Next run in ${this.config.consolidationInterval}s`
782
+ );
694
783
  }
695
784
 
696
785
  this.emit('eventual-consistency.consolidated', {
@@ -698,10 +787,16 @@ export class EventualConsistencyPlugin extends Plugin {
698
787
  field: this.config.field,
699
788
  recordCount: uniqueIds.length,
700
789
  successCount: results.length,
701
- errorCount: errors.length
790
+ errorCount: errors.length,
791
+ duration
702
792
  });
703
793
  } catch (error) {
704
- console.error('Consolidation error:', error);
794
+ const duration = Date.now() - startTime;
795
+ console.error(
796
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
797
+ `Consolidation error after ${duration}ms:`,
798
+ error
799
+ );
705
800
  this.emit('eventual-consistency.consolidation-error', error);
706
801
  }
707
802
  }
@@ -749,9 +844,23 @@ export class EventualConsistencyPlugin extends Plugin {
749
844
  );
750
845
 
751
846
  if (!ok || !transactions || transactions.length === 0) {
847
+ if (this.config.verbose) {
848
+ console.log(
849
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
850
+ `No pending transactions for ${originalId}, skipping`
851
+ );
852
+ }
752
853
  return currentValue;
753
854
  }
754
855
 
856
+ if (this.config.verbose) {
857
+ console.log(
858
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
859
+ `Consolidating ${originalId}: ${transactions.length} pending transactions ` +
860
+ `(current: ${currentValue})`
861
+ );
862
+ }
863
+
755
864
  // Sort transactions by timestamp
756
865
  transactions.sort((a, b) =>
757
866
  new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
@@ -766,6 +875,14 @@ export class EventualConsistencyPlugin extends Plugin {
766
875
  // Apply reducer to get consolidated value
767
876
  const consolidatedValue = this.config.reducer(transactions);
768
877
 
878
+ if (this.config.verbose) {
879
+ console.log(
880
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
881
+ `${originalId}: ${currentValue} → ${consolidatedValue} ` +
882
+ `(${consolidatedValue > currentValue ? '+' : ''}${consolidatedValue - currentValue})`
883
+ );
884
+ }
885
+
769
886
  // Update the original record
770
887
  const [updateOk, updateErr] = await tryFn(() =>
771
888
  this.targetResource.update(originalId, {
@@ -1087,9 +1204,24 @@ export class EventualConsistencyPlugin extends Plugin {
1087
1204
  async updateAnalytics(transactions) {
1088
1205
  if (!this.analyticsResource || transactions.length === 0) return;
1089
1206
 
1207
+ if (this.config.verbose) {
1208
+ console.log(
1209
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
1210
+ `Updating analytics for ${transactions.length} transactions...`
1211
+ );
1212
+ }
1213
+
1090
1214
  try {
1091
1215
  // Group transactions by cohort hour
1092
1216
  const byHour = this._groupByCohort(transactions, 'cohortHour');
1217
+ const cohortCount = Object.keys(byHour).length;
1218
+
1219
+ if (this.config.verbose) {
1220
+ console.log(
1221
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
1222
+ `Updating ${cohortCount} hourly analytics cohorts...`
1223
+ );
1224
+ }
1093
1225
 
1094
1226
  // Update hourly analytics
1095
1227
  for (const [cohort, txns] of Object.entries(byHour)) {
@@ -1099,14 +1231,31 @@ export class EventualConsistencyPlugin extends Plugin {
1099
1231
  // Roll up to daily and monthly if configured
1100
1232
  if (this.config.analyticsConfig.rollupStrategy === 'incremental') {
1101
1233
  const uniqueHours = Object.keys(byHour);
1234
+
1235
+ if (this.config.verbose) {
1236
+ console.log(
1237
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
1238
+ `Rolling up ${uniqueHours.length} hours to daily/monthly analytics...`
1239
+ );
1240
+ }
1241
+
1102
1242
  for (const cohortHour of uniqueHours) {
1103
1243
  await this._rollupAnalytics(cohortHour);
1104
1244
  }
1105
1245
  }
1106
- } catch (error) {
1246
+
1107
1247
  if (this.config.verbose) {
1108
- console.warn(`[EventualConsistency] Analytics update error:`, error.message);
1248
+ console.log(
1249
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
1250
+ `Analytics update complete for ${cohortCount} cohorts`
1251
+ );
1109
1252
  }
1253
+ } catch (error) {
1254
+ console.warn(
1255
+ `[EventualConsistency] ${this.config.resource}.${this.config.field} - ` +
1256
+ `Analytics update error:`,
1257
+ error.message
1258
+ );
1110
1259
  }
1111
1260
  }
1112
1261
 
@@ -1402,14 +1551,86 @@ export class EventualConsistencyPlugin extends Plugin {
1402
1551
  }));
1403
1552
  }
1404
1553
 
1554
+ /**
1555
+ * Fill gaps in analytics data with zeros for continuous time series
1556
+ * @private
1557
+ * @param {Array} data - Sparse analytics data
1558
+ * @param {string} period - Period type ('hour', 'day', 'month')
1559
+ * @param {string} startDate - Start date (ISO format)
1560
+ * @param {string} endDate - End date (ISO format)
1561
+ * @returns {Array} Complete time series with gaps filled
1562
+ */
1563
+ _fillGaps(data, period, startDate, endDate) {
1564
+ if (!data || data.length === 0) {
1565
+ // If no data, still generate empty series
1566
+ data = [];
1567
+ }
1568
+
1569
+ // Create a map of existing data by cohort
1570
+ const dataMap = new Map();
1571
+ data.forEach(item => {
1572
+ dataMap.set(item.cohort, item);
1573
+ });
1574
+
1575
+ const result = [];
1576
+ const emptyRecord = {
1577
+ count: 0,
1578
+ sum: 0,
1579
+ avg: 0,
1580
+ min: 0,
1581
+ max: 0,
1582
+ recordCount: 0
1583
+ };
1584
+
1585
+ if (period === 'hour') {
1586
+ // Generate all hours between startDate and endDate
1587
+ const start = new Date(startDate + 'T00:00:00Z');
1588
+ const end = new Date(endDate + 'T23:59:59Z');
1589
+
1590
+ for (let dt = new Date(start); dt <= end; dt.setHours(dt.getHours() + 1)) {
1591
+ const cohort = dt.toISOString().substring(0, 13); // YYYY-MM-DDTHH
1592
+ result.push(dataMap.get(cohort) || { cohort, ...emptyRecord });
1593
+ }
1594
+ } else if (period === 'day') {
1595
+ // Generate all days between startDate and endDate
1596
+ const start = new Date(startDate);
1597
+ const end = new Date(endDate);
1598
+
1599
+ for (let dt = new Date(start); dt <= end; dt.setDate(dt.getDate() + 1)) {
1600
+ const cohort = dt.toISOString().substring(0, 10); // YYYY-MM-DD
1601
+ result.push(dataMap.get(cohort) || { cohort, ...emptyRecord });
1602
+ }
1603
+ } else if (period === 'month') {
1604
+ // Generate all months between startDate and endDate
1605
+ const startYear = parseInt(startDate.substring(0, 4));
1606
+ const startMonth = parseInt(startDate.substring(5, 7));
1607
+ const endYear = parseInt(endDate.substring(0, 4));
1608
+ const endMonth = parseInt(endDate.substring(5, 7));
1609
+
1610
+ for (let year = startYear; year <= endYear; year++) {
1611
+ const firstMonth = (year === startYear) ? startMonth : 1;
1612
+ const lastMonth = (year === endYear) ? endMonth : 12;
1613
+
1614
+ for (let month = firstMonth; month <= lastMonth; month++) {
1615
+ const cohort = `${year}-${month.toString().padStart(2, '0')}`;
1616
+ result.push(dataMap.get(cohort) || { cohort, ...emptyRecord });
1617
+ }
1618
+ }
1619
+ }
1620
+
1621
+ return result;
1622
+ }
1623
+
1405
1624
  /**
1406
1625
  * Get analytics for entire month, broken down by days
1407
1626
  * @param {string} resourceName - Resource name
1408
1627
  * @param {string} field - Field name
1409
1628
  * @param {string} month - Month in YYYY-MM format
1629
+ * @param {Object} options - Options
1630
+ * @param {boolean} options.fillGaps - Fill missing days with zeros (default: false)
1410
1631
  * @returns {Promise<Array>} Daily analytics for the month
1411
1632
  */
1412
- async getMonthByDay(resourceName, field, month) {
1633
+ async getMonthByDay(resourceName, field, month, options = {}) {
1413
1634
  // month format: '2025-10'
1414
1635
  const year = parseInt(month.substring(0, 4));
1415
1636
  const monthNum = parseInt(month.substring(5, 7));
@@ -1421,11 +1642,17 @@ export class EventualConsistencyPlugin extends Plugin {
1421
1642
  const startDate = firstDay.toISOString().substring(0, 10);
1422
1643
  const endDate = lastDay.toISOString().substring(0, 10);
1423
1644
 
1424
- return await this.getAnalytics(resourceName, field, {
1645
+ const data = await this.getAnalytics(resourceName, field, {
1425
1646
  period: 'day',
1426
1647
  startDate,
1427
1648
  endDate
1428
1649
  });
1650
+
1651
+ if (options.fillGaps) {
1652
+ return this._fillGaps(data, 'day', startDate, endDate);
1653
+ }
1654
+
1655
+ return data;
1429
1656
  }
1430
1657
 
1431
1658
  /**
@@ -1433,14 +1660,22 @@ export class EventualConsistencyPlugin extends Plugin {
1433
1660
  * @param {string} resourceName - Resource name
1434
1661
  * @param {string} field - Field name
1435
1662
  * @param {string} date - Date in YYYY-MM-DD format
1663
+ * @param {Object} options - Options
1664
+ * @param {boolean} options.fillGaps - Fill missing hours with zeros (default: false)
1436
1665
  * @returns {Promise<Array>} Hourly analytics for the day
1437
1666
  */
1438
- async getDayByHour(resourceName, field, date) {
1667
+ async getDayByHour(resourceName, field, date, options = {}) {
1439
1668
  // date format: '2025-10-09'
1440
- return await this.getAnalytics(resourceName, field, {
1669
+ const data = await this.getAnalytics(resourceName, field, {
1441
1670
  period: 'hour',
1442
1671
  date
1443
1672
  });
1673
+
1674
+ if (options.fillGaps) {
1675
+ return this._fillGaps(data, 'hour', date, date);
1676
+ }
1677
+
1678
+ return data;
1444
1679
  }
1445
1680
 
1446
1681
  /**
@@ -1448,20 +1683,28 @@ export class EventualConsistencyPlugin extends Plugin {
1448
1683
  * @param {string} resourceName - Resource name
1449
1684
  * @param {string} field - Field name
1450
1685
  * @param {number} days - Number of days to look back (default: 7)
1686
+ * @param {Object} options - Options
1687
+ * @param {boolean} options.fillGaps - Fill missing days with zeros (default: false)
1451
1688
  * @returns {Promise<Array>} Daily analytics
1452
1689
  */
1453
- async getLastNDays(resourceName, field, days = 7) {
1690
+ async getLastNDays(resourceName, field, days = 7, options = {}) {
1454
1691
  const dates = Array.from({ length: days }, (_, i) => {
1455
1692
  const date = new Date();
1456
1693
  date.setDate(date.getDate() - i);
1457
1694
  return date.toISOString().substring(0, 10);
1458
1695
  }).reverse();
1459
1696
 
1460
- return await this.getAnalytics(resourceName, field, {
1697
+ const data = await this.getAnalytics(resourceName, field, {
1461
1698
  period: 'day',
1462
1699
  startDate: dates[0],
1463
1700
  endDate: dates[dates.length - 1]
1464
1701
  });
1702
+
1703
+ if (options.fillGaps) {
1704
+ return this._fillGaps(data, 'day', dates[0], dates[dates.length - 1]);
1705
+ }
1706
+
1707
+ return data;
1465
1708
  }
1466
1709
 
1467
1710
  /**
@@ -1469,13 +1712,23 @@ export class EventualConsistencyPlugin extends Plugin {
1469
1712
  * @param {string} resourceName - Resource name
1470
1713
  * @param {string} field - Field name
1471
1714
  * @param {number} year - Year (e.g., 2025)
1715
+ * @param {Object} options - Options
1716
+ * @param {boolean} options.fillGaps - Fill missing months with zeros (default: false)
1472
1717
  * @returns {Promise<Array>} Monthly analytics for the year
1473
1718
  */
1474
- async getYearByMonth(resourceName, field, year) {
1475
- return await this.getAnalytics(resourceName, field, {
1719
+ async getYearByMonth(resourceName, field, year, options = {}) {
1720
+ const data = await this.getAnalytics(resourceName, field, {
1476
1721
  period: 'month',
1477
1722
  year
1478
1723
  });
1724
+
1725
+ if (options.fillGaps) {
1726
+ const startDate = `${year}-01`;
1727
+ const endDate = `${year}-12`;
1728
+ return this._fillGaps(data, 'month', startDate, endDate);
1729
+ }
1730
+
1731
+ return data;
1479
1732
  }
1480
1733
 
1481
1734
  /**
@@ -1483,9 +1736,11 @@ export class EventualConsistencyPlugin extends Plugin {
1483
1736
  * @param {string} resourceName - Resource name
1484
1737
  * @param {string} field - Field name
1485
1738
  * @param {string} month - Month in YYYY-MM format (or 'last' for previous month)
1739
+ * @param {Object} options - Options
1740
+ * @param {boolean} options.fillGaps - Fill missing hours with zeros (default: false)
1486
1741
  * @returns {Promise<Array>} Hourly analytics for the month (up to 24*31=744 records)
1487
1742
  */
1488
- async getMonthByHour(resourceName, field, month) {
1743
+ async getMonthByHour(resourceName, field, month, options = {}) {
1489
1744
  // month format: '2025-10' or 'last'
1490
1745
  let year, monthNum;
1491
1746
 
@@ -1506,11 +1761,17 @@ export class EventualConsistencyPlugin extends Plugin {
1506
1761
  const startDate = firstDay.toISOString().substring(0, 10);
1507
1762
  const endDate = lastDay.toISOString().substring(0, 10);
1508
1763
 
1509
- return await this.getAnalytics(resourceName, field, {
1764
+ const data = await this.getAnalytics(resourceName, field, {
1510
1765
  period: 'hour',
1511
1766
  startDate,
1512
1767
  endDate
1513
1768
  });
1769
+
1770
+ if (options.fillGaps) {
1771
+ return this._fillGaps(data, 'hour', startDate, endDate);
1772
+ }
1773
+
1774
+ return data;
1514
1775
  }
1515
1776
 
1516
1777
  /**