@justair/justair-library 7.3.0-alpha.e85bbda → 7.3.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/constants/dataDownloads.d.ts +1 -1
- package/dist/constants/dataDownloads.d.ts.map +1 -1
- package/dist/constants/timezones.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/models/dataDownloads.d.ts.map +1 -1
- package/dist/utils/dataDownloadValidation.d.ts +5 -0
- package/dist/utils/dataDownloadValidation.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/constants/dataDownloads.js +4 -1
- package/src/constants/timezones.js +6 -0
- package/src/index.js +4 -0
- package/src/models/dataDownloads.js +14 -7
- package/src/utils/dataDownloadValidation.js +42 -2
- package/src/utils/tests/dataDownloadValidation.test.js +56 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const DATA_DOWNLOAD_TYPES: string[];
|
|
1
|
+
export const DATA_DOWNLOAD_TYPES: readonly string[];
|
|
2
2
|
//# sourceMappingURL=dataDownloads.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dataDownloads.d.ts","sourceRoot":"","sources":["../../src/constants/dataDownloads.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"dataDownloads.d.ts","sourceRoot":"","sources":["../../src/constants/dataDownloads.js"],"names":[],"mappings":"AAkBA,oDAAoE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"timezones.d.ts","sourceRoot":"","sources":["../../src/constants/timezones.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"timezones.d.ts","sourceRoot":"","sources":["../../src/constants/timezones.js"],"names":[],"mappings":"AAWA,kDAAmD"}
|
package/dist/index.d.ts
CHANGED
|
@@ -97,6 +97,8 @@ import { validateDateOrder as validateDataDownloadDateOrder } from "./utils/data
|
|
|
97
97
|
import { validateMaxDateRange as validateDataDownloadMaxDateRange } from "./utils/dataDownloadValidation.js";
|
|
98
98
|
import { validateDownloadEmail as validateDataDownloadEmail } from "./utils/dataDownloadValidation.js";
|
|
99
99
|
import { normalizeDownloadEmail as normalizeDataDownloadEmail } from "./utils/dataDownloadValidation.js";
|
|
100
|
+
import { validateDownloadTimezone as validateDataDownloadTimezone } from "./utils/dataDownloadValidation.js";
|
|
101
|
+
import { normalizeDownloadTimezone as normalizeDataDownloadTimezone } from "./utils/dataDownloadValidation.js";
|
|
100
102
|
import { PARAMETERS } from "./constants/pollutants.js";
|
|
101
103
|
import { HEAVY_METALS } from "./constants/pollutants.js";
|
|
102
104
|
import { ALERT_LEVEL_WEIGHTS } from "./constants/alertLevels.js";
|
|
@@ -111,5 +113,5 @@ import { validateQuietHours } from "./utils/quietHours.js";
|
|
|
111
113
|
import { getZoneMinutes } from "./utils/quietHours.js";
|
|
112
114
|
import { isInQuietWindow } from "./utils/quietHours.js";
|
|
113
115
|
import { resolveQuietHours } from "./utils/quietHours.js";
|
|
114
|
-
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, DATA_DOWNLOAD_TYPES, dataDownloadSchema, DataDownloads, AqiDataDownload, SampleDataDownload, DATA_DOWNLOAD_MAX_RANGE_DAYS, DATA_DOWNLOAD_ERRORS, validateDataDownloadDateOrder, validateDataDownloadMaxDateRange, validateDataDownloadEmail, normalizeDataDownloadEmail, PARAMETERS, HEAVY_METALS, ALERT_LEVEL_WEIGHTS, getAlertWeight, splitName, DEFAULT_QUIET_HOURS, DEFAULT_TIMEZONE, QUIET_HOURS_ERRORS, isValidTimeString, isValidIANATimezone, validateQuietHours, getZoneMinutes, isInQuietWindow, resolveQuietHours };
|
|
116
|
+
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, DATA_DOWNLOAD_TYPES, dataDownloadSchema, DataDownloads, AqiDataDownload, SampleDataDownload, DATA_DOWNLOAD_MAX_RANGE_DAYS, DATA_DOWNLOAD_ERRORS, validateDataDownloadDateOrder, validateDataDownloadMaxDateRange, validateDataDownloadEmail, normalizeDataDownloadEmail, validateDataDownloadTimezone, normalizeDataDownloadTimezone, PARAMETERS, HEAVY_METALS, ALERT_LEVEL_WEIGHTS, getAlertWeight, splitName, DEFAULT_QUIET_HOURS, DEFAULT_TIMEZONE, QUIET_HOURS_ERRORS, isValidTimeString, isValidIANATimezone, validateQuietHours, getZoneMinutes, isInQuietWindow, resolveQuietHours };
|
|
115
117
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.js"],"names":[],"mappings":"AAoJA;;;iBAEC;yBApFwB,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;4BAmCT,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;oCAfF,8BAA8B;mCAsD3D,2BAA2B;8BAA3B,2BAA2B;gCAA3B,2BAA2B;mCAA3B,2BAA2B;+DAsB3B,mCAAmC;qCAAnC,mCAAmC;mEAAnC,mCAAmC;yEAAnC,mCAAmC;mEAAnC,mCAAmC;qEAAnC,mCAAmC;yEAAnC,mCAAmC;2EAAnC,mCAAmC;2BA/ED,2BAA2B;6BAA3B,2BAA2B;oCAChB,4BAA4B;+BAA5B,4BAA4B;0BAkBtD,sBAAsB;oCASzC,uBAAuB;iCA1BG,0BAA0B;mCA0BpD,uBAAuB;kCAAvB,uBAAuB;oCACM,qBAAqB;mCADlD,uBAAuB;+BAAvB,uBAAuB;gCAAvB,uBAAuB;kCAAvB,uBAAuB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dataDownloads.d.ts","sourceRoot":"","sources":["../../src/models/dataDownloads.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"dataDownloads.d.ts","sourceRoot":"","sources":["../../src/models/dataDownloads.js"],"names":[],"mappings":"AAwBA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+DE;AA2DF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAA0E;AAY1E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAEG;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAIE;qBAtKmB,UAAU"}
|
|
@@ -18,4 +18,9 @@ export function validateDownloadEmail({ email }?: {}): {
|
|
|
18
18
|
code: "invalid_email";
|
|
19
19
|
message: string;
|
|
20
20
|
}[];
|
|
21
|
+
export function normalizeDownloadTimezone(value: any): any;
|
|
22
|
+
export function validateDownloadTimezone({ timezone }?: {}): {
|
|
23
|
+
code: "invalid_timezone";
|
|
24
|
+
message: string;
|
|
25
|
+
}[];
|
|
21
26
|
//# sourceMappingURL=dataDownloadValidation.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dataDownloadValidation.d.ts","sourceRoot":"","sources":["../../src/utils/dataDownloadValidation.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"dataDownloadValidation.d.ts","sourceRoot":"","sources":["../../src/utils/dataDownloadValidation.js"],"names":[],"mappings":"AAYA,gCAAiC;AAUjC;;;;;GAKG;AAmBI;;;IAcN;AAIM;;;IAcN;AAgBM,wDAMN;AAIM;;;IAqBN;AAOM,2DAWN;AAOM;;;IAaN"}
|
package/package.json
CHANGED
|
@@ -13,4 +13,7 @@
|
|
|
13
13
|
// as a third type — add it here and register its discriminator in
|
|
14
14
|
// models/dataDownloads.js, so no consumer has to hardcode the string.
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
// Frozen because every importer in a process gets this same instance — one
|
|
17
|
+
// consumer calling .sort() or .push() would silently reorder the picker for
|
|
18
|
+
// all of them. Matches DATA_DOWNLOAD_ERRORS in dataDownloadValidation.js.
|
|
19
|
+
export const DATA_DOWNLOAD_TYPES = Object.freeze(["AQI", "Sample"]);
|
|
@@ -3,4 +3,10 @@
|
|
|
3
3
|
// Originally lived in utils/quietHours.js (JA-3054); moved here once a
|
|
4
4
|
// second, unrelated domain (dataDownloads.js, JA-3080) needed it too.
|
|
5
5
|
|
|
6
|
+
// Deliberately no DOWNLOAD_TIMEZONES / TIMEZONE_OPTIONS list here. The
|
|
7
|
+
// download modal's "Set Timezone" dropdown offers display labels ("Eastern
|
|
8
|
+
// Standard Time (EST)") over IANA values, and labels are a view concern:
|
|
9
|
+
// this library has no i18n and no view layer, so a label map would drift from
|
|
10
|
+
// design the first time the copy changed. The validator accepts any zone Intl
|
|
11
|
+
// accepts (utils/timezone.js), so the consumer owns which subset it offers.
|
|
6
12
|
export const DEFAULT_TIMEZONE = "America/New_York";
|
package/src/index.js
CHANGED
|
@@ -142,6 +142,8 @@ import {
|
|
|
142
142
|
validateMaxDateRange as validateDataDownloadMaxDateRange,
|
|
143
143
|
validateDownloadEmail as validateDataDownloadEmail,
|
|
144
144
|
normalizeDownloadEmail as normalizeDataDownloadEmail,
|
|
145
|
+
validateDownloadTimezone as validateDataDownloadTimezone,
|
|
146
|
+
normalizeDownloadTimezone as normalizeDataDownloadTimezone,
|
|
145
147
|
} from "./utils/dataDownloadValidation.js";
|
|
146
148
|
|
|
147
149
|
export function createLoggerInstance({ DATADOG_API_KEY, APPLICATION_NAME }) {
|
|
@@ -249,6 +251,8 @@ export {
|
|
|
249
251
|
validateDataDownloadMaxDateRange,
|
|
250
252
|
validateDataDownloadEmail,
|
|
251
253
|
normalizeDataDownloadEmail,
|
|
254
|
+
validateDataDownloadTimezone,
|
|
255
|
+
normalizeDataDownloadTimezone,
|
|
252
256
|
// Pollutant constants
|
|
253
257
|
PARAMETERS,
|
|
254
258
|
HEAVY_METALS,
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import mongoose from "mongoose";
|
|
2
2
|
import { DATA_DOWNLOAD_TYPES } from "../constants/dataDownloads.js";
|
|
3
3
|
import { DEFAULT_TIMEZONE } from "../constants/timezones.js";
|
|
4
|
-
import { isValidIANATimezone } from "../utils/timezone.js";
|
|
5
4
|
import {
|
|
6
5
|
DATA_DOWNLOAD_ERRORS,
|
|
7
6
|
validateDateOrder,
|
|
8
7
|
validateMaxDateRange,
|
|
9
8
|
validateDownloadEmail,
|
|
10
9
|
normalizeDownloadEmail,
|
|
10
|
+
validateDownloadTimezone,
|
|
11
|
+
normalizeDownloadTimezone,
|
|
11
12
|
} from "../utils/dataDownloadValidation.js";
|
|
12
13
|
|
|
13
14
|
// DataDownloads — one doc per data download request (JA-3080). Written by
|
|
@@ -30,10 +31,10 @@ const dataDownloadSchema = new mongoose.Schema(
|
|
|
30
31
|
},
|
|
31
32
|
dateRangeStart: { type: Date, required: [true, "dateRangeStart is required"] },
|
|
32
33
|
dateRangeEnd: { type: Date, required: [true, "dateRangeEnd is required"] },
|
|
33
|
-
// `default` only applies to `undefined`, not `null
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
// hook below does not.
|
|
34
|
+
// `default` only applies to `undefined`, not `null` or "", so both of the
|
|
35
|
+
// other "didn't pick one" shapes (a JSON body's null, an untouched HTML
|
|
36
|
+
// <select>'s empty string) are coerced in a setter — setters run on query
|
|
37
|
+
// updates, the pre('validate') hook below does not.
|
|
37
38
|
//
|
|
38
39
|
// This guarantees less than "always valid": `runValidators` defaults to
|
|
39
40
|
// false on query updates, so an updateOne can still persist a bad zone.
|
|
@@ -42,9 +43,9 @@ const dataDownloadSchema = new mongoose.Schema(
|
|
|
42
43
|
type: String,
|
|
43
44
|
trim: true,
|
|
44
45
|
default: DEFAULT_TIMEZONE,
|
|
45
|
-
set:
|
|
46
|
+
set: normalizeDownloadTimezone,
|
|
46
47
|
validate: {
|
|
47
|
-
validator:
|
|
48
|
+
validator: (value) => validateDownloadTimezone({ timezone: value }).length === 0,
|
|
48
49
|
message: "timezone must be a valid IANA timezone identifier",
|
|
49
50
|
type: DATA_DOWNLOAD_ERRORS.INVALID_TIMEZONE,
|
|
50
51
|
},
|
|
@@ -93,6 +94,12 @@ const dataDownloadSchema = new mongoose.Schema(
|
|
|
93
94
|
//
|
|
94
95
|
// Passes the error code as invalidate()'s `kind` so a caught ValidationError
|
|
95
96
|
// can be classified via err.errors.<path>.kind, not by message wording.
|
|
97
|
+
//
|
|
98
|
+
// Same caveat the `timezone` field carries: pre('validate') never runs on a
|
|
99
|
+
// query update, so a `$set` of dateRangeStart/dateRangeEnd can persist a
|
|
100
|
+
// reversed or over-length range even with { runValidators: true }. Deliberate
|
|
101
|
+
// — the rules are cross-field, and query updates have no full document to
|
|
102
|
+
// check. Writers should go through create()/save(); readers must still defend.
|
|
96
103
|
dataDownloadSchema.pre("validate", function () {
|
|
97
104
|
const rangeErrors = validateDateOrder({ start: this.dateRangeStart, end: this.dateRangeEnd });
|
|
98
105
|
for (const error of rangeErrors) {
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// Data download validation (JA-3080/JA-3245): the shared field and cross-field
|
|
2
2
|
// rules for a DataDownloads document — date range order, the AQI-only length
|
|
3
|
-
// cap,
|
|
4
|
-
// shape as quietHours.js's cross-field validator.
|
|
3
|
+
// cap, the optional email, and the timezone. Pure functions, no DB, no clock
|
|
4
|
+
// reads. Same shape as quietHours.js's cross-field validator.
|
|
5
5
|
//
|
|
6
6
|
// Two separate rules, deliberately not one function: end-must-not-precede-
|
|
7
7
|
// start applies to every download kind, but the 31-day cap is AQI-only.
|
|
8
|
+
import { isValidIANATimezone } from "./timezone.js";
|
|
9
|
+
import { DEFAULT_TIMEZONE } from "../constants/timezones.js";
|
|
8
10
|
|
|
9
11
|
// Messaging only ("range cannot exceed 31 days"). validateMaxDateRange is the
|
|
10
12
|
// sole authority on the enforced bound; don't rebuild the cap from this.
|
|
@@ -122,3 +124,41 @@ export const validateDownloadEmail = ({ email } = {}) => {
|
|
|
122
124
|
},
|
|
123
125
|
];
|
|
124
126
|
};
|
|
127
|
+
|
|
128
|
+
// Every shape of "didn't pick one" resolves to the default, mirroring
|
|
129
|
+
// normalizeDownloadEmail's blank-collapses-to-null contract. An untouched HTML
|
|
130
|
+
// <select> posts "" and a JSON body usually carries null, so treating only one
|
|
131
|
+
// of them as unset rejects a download for a field the user never touched.
|
|
132
|
+
// `undefined` passes through so the schema's own `default` still applies.
|
|
133
|
+
export const normalizeDownloadTimezone = (value) => {
|
|
134
|
+
if (value === undefined) {
|
|
135
|
+
return value;
|
|
136
|
+
}
|
|
137
|
+
if (value === null) {
|
|
138
|
+
return DEFAULT_TIMEZONE;
|
|
139
|
+
}
|
|
140
|
+
if (typeof value === "string" && value.trim() === "") {
|
|
141
|
+
return DEFAULT_TIMEZONE;
|
|
142
|
+
}
|
|
143
|
+
return value;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// Returns { code, message }[]; empty = valid. Anything normalizeDownloadTimezone
|
|
147
|
+
// resolves to the default is valid here too, so a consumer pre-validating a
|
|
148
|
+
// request body accepts exactly what the model stores. isValidIANATimezone alone
|
|
149
|
+
// rejects null and "", which would fail a request the model saves happily —
|
|
150
|
+
// the write-path/read-path split the email rules exist to avoid.
|
|
151
|
+
export const validateDownloadTimezone = ({ timezone } = {}) => {
|
|
152
|
+
if (normalizeDownloadTimezone(timezone) !== timezone) {
|
|
153
|
+
return [];
|
|
154
|
+
}
|
|
155
|
+
if (timezone === undefined || isValidIANATimezone(timezone)) {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
return [
|
|
159
|
+
{
|
|
160
|
+
code: DATA_DOWNLOAD_ERRORS.INVALID_TIMEZONE,
|
|
161
|
+
message: "timezone must be a valid IANA timezone identifier",
|
|
162
|
+
},
|
|
163
|
+
];
|
|
164
|
+
};
|
|
@@ -4,8 +4,11 @@ import {
|
|
|
4
4
|
validateDateOrder,
|
|
5
5
|
validateMaxDateRange,
|
|
6
6
|
validateDownloadEmail,
|
|
7
|
+
validateDownloadTimezone,
|
|
8
|
+
normalizeDownloadTimezone,
|
|
7
9
|
normalizeDownloadEmail,
|
|
8
10
|
} from "../dataDownloadValidation.js";
|
|
11
|
+
import { DEFAULT_TIMEZONE } from "../../constants/timezones.js";
|
|
9
12
|
|
|
10
13
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
11
14
|
const HOUR_MS = 60 * 60 * 1000;
|
|
@@ -155,3 +158,56 @@ describe("validateDownloadEmail (email is optional — JA-3080)", () => {
|
|
|
155
158
|
]);
|
|
156
159
|
});
|
|
157
160
|
});
|
|
161
|
+
|
|
162
|
+
describe("validateDownloadTimezone", () => {
|
|
163
|
+
test.each(["America/New_York", "America/Detroit", "UTC", "Asia/Tokyo"])(
|
|
164
|
+
"accepts %s",
|
|
165
|
+
(timezone) => {
|
|
166
|
+
expect(validateDownloadTimezone({ timezone })).toEqual([]);
|
|
167
|
+
}
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
// The schema setter resolves both to DEFAULT_TIMEZONE, so a consumer
|
|
171
|
+
// pre-validating a request body must accept what the model stores.
|
|
172
|
+
// An untouched <select> posts "", a JSON body usually carries null. Both mean
|
|
173
|
+
// "didn't pick one", so both must validate the way the setter resolves them.
|
|
174
|
+
test.each([null, undefined, "", " "])(
|
|
175
|
+
"treats %p as valid (the setter fills the default)",
|
|
176
|
+
(timezone) => {
|
|
177
|
+
expect(validateDownloadTimezone({ timezone })).toEqual([]);
|
|
178
|
+
}
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
test("accepts a call with no argument at all", () => {
|
|
182
|
+
expect(validateDownloadTimezone()).toEqual([]);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test.each(["America/Fake_City", "Not/A_Zone", 5, {}])(
|
|
186
|
+
"flags %p with INVALID_TIMEZONE",
|
|
187
|
+
(timezone) => {
|
|
188
|
+
expect(validateDownloadTimezone({ timezone })).toEqual([
|
|
189
|
+
expect.objectContaining({ code: DATA_DOWNLOAD_ERRORS.INVALID_TIMEZONE }),
|
|
190
|
+
]);
|
|
191
|
+
}
|
|
192
|
+
);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe("normalizeDownloadTimezone", () => {
|
|
196
|
+
test.each([null, "", " "])("resolves %p to the default", (value) => {
|
|
197
|
+
expect(normalizeDownloadTimezone(value)).toBe(DEFAULT_TIMEZONE);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Passes through so the schema's own `default` still applies.
|
|
201
|
+
test("leaves undefined alone", () => {
|
|
202
|
+
expect(normalizeDownloadTimezone(undefined)).toBeUndefined();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("leaves a real zone untouched", () => {
|
|
206
|
+
expect(normalizeDownloadTimezone("Asia/Tokyo")).toBe("Asia/Tokyo");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Validation, not normalization, is what rejects these.
|
|
210
|
+
test.each(["Not/A_Zone", 5])("leaves invalid input %p for the validator", (value) => {
|
|
211
|
+
expect(normalizeDownloadTimezone(value)).toBe(value);
|
|
212
|
+
});
|
|
213
|
+
});
|