@justair/justair-library 6.11.0 → 6.12.0-alpha.25e595a

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.
@@ -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,3 @@
1
+ export * from "./pollutants.js";
2
+ export * from "./alertLevels.js";
3
+ //# 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":""}
package/dist/index.d.ts CHANGED
@@ -75,8 +75,15 @@ 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 { NOTIFICATION_KINDS } from "./models/userNotifications.js";
78
83
  import { PARAMETERS } from "./constants/pollutants.js";
79
84
  import { HEAVY_METALS } from "./constants/pollutants.js";
85
+ import { ALERT_LEVEL_WEIGHTS } from "./constants/alertLevels.js";
86
+ import { getAlertWeight } from "./constants/alertLevels.js";
80
87
  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 };
88
+ 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, NOTIFICATION_KINDS, PARAMETERS, HEAVY_METALS, ALERT_LEVEL_WEIGHTS, getAlertWeight, splitName };
82
89
  //# 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":"AAsGA;;;iBAEC;yBAtCwB,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;4BAiBT,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;sCA5BtF,6BAA6B;+BAA7B,6BAA6B;wCAK7B,+BAA+B;iCAA/B,+BAA+B;mCAA/B,+BAA+B;2BAVG,2BAA2B;6BAA3B,2BAA2B;oCAChB,4BAA4B;+BAA5B,4BAA4B;0BAUtD,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,iCA4IE;AAuIF,2BAA4D;AA1T5D,qCAiCE;AAGF,+BAAwE;AAtKxE,sCAAoD"}
