@justair/justair-library 6.12.0 → 6.13.0-alpha.ef692bc

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.
Files changed (30) hide show
  1. package/dist/constants/alertLevels.d.ts +10 -0
  2. package/dist/constants/alertLevels.d.ts.map +1 -0
  3. package/dist/constants/index.d.ts +4 -0
  4. package/dist/constants/index.d.ts.map +1 -0
  5. package/dist/constants/tests/constantsBarrel.test.d.ts +2 -0
  6. package/dist/constants/tests/constantsBarrel.test.d.ts.map +1 -0
  7. package/dist/constants/userNotifications.d.ts +4 -0
  8. package/dist/constants/userNotifications.d.ts.map +1 -0
  9. package/dist/index.d.ts +14 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/models/monitorEpisodes.d.ts +266 -0
  12. package/dist/models/monitorEpisodes.d.ts.map +1 -0
  13. package/dist/models/monitors.d.ts.map +1 -1
  14. package/dist/models/tests/monitorEpisodes.test.d.ts +2 -0
  15. package/dist/models/tests/monitorEpisodes.test.d.ts.map +1 -0
  16. package/dist/models/tests/monitorsLeftGoodAt.test.d.ts +2 -0
  17. package/dist/models/tests/monitorsLeftGoodAt.test.d.ts.map +1 -0
  18. package/dist/models/tests/userNotifications.test.d.ts +2 -0
  19. package/dist/models/tests/userNotifications.test.d.ts.map +1 -0
  20. package/dist/models/userNotifications.d.ts +175 -0
  21. package/dist/models/userNotifications.d.ts.map +1 -0
  22. package/package.json +3 -3
  23. package/src/constants/alertLevels.js +24 -0
  24. package/src/constants/index.js +12 -0
  25. package/src/constants/tests/constantsBarrel.test.js +46 -0
  26. package/src/constants/userNotifications.js +22 -0
  27. package/src/index.js +31 -0
  28. package/src/models/monitorEpisodes.js +80 -0
  29. package/src/models/monitors.js +6 -0
  30. package/src/models/userNotifications.js +121 -0
@@ -0,0 +1,10 @@
1
+ export const ALERT_LEVEL_WEIGHTS: {
2
+ Good: number;
3
+ Moderate: number;
4
+ "Unhealthy for SG": number;
5
+ Unhealthy: number;
6
+ "Very Unhealthy": number;
7
+ Hazardous: number;
8
+ };
9
+ export function getAlertWeight(alertLevel: any): any;
10
+ //# sourceMappingURL=alertLevels.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"alertLevels.d.ts","sourceRoot":"","sources":["../../src/constants/alertLevels.js"],"names":[],"mappings":"AAQA;;;;;;;EAOE;AAOK,qDACsD"}
@@ -0,0 +1,4 @@
1
+ export * from "./pollutants.js";
2
+ export * from "./alertLevels.js";
3
+ export * from "./userNotifications.js";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/constants/index.js"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=constantsBarrel.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constantsBarrel.test.d.ts","sourceRoot":"","sources":["../../../src/constants/tests/constantsBarrel.test.js"],"names":[],"mappings":""}
@@ -0,0 +1,4 @@
1
+ export const NOTIFICATION_KINDS: string[];
2
+ export const NOTIFICATION_CHANNELS: string[];
3
+ export const DELIVERY_STATUSES: string[];
4
+ //# sourceMappingURL=userNotifications.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"userNotifications.d.ts","sourceRoot":"","sources":["../../src/constants/userNotifications.js"],"names":[],"mappings":"AAYA,0CAKE;AAEF,6CAAwE;AAExE,yCAA8D"}
package/dist/index.d.ts CHANGED
@@ -75,8 +75,21 @@ import { SamplesAudit } from "./models/samples.js";
75
75
  import { sampleParameterReferenceConcentrations } from "./models/samples.js";
76
76
  import { passwordResetTokenSchema } from "./models/passwordResetToken.js";
77
77
  import { PasswordResetToken } from "./models/passwordResetToken.js";
78
+ import { monitorEpisodesSchema } from "./models/monitorEpisodes.js";
79
+ import { MonitorEpisode } from "./models/monitorEpisodes.js";
80
+ import { userNotificationsSchema } from "./models/userNotifications.js";
81
+ import { UserNotification } from "./models/userNotifications.js";
82
+ import { AqiAlertNotification } from "./models/userNotifications.js";
83
+ import { DeviceOfflineNotification } from "./models/userNotifications.js";
84
+ import { WeeklyDigestNotification } from "./models/userNotifications.js";
85
+ import { AccountNotification } from "./models/userNotifications.js";
86
+ import { NOTIFICATION_KINDS } from "./models/userNotifications.js";
87
+ import { NOTIFICATION_CHANNELS } from "./models/userNotifications.js";
88
+ import { DELIVERY_STATUSES } from "./models/userNotifications.js";
78
89
  import { PARAMETERS } from "./constants/pollutants.js";
79
90
  import { HEAVY_METALS } from "./constants/pollutants.js";
91
+ import { ALERT_LEVEL_WEIGHTS } from "./constants/alertLevels.js";
92
+ import { getAlertWeight } from "./constants/alertLevels.js";
80
93
  import { splitName } from "./utils/splitName.js";
