@justair/justair-library 4.8.18 → 4.8.20
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/package.json +1 -1
- package/src/models/monitors.js +2 -0
- package/src/models/organizations.js +74 -0
- package/src/models/sampleSites.js +198 -0
package/package.json
CHANGED
package/src/models/monitors.js
CHANGED
|
@@ -248,6 +248,8 @@ const monitorsSchema = mongoose.Schema(
|
|
|
248
248
|
default: [],
|
|
249
249
|
},
|
|
250
250
|
applyCorrections: { type: Boolean, default: false },
|
|
251
|
+
// Optional readKey for private API access (e.g. PurpleAir)
|
|
252
|
+
readKey: { type: String, required: false },
|
|
251
253
|
pausedParameters: {
|
|
252
254
|
type: [{ parameter: String, timestamp: Date, isPaused: Boolean }],
|
|
253
255
|
default: [],
|
|
@@ -61,6 +61,32 @@ const featureSchema = new Schema({
|
|
|
61
61
|
},
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
+
const computeResetTime = (rateLimitType, baseTime = new Date()) => {
|
|
65
|
+
const resetTime = new Date(baseTime);
|
|
66
|
+
|
|
67
|
+
switch (rateLimitType) {
|
|
68
|
+
case "second":
|
|
69
|
+
resetTime.setSeconds(resetTime.getSeconds() + 1);
|
|
70
|
+
break;
|
|
71
|
+
case "hourly":
|
|
72
|
+
resetTime.setHours(resetTime.getHours() + 1);
|
|
73
|
+
break;
|
|
74
|
+
case "daily":
|
|
75
|
+
resetTime.setDate(resetTime.getDate() + 1);
|
|
76
|
+
break;
|
|
77
|
+
case "weekly":
|
|
78
|
+
resetTime.setDate(resetTime.getDate() + 7);
|
|
79
|
+
break;
|
|
80
|
+
case "monthly":
|
|
81
|
+
resetTime.setMonth(resetTime.getMonth() + 1);
|
|
82
|
+
break;
|
|
83
|
+
default:
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return resetTime;
|
|
88
|
+
};
|
|
89
|
+
|
|
64
90
|
const organizationsSchema = mongoose.Schema(
|
|
65
91
|
{
|
|
66
92
|
name: String,
|
|
@@ -85,6 +111,54 @@ const organizationsSchema = mongoose.Schema(
|
|
|
85
111
|
}
|
|
86
112
|
);
|
|
87
113
|
|
|
114
|
+
organizationsSchema.methods.setRateLimitProperties = function () {
|
|
115
|
+
if (!Array.isArray(this.orgAPIKey)) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const now = new Date();
|
|
120
|
+
|
|
121
|
+
this.orgAPIKey = this.orgAPIKey.map((apiKeyEntry) => {
|
|
122
|
+
if (!apiKeyEntry || !Array.isArray(apiKeyEntry.rateLimits)) {
|
|
123
|
+
return apiKeyEntry;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const updatedRateLimits = apiKeyEntry.rateLimits.map((rateLimit) => {
|
|
127
|
+
if (!rateLimit) {
|
|
128
|
+
return rateLimit;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const updatedRateLimit = { ...rateLimit };
|
|
132
|
+
|
|
133
|
+
if (updatedRateLimit.apiCount == null) {
|
|
134
|
+
updatedRateLimit.apiCount = 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!updatedRateLimit.resetTime) {
|
|
138
|
+
updatedRateLimit.resetTime = computeResetTime(
|
|
139
|
+
updatedRateLimit.type,
|
|
140
|
+
now
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return updatedRateLimit;
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
...apiKeyEntry,
|
|
149
|
+
rateLimits: updatedRateLimits,
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
organizationsSchema.pre("save", function (next) {
|
|
155
|
+
if (this.isModified("orgAPIKey")) {
|
|
156
|
+
this.setRateLimitProperties();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
next();
|
|
160
|
+
});
|
|
161
|
+
|
|
88
162
|
// Name-based sorting index
|
|
89
163
|
organizationsSchema.index({ name: 1 });
|
|
90
164
|
// Connected monitors array queries
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import mongoose from "mongoose";
|
|
2
|
+
const sampleParametersEnum = [
|
|
3
|
+
"C6H6", // Benzene
|
|
4
|
+
"PB", // Lead
|
|
5
|
+
"AS", // Arsenic
|
|
6
|
+
"CD", // Cadmium
|
|
7
|
+
"CR", // Chromium
|
|
8
|
+
"NI", // Nickel
|
|
9
|
+
"BA", // Barium
|
|
10
|
+
"FE", // Iron
|
|
11
|
+
"CU", // Copper
|
|
12
|
+
"ZN", // Zinc
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const noteSchema = mongoose.Schema({
|
|
16
|
+
note: {
|
|
17
|
+
type: String,
|
|
18
|
+
required: true,
|
|
19
|
+
},
|
|
20
|
+
type: {
|
|
21
|
+
type: String,
|
|
22
|
+
required: true,
|
|
23
|
+
},
|
|
24
|
+
adminId: {
|
|
25
|
+
type: mongoose.Types.ObjectId,
|
|
26
|
+
ref: "Admin",
|
|
27
|
+
required: true,
|
|
28
|
+
},
|
|
29
|
+
adminName: {
|
|
30
|
+
type: String,
|
|
31
|
+
},
|
|
32
|
+
date: {
|
|
33
|
+
type: Date,
|
|
34
|
+
default: Date.now,
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Sample Site Audit Schema
|
|
39
|
+
const sampleSiteAuditSchema = mongoose.Schema(
|
|
40
|
+
{
|
|
41
|
+
monitorId: { type: mongoose.Types.ObjectId, ref: "SampleSites" },
|
|
42
|
+
orgId: { type: mongoose.Types.ObjectId, ref: "Organizations" },
|
|
43
|
+
timeUpdated: Date,
|
|
44
|
+
deletedAt: { type: Date, default: Date.now }, // Only populated on delete
|
|
45
|
+
monitorLocation: {
|
|
46
|
+
// Monitor GPS location (lat, lon)
|
|
47
|
+
type: { type: String, enum: ["Point"], required: true },
|
|
48
|
+
coordinates: { type: [Number], required: true },
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
timestamps: true,
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
// Create the SampleSiteAudit model
|
|
57
|
+
const SampleSiteAudit = mongoose.model("SampleSiteAudit", sampleSiteAuditSchema);
|
|
58
|
+
|
|
59
|
+
// Sample Sites Schema
|
|
60
|
+
const sampleSitesSchema = mongoose.Schema(
|
|
61
|
+
{
|
|
62
|
+
monitorCode: String,
|
|
63
|
+
// Managed By and Sponsored By fields
|
|
64
|
+
managedBy: String,
|
|
65
|
+
sponsoredBy: String,
|
|
66
|
+
measurementUpdate: Date,
|
|
67
|
+
isPrivate: { type: Boolean, default: false, required: true },
|
|
68
|
+
sponsor: { type: mongoose.Types.ObjectId, ref: "Organizations" },
|
|
69
|
+
sponsorName: String,
|
|
70
|
+
monitorLatitude: Number,
|
|
71
|
+
monitorLongitude: Number,
|
|
72
|
+
gpsLocation: {
|
|
73
|
+
type: { type: String, enum: ["Point"], required: true },
|
|
74
|
+
coordinates: { type: [Number], required: true },
|
|
75
|
+
},
|
|
76
|
+
location: Object,
|
|
77
|
+
context: [String],
|
|
78
|
+
parameters: [
|
|
79
|
+
{
|
|
80
|
+
type: String,
|
|
81
|
+
enum: sampleParametersEnum,
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
notes: [noteSchema],
|
|
85
|
+
image: String,
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
timestamps: true,
|
|
89
|
+
}
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// Geographic queries - already exists
|
|
93
|
+
sampleSitesSchema.index({ gpsLocation: "2dsphere" });
|
|
94
|
+
|
|
95
|
+
// Sponsor-based queries
|
|
96
|
+
sampleSitesSchema.index({ sponsor: 1, isActive: 1 });
|
|
97
|
+
|
|
98
|
+
// Location-based filtering
|
|
99
|
+
sampleSitesSchema.index({ "location.city": 1, "location.state": 1 });
|
|
100
|
+
sampleSitesSchema.index({ "location.neighborhood": 1 });
|
|
101
|
+
sampleSitesSchema.index({ "location.county": 1 });
|
|
102
|
+
|
|
103
|
+
// Query parameter filtering
|
|
104
|
+
sampleSitesSchema.index({ context: 1 });
|
|
105
|
+
sampleSitesSchema.index({ parameters: 1 });
|
|
106
|
+
|
|
107
|
+
// Pre-hook to log single document deletions
|
|
108
|
+
sampleSitesSchema.pre("findOneAndDelete", async function () {
|
|
109
|
+
const docToDelete = await this.model.findOne(this.getFilter()).lean();
|
|
110
|
+
if (docToDelete) {
|
|
111
|
+
console.log("Logging findOneAndDelete to sample site audit", docToDelete);
|
|
112
|
+
const auditLog = new SampleSiteAudit({
|
|
113
|
+
monitorId: docToDelete._id,
|
|
114
|
+
orgId: docToDelete.sponsor,
|
|
115
|
+
timeUpdated: docToDelete.updatedAt,
|
|
116
|
+
monitorLocation: {
|
|
117
|
+
type: "Point",
|
|
118
|
+
coordinates: [
|
|
119
|
+
docToDelete.monitorLongitude,
|
|
120
|
+
docToDelete.monitorLatitude,
|
|
121
|
+
],
|
|
122
|
+
},
|
|
123
|
+
deletedAt: new Date(),
|
|
124
|
+
});
|
|
125
|
+
await auditLog.save();
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Pre-hook to log multiple document deletions
|
|
130
|
+
sampleSitesSchema.pre("deleteMany", async function () {
|
|
131
|
+
console.log("deleteMany pre-hook triggered for sample sites");
|
|
132
|
+
const docsToDelete = await this.model.find(this.getFilter()).lean();
|
|
133
|
+
|
|
134
|
+
if (docsToDelete.length) {
|
|
135
|
+
console.log(`Logging ${docsToDelete.length} sample site documents to audit`);
|
|
136
|
+
const auditLogs = docsToDelete.map((doc) => ({
|
|
137
|
+
monitorId: doc._id,
|
|
138
|
+
orgId: doc.sponsor,
|
|
139
|
+
timeUpdated: doc.updatedAt,
|
|
140
|
+
monitorLocation: {
|
|
141
|
+
type: "Point",
|
|
142
|
+
coordinates: [doc.monitorLongitude, doc.monitorLatitude],
|
|
143
|
+
},
|
|
144
|
+
deletedAt: new Date(),
|
|
145
|
+
}));
|
|
146
|
+
|
|
147
|
+
await SampleSiteAudit.insertMany(auditLogs);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Pre-hook to log a single document deletion (for deleteOne)
|
|
152
|
+
sampleSitesSchema.pre("deleteOne", async function () {
|
|
153
|
+
console.log("deleteOne pre-hook triggered for sample sites");
|
|
154
|
+
const docToDelete = await this.model.findOne(this.getFilter()).lean();
|
|
155
|
+
|
|
156
|
+
if (docToDelete) {
|
|
157
|
+
console.log("Logging deleteOne to sample site audit", docToDelete);
|
|
158
|
+
const auditLog = new SampleSiteAudit({
|
|
159
|
+
monitorId: docToDelete._id,
|
|
160
|
+
orgId: docToDelete.sponsor,
|
|
161
|
+
timeUpdated: docToDelete.updatedAt,
|
|
162
|
+
monitorLocation: {
|
|
163
|
+
type: "Point",
|
|
164
|
+
coordinates: [
|
|
165
|
+
docToDelete.monitorLongitude,
|
|
166
|
+
docToDelete.monitorLatitude,
|
|
167
|
+
],
|
|
168
|
+
},
|
|
169
|
+
deletedAt: new Date(),
|
|
170
|
+
});
|
|
171
|
+
await auditLog.save();
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Pre-hook to log multiple document updates
|
|
176
|
+
sampleSitesSchema.pre("updateMany", async function () {
|
|
177
|
+
const docsToUpdate = await this.model.find(this.getFilter()).lean();
|
|
178
|
+
if (docsToUpdate.length) {
|
|
179
|
+
console.log(`Logging ${docsToUpdate.length} sample site documents to audit`);
|
|
180
|
+
const auditLogs = docsToUpdate.map((doc) => ({
|
|
181
|
+
monitorId: doc._id,
|
|
182
|
+
orgId: doc.sponsor,
|
|
183
|
+
timeUpdated: doc.updatedAt,
|
|
184
|
+
monitorLocation: {
|
|
185
|
+
type: "Point",
|
|
186
|
+
coordinates: [doc.monitorLongitude, doc.monitorLatitude],
|
|
187
|
+
},
|
|
188
|
+
deletedAt: null, // Not a deletion, so this field is null
|
|
189
|
+
}));
|
|
190
|
+
|
|
191
|
+
await SampleSiteAudit.insertMany(auditLogs);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Create the SampleSites model
|
|
196
|
+
const SampleSites = mongoose.model("SampleSites", sampleSitesSchema);
|
|
197
|
+
|
|
198
|
+
export { sampleSitesSchema, SampleSites, sampleSiteAuditSchema, SampleSiteAudit, sampleParametersEnum };
|