@justair/justair-library 6.14.0 → 7.0.0

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.
@@ -1,261 +1,270 @@
1
- import mongoose from "mongoose";
2
- import { customAlphabet } from 'nanoid';
3
- import { HEAVY_METALS } from "../constants/pollutants.js";
4
-
5
- const generateSiteCode = customAlphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZ', 8);
6
-
7
- const sampleParametersEnum = Object.keys(HEAVY_METALS);
8
-
9
- const noteSchema = mongoose.Schema({
10
- note: {
11
- type: String,
12
- required: true,
13
- },
14
- type: {
15
- type: String,
16
- required: true,
17
- },
18
- adminId: {
19
- type: mongoose.Types.ObjectId,
20
- ref: "Admin",
21
- required: true,
22
- },
23
- adminName: {
24
- type: String,
25
- },
26
- date: {
27
- type: Date,
28
- default: Date.now,
29
- },
30
- });
31
-
32
- // Sample Site Audit Schema
33
- const sampleSiteAuditSchema = mongoose.Schema(
34
- {
35
- sampleSiteId: { type: mongoose.Types.ObjectId, ref: "SampleSites" },
36
- sampleSiteCode: { type: String }, // reference to SampleSites.code
37
- orgId: { type: mongoose.Types.ObjectId, ref: "Organizations" },
38
- timeUpdated: Date,
39
- deletedAt: { type: Date, default: Date.now }, // Only populated on delete
40
- sampleSiteLocation: {
41
- // Sample Site GPS location (lat, lon)
42
- type: { type: String, enum: ["Point"], required: true },
43
- coordinates: { type: [Number], required: true },
44
- },
45
- },
46
- {
47
- timestamps: true,
48
- }
49
- );
50
-
51
- // Create the SampleSiteAudit model
52
- const SampleSiteAudit = mongoose.model("SampleSiteAudit", sampleSiteAuditSchema);
53
-
54
- // Sample Sites Schema
55
- const sampleSitesSchema = mongoose.Schema(
56
- {
57
- name: { type: String },
58
- code: { type: String, unique: true },
59
- managedBy: { type: String },
60
- sponsoredBy: { type: String },
61
- lastSamplePeriodStartDate: { type: Date },
62
- lastSamplePeriodEndDate: { type: Date },
63
- lastSampleUploadDate: { type: Date },
64
- isPrivate: { type: Boolean, default: false, required: true },
65
- isActive: { type: Boolean, default: true, required: true },
66
- sponsor: { type: mongoose.Types.ObjectId, ref: "Organizations" },
67
- sponsorName: { type: String },
68
- /*
69
- * Location object: unified structure for all address and geospatial data.
70
- * - monitorLatitude and monitorLongitude are deprecated and no longer used.
71
- * - The location object (including location.gps) is now built in config/google.js.
72
- * - location.gps is a GeoJSON Point used for all geospatial queries (replaces gpsLocation).
73
- */
74
- location: {
75
- city: { type: String },
76
- state: { type: String },
77
- county: { type: String },
78
- street: { type: String },
79
- zip_code: { type: String },
80
- neighborhood: { type: String },
81
- gps: {
82
- type: {
83
- type: String,
84
- enum: ["Point"],
85
- required: true,
86
- },
87
- coordinates: {
88
- type: [Number],
89
- required: true,
90
- },
91
- },
92
- },
93
- context: { type: [String] },
94
- parameters: [
95
- {
96
- type: String,
97
- enum: sampleParametersEnum,
98
- },
99
- ],
100
- notes: { type: [noteSchema] },
101
- image: { type: String },
102
- siteId: {
103
- type: mongoose.Types.ObjectId,
104
- ref: "Sites",
105
- },
106
- },
107
- {
108
- timestamps: true,
109
- }
110
- );
111
-
112
- // Update pre-save hook
113
- sampleSitesSchema.pre('save', function (next) {
114
- if (!this.code) {
115
- this.code = generateSiteCode();
116
- }
117
- next();
118
- });
119
-
120
-
121
- // Geographic queries - already exists
122
- sampleSitesSchema.index({ "location.gps": "2dsphere" }); // use location.gps for geospatial queries
123
-
124
- // Sponsor-based queries
125
- sampleSitesSchema.index({ sponsor: 1, isPrivate: 1 });
126
-
127
- // Location-based filtering
128
- sampleSitesSchema.index({ "location.city": 1, "location.state": 1 });
129
- sampleSitesSchema.index({ "location.neighborhood": 1 });
130
- sampleSitesSchema.index({ "location.county": 1 });
131
-
132
- // Query parameter filtering
133
- sampleSitesSchema.index({ context: 1 });
134
- sampleSitesSchema.index({ parameters: 1 });
135
-
136
- // Pre-hook to log single document deletions
137
- sampleSitesSchema.pre("findOneAndDelete", async function () {
138
- const docToDelete = await this.model.findOne(this.getFilter()).lean();
139
- if (docToDelete) {
140
- let coordinates = null;
141
- if (docToDelete.location && docToDelete.location.gps && Array.isArray(docToDelete.location.gps.coordinates)) {
142
- coordinates = docToDelete.location.gps.coordinates;
143
- } else if (docToDelete.gpsLocation && Array.isArray(docToDelete.gpsLocation.coordinates)) {
144
- coordinates = docToDelete.gpsLocation.coordinates;
145
- }
146
- if (coordinates) {
147
- const auditLog = new SampleSiteAudit({
148
- sampleSiteId: docToDelete._id,
149
- sampleSiteCode: docToDelete.code,
150
- orgId: docToDelete.sponsor,
151
- timeUpdated: docToDelete.updatedAt,
152
- sampleSiteLocation: {
153
- type: "Point",
154
- coordinates,
155
- },
156
- deletedAt: new Date(),
157
- });
158
- await auditLog.save();
159
- }
160
- }
161
- });
162
-
163
- // Pre-hook to log multiple document deletions
164
- sampleSitesSchema.pre("deleteMany", async function () {
165
- const docsToDelete = await this.model.find(this.getFilter()).lean();
166
-
167
- if (docsToDelete.length) {
168
- const auditLogs = docsToDelete.map((doc) => {
169
- let coordinates = null;
170
- if (doc.location && doc.location.gps && Array.isArray(doc.location.gps.coordinates)) {
171
- coordinates = doc.location.gps.coordinates;
172
- } else if (doc.gpsLocation && Array.isArray(doc.gpsLocation.coordinates)) {
173
- coordinates = doc.gpsLocation.coordinates;
174
- }
175
- if (coordinates) {
176
- return {
177
- sampleSiteId: doc._id,
178
- sampleSiteCode: doc.code,
179
- orgId: doc.sponsor,
180
- timeUpdated: doc.updatedAt,
181
- sampleSiteLocation: {
182
- type: "Point",
183
- coordinates,
184
- },
185
- deletedAt: new Date(),
186
- };
187
- }
188
- return null;
189
- }).filter(Boolean);
190
-
191
- if (auditLogs.length) {
192
- await SampleSiteAudit.insertMany(auditLogs);
193
- }
194
- }
195
- });
196
-
197
- // Pre-hook to log a single document deletion (for deleteOne)
198
- sampleSitesSchema.pre("deleteOne", async function () {
199
- const docToDelete = await this.model.findOne(this.getFilter()).lean();
200
-
201
- if (docToDelete) {
202
- let coordinates = null;
203
- if (docToDelete.location && docToDelete.location.gps && Array.isArray(docToDelete.location.gps.coordinates)) {
204
- coordinates = docToDelete.location.gps.coordinates;
205
- } else if (docToDelete.gpsLocation && Array.isArray(docToDelete.gpsLocation.coordinates)) {
206
- coordinates = docToDelete.gpsLocation.coordinates;
207
- }
208
- if (coordinates) {
209
- const auditLog = new SampleSiteAudit({
210
- sampleSiteId: docToDelete._id,
211
- sampleSiteCode: docToDelete.code,
212
- orgId: docToDelete.sponsor,
213
- timeUpdated: docToDelete.updatedAt,
214
- sampleSiteLocation: {
215
- type: "Point",
216
- coordinates,
217
- },
218
- deletedAt: new Date(),
219
- });
220
- await auditLog.save();
221
- }
222
- }
223
- });
224
-
225
- // Pre-hook to log multiple document updates
226
- sampleSitesSchema.pre("updateMany", async function () {
227
- const docsToUpdate = await this.model.find(this.getFilter()).lean();
228
- if (docsToUpdate.length) {
229
- const auditLogs = docsToUpdate.map((doc) => {
230
- let coordinates = null;
231
- if (doc.location && doc.location.gps && Array.isArray(doc.location.gps.coordinates)) {
232
- coordinates = doc.location.gps.coordinates;
233
- } else if (doc.gpsLocation && Array.isArray(doc.gpsLocation.coordinates)) {
234
- coordinates = doc.gpsLocation.coordinates;
235
- }
236
- if (coordinates) {
237
- return {
238
- sampleSiteId: doc._id,
239
- sampleSiteCode: doc.code,
240
- orgId: doc.sponsor,
241
- timeUpdated: doc.updatedAt,
242
- sampleSiteLocation: {
243
- type: "Point",
244
- coordinates,
245
- },
246
- deletedAt: null, // Not a deletion, so this field is null
247
- };
248
- }
249
- return null;
250
- }).filter(Boolean);
251
-
252
- if (auditLogs.length) {
253
- await SampleSiteAudit.insertMany(auditLogs);
254
- }
255
- }
256
- });
257
-
258
- // Create the SampleSites model
259
- const SampleSites = mongoose.model("SampleSites", sampleSitesSchema);
260
-
1
+ import mongoose from "mongoose";
2
+ import { customAlphabet } from 'nanoid';
3
+ import { HEAVY_METALS } from "../constants/pollutants.js";
4
+
5
+ const generateSiteCode = customAlphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZ', 8);
6
+
7
+ const sampleParametersEnum = Object.keys(HEAVY_METALS);
8
+
9
+ const noteSchema = mongoose.Schema({
10
+ note: {
11
+ type: String,
12
+ required: true,
13
+ },
14
+ type: {
15
+ type: String,
16
+ required: true,
17
+ },
18
+ adminId: {
19
+ type: mongoose.Types.ObjectId,
20
+ ref: "Admin",
21
+ required: true,
22
+ },
23
+ adminName: {
24
+ type: String,
25
+ },
26
+ date: {
27
+ type: Date,
28
+ default: Date.now,
29
+ },
30
+ });
31
+
32
+ // Sample Site Audit Schema
33
+ const sampleSiteAuditSchema = mongoose.Schema(
34
+ {
35
+ sampleSiteId: { type: mongoose.Types.ObjectId, ref: "SampleSites" },
36
+ sampleSiteCode: { type: String }, // reference to SampleSites.code
37
+ orgId: { type: mongoose.Types.ObjectId, ref: "Organizations" },
38
+ timeUpdated: Date,
39
+ deletedAt: { type: Date, default: Date.now }, // Only populated on delete
40
+ sampleSiteLocation: {
41
+ // Sample Site GPS location (lat, lon)
42
+ type: { type: String, enum: ["Point"], required: true },
43
+ coordinates: { type: [Number], required: true },
44
+ },
45
+ },
46
+ {
47
+ timestamps: true,
48
+ }
49
+ );
50
+
51
+ // Create the SampleSiteAudit model
52
+ const SampleSiteAudit = mongoose.model("SampleSiteAudit", sampleSiteAuditSchema);
53
+
54
+ // Sample Sites Schema
55
+ const sampleSitesSchema = mongoose.Schema(
56
+ {
57
+ name: { type: String },
58
+ code: { type: String, unique: true },
59
+ managedBy: { type: String },
60
+ sponsoredBy: { type: String },
61
+ lastSamplePeriodStartDate: { type: Date },
62
+ lastSamplePeriodEndDate: { type: Date },
63
+ lastSampleUploadDate: { type: Date },
64
+ isPrivate: { type: Boolean, default: false, required: true },
65
+ isActive: { type: Boolean, default: true, required: true },
66
+ sponsor: { type: mongoose.Types.ObjectId, ref: "Organizations" },
67
+ sponsorName: { type: String },
68
+ /*
69
+ * Location object: unified structure for all address and geospatial data.
70
+ * - monitorLatitude and monitorLongitude are deprecated and no longer used.
71
+ * - The location object (including location.gps) is now built in config/google.js.
72
+ * - location.gps is a GeoJSON Point used for all geospatial queries (replaces gpsLocation).
73
+ */
74
+ location: {
75
+ city: { type: String },
76
+ state: { type: String },
77
+ county: { type: String },
78
+ street: { type: String },
79
+ zip_code: { type: String },
80
+ neighborhood: { type: String },
81
+ // GPS is optional (legacy docs predate it — D14), but a partial Point is
82
+ // invalid: providing either half makes both halves required.
83
+ gps: {
84
+ type: {
85
+ type: String,
86
+ enum: ["Point"],
87
+ required: function () {
88
+ const gps = this.location?.gps;
89
+ return Array.isArray(gps?.coordinates) && gps.coordinates.length > 0;
90
+ },
91
+ },
92
+ coordinates: {
93
+ type: [Number],
94
+ // no [] default — an empty coordinates array is invalid GeoJSON and
95
+ // would poison the 2dsphere index
96
+ default: undefined,
97
+ required: function () {
98
+ return this.location?.gps?.type != null;
99
+ },
100
+ },
101
+ },
102
+ },
103
+ context: { type: [String] },
104
+ parameters: [
105
+ {
106
+ type: String,
107
+ enum: sampleParametersEnum,
108
+ },
109
+ ],
110
+ notes: { type: [noteSchema] },
111
+ image: { type: String },
112
+ siteId: {
113
+ type: mongoose.Types.ObjectId,
114
+ ref: "Sites",
115
+ },
116
+ },
117
+ {
118
+ timestamps: true,
119
+ }
120
+ );
121
+
122
+ // Update pre-save hook
123
+ sampleSitesSchema.pre('save', function () {
124
+ if (!this.code) {
125
+ this.code = generateSiteCode();
126
+ }
127
+ });
128
+
129
+
130
+ // Geographic queries - already exists
131
+ sampleSitesSchema.index({ "location.gps": "2dsphere" }); // use location.gps for geospatial queries
132
+
133
+ // Sponsor-based queries
134
+ sampleSitesSchema.index({ sponsor: 1, isPrivate: 1 });
135
+
136
+ // Location-based filtering
137
+ sampleSitesSchema.index({ "location.city": 1, "location.state": 1 });
138
+ sampleSitesSchema.index({ "location.neighborhood": 1 });
139
+ sampleSitesSchema.index({ "location.county": 1 });
140
+
141
+ // Query parameter filtering
142
+ sampleSitesSchema.index({ context: 1 });
143
+ sampleSitesSchema.index({ parameters: 1 });
144
+
145
+ // Pre-hook to log single document deletions
146
+ sampleSitesSchema.pre("findOneAndDelete", async function () {
147
+ const docToDelete = await this.model.findOne(this.getFilter()).lean();
148
+ if (docToDelete) {
149
+ let coordinates = null;
150
+ if (docToDelete.location && docToDelete.location.gps && Array.isArray(docToDelete.location.gps.coordinates)) {
151
+ coordinates = docToDelete.location.gps.coordinates;
152
+ } else if (docToDelete.gpsLocation && Array.isArray(docToDelete.gpsLocation.coordinates)) {
153
+ coordinates = docToDelete.gpsLocation.coordinates;
154
+ }
155
+ if (coordinates) {
156
+ const auditLog = new SampleSiteAudit({
157
+ sampleSiteId: docToDelete._id,
158
+ sampleSiteCode: docToDelete.code,
159
+ orgId: docToDelete.sponsor,
160
+ timeUpdated: docToDelete.updatedAt,
161
+ sampleSiteLocation: {
162
+ type: "Point",
163
+ coordinates,
164
+ },
165
+ deletedAt: new Date(),
166
+ });
167
+ await auditLog.save();
168
+ }
169
+ }
170
+ });
171
+
172
+ // Pre-hook to log multiple document deletions
173
+ sampleSitesSchema.pre("deleteMany", async function () {
174
+ const docsToDelete = await this.model.find(this.getFilter()).lean();
175
+
176
+ if (docsToDelete.length) {
177
+ const auditLogs = docsToDelete.map((doc) => {
178
+ let coordinates = null;
179
+ if (doc.location && doc.location.gps && Array.isArray(doc.location.gps.coordinates)) {
180
+ coordinates = doc.location.gps.coordinates;
181
+ } else if (doc.gpsLocation && Array.isArray(doc.gpsLocation.coordinates)) {
182
+ coordinates = doc.gpsLocation.coordinates;
183
+ }
184
+ if (coordinates) {
185
+ return {
186
+ sampleSiteId: doc._id,
187
+ sampleSiteCode: doc.code,
188
+ orgId: doc.sponsor,
189
+ timeUpdated: doc.updatedAt,
190
+ sampleSiteLocation: {
191
+ type: "Point",
192
+ coordinates,
193
+ },
194
+ deletedAt: new Date(),
195
+ };
196
+ }
197
+ return null;
198
+ }).filter(Boolean);
199
+
200
+ if (auditLogs.length) {
201
+ await SampleSiteAudit.insertMany(auditLogs);
202
+ }
203
+ }
204
+ });
205
+
206
+ // Pre-hook to log a single document deletion (for deleteOne)
207
+ sampleSitesSchema.pre("deleteOne", async function () {
208
+ const docToDelete = await this.model.findOne(this.getFilter()).lean();
209
+
210
+ if (docToDelete) {
211
+ let coordinates = null;
212
+ if (docToDelete.location && docToDelete.location.gps && Array.isArray(docToDelete.location.gps.coordinates)) {
213
+ coordinates = docToDelete.location.gps.coordinates;
214
+ } else if (docToDelete.gpsLocation && Array.isArray(docToDelete.gpsLocation.coordinates)) {
215
+ coordinates = docToDelete.gpsLocation.coordinates;
216
+ }
217
+ if (coordinates) {
218
+ const auditLog = new SampleSiteAudit({
219
+ sampleSiteId: docToDelete._id,
220
+ sampleSiteCode: docToDelete.code,
221
+ orgId: docToDelete.sponsor,
222
+ timeUpdated: docToDelete.updatedAt,
223
+ sampleSiteLocation: {
224
+ type: "Point",
225
+ coordinates,
226
+ },
227
+ deletedAt: new Date(),
228
+ });
229
+ await auditLog.save();
230
+ }
231
+ }
232
+ });
233
+
234
+ // Pre-hook to log multiple document updates
235
+ sampleSitesSchema.pre("updateMany", async function () {
236
+ const docsToUpdate = await this.model.find(this.getFilter()).lean();
237
+ if (docsToUpdate.length) {
238
+ const auditLogs = docsToUpdate.map((doc) => {
239
+ let coordinates = null;
240
+ if (doc.location && doc.location.gps && Array.isArray(doc.location.gps.coordinates)) {
241
+ coordinates = doc.location.gps.coordinates;
242
+ } else if (doc.gpsLocation && Array.isArray(doc.gpsLocation.coordinates)) {
243
+ coordinates = doc.gpsLocation.coordinates;
244
+ }
245
+ if (coordinates) {
246
+ return {
247
+ sampleSiteId: doc._id,
248
+ sampleSiteCode: doc.code,
249
+ orgId: doc.sponsor,
250
+ timeUpdated: doc.updatedAt,
251
+ sampleSiteLocation: {
252
+ type: "Point",
253
+ coordinates,
254
+ },
255
+ deletedAt: null, // Not a deletion, so this field is null
256
+ };
257
+ }
258
+ return null;
259
+ }).filter(Boolean);
260
+
261
+ if (auditLogs.length) {
262
+ await SampleSiteAudit.insertMany(auditLogs);
263
+ }
264
+ }
265
+ });
266
+
267
+ // Create the SampleSites model
268
+ const SampleSites = mongoose.model("SampleSites", sampleSitesSchema);
269
+
261
270
  export { sampleSitesSchema, SampleSites, sampleSiteAuditSchema, SampleSiteAudit, sampleParametersEnum };
