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/dist/s3db.cjs.js +206 -17
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.es.js +206 -17
- package/dist/s3db.es.js.map +1 -1
- package/package.json +18 -18
- package/src/plugins/eventual-consistency.plugin.js +279 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "s3db.js",
|
|
3
|
-
"version": "10.0.
|
|
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.
|
|
62
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
63
|
-
"@smithy/node-http-handler": "^4.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
104
|
+
"@types/node": "24.7.0",
|
|
105
105
|
"babel-loader": "^10.0.0",
|
|
106
|
-
"chalk": "^5.6.
|
|
106
|
+
"chalk": "^5.6.2",
|
|
107
107
|
"cli-table3": "^0.6.5",
|
|
108
|
-
"commander": "^14.0.
|
|
109
|
-
"esbuild": "^0.25.
|
|
110
|
-
"inquirer": "^12.9.
|
|
111
|
-
"jest": "^30.0
|
|
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": "^
|
|
113
|
+
"ora": "^9.0.0",
|
|
114
114
|
"pkg": "^5.8.1",
|
|
115
|
-
"rollup": "^4.
|
|
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.
|
|
122
|
-
"webpack": "^5.
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
1246
|
+
|
|
1107
1247
|
if (this.config.verbose) {
|
|
1108
|
-
console.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
/**
|