@justair/justair-library 7.2.0-alpha.e76a08c → 7.3.0-alpha.0df53d8

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.
@@ -1,5 +1,4 @@
1
1
  export * from "./pollutants.js";
2
2
  export * from "./alertLevels.js";
3
3
  export * from "./userNotifications.js";
4
- export * from "./timezones.js";
5
4
  //# sourceMappingURL=index.d.ts.map
@@ -279,7 +279,7 @@ export namespace PARAMETERS {
279
279
  let impacts_15: any;
280
280
  export { impacts_15 as impacts };
281
281
  }
282
- namespace RH {
282
+ namespace DP_AMB {
283
283
  let id_16: string;
284
284
  export { id_16 as id };
285
285
  let label_16: string;
@@ -297,7 +297,7 @@ export namespace PARAMETERS {
297
297
  let impacts_16: any;
298
298
  export { impacts_16 as impacts };
299
299
  }
300
- namespace T {
300
+ namespace RH {
301
301
  let id_17: string;
302
302
  export { id_17 as id };
303
303
  let label_17: string;
@@ -315,7 +315,7 @@ export namespace PARAMETERS {
315
315
  let impacts_17: any;
316
316
  export { impacts_17 as impacts };
317
317
  }
318
- namespace WD {
318
+ namespace RH_AMB {
319
319
  let id_18: string;
320
320
  export { id_18 as id };
321
321
  let label_18: string;
@@ -333,7 +333,7 @@ export namespace PARAMETERS {
333
333
  let impacts_18: any;
334
334
  export { impacts_18 as impacts };
335
335
  }
336
- namespace WS {
336
+ namespace T {
337
337
  let id_19: string;
338
338
  export { id_19 as id };
339
339
  let label_19: string;
@@ -351,7 +351,7 @@ export namespace PARAMETERS {
351
351
  let impacts_19: any;
352
352
  export { impacts_19 as impacts };
353
353
  }
354
- namespace GS {
354
+ namespace T_AMB {
355
355
  let id_20: string;
356
356
  export { id_20 as id };
357
357
  let label_20: string;
@@ -369,7 +369,7 @@ export namespace PARAMETERS {
369
369
  let impacts_20: any;
370
370
  export { impacts_20 as impacts };
371
371
  }
372
- namespace R {
372
+ namespace WD {
373
373
  let id_21: string;
374
374
  export { id_21 as id };
375
375
  let label_21: string;
@@ -387,7 +387,7 @@ export namespace PARAMETERS {
387
387
  let impacts_21: any;
388
388
  export { impacts_21 as impacts };
389
389
  }
390
- namespace SR {
390
+ namespace WS {
391
391
  let id_22: string;
392
392
  export { id_22 as id };
393
393
  let label_22: string;
@@ -405,7 +405,7 @@ export namespace PARAMETERS {
405
405
  let impacts_22: any;
406
406
  export { impacts_22 as impacts };
407
407
  }
408
- namespace P {
408
+ namespace GS {
409
409
  let id_23: string;
410
410
  export { id_23 as id };
411
411
  let label_23: string;
@@ -423,7 +423,7 @@ export namespace PARAMETERS {
423
423
  let impacts_23: any;
424
424
  export { impacts_23 as impacts };
425
425
  }
426
- namespace PAR {
426
+ namespace R {
427
427
  let id_24: string;
428
428
  export { id_24 as id };
429
429
  let label_24: string;
@@ -441,7 +441,7 @@ export namespace PARAMETERS {
441
441
  let impacts_24: any;
442
442
  export { impacts_24 as impacts };
443
443
  }
444
- namespace ARM {
444
+ namespace SR {
445
445
  let id_25: string;
446
446
  export { id_25 as id };
447
447
  let label_25: string;
@@ -459,7 +459,7 @@ export namespace PARAMETERS {
459
459
  let impacts_25: any;
460
460
  export { impacts_25 as impacts };
461
461
  }
462
- namespace ARW {
462
+ namespace P {
463
463
  let id_26: string;
464
464
  export { id_26 as id };
465
465
  let label_26: string;
@@ -477,7 +477,7 @@ export namespace PARAMETERS {
477
477
  let impacts_26: any;
478
478
  export { impacts_26 as impacts };
479
479
  }
480
- namespace ARD {
480
+ namespace PAR {
481
481
  let id_27: string;
482
482
  export { id_27 as id };
483
483
  let label_27: string;
@@ -495,7 +495,7 @@ export namespace PARAMETERS {
495
495
  let impacts_27: any;
496
496
  export { impacts_27 as impacts };
497
497
  }
498
- namespace ARH {
498
+ namespace ARM {
499
499
  let id_28: string;
500
500
  export { id_28 as id };
501
501
  let label_28: string;
@@ -513,9 +513,7 @@ export namespace PARAMETERS {
513
513
  let impacts_28: any;
514
514
  export { impacts_28 as impacts };
515
515
  }
516
- }
517
- export namespace HEAVY_METALS {
518
- export namespace AS {
516
+ namespace ARW {
519
517
  let id_29: string;
520
518
  export { id_29 as id };
521
519
  let label_29: string;
@@ -526,14 +524,14 @@ export namespace HEAVY_METALS {
526
524
  export { unit_29 as unit };
527
525
  let type_29: string;
528
526
  export { type_29 as type };
529
- let rfc_29: number;
527
+ let rfc_29: any;
530
528
  export { rfc_29 as rfc };
531
- let origin_29: string;
529
+ let origin_29: any;
532
530
  export { origin_29 as origin };
533
- let impacts_29: string;
531
+ let impacts_29: any;
534
532
  export { impacts_29 as impacts };
535
533
  }
536
- export namespace BA {
534
+ namespace ARD {
537
535
  let id_30: string;
538
536
  export { id_30 as id };
539
537
  let label_30: string;
@@ -546,12 +544,12 @@ export namespace HEAVY_METALS {
546
544
  export { type_30 as type };
547
545
  let rfc_30: any;
548
546
  export { rfc_30 as rfc };
549
- let origin_30: string;
547
+ let origin_30: any;
550
548
  export { origin_30 as origin };
551
- let impacts_30: string;
549
+ let impacts_30: any;
552
550
  export { impacts_30 as impacts };
553
551
  }
554
- export namespace C6H6 {
552
+ namespace ARH {
555
553
  let id_31: string;
556
554
  export { id_31 as id };
557
555
  let label_31: string;
@@ -562,14 +560,16 @@ export namespace HEAVY_METALS {
562
560
  export { unit_31 as unit };
563
561
  let type_31: string;
564
562
  export { type_31 as type };
565
- let rfc_31: number;
563
+ let rfc_31: any;
566
564
  export { rfc_31 as rfc };
567
- let origin_31: string;
565
+ let origin_31: any;
568
566
  export { origin_31 as origin };
569
- let impacts_31: string;
567
+ let impacts_31: any;
570
568
  export { impacts_31 as impacts };
571
569
  }
572
- export namespace BE {
570
+ }
571
+ export namespace HEAVY_METALS {
572
+ export namespace AS {
573
573
  let id_32: string;
574
574
  export { id_32 as id };
575
575
  let label_32: string;
@@ -587,7 +587,7 @@ export namespace HEAVY_METALS {
587
587
  let impacts_32: string;
588
588
  export { impacts_32 as impacts };
589
589
  }
590
- export namespace CD {
590
+ export namespace BA {
591
591
  let id_33: string;
592
592
  export { id_33 as id };
593
593
  let label_33: string;
@@ -598,14 +598,14 @@ export namespace HEAVY_METALS {
598
598
  export { unit_33 as unit };
599
599
  let type_33: string;
600
600
  export { type_33 as type };
601
- let rfc_33: number;
601
+ let rfc_33: any;
602
602
  export { rfc_33 as rfc };
603
603
  let origin_33: string;
604
604
  export { origin_33 as origin };
605
605
  let impacts_33: string;
606
606
  export { impacts_33 as impacts };
607
607
  }
608
- export namespace CR {
608
+ export namespace C6H6 {
609
609
  let id_34: string;
610
610
  export { id_34 as id };
611
611
  let label_34: string;
@@ -623,7 +623,7 @@ export namespace HEAVY_METALS {
623
623
  let impacts_34: string;
624
624
  export { impacts_34 as impacts };
625
625
  }
626
- export namespace CO_1 {
626
+ export namespace BE {
627
627
  let id_35: string;
628
628
  export { id_35 as id };
629
629
  let label_35: string;
@@ -634,15 +634,14 @@ export namespace HEAVY_METALS {
634
634
  export { unit_35 as unit };
635
635
  let type_35: string;
636
636
  export { type_35 as type };
637
- let rfc_35: any;
637
+ let rfc_35: number;
638
638
  export { rfc_35 as rfc };
639
639
  let origin_35: string;
640
640
  export { origin_35 as origin };
641
641
  let impacts_35: string;
642
642
  export { impacts_35 as impacts };
643
643
  }
644
- export { CO_1 as CO };
645
- export namespace CU {
644
+ export namespace CD {
646
645
  let id_36: string;
647
646
  export { id_36 as id };
648
647
  let label_36: string;
@@ -653,14 +652,14 @@ export namespace HEAVY_METALS {
653
652
  export { unit_36 as unit };
654
653
  let type_36: string;
655
654
  export { type_36 as type };
656
- let rfc_36: any;
655
+ let rfc_36: number;
657
656
  export { rfc_36 as rfc };
658
657
  let origin_36: string;
659
658
  export { origin_36 as origin };
660
659
  let impacts_36: string;
661
660
  export { impacts_36 as impacts };
662
661
  }
663
- export namespace FE {
662
+ export namespace CR {
664
663
  let id_37: string;
665
664
  export { id_37 as id };
666
665
  let label_37: string;
@@ -671,14 +670,14 @@ export namespace HEAVY_METALS {
671
670
  export { unit_37 as unit };
672
671
  let type_37: string;
673
672
  export { type_37 as type };
674
- let rfc_37: any;
673
+ let rfc_37: number;
675
674
  export { rfc_37 as rfc };
676
675
  let origin_37: string;
677
676
  export { origin_37 as origin };
678
677
  let impacts_37: string;
679
678
  export { impacts_37 as impacts };
680
679
  }
681
- export namespace PB {
680
+ export namespace CO_1 {
682
681
  let id_38: string;
683
682
  export { id_38 as id };
684
683
  let label_38: string;
@@ -689,14 +688,15 @@ export namespace HEAVY_METALS {
689
688
  export { unit_38 as unit };
690
689
  let type_38: string;
691
690
  export { type_38 as type };
692
- let rfc_38: number;
691
+ let rfc_38: any;
693
692
  export { rfc_38 as rfc };
694
693
  let origin_38: string;
695
694
  export { origin_38 as origin };
696
695
  let impacts_38: string;
697
696
  export { impacts_38 as impacts };
698
697
  }
699
- export namespace MN {
698
+ export { CO_1 as CO };
699
+ export namespace CU {
700
700
  let id_39: string;
701
701
  export { id_39 as id };
702
702
  let label_39: string;
@@ -707,14 +707,14 @@ export namespace HEAVY_METALS {
707
707
  export { unit_39 as unit };
708
708
  let type_39: string;
709
709
  export { type_39 as type };
710
- let rfc_39: number;
710
+ let rfc_39: any;
711
711
  export { rfc_39 as rfc };
712
712
  let origin_39: string;
713
713
  export { origin_39 as origin };
714
714
  let impacts_39: string;
715
715
  export { impacts_39 as impacts };
716
716
  }
717
- export namespace NI {
717
+ export namespace FE {
718
718
  let id_40: string;
719
719
  export { id_40 as id };
720
720
  let label_40: string;
@@ -732,7 +732,7 @@ export namespace HEAVY_METALS {
732
732
  let impacts_40: string;
733
733
  export { impacts_40 as impacts };
734
734
  }
735
- export namespace SE {
735
+ export namespace PB {
736
736
  let id_41: string;
737
737
  export { id_41 as id };
738
738
  let label_41: string;
@@ -743,14 +743,14 @@ export namespace HEAVY_METALS {
743
743
  export { unit_41 as unit };
744
744
  let type_41: string;
745
745
  export { type_41 as type };
746
- let rfc_41: any;
746
+ let rfc_41: number;
747
747
  export { rfc_41 as rfc };
748
748
  let origin_41: string;
749
749
  export { origin_41 as origin };
750
750
  let impacts_41: string;
751
751
  export { impacts_41 as impacts };
752
752
  }
753
- export namespace ZN {
753
+ export namespace MN {
754
754
  let id_42: string;
755
755
  export { id_42 as id };
756
756
  let label_42: string;
@@ -761,12 +761,66 @@ export namespace HEAVY_METALS {
761
761
  export { unit_42 as unit };
762
762
  let type_42: string;
763
763
  export { type_42 as type };
764
- let rfc_42: any;
764
+ let rfc_42: number;
765
765
  export { rfc_42 as rfc };
766
766
  let origin_42: string;
767
767
  export { origin_42 as origin };
768
768
  let impacts_42: string;
769
769
  export { impacts_42 as impacts };
770
770
  }
771
+ export namespace NI {
772
+ let id_43: string;
773
+ export { id_43 as id };
774
+ let label_43: string;
775
+ export { label_43 as label };
776
+ let name_43: string;
777
+ export { name_43 as name };
778
+ let unit_43: string;
779
+ export { unit_43 as unit };
780
+ let type_43: string;
781
+ export { type_43 as type };
782
+ let rfc_43: any;
783
+ export { rfc_43 as rfc };
784
+ let origin_43: string;
785
+ export { origin_43 as origin };
786
+ let impacts_43: string;
787
+ export { impacts_43 as impacts };
788
+ }
789
+ export namespace SE {
790
+ let id_44: string;
791
+ export { id_44 as id };
792
+ let label_44: string;
793
+ export { label_44 as label };
794
+ let name_44: string;
795
+ export { name_44 as name };
796
+ let unit_44: string;
797
+ export { unit_44 as unit };
798
+ let type_44: string;
799
+ export { type_44 as type };
800
+ let rfc_44: any;
801
+ export { rfc_44 as rfc };
802
+ let origin_44: string;
803
+ export { origin_44 as origin };
804
+ let impacts_44: string;
805
+ export { impacts_44 as impacts };
806
+ }
807
+ export namespace ZN {
808
+ let id_45: string;
809
+ export { id_45 as id };
810
+ let label_45: string;
811
+ export { label_45 as label };
812
+ let name_45: string;
813
+ export { name_45 as name };
814
+ let unit_45: string;
815
+ export { unit_45 as unit };
816
+ let type_45: string;
817
+ export { type_45 as type };
818
+ let rfc_45: any;
819
+ export { rfc_45 as rfc };
820
+ let origin_45: string;
821
+ export { origin_45 as origin };
822
+ let impacts_45: string;
823
+ export { impacts_45 as impacts };
824
+ }
771
825
  }
772
826
  //# sourceMappingURL=pollutants.d.ts.map
package/dist/index.d.ts CHANGED
@@ -86,21 +86,13 @@ import { AccountNotification } from "./models/userNotifications.js";
86
86
  import { NOTIFICATION_KINDS } from "./models/userNotifications.js";
87
87
  import { NOTIFICATION_CHANNELS } from "./models/userNotifications.js";
88
88
  import { DELIVERY_STATUSES } from "./models/userNotifications.js";
89
- import { dataDownloadSchema } from "./models/dataDownloads.js";
90
- import { DataDownloads } from "./models/dataDownloads.js";
91
- import { AqiDataDownload } from "./models/dataDownloads.js";
92
- import { SampleDataDownload } from "./models/dataDownloads.js";
93
- import { MAX_RANGE_DAYS } from "./utils/dataDownloadRange.js";
94
- import { DATA_DOWNLOAD_ERRORS } from "./utils/dataDownloadRange.js";
95
- import { validateDateOrder } from "./utils/dataDownloadRange.js";
96
- import { validateMaxDateRange } from "./utils/dataDownloadRange.js";
97
89
  import { PARAMETERS } from "./constants/pollutants.js";
98
90
  import { HEAVY_METALS } from "./constants/pollutants.js";
99
91
  import { ALERT_LEVEL_WEIGHTS } from "./constants/alertLevels.js";
100
92
  import { getAlertWeight } from "./constants/alertLevels.js";
101
93
  import { splitName } from "./utils/splitName.js";
102
94
  import { DEFAULT_QUIET_HOURS } from "./utils/quietHours.js";
103
- import { DEFAULT_TIMEZONE } from "./constants/timezones.js";
95
+ import { DEFAULT_TIMEZONE } from "./utils/quietHours.js";
104
96
  import { QUIET_HOURS_ERRORS } from "./utils/quietHours.js";
105
97
  import { isValidTimeString } from "./utils/quietHours.js";
106
98
  import { isValidIANATimezone } from "./utils/quietHours.js";
@@ -108,5 +100,5 @@ import { validateQuietHours } from "./utils/quietHours.js";
108
100
  import { getZoneMinutes } from "./utils/quietHours.js";
109
101
  import { isInQuietWindow } from "./utils/quietHours.js";
110
102
  import { resolveQuietHours } from "./utils/quietHours.js";
111
- export { Database, adminSchema, Admin, configurationsSchema, Configurations, measurementsSchema, Measurements, monitorRequestsSchema, MonitorRequests, monitorsSchema, Monitors, organizationsSchema, Organizations, usersSchema, Users, COOKIE_CONSENT_CATEGORIES, COOKIE_CONSENT_VALUES, validateCookieConsent, eventsSchema, Events, lightMonitorSchema, LightMonitors, monitorSuppliersSchema, MonitorSuppliers, contextsSchema, Contexts, parametersSchema, Parameters, announcementSchema, Announcements, jobsSchema, Jobs, apiKeySchema, ApiKey, UsageMetrics, usageMetricsSchema, Audit, auditSchema, EventsAudit, eventsAuditSchema, MonitorAudit, monitorAuditSchema, parametersEnum, deploymentTypesEnum, AlertsAudit, Alerts, alertsSchema, alertsAuditSchema, Features, featuresSchema, dataCompletenessSchema, DataCompleteness, networkMetricsSchema, NetworkMetrics, rateOfChangeSchema, RateOfChange, changeStreamCheckpointsSchema, ChangeStreamCheckpoints, sitesSchema, Sites, sampleSitesSchema, SampleSites, sampleSiteAuditSchema, SampleSiteAudit, sampleParametersEnum, samplesSchema, Samples, samplesAuditSchema, SamplesAudit, sampleParameterReferenceConcentrations, passwordResetTokenSchema, PasswordResetToken, monitorEpisodesSchema, MonitorEpisode, userNotificationsSchema, UserNotification, AqiAlertNotification, DeviceOfflineNotification, WeeklyDigestNotification, AccountNotification, NOTIFICATION_KINDS, NOTIFICATION_CHANNELS, DELIVERY_STATUSES, dataDownloadSchema, DataDownloads, AqiDataDownload, SampleDataDownload, MAX_RANGE_DAYS, DATA_DOWNLOAD_ERRORS, validateDateOrder, validateMaxDateRange, PARAMETERS, HEAVY_METALS, ALERT_LEVEL_WEIGHTS, getAlertWeight, splitName, DEFAULT_QUIET_HOURS, DEFAULT_TIMEZONE, QUIET_HOURS_ERRORS, isValidTimeString, isValidIANATimezone, validateQuietHours, getZoneMinutes, isInQuietWindow, resolveQuietHours };
103
+ export { Database, adminSchema, Admin, configurationsSchema, Configurations, measurementsSchema, Measurements, monitorRequestsSchema, MonitorRequests, monitorsSchema, Monitors, organizationsSchema, Organizations, usersSchema, Users, COOKIE_CONSENT_CATEGORIES, COOKIE_CONSENT_VALUES, validateCookieConsent, eventsSchema, Events, lightMonitorSchema, LightMonitors, monitorSuppliersSchema, MonitorSuppliers, contextsSchema, Contexts, parametersSchema, Parameters, announcementSchema, Announcements, jobsSchema, Jobs, apiKeySchema, ApiKey, UsageMetrics, usageMetricsSchema, Audit, auditSchema, EventsAudit, eventsAuditSchema, MonitorAudit, monitorAuditSchema, parametersEnum, deploymentTypesEnum, AlertsAudit, Alerts, alertsSchema, alertsAuditSchema, Features, featuresSchema, dataCompletenessSchema, DataCompleteness, networkMetricsSchema, NetworkMetrics, rateOfChangeSchema, RateOfChange, changeStreamCheckpointsSchema, ChangeStreamCheckpoints, sitesSchema, Sites, sampleSitesSchema, SampleSites, sampleSiteAuditSchema, SampleSiteAudit, sampleParametersEnum, samplesSchema, Samples, samplesAuditSchema, SamplesAudit, sampleParameterReferenceConcentrations, passwordResetTokenSchema, PasswordResetToken, monitorEpisodesSchema, MonitorEpisode, userNotificationsSchema, UserNotification, AqiAlertNotification, DeviceOfflineNotification, WeeklyDigestNotification, AccountNotification, NOTIFICATION_KINDS, NOTIFICATION_CHANNELS, DELIVERY_STATUSES, PARAMETERS, HEAVY_METALS, ALERT_LEVEL_WEIGHTS, getAlertWeight, splitName, DEFAULT_QUIET_HOURS, DEFAULT_TIMEZONE, QUIET_HOURS_ERRORS, isValidTimeString, isValidIANATimezone, validateQuietHours, getZoneMinutes, isInQuietWindow, resolveQuietHours };
112
104
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.js"],"names":[],"mappings":"AAmIA;;;iBAEC;yBAnEwB,oBAAoB;qBADxB,gBAAgB;4BAjEF,mBAAmB;sBAAnB,mBAAmB;qCAI/C,4BAA4B;+BAA5B,4BAA4B;mCAM5B,0BAA0B;6BAA1B,0BAA0B;sCAI1B,6BAA6B;gCAA7B,6BAA6B;+BAQ7B,sBAAsB;yBAAtB,sBAAsB;oCACsB,2BAA2B;8BAA3B,2BAA2B;4BAcvE,mBAAmB;sBAAnB,mBAAmB;0CAAnB,mBAAmB;sCAAnB,mBAAmB;sCAAnB,mBAAmB;6BAMnB,oBAAoB;uBAApB,oBAAoB;mCAnBuB,2BAA2B;8BAA3B,2BAA2B;uCAItE,8BAA8B;iCAA9B,8BAA8B;+BACI,sBAAsB;yBAAtB,sBAAsB;iCAClB,wBAAwB;2BAAxB,wBAAwB;mCAcnB,2BAA2B;8BAA3B,2BAA2B;2BAC5C,kBAAkB;qBAAlB,kBAAkB;6BACd,oBAAoB;uBAApB,oBAAoB;6BACR,0BAA0B;mCAA1B,0BAA0B;sBArCpE,0BAA0B;4BAA1B,0BAA0B;4BAiC1B,oBAAoB;kCAApB,oBAAoB;6BArBpB,sBAAsB;mCAAtB,sBAAsB;+BAAtB,sBAAsB;oCAAtB,sBAAsB;4BA+BtB,oBAAoB;uBAApB,oBAAoB;6BAApB,oBAAoB;kCAApB,oBAAoB;yBACc,sBAAsB;+BAAtB,sBAAsB;uCAIxD,8BAA8B;iCAA9B,8BAA8B;qCACgB,4BAA4B;+BAA5B,4BAA4B;mCAChC,0BAA0B;6BAA1B,0BAA0B;8CAIpE,qCAAqC;wCAArC,qCAAqC;4BAkCT,mBAAmB;sBAAnB,mBAAmB;kCAS/C,yBAAyB;4BAAzB,yBAAyB;sCAAzB,yBAAyB;gCAAzB,yBAAyB;qCAAzB,yBAAyB;8BASzB,qBAAqB;wBAArB,qBAAqB;mCAArB,qBAAqB;6BAArB,qBAAqB;uDAArB,qBAAqB;yCACiC,gCAAgC;mCAAhC,gCAAgC;sCA5CtF,6BAA6B;+BAA7B,6BAA6B;wCAW7B,+BAA+B;iCAA/B,+BAA+B;qCAA/B,+BAA+B;0CAA/B,+BAA+B;yCAA/B,+BAA+B;oCAA/B,+BAA+B;mCAA/B,+BAA+B;sCAA/B,+BAA+B;kCAA/B,+BAA+B;mCAuC/B,2BAA2B;8BAA3B,2BAA2B;gCAA3B,2BAA2B;mCAA3B,2BAA2B;+BAM3B,8BAA8B;qCAA9B,8BAA8B;kCAA9B,8BAA8B;qCAA9B,8BAA8B;2BA9DI,2BAA2B;6BAA3B,2BAA2B;oCAChB,4BAA4B;+BAA5B,4BAA4B;0BAiBtD,sBAAsB;oCAUzC,uBAAuB;iCA1BG,0BAA0B;mCA0BpD,uBAAuB;kCAAvB,uBAAuB;oCAAvB,uBAAuB;mCAAvB,uBAAuB;+BAAvB,uBAAuB;gCAAvB,uBAAuB;kCAAvB,uBAAuB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.js"],"names":[],"mappings":"AAuHA;;;iBAEC;yBAvDwB,oBAAoB;qBADxB,gBAAgB;4BAjEF,mBAAmB;sBAAnB,mBAAmB;qCAI/C,4BAA4B;+BAA5B,4BAA4B;mCAM5B,0BAA0B;6BAA1B,0BAA0B;sCAI1B,6BAA6B;gCAA7B,6BAA6B;+BAQ7B,sBAAsB;yBAAtB,sBAAsB;oCACsB,2BAA2B;8BAA3B,2BAA2B;4BAcvE,mBAAmB;sBAAnB,mBAAmB;0CAAnB,mBAAmB;sCAAnB,mBAAmB;sCAAnB,mBAAmB;6BAMnB,oBAAoB;uBAApB,oBAAoB;mCAnBuB,2BAA2B;8BAA3B,2BAA2B;uCAItE,8BAA8B;iCAA9B,8BAA8B;+BACI,sBAAsB;yBAAtB,sBAAsB;iCAClB,wBAAwB;2BAAxB,wBAAwB;mCAcnB,2BAA2B;8BAA3B,2BAA2B;2BAC5C,kBAAkB;qBAAlB,kBAAkB;6BACd,oBAAoB;uBAApB,oBAAoB;6BACR,0BAA0B;mCAA1B,0BAA0B;sBArCpE,0BAA0B;4BAA1B,0BAA0B;4BAiC1B,oBAAoB;kCAApB,oBAAoB;6BArBpB,sBAAsB;mCAAtB,sBAAsB;+BAAtB,sBAAsB;oCAAtB,sBAAsB;4BA+BtB,oBAAoB;uBAApB,oBAAoB;6BAApB,oBAAoB;kCAApB,oBAAoB;yBACc,sBAAsB;+BAAtB,sBAAsB;uCAIxD,8BAA8B;iCAA9B,8BAA8B;qCACgB,4BAA4B;+BAA5B,4BAA4B;mCAChC,0BAA0B;6BAA1B,0BAA0B;8CAIpE,qCAAqC;wCAArC,qCAAqC;4BAkCT,mBAAmB;sBAAnB,mBAAmB;kCAS/C,yBAAyB;4BAAzB,yBAAyB;sCAAzB,yBAAyB;gCAAzB,yBAAyB;qCAAzB,yBAAyB;8BASzB,qBAAqB;wBAArB,qBAAqB;mCAArB,qBAAqB;6BAArB,qBAAqB;uDAArB,qBAAqB;yCACiC,gCAAgC;mCAAhC,gCAAgC;sCA7CtF,6BAA6B;+BAA7B,6BAA6B;wCAW7B,+BAA+B;iCAA/B,+BAA+B;qCAA/B,+BAA+B;0CAA/B,+BAA+B;yCAA/B,+BAA+B;oCAA/B,+BAA+B;mCAA/B,+BAA+B;sCAA/B,+BAA+B;kCAA/B,+BAA+B;2BAhBG,2BAA2B;6BAA3B,2BAA2B;oCAChB,4BAA4B;+BAA5B,4BAA4B;0BAgBtD,sBAAsB;oCAWzC,uBAAuB;iCAAvB,uBAAuB;mCAAvB,uBAAuB;kCAAvB,uBAAuB;oCAAvB,uBAAuB;mCAAvB,uBAAuB;+BAAvB,uBAAuB;gCAAvB,uBAAuB;kCAAvB,uBAAuB"}
@@ -1 +1 @@
1
- {"version":3,"file":"monitors.d.ts","sourceRoot":"","sources":["../../src/models/monitors.js"],"names":[],"mappings":"AA4KA,iCAoJE;AAuIF,2BAA4D;AAlU5D,qCAiCE;AAGF,+BAAwE;AAtKxE,sCAAoD"}
1
+ {"version":3,"file":"monitors.d.ts","sourceRoot":"","sources":["../../src/models/monitors.js"],"names":[],"mappings":"AA4KA,iCAqJE;AAuIF,2BAA4D;AAnU5D,qCAiCE;AAGF,+BAAwE;AAtKxE,sCAAoD"}
@@ -1,8 +1,8 @@
1
- export { DEFAULT_TIMEZONE };
2
1
  export const DEFAULT_QUIET_HOURS: Readonly<{
3
2
  start: "22:00";
4
3
  end: "07:00";
5
4
  }>;
5
+ export const DEFAULT_TIMEZONE: "America/New_York";
6
6
  export const QUIET_HOURS_ERRORS: Readonly<{
7
7
  HALF_PAIR: "half_pair";
8
8
  FORMAT: "format";
@@ -30,5 +30,4 @@ export function resolveQuietHours(user: any): {
30
30
  timezone: string;
31
31
  warnings: string[];
32
32
  };
33
- import { DEFAULT_TIMEZONE } from "../constants/timezones.js";
34
33
  //# sourceMappingURL=quietHours.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"quietHours.d.ts","sourceRoot":"","sources":["../../src/utils/quietHours.js"],"names":[],"mappings":";AAwBA;;;GAGG;AAKH;;;;GAIG;AAuBI,uDACgD;AAEhD,4DAUN;AAOM;;;;;;;;;KAsCN;AASM,iEASN;AAKM,uEAaN;AAeM;;;;;;;EA6CN;iCAnMgC,2BAA2B"}
1
+ {"version":3,"file":"quietHours.d.ts","sourceRoot":"","sources":["../../src/utils/quietHours.js"],"names":[],"mappings":"AAkBA;;;GAGG;AAEH,kDAAmD;AAKnD;;;;GAIG;AAuBI,uDACgD;AAEhD,4DAUN;AAOM;;;;;;;;;KAsCN;AASM,iEASN;AAKM,uEAaN;AAeM;;;;;;;EA6CN"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@justair/justair-library",
3
- "version": "7.2.0-alpha.e76a08c",
3
+ "version": "7.3.0-alpha.0df53d8",
4
4
  "description": "JustAir Internal Library",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -10,4 +10,3 @@
10
10
  export * from "./pollutants.js";
11
11
  export * from "./alertLevels.js";
12
12
  export * from "./userNotifications.js";
13
- export * from "./timezones.js";
@@ -151,6 +151,10 @@ export const PARAMETERS = {
151
151
  origin: null,
152
152
  impacts: null,
153
153
  },
154
+ // JA-3058: T/RH are enclosure-internal (corrections + fog detection only); T_AMB/RH_AMB are true
155
+ // ambient and the only ones safe for public display or Heat Index. Never substitute one for the
156
+ // other. DP is already ambient today; DP_AMB is declared for symmetry with its role unresolved.
157
+ // Nothing consumes an _AMB id yet — see "Ambient vs internal met parameters" in CLAUDE.md first.
154
158
  DP: {
155
159
  id: "DP",
156
160
  label: "Dew Point",
@@ -161,6 +165,16 @@ export const PARAMETERS = {
161
165
  origin: null,
162
166
  impacts: null,
163
167
  },
168
+ DP_AMB: {
169
+ id: "DP_AMB",
170
+ label: "Ambient Dew Point",
171
+ name: "Ambient Dew Point (DP_AMB)",
172
+ unit: "°F",
173
+ type: "Weather",
174
+ rfc: null,
175
+ origin: null,
176
+ impacts: null,
177
+ },
164
178
  RH: {
165
179
  id: "RH",
166
180
  label: "Humidity",
@@ -171,6 +185,16 @@ export const PARAMETERS = {
171
185
  origin: null,
172
186
  impacts: null,
173
187
  },
188
+ RH_AMB: {
189
+ id: "RH_AMB",
190
+ label: "Ambient Humidity",
191
+ name: "Ambient Relative Humidity (RH_AMB)",
192
+ unit: "%",
193
+ type: "Weather",
194
+ rfc: null,
195
+ origin: null,
196
+ impacts: null,
197
+ },
174
198
  T: {
175
199
  id: "T",
176
200
  label: "Temperature",
@@ -181,6 +205,16 @@ export const PARAMETERS = {
181
205
  origin: null,
182
206
  impacts: null,
183
207
  },
208
+ T_AMB: {
209
+ id: "T_AMB",
210
+ label: "Ambient Temperature",
211
+ name: "Ambient Temperature (T_AMB)",
212
+ unit: "°F",
213
+ type: "Weather",
214
+ rfc: null,
215
+ origin: null,
216
+ impacts: null,
217
+ },
184
218
  WD: {
185
219
  id: "WD",
186
220
  label: "Wind Direction",
@@ -6,7 +6,6 @@
6
6
  import * as barrel from '../index.js';
7
7
  import { PARAMETERS, HEAVY_METALS } from '../pollutants.js';
8
8
  import { ALERT_LEVEL_WEIGHTS, getAlertWeight } from '../alertLevels.js';
9
- import { DEFAULT_TIMEZONE } from '../timezones.js';
10
9
 
11
10
  test('barrel re-exports PARAMETERS and HEAVY_METALS unchanged (identity, not copies)', () => {
12
11
  expect(barrel.PARAMETERS).toBe(PARAMETERS);
@@ -45,8 +44,3 @@ test('unknown and legacy statuses fall back to the Good weight (never alert, nev
45
44
  expect(getAlertWeight(null)).toBe(1);
46
45
  expect(getAlertWeight('')).toBe(1);
47
46
  });
48
-
49
- test('barrel exports DEFAULT_TIMEZONE (browser-safe, no Mongoose)', () => {
50
- expect(barrel.DEFAULT_TIMEZONE).toBe(DEFAULT_TIMEZONE);
51
- expect(DEFAULT_TIMEZONE).toBe('America/New_York');
52
- });
package/src/index.js CHANGED
@@ -67,7 +67,6 @@ import Database from "./config/db.js"; // Import the new Database class
67
67
  import CustomLogger from "./config/logger.js";
68
68
  import { PARAMETERS, HEAVY_METALS } from "./constants/pollutants.js";
69
69
  import { ALERT_LEVEL_WEIGHTS, getAlertWeight } from "./constants/alertLevels.js";
70
- import { DEFAULT_TIMEZONE } from "./constants/timezones.js";
71
70
  import {
72
71
  monitorEpisodesSchema,
73
72
  MonitorEpisode,
@@ -86,6 +85,7 @@ import {
86
85
  import { splitName } from "./utils/splitName.js";
87
86
  import {
88
87
  DEFAULT_QUIET_HOURS,
88
+ DEFAULT_TIMEZONE,
89
89
  QUIET_HOURS_ERRORS,
90
90
  isValidTimeString,
91
91
  isValidIANATimezone,
@@ -116,18 +116,6 @@ import {
116
116
  sampleParameterReferenceConcentrations,
117
117
  } from "./models/samples.js";
118
118
  import { passwordResetTokenSchema, PasswordResetToken } from "./models/passwordResetToken.js";
119
- import {
120
- dataDownloadSchema,
121
- DataDownloads,
122
- AqiDataDownload,
123
- SampleDataDownload,
124
- } from "./models/dataDownloads.js";
125
- import {
126
- MAX_RANGE_DAYS,
127
- DATA_DOWNLOAD_ERRORS,
128
- validateDateOrder,
129
- validateMaxDateRange,
130
- } from "./utils/dataDownloadRange.js";
131
119
 
132
120
  export function createLoggerInstance({ DATADOG_API_KEY, APPLICATION_NAME }) {
133
121
  return new CustomLogger({ DATADOG_API_KEY, APPLICATION_NAME });
@@ -222,15 +210,6 @@ export {
222
210
  NOTIFICATION_KINDS,
223
211
  NOTIFICATION_CHANNELS,
224
212
  DELIVERY_STATUSES,
225
- // Data downloads (JA-3080)
226
- dataDownloadSchema,
227
- DataDownloads,
228
- AqiDataDownload,
229
- SampleDataDownload,
230
- MAX_RANGE_DAYS,
231
- DATA_DOWNLOAD_ERRORS,
232
- validateDateOrder,
233
- validateMaxDateRange,
234
213
  // Pollutant constants
235
214
  PARAMETERS,
236
215
  HEAVY_METALS,
@@ -195,6 +195,7 @@ const monitorsSchema = mongoose.Schema(
195
195
  "atmotube",
196
196
  "hobolink",
197
197
  "dasberry",
198
+ "airThings",
198
199
  ],
199
200
  },
200
201
  monitorType: String,
@@ -16,17 +16,13 @@
16
16
  // Boundary rule: start is inclusive, end is exclusive — a 22:00–07:00 user
17
17
  // is suppressed at 22:00 sharp and receives alerts again at 07:00 sharp.
18
18
 
19
- import { DEFAULT_TIMEZONE } from "../constants/timezones.js";
20
-
21
- // Re-exported for existing backend imports — canonical definition now lives
22
- // in src/constants/timezones.js (Mongoose-free, browser-safe).
23
- export { DEFAULT_TIMEZONE };
24
-
25
19
  export const DEFAULT_QUIET_HOURS = Object.freeze({
26
20
  start: "22:00",
27
21
  end: "07:00",
28
22
  });
29
23
 
24
+ export const DEFAULT_TIMEZONE = "America/New_York";
25
+
30
26
  // Stable error codes for validateQuietHours. Consumers filter on `code`
31
27
  // (e.g. the users schema pre-validate hook re-attaches only cross-field
32
28
  // errors) — never on message wording, which is free to change.
@@ -1,2 +0,0 @@
1
- export const DEFAULT_TIMEZONE: "America/New_York";
2
- //# sourceMappingURL=timezones.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"timezones.d.ts","sourceRoot":"","sources":["../../src/constants/timezones.js"],"names":[],"mappings":"AAMA,kDAAmD"}
@@ -1,5 +0,0 @@
1
- export const dataDownloadSchema: any;
2
- export const DataDownloads: any;
3
- export const AqiDataDownload: any;
4
- export const SampleDataDownload: any;
5
- //# sourceMappingURL=dataDownloads.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"dataDownloads.d.ts","sourceRoot":"","sources":["../../src/models/dataDownloads.js"],"names":[],"mappings":"AAkBA,qCAkCE;AA+CF,gCAA0E;AAO1E,kCAEG;AAEH,qCAIE"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=dataDownloads.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"dataDownloads.test.d.ts","sourceRoot":"","sources":["../../../src/models/tests/dataDownloads.test.js"],"names":[],"mappings":""}
@@ -1,14 +0,0 @@
1
- export const MAX_RANGE_DAYS: 31;
2
- export const DATA_DOWNLOAD_ERRORS: Readonly<{
3
- END_BEFORE_START: "end_before_start";
4
- RANGE_TOO_LONG: "range_too_long";
5
- }>;
6
- export function validateDateOrder({ start, end }?: {}): {
7
- code: "end_before_start";
8
- message: string;
9
- }[];
10
- export function validateMaxDateRange({ start, end }?: {}): {
11
- code: "range_too_long";
12
- message: string;
13
- }[];
14
- //# sourceMappingURL=dataDownloadRange.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"dataDownloadRange.d.ts","sourceRoot":"","sources":["../../src/utils/dataDownloadRange.js"],"names":[],"mappings":"AAYA,gCAAiC;AASjC;;;GAGG;AA0BI;;;IAcN;AAMM;;;IAcN"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=dataDownloadRange.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"dataDownloadRange.test.d.ts","sourceRoot":"","sources":["../../../src/utils/tests/dataDownloadRange.test.js"],"names":[],"mappings":""}
@@ -1,7 +0,0 @@
1
- // Mongoose-free timezone defaults, so a browser-safe consumer (e.g.
2
- // JustAir-Client) can use the same default other domains store server-side.
3
- // Originally lived in utils/quietHours.js (JA-3054); moved here once a
4
- // second, unrelated domain (dataDownloads.js, JA-3080) needed it too — see
5
- // CLAUDE.md's "Note on constants placement".
6
-
7
- export const DEFAULT_TIMEZONE = "America/New_York";
@@ -1,117 +0,0 @@
1
- import mongoose from "mongoose";
2
- import { DEFAULT_TIMEZONE } from "../constants/timezones.js";
3
- import { isValidIANATimezone } from "../utils/quietHours.js";
4
- import { validateDateOrder, validateMaxDateRange } from "../utils/dataDownloadRange.js";
5
-
6
- const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
7
-
8
- // DataDownloads — one doc per data download request (JA-3080). Written by
9
- // JustAirService on download (JA-3081); read by the org-level admin
10
- // communication screen. AQI downloads are monitor-scoped, Sample downloads
11
- // are site-scoped (samples.js keys samples to SampleSites, not Monitors) —
12
- // modeled as discriminators per CLAUDE.md's multi-kind pattern rather than
13
- // a flat schema with a monitorId/siteId pair that's nullable either way.
14
- //
15
- // The base model permits a kind-less document (create() without `dataType`)
16
- // — same property `userNotifications.js`'s base model has. Write through
17
- // AqiDataDownload/SampleDataDownload, not DataDownloads, so every stored
18
- // document has the ref field the admin screen needs to render it.
19
- const dataDownloadSchema = mongoose.Schema(
20
- {
21
- orgId: {
22
- type: mongoose.Types.ObjectId,
23
- ref: "Organizations",
24
- required: [true, "orgId is required"],
25
- },
26
- dateRangeStart: { type: Date, required: [true, "dateRangeStart is required"] },
27
- dateRangeEnd: { type: Date, required: [true, "dateRangeEnd is required"] },
28
- timezone: {
29
- type: String,
30
- default: DEFAULT_TIMEZONE,
31
- validate: {
32
- validator: isValidIANATimezone,
33
- message: "timezone must be a valid IANA timezone identifier",
34
- },
35
- },
36
- downloadedAt: { type: Date, required: true, default: Date.now },
37
- email: {
38
- type: String,
39
- trim: true,
40
- lowercase: true,
41
- default: null,
42
- set: (value) => (typeof value === "string" && value.trim() === "" ? null : value),
43
- validate: {
44
- validator: (value) => value === null || EMAIL_PATTERN.test(value),
45
- message: "email must be a valid email address",
46
- },
47
- },
48
- },
49
- {
50
- timestamps: true,
51
- discriminatorKey: "dataType",
52
- }
53
- );
54
-
55
- // Cross-field rule (quiet-hours shape, CLAUDE.md): delegated to
56
- // validateDateOrder so this schema and JA-3081's write-path validation can
57
- // never drift. Applies to every kind — the 31-day cap is AQI-only and lives
58
- // on AqiDataDownload's own schema below, not here. Lives in pre('validate')
59
- // so create()/save() can't persist an invalid range even if a caller
60
- // forgets to check.
61
- dataDownloadSchema.pre("validate", function () {
62
- const errors = validateDateOrder({ start: this.dateRangeStart, end: this.dateRangeEnd });
63
- for (const error of errors) {
64
- this.invalidate("dateRangeEnd", error.message);
65
- }
66
- });
67
-
68
- // Org-level admin screen lists a given org's downloads, most recent first,
69
- // by the domain-meaningful "date of the download" (JA-3080), not createdAt.
70
- dataDownloadSchema.index({ orgId: 1, downloadedAt: -1 });
71
-
72
- const aqiDataDownloadSchema = new mongoose.Schema({
73
- monitorId: {
74
- type: mongoose.Types.ObjectId,
75
- ref: "Monitors",
76
- required: [true, "monitorId is required"],
77
- },
78
- });
79
- aqiDataDownloadSchema.index({ monitorId: 1, downloadedAt: -1 });
80
-
81
- // AQI-only: the 31-day cap does not apply to Sample downloads. Mongoose
82
- // merges base + discriminator schema hooks by default (mergeHooks), so this
83
- // runs alongside (not instead of) the base's validateDateOrder check above.
84
- aqiDataDownloadSchema.pre("validate", function () {
85
- const errors = validateMaxDateRange({ start: this.dateRangeStart, end: this.dateRangeEnd });
86
- for (const error of errors) {
87
- this.invalidate("dateRangeEnd", error.message);
88
- }
89
- });
90
-
91
- const sampleDataDownloadSchema = new mongoose.Schema({
92
- siteId: {
93
- type: mongoose.Types.ObjectId,
94
- ref: "SampleSites",
95
- required: [true, "siteId is required"],
96
- },
97
- });
98
- sampleDataDownloadSchema.index({ siteId: 1, downloadedAt: -1 });
99
-
100
- const DataDownloads = mongoose.model("DataDownloads", dataDownloadSchema);
101
-
102
- // Discriminator model names are namespaced (matches userNotifications.js's
103
- // aqi_alert/device_offline/weekly_digest) — "AQI"/"Sample" as bare mongoose
104
- // model-registry keys would collide with any consuming service's own models
105
- // of those names. { value } keeps the persisted dataType exactly what
106
- // JA-3245 specifies regardless of the registry name.
107
- const AqiDataDownload = DataDownloads.discriminator("AqiDataDownload", aqiDataDownloadSchema, {
108
- value: "AQI",
109
- });
110
-
111
- const SampleDataDownload = DataDownloads.discriminator(
112
- "SampleDataDownload",
113
- sampleDataDownloadSchema,
114
- { value: "Sample" }
115
- );
116
-
117
- export { dataDownloadSchema, DataDownloads, AqiDataDownload, SampleDataDownload };
@@ -1,85 +0,0 @@
1
- // Data download date range validation (JA-3080/JA-3245): shared cross-field
2
- // rules for the requested range on a DataDownloads document. Single source
3
- // of truth for every consumer — this library's schema enforces these on
4
- // write, JustAirService's intake endpoint (JA-3081) validates the same
5
- // request shape before ever constructing a document. Pure functions, no DB,
6
- // no clock reads. Same shape as quietHours.js's cross-field validator.
7
- //
8
- // Two separate rules, deliberately not one function: end-must-not-precede-
9
- // start applies to every download kind, but the 31-day cap is AQI-only —
10
- // Sample downloads have no range limit. Compose the one(s) that apply to
11
- // the kind being validated; don't call validateMaxDateRange for Sample.
12
-
13
- export const MAX_RANGE_DAYS = 31;
14
-
15
- // DST slack: a calendar-accurate 31-day span crossing a fall-back transition
16
- // is 745 (not 744) hours long. Allow it rather than rejecting a range the
17
- // product spec (Figma) explicitly promises as valid.
18
- const MAX_RANGE_MS = MAX_RANGE_DAYS * 24 * 60 * 60 * 1000 + 60 * 60 * 1000;
19
-
20
- // Stable error codes for validateDateOrder/validateMaxDateRange. Consumers
21
- // filter on `code` — never on message wording, which is free to change.
22
- export const DATA_DOWNLOAD_ERRORS = Object.freeze({
23
- END_BEFORE_START: "end_before_start",
24
- RANGE_TOO_LONG: "range_too_long",
25
- });
26
-
27
- // Coerces both ends through `new Date(...)` before comparing/subtracting: a
28
- // JSON request body (JA-3081's intake endpoint) hands these functions raw
29
- // strings, not Date instances, and comparing/subtracting un-coerced strings
30
- // silently produces NaN (`NaN > x` is always false) — the caps would never
31
- // fire for exactly the caller these functions exist to serve. An unparsable
32
- // value coerces to an Invalid Date (NaN time); format validation is the
33
- // schema/type layer's job, not a cross-field rule's, so that's treated the
34
- // same as "not this validator's concern" and returns null rather than
35
- // throwing or reporting a spurious range error.
36
- const toTimes = (start, end) => {
37
- if (!start || !end) {
38
- return null;
39
- }
40
- const startTime = new Date(start).getTime();
41
- const endTime = new Date(end).getTime();
42
- if (Number.isNaN(startTime) || Number.isNaN(endTime)) {
43
- return null;
44
- }
45
- return { startTime, endTime };
46
- };
47
-
48
- // Returns an array of { code, message } errors; empty array = valid. Applies
49
- // to every download kind (AQI and Sample) — the Figma "31 days" cap is
50
- // AQI-only and is NOT enforced here, see validateMaxDateRange.
51
- export const validateDateOrder = ({ start, end } = {}) => {
52
- const times = toTimes(start, end);
53
- if (!times) {
54
- return [];
55
- }
56
- if (times.endTime < times.startTime) {
57
- return [
58
- {
59
- code: DATA_DOWNLOAD_ERRORS.END_BEFORE_START,
60
- message: "dateRangeEnd must not be before dateRangeStart",
61
- },
62
- ];
63
- }
64
- return [];
65
- };
66
-
67
- // Returns an array of { code, message } errors; empty array = valid. AQI
68
- // downloads only — Sample downloads have no range limit. Assumes the caller
69
- // has already confirmed order via validateDateOrder (a reversed range isn't
70
- // re-reported here as "too long").
71
- export const validateMaxDateRange = ({ start, end } = {}) => {
72
- const times = toTimes(start, end);
73
- if (!times || times.endTime < times.startTime) {
74
- return [];
75
- }
76
- if (times.endTime - times.startTime > MAX_RANGE_MS) {
77
- return [
78
- {
79
- code: DATA_DOWNLOAD_ERRORS.RANGE_TOO_LONG,
80
- message: `date range cannot exceed ${MAX_RANGE_DAYS} days`,
81
- },
82
- ];
83
- }
84
- return [];
85
- };
@@ -1,89 +0,0 @@
1
- import {
2
- MAX_RANGE_DAYS,
3
- DATA_DOWNLOAD_ERRORS,
4
- validateDateOrder,
5
- validateMaxDateRange,
6
- } from "../dataDownloadRange.js";
7
-
8
- const DAY_MS = 24 * 60 * 60 * 1000;
9
- const HOUR_MS = 60 * 60 * 1000;
10
- const start = new Date("2026-01-01T00:00:00.000Z");
11
-
12
- describe("validateDateOrder (applies to every download kind)", () => {
13
- test("returns no errors when start/end are missing", () => {
14
- expect(validateDateOrder({})).toEqual([]);
15
- expect(validateDateOrder({ start })).toEqual([]);
16
- expect(validateDateOrder({ end: start })).toEqual([]);
17
- });
18
-
19
- test("returns no errors for a valid order", () => {
20
- const end = new Date(start.getTime() + DAY_MS);
21
- expect(validateDateOrder({ start, end })).toEqual([]);
22
- });
23
-
24
- test("flags end before start with END_BEFORE_START", () => {
25
- const end = new Date(start.getTime() - DAY_MS);
26
- expect(validateDateOrder({ start, end })).toEqual([
27
- expect.objectContaining({ code: DATA_DOWNLOAD_ERRORS.END_BEFORE_START }),
28
- ]);
29
- });
30
-
31
- test("does not flag a range far longer than MAX_RANGE_DAYS — that's validateMaxDateRange's job", () => {
32
- const end = new Date(start.getTime() + 365 * DAY_MS);
33
- expect(validateDateOrder({ start, end })).toEqual([]);
34
- });
35
-
36
- // JA-3081's intake endpoint hands this raw JSON request-body strings, not
37
- // Date instances — must not silently misjudge order via NaN-producing
38
- // string arithmetic (see dataDownloadRange.js's toTimes coercion comment).
39
- test("flags end before start given as raw date strings", () => {
40
- expect(
41
- validateDateOrder({ start: "2026-05-01", end: "2026-01-01" })
42
- ).toEqual([expect.objectContaining({ code: DATA_DOWNLOAD_ERRORS.END_BEFORE_START })]);
43
- });
44
-
45
- test("returns no errors for an unparsable date value", () => {
46
- expect(validateDateOrder({ start: "not-a-date", end: start })).toEqual([]);
47
- });
48
- });
49
-
50
- describe("validateMaxDateRange (AQI only — Sample downloads have no cap)", () => {
51
- test("returns no errors when start/end are missing", () => {
52
- expect(validateMaxDateRange({})).toEqual([]);
53
- });
54
-
55
- test("accepts exactly MAX_RANGE_DAYS", () => {
56
- const end = new Date(start.getTime() + MAX_RANGE_DAYS * DAY_MS);
57
- expect(validateMaxDateRange({ start, end })).toEqual([]);
58
- });
59
-
60
- // The +1h grace exists so a calendar-accurate 31-day span crossing a DST
61
- // fall-back transition (745 real hours, not 744) still validates — see
62
- // dataDownloadRange.js's MAX_RANGE_MS comment.
63
- test("accepts MAX_RANGE_DAYS plus the 1-hour DST grace", () => {
64
- const end = new Date(start.getTime() + MAX_RANGE_DAYS * DAY_MS + HOUR_MS);
65
- expect(validateMaxDateRange({ start, end })).toEqual([]);
66
- });
67
-
68
- test("flags a range longer than MAX_RANGE_DAYS plus the DST grace with RANGE_TOO_LONG", () => {
69
- const end = new Date(start.getTime() + MAX_RANGE_DAYS * DAY_MS + HOUR_MS + 1);
70
- expect(validateMaxDateRange({ start, end })).toEqual([
71
- expect.objectContaining({ code: DATA_DOWNLOAD_ERRORS.RANGE_TOO_LONG }),
72
- ]);
73
- });
74
-
75
- test("flags a too-long range given as raw date strings (JSON request-body shape)", () => {
76
- expect(
77
- validateMaxDateRange({ start: "2026-01-01", end: "2027-06-01" })
78
- ).toEqual([expect.objectContaining({ code: DATA_DOWNLOAD_ERRORS.RANGE_TOO_LONG })]);
79
- });
80
-
81
- test("does not re-report a reversed range as RANGE_TOO_LONG — validateDateOrder owns that", () => {
82
- const end = new Date(start.getTime() - DAY_MS);
83
- expect(validateMaxDateRange({ start, end })).toEqual([]);
84
- });
85
-
86
- test("returns no errors for an unparsable date value", () => {
87
- expect(validateMaxDateRange({ start: "not-a-date", end: start })).toEqual([]);
88
- });
89
- });