@@ -1,55 +1,65 @@
1
- import mongoose from "mongoose";
2
-
3
- // Sites Schema
4
- const sitesSchema = mongoose.Schema(
5
- {
6
- name: { type: String },
7
- sampleSiteId: {
8
- type: mongoose.Types.ObjectId,
9
- ref: "SampleSites",
10
- },
11
- monitorIds: {
12
- type: [{ type: mongoose.Types.ObjectId, ref: "Monitors" }],
13
- default: undefined,
14
- },
15
- sponsor: { type: mongoose.Types.ObjectId, ref: "Organizations" },
16
- location: {
17
- city: { type: String },
18
- state: { type: String },
19
- county: { type: String },
20
- street: { type: String },
21
- zip_code: { type: String },
22
- neighborhood: { type: String },
23
- gps: {
24
- type: {
25
- type: String,
26
- enum: ["Point"],
27
- required: true,
28
- },
29
- coordinates: {
30
- type: [Number],
31
- required: true,
32
- },
33
- },
34
- },
35
- isActive: { type: Boolean, default: true },
36
- },
37
- {
38
- timestamps: true,
39
- }
40
- );
41
-
42
- // Geographic queries
43
- sitesSchema.index({ "location.gps": "2dsphere" });
44
-
45
- // Sponsor-based queries
46
- sitesSchema.index({ sponsor: 1, isActive: 1 });
47
-
48
- // Back-reference lookups
49
- sitesSchema.index({ sampleSiteId: 1 });
50
- sitesSchema.index({ monitorIds: 1 });
51
-
52
- // Create the Sites model
53
- const Sites = mongoose.model("Sites", sitesSchema);
54
-
55
- export { sitesSchema, Sites };
1
+ import mongoose from "mongoose";
2
+
3
+ // Sites Schema
4
+ const sitesSchema = mongoose.Schema(
5
+ {
6
+ name: { type: String },
7
+ sampleSiteId: {
8
+ type: mongoose.Types.ObjectId,
9
+ ref: "SampleSites",
10
+ },
11
+ monitorIds: {
12
+ type: [{ type: mongoose.Types.ObjectId, ref: "Monitors" }],
13
+ default: undefined,
14
+ },
15
+ sponsor: { type: mongoose.Types.ObjectId, ref: "Organizations" },
16
+ location: {
17
+ city: { type: String },
18
+ state: { type: String },
19
+ county: { type: String },
20
+ street: { type: String },
21
+ zip_code: { type: String },
22
+ neighborhood: { type: String },
23
+ // GPS is optional (legacy docs predate it — D14), but a partial Point is
24
+ // invalid: providing either half makes both halves required.
25
+ gps: {
26
+ type: {
27
+ type: String,
28
+ enum: ["Point"],
29
+ required: function () {
30
+ const gps = this.location?.gps;
31
+ return Array.isArray(gps?.coordinates) && gps.coordinates.length > 0;
32
+ },
33
+ },
34
+ coordinates: {
35
+ type: [Number],
36
+ // no [] default — an empty coordinates array is invalid GeoJSON and
37
+ // would poison the 2dsphere index
38
+ default: undefined,
39
+ required: function () {
40
+ return this.location?.gps?.type != null;
41
+ },
42
+ },
43
+ },
44
+ },
45
+ isActive: { type: Boolean, default: true },
46
+ },
47
+ {
48
+ timestamps: true,
49
+ }
50
+ );
51
+
52
+ // Geographic queries
53
+ sitesSchema.index({ "location.gps": "2dsphere" });
54
+
55
+ // Sponsor-based queries
56
+ sitesSchema.index({ sponsor: 1, isActive: 1 });
57
+
58
+ // Back-reference lookups
59
+ sitesSchema.index({ sampleSiteId: 1 });
60
+ sitesSchema.index({ monitorIds: 1 });
61
+
62
+ // Create the Sites model
63
+ const Sites = mongoose.model("Sites", sitesSchema);
64
+
65
+ export { sitesSchema, Sites };