1
+ {"version":3,"file":"monitors.d.ts","sourceRoot":"","sources":["../../src/models/monitors.js"],"names":[],"mappings":"AA4KA,iCAkJE;AAuIF,2BAA4D;AAhU5D,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,330 @@
1
+ export const userNotificationsSchema: 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
+ kind: string;
17
+ monitors: {
18
+ prototype?: mongoose.Types.ObjectId;
19
+ cacheHexString?: unknown;
20
+ generate?: {};
21
+ createFromTime?: {};
22
+ createFromHexString?: {};
23
+ createFromBase64?: {};
24
+ isValid?: {};
25
+ }[];
26
+ episodes: {
27
+ prototype?: mongoose.Types.ObjectId;
28
+ cacheHexString?: unknown;
29
+ generate?: {};
30
+ createFromTime?: {};
31
+ createFromHexString?: {};
32
+ createFromBase64?: {};
33
+ isValid?: {};
34
+ }[];
35
+ sentAt: Date;
36
+ message?: string;
37
+ status?: string;
38
+ }, mongoose.Document<unknown, {}, 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
+ kind: string;
52
+ monitors: {
53
+ prototype?: mongoose.Types.ObjectId;
54
+ cacheHexString?: unknown;
55
+ generate?: {};
56
+ createFromTime?: {};
57
+ createFromHexString?: {};
58
+ createFromBase64?: {};
59
+ isValid?: {};
60
+ }[];
61
+ episodes: {
62
+ prototype?: mongoose.Types.ObjectId;
63
+ cacheHexString?: unknown;
64
+ generate?: {};
65
+ createFromTime?: {};
66
+ createFromHexString?: {};
67
+ createFromBase64?: {};
68
+ isValid?: {};
69
+ }[];
70
+ sentAt: Date;
71
+ message?: string;
72
+ status?: string;
73
+ }>> & mongoose.FlatRecord<{
74
+ createdAt: NativeDate;
75
+ updatedAt: NativeDate;
76
+ } & {
77
+ user: {
78
+ prototype?: mongoose.Types.ObjectId;
79
+ cacheHexString?: unknown;
80
+ generate?: {};
81
+ createFromTime?: {};
82
+ createFromHexString?: {};
83
+ createFromBase64?: {};
84
+ isValid?: {};
85
+ };
86
+ kind: string;
87
+ monitors: {
88
+ prototype?: mongoose.Types.ObjectId;
89
+ cacheHexString?: unknown;
90
+ generate?: {};
91
+ createFromTime?: {};
92
+ createFromHexString?: {};
93
+ createFromBase64?: {};
94
+ isValid?: {};
95
+ }[];
96
+ episodes: {
97
+ prototype?: mongoose.Types.ObjectId;
98
+ cacheHexString?: unknown;
99
+ generate?: {};
100
+ createFromTime?: {};
101
+ createFromHexString?: {};
102
+ createFromBase64?: {};
103
+ isValid?: {};
104
+ }[];
105
+ sentAt: Date;
106
+ message?: string;
107
+ status?: string;
108
+ }> & {
109
+ _id: mongoose.Types.ObjectId;
110
+ }>;
111
+ export const UserNotification: mongoose.Model<{
112
+ createdAt: NativeDate;
113
+ updatedAt: NativeDate;
114
+ } & {
115
+ user: {
116
+ prototype?: mongoose.Types.ObjectId;
117
+ cacheHexString?: unknown;
118
+ generate?: {};
119
+ createFromTime?: {};
120
+ createFromHexString?: {};
121
+ createFromBase64?: {};
122
+ isValid?: {};
123
+ };
124
+ kind: string;
125
+ monitors: {
126
+ prototype?: mongoose.Types.ObjectId;
127
+ cacheHexString?: unknown;
128
+ generate?: {};
129
+ createFromTime?: {};
130
+ createFromHexString?: {};
131
+ createFromBase64?: {};
132
+ isValid?: {};
133
+ }[];
134
+ episodes: {
135
+ prototype?: mongoose.Types.ObjectId;
136
+ cacheHexString?: unknown;
137
+ generate?: {};
138
+ createFromTime?: {};
139
+ createFromHexString?: {};
140
+ createFromBase64?: {};
141
+ isValid?: {};
142
+ }[];
143
+ sentAt: Date;
144
+ message?: string;
145
+ status?: string;
146
+ }, {}, {}, {}, mongoose.Document<unknown, {}, {
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
+ kind: string;
160
+ monitors: {
161
+ prototype?: mongoose.Types.ObjectId;
162
+ cacheHexString?: unknown;
163
+ generate?: {};
164
+ createFromTime?: {};
165
+ createFromHexString?: {};
166
+ createFromBase64?: {};
167
+ isValid?: {};
168
+ }[];
169
+ episodes: {
170
+ prototype?: mongoose.Types.ObjectId;
171
+ cacheHexString?: unknown;
172
+ generate?: {};
173
+ createFromTime?: {};
174
+ createFromHexString?: {};
175
+ createFromBase64?: {};
176
+ isValid?: {};
177
+ }[];
178
+ sentAt: Date;
179
+ message?: string;
180
+ status?: string;
181
+ }> & {
182
+ createdAt: NativeDate;
183
+ updatedAt: NativeDate;
184
+ } & {
185
+ user: {
186
+ prototype?: mongoose.Types.ObjectId;
187
+ cacheHexString?: unknown;
188
+ generate?: {};
189
+ createFromTime?: {};
190
+ createFromHexString?: {};
191
+ createFromBase64?: {};
192
+ isValid?: {};
193
+ };
194
+ kind: string;
195
+ monitors: {
196
+ prototype?: mongoose.Types.ObjectId;
197
+ cacheHexString?: unknown;
198
+ generate?: {};
199
+ createFromTime?: {};
200
+ createFromHexString?: {};
201
+ createFromBase64?: {};
202
+ isValid?: {};
203
+ }[];
204
+ episodes: {
205
+ prototype?: mongoose.Types.ObjectId;
206
+ cacheHexString?: unknown;
207
+ generate?: {};
208
+ createFromTime?: {};
209
+ createFromHexString?: {};
210
+ createFromBase64?: {};
211
+ isValid?: {};
212
+ }[];
213
+ sentAt: Date;
214
+ message?: string;
215
+ status?: string;
216
+ } & {
217
+ _id: mongoose.Types.ObjectId;
218
+ }, mongoose.Schema<any, mongoose.Model<any, any, any, any, any, any>, {}, {}, {}, {}, {
219
+ timestamps: true;
220
+ }, {
221
+ createdAt: NativeDate;
222
+ updatedAt: NativeDate;
223
+ } & {
224
+ user: {
225
+ prototype?: mongoose.Types.ObjectId;
226
+ cacheHexString?: unknown;
227
+ generate?: {};
228
+ createFromTime?: {};
229
+ createFromHexString?: {};
230
+ createFromBase64?: {};
231
+ isValid?: {};
232
+ };
233
+ kind: string;
234
+ monitors: {
235
+ prototype?: mongoose.Types.ObjectId;
236
+ cacheHexString?: unknown;
237
+ generate?: {};
238
+ createFromTime?: {};
239
+ createFromHexString?: {};
240
+ createFromBase64?: {};
241
+ isValid?: {};
242
+ }[];
243
+ episodes: {
244
+ prototype?: mongoose.Types.ObjectId;
245
+ cacheHexString?: unknown;
246
+ generate?: {};
247
+ createFromTime?: {};
248
+ createFromHexString?: {};
249
+ createFromBase64?: {};
250
+ isValid?: {};
251
+ }[];
252
+ sentAt: Date;
253
+ message?: string;
254
+ status?: string;
255
+ }, mongoose.Document<unknown, {}, mongoose.FlatRecord<{
256
+ createdAt: NativeDate;
257
+ updatedAt: NativeDate;
258
+ } & {
259
+ user: {
260
+ prototype?: mongoose.Types.ObjectId;
261
+ cacheHexString?: unknown;
262
+ generate?: {};
263
+ createFromTime?: {};
264
+ createFromHexString?: {};
265
+ createFromBase64?: {};
266
+ isValid?: {};
267
+ };
268
+ kind: string;
269
+ monitors: {
270
+ prototype?: mongoose.Types.ObjectId;
271
+ cacheHexString?: unknown;
272
+ generate?: {};
273
+ createFromTime?: {};
274
+ createFromHexString?: {};
275
+ createFromBase64?: {};
276
+ isValid?: {};
277
+ }[];
278
+ episodes: {
279
+ prototype?: mongoose.Types.ObjectId;
280
+ cacheHexString?: unknown;
281
+ generate?: {};
282
+ createFromTime?: {};
283
+ createFromHexString?: {};
284
+ createFromBase64?: {};
285
+ isValid?: {};
286
+ }[];
287
+ sentAt: Date;
288
+ message?: string;
289
+ status?: string;
290
+ }>> & mongoose.FlatRecord<{
291
+ createdAt: NativeDate;
292
+ updatedAt: NativeDate;
293
+ } & {
294
+ user: {
295
+ prototype?: mongoose.Types.ObjectId;
296
+ cacheHexString?: unknown;
297
+ generate?: {};
298
+ createFromTime?: {};
299
+ createFromHexString?: {};
300
+ createFromBase64?: {};
301
+ isValid?: {};
302
+ };
303
+ kind: string;
304
+ monitors: {
305
+ prototype?: mongoose.Types.ObjectId;
306
+ cacheHexString?: unknown;
307
+ generate?: {};
308
+ createFromTime?: {};
309
+ createFromHexString?: {};
310
+ createFromBase64?: {};
311
+ isValid?: {};
312
+ }[];
313
+ episodes: {
314
+ prototype?: mongoose.Types.ObjectId;
315
+ cacheHexString?: unknown;
316
+ generate?: {};
317
+ createFromTime?: {};
318
+ createFromHexString?: {};
319
+ createFromBase64?: {};
320
+ isValid?: {};
321
+ }[];
322
+ sentAt: Date;
323
+ message?: string;
324
+ status?: string;
325
+ }> & {
326
+ _id: mongoose.Types.ObjectId;
327
+ }>>;
328
+ export const NOTIFICATION_KINDS: string[];
329
+ import mongoose from "mongoose";
330
+ //# sourceMappingURL=userNotifications.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"userNotifications.d.ts","sourceRoot":"","sources":["../../src/models/userNotifications.js"],"names":[],"mappings":"AAmBA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAeE;AAWF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAGE;AAjCF,0CAAoD;qBAf/B,UAAU"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@justair/justair-library",
3
- "version": "6.11.0",
3
+ "version": "6.12.0-alpha.25e595a",
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,11 @@
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";
@@ -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
+ });
package/src/index.js CHANGED
@@ -66,6 +66,16 @@ 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
+ NOTIFICATION_KINDS,
78
+ } from "./models/userNotifications.js";
69
79
  import { splitName } from "./utils/splitName.js";
