tabletcommand-location 2.3.8 → 2.4.1

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.
Files changed (43) hide show
  1. package/build/audience.js +38 -0
  2. package/build/audience.js.map +1 -0
  3. package/build/index.js +38 -20
  4. package/build/index.js.map +1 -1
  5. package/build/location.js +63 -6
  6. package/build/location.js.map +1 -1
  7. package/build/query.js +373 -0
  8. package/build/query.js.map +1 -0
  9. package/build/store.js +54 -5
  10. package/build/store.js.map +1 -1
  11. package/build/test/location.js +313 -0
  12. package/build/test/location.js.map +1 -1
  13. package/build/test/mock.js +5 -0
  14. package/build/test/mock.js.map +1 -1
  15. package/build/test/query.js +174 -0
  16. package/build/test/query.js.map +1 -0
  17. package/cspell.json +5 -1
  18. package/definitions/audience.d.ts +18 -0
  19. package/definitions/audience.d.ts.map +1 -0
  20. package/definitions/index.d.ts +1 -0
  21. package/definitions/index.d.ts.map +1 -1
  22. package/definitions/location.d.ts.map +1 -1
  23. package/definitions/query.d.ts +36 -0
  24. package/definitions/query.d.ts.map +1 -0
  25. package/definitions/store.d.ts +5 -0
  26. package/definitions/store.d.ts.map +1 -1
  27. package/definitions/test/mock.d.ts +15 -0
  28. package/definitions/test/mock.d.ts.map +1 -1
  29. package/definitions/test/query.d.ts +2 -0
  30. package/definitions/test/query.d.ts.map +1 -0
  31. package/definitions/types.d.ts +5 -0
  32. package/definitions/types.d.ts.map +1 -1
  33. package/package.json +18 -14
  34. package/src/audience.ts +54 -0
  35. package/src/index.ts +5 -20
  36. package/src/location.ts +67 -6
  37. package/src/query.ts +492 -0
  38. package/src/store.ts +55 -5
  39. package/src/test/location.ts +306 -1
  40. package/src/test/mock.ts +6 -0
  41. package/src/test/query.ts +209 -0
  42. package/src/types.ts +15 -0
  43. package/test.sh +1 -1
package/src/location.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  import _ from "lodash";
2
2
  import {
3
+ AccountIndustry,
3
4
  Department,
4
5
  Location,
5
6
  CADVehicle,
7
+ LocationKindType,
6
8
  LocationVisibility,
7
9
  } from "tabletcommand-backend-models";
8
10
  import {
@@ -49,7 +51,20 @@ export function location(store: StoreModule) {
49
51
  }
50
52
  }
51
53
 
52
- const userId = `${item.radioName}-${item.vehicleId}`; // Fake user id radioName - vehicleId
54
+ // Originally always was this fake id, but now can override these values
55
+ // If overridden then we use the values on the record instead of the fake userId
56
+ let userId = `${item.radioName}-${item.vehicleId}`; // Fake user id radioName - vehicleId
57
+ if (_.isString(item.userId) && _.trim(item.userId) !== "") {
58
+ userId = item.userId;
59
+ }
60
+ let session = userId;
61
+ if (_.isString(item.session) && _.trim(item.session) !== "") {
62
+ session = item.session;
63
+ }
64
+ let username = item.radioName;
65
+ if (_.isString(item.username) && _.trim(item.username) !== "") {
66
+ username = item.username;
67
+ }
53
68
 
54
69
  let sharingEnabled = false;
55
70
  let opAreaName = "";
@@ -72,7 +87,9 @@ export function location(store: StoreModule) {
72
87
  }
73
88
  }
74
89
  const deleteAfterDate = new Date(atDate.valueOf() + 1000 * 60 * locationStaleMinutes);
75
- const personnelUUID = "";
90
+ const personnelUUID = _.isString(item.personnelUUID) ? item.personnelUUID : "";
91
+ const transponderUUID = _.isString(item.transponderUUID) ? item.transponderUUID : "";
92
+ const industry: AccountIndustry = department?.industry ?? AccountIndustry.Fire; // If not set, fallback to fire
76
93
 
