tabletcommand-location 2.4.2 → 2.4.3

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 (62) hide show
  1. package/.claude/worktrees/sc-36275/.buildkite/pipeline.yml +5 -0
  2. package/.claude/worktrees/sc-36275/README.md +3 -0
  3. package/.claude/worktrees/sc-36275/build/audience.js +38 -0
  4. package/.claude/worktrees/sc-36275/build/audience.js.map +1 -0
  5. package/.claude/worktrees/sc-36275/build/index.js +95 -0
  6. package/.claude/worktrees/sc-36275/build/index.js.map +1 -0
  7. package/.claude/worktrees/sc-36275/build/location.js +278 -0
  8. package/.claude/worktrees/sc-36275/build/location.js.map +1 -0
  9. package/.claude/worktrees/sc-36275/build/query.js +378 -0
  10. package/.claude/worktrees/sc-36275/build/query.js.map +1 -0
  11. package/.claude/worktrees/sc-36275/build/store.js +182 -0
  12. package/.claude/worktrees/sc-36275/build/store.js.map +1 -0
  13. package/.claude/worktrees/sc-36275/build/test/location.js +522 -0
  14. package/.claude/worktrees/sc-36275/build/test/location.js.map +1 -0
  15. package/.claude/worktrees/sc-36275/build/test/mock.js +165 -0
  16. package/.claude/worktrees/sc-36275/build/test/mock.js.map +1 -0
  17. package/.claude/worktrees/sc-36275/build/test/query.js +174 -0
  18. package/.claude/worktrees/sc-36275/build/test/query.js.map +1 -0
  19. package/.claude/worktrees/sc-36275/build/types.js +8 -0
  20. package/.claude/worktrees/sc-36275/build/types.js.map +1 -0
  21. package/.claude/worktrees/sc-36275/cspell.json +64 -0
  22. package/.claude/worktrees/sc-36275/definitions/audience.d.ts +18 -0
  23. package/.claude/worktrees/sc-36275/definitions/audience.d.ts.map +1 -0
  24. package/.claude/worktrees/sc-36275/definitions/index.d.ts +11 -0
  25. package/.claude/worktrees/sc-36275/definitions/index.d.ts.map +1 -0
  26. package/.claude/worktrees/sc-36275/definitions/location.d.ts +32 -0
  27. package/.claude/worktrees/sc-36275/definitions/location.d.ts.map +1 -0
  28. package/.claude/worktrees/sc-36275/definitions/query.d.ts +36 -0
  29. package/.claude/worktrees/sc-36275/definitions/query.d.ts.map +1 -0
  30. package/.claude/worktrees/sc-36275/definitions/store.d.ts +64 -0
  31. package/.claude/worktrees/sc-36275/definitions/store.d.ts.map +1 -0
  32. package/.claude/worktrees/sc-36275/definitions/test/location.d.ts +2 -0
  33. package/.claude/worktrees/sc-36275/definitions/test/location.d.ts.map +1 -0
  34. package/.claude/worktrees/sc-36275/definitions/test/mock.d.ts +346 -0
  35. package/.claude/worktrees/sc-36275/definitions/test/mock.d.ts.map +1 -0
  36. package/.claude/worktrees/sc-36275/definitions/test/query.d.ts +2 -0
  37. package/.claude/worktrees/sc-36275/definitions/test/query.d.ts.map +1 -0
  38. package/.claude/worktrees/sc-36275/definitions/types.d.ts +53 -0
  39. package/.claude/worktrees/sc-36275/definitions/types.d.ts.map +1 -0
  40. package/.claude/worktrees/sc-36275/eslint.config.mjs +95 -0
  41. package/.claude/worktrees/sc-36275/gulpfile.js +23 -0
  42. package/.claude/worktrees/sc-36275/package-lock.json +9404 -0
  43. package/.claude/worktrees/sc-36275/package.json +54 -0
  44. package/.claude/worktrees/sc-36275/src/audience.ts +54 -0
  45. package/.claude/worktrees/sc-36275/src/index.ts +81 -0
  46. package/.claude/worktrees/sc-36275/src/location.ts +315 -0
  47. package/.claude/worktrees/sc-36275/src/query.ts +497 -0
  48. package/.claude/worktrees/sc-36275/src/store.ts +204 -0
  49. package/.claude/worktrees/sc-36275/src/test/location.ts +485 -0
  50. package/.claude/worktrees/sc-36275/src/test/mock.ts +186 -0
  51. package/.claude/worktrees/sc-36275/src/test/query.ts +209 -0
  52. package/.claude/worktrees/sc-36275/src/tsconfig.json +30 -0
  53. package/.claude/worktrees/sc-36275/src/types.ts +74 -0
  54. package/.claude/worktrees/sc-36275/test.sh +32 -0
  55. package/build/query.js +1 -1
  56. package/build/query.js.map +1 -1
  57. package/build/test/query.js +2 -2
  58. package/build/test/query.js.map +1 -1
  59. package/cspell.json +1 -0
  60. package/package.json +1 -1
  61. package/src/query.ts +1 -1
  62. package/src/test/query.ts +2 -2
