s3db.js 11.0.3 → 11.0.5

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": "11.0.3",
3
+ "version": "11.0.5",
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",
@@ -265,9 +265,9 @@ async function rollupPeriod(period, cohort, sourcePrefix, analyticsResource, con
265
265
  if (period === 'day') {
266
266
  sourcePeriod = 'hour';
267
267
  } else if (period === 'week') {
268
- sourcePeriod = 'day';
268
+ sourcePeriod = 'day'; // Week aggregates from days
269
269
  } else if (period === 'month') {
270
- sourcePeriod = 'week'; // Aggregate weeks to month
270
+ sourcePeriod = 'day'; // Month aggregates from days AND hours (like week)
271
271
  } else {
272
272
  sourcePeriod = 'day'; // Fallback
273
273
  }
@@ -291,6 +291,8 @@ async function rollupPeriod(period, cohort, sourcePrefix, analyticsResource, con
291
291
  });
292
292
  } else {
293
293
  // For day and month, simple prefix matching works
294
+ // day: aggregates from hours (cohort '2025-10-09' matches '2025-10-09T14', '2025-10-09T15', etc)
295
+ // month: aggregates from days (cohort '2025-10' matches '2025-10-01', '2025-10-02', etc)
294
296
  sourceAnalytics = allAnalytics.filter(a =>
295
297
  a.period === sourcePeriod && a.cohort.startsWith(sourcePrefix)
296
298
  );
@@ -806,3 +808,242 @@ export async function getTopRecords(resourceName, field, options, fieldHandlers)
806
808
  // Limit results
807
809
  return records.slice(0, limit);
808
810
  }
