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.
- package/.claude/worktrees/sc-36275/.buildkite/pipeline.yml +5 -0
- package/.claude/worktrees/sc-36275/README.md +3 -0
- package/.claude/worktrees/sc-36275/build/audience.js +38 -0
- package/.claude/worktrees/sc-36275/build/audience.js.map +1 -0
- package/.claude/worktrees/sc-36275/build/index.js +95 -0
- package/.claude/worktrees/sc-36275/build/index.js.map +1 -0
- package/.claude/worktrees/sc-36275/build/location.js +278 -0
- package/.claude/worktrees/sc-36275/build/location.js.map +1 -0
- package/.claude/worktrees/sc-36275/build/query.js +378 -0
- package/.claude/worktrees/sc-36275/build/query.js.map +1 -0
- package/.claude/worktrees/sc-36275/build/store.js +182 -0
- package/.claude/worktrees/sc-36275/build/store.js.map +1 -0
- package/.claude/worktrees/sc-36275/build/test/location.js +522 -0
- package/.claude/worktrees/sc-36275/build/test/location.js.map +1 -0
- package/.claude/worktrees/sc-36275/build/test/mock.js +165 -0
- package/.claude/worktrees/sc-36275/build/test/mock.js.map +1 -0
- package/.claude/worktrees/sc-36275/build/test/query.js +174 -0
- package/.claude/worktrees/sc-36275/build/test/query.js.map +1 -0
- package/.claude/worktrees/sc-36275/build/types.js +8 -0
- package/.claude/worktrees/sc-36275/build/types.js.map +1 -0
- package/.claude/worktrees/sc-36275/cspell.json +64 -0
- package/.claude/worktrees/sc-36275/definitions/audience.d.ts +18 -0
- package/.claude/worktrees/sc-36275/definitions/audience.d.ts.map +1 -0
- package/.claude/worktrees/sc-36275/definitions/index.d.ts +11 -0
- package/.claude/worktrees/sc-36275/definitions/index.d.ts.map +1 -0
- package/.claude/worktrees/sc-36275/definitions/location.d.ts +32 -0
- package/.claude/worktrees/sc-36275/definitions/location.d.ts.map +1 -0
- package/.claude/worktrees/sc-36275/definitions/query.d.ts +36 -0
- package/.claude/worktrees/sc-36275/definitions/query.d.ts.map +1 -0
- package/.claude/worktrees/sc-36275/definitions/store.d.ts +64 -0
- package/.claude/worktrees/sc-36275/definitions/store.d.ts.map +1 -0
- package/.claude/worktrees/sc-36275/definitions/test/location.d.ts +2 -0
- package/.claude/worktrees/sc-36275/definitions/test/location.d.ts.map +1 -0
- package/.claude/worktrees/sc-36275/definitions/test/mock.d.ts +346 -0
- package/.claude/worktrees/sc-36275/definitions/test/mock.d.ts.map +1 -0
- package/.claude/worktrees/sc-36275/definitions/test/query.d.ts +2 -0
- package/.claude/worktrees/sc-36275/definitions/test/query.d.ts.map +1 -0
- package/.claude/worktrees/sc-36275/definitions/types.d.ts +53 -0
- package/.claude/worktrees/sc-36275/definitions/types.d.ts.map +1 -0
- package/.claude/worktrees/sc-36275/eslint.config.mjs +95 -0
- package/.claude/worktrees/sc-36275/gulpfile.js +23 -0
- package/.claude/worktrees/sc-36275/package-lock.json +9404 -0
- package/.claude/worktrees/sc-36275/package.json +54 -0
- package/.claude/worktrees/sc-36275/src/audience.ts +54 -0
- package/.claude/worktrees/sc-36275/src/index.ts +81 -0
- package/.claude/worktrees/sc-36275/src/location.ts +315 -0
- package/.claude/worktrees/sc-36275/src/query.ts +497 -0
- package/.claude/worktrees/sc-36275/src/store.ts +204 -0
- package/.claude/worktrees/sc-36275/src/test/location.ts +485 -0
- package/.claude/worktrees/sc-36275/src/test/mock.ts +186 -0
- package/.claude/worktrees/sc-36275/src/test/query.ts +209 -0
- package/.claude/worktrees/sc-36275/src/tsconfig.json +30 -0
- package/.claude/worktrees/sc-36275/src/types.ts +74 -0
- package/.claude/worktrees/sc-36275/test.sh +32 -0
- package/build/query.js +1 -1
- package/build/query.js.map +1 -1
- package/build/test/query.js +2 -2
- package/build/test/query.js.map +1 -1
- package/cspell.json +1 -0
- package/package.json +1 -1
- package/src/query.ts +1 -1
- 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>;
|