77
94
  const cadAVL: Partial<Location> = {
78
95
  device_type: deviceType,
@@ -86,10 +103,10 @@ export function location(store: StoreModule) {
86
103
  modified: atDate,
87
104
  movedAt: new Date(movedAtUnix * 1000),
88
105
  active: true,
89
- username: item.radioName,
106
+ username,
90
107
  userId,
91
108
  departmentId: String(department._id),
92
- session: userId,
109
+ session,
93
110
  version: 2,
94
111
  shared: sharingEnabled,
95
112
  sendToCAD: false,
@@ -98,6 +115,8 @@ export function location(store: StoreModule) {
98
115
  state: deptState,
99
116
  visibility: [], // will be updated after .active is set
100
117
  personnelUUID,
118
+ transponderUUID,
119
+ industry,
101
120
  };
102
121
 
103
122
  if (_.isNumber(item.speed)) {
@@ -165,12 +184,46 @@ export function location(store: StoreModule) {
165
184
  return cadItem;
166
185
  }
167
186
 
187
+ // Returns true when the item is a person location and was fully handled here
188
+ // Returns false if trying to handle vehicle item
189
+ async function processPersonItem(decoded: DecodedLocation, item: LocationPayload, atDate: Date): Promise<boolean> {
190
+ const isPerson = decoded.cadAVL.kindType === LocationKindType.Person ||
191
+ decoded.cadAVL.locationType === LocationKindType.Person;
192
+ if (!isPerson) {
193
+ return false;
194
+ }
195
+ const hasPersonnelUUID = _.isString(decoded.cadAVL.personnelUUID) && decoded.cadAVL.personnelUUID !== "";
196
+ const hasTransponderUUID = _.isString(decoded.cadAVL.transponderUUID) && decoded.cadAVL.transponderUUID !== "";
197
+ if (!hasPersonnelUUID && !hasTransponderUUID) {
198
+ // A person Location with neither personnelUUID nor transponderUUID
199
+ // would leak into the vehicle sync endpoints (they filter on
200
+ // personnelUUID: "") so to process person must have one or other
201
+ debug(`skipping person location without personnelUUID/transponderUUID: ${JSON.stringify(item)}`);
202
+ return true;
203
+ }
204
+ // Person records: no CAD vehicle, always active/visible, never shared or CAD.
205
+ // Stamp kindType so records classified by locationType alone still match
206
+ // the person feed query (kindType: "person")
207
+ decoded.cadAVL.kindType = LocationKindType.Person;
208
+ decoded.cadAVL.active = true;
209
+ decoded.cadAVL.shared = false;
210
+ decoded.cadAVL.sendToCAD = false;
211
+ decoded.cadAVL.visibility = [LocationVisibility.Visible];
212
+ await store.updateLocation(decoded.cadAVL, atDate);
213
+ return true;
214
+ }
215
+
168
216
  async function processItems(department: Partial<Department>, items: LocationPayload[], atDate: Date) {
169
217
  for (const item of items) {
170
218
  const decoded = decodeLocation(item, department, atDate, "cad");
171
219
  debug(`processCADVehicleLocation item: ${JSON.stringify(item)} d:${JSON.stringify(department)} at: ${atDate.toISOString()}.`);
172
220
  if (!decoded.isValid) {
173
- return;
221
+ // Skip only the invalid item, not the rest of the batch
222
+ continue;
223
+ }
224
+
225
+ if (await processPersonItem(decoded, item, atDate)) {
226
+ continue;
174
227
  }
175
228
 
176
229
  let active = true;
@@ -203,7 +256,15 @@ export function location(store: StoreModule) {
203
256
  const decoded = decodeLocation(item, department, atDate, "geotab");
204
257
  debug(`processItemsDeviceMappingGeotab item: ${JSON.stringify(item)} d:${JSON.stringify(department)} at: ${atDate.toISOString()}.`);
205
258
  if (!decoded.isValid) {
206
- return;
259
+ // Skip only the invalid item, not the rest of the batch
260
+ // This is actually a fix for vehicle processing as well
261
+ continue;
262
+ }
263
+
264
+ // Defensive code technically we shouldn't be getting here with person
265
+ // But it would still be valid to use this with a person so added the logic
266
+ if (await processPersonItem(decoded, item, atDate)) {
267
+ continue;
207
268
  }
208
269
 
209
270
  let active = true;
package/src/query.ts ADDED
@@ -0,0 +1,492 @@
1
+ import {
2
+ FilterQuery,
3
+ ProjectionType,
4
+ SortOrder,
5
+ } from "mongoose";
6
+
7
+ import {
8
+ Location,
9
+ } from "tabletcommand-backend-models";
10
+
11
+ export type MQP = {
12
+ list: FilterQuery<Location>,
13
+ projection: ProjectionType<Location>,
14
+ sortBy?: Record<string, SortOrder>,
15
+ };
16
+
17
+ export type AdminListLocationParams1 = {
18
+ departmentId: string,
19
+ movedAt: Date,
20
+ showAll?: boolean,
21
+ };
22
+
23
+ export type AdminListLocationParams2 = {
24
+ departmentId: string,
25
+ movedAt: Date,
26
+ longitude: number,
27
+ latitude: number,
28
+ diagonal: number,
29
+ showAll?: boolean,
30
+ };
31
+
32
+ export type SyncModifiedDate = {
33
+ modifiedDate: Date,
34
+ };
35
+
36
+ export const metersToRadians = 6378100; // The equatorial radius of the Earth in meters
37
+
38
+ function centerSphereFilter(longitude: number, latitude: number, diagonal: number) {
39
+ return {
40
+ $geoWithin: {
41
+ $centerSphere: [
42
+ [longitude, latitude],
43
+ diagonal / metersToRadians,
44
+ ] as [[number, number], number],
45
+ },
46
+ };
47
+ }
48
+
49
+ // Transponder-tracked person records carry an empty personnelUUID;
50
+ // they surface via the person/responder feeds, never in unit/vehicle lists
51
+ const excludePersonAndTransponderRecords = [
52
+ {
53
+ $or: [
54
+ { personnelUUID: "" },
55
+ { personnelUUID: { $exists: false } }, // TODO: Remove $exists check once all location services are running the latest
56
+ ],
57
+ },
58
+ {
59
+ $or: [
60
+ { transponderUUID: "" },
61
+ { transponderUUID: { $exists: false } },
62
+ ],
63
+ },
64
+ ];
65
+
66
+ // Same fields defined in the frontend
67
+ // api/a1/location/units and api/a1/location/units-shared
68
+ const projectionAdmin = {
69
+ _id: 1,
70
+ active: 1,
71
+ altitude: 1,
72
+ color: 1,
73
+ device_type: 1,
74
+ kindType: 1,
75
+ location: 1,
76
+ locationGeoJSON: 1,
77
+ modified: 1,
78
+ movedAt: 1,
79
+ sendToCAD: 1,
80
+ source: 1,
81
+ speed: 1,
82
+ uuid: 1, // Legacy, remove later
83
+ };
84
+
85
+ export function adminListLocations(params: AdminListLocationParams1): MQP {
86
+ const {
87
+ departmentId,
88
+ movedAt,
89
+ showAll,
90
+ } = params;
91
+
92
+ const query: FilterQuery<Location> = {
93
+ // TODO: Replace models.Location active: true with visibility in "visible"
94
+ active: true,
95
+ departmentId,
96
+ $and: excludePersonAndTransponderRecords,
97
+ kindType: {
98
+ $ne: "uav",
99
+ },
100
+ // 03/14/23 (Joe) - Trying movedAt only [sc-11794] - determined modified was being overwritten by cad vehicle status updates
101
+ movedAt: {
102
+ $gte: movedAt,
103
+ },
104
+ };
105
+ if (showAll) {
106
+ delete query.active;
107
+ }
108
+
109
+ return {
110
+ list: query,
111
+ projection: projectionAdmin,
112
+ };
113
+ }
114
+
115
+ export function adminListPersonLocations(params: AdminListLocationParams1): MQP {
116
+ const {
117
+ departmentId,
118
+ movedAt,
119
+ showAll
120
+ } = params;
121
+
122
+ const query: FilterQuery<Location> = {
123
+ active: true,
124
+ departmentId,
125
+ kindType: "person",
126
+ movedAt: {
127
+ $gte: movedAt,
128
+ },
129
+ };
130
+ if (showAll) {
131
+ delete query.active;
132
+ }
133
+
134
+ // Same fields defined in the frontend
135
+ // api/a1/location/person
136
+ const projection = {
137
+ _id: 1,
138
+ active: 1,
139
+ altitude: 1,
140
+ color: 1,
141
+ device_type: 1,
142
+ kindType: 1,
143
+ location: 1,
144
+ locationGeoJSON: 1,
145
+ modified: 1,
146
+ movedAt: 1,
147
+ personnelUUID: 1,
148
+ sendToCAD: 1,
149
+ source: 1,
150
+ speed: 1,
151
+ transponderUUID: 1,
152
+ uuid: 1, // Legacy, remove later
153
+ };
154
+
155
+ return {
156
+ list: query,
157
+ projection,
158
+ };
159
+ }
160
+
161
+ export function adminListUAVLocations(params: AdminListLocationParams1): MQP {
162
+ const {
163
+ departmentId,
164
+ movedAt,
165
+ showAll,
166
+ } = params;
167
+
168
+ const query: FilterQuery<Location> = {
169
+ active: true,
170
+ departmentId,
171
+ kindType: "uav",
172
+ movedAt: {
173
+ $gte: movedAt,
174
+ },
175
+ };
176
+ if (showAll) {
177
+ delete query.active;
178
+ }
179
+
180
+ const projection = {
181
+ _id: 1,
182
+ active: 1,
183
+ agencyCode: 1,
184
+ agencyName: 1,
185
+ altitude: 1,
186
+ color: 1,
187
+ device_type: 1,
188
+ heading: 1,
189
+ kindType: 1,
190
+ locationGeoJSON: 1,
191
+ modified: 1,
192
+ movedAt: 1,
193
+ opAreaCode: 1,
194
+ opAreaName: 1,
195
+ sendToCAD: 1,
196
+ source: 1,
197
+ speed: 1,
198
+ state: 1,
199
+ userId: 1,
200
+ username: 1,
201
+ uuid: 1, // Legacy, remove later
202
+ vehicleId: 1,
203
+ };
204
+
205
+ return {
206
+ list: query,
207
+ projection,
208
+ };
209
+ }
210
+
211
+ export function adminListAircraftLocations(params: AdminListLocationParams2): MQP {
212
+ const {
213
+ departmentId,
214
+ movedAt,
215
+ longitude,
216
+ latitude,
217
+ diagonal
218
+ } = params;
219
+
220
+ const list = {
221
+ shared: true,
222
+ active: true,
223
+ source: "ADSB",
224
+ kindType: {
225
+ $in: ["fixed-wing", "helicopter"],
226
+ },
227
+ departmentId: {
228
+ $ne: departmentId,
229
+ },
230
+ movedAt: {
231
+ $gte: movedAt.toISOString(),
232
+ },
233
+ locationGeoJSON: centerSphereFilter(longitude, latitude, diagonal),
234
+ };
235
+
236
+ return {
237
+ list,
238
+ projection: projectionAdmin,
239
+ };
240
+ }
241
+
242
+ export function adminListSharedLocations(params: AdminListLocationParams2): MQP {
243
+ const {
244
+ departmentId,
245
+ movedAt,
246
+ longitude,
247
+ latitude,
248
+ diagonal
249
+ } = params;
250
+
251
+ const listSharedAVLQuery = {
252
+ shared: true,
253
+ active: true,
254
+ source: {
255
+ $ne: "ADSB"
256
+ },
257
+ departmentId: {
258
+ $ne: departmentId,
259
+ },
260
+ kindType: {
261
+ $ne: "uav",
262
+ },
263
+ movedAt: {
264
+ $gte: movedAt.toISOString(),
265
+ },
266
+ locationGeoJSON: centerSphereFilter(longitude, latitude, diagonal),
267
+ };
268
+
269
+ return {
270
+ list: listSharedAVLQuery,
271
+ projection: projectionAdmin,
272
+ };
273
+ }
274
+
275
+ export function adminListFocusedLocations(params: AdminListLocationParams2): MQP {
276
+ const {
277
+ departmentId,
278
+ movedAt,
279
+ longitude,
280
+ latitude,
281
+ diagonal,
282
+ showAll
283
+ } = params;
284
+
285
+ const listFocusedAVLQuery: FilterQuery<Location> = {
286
+ active: true,
287
+ departmentId,
288
+ movedAt: {
289
+ $gte: movedAt.toISOString(),
290
+ },
291
+ locationGeoJSON: centerSphereFilter(longitude, latitude, diagonal),
292
+ };
293
+
294
+ if (showAll) {
295
+ delete listFocusedAVLQuery.locationGeoJSON;
296
+ }
297
+
298
+ return {
299
+ list: listFocusedAVLQuery,
300
+ projection: projectionAdmin,
301
+ };
302
+ }
303
+
304
+ // Sync queries
305
+
306
+ const projectionSync: ProjectionType<Location> = {
307
+ active: 1,
308
+ departmentId: 1,
309
+ device_type: 1, // used in web ui
310
+ location: 1,
311
+ locationGeoJSON: 1,
312
+ modified: 1,
313
+ userId: 1,
314
+ username: 1,
315
+ uuid: 1, // used in iOS
316
+ // both used by AVL sharing
317
+ opAreaName: 1,
318
+ opAreaCode: 1,
319
+ shared: 1,
320
+ state: 1,
321
+ // v3
322
+ altitude: 1,
323
+ speed: 1,
324
+ movedAt: 1,
325
+ agencyName: 1,
326
+ agencyCode: 1,
327
+ // v4
328
+ color: 1,
329
+ colorChangedAt: 1,
330
+ propsChangedAt: 1,
331
+ heading: 1,
332
+ // v4 extra
333
+ kindType: 1,
334
+ source: 1,
335
+ personnelUUID: 1,
336
+ transponderUUID: 1,
337
+ // v4 kind/type/locationType
338
+ kind: 1,
339
+ type: 1,
340
+ locationType: 1,
341
+ kindColor: 1,
342
+ };
343
+
344
+ export function syncListSharedLocations(params: AdminListLocationParams2 & SyncModifiedDate): MQP {
345
+ const {
346
+ departmentId,
347
+ modifiedDate,
348
+ movedAt,
349
+ longitude,
350
+ latitude,
351
+ diagonal,
352
+ } = params;
353
+
354
+ const list: FilterQuery<Location> = {
355
+ shared: true,
356
+ source: {
357
+ $nin: ["ADSB", "DroneSense"]
358
+ },
359
+ departmentId: {
360
+ $ne: departmentId,
361
+ },
362
+ modified: {
363
+ $gte: modifiedDate.toISOString(),
364
+ },
365
+ movedAt: {
366
+ $gte: movedAt.toISOString(),
367
+ },
368
+ locationGeoJSON: centerSphereFilter(longitude, latitude, diagonal),
369
+ };
370
+
371
+ return {
372
+ list,
373
+ projection: projectionSync,
374
+ };
375
+ }
376
+
377
+ export function syncListLocations(params: AdminListLocationParams1 & SyncModifiedDate): MQP {
378
+ const {
379
+ departmentId,
380
+ modifiedDate,
381
+ movedAt,
382
+ } = params;
383
+
384
+ const list: FilterQuery<Location> = {
385
+ departmentId,
386
+ $and: excludePersonAndTransponderRecords,
387
+ kindType: {
388
+ $ne: "uav"
389
+ },
390
+ modified: {
391
+ $gte: modifiedDate.toISOString(),
392
+ },
393
+ movedAt: {
394
+ $gte: movedAt.toISOString(),
395
+ }
396
+ };
397
+
398
+ return {
399
+ list,
400
+ projection: projectionSync,
401
+ };
402
+ }
403
+
404
+ export function syncListPersonLocations(params: AdminListLocationParams1 & SyncModifiedDate): MQP {
405
+ const {
406
+ departmentId,
407
+ modifiedDate,
408
+ movedAt,
409
+ } = params;
410
+
411
+ const list: FilterQuery<Location> = {
412
+ departmentId,
413
+ kindType: "person",
414
+ modified: {
415
+ $gte: modifiedDate.toISOString(),
416
+ },
417
+ movedAt: {
418
+ $gte: movedAt.toISOString(),
419
+ },
420
+ };
421
+
422
+ return {
423
+ list,
424
+ projection: projectionSync,
425
+ };
426
+ }
427
+
428
+ export function syncListAircraftLocations(params: AdminListLocationParams2 & SyncModifiedDate): MQP {
429
+ const {
430
+ departmentId,
431
+ movedAt,
432
+ modifiedDate,
433
+ // Possibly missing
434
+ longitude,
435
+ latitude,
436
+ diagonal
437
+ } = params;
438
+
439
+ const list: FilterQuery<Location> = {
440
+ shared: true,
441
+ source: "ADSB",
442
+ kindType: {
443
+ $in: ["fixed-wing", "helicopter"]
444
+ },
445
+ departmentId: {
446
+ $ne: departmentId,
447
+ },
448
+ modified: {
449
+ $gte: modifiedDate.toISOString(),
450
+ },
451
+ movedAt: {
452
+ $gte: movedAt.toISOString(),
453
+ }
454
+ };
455
+
456
+ const hasCoordinate = Math.abs(latitude) > 0.01 || Math.abs(longitude) > 0.01;
457
+ const validCoordinate = Math.abs(latitude) < 90 && Math.abs(longitude) < 180;
458
+ const filterOnLocation = hasCoordinate && validCoordinate;
459
+ if (filterOnLocation) {
460
+ list.locationGeoJSON = centerSphereFilter(longitude, latitude, diagonal);
461
+ }
462
+
463
+ return {
464
+ list,
465
+ projection: projectionSync,
466
+ };
467
+ }
468
+
469
+ export function syncListUAVLocations(params: AdminListLocationParams1 & SyncModifiedDate): MQP {
470
+ const {
471
+ departmentId,
472
+ modifiedDate,
473
+ movedAt,
474
+
475
+ } = params;
476
+
477
+ const list: FilterQuery<Location> = {
478
+ departmentId,
479
+ kindType: "uav",
480
+ modified: {
481
+ $gte: modifiedDate.toISOString(),
482
+ },
483
+ movedAt: {
484
+ $gte: movedAt.toISOString(),
485
+ },
486
+ };
487
+
488
+ return {
489
+ list,
490
+ projection: projectionSync,
491
+ };
492
+ }
package/src/store.ts CHANGED
@@ -2,6 +2,7 @@ import debugModule from "debug";
2
2
  import {
3
3
  DepartmentModel,
4
4
  Location,
5
+ LocationKindType,
5
6
  LocationModel,
6
7
  CADVehicleModel,
7
8
  CADVehicle,
@@ -48,14 +49,43 @@ export function store(
48
49
  return await cadVehicleModel.findOneAndUpdate(query, item, options);
49
50
  }
50
51
 
52
+ function isDuplicateKeyError(error: unknown): boolean {
53
+ return _.isObject(error) && (error as { code?: number }).code === 11000;
54
+ }
55
+
51
56
  async function updateLocation(item: Partial<Location>, atDate: Date) {
57
+ try {
58
+ await performUpdateLocation(item, atDate);
59
+ } catch (error) {
60
+ if (!isDuplicateKeyError(error)) {
61
+ throw error;
62
+ }
63
+ // A concurrent first-write hit the unique (departmentId, session, device_type)
64
+ // index; the record exists now so we can retry once, so this update goes through.
65
+ await performUpdateLocation(item, atDate);
66
+ }
67
+ }
68
+
69
+ async function performUpdateLocation(item: Partial<Location>, atDate: Date) {
52
70
  // eslint-disable-next-line new-cap
53
71
  const modelItem = new locationModel(item);
54
- const query = {
55
- device_type: modelItem.device_type,
56
- departmentId: modelItem.departmentId,
57
- username: modelItem.username
58
- };
72
+ const hasPersonIdentity = (_.isString(modelItem.personnelUUID) && modelItem.personnelUUID !== "") ||
73
+ (_.isString(modelItem.transponderUUID) && modelItem.transponderUUID !== "");
74
+ const isPerson = (modelItem.kindType === LocationKindType.Person || modelItem.locationType === LocationKindType.Person) &&
75
+ hasPersonIdentity;
76
+ // Person records upsert by session (their stable identity key); username is
77
+ // display-only for transponder stuff, so renames must not create new records from updates
78
+ const query = isPerson
79
+ ? {
80
+ device_type: modelItem.device_type,
81
+ departmentId: modelItem.departmentId,
82
+ session: modelItem.session
83
+ }
84
+ : {
85
+ device_type: modelItem.device_type,
86
+ departmentId: modelItem.departmentId,
87
+ username: modelItem.username
88
+ };
59
89
  debug(`Location.findOne: ${JSON.stringify(query)}`);
60
90
  const dbItem = await locationModel.findOne(query).lean().exec() as Partial<LocationObject>;
61
91
  const cloneItem = { ...dbItem };
@@ -95,6 +125,25 @@ export function store(
95
125
  dbItem.locationGeoJSON = modelItem.locationGeoJSON;
96
126
  dbItem.source = modelItem.source;
97
127
  dbItem.kindType = modelItem.kindType;
128
+ dbItem.industry = modelItem.industry;
129
+ // Carry person/category fields on the update path; only overwrite when the
130
+ // incoming payload actually provides a value, so sources that don't send
131
+ // these fields don't wipe values written by other sources.
132
+ if (_.isString(modelItem.locationType) && modelItem.locationType !== "") {
133
+ dbItem.locationType = modelItem.locationType;
134
+ }
135
+ if (_.isString(modelItem.kind) && modelItem.kind !== "") {
136
+ dbItem.kind = modelItem.kind;
137
+ }
138
+ if (_.isString(modelItem.type) && modelItem.type !== "") {
139
+ dbItem.type = modelItem.type;
140
+ }
141
+ if (_.isString(modelItem.personnelUUID) && modelItem.personnelUUID !== "") {
142
+ dbItem.personnelUUID = modelItem.personnelUUID;
143
+ }
144
+ if (_.isString(modelItem.transponderUUID) && modelItem.transponderUUID !== "") {
145
+ dbItem.transponderUUID = modelItem.transponderUUID;
146
+ }
98
147
 
99
148
  // Propagate sharedAVL items
100
149
  dbItem.opAreaCode = modelItem.opAreaCode;
@@ -135,6 +184,7 @@ export function store(
135
184
  "shared",
136
185
  "state",
137
186
  "username",
187
+ "industry",
138
188
  ];
139
189
  if (_.intersection(updatedKeys, propsKeys).length > 0) {
140
190
  locationDelta.propsChangedAt = atDate;