@@ -0,0 +1,497 @@
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
+ industry: 1,
75
+ kindType: 1,
76
+ location: 1,
77
+ locationGeoJSON: 1,
78
+ modified: 1,
79
+ movedAt: 1,
80
+ sendToCAD: 1,
81
+ source: 1,
82
+ speed: 1,
83
+ uuid: 1, // Legacy, remove later
84
+ };
85
+
86
+ export function adminListLocations(params: AdminListLocationParams1): MQP {
87
+ const {
88
+ departmentId,
89
+ movedAt,
90
+ showAll,
91
+ } = params;
92
+
93
+ const query: FilterQuery<Location> = {
94
+ // TODO: Replace models.Location active: true with visibility in "visible"
95
+ active: true,
96
+ departmentId,
97
+ $and: excludePersonAndTransponderRecords,
98
+ kindType: {
99
+ $ne: "uav",
100
+ },
101
+ // 03/14/23 (Joe) - Trying movedAt only [sc-11794] - determined modified was being overwritten by cad vehicle status updates
102
+ movedAt: {
103
+ $gte: movedAt,
104
+ },
105
+ };
106
+ if (showAll) {
107
+ delete query.active;
108
+ }
109
+
110
+ return {
111
+ list: query,
112
+ projection: projectionAdmin,
113
+ };
114
+ }
115
+
116
+ export function adminListPersonLocations(params: AdminListLocationParams1): MQP {
117
+ const {
118
+ departmentId,
119
+ movedAt,
120
+ showAll
121
+ } = params;
122
+
123
+ const query: FilterQuery<Location> = {
124
+ active: true,
125
+ departmentId,
126
+ kindType: "person",
127
+ movedAt: {
128
+ $gte: movedAt,
129
+ },
130
+ };
131
+ if (showAll) {
132
+ delete query.active;
133
+ }
134
+
135
+ // Same fields defined in the frontend
136
+ // api/a1/location/person
137
+ const projection = {
138
+ _id: 1,
139
+ active: 1,
140
+ altitude: 1,
141
+ color: 1,
142
+ device_type: 1,
143
+ industry: 1,
144
+ kindType: 1,
145
+ location: 1,
146
+ locationGeoJSON: 1,
147
+ modified: 1,
148
+ movedAt: 1,
149
+ personnelUUID: 1,
150
+ sendToCAD: 1,
151
+ source: 1,
152
+ speed: 1,
153
+ transponderUUID: 1,
154
+ uuid: 1, // Legacy, remove later
155
+ };
156
+
157
+ return {
158
+ list: query,
159
+ projection,
160
+ };
161
+ }
162
+
163
+ export function adminListUAVLocations(params: AdminListLocationParams1): MQP {
164
+ const {
165
+ departmentId,
166
+ movedAt,
167
+ showAll,
168
+ } = params;
169
+
170
+ const query: FilterQuery<Location> = {
171
+ active: true,
172
+ departmentId,
173
+ kindType: "uav",
174
+ movedAt: {
175
+ $gte: movedAt,
176
+ },
177
+ };
178
+ if (showAll) {
179
+ delete query.active;
180
+ }
181
+
182
+ const projection = {
183
+ _id: 1,
184
+ active: 1,
185
+ agencyCode: 1,
186
+ agencyName: 1,
187
+ altitude: 1,
188
+ color: 1,
189
+ device_type: 1,
190
+ heading: 1,
191
+ industry: 1,
192
+ kindType: 1,
193
+ locationGeoJSON: 1,
194
+ modified: 1,
195
+ movedAt: 1,
196
+ opAreaCode: 1,
197
+ opAreaName: 1,
198
+ sendToCAD: 1,
199
+ source: 1,
200
+ speed: 1,
201
+ state: 1,
202
+ userId: 1,
203
+ username: 1,
204
+ uuid: 1, // Legacy, remove later
205
+ vehicleId: 1,
206
+ };
207
+
208
+ return {
209
+ list: query,
210
+ projection,
211
+ };
212
+ }
213
+
214
+ export function adminListAircraftLocations(params: AdminListLocationParams2): MQP {
215
+ const {
216
+ departmentId,
217
+ movedAt,
218
+ longitude,
219
+ latitude,
220
+ diagonal
221
+ } = params;
222
+
223
+ const list = {
224
+ shared: true,
225
+ active: true,
226
+ source: "ADSB",
227
+ kindType: {
228
+ $in: ["fixed-wing", "helicopter"],
229
+ },
230
+ departmentId: {
231
+ $ne: departmentId,
232
+ },
233
+ movedAt: {
234
+ $gte: movedAt.toISOString(),
235
+ },
236
+ locationGeoJSON: centerSphereFilter(longitude, latitude, diagonal),
237
+ };
238
+
239
+ return {
240
+ list,
241
+ projection: projectionAdmin,
242
+ };
243
+ }
244
+
245
+ export function adminListSharedLocations(params: AdminListLocationParams2): MQP {
246
+ const {
247
+ departmentId,
248
+ movedAt,
249
+ longitude,
250
+ latitude,
251
+ diagonal
252
+ } = params;
253
+
254
+ const listSharedAVLQuery = {
255
+ shared: true,
256
+ active: true,
257
+ source: {
258
+ $ne: "ADSB"
259
+ },
260
+ departmentId: {
261
+ $ne: departmentId,
262
+ },
263
+ kindType: {
264
+ $ne: "uav",
265
+ },
266
+ movedAt: {
267
+ $gte: movedAt.toISOString(),
268
+ },
269
+ locationGeoJSON: centerSphereFilter(longitude, latitude, diagonal),
270
+ };
271
+
272
+ return {
273
+ list: listSharedAVLQuery,
274
+ projection: projectionAdmin,
275
+ };
276
+ }
277
+
278
+ export function adminListFocusedLocations(params: AdminListLocationParams2): MQP {
279
+ const {
280
+ departmentId,
281
+ movedAt,
282
+ longitude,
283
+ latitude,
284
+ diagonal,
285
+ showAll
286
+ } = params;
287
+
288
+ const listFocusedAVLQuery: FilterQuery<Location> = {
289
+ active: true,
290
+ departmentId,
291
+ movedAt: {
292
+ $gte: movedAt.toISOString(),
293
+ },
294
+ locationGeoJSON: centerSphereFilter(longitude, latitude, diagonal),
295
+ };
296
+
297
+ if (showAll) {
298
+ delete listFocusedAVLQuery.locationGeoJSON;
299
+ }
300
+
301
+ return {
302
+ list: listFocusedAVLQuery,
303
+ projection: projectionAdmin,
304
+ };
305
+ }
306
+
307
+ // Sync queries
308
+
309
+ const projectionSync: ProjectionType<Location> = {
310
+ active: 1,
311
+ departmentId: 1,
312
+ device_type: 1, // used in web ui
313
+ location: 1,
314
+ locationGeoJSON: 1,
315
+ modified: 1,
316
+ userId: 1,
317
+ username: 1,
318
+ uuid: 1, // used in iOS
319
+ // both used by AVL sharing
320
+ opAreaName: 1,
321
+ opAreaCode: 1,
322
+ shared: 1,
323
+ state: 1,
324
+ // v3
325
+ altitude: 1,
326
+ speed: 1,
327
+ movedAt: 1,
328
+ agencyName: 1,
329
+ agencyCode: 1,
330
+ // v4
331
+ color: 1,
332
+ colorChangedAt: 1,
333
+ propsChangedAt: 1,
334
+ heading: 1,
335
+ // v4 extra
336
+ kindType: 1,
337
+ source: 1,
338
+ personnelUUID: 1,
339
+ transponderUUID: 1,
340
+ // v4 kind/type/locationType
341
+ kind: 1,
342
+ type: 1,
343
+ locationType: 1,
344
+ kindColor: 1,
345
+ // v4
346
+ industry: 1,
347
+ };
348
+
349
+ export function syncListSharedLocations(params: AdminListLocationParams2 & SyncModifiedDate): MQP {
350
+ const {
351
+ departmentId,
352
+ modifiedDate,
353
+ movedAt,
354
+ longitude,
355
+ latitude,
356
+ diagonal,
357
+ } = params;
358
+
359
+ const list: FilterQuery<Location> = {
360
+ shared: true,
361
+ source: {
362
+ $nin: ["ADSB", "DroneSense", "Skydio"]
363
+ },
364
+ departmentId: {
365
+ $ne: departmentId,
366
+ },
367
+ modified: {
368
+ $gte: modifiedDate.toISOString(),
369
+ },
370
+ movedAt: {
371
+ $gte: movedAt.toISOString(),
372
+ },
373
+ locationGeoJSON: centerSphereFilter(longitude, latitude, diagonal),
374
+ };
375
+
376
+ return {
377
+ list,
378
+ projection: projectionSync,
379
+ };
380
+ }
381
+
382
+ export function syncListLocations(params: AdminListLocationParams1 & SyncModifiedDate): MQP {
383
+ const {
384
+ departmentId,
385
+ modifiedDate,
386
+ movedAt,
387
+ } = params;
388
+
389
+ const list: FilterQuery<Location> = {
390
+ departmentId,
391
+ $and: excludePersonAndTransponderRecords,
392
+ kindType: {
393
+ $ne: "uav"
394
+ },
395
+ modified: {
396
+ $gte: modifiedDate.toISOString(),
397
+ },
398
+ movedAt: {
399
+ $gte: movedAt.toISOString(),
400
+ }
401
+ };
402
+
403
+ return {
404
+ list,
405
+ projection: projectionSync,
406
+ };
407
+ }
408
+
409
+ export function syncListPersonLocations(params: AdminListLocationParams1 & SyncModifiedDate): MQP {
410
+ const {
411
+ departmentId,
412
+ modifiedDate,
413
+ movedAt,
414
+ } = params;
415
+
416
+ const list: FilterQuery<Location> = {
417
+ departmentId,
418
+ kindType: "person",
419
+ modified: {
420
+ $gte: modifiedDate.toISOString(),
421
+ },
422
+ movedAt: {
423
+ $gte: movedAt.toISOString(),
424
+ },
425
+ };
426
+
427
+ return {
428
+ list,
429
+ projection: projectionSync,
430
+ };
431
+ }
432
+
433
+ export function syncListAircraftLocations(params: AdminListLocationParams2 & SyncModifiedDate): MQP {
434
+ const {
435
+ departmentId,
436
+ movedAt,
437
+ modifiedDate,
438
+ // Possibly missing
439
+ longitude,
440
+ latitude,
441
+ diagonal
442
+ } = params;
443
+
444
+ const list: FilterQuery<Location> = {
445
+ shared: true,
446
+ source: "ADSB",
447
+ kindType: {
448
+ $in: ["fixed-wing", "helicopter"]
449
+ },
450
+ departmentId: {
451
+ $ne: departmentId,
452
+ },
453
+ modified: {
454
+ $gte: modifiedDate.toISOString(),
455
+ },
456
+ movedAt: {
457
+ $gte: movedAt.toISOString(),
458
+ }
459
+ };
460
+
461
+ const hasCoordinate = Math.abs(latitude) > 0.01 || Math.abs(longitude) > 0.01;
462
+ const validCoordinate = Math.abs(latitude) < 90 && Math.abs(longitude) < 180;
463
+ const filterOnLocation = hasCoordinate && validCoordinate;
464
+ if (filterOnLocation) {
465
+ list.locationGeoJSON = centerSphereFilter(longitude, latitude, diagonal);
466
+ }
467
+
468
+ return {
469
+ list,
470
+ projection: projectionSync,
471
+ };
472
+ }
473
+
474
+ export function syncListUAVLocations(params: AdminListLocationParams1 & SyncModifiedDate): MQP {
475
+ const {
476
+ departmentId,
477
+ modifiedDate,
478
+ movedAt,
479
+
480
+ } = params;
481
+
482
+ const list: FilterQuery<Location> = {
483
+ departmentId,
484
+ kindType: "uav",
485
+ modified: {
486
+ $gte: modifiedDate.toISOString(),
487
+ },
488
+ movedAt: {
489
+ $gte: movedAt.toISOString(),
490
+ },
491
+ };
492
+
493
+ return {
494
+ list,
495
+ projection: projectionSync,
496
+ };
497
+ }
@@ -0,0 +1,204 @@
1
+ import debugModule from "debug";
2
+ import {
3
+ DepartmentModel,
4
+ Location,
5
+ LocationKindType,
6
+ LocationModel,
7
+ CADVehicleModel,
8
+ CADVehicle,
9
+ DeviceMappingModel,
10
+ } from "tabletcommand-backend-models";
11
+ import _ from "lodash";
12
+ import { LocationObject, LocationPayload } from "./types";
13
+
14
+ export function store(
15
+ departmentModel: DepartmentModel,
16
+ locationModel: LocationModel,
17
+ cadVehicleModel: CADVehicleModel,
18
+ deviceMappingModel: DeviceMappingModel
19
+ ) {
20
+ const debug = debugModule("tabletcommand-location:location:store");
21
+
22
+ async function getVehicle(item: LocationPayload, departmentId: string) {
23
+ const query = {
24
+ vehicleId: item.vehicleId,
25
+ departmentId,
26
+ };
27
+ return cadVehicleModel.findOne(query).lean();
28
+ }
29
+
30
+ async function getDeviceMappingGeotab(item: LocationPayload, departmentId: string) {
31
+ const query = {
32
+ deviceId: item.vehicleId,
33
+ departmentId,
34
+ deviceType: "geotab",
35
+ };
36
+ return deviceMappingModel.findOne(query).lean();
37
+ }
38
+
39
+ async function updateVehicle(item: Partial<CADVehicle>, departmentId: string) {
40
+ const query = {
41
+ departmentId,
42
+ vehicleId: item.vehicleId,
43
+ };
44
+ const options = {
45
+ upsert: true,
46
+ setDefaultsOnInsert: true,
47
+ new: true
48
+ };
49
+ return await cadVehicleModel.findOneAndUpdate(query, item, options);
50
+ }
51
+
52
+ function isDuplicateKeyError(error: unknown): boolean {
53
+ return _.isObject(error) && (error as { code?: number }).code === 11000;
54
+ }
55
+
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) {
70
+ // eslint-disable-next-line new-cap
71
+ const modelItem = new locationModel(item);
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
+ };
89
+ debug(`Location.findOne: ${JSON.stringify(query)}`);
90
+ const dbItem = await locationModel.findOne(query).lean().exec() as Partial<LocationObject>;
91
+ const cloneItem = { ...dbItem };
92
+ let updatedItem = propagateLocation(modelItem, dbItem);
93
+ debug(`Location.save: ${JSON.stringify(updatedItem)}`);
94
+ if (cloneItem._id) {
95
+ const locationDelta = generateLocationDelta(cloneItem, updatedItem, atDate);
96
+ updatedItem = locationDelta;
97
+ debug(`Location.delta: ${JSON.stringify(locationDelta)}`);
98
+ } else {
99
+ // if new location record - Attempt to reconcile agency name and code to resources
100
+ // this is going to be resolved by the bin/cron-cleanup-minute cron
101
+ }
102
+ await locationModel.updateOne(query, {
103
+ $set: updatedItem,
104
+ }, {
105
+ upsert: true,
106
+ });
107
+ }
108
+
109
+ function propagateLocation(modelItem: Location, dbItem: Partial<LocationObject>) {
110
+ if (!_.isObject(dbItem)) {
111
+ return modelItem;
112
+ }
113
+
114
+ // We keep the same value for _id, uuid, departmentId
115
+ dbItem.altitude = modelItem.altitude;
116
+ dbItem.device_type = modelItem.device_type;
117
+ dbItem.heading = modelItem.heading;
118
+ dbItem.modified = modelItem.modified;
119
+ dbItem.movedAt = modelItem.movedAt;
120
+ dbItem.session = modelItem.session;
121
+ dbItem.speed = modelItem.speed;
122
+ dbItem.userId = modelItem.userId;
123
+ dbItem.username = modelItem.username;
124
+ dbItem.version = modelItem.version;
125
+ dbItem.locationGeoJSON = modelItem.locationGeoJSON;
126
+ dbItem.source = modelItem.source;
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
+ }
147
+
148
+ // Propagate sharedAVL items
149
+ dbItem.opAreaCode = modelItem.opAreaCode;
150
+ dbItem.opAreaName = modelItem.opAreaName;
151
+ dbItem.state = modelItem.state;
152
+
153
+ // Legacy
154
+ dbItem.active = modelItem.active;
155
+ dbItem.shared = modelItem.shared;
156
+ dbItem.sendToCAD = modelItem.sendToCAD;
157
+ //
158
+ dbItem.visibility = modelItem.visibility;
159
+
160
+ return dbItem;
161
+ }
162
+
163
+ function generateLocationDelta(locationRecord: Partial<LocationObject>, updateObject: Partial<LocationObject>, atDate: Date) {
164
+ if (!_.isObject(locationRecord)) {
165
+ return updateObject;
166
+ }
167
+ const locationDelta: Partial<LocationObject> = {};
168
+ for (const [key, value] of Object.entries(updateObject)) {
169
+ // eslint-disable-next-line security/detect-object-injection
170
+ if (JSON.stringify(value) !== JSON.stringify(locationRecord[key])) {
171
+ // eslint-disable-next-line security/detect-object-injection
172
+ locationDelta[key] = value;
173
+ }
174
+ }
175
+
176
+ const updatedKeys = Object.keys(locationDelta);
177
+ const propsKeys = [
178
+ "active",
179
+ "agencyCode",
180
+ "agencyName",
181
+ "device_type",
182
+ "opAreaCode",
183
+ "opAreaName",
184
+ "shared",
185
+ "state",
186
+ "username",
187
+ "industry",
188
+ ];
189
+ if (_.intersection(updatedKeys, propsKeys).length > 0) {
190
+ locationDelta.propsChangedAt = atDate;
191
+ }
192
+
193
+ return locationDelta;
194
+ }
195
+
196
+ return {
197
+ getVehicle,
198
+ getDeviceMappingGeotab,
199
+ updateVehicle,
200
+ updateLocation,
201
+ };
202
+ }
203
+ export default store;
204
+ export type StoreModule = ReturnType<typeof store>;