@justair/justair-library 7.6.0-alpha.2ec581a → 7.6.0-alpha.4ff5aae

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.
@@ -5,7 +5,6 @@ export const DEVICE_TYPES: Readonly<{
5
5
  }>;
6
6
  export const DEVICE_TYPE_VALUES: readonly ("monitor" | "sample-site" | "linked-device")[];
7
7
  export function isAqiCapableDeviceType(deviceType: any): boolean;
8
- export const AQI_CAPABLE_DEVICE_TYPES: readonly ("monitor" | "sample-site" | "linked-device")[];
9
8
  /**
10
9
  * The device-type vocabulary as a type, so consumers get autocomplete and a
11
10
  * compile error on a mis-cased value like "sampleSite" — the exact bug the
@@ -1 +1 @@
1
- {"version":3,"file":"deviceTypes.d.ts","sourceRoot":"","sources":["../../src/constants/deviceTypes.js"],"names":[],"mappings":"AA0BA;;;;GAIG;AAEH,0FAA6E;AAiBtE,iEAEoC;AAiB3C,gGAEE;;;;;;yBA/BW,CAAA,OAAO,YAAY,EAAC,MAAM,OAAO,YAAY,CAAC"}
1
+ {"version":3,"file":"deviceTypes.d.ts","sourceRoot":"","sources":["../../src/constants/deviceTypes.js"],"names":[],"mappings":"AA0BA;;;;GAIG;AAEH,0FAA6E;AAiBtE,iEAEoC;;;;;;yBAZ9B,CAAA,OAAO,YAAY,EAAC,MAAM,OAAO,YAAY,CAAC"}
package/dist/index.d.ts CHANGED
@@ -106,9 +106,8 @@ import { getAlertWeight } from "./constants/alertLevels.js";
106
106
  import { DEVICE_TYPES } from "./constants/deviceTypes.js";
107
107
  import { DEVICE_TYPE_VALUES } from "./constants/deviceTypes.js";
108
108
  import { isAqiCapableDeviceType } from "./constants/deviceTypes.js";
109
- import { AQI_CAPABLE_DEVICE_TYPES } from "./constants/deviceTypes.js";
110
109
  import { projectSubscribedMonitors } from "./utils/subscribedDevices.js";
111
- import { resolveSubscribedMonitorIdsByUser } from "./utils/subscribedDevices.js";
110
+ import { resolveMonitorIdsByUser } from "./utils/subscribedDevices.js";
112
111
  import { VISIBLE_SITE_MONITOR_FILTER } from "./utils/subscribedDevices.js";
113
112
  import { SUBSCRIBED_DEVICES_ERRORS } from "./utils/subscribedDevices.js";
114
113
  import { splitName } from "./utils/splitName.js";
@@ -121,5 +120,5 @@ import { validateQuietHours } from "./utils/quietHours.js";
121
120
  import { getZoneMinutes } from "./utils/quietHours.js";
122
121
  import { isInQuietWindow } from "./utils/quietHours.js";
123
122
  import { resolveQuietHours } from "./utils/quietHours.js";
124
- 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, DEVICE_TYPES, DEVICE_TYPE_VALUES, isAqiCapableDeviceType, AQI_CAPABLE_DEVICE_TYPES, projectSubscribedMonitors, resolveSubscribedMonitorIdsByUser, VISIBLE_SITE_MONITOR_FILTER, SUBSCRIBED_DEVICES_ERRORS, splitName, DEFAULT_QUIET_HOURS, DEFAULT_TIMEZONE, QUIET_HOURS_ERRORS, isValidTimeString, isValidIANATimezone, validateQuietHours, getZoneMinutes, isInQuietWindow, resolveQuietHours };
123
+ 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, DEVICE_TYPES, DEVICE_TYPE_VALUES, isAqiCapableDeviceType, projectSubscribedMonitors, resolveMonitorIdsByUser, VISIBLE_SITE_MONITOR_FILTER, SUBSCRIBED_DEVICES_ERRORS, splitName, DEFAULT_QUIET_HOURS, DEFAULT_TIMEZONE, QUIET_HOURS_ERRORS, isValidTimeString, isValidIANATimezone, validateQuietHours, getZoneMinutes, isInQuietWindow, resolveQuietHours };
125
124
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.js"],"names":[],"mappings":"AAyKA;;;iBAEC;yBAtGwB,oBAAoB;qBADxB,gBAAgB;4BApEF,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;qCAI9B,4BAA4B;+BAA5B,4BAA4B;mCACc,0BAA0B;6BAA1B,0BAA0B;8CAIpE,qCAAqC;wCAArC,qCAAqC;4BAkDT,mBAAmB;sBAAnB,mBAAmB;kCAS/C,yBAAyB;4BAAzB,yBAAyB;sCAAzB,yBAAyB;gCAAzB,yBAAyB;qCAAzB,yBAAyB;8BASzB,qBAAqB;wBAArB,qBAAqB;mCAArB,qBAAqB;6BAArB,qBAAqB;uDAArB,qBAAqB;yCAIrB,gCAAgC;mCAAhC,gCAAgC;sCArDhC,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;oCArBF,8BAA8B;mCAqE3D,2BAA2B;8BAA3B,2BAA2B;gCAA3B,2BAA2B;mCAA3B,2BAA2B;+DAsB3B,mCAAmC;qCAAnC,mCAAmC;mEAAnC,mCAAmC;yEAAnC,mCAAmC;mEAAnC,mCAAmC;qEAAnC,mCAAmC;yEAAnC,mCAAmC;2EAAnC,mCAAmC;2BAjGD,2BAA2B;6BAA3B,2BAA2B;oCAI7D,4BAA4B;+BAA5B,4BAA4B;6BAQ5B,4BAA4B;mCAA5B,4BAA4B;uCAA5B,4BAA4B;yCAA5B,4BAA4B;0CAgC5B,8BAA8B;kDAA9B,8BAA8B;4CAA9B,8BAA8B;0CAA9B,8BAA8B;0BAhBX,sBAAsB;oCASzC,uBAAuB;iCAhCG,0BAA0B;mCAgCpD,uBAAuB;kCAAvB,uBAAuB;oCACM,qBAAqB;mCADlD,uBAAuB;+BAAvB,uBAAuB;gCAAvB,uBAAuB;kCAAvB,uBAAuB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.js"],"names":[],"mappings":"AA+JA;;;iBAEC;yBA/FwB,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;4BA8CT,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;sCAlDtF,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;oCApBF,8BAA8B;mCAiE3D,2BAA2B;8BAA3B,2BAA2B;gCAA3B,2BAA2B;mCAA3B,2BAA2B;+DAsB3B,mCAAmC;qCAAnC,mCAAmC;mEAAnC,mCAAmC;yEAAnC,mCAAmC;mEAAnC,mCAAmC;qEAAnC,mCAAmC;yEAAnC,mCAAmC;2EAAnC,mCAAmC;2BA1FD,2BAA2B;6BAA3B,2BAA2B;oCAChB,4BAA4B;+BAA5B,4BAA4B;6BAOzE,4BAA4B;mCAA5B,4BAA4B;uCAA5B,4BAA4B;0CAgC5B,8BAA8B;wCAA9B,8BAA8B;4CAA9B,8BAA8B;0CAA9B,8BAA8B;0BAhBX,sBAAsB;oCASzC,uBAAuB;iCA/BG,0BAA0B;mCA+BpD,uBAAuB;kCAAvB,uBAAuB;oCACM,qBAAqB;mCADlD,uBAAuB;+BAAvB,uBAAuB;gCAAvB,uBAAuB;kCAAvB,uBAAuB"}
@@ -1 +1 @@
1
- {"version":3,"file":"users.d.ts","sourceRoot":"","sources":["../../src/models/users.js"],"names":[],"mappings":"AA6FA,8BA0GE;AAiCF,wBAAmD;AA/NnD,iDAA4D;AAC5D,6CAAmD;AAUnD,4DA0BC"}
1
+ {"version":3,"file":"users.d.ts","sourceRoot":"","sources":["../../src/models/users.js"],"names":[],"mappings":"AAyFA,8BAgGE;AAiCF,wBAAmD;AAjNnD,iDAA4D;AAC5D,6CAAmD;AAUnD,4DAsBC"}
@@ -8,9 +8,9 @@ export const SUBSCRIBED_DEVICES_ERRORS: Readonly<{
8
8
  COHORT_USER_MISSING_ID: "cohort_user_missing_id";
9
9
  }>;
10
10
  export const VISIBLE_SITE_MONITOR_FILTER: Readonly<{
11
- isPrivate: Readonly<{
12
- $ne: true;
13
- }>;
11
+ isPrivate: {
12
+ $ne: boolean;
13
+ };
14
14
  isActive: true;
15
15
  }>;
16
16
  export function projectSubscribedMonitors(subscribedDevices?: Array<{
@@ -19,7 +19,7 @@ export function projectSubscribedMonitors(subscribedDevices?: Array<{
19
19
  }>, { resolveSiteMonitorIds }?: {
20
20
  resolveSiteMonitorIds?: (siteIds: import("mongoose").Types.ObjectId[]) => Promise<Array<import("mongoose").Types.ObjectId | null | undefined>>;
21
21
  }): Promise<import("mongoose").Types.ObjectId[]>;
22
- export function resolveSubscribedMonitorIdsByUser(users?: Array<{
22
+ export function resolveMonitorIdsByUser(users?: Array<{
23
23
  _id: any;
24
24
  subscribedDevices?: Array<{
25
25
  deviceId: import("mongoose").Types.ObjectId;
@@ -1 +1 @@
1
- {"version":3,"file":"subscribedDevices.d.ts","sourceRoot":"","sources":["../../src/utils/subscribedDevices.js"],"names":[],"mappings":"AA0FA;;;;;;;;GASG;AA+CH;;;;;GAGG;AA6LI,8DANI,KAAK,CAAC;IAAE,QAAQ,EAAE,OAAO,UAAU,EAAE,KAAK,CAAC,QAAQ,CAAC;IAC1D,UAAU,EAAE,OAAO,6BAA6B,EAAE,UAAU,CAAA;CAAE,CAAC,8BACzD;IAAE,qBAAqB,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,UAAU,EAAE,KAAK,CAAC,QAAQ,EAAE,KAC7E,OAAO,CAAC,KAAK,CAAC,OAAO,UAAU,EAAE,KAAK,CAAC,QAAQ,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC,CAAA;CAAE,GAC9D,OAAO,CAAC,OAAO,UAAU,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC,CA0BxD;AA0EM,0DAPI,KAAK,CAAC;IAAE,GAAG,EAAE,GAAG,CAAC;IAAC,iBAAiB,CAAC,EAAE,KAAK,CAAC;QAClD,QAAQ,EAAE,OAAO,UAAU,EAAE,KAAK,CAAC,QAAQ,CAAC;QAC5C,UAAU,EAAE,OAAO,6BAA6B,EAAE,UAAU,CAAA;KAAE,CAAC,CAAA;CAAE,CAAC,mCAC5D;IAAE,0BAA0B,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,UAAU,EAAE,KAAK,CAAC,QAAQ,EAAE,KAClF,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,UAAU,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAA;CAAE,GACnD,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,UAAU,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC,CAmErE"}
1
+ {"version":3,"file":"subscribedDevices.d.ts","sourceRoot":"","sources":["../../src/utils/subscribedDevices.js"],"names":[],"mappings":"AAmEA;;;;;;;;GASG;AAyBH;;;;;GAGG;AAiLI,8DANI,KAAK,CAAC;IAAE,QAAQ,EAAE,OAAO,UAAU,EAAE,KAAK,CAAC,QAAQ,CAAC;IAC1D,UAAU,EAAE,OAAO,6BAA6B,EAAE,UAAU,CAAA;CAAE,CAAC,8BACzD;IAAE,qBAAqB,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,UAAU,EAAE,KAAK,CAAC,QAAQ,EAAE,KAC7E,OAAO,CAAC,KAAK,CAAC,OAAO,UAAU,EAAE,KAAK,CAAC,QAAQ,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC,CAAA;CAAE,GAC9D,OAAO,CAAC,OAAO,UAAU,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC,CA0BxD;AAkEM,gDAPI,KAAK,CAAC;IAAE,GAAG,EAAE,GAAG,CAAC;IAAC,iBAAiB,CAAC,EAAE,KAAK,CAAC;QAClD,QAAQ,EAAE,OAAO,UAAU,EAAE,KAAK,CAAC,QAAQ,CAAC;QAC5C,UAAU,EAAE,OAAO,6BAA6B,EAAE,UAAU,CAAA;KAAE,CAAC,CAAA;CAAE,CAAC,mCAC5D;IAAE,0BAA0B,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,UAAU,EAAE,KAAK,CAAC,QAAQ,EAAE,KAClF,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,UAAU,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAA;CAAE,GACnD,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,UAAU,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC,CA2DrE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@justair/justair-library",
3
- "version": "7.6.0-alpha.2ec581a",
3
+ "version": "7.6.0-alpha.4ff5aae",
4
4
  "description": "JustAir Internal Library",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -50,22 +50,3 @@ export const DEVICE_TYPE_VALUES = Object.freeze(Object.values(DEVICE_TYPES));
50
50
  export const isAqiCapableDeviceType = (deviceType) =>
51
51
  deviceType === DEVICE_TYPES.MONITOR ||
52
52
  deviceType === DEVICE_TYPES.LINKED_DEVICE;
53
-
54
- // The same predicate in the shape a Mongo query wants, so a cohort read can
55
- // leave out rows that could never alert instead of loading and discarding
56
- // them:
57
- //
58
- // { "subscribedDevices.deviceType": { $in: AQI_CAPABLE_DEVICE_TYPES } }
59
- //
60
- // Derived from isAqiCapableDeviceType's own answer rather than hand-listed, so
61
- // the two cannot disagree if a fourth device type ever arrives. Frozen for the
62
- // same reason DEVICE_TYPE_VALUES is: every importer in a process shares this
63
- // instance, and one `.push()` would silently widen the audience for all of
64
- // them.
65
- //
66
- // Carries the same asymmetry the predicate does: `linked-device` being in this
67
- // list is necessary and NOT sufficient — a Site only actually alerts when it
68
- // links at least one monitor, which no query on deviceType can tell you.
69
- export const AQI_CAPABLE_DEVICE_TYPES = Object.freeze(
70
- DEVICE_TYPE_VALUES.filter(isAqiCapableDeviceType),
71
- );
@@ -8,14 +8,12 @@ import {
8
8
  DEVICE_TYPES,
9
9
  DEVICE_TYPE_VALUES,
10
10
  isAqiCapableDeviceType,
11
- AQI_CAPABLE_DEVICE_TYPES,
12
11
  } from '../deviceTypes.js';
13
12
 
14
13
  test('barrel exports the device-type constants', () => {
15
14
  expect(barrel.DEVICE_TYPES).toBe(DEVICE_TYPES);
16
15
  expect(barrel.DEVICE_TYPE_VALUES).toBe(DEVICE_TYPE_VALUES);
17
16
  expect(barrel.isAqiCapableDeviceType).toBe(isAqiCapableDeviceType);
18
- expect(barrel.AQI_CAPABLE_DEVICE_TYPES).toBe(AQI_CAPABLE_DEVICE_TYPES);
19
17
  });
20
18
 
21
19
  test('DEVICE_TYPES values match the strings the devices API emits', () => {
@@ -53,23 +51,3 @@ describe('isAqiCapableDeviceType', () => {
53
51
  expect(isAqiCapableDeviceType('')).toBe(false);
54
52
  });
55
53
  });
56
-
57
- describe('AQI_CAPABLE_DEVICE_TYPES', () => {
58
- test('is the $in form of isAqiCapableDeviceType, in vocabulary order', () => {
59
- expect(AQI_CAPABLE_DEVICE_TYPES).toEqual(['monitor', 'linked-device']);
60
- });
61
-
62
- test('agrees with the predicate for every device type', () => {
63
- // Derived from the predicate rather than hand-listed, so this pins that
64
- // the derivation stays wired up if a fourth device type is added.
65
- DEVICE_TYPE_VALUES.forEach((deviceType) => {
66
- expect(AQI_CAPABLE_DEVICE_TYPES.includes(deviceType)).toBe(
67
- isAqiCapableDeviceType(deviceType),
68
- );
69
- });
70
- });
71
-
72
- test('is frozen — every importer shares this instance', () => {
73
- expect(Object.isFrozen(AQI_CAPABLE_DEVICE_TYPES)).toBe(true);
74
- });
75
- });
package/src/index.js CHANGED
@@ -57,10 +57,7 @@ import {
57
57
  DataCompleteness,
58
58
  dataCompletenessSchema,
59
59
  } from "./models/dataCompleteness.js";
60
- import {
61
- NetworkMetrics,
62
- networkMetricsSchema,
63
- } from "./models/networkMetrics.js";
60
+ import { NetworkMetrics, networkMetricsSchema } from "./models/networkMetrics.js";
64
61
  import { RateOfChange, rateOfChangeSchema } from "./models/rateOfChange.js";
65
62
  import {
66
63
  ChangeStreamCheckpoints,
@@ -69,17 +66,13 @@ import {
69
66
  import Database from "./config/db.js"; // Import the new Database class
70
67
  import CustomLogger from "./config/logger.js";
71
68
  import { PARAMETERS, HEAVY_METALS } from "./constants/pollutants.js";
72
- import {
73
- ALERT_LEVEL_WEIGHTS,
74
- getAlertWeight,
75
- } from "./constants/alertLevels.js";
69
+ import { ALERT_LEVEL_WEIGHTS, getAlertWeight } from "./constants/alertLevels.js";
76
70
  import { DEFAULT_TIMEZONE } from "./constants/timezones.js";
77
71
  import { DATA_DOWNLOAD_TYPES } from "./constants/dataDownloads.js";
78
72
  import {
79
73
  DEVICE_TYPES,
80
74
  DEVICE_TYPE_VALUES,
81
75
  isAqiCapableDeviceType,
82
- AQI_CAPABLE_DEVICE_TYPES,
83
76
  } from "./constants/deviceTypes.js";
84
77
  import {
85
78
  monitorEpisodesSchema,
@@ -109,7 +102,7 @@ import {
109
102
  import { isValidIANATimezone } from "./utils/timezone.js";
110
103
  import {
111
104
  projectSubscribedMonitors,
112
- resolveSubscribedMonitorIdsByUser,
105
+ resolveMonitorIdsByUser,
113
106
  VISIBLE_SITE_MONITOR_FILTER,
114
107
  SUBSCRIBED_DEVICES_ERRORS,
115
108
  } from "./utils/subscribedDevices.js";
@@ -134,10 +127,7 @@ import {
134
127
  SamplesAudit,
135
128
  sampleParameterReferenceConcentrations,
136
129
  } from "./models/samples.js";
137
- import {
138
- passwordResetTokenSchema,
139
- PasswordResetToken,
140
- } from "./models/passwordResetToken.js";
130
+ import { passwordResetTokenSchema, PasswordResetToken } from "./models/passwordResetToken.js";
141
131
  import {
142
132
  dataDownloadSchema,
143
133
  DataDownloads,
@@ -284,22 +274,14 @@ export {
284
274
  DEVICE_TYPES,
285
275
  DEVICE_TYPE_VALUES,
286
276
  isAqiCapableDeviceType,
287
- // The query form of isAqiCapableDeviceType, for narrowing a Users read to
288
- // rows that could actually alert:
289
- // { "subscribedDevices.deviceType": { $in: AQI_CAPABLE_DEVICE_TYPES } }
290
- AQI_CAPABLE_DEVICE_TYPES,
291
277
  // subscribedDevices → monitor ids, resolved at READ time (JA-3110 / JA-3335).
292
278
  // Users.subscribedDevices is the source of truth; Users.subscribedMonitors is
293
279
  // being retired rather than kept in step with it. Nothing should store this
294
280
  // result — see the module header for the five bugs a stored copy caused.
295
281
  //
296
- // Both resolvers are only correct for users whose subscribedDevices has been
297
- // backfilled from subscribedMonitors — read the PRECONDITION block in the
298
- // module header before pointing a service at either of them.
299
- //
300
- // resolveSubscribedMonitorIdsByUser — the batch form. One Sites lookup for
301
- // a whole cohort. What alert runs, org pages and
302
- // session responses should call.
282
+ // resolveMonitorIdsByUser — the batch form. One Sites lookup for a whole
283
+ // cohort. What alert runs, org pages and session
284
+ // responses should call.
303
285
  // projectSubscribedMonitors — the single-user rule the batch form is built
304
286
  // on. Named after the field being retired; it
305
287
  // gets renamed when that field is dropped.
@@ -310,7 +292,7 @@ export {
310
292
  // SUBSCRIBED_DEVICES_ERRORS — stable `err.code` vocabulary; consumers
311
293
  // branch on it, never on message wording.
312
294
  projectSubscribedMonitors,
313
- resolveSubscribedMonitorIdsByUser,
295
+ resolveMonitorIdsByUser,
314
296
  VISIBLE_SITE_MONITOR_FILTER,
315
297
  SUBSCRIBED_DEVICES_ERRORS,
316
298
  // Utilities
@@ -15,15 +15,11 @@ const cookieConsentSchemaDefinition = COOKIE_CONSENT_CATEGORIES.reduce(
15
15
  definition[category] = { type: String, enum: COOKIE_CONSENT_VALUES };
16
16
  return definition;
17
17
  },
18
- {},
18
+ {}
19
19
  );
20
20
 
21
21
  const validateCookieConsent = (consent) => {
22
- if (
23
- typeof consent !== "object" ||
24
- consent === null ||
25
- Array.isArray(consent)
26
- ) {
22
+ if (typeof consent !== "object" || consent === null || Array.isArray(consent)) {
27
23
  return "cookie_consent must be an object";
28
24
  }
29
25
 
@@ -38,7 +34,7 @@ const validateCookieConsent = (consent) => {
38
34
  }
39
35
  if (!COOKIE_CONSENT_VALUES.includes(value)) {
40
36
  return `Invalid value for ${category}: must be one of ${COOKIE_CONSENT_VALUES.join(
41
- ", ",
37
+ ", "
42
38
  )}`;
43
39
  }
44
40
  }
@@ -88,7 +84,7 @@ const subscribedDeviceSchema = mongoose.Schema(
88
84
  // writing (and/or pass `{ runValidators: true }` to the update).
89
85
  deviceType: { type: String, enum: DEVICE_TYPE_VALUES, required: true },
90
86
  },
91
- { _id: false },
87
+ { _id: false }
92
88
  );
93
89
 
94
90
  const usersSchema = mongoose.Schema(
@@ -103,31 +99,21 @@ const usersSchema = mongoose.Schema(
103
99
  organizations: [{ type: mongoose.Types.ObjectId, ref: "Organizations" }],
104
100
  alertPhone: String,
105
101
  alertCount: { type: Number, default: 0 },
106
- // BEING RETIRED (JA-3110 / JA-3335). This was a denormalized copy of the
107
- // AQI-alert audience, rewritten on every subscribe and unsubscribe. Do not
108
- // write it from new code and do not keep it in step with anything: the
109
- // audience is now RESOLVED AT READ TIME from subscribedDevices below, and
110
- // the field is dropped once every consumer has moved off it. Five distinct
111
- // silent-failure bugs traced back to maintaining this copy; they are
112
- // enumerated in the header of src/utils/subscribedDevices.js.
113
- //
114
- // It is still read (and still the correct thing to read) for any user
115
- // whose subscribedDevices has not been backfilled yet — the same header's
116
- // PRECONDITION block has the ordering.
117
102
  subscribedMonitors: [{ type: mongoose.Types.ObjectId, ref: "Monitors" }],
118
- // Subscribed devices (JA-3110 / JA-3335) — the SOURCE OF TRUTH for what the
103
+ // Subscribed devices (JA-3110 / JA-3335) — the source of truth for what the
119
104
  // user has added to "My Monitors", across all three device types.
120
105
  //
121
- // The monitor ids this implies are derived on read, never stored:
122
- // `resolveSubscribedMonitorIdsByUser` for a cohort,
123
- // `projectSubscribedMonitors` for one user, both exported from
124
- // src/index.js. Deliberately not a schema validator — the derivation needs
125
- // a Site lookup, which the schema has no business doing.
106
+ // subscribedMonitors above is its derived AQI-alert projection (monitor ids
107
+ // only). The two must be written together on every subscribe and
108
+ // unsubscribe path, using `projectSubscribedMonitors` rather than a
109
+ // per-consumer reimplementation. It is deliberately not a schema validator:
110
+ // the projection needs a Site lookup, which the schema has no business
111
+ // doing.
126
112
  //
127
113
  // Everything else — the per-device-type rule, the backfill PRECONDITION,
128
- // the privacy obligation, and why the field is split at all — is specified
129
- // once, in the header of src/utils/subscribedDevices.js. Read it there; it
130
- // is deliberately not summarised here.
114
+ // the STALENESS gap, and why the field is split at all — is specified once,
115
+ // in the header of src/utils/subscribedDevices.js. Read it there; it is
116
+ // deliberately not summarised here.
131
117
  subscribedDevices: [subscribedDeviceSchema],
132
118
  currentAlertMode: {
133
119
  type: String,
@@ -196,7 +182,7 @@ const usersSchema = mongoose.Schema(
196
182
  },
197
183
  {
198
184
  timestamps: true,
199
- },
185
+ }
200
186
  );
201
187
 
202
188
  // Cross-field quiet-hours rules (both-or-neither, start ≠ end) delegated to
@@ -39,29 +39,6 @@
39
39
  // this result, you are reintroducing all five.
40
40
  //
41
41
  // ---------------------------------------------------------------------------
42
- // PRECONDITION: THE BACKFILL LANDS BEFORE ANY CONSUMER SWITCHES OVER
43
- // ---------------------------------------------------------------------------
44
- // Resolving at read time is only CORRECT for a user whose `subscribedDevices`
45
- // has already been backfilled from `subscribedMonitors`. A user who predates
46
- // the field carries a full `subscribedMonitors` and no `subscribedDevices` key
47
- // at all on a `.lean()` read — real state, pinned in
48
- // `src/models/tests/users.test.js`. Call `resolveSubscribedMonitorIdsByUser`
49
- // on that user and you get `[]` back: their AQI alerts stop, and nothing
50
- // anywhere errors.
51
- //
52
- // So the ordering is not negotiable. Per environment: backfill
53
- // `subscribedDevices` from `subscribedMonitors` FIRST, verify it, and only
54
- // then point that environment's consumers at this module. Until the backfill
55
- // has run there, consumers keep reading `subscribedMonitors`. Dropping the
56
- // field is the last step of the JA-3110 plan (see "JA-3110 — Retire
57
- // subscribedMonitors" in JustAirService `TODOS.md`), never the first.
58
- //
59
- // This module cannot detect the un-backfilled case for you — an empty
60
- // `subscribedDevices` and "follows nothing" are the same value — which is
61
- // exactly why the ordering has to be an operational guarantee rather than a
62
- // runtime check.
63
- //
64
- // ---------------------------------------------------------------------------
65
42
  // PRIVACY IS THE CALLER'S JOB, AND IT IS NOT OPTIONAL
66
43
  // ---------------------------------------------------------------------------
67
44
  // A Site's `monitorIds` is a raw membership list with no visibility filtering.
@@ -93,7 +70,7 @@ export const SUBSCRIBED_DEVICES_ERRORS = Object.freeze({
93
70
  RESOLVER_RETURNED_NON_ARRAY: "resolver_returned_non_array",
94
71
  RESOLVER_RETURNED_NESTED_ARRAYS: "resolver_returned_nested_arrays",
95
72
  RESOLVER_RETURNED_DOCUMENTS: "resolver_returned_documents",
96
- // resolveSubscribedMonitorIdsByUser only
73
+ // resolveMonitorIdsByUser only
97
74
  MISSING_COHORT_SITE_RESOLVER: "missing_cohort_site_resolver",
98
75
  COHORT_RESOLVER_RETURNED_NON_MAP: "cohort_resolver_returned_non_map",
99
76
  COHORT_USER_MISSING_ID: "cohort_user_missing_id",
@@ -116,36 +93,14 @@ export const SUBSCRIBED_DEVICES_ERRORS = Object.freeze({
116
93
  // "_id",
117
94
  // ).lean();
118
95
  //
119
- // `isActive: true` is an EXACT match, and that asymmetry with the `$ne` above
120
- // is DELIBERATE — do not "fix" it to `{ $ne: false }`. The two clauses answer
121
- // different questions. `isPrivate` is a privacy gate: a monitor with no
122
- // `isPrivate` key was never marked private, so excluding it would hide a
123
- // monitor its followers are entitled to see. `isActive` is an eligibility
124
- // gate: a monitor only earns a place in an alert audience by positively
125
- // asserting it is active. A document with no `isActive` key has made no such
126
- // assertion, and the safe reading of "we don't know" is to leave it out rather
127
- // than alert on it.
128
- //
129
- // So the failure modes are not symmetric and should not be made symmetric.
130
- // Over-including here means alerting people about a monitor nobody confirmed
131
- // is live; under-including means one unconfirmed monitor is missing from an
132
- // audience until someone sets the flag. This predicate takes the second.
133
- //
134
96
  // `monitorState: "Deployed"` is deliberately NOT here. That is an
135
97
  // alerting-eligibility rule, not a visibility rule, and it belongs to the
136
98
  // notification service alone — the org admin page and the session response
137
99
  // must still show a follower the monitors they follow while those monitors sit
138
100
  // in a non-Deployed state. Do not consolidate the two filters; keep
139
101
  // JustAirNotificationService's own `monitorState` predicate where it is.
140
- //
141
- // Frozen at every level, not just the top. `Object.freeze` is shallow, so a
142
- // top-level-only freeze still lets a consumer write
143
- // `VISIBLE_SITE_MONITOR_FILTER.isPrivate.$ne = false` — and because every
144
- // consumer spreads THIS instance, that one write flips the privacy predicate
145
- // for the whole process. `isPrivate` is the only nested object here; freeze it
146
- // too, and freeze any operator object a future clause introduces.
147
102
  export const VISIBLE_SITE_MONITOR_FILTER = Object.freeze({
148
- isPrivate: Object.freeze({ $ne: true }),
103
+ isPrivate: { $ne: true },
149
104
  isActive: true,
150
105
  });
151
106
 
@@ -207,7 +162,7 @@ const validateResolvedSiteMonitorIds = (resolved) => {
207
162
 
208
163
  if (!Array.isArray(resolved)) {
209
164
  const error = new TypeError(
210
- "resolveSiteMonitorIds must resolve to an array of monitor ids",
165
+ "resolveSiteMonitorIds must resolve to an array of monitor ids"
211
166
  );
212
167
  error.code = SUBSCRIBED_DEVICES_ERRORS.RESOLVER_RETURNED_NON_ARRAY;
213
168
  throw error;
@@ -219,7 +174,7 @@ const validateResolvedSiteMonitorIds = (resolved) => {
219
174
  // silently works, two throws a CastError at write time, far from the cause.
220
175
  if (resolved.some(Array.isArray)) {
221
176
  const error = new TypeError(
222
- "resolveSiteMonitorIds must resolve to a flat array of monitor ids",
177
+ "resolveSiteMonitorIds must resolve to a flat array of monitor ids"
223
178
  );
224
179
  error.code = SUBSCRIBED_DEVICES_ERRORS.RESOLVER_RETURNED_NESTED_ARRAYS;
225
180
  throw error;
@@ -235,11 +190,11 @@ const validateResolvedSiteMonitorIds = (resolved) => {
235
190
  // (`Sites.find(..., "monitorIds -_id").lean()`) carries none.
236
191
  if (
237
192
  resolved.some(
238
- (id) => isPlainObject(id) && typeof id.toHexString !== "function",
193
+ (id) => isPlainObject(id) && typeof id.toHexString !== "function"
239
194
  )
240
195
  ) {
241
196
  const error = new TypeError(
242
- "resolveSiteMonitorIds must resolve to monitor ids, not Site documents",
197
+ "resolveSiteMonitorIds must resolve to monitor ids, not Site documents"
243
198
  );
244
199
  error.code = SUBSCRIBED_DEVICES_ERRORS.RESOLVER_RETURNED_DOCUMENTS;
245
200
  throw error;
@@ -265,7 +220,7 @@ const validateResolvedSiteMonitorMap = (resolved) => {
265
220
 
266
221
  if (!(resolved instanceof Map)) {
267
222
  const error = new TypeError(
268
- "resolveVisibleSiteMonitors must resolve to a Map of site id to monitor ids",
223
+ "resolveVisibleSiteMonitors must resolve to a Map of site id to monitor ids"
269
224
  );
270
225
  error.code = SUBSCRIBED_DEVICES_ERRORS.COHORT_RESOLVER_RETURNED_NON_MAP;
271
226
  throw error;
@@ -275,15 +230,15 @@ const validateResolvedSiteMonitorMap = (resolved) => {
275
230
  [...resolved.entries()].map(([siteId, monitorIds]) => [
276
231
  String(siteId),
277
232
  validateResolvedSiteMonitorIds(monitorIds),
278
- ]),
233
+ ])
279
234
  );
280
235
  };
281
236
 
282
- // Resolves one user's subscribedDevices array down to the flat monitor ids
283
- // that make up their AQI-alert audience. Order is deterministic — direct
284
- // `monitor` subscriptions in array order, then Site-derived ids in resolver
285
- // order, first occurrence wins — but carries no meaning; treat the result as a
286
- // set. This is a value to use, not a value to persist (see below).
237
+
238
+ // Returns the monitor ids that should be stored in Users.subscribedMonitors for
239
+ // the given subscribedDevices array. Order is deterministic — direct `monitor`
240
+ // subscriptions in array order, then Site-derived ids in resolver order, first
241
+ // occurrence wins — but carries no meaning; treat the result as a set.
287
242
  //
288
243
  // `subscribedDevices` tolerates undefined/null: a user row written before this
289
244
  // field existed has no key at all on a `.lean()` read (pinned in
@@ -292,21 +247,10 @@ const validateResolvedSiteMonitorMap = (resolved) => {
292
247
  //
293
248
  // `resolveSiteMonitorIds(siteIds)` is called at most once, with every
294
249
  // linked-device id at a time, and must resolve to the flat union of those
295
- // Sites' VISIBLE monitorIds — the ids themselves, not the Site documents.
296
- // `Sites.monitorIds` is a raw membership list, so it goes through
297
- // VISIBLE_SITE_MONITOR_FILTER before it becomes an alert audience; handing
298
- // back `site.monitorIds` untouched is the leak the privacy note above
299
- // describes:
250
+ // Sites' monitorIds — the ids themselves, not the Site documents:
300
251
  //
301
252
  // const sites = await Sites.find({ _id: { $in: siteIds } }, "monitorIds").lean();
302
- // const monitors = await Monitors.find(
303
- // {
304
- // _id: { $in: sites.flatMap((site) => site.monitorIds ?? []) },
305
- // ...VISIBLE_SITE_MONITOR_FILTER,
306
- // },
307
- // "_id",
308
- // ).lean();
309
- // return monitors.map((monitor) => monitor._id);
253
+ // return sites.flatMap((site) => site.monitorIds ?? []);
310
254
  //
311
255
  // Returning the documents is the easy mistake, so it is rejected rather than
312
256
  // merely documented: the return is validated and a document-shaped entry
@@ -326,9 +270,8 @@ const validateResolvedSiteMonitorMap = (resolved) => {
326
270
  // from. Its privacy obligation applies here too: the ids this returns are only
327
271
  // as safe to alert on as the resolver that produced them.
328
272
  //
329
- // For a whole cohort of users, prefer resolveSubscribedMonitorIdsByUser
330
- // below — it does the Sites lookup once for the batch instead of once per
331
- // user.
273
+ // For a whole cohort of users, prefer resolveMonitorIdsByUser below — it does
274
+ // the Sites lookup once for the batch instead of once per user.
332
275
  /**
333
276
  * @param {Array<{ deviceId: import("mongoose").Types.ObjectId,
334
277
  * deviceType: import("../constants/deviceTypes.js").DeviceType }>} [subscribedDevices]
@@ -338,7 +281,7 @@ const validateResolvedSiteMonitorMap = (resolved) => {
338
281
  */
