@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@justair/justair-library",
3
- "version": "4.8.18",
3
+ "version": "4.8.20",
4
4
  "description": "JustAir Internal Library",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -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 };