s3db.js 11.1.0 → 11.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/s3db.es.js CHANGED
@@ -5399,6 +5399,38 @@ function groupByCohort(transactions, cohortField) {
5399
5399
  }
5400
5400
  return groups;
5401
5401
  }
5402
+ function ensureCohortHour(transaction, timezone = "UTC", verbose = false) {
5403
+ if (transaction.cohortHour) {
5404
+ return transaction;
5405
+ }
5406
+ if (transaction.timestamp) {
5407
+ const date = new Date(transaction.timestamp);
5408
+ const cohortInfo = getCohortInfo(date, timezone, verbose);
5409
+ if (verbose) {
5410
+ console.log(
5411
+ `[EventualConsistency] Transaction ${transaction.id} missing cohortHour, calculated from timestamp: ${cohortInfo.hour}`
5412
+ );
5413
+ }
5414
+ transaction.cohortHour = cohortInfo.hour;
5415
+ if (!transaction.cohortWeek) {
5416
+ transaction.cohortWeek = cohortInfo.week;
5417
+ }
5418
+ if (!transaction.cohortMonth) {
5419
+ transaction.cohortMonth = cohortInfo.month;
5420
+ }
5421
+ } else if (verbose) {
5422
+ console.warn(
5423
+ `[EventualConsistency] Transaction ${transaction.id} missing both cohortHour and timestamp, cannot calculate cohort`
5424
+ );
5425
+ }
5426
+ return transaction;
5427
+ }
5428
+ function ensureCohortHours(transactions, timezone = "UTC", verbose = false) {
5429
+ if (!transactions || !Array.isArray(transactions)) {
5430
+ return transactions;
5431
+ }
5432
+ return transactions.map((txn) => ensureCohortHour(txn, timezone, verbose));
5433
+ }
5402
5434
 
5403
5435
  function createPartitionConfig() {
5404
5436
  const partitions = {
@@ -6523,7 +6555,10 @@ async function getAnalytics(resourceName, field, options, fieldHandlers) {
6523
6555
  if (!handler.analyticsResource) {
6524
6556
  throw new Error("Analytics not enabled for this plugin");
6525
6557
  }
6526
- const { period = "day", date, startDate, endDate, month, year, breakdown = false } = options;
6558
+ const { period = "day", date, startDate, endDate, month, year, breakdown = false, recordId } = options;
6559
+ if (recordId) {
6560
+ return await getAnalyticsForRecord(resourceName, field, recordId, options, handler);
6561
+ }
6527
6562
  const [ok, err, allAnalytics] = await tryFn(
6528
6563
  () => handler.analyticsResource.list()
6529
6564
  );
@@ -6562,6 +6597,105 @@ async function getAnalytics(resourceName, field, options, fieldHandlers) {
6562
6597
  recordCount: a.recordCount
6563
6598
  }));
6564
6599
  }