811
+
812
+ /**
813
+ * Get analytics for entire year, broken down by days
814
+ *
815
+ * @param {string} resourceName - Resource name
816
+ * @param {string} field - Field name
817
+ * @param {number} year - Year (e.g., 2025)
818
+ * @param {Object} options - Options
819
+ * @param {Object} fieldHandlers - Field handlers map
820
+ * @returns {Promise<Array>} Daily analytics for the year (up to 365/366 records)
821
+ */
822
+ export async function getYearByDay(resourceName, field, year, options, fieldHandlers) {
823
+ const startDate = `${year}-01-01`;
824
+ const endDate = `${year}-12-31`;
825
+
826
+ const data = await getAnalytics(resourceName, field, {
827
+ period: 'day',
828
+ startDate,
829
+ endDate
830
+ }, fieldHandlers);
831
+
832
+ if (options.fillGaps) {
833
+ return fillGaps(data, 'day', startDate, endDate);
834
+ }
835
+
836
+ return data;
837
+ }
838
+
839
+ /**
840
+ * Get analytics for entire week, broken down by days
841
+ *
842
+ * @param {string} resourceName - Resource name
843
+ * @param {string} field - Field name
844
+ * @param {string} week - Week in YYYY-Www format (e.g., '2025-W42')
845
+ * @param {Object} options - Options
846
+ * @param {Object} fieldHandlers - Field handlers map
847
+ * @returns {Promise<Array>} Daily analytics for the week (7 records)
848
+ */
849
+ export async function getWeekByDay(resourceName, field, week, options, fieldHandlers) {
850
+ // week format: '2025-W42'
851
+ const year = parseInt(week.substring(0, 4));
852
+ const weekNum = parseInt(week.substring(6, 8));
853
+
854
+ // Calculate the first day of the week (Monday) using ISO 8601 - use UTC
855
+ const jan4 = new Date(Date.UTC(year, 0, 4));
856
+ const jan4Day = jan4.getUTCDay() || 7; // Sunday = 7
857
+ const firstMonday = new Date(Date.UTC(year, 0, 4 - jan4Day + 1));
858
+ const weekStart = new Date(firstMonday);
859
+ weekStart.setUTCDate(weekStart.getUTCDate() + (weekNum - 1) * 7);
860
+
861
+ // Get all 7 days of the week
862
+ const days = [];
863
+ for (let i = 0; i < 7; i++) {
864
+ const day = new Date(weekStart);
865
+ day.setUTCDate(weekStart.getUTCDate() + i);
866
+ days.push(day.toISOString().substring(0, 10));
867
+ }
868
+
869
+ const startDate = days[0];
870
+ const endDate = days[6];
871
+
872
+ const data = await getAnalytics(resourceName, field, {
873
+ period: 'day',
874
+ startDate,
875
+ endDate
876
+ }, fieldHandlers);
877
+
878
+ if (options.fillGaps) {
879
+ return fillGaps(data, 'day', startDate, endDate);
880
+ }
881
+
882
+ return data;
883
+ }
884
+
885
+ /**
886
+ * Get analytics for entire week, broken down by hours
887
+ *
888
+ * @param {string} resourceName - Resource name
889
+ * @param {string} field - Field name
890
+ * @param {string} week - Week in YYYY-Www format (e.g., '2025-W42')
891
+ * @param {Object} options - Options
892
+ * @param {Object} fieldHandlers - Field handlers map
893
+ * @returns {Promise<Array>} Hourly analytics for the week (168 records)
894
+ */
895
+ export async function getWeekByHour(resourceName, field, week, options, fieldHandlers) {
896
+ // week format: '2025-W42'
897
+ const year = parseInt(week.substring(0, 4));
898
+ const weekNum = parseInt(week.substring(6, 8));
899
+
900
+ // Calculate the first day of the week (Monday) using ISO 8601 - use UTC
901
+ const jan4 = new Date(Date.UTC(year, 0, 4));
902
+ const jan4Day = jan4.getUTCDay() || 7; // Sunday = 7
903
+ const firstMonday = new Date(Date.UTC(year, 0, 4 - jan4Day + 1));
904
+ const weekStart = new Date(firstMonday);
905
+ weekStart.setUTCDate(weekStart.getUTCDate() + (weekNum - 1) * 7);
906
+
907
+ // Get first and last day of week
908
+ const weekEnd = new Date(weekStart);
909
+ weekEnd.setUTCDate(weekEnd.getUTCDate() + 6);
910
+
911
+ const startDate = weekStart.toISOString().substring(0, 10);
912
+ const endDate = weekEnd.toISOString().substring(0, 10);
913
+
914
+ const data = await getAnalytics(resourceName, field, {
915
+ period: 'hour',
916
+ startDate,
917
+ endDate
918
+ }, fieldHandlers);
919
+
920
+ if (options.fillGaps) {
921
+ return fillGaps(data, 'hour', startDate, endDate);
922
+ }
923
+
924
+ return data;
925
+ }
926
+
927
+ /**
928
+ * Get analytics for last N hours
929
+ *
930
+ * @param {string} resourceName - Resource name
931
+ * @param {string} field - Field name
932
+ * @param {number} hours - Number of hours to look back (default: 24)
933
+ * @param {Object} options - Options
934
+ * @param {Object} fieldHandlers - Field handlers map
935
+ * @returns {Promise<Array>} Hourly analytics
936
+ */
937
+ export async function getLastNHours(resourceName, field, hours = 24, options, fieldHandlers) {
938
+ const now = new Date();
939
+ const hoursAgo = new Date(now);
940
+ hoursAgo.setHours(hoursAgo.getHours() - hours + 1); // +1 to include current hour
941
+
942
+ const startHour = hoursAgo.toISOString().substring(0, 13); // YYYY-MM-DDTHH
943
+ const endHour = now.toISOString().substring(0, 13);
944
+
945
+ const data = await getAnalytics(resourceName, field, {
946
+ period: 'hour',
947
+ startDate: startHour,
948
+ endDate: endHour
949
+ }, fieldHandlers);
950
+
951
+ if (options.fillGaps) {
952
+ // For hour-level gaps, we need to manually generate the exact hours requested
953
+ const result = [];
954
+ const emptyRecord = { count: 0, sum: 0, avg: 0, min: 0, max: 0, recordCount: 0 };
955
+ const dataMap = new Map(data.map(d => [d.cohort, d]));
956
+
957
+ const current = new Date(hoursAgo);
958
+ for (let i = 0; i < hours; i++) {
959
+ const cohort = current.toISOString().substring(0, 13);
960
+ result.push(dataMap.get(cohort) || { cohort, ...emptyRecord });
961
+ current.setHours(current.getHours() + 1);
962
+ }
963
+
964
+ return result;
965
+ }
966
+
967
+ return data;
968
+ }
969
+
970
+ /**
971
+ * Get analytics for last N weeks
972
+ *
973
+ * @param {string} resourceName - Resource name
974
+ * @param {string} field - Field name
975
+ * @param {number} weeks - Number of weeks to look back (default: 4)
976
+ * @param {Object} options - Options
977
+ * @param {Object} fieldHandlers - Field handlers map
978
+ * @returns {Promise<Array>} Weekly analytics
979
+ */
980
+ export async function getLastNWeeks(resourceName, field, weeks = 4, options, fieldHandlers) {
981
+ const now = new Date();
982
+ const weeksAgo = new Date(now);
983
+ weeksAgo.setDate(weeksAgo.getDate() - (weeks * 7));
984
+
985
+ // Get week cohorts for the range
986
+ const weekCohorts = [];
987
+ const currentDate = new Date(weeksAgo);
988
+ while (currentDate <= now) {
989
+ const weekCohort = getCohortWeekFromDate(currentDate);
990
+ if (!weekCohorts.includes(weekCohort)) {
991
+ weekCohorts.push(weekCohort);
992
+ }
993
+ currentDate.setDate(currentDate.getDate() + 7);
994
+ }
995
+
996
+ const startWeek = weekCohorts[0];
997
+ const endWeek = weekCohorts[weekCohorts.length - 1];
998
+
999
+ const data = await getAnalytics(resourceName, field, {
1000
+ period: 'week',
1001
+ startDate: startWeek,
1002
+ endDate: endWeek
1003
+ }, fieldHandlers);
1004
+
1005
+ return data;
1006
+ }
1007
+
1008
+ /**
1009
+ * Get analytics for last N months
1010
+ *
1011
+ * @param {string} resourceName - Resource name
1012
+ * @param {string} field - Field name
1013
+ * @param {number} months - Number of months to look back (default: 12)
1014
+ * @param {Object} options - Options
1015
+ * @param {Object} fieldHandlers - Field handlers map
1016
+ * @returns {Promise<Array>} Monthly analytics
1017
+ */
1018
+ export async function getLastNMonths(resourceName, field, months = 12, options, fieldHandlers) {
1019
+ const now = new Date();
1020
+ const monthsAgo = new Date(now);
1021
+ monthsAgo.setMonth(monthsAgo.getMonth() - months + 1); // +1 to include current month
1022
+
1023
+ const startDate = monthsAgo.toISOString().substring(0, 7); // YYYY-MM
1024
+ const endDate = now.toISOString().substring(0, 7);
1025
+
1026
+ const data = await getAnalytics(resourceName, field, {
1027
+ period: 'month',
1028
+ startDate,
1029
+ endDate
1030
+ }, fieldHandlers);
1031
+
1032
+ if (options.fillGaps) {
1033
+ // Generate exact months requested
1034
+ const result = [];
1035
+ const emptyRecord = { count: 0, sum: 0, avg: 0, min: 0, max: 0, recordCount: 0 };
1036
+ const dataMap = new Map(data.map(d => [d.cohort, d]));
1037
+
1038
+ const current = new Date(monthsAgo);
1039
+ for (let i = 0; i < months; i++) {
1040
+ const cohort = current.toISOString().substring(0, 7);
1041
+ result.push(dataMap.get(cohort) || { cohort, ...emptyRecord });
1042
+ current.setMonth(current.getMonth() + 1);
1043
+ }
1044
+
1045
+ return result;
1046
+ }
1047
+
1048
+ return data;
1049
+ }
@@ -17,7 +17,7 @@ import {
17
17
  runConsolidation
18
18
  } from "./consolidation.js";