81
- 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, PARAMETERS, HEAVY_METALS, splitName };
94
+ 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 };
82
95
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.js"],"names":[],"mappings":"AA4FA;;;iBAEC;yBA5BwB,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;4BAOT,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;2BAvBpD,2BAA2B;6BAA3B,2BAA2B;0BAC1C,sBAAsB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.js"],"names":[],"mappings":"AA4GA;;;iBAEC;yBA5CwB,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;4BAuBT,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;sCAlCtF,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"}
@@ -0,0 +1,266 @@
1
+ export const monitorEpisodesSchema: mongoose.Schema<any, mongoose.Model<any, any, any, any, any, any>, {}, {}, {}, {}, {
2
+ timestamps: true;
3
+ }, {
4
+ createdAt: NativeDate;
5
+ updatedAt: NativeDate;
6
+ } & {
7
+ user: {
8
+ prototype?: mongoose.Types.ObjectId;
9
+ cacheHexString?: unknown;
10
+ generate?: {};
11
+ createFromTime?: {};
12
+ createFromHexString?: {};
13
+ createFromBase64?: {};
14
+ isValid?: {};
15
+ };
16
+ monitor: {
17
+ prototype?: mongoose.Types.ObjectId;
18
+ cacheHexString?: unknown;
19
+ generate?: {};
20
+ createFromTime?: {};
21
+ createFromHexString?: {};
22
+ createFromBase64?: {};
23
+ isValid?: {};
24
+ };
25
+ open: boolean;
26
+ startedAt: Date;
27
+ alertCount: number;
28
+ firstNotifiedAt?: Date;
29
+ endedAt?: Date;
30
+ peakStatus?: string;
31
+ }, mongoose.Document<unknown, {}, mongoose.FlatRecord<{
32
+ createdAt: NativeDate;
33
+ updatedAt: NativeDate;
34
+ } & {
35
+ user: {
36
+ prototype?: mongoose.Types.ObjectId;
37
+ cacheHexString?: unknown;
38
+ generate?: {};
39
+ createFromTime?: {};
40
+ createFromHexString?: {};
41
+ createFromBase64?: {};
42
+ isValid?: {};
43
+ };
44
+ monitor: {
45
+ prototype?: mongoose.Types.ObjectId;
46
+ cacheHexString?: unknown;
47
+ generate?: {};
48
+ createFromTime?: {};
49
+ createFromHexString?: {};
50
+ createFromBase64?: {};
51
+ isValid?: {};
52
+ };
53
+ open: boolean;
54
+ startedAt: Date;
55
+ alertCount: number;
56
+ firstNotifiedAt?: Date;
57
+ endedAt?: Date;
58
+ peakStatus?: string;
59
+ }>> & mongoose.FlatRecord<{
60
+ createdAt: NativeDate;
61
+ updatedAt: NativeDate;
62
+ } & {
63
+ user: {
64
+ prototype?: mongoose.Types.ObjectId;
65
+ cacheHexString?: unknown;
66
+ generate?: {};
67
+ createFromTime?: {};
68
+ createFromHexString?: {};
69
+ createFromBase64?: {};
70
+ isValid?: {};
71
+ };
72
+ monitor: {
73
+ prototype?: mongoose.Types.ObjectId;
74
+ cacheHexString?: unknown;
75
+ generate?: {};
76
+ createFromTime?: {};
77
+ createFromHexString?: {};
78
+ createFromBase64?: {};
79
+ isValid?: {};
80
+ };
81
+ open: boolean;
82
+ startedAt: Date;
83
+ alertCount: number;
84
+ firstNotifiedAt?: Date;
85
+ endedAt?: Date;
86
+ peakStatus?: string;
87
+ }> & {
88
+ _id: mongoose.Types.ObjectId;
89
+ }>;
90
+ export const MonitorEpisode: mongoose.Model<{
91
+ createdAt: NativeDate;
92
+ updatedAt: NativeDate;
93
+ } & {
94
+ user: {
95
+ prototype?: mongoose.Types.ObjectId;
96
+ cacheHexString?: unknown;
97
+ generate?: {};
98
+ createFromTime?: {};
99
+ createFromHexString?: {};
100
+ createFromBase64?: {};
101
+ isValid?: {};
102
+ };
103
+ monitor: {
104
+ prototype?: mongoose.Types.ObjectId;
105
+ cacheHexString?: unknown;
106
+ generate?: {};
107
+ createFromTime?: {};
108
+ createFromHexString?: {};
109
+ createFromBase64?: {};
110
+ isValid?: {};
111
+ };
112
+ open: boolean;
113
+ startedAt: Date;
114
+ alertCount: number;
115
+ firstNotifiedAt?: Date;
116
+ endedAt?: Date;
117
+ peakStatus?: string;
118
+ }, {}, {}, {}, mongoose.Document<unknown, {}, {
119
+ createdAt: NativeDate;
120
+ updatedAt: NativeDate;
121
+ } & {
122
+ user: {
123
+ prototype?: mongoose.Types.ObjectId;
124
+ cacheHexString?: unknown;
125
+ generate?: {};
126
+ createFromTime?: {};
127
+ createFromHexString?: {};
128
+ createFromBase64?: {};
129
+ isValid?: {};
130
+ };
131
+ monitor: {
132
+ prototype?: mongoose.Types.ObjectId;
133
+ cacheHexString?: unknown;
134
+ generate?: {};
135
+ createFromTime?: {};
136
+ createFromHexString?: {};
137
+ createFromBase64?: {};
138
+ isValid?: {};
139
+ };
140
+ open: boolean;
141
+ startedAt: Date;
142
+ alertCount: number;
143
+ firstNotifiedAt?: Date;
144
+ endedAt?: Date;
145
+ peakStatus?: string;
146
+ }> & {
147
+ createdAt: NativeDate;
148
+ updatedAt: NativeDate;
149
+ } & {
150
+ user: {
151
+ prototype?: mongoose.Types.ObjectId;
152
+ cacheHexString?: unknown;
153
+ generate?: {};
154
+ createFromTime?: {};
155
+ createFromHexString?: {};
156
+ createFromBase64?: {};
157
+ isValid?: {};
158
+ };
159
+ monitor: {
160
+ prototype?: mongoose.Types.ObjectId;
161
+ cacheHexString?: unknown;
162
+ generate?: {};
163
+ createFromTime?: {};
164
+ createFromHexString?: {};
165
+ createFromBase64?: {};
166
+ isValid?: {};
167
+ };
168
+ open: boolean;
169
+ startedAt: Date;
170
+ alertCount: number;
171
+ firstNotifiedAt?: Date;
172
+ endedAt?: Date;
173
+ peakStatus?: string;
174
+ } & {
175
+ _id: mongoose.Types.ObjectId;
176
+ }, mongoose.Schema<any, mongoose.Model<any, any, any, any, any, any>, {}, {}, {}, {}, {
177
+ timestamps: true;
178
+ }, {
179
+ createdAt: NativeDate;
180
+ updatedAt: NativeDate;
181
+ } & {
182
+ user: {
183
+ prototype?: mongoose.Types.ObjectId;
184
+ cacheHexString?: unknown;
185
+ generate?: {};
186
+ createFromTime?: {};
187
+ createFromHexString?: {};
188
+ createFromBase64?: {};
189
+ isValid?: {};
190
+ };
191
+ monitor: {
192
+ prototype?: mongoose.Types.ObjectId;
193
+ cacheHexString?: unknown;
194
+ generate?: {};
195
+ createFromTime?: {};
196
+ createFromHexString?: {};
197
+ createFromBase64?: {};
198
+ isValid?: {};
199
+ };
200
+ open: boolean;
201
+ startedAt: Date;
202
+ alertCount: number;
203
+ firstNotifiedAt?: Date;
204
+ endedAt?: Date;
205
+ peakStatus?: string;
206
+ }, mongoose.Document<unknown, {}, mongoose.FlatRecord<{
207
+ createdAt: NativeDate;
208
+ updatedAt: NativeDate;
209
+ } & {
210
+ user: {
211
+ prototype?: mongoose.Types.ObjectId;
212
+ cacheHexString?: unknown;
213
+ generate?: {};
214
+ createFromTime?: {};
215
+ createFromHexString?: {};
216
+ createFromBase64?: {};
217
+ isValid?: {};
218
+ };
219
+ monitor: {
220
+ prototype?: mongoose.Types.ObjectId;
221
+ cacheHexString?: unknown;
222
+ generate?: {};
223
+ createFromTime?: {};
224
+ createFromHexString?: {};
225
+ createFromBase64?: {};
226
+ isValid?: {};
227
+ };
228
+ open: boolean;
229
+ startedAt: Date;
230
+ alertCount: number;
231
+ firstNotifiedAt?: Date;
232
+ endedAt?: Date;
233
+ peakStatus?: string;
234
+ }>> & mongoose.FlatRecord<{
235
+ createdAt: NativeDate;
236
+ updatedAt: NativeDate;
237
+ } & {
238
+ user: {
239
+ prototype?: mongoose.Types.ObjectId;
240
+ cacheHexString?: unknown;
241
+ generate?: {};
242
+ createFromTime?: {};
243
+ createFromHexString?: {};
244
+ createFromBase64?: {};
245
+ isValid?: {};
246
+ };
247
+ monitor: {
248
+ prototype?: mongoose.Types.ObjectId;
249
+ cacheHexString?: unknown;
250
+ generate?: {};
251
+ createFromTime?: {};
252
+ createFromHexString?: {};
253
+ createFromBase64?: {};
254
+ isValid?: {};
255
+ };
256
+ open: boolean;
257
+ startedAt: Date;
258
+ alertCount: number;
259
+ firstNotifiedAt?: Date;
260
+ endedAt?: Date;
261
+ peakStatus?: string;
262
+ }> & {
263
+ _id: mongoose.Types.ObjectId;
264
+ }>>;
265
+ import mongoose from "mongoose";
266
+ //# sourceMappingURL=monitorEpisodes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"monitorEpisodes.d.ts","sourceRoot":"","sources":["../../src/models/monitorEpisodes.js"],"names":[],"mappings":"AA8BA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoBE;AA2BF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAA+E;qBA7E1D,UAAU"}
@@ -1 +1 @@
1
- {"version":3,"file":"monitors.d.ts","sourceRoot":"","sources":["../../src/models/monitors.js"],"names":[],"mappings":"AA4KA,iCA6IE;AAuIF,2BAA4D;AA3T5D,qCAiCE;AAGF,+BAAwE;AAtKxE,sCAAoD"}
1
+ {"version":3,"file":"monitors.d.ts","sourceRoot":"","sources":["../../src/models/monitors.js"],"names":[],"mappings":"AA4KA,iCAmJE;AAuIF,2BAA4D;AAjU5D,qCAiCE;AAGF,+BAAwE;AAtKxE,sCAAoD"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=monitorEpisodes.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"monitorEpisodes.test.d.ts","sourceRoot":"","sources":["../../../src/models/tests/monitorEpisodes.test.js"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=monitorsLeftGoodAt.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"monitorsLeftGoodAt.test.d.ts","sourceRoot":"","sources":["../../../src/models/tests/monitorsLeftGoodAt.test.js"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=userNotifications.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"userNotifications.test.d.ts","sourceRoot":"","sources":["../../../src/models/tests/userNotifications.test.js"],"names":[],"mappings":""}
@@ -0,0 +1,175 @@
1
+ declare const baseSchema: mongoose.Schema<any, mongoose.Model<any, any, any, any, any, any>, {}, {}, {}, {}, {
2
+ timestamps: true;
3
+ discriminatorKey: string;
4
+ }, {
5
+ createdAt: NativeDate;
6
+ updatedAt: NativeDate;
7
+ } & {
8
+ user: {
9
+ prototype?: mongoose.Types.ObjectId;
10
+ cacheHexString?: unknown;
11
+ generate?: {};
12
+ createFromTime?: {};
13
+ createFromHexString?: {};
14
+ createFromBase64?: {};
15
+ isValid?: {};
16
+ };
17
+ channel: string;
18
+ status: string;
19
+ sentAt: Date;
20
+ message?: string;
21
+ }, mongoose.Document<unknown, {}, mongoose.FlatRecord<{
22
+ createdAt: NativeDate;
23
+ updatedAt: NativeDate;
24
+ } & {
25
+ user: {
26
+ prototype?: mongoose.Types.ObjectId;
27
+ cacheHexString?: unknown;
28
+ generate?: {};
29
+ createFromTime?: {};
30
+ createFromHexString?: {};
31
+ createFromBase64?: {};
32
+ isValid?: {};
33
+ };
34
+ channel: string;
35
+ status: string;
36
+ sentAt: Date;
37
+ message?: string;
38
+ }>> & mongoose.FlatRecord<{
39
+ createdAt: NativeDate;
40
+ updatedAt: NativeDate;
41
+ } & {
42
+ user: {
43
+ prototype?: mongoose.Types.ObjectId;
44
+ cacheHexString?: unknown;
45
+ generate?: {};
46
+ createFromTime?: {};
47
+ createFromHexString?: {};
48
+ createFromBase64?: {};
49
+ isValid?: {};
50
+ };
51
+ channel: string;
52
+ status: string;
53
+ sentAt: Date;
54
+ message?: string;
55
+ }> & {
56
+ _id: mongoose.Types.ObjectId;
57
+ }>;
58
+ export const UserNotification: mongoose.Model<{
59
+ createdAt: NativeDate;
60
+ updatedAt: NativeDate;
61
+ } & {
62
+ user: {
63
+ prototype?: mongoose.Types.ObjectId;
64
+ cacheHexString?: unknown;
65
+ generate?: {};
66
+ createFromTime?: {};
67
+ createFromHexString?: {};
68
+ createFromBase64?: {};
69
+ isValid?: {};
70
+ };
71
+ channel: string;
72
+ status: string;
73
+ sentAt: Date;
74
+ message?: string;
75
+ }, {}, {}, {}, mongoose.Document<unknown, {}, {
76
+ createdAt: NativeDate;
77
+ updatedAt: NativeDate;
78
+ } & {
79
+ user: {
80
+ prototype?: mongoose.Types.ObjectId;
81
+ cacheHexString?: unknown;
82
+ generate?: {};
83
+ createFromTime?: {};
84
+ createFromHexString?: {};
85
+ createFromBase64?: {};
86
+ isValid?: {};
87
+ };
88
+ channel: string;
89
+ status: string;
90
+ sentAt: Date;
91
+ message?: string;
92
+ }> & {
93
+ createdAt: NativeDate;
94
+ updatedAt: NativeDate;
95
+ } & {
96
+ user: {
97
+ prototype?: mongoose.Types.ObjectId;
98
+ cacheHexString?: unknown;
99
+ generate?: {};
100
+ createFromTime?: {};
101
+ createFromHexString?: {};
102
+ createFromBase64?: {};
103
+ isValid?: {};
104
+ };
105
+ channel: string;
106
+ status: string;
107
+ sentAt: Date;
108
+ message?: string;
109
+ } & {
110
+ _id: mongoose.Types.ObjectId;
111
+ }, mongoose.Schema<any, mongoose.Model<any, any, any, any, any, any>, {}, {}, {}, {}, {
112
+ timestamps: true;
113
+ discriminatorKey: string;
114
+ }, {
115
+ createdAt: NativeDate;
116
+ updatedAt: NativeDate;
117
+ } & {
118
+ user: {
119
+ prototype?: mongoose.Types.ObjectId;
120
+ cacheHexString?: unknown;
121
+ generate?: {};
122
+ createFromTime?: {};
123
+ createFromHexString?: {};
124
+ createFromBase64?: {};
125
+ isValid?: {};
126
+ };
127
+ channel: string;
128
+ status: string;
129
+ sentAt: Date;
130
+ message?: string;
131
+ }, mongoose.Document<unknown, {}, mongoose.FlatRecord<{
132
+ createdAt: NativeDate;
133
+ updatedAt: NativeDate;
134
+ } & {
135
+ user: {
136
+ prototype?: mongoose.Types.ObjectId;
137
+ cacheHexString?: unknown;
138
+ generate?: {};
139
+ createFromTime?: {};
140
+ createFromHexString?: {};
141
+ createFromBase64?: {};
142
+ isValid?: {};
143
+ };
144
+ channel: string;
145
+ status: string;
146
+ sentAt: Date;
147
+ message?: string;
148
+ }>> & mongoose.FlatRecord<{
149
+ createdAt: NativeDate;
150
+ updatedAt: NativeDate;
151
+ } & {
152
+ user: {
153
+ prototype?: mongoose.Types.ObjectId;
154
+ cacheHexString?: unknown;
155
+ generate?: {};
156
+ createFromTime?: {};
157
+ createFromHexString?: {};
158
+ createFromBase64?: {};
159
+ isValid?: {};
160
+ };
161
+ channel: string;
162
+ status: string;
163
+ sentAt: Date;
164
+ message?: string;
165
+ }> & {
166
+ _id: mongoose.Types.ObjectId;
167
+ }>>;
168
+ export const AqiAlertNotification: mongoose.Model<any, {}, {}, {}, any, any>;
169
+ export const DeviceOfflineNotification: mongoose.Model<any, {}, {}, {}, any, any>;
170
+ export const WeeklyDigestNotification: mongoose.Model<any, {}, {}, {}, any, any>;
171
+ export const AccountNotification: mongoose.Model<any, {}, {}, {}, any, any>;
172
+ import mongoose from "mongoose";
173
+ export { baseSchema as userNotificationsSchema };
174
+ export { NOTIFICATION_KINDS, NOTIFICATION_CHANNELS, DELIVERY_STATUSES } from "../constants/userNotifications.js";
175
+ //# sourceMappingURL=userNotifications.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"userNotifications.d.ts","sourceRoot":"","sources":["../../src/models/userNotifications.js"],"names":[],"mappings":"AA6BA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuBE;AAQF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAAwE;AAgBxE,6EAGE;AAOF,kFAGE;AAQF,iFAGE;AAGF,4EAGE;qBA1GmB,UAAU"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@justair/justair-library",
3
- "version": "6.12.0",
3
+ "version": "6.13.0-alpha.ef692bc",
4
4
  "description": "JustAir Internal Library",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -11,8 +11,8 @@
11
11
  "default": "./src/index.js"
12
12
  },
13
13
  "./constants": {
14
- "types": "./dist/constants/pollutants.d.ts",
15
- "default": "./src/constants/pollutants.js"
14
+ "types": "./dist/constants/index.d.ts",
15
+ "default": "./src/constants/index.js"
16
16
  },
17
17
  "./auth": {
18
18
  "types": "./dist/modules/auth/index.d.ts",
@@ -0,0 +1,24 @@
1
+ // AQI alert-level hierarchy — single source of truth (JA-2658, eng review
2
+ // issue 7). Consumers: JustAirNotificationService statusAlerts threshold
3
+ // logic, CalculateAQIService determineHighestAlertLevel, and the
4
+ // MonitorEpisode/UserNotification schema enums.
5
+ //
6
+ // DO NOT change these values — the integers map directly to user
7
+ // alertPreferenceLevel thresholds and Courier email template selection.
8
+
9
+ export const ALERT_LEVEL_WEIGHTS = {
10
+ Good: 1,
11
+ Moderate: 2,
12
+ "Unhealthy for SG": 3,
13
+ Unhealthy: 4,
14
+ "Very Unhealthy": 5,
15
+ Hazardous: 6,
16
+ };
17
+
18
+ // Weight lookup with the platform's long-standing fallback: unknown statuses
19
+ // weigh the same as Good, i.e. they never alert and never open episodes.
20
+ // This deliberately covers the legacy "Bad" value still present in the
21
+ // monitorAlertStatus enum (monitors.js) — candidate for a data migration,
22
+ // tracked in JustAirNotificationService/TODOS.md context.
23
+ export const getAlertWeight = (alertLevel) =>
24
+ ALERT_LEVEL_WEIGHTS[alertLevel] || ALERT_LEVEL_WEIGHTS.Good;
@@ -0,0 +1,12 @@
1
+ // Barrel for the `@justair/justair-library/constants` entry point.
2
+ //
3
+ // Backward compatibility contract: this file MUST re-export everything
4
+ // pollutants.js exports, because package.json's `./constants` subpath pointed
5
+ // straight at pollutants.js before alertLevels.js existed — existing
6
+ // `import { PARAMETERS, HEAVY_METALS } from "@justair/justair-library/constants"`
7
+ // call sites across the platform must keep working unchanged.
8
+ // (Verified by src/constants/tests/constantsBarrel.test.js.)
9
+
10
+ export * from "./pollutants.js";
11
+ export * from "./alertLevels.js";
12
+ export * from "./userNotifications.js";
@@ -0,0 +1,46 @@
1
+ // The `./constants` package subpath used to point directly at pollutants.js.
2
+ // It now points at the index.js barrel so alertLevels.js can share the entry
3
+ // point. These tests pin the backward-compatibility contract: every consumer
4
+ // importing PARAMETERS / HEAVY_METALS through the barrel must see the exact
5
+ // same objects as before.
6
+ import * as barrel from '../index.js';
7
+ import { PARAMETERS, HEAVY_METALS } from '../pollutants.js';
8
+ import { ALERT_LEVEL_WEIGHTS, getAlertWeight } from '../alertLevels.js';
9
+
10
+ test('barrel re-exports PARAMETERS and HEAVY_METALS unchanged (identity, not copies)', () => {
11
+ expect(barrel.PARAMETERS).toBe(PARAMETERS);
12
+ expect(barrel.HEAVY_METALS).toBe(HEAVY_METALS);
13
+ });
14
+
15
+ test('barrel exports the alert-level constants', () => {
16
+ expect(barrel.ALERT_LEVEL_WEIGHTS).toBe(ALERT_LEVEL_WEIGHTS);
17
+ expect(barrel.getAlertWeight).toBe(getAlertWeight);
18
+ });
19
+
20
+ test('ALERT_LEVEL_WEIGHTS carries the exact platform hierarchy (values map to thresholds + Courier templates)', () => {
21
+ expect(ALERT_LEVEL_WEIGHTS).toEqual({
22
+ Good: 1,
23
+ Moderate: 2,
24
+ 'Unhealthy for SG': 3,
25
+ Unhealthy: 4,
26
+ 'Very Unhealthy': 5,
27
+ Hazardous: 6,
28
+ });
29
+ });
30
+
31
+ test('getAlertWeight resolves every known level', () => {
32
+ expect(getAlertWeight('Good')).toBe(1);
33
+ expect(getAlertWeight('Moderate')).toBe(2);
34
+ expect(getAlertWeight('Unhealthy for SG')).toBe(3);
35
+ expect(getAlertWeight('Unhealthy')).toBe(4);
36
+ expect(getAlertWeight('Very Unhealthy')).toBe(5);
37
+ expect(getAlertWeight('Hazardous')).toBe(6);
38
+ });
39
+
40
+ test('unknown and legacy statuses fall back to the Good weight (never alert, never open episodes)', () => {
41
+ // "Bad" still exists in the monitorAlertStatus enum — deliberately non-alerting.
42
+ expect(getAlertWeight('Bad')).toBe(1);
43
+ expect(getAlertWeight(undefined)).toBe(1);
44
+ expect(getAlertWeight(null)).toBe(1);
45
+ expect(getAlertWeight('')).toBe(1);
46
+ });
@@ -0,0 +1,22 @@
1
+ // UserNotification vocabulary — source of truth for the send-log enums
2
+ // (JA-2658). Kept here (Mongoose-free) so consumers can import the kind /
3
+ // channel / status lists without pulling in the models.
4
+ //
5
+ // Three orthogonal axes, deliberately separated:
6
+ // - `kind` — what the notification is ABOUT (selects the discriminator
7
+ // that carries the kind-specific payload).
8
+ // - `channel` — HOW it was delivered. Independent of kind: an aqi_alert can
9
+ // go out over sms and email as two separate rows.
10
+ // - `status` — the DELIVERY outcome. Universal across every kind; this is
11
+ // what "status" means and must not be confused with AQI level.
12
+
13
+ export const NOTIFICATION_KINDS = [
14
+ "aqi_alert",
15
+ "device_offline",
16
+ "weekly_digest",
17
+ "account",
18
+ ];
19
+
20
+ export const NOTIFICATION_CHANNELS = ["sms", "email", "push", "in_app"];
21
+
22
+ export const DELIVERY_STATUSES = ["queued", "sent", "failed"];
package/src/index.js CHANGED
@@ -66,6 +66,22 @@ import {
66
66
  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
+ import { ALERT_LEVEL_WEIGHTS, getAlertWeight } from "./constants/alertLevels.js";
70
+ import {
71
+ monitorEpisodesSchema,
72
+ MonitorEpisode,
73
+ } from "./models/monitorEpisodes.js";
74
+ import {
75
+ userNotificationsSchema,
76
+ UserNotification,
77
+ AqiAlertNotification,
78
+ DeviceOfflineNotification,
79
+ WeeklyDigestNotification,
80
+ AccountNotification,
81
+ NOTIFICATION_KINDS,
82
+ NOTIFICATION_CHANNELS,
83
+ DELIVERY_STATUSES,
84
+ } from "./models/userNotifications.js";
69
85
  import { splitName } from "./utils/splitName.js";
70
86
 
71
87
  // Sites related imports
@@ -171,9 +187,24 @@ export {
171
187
  // Password reset tokens (JA-2946)
172
188
  passwordResetTokenSchema,
173
189
  PasswordResetToken,
190
+ // Notification episodes + send log (JA-2658 / JA-2691 stack)
191
+ monitorEpisodesSchema,
192
+ MonitorEpisode,
193
+ userNotificationsSchema,
194
+ UserNotification,
195
+ AqiAlertNotification,
196
+ DeviceOfflineNotification,
197
+ WeeklyDigestNotification,
198
+ AccountNotification,
199
+ NOTIFICATION_KINDS,
200
+ NOTIFICATION_CHANNELS,
201
+ DELIVERY_STATUSES,
174
202
  // Pollutant constants
175
203
  PARAMETERS,
176
204
  HEAVY_METALS,
205
+ // AQI alert-level hierarchy (single source of truth)
206
+ ALERT_LEVEL_WEIGHTS,
207
+ getAlertWeight,
177
208
  // Utilities
178
209
  splitName,
179
210
  };
@@ -0,0 +1,80 @@
1
+ import mongoose from "mongoose";
2
+ import { ALERT_LEVEL_WEIGHTS } from "../constants/alertLevels.js";
3
+
4
+ // MonitorEpisode — one mutable doc per OPEN episode per user+device (JA-2658).
5
+ // Written by JustAirNotificationService's alert cron (JA-2958); read by
6
+ // JustAirUserService GET /users/my-monitors (JA-2691).
7
+ //
8
+ // Good→nonGood (cron observes; startedAt = Monitors.leftGoodAt ?? now)
9
+ // (no doc) ───────────────────────────────► OPEN {open:true, startedAt, peakStatus}
10
+ // │ alert event names this monitor
11
+ // ▼
12
+ // OPEN {firstNotifiedAt set once, alertCount++}
13
+ // nonGood→Good / sweep │
14
+ // TTL 24mo ◄── CLOSED {open:false, endedAt} ◄┘
15
+ //
16
+ // Conventions the indexes DEPEND on — do not relax:
17
+ // - `open` is the lifecycle state field and the partial-index key. MongoDB
18
+ // partialFilterExpression does NOT support `$exists: false`, so open-ness
19
+ // must be an explicit boolean, not the absence of endedAt. Close = one
20
+ // atomic update: { $set: { open: false, endedAt: <now> } }.
21
+ // - endedAt is OMITTED while open (never null) and set exactly once on close;
22
+ // the TTL index only ever sees closed docs because open docs carry no date.
23
+ // - firstNotifiedAt is OMITTED until the first alert event that names this
24
+ // monitor; stamps must be only-if-unset so retries can't move the time.
25
+ // - startedAt is the best-known "left Good" time: precise when CalcAQI has
26
+ // stamped Monitors.leftGoodAt, first-observation time otherwise (day-one
27
+ // opens and stale monitors carry the cron-observed fallback).
28
+
29
+ const alertLevelsEnum = [...Object.keys(ALERT_LEVEL_WEIGHTS)];
30
+
31
+ const monitorEpisodesSchema = new mongoose.Schema(
32
+ {
33
+ user: {
34
+ type: mongoose.Types.ObjectId,
35
+ ref: "Users",
36
+ required: true,
37
+ },
38
+ monitor: {
39
+ type: mongoose.Types.ObjectId,
40
+ ref: "Monitors",
41
+ required: true,
42
+ },
43
+ open: { type: Boolean, default: true, required: true },
44
+ startedAt: { type: Date, required: true },
45
+ firstNotifiedAt: { type: Date },
46
+ endedAt: { type: Date },
47
+ alertCount: { type: Number, default: 0 },
48
+ peakStatus: { type: String, enum: alertLevelsEnum },
49
+ },
50
+ { timestamps: true },
51
+ );
52
+
53
+ // Hot path + correctness guarantee: at most ONE open episode per user+device.
54
+ // Duplicate opens from overlapping cron runs surface as E11000 — writers use
55
+ // ordered:false bulkWrite and tolerate the duplicate as a lost race.
56
+ monitorEpisodesSchema.index(
57
+ { user: 1, monitor: 1 },
58
+ { unique: true, partialFilterExpression: { open: true } },
59
+ );
60
+
61
+ // JA-2691 read: all open episodes for one user — query { user, open: true }.
62
+ monitorEpisodesSchema.index(
63
+ { user: 1 },
64
+ { partialFilterExpression: { open: true } },
65
+ );
66
+
67
+ // JA-2958 lifecycle sweep: fetch/inspect all open episodes without scanning
68
+ // 24 months of closed docs — query includes { open: true }.
69
+ monitorEpisodesSchema.index(
70
+ { monitor: 1 },
71
+ { partialFilterExpression: { open: true } },
72
+ );
73
+
74
+ // Retention: closed episodes expire ~24 months after endedAt. Open episodes
75
+ // carry no endedAt date, so the TTL monitor never touches them.
76
+ monitorEpisodesSchema.index({ endedAt: 1 }, { expireAfterSeconds: 63113904 });
77
+
78
+ const MonitorEpisode = mongoose.model("MonitorEpisode", monitorEpisodesSchema);
79
+
80
+ export { monitorEpisodesSchema, MonitorEpisode };
@@ -199,6 +199,12 @@ const monitorsSchema = mongoose.Schema(
199
199
  monitorType: String,
200
200
  monitorIdFromSupplier: String,
201
201
  measurementUpdate: Date,
202
+ // When the device last crossed Good→nonGood, stamped by CalculateAQIService
203
+ // updateMonitorStatus and $unset on nonGood→Good recovery — so the field
204
+ // being PRESENT means "currently nonGood since this time". Absent = never
205
+ // stamped (or currently Good). Read by JustAirNotificationService as the
206
+ // MonitorEpisode startedAt source (JA-2691 stack). No default on purpose.
207
+ leftGoodAt: Date,
202
208
  monitorProperties: Object,
203
209
  isPrivate: { type: Boolean, default: false, required: true },
204
210
  monitorState: {
@@ -0,0 +1,121 @@
1
+ import mongoose from "mongoose";
2
+ import { ALERT_LEVEL_WEIGHTS } from "../constants/alertLevels.js";
3
+ import {
4
+ NOTIFICATION_CHANNELS,
5
+ DELIVERY_STATUSES,
6
+ } from "../constants/userNotifications.js";
7
+
8
+ // UserNotification — append-only send log, one doc per outbound notification
9
+ // per channel (JA-2658). Written best-effort by JustAirNotificationService
10
+ // after a send is queued (JA-2958); never blocks or gates the send itself.
11
+ //
12
+ // Shape (reshaped for multi-kind support, superseding the 2026-07-07 eng
13
+ // review's aqi-only draft):
14
+ // - The BASE doc holds only what every notification kind shares: the user,
15
+ // the delivery `channel`, the delivery `status`, the rendered `message`,
16
+ // and `sentAt`.
17
+ // - `kind` is the Mongoose discriminator key. Each kind registers a
18
+ // discriminator carrying its own payload, so kind-specific fields
19
+ // (e.g. the AQI `alertLevel`, `monitors`, `episodes`) never appear on
20
+ // rows of a kind that has no such concept.
21
+ // - `status` is the DELIVERY outcome (queued / sent / failed) — NOT the AQI
22
+ // level. The AQI level lives on the aqi_alert discriminator as `alertLevel`.
23
+ //
24
+ // Reconciliation: one alert EVENT (the per-user threshold gate firing)
25
+ // produces one row PER channel actually sent. Episode alertCount is
26
+ // incremented once per EVENT — reconcile counts against events, not rows.
27
+
28
+ const alertLevelsEnum = [...Object.keys(ALERT_LEVEL_WEIGHTS)];
29
+
30
+ const baseSchema = new mongoose.Schema(
31
+ {
32
+ user: {
33
+ type: mongoose.Types.ObjectId,
34
+ ref: "Users",
35
+ required: true,
36
+ },
37
+ channel: { type: String, enum: NOTIFICATION_CHANNELS, required: true },
38
+ // Delivery outcome, shared across all kinds. Defaults to "queued" since a
39
+ // row is written as soon as the send is handed off.
40
+ status: {
41
+ type: String,
42
+ enum: DELIVERY_STATUSES,
43
+ required: true,
44
+ default: "queued",
45
+ },
46
+ message: String,
47
+ sentAt: { type: Date, default: Date.now, required: true },
48
+ },
49
+ {
50
+ timestamps: true,
51
+ discriminatorKey: "kind",
52
+ },
53
+ );
54
+
55
+ // Per-user history, newest first — every kind.
56
+ baseSchema.index({ user: 1, sentAt: -1 });
57
+
58
+ // Retention: rows expire ~24 months after send.
59
+ baseSchema.index({ sentAt: 1 }, { expireAfterSeconds: 63113904 });
60
+
61
+ const UserNotification = mongoose.model("UserNotification", baseSchema);
62
+
63
+ // aqi_alert — the only kind carrying monitors / episodes / AQI level.
64
+ // `monitors` lists exactly the monitors named in the message
65
+ // (monitorsAtHighestStatus); `episodes` the matching MonitorEpisode _ids —
66
+ // a row is 1:1 reconcilable with the SMS/email content.
67
+ const aqiAlertSchema = new mongoose.Schema({
68
+ monitors: [{ type: mongoose.Types.ObjectId, ref: "Monitors" }],
69
+ episodes: [{ type: mongoose.Types.ObjectId, ref: "MonitorEpisode" }],
70
+ alertLevel: { type: String, enum: alertLevelsEnum, required: true },
71
+ });
72
+
73
+ // Device-scoped history for one user — only meaningful where `monitors`
74
+ // exists, so it lives on the discriminator (lands on the same collection).
75
+ aqiAlertSchema.index({ user: 1, monitors: 1, sentAt: -1 });
76
+
77
+ const AqiAlertNotification = UserNotification.discriminator(
78
+ "aqi_alert",
79
+ aqiAlertSchema,
80
+ );
81
+
82
+ // device_offline — references the device(s) that went quiet, no AQI level.
83
+ const deviceOfflineSchema = new mongoose.Schema({
84
+ monitors: [{ type: mongoose.Types.ObjectId, ref: "Monitors" }],
85
+ });
86
+
87
+ const DeviceOfflineNotification = UserNotification.discriminator(
88
+ "device_offline",
89
+ deviceOfflineSchema,
90
+ );
91
+
92
+ // weekly_digest — the period the digest covers; no per-monitor payload.
93
+ const weeklyDigestSchema = new mongoose.Schema({
94
+ periodStart: Date,
95
+ periodEnd: Date,
96
+ });
97
+
98
+ const WeeklyDigestNotification = UserNotification.discriminator(
99
+ "weekly_digest",
100
+ weeklyDigestSchema,
101
+ );
102
+
103
+ // account — no payload beyond the shared base + message.
104
+ const AccountNotification = UserNotification.discriminator(
105
+ "account",
106
+ new mongoose.Schema({}),
107
+ );
108
+
109
+ export {
110
+ baseSchema as userNotificationsSchema,
111
+ UserNotification,
112
+ AqiAlertNotification,
113
+ DeviceOfflineNotification,
114
+ WeeklyDigestNotification,
115
+ AccountNotification,
116
+ };
117
+ export {
118
+ NOTIFICATION_KINDS,
119
+ NOTIFICATION_CHANNELS,
120
+ DELIVERY_STATUSES,
121
+ } from "../constants/userNotifications.js";