@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.
- package/dist/constants/alertLevels.d.ts +10 -0
- package/dist/constants/alertLevels.d.ts.map +1 -0
- package/dist/constants/index.d.ts +4 -0
- package/dist/constants/index.d.ts.map +1 -0
- package/dist/constants/tests/constantsBarrel.test.d.ts +2 -0
- package/dist/constants/tests/constantsBarrel.test.d.ts.map +1 -0
- package/dist/constants/userNotifications.d.ts +4 -0
- package/dist/constants/userNotifications.d.ts.map +1 -0
- package/dist/index.d.ts +14 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/models/monitorEpisodes.d.ts +266 -0
- package/dist/models/monitorEpisodes.d.ts.map +1 -0
- package/dist/models/monitors.d.ts.map +1 -1
- package/dist/models/tests/monitorEpisodes.test.d.ts +2 -0
- package/dist/models/tests/monitorEpisodes.test.d.ts.map +1 -0
- package/dist/models/tests/monitorsLeftGoodAt.test.d.ts +2 -0
- package/dist/models/tests/monitorsLeftGoodAt.test.d.ts.map +1 -0
- package/dist/models/tests/userNotifications.test.d.ts +2 -0
- package/dist/models/tests/userNotifications.test.d.ts.map +1 -0
- package/dist/models/userNotifications.d.ts +175 -0
- package/dist/models/userNotifications.d.ts.map +1 -0
- package/package.json +3 -3
- package/src/constants/alertLevels.js +24 -0
- package/src/constants/index.js +12 -0
- package/src/constants/tests/constantsBarrel.test.js +46 -0
- package/src/constants/userNotifications.js +22 -0
- package/src/index.js +31 -0
- package/src/models/monitorEpisodes.js +80 -0
- package/src/models/monitors.js +6 -0
- 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 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/constants/index.js"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constantsBarrel.test.d.ts","sourceRoot":"","sources":["../../../src/constants/tests/constantsBarrel.test.js"],"names":[],"mappings":""}
|
|
@@ -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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.js"],"names":[],"mappings":"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,
|
|
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 @@
|
|
|
1
|
+
{"version":3,"file":"monitorEpisodes.test.d.ts","sourceRoot":"","sources":["../../../src/models/tests/monitorEpisodes.test.js"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"monitorsLeftGoodAt.test.d.ts","sourceRoot":"","sources":["../../../src/models/tests/monitorsLeftGoodAt.test.js"],"names":[],"mappings":""}
|
|
@@ -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.
|
|
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/
|
|
15
|
-
"default": "./src/constants/
|
|
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 };
|
package/src/models/monitors.js
CHANGED
|
@@ -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";
|