339
282
  export const projectSubscribedMonitors = async (
340
283
  subscribedDevices,
341
- { resolveSiteMonitorIds } = {},
284
+ { resolveSiteMonitorIds } = {}
342
285
  ) => {
343
286
  const entries = (subscribedDevices ?? []).filter(isPlainObject);
344
287
  const direct = directMonitorIds(entries);
@@ -350,14 +293,14 @@ export const projectSubscribedMonitors = async (
350
293
 
351
294
  if (typeof resolveSiteMonitorIds !== "function") {
352
295
  const error = new TypeError(
353
- "projectSubscribedMonitors requires resolveSiteMonitorIds when a linked-device is subscribed",
296
+ "projectSubscribedMonitors requires resolveSiteMonitorIds when a linked-device is subscribed"
354
297
  );
355
298
  error.code = SUBSCRIBED_DEVICES_ERRORS.MISSING_SITE_RESOLVER;
356
299
  throw error;
357
300
  }
358
301
 
359
302
  const siteMonitorIds = validateResolvedSiteMonitorIds(
360
- await resolveSiteMonitorIds(siteIds),
303
+ await resolveSiteMonitorIds(siteIds)
361
304
  );
362
305
  return dedupeById([...direct, ...siteMonitorIds]);
363
306
  };
@@ -404,21 +347,13 @@ export const projectSubscribedMonitors = async (
404
347
  // to nothing, so their alerts stop) is the subscription-identity question
405
348
  // tracked in TODOS, and is deliberately not papered over here.
406
349
  //
407
- // Users are keyed by `String(user._id)`, so callers look up with the same.
408
- // Anything in the cohort that is not a user object carrying an `_id` — a
409
- // missing `_id`, a `null` slot, a bare id string — throws COHORT_USER_MISSING_ID
410
- // rather than being skipped. Skipping would silently remove a real person from
411
- // an alert audience, which is the failure mode this whole module exists to
412
- // avoid, and a caller cannot tell a dropped entry apart from one that resolved
413
- // to nothing.
350
+ // Users are keyed by `String(user._id)`, so callers look up with the same. A
351
+ // user with no `_id` throws rather than being skipped: skipping would silently
352
+ // remove a real person from an alert audience, which is the failure mode this
353
+ // whole module exists to avoid.
414
354
  //
415
355
  // Consumers can shrink the cohort by pushing the AQI-capability predicate into
416
- // the Users query, so rows that can never alert are not loaded at all.
417
- // AQI_CAPABLE_DEVICE_TYPES is the query form of isAqiCapableDeviceType and is
418
- // exported from constants/deviceTypes.js (and the root barrel) for exactly
419
- // this:
420
- //
421
- // import { AQI_CAPABLE_DEVICE_TYPES } from "@justair/justair-library";
356
+ // the Users query, so rows that can never alert are not loaded at all:
422
357
  //
423
358
  // { "subscribedDevices.deviceType": { $in: AQI_CAPABLE_DEVICE_TYPES } }
424
359
  //
@@ -434,26 +369,18 @@ export const projectSubscribedMonitors = async (
434
369
  * Promise<Map<string, import("mongoose").Types.ObjectId[]>> }} [options]
435
370
  * @returns {Promise<Map<string, import("mongoose").Types.ObjectId[]>>}
436
371
  */
437
- export const resolveSubscribedMonitorIdsByUser = async (
372
+ export const resolveMonitorIdsByUser = async (
438
373
  users,
439
- { resolveVisibleSiteMonitors } = {},
374
+ { resolveVisibleSiteMonitors } = {}
440
375
  ) => {
441
- const cohort = users ?? [];
376
+ const cohort = (users ?? []).filter(isPlainObject);
442
377
 
443
- // Every entry must be a user object carrying an _id, and anything else
444
- // throws rather than being filtered out. A `null` slot used to be dropped by
445
- // an isPlainObject filter and then never appear in the returned Map, which
446
- // is the same silent removal from an alert audience that the missing-_id
447
- // check below already rejects — a caller cannot tell "that user follows
448
- // nothing" apart from "that user was quietly discarded". One rule for both:
449
- // if the cohort contains something that is not a resolvable user, say so.
450
- const invalid = cohort.some(
451
- (user) =>
452
- !isPlainObject(user) || user._id === null || user._id === undefined,
378
+ const missing = cohort.find(
379
+ (user) => user._id === null || user._id === undefined
453
380
  );
454
- if (invalid) {
381
+ if (missing) {
455
382
  const error = new TypeError(
456
- "resolveSubscribedMonitorIdsByUser requires every cohort entry to be a user object carrying an _id",
383
+ "resolveMonitorIdsByUser requires every user to carry an _id"
457
384
  );
458
385
  error.code = SUBSCRIBED_DEVICES_ERRORS.COHORT_USER_MISSING_ID;
459
386
  throw error;
@@ -463,21 +390,21 @@ export const resolveSubscribedMonitorIdsByUser = async (
463
390
  // of the batch form. Two users following the same Site cost one entry.
464
391
  const siteIds = dedupeById(
465
392
  cohort.flatMap((user) =>
466
- linkedSiteIds((user.subscribedDevices ?? []).filter(isPlainObject)),
467
- ),
393
+ linkedSiteIds((user.subscribedDevices ?? []).filter(isPlainObject))
394
+ )
468
395
  );
469
396
 
470
397
  let monitorIdsBySite = new Map();
471
398
  if (siteIds.length > 0) {
472
399
  if (typeof resolveVisibleSiteMonitors !== "function") {
473
400
  const error = new TypeError(
474
- "resolveSubscribedMonitorIdsByUser requires resolveVisibleSiteMonitors when anyone in the cohort follows a linked-device",
401
+ "resolveMonitorIdsByUser requires resolveVisibleSiteMonitors when anyone in the cohort follows a linked-device"
475
402
  );
476
403
  error.code = SUBSCRIBED_DEVICES_ERRORS.MISSING_COHORT_SITE_RESOLVER;
477
404
  throw error;
478
405
  }
479
406
  monitorIdsBySite = validateResolvedSiteMonitorMap(
480
- await resolveVisibleSiteMonitors(siteIds),
407
+ await resolveVisibleSiteMonitors(siteIds)
481
408
  );
482
409
  }
483
410
 
@@ -492,11 +419,11 @@ export const resolveSubscribedMonitorIdsByUser = async (
492
419
  cohort.map((user) =>
493
420
  projectSubscribedMonitors(user.subscribedDevices, {
494
421
  resolveSiteMonitorIds,
495
- }),
496
- ),
422
+ })
423
+ )
497
424
  );
498
425
 
499
426
  return new Map(
500
- cohort.map((user, index) => [String(user._id), resolved[index]]),
427
+ cohort.map((user, index) => [String(user._id), resolved[index]])
501
428
  );
502
429
  };
@@ -6,7 +6,7 @@ import {
6
6
  } from '../../constants/deviceTypes.js';
7
7
  import {
8
8
  projectSubscribedMonitors,
9
- resolveSubscribedMonitorIdsByUser,
9
+ resolveMonitorIdsByUser,
10
10
  VISIBLE_SITE_MONITOR_FILTER,
11
11
  SUBSCRIBED_DEVICES_ERRORS,
12
12
  } from '../subscribedDevices.js';
@@ -417,7 +417,7 @@ describe('isAqiCapableDeviceType agrees with the projection', () => {
417
417
  });
418
418
 
419
419
  // ---------------------------------------------------------------------------
420
- // resolveSubscribedMonitorIdsByUser — the cohort form
420
+ // resolveMonitorIdsByUser — the cohort form
421
421
  // ---------------------------------------------------------------------------
422
422
  // The batch form exists for exactly two reasons, and both are asserted here
423
423
  // rather than just described: it must issue ONE Sites lookup for a whole
@@ -439,11 +439,11 @@ const recordingSiteMap = (byId) => {
439
439
 
440
440
  const user = (subscribedDevices) => ({ _id: oid(), subscribedDevices });
441
441
 
442
- describe('resolveSubscribedMonitorIdsByUser', () => {
442
+ describe('resolveMonitorIdsByUser', () => {
443
443
  test('keys the result by String(user._id)', async () => {
444
444
  const u = user([]);
445
445
 
446
- const result = await resolveSubscribedMonitorIdsByUser([u]);
446
+ const result = await resolveMonitorIdsByUser([u]);
447
447
 
448
448
  expect([...result.keys()]).toEqual([String(u._id)]);
449
449
  });
@@ -457,7 +457,7 @@ describe('resolveSubscribedMonitorIdsByUser', () => {
457
457
  user([{ deviceId: m2, deviceType: 'monitor' }]),
458
458
  ];
459
459
 
460
- const result = await resolveSubscribedMonitorIdsByUser([u1, u2], {
460
+ const result = await resolveMonitorIdsByUser([u1, u2], {
461
461
  resolveVisibleSiteMonitors: resolve,
462
462
  });
463
463
 
@@ -475,7 +475,7 @@ describe('resolveSubscribedMonitorIdsByUser', () => {
475
475
  [siteB]: [oid()],
476
476
  });
477
477
 
478
- await resolveSubscribedMonitorIdsByUser(
478
+ await resolveMonitorIdsByUser(
479
479
  [
480
480
  user([{ deviceId: siteA, deviceType: 'linked-device' }]),
481
481
  user([{ deviceId: siteA, deviceType: 'linked-device' }]),
@@ -499,7 +499,7 @@ describe('resolveSubscribedMonitorIdsByUser', () => {
499
499
  user([{ deviceId: siteB, deviceType: 'linked-device' }]),
500
500
  ];
501
501
 
502
- const result = await resolveSubscribedMonitorIdsByUser([ua, ub], {
502
+ const result = await resolveMonitorIdsByUser([ua, ub], {
503
503
  resolveVisibleSiteMonitors: resolve,
504
504
  });
505
505
 
@@ -516,7 +516,7 @@ describe('resolveSubscribedMonitorIdsByUser', () => {
516
516
  { deviceId: siteId, deviceType: 'linked-device' },
517
517
  ]);
518
518
 
519
- const result = await resolveSubscribedMonitorIdsByUser([u], {
519
+ const result = await resolveMonitorIdsByUser([u], {
520
520
  resolveVisibleSiteMonitors: resolve,
521
521
  });
522
522
 
@@ -526,7 +526,7 @@ describe('resolveSubscribedMonitorIdsByUser', () => {
526
526
  test('a sample-site follower resolves to no monitors', async () => {
527
527
  const u = user([{ deviceId: oid(), deviceType: 'sample-site' }]);
528
528
 
529
- const result = await resolveSubscribedMonitorIdsByUser([u]);
529
+ const result = await resolveMonitorIdsByUser([u]);
530
530
 
531
531
  expect(result.get(String(u._id))).toEqual([]);
532
532
  });
@@ -538,7 +538,7 @@ describe('resolveSubscribedMonitorIdsByUser', () => {
538
538
  const { resolve } = recordingSiteMap({});
539
539
  const u = user([{ deviceId: deletedSite, deviceType: 'linked-device' }]);
540
540
 
541
- const result = await resolveSubscribedMonitorIdsByUser([u], {
541
+ const result = await resolveMonitorIdsByUser([u], {
542
542
  resolveVisibleSiteMonitors: resolve,
543
543
  });
544
544
 
@@ -553,7 +553,7 @@ describe('resolveSubscribedMonitorIdsByUser', () => {
553
553
  const monitorId = oid();
554
554
  const u = user([{ deviceId: siteId, deviceType: 'linked-device' }]);
555
555
 
556
- const result = await resolveSubscribedMonitorIdsByUser([u], {
556
+ const result = await resolveMonitorIdsByUser([u], {
557
557
  resolveVisibleSiteMonitors: async () => new Map([[siteId, [monitorId]]]),
558
558
  });
559
559
 
@@ -563,21 +563,21 @@ describe('resolveSubscribedMonitorIdsByUser', () => {
563
563
  test('tolerates a user with no subscribedDevices key at all', async () => {
564
564
  const u = { _id: oid() };
565
565
 
566
- const result = await resolveSubscribedMonitorIdsByUser([u]);
566
+ const result = await resolveMonitorIdsByUser([u]);
567
567
 
568
568
  expect(result.get(String(u._id))).toEqual([]);
569
569
  });
570
570
 
571
571
  test('an empty or nullish cohort resolves to an empty Map', async () => {
572
- expect(await resolveSubscribedMonitorIdsByUser([])).toEqual(new Map());
573
- expect(await resolveSubscribedMonitorIdsByUser(undefined)).toEqual(new Map());
572
+ expect(await resolveMonitorIdsByUser([])).toEqual(new Map());
573
+ expect(await resolveMonitorIdsByUser(undefined)).toEqual(new Map());
574
574
  });
575
575
  });
576
576
 
577
- describe('resolveSubscribedMonitorIdsByUser — rejects what would otherwise fail silently', () => {
577
+ describe('resolveMonitorIdsByUser — rejects what would otherwise fail silently', () => {
578
578
  test('throws MISSING_COHORT_SITE_RESOLVER when anyone follows a linked-device', async () => {
579
579
  await expect(
580
- resolveSubscribedMonitorIdsByUser([
580
+ resolveMonitorIdsByUser([
581
581
  user([{ deviceId: oid(), deviceType: 'linked-device' }]),
582
582
  ]),
583
583
  ).rejects.toMatchObject({
@@ -589,7 +589,7 @@ describe('resolveSubscribedMonitorIdsByUser — rejects what would otherwise fai
589
589
  const monitorId = oid();
590
590
  const u = user([{ deviceId: monitorId, deviceType: 'monitor' }]);
591
591
 
592
- const result = await resolveSubscribedMonitorIdsByUser([u]);
592
+ const result = await resolveMonitorIdsByUser([u]);
593
593
 
594
594
  expect(result.get(String(u._id))).toEqual([monitorId]);
595
595
  });
@@ -599,7 +599,7 @@ describe('resolveSubscribedMonitorIdsByUser — rejects what would otherwise fai
599
599
  const siteId = oid();
600
600
 
601
601
  await expect(
602
- resolveSubscribedMonitorIdsByUser(
602
+ resolveMonitorIdsByUser(
603
603
  [user([{ deviceId: siteId, deviceType: 'linked-device' }])],
604
604
  { resolveVisibleSiteMonitors: async () => [[String(siteId), [oid()]]] },
605
605
  ),
@@ -611,7 +611,7 @@ describe('resolveSubscribedMonitorIdsByUser — rejects what would otherwise fai
611
611
  test('a nullish resolver return is an empty audience, not a throw', async () => {
612
612
  const u = user([{ deviceId: oid(), deviceType: 'linked-device' }]);
613
613
 
614
- const result = await resolveSubscribedMonitorIdsByUser([u], {
614
+ const result = await resolveMonitorIdsByUser([u], {
615
615
  resolveVisibleSiteMonitors: async () => null,
616
616
  });
617
617
 
@@ -625,7 +625,7 @@ describe('resolveSubscribedMonitorIdsByUser — rejects what would otherwise fai
625
625
  const siteId = oid();
626
626
 
627
627
  await expect(
628
- resolveSubscribedMonitorIdsByUser(
628
+ resolveMonitorIdsByUser(
629
629
  [user([{ deviceId: siteId, deviceType: 'linked-device' }])],
630
630
  {
631
631
  resolveVisibleSiteMonitors: async () =>
@@ -641,23 +641,7 @@ describe('resolveSubscribedMonitorIdsByUser — rejects what would otherwise fai
641
641
  // Skipping would remove a real person from an alert audience with no error
642
642
  // anywhere — the exact failure mode this module exists to prevent.
643
643
  await expect(
644
- resolveSubscribedMonitorIdsByUser([{ subscribedDevices: [] }]),
645
- ).rejects.toMatchObject({
646
- code: SUBSCRIBED_DEVICES_ERRORS.COHORT_USER_MISSING_ID,
647
- });
648
- });
649
-
650
- test.each([
651
- ['a null slot', null],
652
- ['an undefined slot', undefined],
653
- ['a bare id string', 'deadbeefdeadbeefdeadbeef'],
654
- ['an array', []],
655
- ])('throws COHORT_USER_MISSING_ID for %s in the cohort', async (_label, entry) => {
656
- // A non-object entry used to be filtered out and then simply never appear
657
- // in the returned Map — the same silent removal the missing-_id case above
658
- // rejects, and indistinguishable to the caller from "follows nothing".
659
- await expect(
660
- resolveSubscribedMonitorIdsByUser([user([]), entry]),
644
+ resolveMonitorIdsByUser([{ subscribedDevices: [] }]),
661
645
  ).rejects.toMatchObject({
662
646
  code: SUBSCRIBED_DEVICES_ERRORS.COHORT_USER_MISSING_ID,
663
647
  });
@@ -665,35 +649,17 @@ describe('resolveSubscribedMonitorIdsByUser — rejects what would otherwise fai
665
649
  });
666
650
 
667
651
  describe('VISIBLE_SITE_MONITOR_FILTER', () => {
668
- test('privacy uses $ne, eligibility uses an exact match — deliberately', () => {
669
- // The asymmetry is the point, and this test exists to stop it being
670
- // "fixed". isPrivate is a privacy gate: $ne keeps a monitor that was never
671
- // marked private, which its followers are entitled to see. isActive is an
672
- // eligibility gate: a monitor earns a place in an alert audience only by
673
- // positively asserting it is active, so a document that has made no such
674
- // assertion stays out. Confirmed product decision — flipping either half
675
- // is a behaviour change, not a cleanup.
652
+ test('excludes private monitors with $ne: true, not an exact false match', () => {
653
+ // $ne matches both `false` and a MISSING key. Documents written before
654
+ // isPrivate existed carry no key, and an exact { isPrivate: false } match
655
+ // would drop them from every Site's audience.
676
656
  expect(VISIBLE_SITE_MONITOR_FILTER).toEqual({
677
657
  isPrivate: { $ne: true },
678
658
  isActive: true,
679
659
  });
680
660
  });
681
661
 
682
- test('is frozen all the way down — one consumer must not change it for the rest', () => {
683
- // Object.freeze is shallow, so the top-level assertion alone would pass
684
- // while `FILTER.isPrivate.$ne = false` still silently flipped the privacy
685
- // predicate for every consumer that spreads this instance.
662
+ test('is frozen — one consumer mutating it must not change it for the rest', () => {
686
663
  expect(Object.isFrozen(VISIBLE_SITE_MONITOR_FILTER)).toBe(true);
687
- expect(Object.isFrozen(VISIBLE_SITE_MONITOR_FILTER.isPrivate)).toBe(true);
688
- });
689
-
690
- test('a nested write is rejected rather than shared with every consumer', () => {
691
- // ES modules are strict, so a write to a frozen property throws rather
692
- // than failing silently.
693
- expect(() => {
694
- VISIBLE_SITE_MONITOR_FILTER.isPrivate.$ne = false;
695
- }).toThrow(TypeError);
696
-
697
- expect(VISIBLE_SITE_MONITOR_FILTER.isPrivate.$ne).toBe(true);
698
664
  });
699
665
  });
@@ -9,7 +9,7 @@
9
9
  // deliberate contract change, not a test fix.
10
10
  import {
11
11
  projectSubscribedMonitors,
12
- resolveSubscribedMonitorIdsByUser,
12
+ resolveMonitorIdsByUser,
13
13
  VISIBLE_SITE_MONITOR_FILTER,
14
14
  SUBSCRIBED_DEVICES_ERRORS,
15
15
  } from '../subscribedDevices.js';
@@ -17,14 +17,13 @@ import {
17
17
  DEVICE_TYPES,
18
18
  DEVICE_TYPE_VALUES,
19
19
  isAqiCapableDeviceType,
20
- AQI_CAPABLE_DEVICE_TYPES,
21
20
  } from '../../constants/deviceTypes.js';
22
21
 
23
22
  test('barrel re-exports the subscribedDevices surface unchanged (identity, not copies)', async () => {
24
23
  const barrel = await import('../../index.js');
25
24
 
26
25
  expect(barrel.projectSubscribedMonitors).toBe(projectSubscribedMonitors);
27
- expect(barrel.resolveSubscribedMonitorIdsByUser).toBe(resolveSubscribedMonitorIdsByUser);
26
+ expect(barrel.resolveMonitorIdsByUser).toBe(resolveMonitorIdsByUser);
28
27
  expect(barrel.VISIBLE_SITE_MONITOR_FILTER).toBe(VISIBLE_SITE_MONITOR_FILTER);
29
28
  expect(barrel.SUBSCRIBED_DEVICES_ERRORS).toBe(SUBSCRIBED_DEVICES_ERRORS);
30
29
  });
@@ -40,5 +39,4 @@ test('barrel re-exports the device-type vocabulary unchanged (identity, not copi
40
39
  expect(barrel.DEVICE_TYPES).toBe(DEVICE_TYPES);
41
40
  expect(barrel.DEVICE_TYPE_VALUES).toBe(DEVICE_TYPE_VALUES);
42
41
  expect(barrel.isAqiCapableDeviceType).toBe(isAqiCapableDeviceType);
43
- expect(barrel.AQI_CAPABLE_DEVICE_TYPES).toBe(AQI_CAPABLE_DEVICE_TYPES);
44
42
  });