s3db.js 10.0.3 → 10.0.4
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 +461 -2
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.es.js +461 -2
- package/dist/s3db.es.js.map +1 -1
- package/package.json +1 -1
- package/src/plugins/eventual-consistency.plugin.js +574 -2
package/dist/s3db.es.js
CHANGED
|
@@ -4339,10 +4339,20 @@ class EventualConsistencyPlugin extends Plugin {
|
|
|
4339
4339
|
// Days to keep applied transactions
|
|
4340
4340
|
gcInterval: options.gcInterval || 86400,
|
|
4341
4341
|
// 24 hours (in seconds)
|
|
4342
|
-
verbose: options.verbose || false
|
|
4342
|
+
verbose: options.verbose || false,
|
|
4343
|
+
// Analytics configuration
|
|
4344
|
+
enableAnalytics: options.enableAnalytics || false,
|
|
4345
|
+
analyticsConfig: {
|
|
4346
|
+
periods: options.analyticsConfig?.periods || ["hour", "day", "month"],
|
|
4347
|
+
metrics: options.analyticsConfig?.metrics || ["count", "sum", "avg", "min", "max"],
|
|
4348
|
+
rollupStrategy: options.analyticsConfig?.rollupStrategy || "incremental",
|
|
4349
|
+
// 'incremental' or 'batch'
|
|
4350
|
+
retentionDays: options.analyticsConfig?.retentionDays || 365
|
|
4351
|
+
}
|
|
4343
4352
|
};
|
|
4344
4353
|
this.transactionResource = null;
|
|
4345
4354
|
this.targetResource = null;
|
|
4355
|
+
this.analyticsResource = null;
|
|
4346
4356
|
this.consolidationTimer = null;
|
|
4347
4357
|
this.gcTimer = null;
|
|
4348
4358
|
this.pendingTransactions = /* @__PURE__ */ new Map();
|
|
@@ -4429,6 +4439,9 @@ class EventualConsistencyPlugin extends Plugin {
|
|
|
4429
4439
|
throw new Error(`Failed to create lock resource: ${lockErr?.message}`);
|
|
4430
4440
|
}
|
|
4431
4441
|
this.lockResource = lockOk ? lockResource : this.database.resources[lockResourceName];
|
|
4442
|
+
if (this.config.enableAnalytics) {
|
|
4443
|
+
await this.createAnalyticsResource();
|
|
4444
|
+
}
|
|
4432
4445
|
this.addHelperMethods();
|
|
4433
4446
|
if (this.config.autoConsolidate) {
|
|
4434
4447
|
this.startConsolidationTimer();
|
|
@@ -4480,6 +4493,53 @@ class EventualConsistencyPlugin extends Plugin {
|
|
|
4480
4493
|
};
|
|
4481
4494
|
return partitions;
|
|
4482
4495
|
}
|
|
4496
|
+
async createAnalyticsResource() {
|
|
4497
|
+
const analyticsResourceName = `${this.config.resource}_analytics_${this.config.field}`;
|
|
4498
|
+
const [ok, err, analyticsResource] = await tryFn(
|
|
4499
|
+
() => this.database.createResource({
|
|
4500
|
+
name: analyticsResourceName,
|
|
4501
|
+
attributes: {
|
|
4502
|
+
id: "string|required",
|
|
4503
|
+
period: "string|required",
|
|
4504
|
+
// 'hour', 'day', 'month'
|
|
4505
|
+
cohort: "string|required",
|
|
4506
|
+
// ISO format: '2025-10-09T14', '2025-10-09', '2025-10'
|
|
4507
|
+
// Aggregated metrics
|
|
4508
|
+
transactionCount: "number|required",
|
|
4509
|
+
totalValue: "number|required",
|
|
4510
|
+
avgValue: "number|required",
|
|
4511
|
+
minValue: "number|required",
|
|
4512
|
+
maxValue: "number|required",
|
|
4513
|
+
// Operation breakdown
|
|
4514
|
+
operations: "object|optional",
|
|
4515
|
+
// { add: { count, sum }, sub: { count, sum }, set: { count, sum } }
|
|
4516
|
+
// Metadata
|
|
4517
|
+
recordCount: "number|required",
|
|
4518
|
+
// Distinct originalIds
|
|
4519
|
+
consolidatedAt: "string|required",
|
|
4520
|
+
updatedAt: "string|required"
|
|
4521
|
+
},
|
|
4522
|
+
behavior: "body-overflow",
|
|
4523
|
+
timestamps: false,
|
|
4524
|
+
partitions: {
|
|
4525
|
+
byPeriod: {
|
|
4526
|
+
fields: { period: "string" }
|
|
4527
|
+
},
|
|
4528
|
+
byCohort: {
|
|
4529
|
+
fields: { cohort: "string" }
|
|
4530
|
+
}
|
|
4531
|
+
}
|
|
4532
|
+
})
|
|
4533
|
+
);
|
|
4534
|
+
if (!ok && !this.database.resources[analyticsResourceName]) {
|
|
4535
|
+
console.warn(`[EventualConsistency] Failed to create analytics resource: ${err?.message}`);
|
|
4536
|
+
return;
|
|
4537
|
+
}
|
|
4538
|
+
this.analyticsResource = ok ? analyticsResource : this.database.resources[analyticsResourceName];
|
|
4539
|
+
if (this.config.verbose) {
|
|
4540
|
+
console.log(`[EventualConsistency] Analytics resource created: ${analyticsResourceName}`);
|
|
4541
|
+
}
|
|
4542
|
+
}
|
|
4483
4543
|
/**
|
|
4484
4544
|
* Auto-detect timezone from environment or system
|
|
4485
4545
|
* @private
|
|
@@ -4827,6 +4887,9 @@ class EventualConsistencyPlugin extends Plugin {
|
|
|
4827
4887
|
if (errors && errors.length > 0 && this.config.verbose) {
|
|
4828
4888
|
console.warn(`[EventualConsistency] ${errors.length} transactions failed to mark as applied`);
|
|
4829
4889
|
}
|
|
4890
|
+
if (this.config.enableAnalytics && transactionsToUpdate.length > 0) {
|
|
4891
|
+
await this.updateAnalytics(transactionsToUpdate);
|
|
4892
|
+
}
|
|
4830
4893
|
}
|
|
4831
4894
|
return consolidatedValue;
|
|
4832
4895
|
} finally {
|
|
@@ -5034,6 +5097,402 @@ class EventualConsistencyPlugin extends Plugin {
|
|
|
5034
5097
|
await tryFn(() => this.lockResource.delete(gcLockId));
|
|
5035
5098
|
}
|
|
5036
5099
|
}
|
|
5100
|
+
/**
|
|
5101
|
+
* Update analytics with consolidated transactions
|
|
5102
|
+
* @param {Array} transactions - Array of transactions that were just consolidated
|
|
5103
|
+
* @private
|
|
5104
|
+
*/
|
|
5105
|
+
async updateAnalytics(transactions) {
|
|
5106
|
+
if (!this.analyticsResource || transactions.length === 0) return;
|
|
5107
|
+
try {
|
|
5108
|
+
const byHour = this._groupByCohort(transactions, "cohortHour");
|
|
5109
|
+
for (const [cohort, txns] of Object.entries(byHour)) {
|
|
5110
|
+
await this._upsertAnalytics("hour", cohort, txns);
|
|
5111
|
+
}
|
|
5112
|
+
if (this.config.analyticsConfig.rollupStrategy === "incremental") {
|
|
5113
|
+
const uniqueHours = Object.keys(byHour);
|
|
5114
|
+
for (const cohortHour of uniqueHours) {
|
|
5115
|
+
await this._rollupAnalytics(cohortHour);
|
|
5116
|
+
}
|
|
5117
|
+
}
|
|
5118
|
+
} catch (error) {
|
|
5119
|
+
if (this.config.verbose) {
|
|
5120
|
+
console.warn(`[EventualConsistency] Analytics update error:`, error.message);
|
|
5121
|
+
}
|
|
5122
|
+
}
|
|
5123
|
+
}
|
|
5124
|
+
/**
|
|
5125
|
+
* Group transactions by cohort
|
|
5126
|
+
* @private
|
|
5127
|
+
*/
|
|
5128
|
+
_groupByCohort(transactions, cohortField) {
|
|
5129
|
+
const groups = {};
|
|
5130
|
+
for (const txn of transactions) {
|
|
5131
|
+
const cohort = txn[cohortField];
|
|
5132
|
+
if (!cohort) continue;
|
|
5133
|
+
if (!groups[cohort]) {
|
|
5134
|
+
groups[cohort] = [];
|
|
5135
|
+
}
|
|
5136
|
+
groups[cohort].push(txn);
|
|
5137
|
+
}
|
|
5138
|
+
return groups;
|
|
5139
|
+
}
|
|
5140
|
+
/**
|
|
5141
|
+
* Upsert analytics for a specific period and cohort
|
|
5142
|
+
* @private
|
|
5143
|
+
*/
|
|
5144
|
+
async _upsertAnalytics(period, cohort, transactions) {
|
|
5145
|
+
const id = `${period}-${cohort}`;
|
|
5146
|
+
const transactionCount = transactions.length;
|
|
5147
|
+
const signedValues = transactions.map((t) => {
|
|
5148
|
+
if (t.operation === "sub") return -t.value;
|
|
5149
|
+
return t.value;
|
|
5150
|
+
});
|
|
5151
|
+
const totalValue = signedValues.reduce((sum, v) => sum + v, 0);
|
|
5152
|
+
const avgValue = totalValue / transactionCount;
|
|
5153
|
+
const minValue = Math.min(...signedValues);
|
|
5154
|
+
const maxValue = Math.max(...signedValues);
|
|
5155
|
+
const operations = this._calculateOperationBreakdown(transactions);
|
|
5156
|
+
const recordCount = new Set(transactions.map((t) => t.originalId)).size;
|
|
5157
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5158
|
+
const [existingOk, existingErr, existing] = await tryFn(
|
|
5159
|
+
() => this.analyticsResource.get(id)
|
|
5160
|
+
);
|
|
5161
|
+
if (existingOk && existing) {
|
|
5162
|
+
const newTransactionCount = existing.transactionCount + transactionCount;
|
|
5163
|
+
const newTotalValue = existing.totalValue + totalValue;
|
|
5164
|
+
const newAvgValue = newTotalValue / newTransactionCount;
|
|
5165
|
+
const newMinValue = Math.min(existing.minValue, minValue);
|
|
5166
|
+
const newMaxValue = Math.max(existing.maxValue, maxValue);
|
|
5167
|
+
const newOperations = { ...existing.operations };
|
|
5168
|
+
for (const [op, stats] of Object.entries(operations)) {
|
|
5169
|
+
if (!newOperations[op]) {
|
|
5170
|
+
newOperations[op] = { count: 0, sum: 0 };
|
|
5171
|
+
}
|
|
5172
|
+
newOperations[op].count += stats.count;
|
|
5173
|
+
newOperations[op].sum += stats.sum;
|
|
5174
|
+
}
|
|
5175
|
+
const newRecordCount = Math.max(existing.recordCount, recordCount);
|
|
5176
|
+
await tryFn(
|
|
5177
|
+
() => this.analyticsResource.update(id, {
|
|
5178
|
+
transactionCount: newTransactionCount,
|
|
5179
|
+
totalValue: newTotalValue,
|
|
5180
|
+
avgValue: newAvgValue,
|
|
5181
|
+
minValue: newMinValue,
|
|
5182
|
+
maxValue: newMaxValue,
|
|
5183
|
+
operations: newOperations,
|
|
5184
|
+
recordCount: newRecordCount,
|
|
5185
|
+
updatedAt: now
|
|
5186
|
+
})
|
|
5187
|
+
);
|
|
5188
|
+
} else {
|
|
5189
|
+
await tryFn(
|
|
5190
|
+
() => this.analyticsResource.insert({
|
|
5191
|
+
id,
|
|
5192
|
+
period,
|
|
5193
|
+
cohort,
|
|
5194
|
+
transactionCount,
|
|
5195
|
+
totalValue,
|
|
5196
|
+
avgValue,
|
|
5197
|
+
minValue,
|
|
5198
|
+
maxValue,
|
|
5199
|
+
operations,
|
|
5200
|
+
recordCount,
|
|
5201
|
+
consolidatedAt: now,
|
|
5202
|
+
updatedAt: now
|
|
5203
|
+
})
|
|
5204
|
+
);
|
|
5205
|
+
}
|
|
5206
|
+
}
|
|
5207
|
+
/**
|
|
5208
|
+
* Calculate operation breakdown
|
|
5209
|
+
* @private
|
|
5210
|
+
*/
|
|
5211
|
+
_calculateOperationBreakdown(transactions) {
|
|
5212
|
+
const breakdown = {};
|
|
5213
|
+
for (const txn of transactions) {
|
|
5214
|
+
const op = txn.operation;
|
|
5215
|
+
if (!breakdown[op]) {
|
|
5216
|
+
breakdown[op] = { count: 0, sum: 0 };
|
|
5217
|
+
}
|
|
5218
|
+
breakdown[op].count++;
|
|
5219
|
+
const signedValue = op === "sub" ? -txn.value : txn.value;
|
|
5220
|
+
breakdown[op].sum += signedValue;
|
|
5221
|
+
}
|
|
5222
|
+
return breakdown;
|
|
5223
|
+
}
|
|
5224
|
+
/**
|
|
5225
|
+
* Roll up hourly analytics to daily and monthly
|
|
5226
|
+
* @private
|
|
5227
|
+
*/
|
|
5228
|
+
async _rollupAnalytics(cohortHour) {
|
|
5229
|
+
const cohortDate = cohortHour.substring(0, 10);
|
|
5230
|
+
const cohortMonth = cohortHour.substring(0, 7);
|
|
5231
|
+
await this._rollupPeriod("day", cohortDate, cohortDate);
|
|
5232
|
+
await this._rollupPeriod("month", cohortMonth, cohortMonth);
|
|
5233
|
+
}
|
|
5234
|
+
/**
|
|
5235
|
+
* Roll up analytics for a specific period
|
|
5236
|
+
* @private
|
|
5237
|
+
*/
|
|
5238
|
+
async _rollupPeriod(period, cohort, sourcePrefix) {
|
|
5239
|
+
const sourcePeriod = period === "day" ? "hour" : "day";
|
|
5240
|
+
const [ok, err, allAnalytics] = await tryFn(
|
|
5241
|
+
() => this.analyticsResource.list()
|
|
5242
|
+
);
|
|
5243
|
+
if (!ok || !allAnalytics) return;
|
|
5244
|
+
const sourceAnalytics = allAnalytics.filter(
|
|
5245
|
+
(a) => a.period === sourcePeriod && a.cohort.startsWith(sourcePrefix)
|
|
5246
|
+
);
|
|
5247
|
+
if (sourceAnalytics.length === 0) return;
|
|
5248
|
+
const transactionCount = sourceAnalytics.reduce((sum, a) => sum + a.transactionCount, 0);
|
|
5249
|
+
const totalValue = sourceAnalytics.reduce((sum, a) => sum + a.totalValue, 0);
|
|
5250
|
+
const avgValue = totalValue / transactionCount;
|
|
5251
|
+
const minValue = Math.min(...sourceAnalytics.map((a) => a.minValue));
|
|
5252
|
+
const maxValue = Math.max(...sourceAnalytics.map((a) => a.maxValue));
|
|
5253
|
+
const operations = {};
|
|
5254
|
+
for (const analytics of sourceAnalytics) {
|
|
5255
|
+
for (const [op, stats] of Object.entries(analytics.operations || {})) {
|
|
5256
|
+
if (!operations[op]) {
|
|
5257
|
+
operations[op] = { count: 0, sum: 0 };
|
|
5258
|
+
}
|
|
5259
|
+
operations[op].count += stats.count;
|
|
5260
|
+
operations[op].sum += stats.sum;
|
|
5261
|
+
}
|
|
5262
|
+
}
|
|
5263
|
+
const recordCount = Math.max(...sourceAnalytics.map((a) => a.recordCount));
|
|
5264
|
+
const id = `${period}-${cohort}`;
|
|
5265
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5266
|
+
const [existingOk, existingErr, existing] = await tryFn(
|
|
5267
|
+
() => this.analyticsResource.get(id)
|
|
5268
|
+
);
|
|
5269
|
+
if (existingOk && existing) {
|
|
5270
|
+
await tryFn(
|
|
5271
|
+
() => this.analyticsResource.update(id, {
|
|
5272
|
+
transactionCount,
|
|
5273
|
+
totalValue,
|
|
5274
|
+
avgValue,
|
|
5275
|
+
minValue,
|
|
5276
|
+
maxValue,
|
|
5277
|
+
operations,
|
|
5278
|
+
recordCount,
|
|
5279
|
+
updatedAt: now
|
|
5280
|
+
})
|
|
5281
|
+
);
|
|
5282
|
+
} else {
|
|
5283
|
+
await tryFn(
|
|
5284
|
+
() => this.analyticsResource.insert({
|
|
5285
|
+
id,
|
|
5286
|
+
period,
|
|
5287
|
+
cohort,
|
|
5288
|
+
transactionCount,
|
|
5289
|
+
totalValue,
|
|
5290
|
+
avgValue,
|
|
5291
|
+
minValue,
|
|
5292
|
+
maxValue,
|
|
5293
|
+
operations,
|
|
5294
|
+
recordCount,
|
|
5295
|
+
consolidatedAt: now,
|
|
5296
|
+
updatedAt: now
|
|
5297
|
+
})
|
|
5298
|
+
);
|
|
5299
|
+
}
|
|
5300
|
+
}
|
|
5301
|
+
/**
|
|
5302
|
+
* Get analytics for a specific period
|
|
5303
|
+
* @param {string} resourceName - Resource name
|
|
5304
|
+
* @param {string} field - Field name
|
|
5305
|
+
* @param {Object} options - Query options
|
|
5306
|
+
* @returns {Promise<Array>} Analytics data
|
|
5307
|
+
*/
|
|
5308
|
+
async getAnalytics(resourceName, field, options = {}) {
|
|
5309
|
+
if (!this.analyticsResource) {
|
|
5310
|
+
throw new Error("Analytics not enabled for this plugin");
|
|
5311
|
+
}
|
|
5312
|
+
const { period = "day", date, startDate, endDate, month, year, breakdown = false } = options;
|
|
5313
|
+
const [ok, err, allAnalytics] = await tryFn(
|
|
5314
|
+
() => this.analyticsResource.list()
|
|
5315
|
+
);
|
|
5316
|
+
if (!ok || !allAnalytics) {
|
|
5317
|
+
return [];
|
|
5318
|
+
}
|
|
5319
|
+
let filtered = allAnalytics.filter((a) => a.period === period);
|
|
5320
|
+
if (date) {
|
|
5321
|
+
if (period === "hour") {
|
|
5322
|
+
filtered = filtered.filter((a) => a.cohort.startsWith(date));
|
|
5323
|
+
} else {
|
|
5324
|
+
filtered = filtered.filter((a) => a.cohort === date);
|
|
5325
|
+
}
|
|
5326
|
+
} else if (startDate && endDate) {
|
|
5327
|
+
filtered = filtered.filter((a) => a.cohort >= startDate && a.cohort <= endDate);
|
|
5328
|
+
} else if (month) {
|
|
5329
|
+
filtered = filtered.filter((a) => a.cohort.startsWith(month));
|
|
5330
|
+
} else if (year) {
|
|
5331
|
+
filtered = filtered.filter((a) => a.cohort.startsWith(String(year)));
|
|
5332
|
+
}
|
|
5333
|
+
filtered.sort((a, b) => a.cohort.localeCompare(b.cohort));
|
|
5334
|
+
if (breakdown === "operations") {
|
|
5335
|
+
return filtered.map((a) => ({
|
|
5336
|
+
cohort: a.cohort,
|
|
5337
|
+
...a.operations
|
|
5338
|
+
}));
|
|
5339
|
+
}
|
|
5340
|
+
return filtered.map((a) => ({
|
|
5341
|
+
cohort: a.cohort,
|
|
5342
|
+
count: a.transactionCount,
|
|
5343
|
+
sum: a.totalValue,
|
|
5344
|
+
avg: a.avgValue,
|
|
5345
|
+
min: a.minValue,
|
|
5346
|
+
max: a.maxValue,
|
|
5347
|
+
operations: a.operations,
|
|
5348
|
+
recordCount: a.recordCount
|
|
5349
|
+
}));
|
|
5350
|
+
}
|
|
5351
|
+
/**
|
|
5352
|
+
* Get analytics for entire month, broken down by days
|
|
5353
|
+
* @param {string} resourceName - Resource name
|
|
5354
|
+
* @param {string} field - Field name
|
|
5355
|
+
* @param {string} month - Month in YYYY-MM format
|
|
5356
|
+
* @returns {Promise<Array>} Daily analytics for the month
|
|
5357
|
+
*/
|
|
5358
|
+
async getMonthByDay(resourceName, field, month) {
|
|
5359
|
+
const year = parseInt(month.substring(0, 4));
|
|
5360
|
+
const monthNum = parseInt(month.substring(5, 7));
|
|
5361
|
+
const firstDay = new Date(year, monthNum - 1, 1);
|
|
5362
|
+
const lastDay = new Date(year, monthNum, 0);
|
|
5363
|
+
const startDate = firstDay.toISOString().substring(0, 10);
|
|
5364
|
+
const endDate = lastDay.toISOString().substring(0, 10);
|
|
5365
|
+
return await this.getAnalytics(resourceName, field, {
|
|
5366
|
+
period: "day",
|
|
5367
|
+
startDate,
|
|
5368
|
+
endDate
|
|
5369
|
+
});
|
|
5370
|
+
}
|
|
5371
|
+
/**
|
|
5372
|
+
* Get analytics for entire day, broken down by hours
|
|
5373
|
+
* @param {string} resourceName - Resource name
|
|
5374
|
+
* @param {string} field - Field name
|
|
5375
|
+
* @param {string} date - Date in YYYY-MM-DD format
|
|
5376
|
+
* @returns {Promise<Array>} Hourly analytics for the day
|
|
5377
|
+
*/
|
|
5378
|
+
async getDayByHour(resourceName, field, date) {
|
|
5379
|
+
return await this.getAnalytics(resourceName, field, {
|
|
5380
|
+
period: "hour",
|
|
5381
|
+
date
|
|
5382
|
+
});
|
|
5383
|
+
}
|
|
5384
|
+
/**
|
|
5385
|
+
* Get analytics for last N days, broken down by days
|
|
5386
|
+
* @param {string} resourceName - Resource name
|
|
5387
|
+
* @param {string} field - Field name
|
|
5388
|
+
* @param {number} days - Number of days to look back (default: 7)
|
|
5389
|
+
* @returns {Promise<Array>} Daily analytics
|
|
5390
|
+
*/
|
|
5391
|
+
async getLastNDays(resourceName, field, days = 7) {
|
|
5392
|
+
const dates = Array.from({ length: days }, (_, i) => {
|
|
5393
|
+
const date = /* @__PURE__ */ new Date();
|
|
5394
|
+
date.setDate(date.getDate() - i);
|
|
5395
|
+
return date.toISOString().substring(0, 10);
|
|
5396
|
+
}).reverse();
|
|
5397
|
+
return await this.getAnalytics(resourceName, field, {
|
|
5398
|
+
period: "day",
|
|
5399
|
+
startDate: dates[0],
|
|
5400
|
+
endDate: dates[dates.length - 1]
|
|
5401
|
+
});
|
|
5402
|
+
}
|
|
5403
|
+
/**
|
|
5404
|
+
* Get analytics for entire year, broken down by months
|
|
5405
|
+
* @param {string} resourceName - Resource name
|
|
5406
|
+
* @param {string} field - Field name
|
|
5407
|
+
* @param {number} year - Year (e.g., 2025)
|
|
5408
|
+
* @returns {Promise<Array>} Monthly analytics for the year
|
|
5409
|
+
*/
|
|
5410
|
+
async getYearByMonth(resourceName, field, year) {
|
|
5411
|
+
return await this.getAnalytics(resourceName, field, {
|
|
5412
|
+
period: "month",
|
|
5413
|
+
year
|
|
5414
|
+
});
|
|
5415
|
+
}
|
|
5416
|
+
/**
|
|
5417
|
+
* Get analytics for entire month, broken down by hours
|
|
5418
|
+
* @param {string} resourceName - Resource name
|
|
5419
|
+
* @param {string} field - Field name
|
|
5420
|
+
* @param {string} month - Month in YYYY-MM format (or 'last' for previous month)
|
|
5421
|
+
* @returns {Promise<Array>} Hourly analytics for the month (up to 24*31=744 records)
|
|
5422
|
+
*/
|
|
5423
|
+
async getMonthByHour(resourceName, field, month) {
|
|
5424
|
+
let year, monthNum;
|
|
5425
|
+
if (month === "last") {
|
|
5426
|
+
const now = /* @__PURE__ */ new Date();
|
|
5427
|
+
now.setMonth(now.getMonth() - 1);
|
|
5428
|
+
year = now.getFullYear();
|
|
5429
|
+
monthNum = now.getMonth() + 1;
|
|
5430
|
+
} else {
|
|
5431
|
+
year = parseInt(month.substring(0, 4));
|
|
5432
|
+
monthNum = parseInt(month.substring(5, 7));
|
|
5433
|
+
}
|
|
5434
|
+
const firstDay = new Date(year, monthNum - 1, 1);
|
|
5435
|
+
const lastDay = new Date(year, monthNum, 0);
|
|
5436
|
+
const startDate = firstDay.toISOString().substring(0, 10);
|
|
5437
|
+
const endDate = lastDay.toISOString().substring(0, 10);
|
|
5438
|
+
return await this.getAnalytics(resourceName, field, {
|
|
5439
|
+
period: "hour",
|
|
5440
|
+
startDate,
|
|
5441
|
+
endDate
|
|
5442
|
+
});
|
|
5443
|
+
}
|
|
5444
|
+
/**
|
|
5445
|
+
* Get top records by volume
|
|
5446
|
+
* @param {string} resourceName - Resource name
|
|
5447
|
+
* @param {string} field - Field name
|
|
5448
|
+
* @param {Object} options - Query options
|
|
5449
|
+
* @returns {Promise<Array>} Top records
|
|
5450
|
+
*/
|
|
5451
|
+
async getTopRecords(resourceName, field, options = {}) {
|
|
5452
|
+
if (!this.transactionResource) {
|
|
5453
|
+
throw new Error("Transaction resource not initialized");
|
|
5454
|
+
}
|
|
5455
|
+
const { period = "day", date, metric = "transactionCount", limit = 10 } = options;
|
|
5456
|
+
const [ok, err, transactions] = await tryFn(
|
|
5457
|
+
() => this.transactionResource.list()
|
|
5458
|
+
);
|
|
5459
|
+
if (!ok || !transactions) {
|
|
5460
|
+
return [];
|
|
5461
|
+
}
|
|
5462
|
+
let filtered = transactions;
|
|
5463
|
+
if (date) {
|
|
5464
|
+
if (period === "hour") {
|
|
5465
|
+
filtered = transactions.filter((t) => t.cohortHour && t.cohortHour.startsWith(date));
|
|
5466
|
+
} else if (period === "day") {
|
|
5467
|
+
filtered = transactions.filter((t) => t.cohortDate === date);
|
|
5468
|
+
} else if (period === "month") {
|
|
5469
|
+
filtered = transactions.filter((t) => t.cohortMonth && t.cohortMonth.startsWith(date));
|
|
5470
|
+
}
|
|
5471
|
+
}
|
|
5472
|
+
const byRecord = {};
|
|
5473
|
+
for (const txn of filtered) {
|
|
5474
|
+
const recordId = txn.originalId;
|
|
5475
|
+
if (!byRecord[recordId]) {
|
|
5476
|
+
byRecord[recordId] = { count: 0, sum: 0 };
|
|
5477
|
+
}
|
|
5478
|
+
byRecord[recordId].count++;
|
|
5479
|
+
byRecord[recordId].sum += txn.value;
|
|
5480
|
+
}
|
|
5481
|
+
const records = Object.entries(byRecord).map(([recordId, stats]) => ({
|
|
5482
|
+
recordId,
|
|
5483
|
+
count: stats.count,
|
|
5484
|
+
sum: stats.sum
|
|
5485
|
+
}));
|
|
5486
|
+
records.sort((a, b) => {
|
|
5487
|
+
if (metric === "transactionCount") {
|
|
5488
|
+
return b.count - a.count;
|
|
5489
|
+
} else if (metric === "totalValue") {
|
|
5490
|
+
return b.sum - a.sum;
|
|
5491
|
+
}
|
|
5492
|
+
return 0;
|
|
5493
|
+
});
|
|
5494
|
+
return records.slice(0, limit);
|
|
5495
|
+
}
|
|
5037
5496
|
}
|
|
5038
5497
|
|
|
5039
5498
|
class FullTextPlugin extends Plugin {
|
|
@@ -11101,7 +11560,7 @@ class Database extends EventEmitter {
|
|
|
11101
11560
|
this.id = idGenerator(7);
|
|
11102
11561
|
this.version = "1";
|
|
11103
11562
|
this.s3dbVersion = (() => {
|
|
11104
|
-
const [ok, err, version] = tryFn(() => true ? "10.0.
|
|
11563
|
+
const [ok, err, version] = tryFn(() => true ? "10.0.4" : "latest");
|
|
11105
11564
|
return ok ? version : "latest";
|
|
11106
11565
|
})();
|
|
11107
11566
|
this.resources = {};
|