70
80
 
71
81
  // Sites related imports
@@ -171,9 +181,18 @@ export {
171
181
  // Password reset tokens (JA-2946)
172
182
  passwordResetTokenSchema,
173
183
  PasswordResetToken,
184
+ // Notification episodes + send log (JA-2658 / JA-2691 stack)
185
+ monitorEpisodesSchema,
186
+ MonitorEpisode,
187
+ userNotificationsSchema,
188
+ UserNotification,
189
+ NOTIFICATION_KINDS,
174
190
  // Pollutant constants
175
191
  PARAMETERS,
176
192
  HEAVY_METALS,
193
+ // AQI alert-level hierarchy (single source of truth)
194
+ ALERT_LEVEL_WEIGHTS,
195
+ getAlertWeight,
177
196
  // Utilities
178
197
  splitName,
179
198
  };
@@ -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 };
@@ -198,6 +198,12 @@ const monitorsSchema = mongoose.Schema(
198
198
  monitorType: String,
199
199
  monitorIdFromSupplier: String,
200
200
  measurementUpdate: Date,
201
+ // When the device last crossed Good→nonGood, stamped by CalculateAQIService
202
+ // updateMonitorStatus and $unset on nonGood→Good recovery — so the field
203
+ // being PRESENT means "currently nonGood since this time". Absent = never
204
+ // stamped (or currently Good). Read by JustAirNotificationService as the
205
+ // MonitorEpisode startedAt source (JA-2691 stack). No default on purpose.
206
+ leftGoodAt: Date,
201
207
  monitorProperties: Object,
202
208
  isPrivate: { type: Boolean, default: false, required: true },
203
209
  monitorState: {
@@ -0,0 +1,51 @@
1
+ import mongoose from "mongoose";
2
+ import { ALERT_LEVEL_WEIGHTS } from "../constants/alertLevels.js";
3
+
4
+ // UserNotification — append-only send log, one doc per outbound notification
5
+ // per channel (JA-2658). Written best-effort by JustAirNotificationService
6
+ // after a send is queued (JA-2958); never blocks or gates the send itself.
7
+ //
8
+ // Semantics (locked in the 2026-07-07 eng review):
9
+ // - One alert EVENT (the per-user threshold gate firing) produces one row PER
10
+ // channel actually sent (kind aqi-sms / aqi-email). Episode alertCount is
11
+ // incremented once per EVENT — reconcile counts against events, not rows.
12
+ // - `monitors` lists exactly the monitors named in the message
13
+ // (monitorsAtHighestStatus), and `episodes` the matching MonitorEpisode
14
+ // _ids — a row is 1:1 reconcilable with the SMS/email content.
15
+
16
+ const NOTIFICATION_KINDS = ["aqi-sms", "aqi-email"];
17
+
18
+ const alertLevelsEnum = [...Object.keys(ALERT_LEVEL_WEIGHTS)];
19
+
20
+ const userNotificationsSchema = new mongoose.Schema(
21
+ {
22
+ user: {
23
+ type: mongoose.Types.ObjectId,
24
+ ref: "Users",
25
+ required: true,
26
+ },
27
+ kind: { type: String, enum: NOTIFICATION_KINDS, required: true },
28
+ monitors: [{ type: mongoose.Types.ObjectId, ref: "Monitors" }],
29
+ episodes: [{ type: mongoose.Types.ObjectId, ref: "MonitorEpisode" }],
30
+ status: { type: String, enum: alertLevelsEnum },
31
+ message: String,
32
+ sentAt: { type: Date, default: Date.now, required: true },
33
+ },
34
+ { timestamps: true },
35
+ );
36
+
37
+ // Per-user history, newest first.
38
+ userNotificationsSchema.index({ user: 1, sentAt: -1 });
39
+
40
+ // Device-scoped history for one user (multikey on monitors).
41
+ userNotificationsSchema.index({ user: 1, monitors: 1, sentAt: -1 });
42
+
43
+ // Retention: rows expire ~24 months after send.
44
+ userNotificationsSchema.index({ sentAt: 1 }, { expireAfterSeconds: 63113904 });
45
+
46
+ const UserNotification = mongoose.model(
47
+ "UserNotification",
48
+ userNotificationsSchema,
49
+ );
50
+
51
+ export { userNotificationsSchema, UserNotification, NOTIFICATION_KINDS };