19
19
  import { runGarbageCollection } from "./garbage-collection.js";
20
- import { updateAnalytics, getAnalytics, getMonthByDay, getDayByHour, getLastNDays, getYearByMonth, getYearByWeek, getMonthByWeek, getMonthByHour, getTopRecords } from "./analytics.js";
20
+ import { updateAnalytics, getAnalytics, getMonthByDay, getDayByHour, getLastNDays, getYearByMonth, getYearByWeek, getMonthByWeek, getMonthByHour, getTopRecords, getYearByDay, getWeekByDay, getWeekByHour, getLastNHours, getLastNWeeks, getLastNMonths } from "./analytics.js";
21
21
  import { onInstall, onStart, onStop, watchForResource, completeFieldSetup } from "./install.js";
22
22
 
23
23
  export class EventualConsistencyPlugin extends Plugin {
@@ -459,6 +459,78 @@ export class EventualConsistencyPlugin extends Plugin {
459
459
  async getTopRecords(resourceName, field, options = {}) {
460
460
  return await getTopRecords(resourceName, field, options, this.fieldHandlers);
461
461
  }
462
+
463
+ /**
464
+ * Get analytics for entire year, broken down by days
465
+ * @param {string} resourceName - Resource name
466
+ * @param {string} field - Field name
467
+ * @param {number} year - Year (e.g., 2025)
468
+ * @param {Object} options - Options
469
+ * @returns {Promise<Array>} Daily analytics for the year (up to 365/366 records)
470
+ */
471
+ async getYearByDay(resourceName, field, year, options = {}) {
472
+ return await getYearByDay(resourceName, field, year, options, this.fieldHandlers);
473
+ }
474
+
475
+ /**
476
+ * Get analytics for entire week, broken down by days
477
+ * @param {string} resourceName - Resource name
478
+ * @param {string} field - Field name
479
+ * @param {string} week - Week in YYYY-Www format (e.g., '2025-W42')
480
+ * @param {Object} options - Options
481
+ * @returns {Promise<Array>} Daily analytics for the week (7 records)
482
+ */
483
+ async getWeekByDay(resourceName, field, week, options = {}) {
484
+ return await getWeekByDay(resourceName, field, week, options, this.fieldHandlers);
485
+ }
486
+
487
+ /**
488
+ * Get analytics for entire week, broken down by hours
489
+ * @param {string} resourceName - Resource name
490
+ * @param {string} field - Field name
491
+ * @param {string} week - Week in YYYY-Www format (e.g., '2025-W42')
492
+ * @param {Object} options - Options
493
+ * @returns {Promise<Array>} Hourly analytics for the week (168 records)
494
+ */
495
+ async getWeekByHour(resourceName, field, week, options = {}) {
496
+ return await getWeekByHour(resourceName, field, week, options, this.fieldHandlers);
497
+ }
498
+
499
+ /**
500
+ * Get analytics for last N hours
501
+ * @param {string} resourceName - Resource name
502
+ * @param {string} field - Field name
503
+ * @param {number} hours - Number of hours to look back (default: 24)
504
+ * @param {Object} options - Options
505
+ * @returns {Promise<Array>} Hourly analytics
506
+ */
507
+ async getLastNHours(resourceName, field, hours = 24, options = {}) {
508
+ return await getLastNHours(resourceName, field, hours, options, this.fieldHandlers);
509
+ }
510
+
511
+ /**
512
+ * Get analytics for last N weeks
513
+ * @param {string} resourceName - Resource name
514
+ * @param {string} field - Field name
515
+ * @param {number} weeks - Number of weeks to look back (default: 4)
516
+ * @param {Object} options - Options
517
+ * @returns {Promise<Array>} Weekly analytics
518
+ */
519
+ async getLastNWeeks(resourceName, field, weeks = 4, options = {}) {
520
+ return await getLastNWeeks(resourceName, field, weeks, options, this.fieldHandlers);
521
+ }
522
+
523
+ /**
524
+ * Get analytics for last N months
525
+ * @param {string} resourceName - Resource name
526
+ * @param {string} field - Field name
527
+ * @param {number} months - Number of months to look back (default: 12)
528
+ * @param {Object} options - Options
529
+ * @returns {Promise<Array>} Monthly analytics
530
+ */
531
+ async getLastNMonths(resourceName, field, months = 12, options = {}) {
532
+ return await getLastNMonths(resourceName, field, months, options, this.fieldHandlers);
533
+ }
462
534
  }
463
535
 
464
536
  export default EventualConsistencyPlugin;