6600
+ async function getAnalyticsForRecord(resourceName, field, recordId, options, handler) {
6601
+ const { period = "day", date, startDate, endDate, month, year } = options;
6602
+ const [okTrue, errTrue, appliedTransactions] = await tryFn(
6603
+ () => handler.transactionResource.query({
6604
+ originalId: recordId,
6605
+ applied: true
6606
+ })
6607
+ );
6608
+ const [okFalse, errFalse, pendingTransactions] = await tryFn(
6609
+ () => handler.transactionResource.query({
6610
+ originalId: recordId,
6611
+ applied: false
6612
+ })
6613
+ );
6614
+ let allTransactions = [
6615
+ ...okTrue && appliedTransactions ? appliedTransactions : [],
6616
+ ...okFalse && pendingTransactions ? pendingTransactions : []
6617
+ ];
6618
+ if (allTransactions.length === 0) {
6619
+ return [];
6620
+ }
6621
+ allTransactions = ensureCohortHours(allTransactions, handler.config?.cohort?.timezone || "UTC", false);
6622
+ let filtered = allTransactions;
6623
+ if (date) {
6624
+ if (period === "hour") {
6625
+ filtered = filtered.filter((t) => t.cohortHour && t.cohortHour.startsWith(date));
6626
+ } else if (period === "day") {
6627
+ filtered = filtered.filter((t) => t.cohortDate === date);
6628
+ } else if (period === "month") {
6629
+ filtered = filtered.filter((t) => t.cohortMonth && t.cohortMonth.startsWith(date));
6630
+ }
6631
+ } else if (startDate && endDate) {
6632
+ if (period === "hour") {
6633
+ filtered = filtered.filter((t) => t.cohortHour && t.cohortHour >= startDate && t.cohortHour <= endDate);
6634
+ } else if (period === "day") {
6635
+ filtered = filtered.filter((t) => t.cohortDate && t.cohortDate >= startDate && t.cohortDate <= endDate);
6636
+ } else if (period === "month") {
6637
+ filtered = filtered.filter((t) => t.cohortMonth && t.cohortMonth >= startDate && t.cohortMonth <= endDate);
6638
+ }
6639
+ } else if (month) {
6640
+ if (period === "hour") {
6641
+ filtered = filtered.filter((t) => t.cohortHour && t.cohortHour.startsWith(month));
6642
+ } else if (period === "day") {
6643
+ filtered = filtered.filter((t) => t.cohortDate && t.cohortDate.startsWith(month));
6644
+ }
6645
+ } else if (year) {
6646
+ if (period === "hour") {
6647
+ filtered = filtered.filter((t) => t.cohortHour && t.cohortHour.startsWith(String(year)));
6648
+ } else if (period === "day") {
6649
+ filtered = filtered.filter((t) => t.cohortDate && t.cohortDate.startsWith(String(year)));
6650
+ } else if (period === "month") {
6651
+ filtered = filtered.filter((t) => t.cohortMonth && t.cohortMonth.startsWith(String(year)));
6652
+ }
6653
+ }
6654
+ const cohortField = period === "hour" ? "cohortHour" : period === "day" ? "cohortDate" : "cohortMonth";
6655
+ const aggregated = aggregateTransactionsByCohort(filtered, cohortField);
6656
+ return aggregated;
6657
+ }
6658
+ function aggregateTransactionsByCohort(transactions, cohortField) {
6659
+ const groups = {};
6660
+ for (const txn of transactions) {
6661
+ const cohort = txn[cohortField];
6662
+ if (!cohort) continue;
6663
+ if (!groups[cohort]) {
6664
+ groups[cohort] = {
6665
+ cohort,
6666
+ count: 0,
6667
+ sum: 0,
6668
+ min: Infinity,
6669
+ max: -Infinity,
6670
+ recordCount: /* @__PURE__ */ new Set(),
6671
+ operations: {}
6672
+ };
6673
+ }
6674
+ const group = groups[cohort];
6675
+ const signedValue = txn.operation === "sub" ? -txn.value : txn.value;
6676
+ group.count++;
6677
+ group.sum += signedValue;
6678
+ group.min = Math.min(group.min, signedValue);
6679
+ group.max = Math.max(group.max, signedValue);
6680
+ group.recordCount.add(txn.originalId);
6681
+ const op = txn.operation;
6682
+ if (!group.operations[op]) {
6683
+ group.operations[op] = { count: 0, sum: 0 };
6684
+ }
6685
+ group.operations[op].count++;
6686
+ group.operations[op].sum += signedValue;
6687
+ }
6688
+ return Object.values(groups).map((g) => ({
6689
+ cohort: g.cohort,
6690
+ count: g.count,
6691
+ sum: g.sum,
6692
+ avg: g.sum / g.count,
6693
+ min: g.min === Infinity ? 0 : g.min,
6694
+ max: g.max === -Infinity ? 0 : g.max,
6695
+ recordCount: g.recordCount.size,
6696
+ operations: g.operations
6697
+ })).sort((a, b) => a.cohort.localeCompare(b.cohort));
6698
+ }
6565
6699
  async function getMonthByDay(resourceName, field, month, options, fieldHandlers) {
6566
6700
  const year = parseInt(month.substring(0, 4));
6567
6701
  const monthNum = parseInt(month.substring(5, 7));
@@ -6596,6 +6730,8 @@ async function getLastNDays(resourceName, field, days, options, fieldHandlers) {
6596
6730
  return date.toISOString().substring(0, 10);
6597
6731
  }).reverse();
6598
6732
  const data = await getAnalytics(resourceName, field, {
6733
+ ...options,
6734
+ // ✅ Include all options (recordId, etc.)
6599
6735
  period: "day",
6600
6736
  startDate: dates[0],
6601
6737
  endDate: dates[dates.length - 1]
@@ -6789,6 +6925,8 @@ async function getLastNHours(resourceName, field, hours = 24, options, fieldHand
6789
6925
  const startHour = hoursAgo.toISOString().substring(0, 13);
6790
6926
  const endHour = now.toISOString().substring(0, 13);
6791
6927
  const data = await getAnalytics(resourceName, field, {
6928
+ ...options,
6929
+ // ✅ Include all options (recordId, etc.)
6792
6930
  period: "hour",
6793
6931
  startDate: startHour,
6794
6932
  endDate: endHour
@@ -6836,6 +6974,8 @@ async function getLastNMonths(resourceName, field, months = 12, options, fieldHa
6836
6974
  const startDate = monthsAgo.toISOString().substring(0, 7);
6837
6975
  const endDate = now.toISOString().substring(0, 7);
6838
6976
  const data = await getAnalytics(resourceName, field, {
6977
+ ...options,
6978
+ // ✅ Include all options (recordId, etc.)
6839
6979
  period: "month",
6840
6980
  startDate,
6841
6981
  endDate
@@ -7028,7 +7168,8 @@ async function completeFieldSetup(handler, database, config, plugin) {
7028
7168
  operation: "string|required",
7029
7169
  timestamp: "string|required",
7030
7170
  cohortDate: "string|required",
7031
- cohortHour: "string|required",
7171
+ cohortHour: "string|optional",
7172
+ // ✅ FIX BUG #2: Changed from required to optional for migration compatibility
7032
7173
  cohortWeek: "string|optional",
7033
7174
  cohortMonth: "string|optional",
7034
7175
  source: "string|optional",
@@ -13606,7 +13747,7 @@ class Database extends EventEmitter {
13606
13747
  this.id = idGenerator(7);
13607
13748
  this.version = "1";
13608
13749
  this.s3dbVersion = (() => {
13609
- const [ok, err, version] = tryFn(() => true ? "11.1.0" : "latest");
13750
+ const [ok, err, version] = tryFn(() => true ? "11.2.0" : "latest");
13610
13751
  return ok ? version : "latest";
13611
13752
  })();
13612
13753
  this.resources = {};