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.
- package/build/audience.js +38 -0
- package/build/audience.js.map +1 -0
- package/build/index.js +38 -20
- package/build/index.js.map +1 -1
- package/build/location.js +63 -6
- package/build/location.js.map +1 -1
- package/build/query.js +373 -0
- package/build/query.js.map +1 -0
- package/build/store.js +54 -5
- package/build/store.js.map +1 -1
- package/build/test/location.js +313 -0
- package/build/test/location.js.map +1 -1
- package/build/test/mock.js +5 -0
- package/build/test/mock.js.map +1 -1
- package/build/test/query.js +174 -0
- package/build/test/query.js.map +1 -0
- package/cspell.json +5 -1
- package/definitions/audience.d.ts +18 -0
- package/definitions/audience.d.ts.map +1 -0
- package/definitions/index.d.ts +1 -0
- package/definitions/index.d.ts.map +1 -1
- package/definitions/location.d.ts.map +1 -1
- package/definitions/query.d.ts +36 -0
- package/definitions/query.d.ts.map +1 -0
- package/definitions/store.d.ts +5 -0
- package/definitions/store.d.ts.map +1 -1
- package/definitions/test/mock.d.ts +15 -0
- package/definitions/test/mock.d.ts.map +1 -1
- package/definitions/test/query.d.ts +2 -0
- package/definitions/test/query.d.ts.map +1 -0
- package/definitions/types.d.ts +5 -0
- package/definitions/types.d.ts.map +1 -1
- package/package.json +18 -14
- package/src/audience.ts +54 -0
- package/src/index.ts +5 -20
- package/src/location.ts +67 -6
- package/src/query.ts +492 -0
- package/src/store.ts +55 -5
- package/src/test/location.ts +306 -1
- package/src/test/mock.ts +6 -0
- package/src/test/query.ts +209 -0
- package/src/types.ts +15 -0
- package/test.sh +1 -1
package/src/test/location.ts
CHANGED
|
@@ -5,7 +5,7 @@ import storeModule, { StoreModule } from "../store";
|
|
|
5
5
|
import mockModule from "./mock";
|
|
6
6
|
import locationModule, { LocationModule } from "../location";
|
|
7
7
|
import { LocationPayload } from "../types";
|
|
8
|
-
import { BackendModels } from "tabletcommand-backend-models";
|
|
8
|
+
import { AccountIndustry, BackendModels } from "tabletcommand-backend-models";
|
|
9
9
|
|
|
10
10
|
let location: LocationModule;
|
|
11
11
|
let store: StoreModule;
|
|
@@ -150,6 +150,259 @@ describe("location", () => {
|
|
|
150
150
|
assert.equal(locationRecord[0]?.visibility.includes("shared"), true);
|
|
151
151
|
/* cSpell:enable */
|
|
152
152
|
});
|
|
153
|
+
it("applies person invariants on the geotab path, ignoring device mapping", async () => {
|
|
154
|
+
const department = mockDepartment;
|
|
155
|
+
// BEEDOO3 has a device mapping with mapId BOOPBEEP99 and locationToCAD:
|
|
156
|
+
// true; a person payload must not pick either of those up.
|
|
157
|
+
const items = [{
|
|
158
|
+
vehicleId: "BEEDOO3",
|
|
159
|
+
radioName: "Smokey Bear",
|
|
160
|
+
latitude: 34.35264,
|
|
161
|
+
longitude: -119.0669,
|
|
162
|
+
avlTime: "2026-07-10T15:43:36.816Z",
|
|
163
|
+
kindType: "person",
|
|
164
|
+
locationType: "person",
|
|
165
|
+
source: "TXRX",
|
|
166
|
+
transponderUUID: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
|
|
167
|
+
userId: "TXRX-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
|
|
168
|
+
session: "TXRX-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
|
|
169
|
+
username: "Smokey Bear",
|
|
170
|
+
visibility: [],
|
|
171
|
+
}] as unknown as LocationPayload[];
|
|
172
|
+
await location.processItemsDeviceMappingGeotab(department, items, new Date());
|
|
173
|
+
const recs = await models.Location.find({});
|
|
174
|
+
assert.equal(recs.length, 1);
|
|
175
|
+
assert.equal(recs[0]?.username, "Smokey Bear");
|
|
176
|
+
assert.equal(recs[0]?.session, "TXRX-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
|
|
177
|
+
assert.equal(recs[0]?.transponderUUID, "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
|
|
178
|
+
assert.equal(recs[0]?.active, true);
|
|
179
|
+
assert.equal(recs[0]?.shared, false);
|
|
180
|
+
assert.equal(recs[0]?.sendToCAD, false);
|
|
181
|
+
assert.deepEqual(recs[0]?.visibility, ["visible"]);
|
|
182
|
+
});
|
|
183
|
+
it("skips geotab person locations with neither personnelUUID nor transponderUUID", async () => {
|
|
184
|
+
const department = mockDepartment;
|
|
185
|
+
const items = [{
|
|
186
|
+
vehicleId: "BEEDOO3",
|
|
187
|
+
radioName: "Smokey Bear",
|
|
188
|
+
latitude: 34.35264,
|
|
189
|
+
longitude: -119.0669,
|
|
190
|
+
avlTime: "2026-07-10T15:43:36.816Z",
|
|
191
|
+
kindType: "person",
|
|
192
|
+
locationType: "person",
|
|
193
|
+
source: "TXRX",
|
|
194
|
+
visibility: [],
|
|
195
|
+
}] as unknown as LocationPayload[];
|
|
196
|
+
await location.processItemsDeviceMappingGeotab(department, items, new Date());
|
|
197
|
+
const recs = await models.Location.find({});
|
|
198
|
+
assert.equal(recs.length, 0);
|
|
199
|
+
});
|
|
200
|
+
it("saves person locations keyed by session with person invariants, no CAD vehicle", async () => {
|
|
201
|
+
const atDate = new Date();
|
|
202
|
+
const department = mockDepartment;
|
|
203
|
+
const items = [{
|
|
204
|
+
vehicleId: "12345",
|
|
205
|
+
radioName: "SmokeyBear",
|
|
206
|
+
latitude: 34.35264,
|
|
207
|
+
longitude: -119.0669,
|
|
208
|
+
avlTime: "2026-07-10T15:43:36.816Z",
|
|
209
|
+
kindType: "person",
|
|
210
|
+
locationType: "person",
|
|
211
|
+
kind: "",
|
|
212
|
+
type: "",
|
|
213
|
+
source: "TXRX",
|
|
214
|
+
personnelUUID: "11111111-2222-3333-4444-555555555555",
|
|
215
|
+
userId: "TXRX-12345",
|
|
216
|
+
session: "TXRX-12345",
|
|
217
|
+
username: "SmokeyBear",
|
|
218
|
+
visibility: [],
|
|
219
|
+
}] as unknown as LocationPayload[];
|
|
220
|
+
await location.processItems(department, items, atDate);
|
|
221
|
+
const recs = await models.Location.find({});
|
|
222
|
+
assert.equal(recs.length, 1);
|
|
223
|
+
assert.equal(recs[0]?.kindType, "person");
|
|
224
|
+
assert.equal(recs[0]?.locationType, "person");
|
|
225
|
+
assert.equal(recs[0]?.personnelUUID, "11111111-2222-3333-4444-555555555555");
|
|
226
|
+
assert.equal(recs[0]?.transponderUUID, "");
|
|
227
|
+
assert.equal(recs[0]?.username, "SmokeyBear");
|
|
228
|
+
assert.equal(recs[0]?.userId, "TXRX-12345");
|
|
229
|
+
assert.equal(recs[0]?.session, "TXRX-12345");
|
|
230
|
+
assert.equal(recs[0]?.active, true);
|
|
231
|
+
assert.equal(recs[0]?.shared, false);
|
|
232
|
+
assert.equal(recs[0]?.sendToCAD, false);
|
|
233
|
+
assert.deepEqual(recs[0]?.visibility, ["visible"]);
|
|
234
|
+
// Person records must not create phantom CAD vehicles
|
|
235
|
+
const vehicles = await models.CADVehicle.find({});
|
|
236
|
+
assert.equal(vehicles.length, 0);
|
|
237
|
+
});
|
|
238
|
+
it("stamps kindType on person locations classified by locationType alone", async () => {
|
|
239
|
+
const atDate = new Date();
|
|
240
|
+
const department = mockDepartment;
|
|
241
|
+
const items = [{
|
|
242
|
+
vehicleId: "12345",
|
|
243
|
+
radioName: "SmokeyBear",
|
|
244
|
+
latitude: 34.35264,
|
|
245
|
+
longitude: -119.0669,
|
|
246
|
+
avlTime: "2026-07-10T15:43:36.816Z",
|
|
247
|
+
locationType: "person",
|
|
248
|
+
source: "TXRX",
|
|
249
|
+
personnelUUID: "11111111-2222-3333-4444-555555555555",
|
|
250
|
+
userId: "TXRX-12345",
|
|
251
|
+
session: "TXRX-12345",
|
|
252
|
+
username: "SmokeyBear",
|
|
253
|
+
visibility: [],
|
|
254
|
+
}] as unknown as LocationPayload[];
|
|
255
|
+
await location.processItems(department, items, atDate);
|
|
256
|
+
const recs = await models.Location.find({});
|
|
257
|
+
assert.equal(recs.length, 1);
|
|
258
|
+
// The person feed queries kindType: "person", so the record must not
|
|
259
|
+
// rely on the payload having set it
|
|
260
|
+
assert.equal(recs[0]?.kindType, "person");
|
|
261
|
+
assert.equal(recs[0]?.locationType, "person");
|
|
262
|
+
assert.equal(recs[0]?.personnelUUID, "11111111-2222-3333-4444-555555555555");
|
|
263
|
+
const vehicles = await models.CADVehicle.find({});
|
|
264
|
+
assert.equal(vehicles.length, 0);
|
|
265
|
+
});
|
|
266
|
+
it("accepts transponder person locations on transponderUUID alone", async () => {
|
|
267
|
+
const atDate = new Date();
|
|
268
|
+
const department = mockDepartment;
|
|
269
|
+
const items = [{
|
|
270
|
+
vehicleId: "FB:9D:2C:3E:C9:E4",
|
|
271
|
+
radioName: "Fire Hose 1",
|
|
272
|
+
latitude: 34.35264,
|
|
273
|
+
longitude: -119.0669,
|
|
274
|
+
avlTime: "2026-07-10T15:43:36.816Z",
|
|
275
|
+
kindType: "person",
|
|
276
|
+
locationType: "person",
|
|
277
|
+
source: "TXRX",
|
|
278
|
+
transponderUUID: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
|
|
279
|
+
userId: "TXRX-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
|
|
280
|
+
session: "TXRX-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
|
|
281
|
+
username: "Fire Hose 1",
|
|
282
|
+
visibility: [],
|
|
283
|
+
}] as unknown as LocationPayload[];
|
|
284
|
+
await location.processItems(department, items, atDate);
|
|
285
|
+
const recs = await models.Location.find({});
|
|
286
|
+
assert.equal(recs.length, 1);
|
|
287
|
+
assert.equal(recs[0]?.transponderUUID, "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
|
|
288
|
+
assert.equal(recs[0]?.personnelUUID, "");
|
|
289
|
+
assert.equal(recs[0]?.session, "TXRX-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
|
|
290
|
+
assert.equal(recs[0]?.active, true);
|
|
291
|
+
assert.equal(recs[0]?.shared, false);
|
|
292
|
+
assert.equal(recs[0]?.sendToCAD, false);
|
|
293
|
+
assert.deepEqual(recs[0]?.visibility, ["visible"]);
|
|
294
|
+
// Person records must not create phantom CAD vehicles
|
|
295
|
+
const vehicles = await models.CADVehicle.find({});
|
|
296
|
+
assert.equal(vehicles.length, 0);
|
|
297
|
+
});
|
|
298
|
+
it("updates person locations in place on rename, preserving person fields", async () => {
|
|
299
|
+
const department = mockDepartment;
|
|
300
|
+
const base = {
|
|
301
|
+
vehicleId: "FB:9D:2C:3E:C9:E4",
|
|
302
|
+
radioName: "William Jones",
|
|
303
|
+
latitude: 34.35264,
|
|
304
|
+
longitude: -119.0669,
|
|
305
|
+
avlTime: "2026-07-10T15:43:36.816Z",
|
|
306
|
+
kindType: "person",
|
|
307
|
+
locationType: "person",
|
|
308
|
+
source: "TXRX",
|
|
309
|
+
transponderUUID: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
|
|
310
|
+
userId: "TXRX-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
|
|
311
|
+
session: "TXRX-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
|
|
312
|
+
username: "William Jones",
|
|
313
|
+
visibility: [],
|
|
314
|
+
};
|
|
315
|
+
await location.processItems(department, [base] as unknown as LocationPayload[], new Date());
|
|
316
|
+
const renamed = {
|
|
317
|
+
...base,
|
|
318
|
+
radioName: "Bill Jones",
|
|
319
|
+
username: "Bill Jones",
|
|
320
|
+
latitude: 34.4,
|
|
321
|
+
};
|
|
322
|
+
await location.processItems(department, [renamed] as unknown as LocationPayload[], new Date());
|
|
323
|
+
const recs = await models.Location.find({});
|
|
324
|
+
assert.equal(recs.length, 1);
|
|
325
|
+
assert.equal(recs[0]?.username, "Bill Jones");
|
|
326
|
+
assert.equal(recs[0]?.session, "TXRX-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
|
|
327
|
+
assert.equal(recs[0]?.locationGeoJSON.coordinates[1], 34.4);
|
|
328
|
+
// Update path must preserve the person fields
|
|
329
|
+
assert.equal(recs[0]?.kindType, "person");
|
|
330
|
+
assert.equal(recs[0]?.locationType, "person");
|
|
331
|
+
assert.equal(recs[0]?.transponderUUID, "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
|
|
332
|
+
assert.deepEqual(recs[0]?.visibility, ["visible"]);
|
|
333
|
+
});
|
|
334
|
+
it("does not wipe transponderUUID when an update omits it", async () => {
|
|
335
|
+
const department = mockDepartment;
|
|
336
|
+
const base = {
|
|
337
|
+
vehicleId: "FB:9D:2C:3E:C9:E4",
|
|
338
|
+
radioName: "Fire Hose 1",
|
|
339
|
+
latitude: 34.35264,
|
|
340
|
+
longitude: -119.0669,
|
|
341
|
+
avlTime: "2026-07-10T15:43:36.816Z",
|
|
342
|
+
kindType: "person",
|
|
343
|
+
locationType: "person",
|
|
344
|
+
source: "TXRX",
|
|
345
|
+
transponderUUID: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
|
|
346
|
+
userId: "TXRX-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
|
|
347
|
+
session: "TXRX-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
|
|
348
|
+
username: "Fire Hose 1",
|
|
349
|
+
visibility: [],
|
|
350
|
+
};
|
|
351
|
+
await location.processItems(department, [base] as unknown as LocationPayload[], new Date());
|
|
352
|
+
// Same session, but this update carries only a personnelUUID
|
|
353
|
+
const update = {
|
|
354
|
+
...base,
|
|
355
|
+
transponderUUID: "",
|
|
356
|
+
personnelUUID: "11111111-2222-3333-4444-555555555555",
|
|
357
|
+
latitude: 34.4,
|
|
358
|
+
};
|
|
359
|
+
await location.processItems(department, [update] as unknown as LocationPayload[], new Date());
|
|
360
|
+
const recs = await models.Location.find({});
|
|
361
|
+
assert.equal(recs.length, 1);
|
|
362
|
+
assert.equal(recs[0]?.transponderUUID, "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
|
|
363
|
+
assert.equal(recs[0]?.personnelUUID, "11111111-2222-3333-4444-555555555555");
|
|
364
|
+
assert.equal(recs[0]?.locationGeoJSON.coordinates[1], 34.4);
|
|
365
|
+
});
|
|
366
|
+
it("skips person locations with neither personnelUUID nor transponderUUID", async () => {
|
|
367
|
+
const department = mockDepartment;
|
|
368
|
+
const items = [{
|
|
369
|
+
vehicleId: "12345",
|
|
370
|
+
radioName: "William Jones",
|
|
371
|
+
latitude: 34.35264,
|
|
372
|
+
longitude: -119.0669,
|
|
373
|
+
avlTime: "2026-07-10T15:43:36.816Z",
|
|
374
|
+
kindType: "person",
|
|
375
|
+
locationType: "person",
|
|
376
|
+
source: "TXRX",
|
|
377
|
+
session: "TXRX-12345",
|
|
378
|
+
visibility: [],
|
|
379
|
+
}] as unknown as LocationPayload[];
|
|
380
|
+
await location.processItems(department, items, new Date());
|
|
381
|
+
const recs = await models.Location.find({});
|
|
382
|
+
assert.equal(recs.length, 0);
|
|
383
|
+
const vehicles = await models.CADVehicle.find({});
|
|
384
|
+
assert.equal(vehicles.length, 0);
|
|
385
|
+
});
|
|
386
|
+
it("continues processing the batch after an invalid item", async () => {
|
|
387
|
+
const department = mockDepartment;
|
|
388
|
+
const items = [{
|
|
389
|
+
vehicleId: "",
|
|
390
|
+
radioName: "",
|
|
391
|
+
latitude: 0,
|
|
392
|
+
longitude: 0,
|
|
393
|
+
avlTime: "1602563494",
|
|
394
|
+
}, {
|
|
395
|
+
vehicleId: "BEEDOO1",
|
|
396
|
+
radioName: "BEEDOO1",
|
|
397
|
+
latitude: 34.35264,
|
|
398
|
+
longitude: -119.0669,
|
|
399
|
+
avlTime: "1602563494",
|
|
400
|
+
}] as unknown as LocationPayload[];
|
|
401
|
+
await location.processItems(department, items, new Date());
|
|
402
|
+
const recs = await models.Location.find({});
|
|
403
|
+
assert.equal(recs.length, 1);
|
|
404
|
+
assert.equal(recs[0]?.username, "BEEDOO1");
|
|
405
|
+
});
|
|
153
406
|
it("carries kind/type/locationType/kindColor onto the saved Location", async () => {
|
|
154
407
|
const atDate = new Date();
|
|
155
408
|
const department = mockDepartment;
|
|
@@ -176,5 +429,57 @@ describe("location", () => {
|
|
|
176
429
|
assert.equal(recs[0]?.kindColor?.text, "#FFFFFF");
|
|
177
430
|
assert.instanceOf(recs[0]?.kindColorChangedAt, Date);
|
|
178
431
|
});
|
|
432
|
+
it("stamps the department's industry on the saved location", async () => {
|
|
433
|
+
const atDate = new Date();
|
|
434
|
+
const session = "item-with-industry-utility";
|
|
435
|
+
const items = [{
|
|
436
|
+
vehicleId: "BEEDOO1",
|
|
437
|
+
radioName: "BEEDOO1",
|
|
438
|
+
latitude: 34.35264,
|
|
439
|
+
longitude: -119.0669,
|
|
440
|
+
avlTime: "1602563494",
|
|
441
|
+
session,
|
|
442
|
+
}] as unknown as LocationPayload[];
|
|
443
|
+
await location.processItems(mockDepartment, items, atDate);
|
|
444
|
+
const sut = await models.Location.findOne({ session });
|
|
445
|
+
assert.equal(sut?.industry, AccountIndustry.Utility);
|
|
446
|
+
});
|
|
447
|
+
it("falls back to fire industry when the department has none set", async () => {
|
|
448
|
+
const atDate = new Date();
|
|
449
|
+
const session = "item-with-industry-fallback";
|
|
450
|
+
const department = { ...mockDepartment, industry: undefined };
|
|
451
|
+
const items = [{
|
|
452
|
+
vehicleId: "BEEDOO1",
|
|
453
|
+
radioName: "BEEDOO1",
|
|
454
|
+
latitude: 34.35264,
|
|
455
|
+
longitude: -119.0669,
|
|
456
|
+
avlTime: "1602563494",
|
|
457
|
+
session,
|
|
458
|
+
}] as unknown as LocationPayload[];
|
|
459
|
+
await location.processItems(department, items, atDate);
|
|
460
|
+
const sut = await models.Location.findOne({ session });
|
|
461
|
+
assert.equal(sut?.industry, AccountIndustry.Fire);
|
|
462
|
+
});
|
|
463
|
+
it("updates industry in place and stamps propsChangedAt when it changes", async () => {
|
|
464
|
+
const base = {
|
|
465
|
+
vehicleId: "BEEDOO1",
|
|
466
|
+
radioName: "BEEDOO1",
|
|
467
|
+
latitude: 34.35264,
|
|
468
|
+
longitude: -119.0669,
|
|
469
|
+
avlTime: "1602563494",
|
|
470
|
+
} as unknown as LocationPayload;
|
|
471
|
+
await location.processItems(mockDepartment, [base], new Date());
|
|
472
|
+
const initial = await models.Location.find({});
|
|
473
|
+
assert.equal(initial[0]?.industry, AccountIndustry.Utility);
|
|
474
|
+
const initialPropsChangedAt = initial[0]?.propsChangedAt;
|
|
475
|
+
assert.instanceOf(initialPropsChangedAt, Date);
|
|
476
|
+
|
|
477
|
+
const lawDepartment = { ...mockDepartment, industry: AccountIndustry.Law };
|
|
478
|
+
await location.processItems(lawDepartment, [base], new Date());
|
|
479
|
+
const recs = await models.Location.find({});
|
|
480
|
+
assert.equal(recs.length, 1);
|
|
481
|
+
assert.equal(recs[0]?.industry, AccountIndustry.Law);
|
|
482
|
+
assert.isAbove(recs[0]!.propsChangedAt.valueOf(), initialPropsChangedAt!.valueOf());
|
|
483
|
+
});
|
|
179
484
|
});
|
|
180
485
|
});
|
package/src/test/mock.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as mongoose from "mongoose";
|
|
2
2
|
import {
|
|
3
3
|
BackendModels,
|
|
4
|
+
AccountIndustry,
|
|
4
5
|
Agency,
|
|
5
6
|
Department,
|
|
6
7
|
convertToObjectId as ObjectId
|
|
@@ -15,11 +16,16 @@ export default function(modelsModule: typeof import("tabletcommand-backend-model
|
|
|
15
16
|
_id: ObjectId(departmentId),
|
|
16
17
|
department: "Test Department",
|
|
17
18
|
accountType: "production",
|
|
19
|
+
industry: AccountIndustry.Utility,
|
|
18
20
|
shareAVL: {
|
|
19
21
|
enabled: true,
|
|
20
22
|
opAreaName: "Test Area",
|
|
21
23
|
opAreaCode: "TEST",
|
|
22
24
|
fadeZoomLevel: 45000,
|
|
25
|
+
area: {
|
|
26
|
+
type: "Polygon",
|
|
27
|
+
coordinates: [],
|
|
28
|
+
},
|
|
23
29
|
},
|
|
24
30
|
signupDomains: [],
|
|
25
31
|
signupKey: "abcd",
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { assert } from "chai";
|
|
2
|
+
import { describe, it, describe as context } from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
adminListLocations,
|
|
5
|
+
adminListPersonLocations,
|
|
6
|
+
adminListUAVLocations,
|
|
7
|
+
adminListAircraftLocations,
|
|
8
|
+
adminListSharedLocations,
|
|
9
|
+
adminListFocusedLocations,
|
|
10
|
+
syncListSharedLocations,
|
|
11
|
+
syncListLocations,
|
|
12
|
+
syncListPersonLocations,
|
|
13
|
+
syncListAircraftLocations,
|
|
14
|
+
syncListUAVLocations,
|
|
15
|
+
metersToRadians,
|
|
16
|
+
} from "../query";
|
|
17
|
+
|
|
18
|
+
const departmentId = "5195426cc4e016a988000969";
|
|
19
|
+
const movedAt = new Date("2024-01-01T00:00:00.000Z");
|
|
20
|
+
const modifiedDate = new Date("2024-01-02T00:00:00.000Z");
|
|
21
|
+
const longitude = -119.0669;
|
|
22
|
+
const latitude = 34.35264;
|
|
23
|
+
const diagonal = 5000;
|
|
24
|
+
|
|
25
|
+
describe("query", () => {
|
|
26
|
+
context("adminListLocations", () => {
|
|
27
|
+
it("Scopes to the department, excludes uav and person/transponder-tracked records", () => {
|
|
28
|
+
const { list } = adminListLocations({ departmentId, movedAt });
|
|
29
|
+
assert.equal(list.active, true);
|
|
30
|
+
assert.equal(list.departmentId, departmentId);
|
|
31
|
+
assert.deepEqual(list.kindType, { $ne: "uav" });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("Drops the active filter when showAll is true", () => {
|
|
35
|
+
const { list } = adminListLocations({ departmentId, movedAt, showAll: true });
|
|
36
|
+
assert.isUndefined(list.active);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("Keeps the active filter when showAll is not set", () => {
|
|
40
|
+
const { list } = adminListLocations({ departmentId, movedAt });
|
|
41
|
+
assert.property(list, "active");
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
context("adminListPersonLocations", () => {
|
|
46
|
+
it("Scopes to the department and person kindType", () => {
|
|
47
|
+
const { list } = adminListPersonLocations({ departmentId, movedAt });
|
|
48
|
+
assert.equal(list.active, true);
|
|
49
|
+
assert.equal(list.departmentId, departmentId);
|
|
50
|
+
assert.equal(list.kindType, "person");
|
|
51
|
+
assert.deepEqual(list.movedAt, { $gte: movedAt });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("Drops the active filter when showAll is true", () => {
|
|
55
|
+
const { list } = adminListPersonLocations({ departmentId, movedAt, showAll: true });
|
|
56
|
+
assert.isUndefined(list.active);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
context("adminListUAVLocations", () => {
|
|
61
|
+
it("Scopes to the department and uav kindType", () => {
|
|
62
|
+
const { list } = adminListUAVLocations({ departmentId, movedAt });
|
|
63
|
+
assert.equal(list.active, true);
|
|
64
|
+
assert.equal(list.departmentId, departmentId);
|
|
65
|
+
assert.equal(list.kindType, "uav");
|
|
66
|
+
assert.deepEqual(list.movedAt, { $gte: movedAt });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("Drops the active filter when showAll is true", () => {
|
|
70
|
+
const { list } = adminListUAVLocations({ departmentId, movedAt, showAll: true });
|
|
71
|
+
assert.isUndefined(list.active);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
context("adminListAircraftLocations", () => {
|
|
76
|
+
it("Excludes the caller's department and matches shared ADSB aircraft", () => {
|
|
77
|
+
const { list } = adminListAircraftLocations({ departmentId, movedAt, longitude, latitude, diagonal });
|
|
78
|
+
assert.equal(list.shared, true);
|
|
79
|
+
assert.equal(list.active, true);
|
|
80
|
+
assert.equal(list.source, "ADSB");
|
|
81
|
+
assert.deepEqual(list.kindType, { $in: ["fixed-wing", "helicopter"] });
|
|
82
|
+
assert.deepEqual(list.departmentId, { $ne: departmentId });
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("Sends movedAt as an ISO string", () => {
|
|
86
|
+
const { list } = adminListAircraftLocations({ departmentId, movedAt, longitude, latitude, diagonal });
|
|
87
|
+
assert.deepEqual(list.movedAt, { $gte: movedAt.toISOString() });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("Builds a $centerSphere filter as [longitude, latitude] with the diagonal converted to radians", () => {
|
|
91
|
+
const { list } = adminListAircraftLocations({ departmentId, movedAt, longitude, latitude, diagonal });
|
|
92
|
+
assert.deepEqual(list.locationGeoJSON.$geoWithin.$centerSphere, [
|
|
93
|
+
[longitude, latitude],
|
|
94
|
+
diagonal / metersToRadians,
|
|
95
|
+
]);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
context("adminListSharedLocations", () => {
|
|
100
|
+
it("Excludes the caller's department, ADSB source, and uav kindType", () => {
|
|
101
|
+
const { list } = adminListSharedLocations({ departmentId, movedAt, longitude, latitude, diagonal });
|
|
102
|
+
assert.equal(list.shared, true);
|
|
103
|
+
assert.equal(list.active, true);
|
|
104
|
+
assert.deepEqual(list.source, { $ne: "ADSB" });
|
|
105
|
+
assert.deepEqual(list.kindType, { $ne: "uav" });
|
|
106
|
+
assert.deepEqual(list.departmentId, { $ne: departmentId });
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
context("adminListFocusedLocations", () => {
|
|
111
|
+
it("Scopes to the caller's department with a geo filter", () => {
|
|
112
|
+
const { list } = adminListFocusedLocations({ departmentId, movedAt, longitude, latitude, diagonal });
|
|
113
|
+
assert.equal(list.active, true);
|
|
114
|
+
assert.equal(list.departmentId, departmentId);
|
|
115
|
+
assert.property(list, "locationGeoJSON");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("Drops the geo filter (not active) when showAll is true", () => {
|
|
119
|
+
const { list } = adminListFocusedLocations({ departmentId, movedAt, longitude, latitude, diagonal, showAll: true });
|
|
120
|
+
assert.isUndefined(list.locationGeoJSON);
|
|
121
|
+
assert.equal(list.active, true);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
context("syncListSharedLocations", () => {
|
|
126
|
+
it("Excludes the caller's department and ADSB/DroneSense sources", () => {
|
|
127
|
+
const { list } = syncListSharedLocations({ departmentId, movedAt, modifiedDate, longitude, latitude, diagonal });
|
|
128
|
+
assert.equal(list.shared, true);
|
|
129
|
+
assert.deepEqual(list.source, { $nin: ["ADSB", "DroneSense"] });
|
|
130
|
+
assert.deepEqual(list.departmentId, { $ne: departmentId });
|
|
131
|
+
assert.deepEqual(list.modified, { $gte: modifiedDate.toISOString() });
|
|
132
|
+
assert.deepEqual(list.movedAt, { $gte: movedAt.toISOString() });
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
context("syncListLocations", () => {
|
|
137
|
+
it("Scopes to the department, excludes uav and person/transponder-tracked records", () => {
|
|
138
|
+
const { list } = syncListLocations({ departmentId, movedAt, modifiedDate });
|
|
139
|
+
assert.equal(list.departmentId, departmentId);
|
|
140
|
+
assert.deepEqual(list.kindType, { $ne: "uav" });
|
|
141
|
+
assert.deepEqual(list.modified, { $gte: modifiedDate.toISOString() });
|
|
142
|
+
assert.deepEqual(list.movedAt, { $gte: movedAt.toISOString() });
|
|
143
|
+
assert.deepEqual(list.$and, [
|
|
144
|
+
{ $or: [{ personnelUUID: "" }, { personnelUUID: { $exists: false } }] },
|
|
145
|
+
{ $or: [{ transponderUUID: "" }, { transponderUUID: { $exists: false } }] },
|
|
146
|
+
]);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
context("syncListPersonLocations", () => {
|
|
151
|
+
it("Scopes to the department and person kindType", () => {
|
|
152
|
+
const { list } = syncListPersonLocations({ departmentId, movedAt, modifiedDate });
|
|
153
|
+
assert.equal(list.departmentId, departmentId);
|
|
154
|
+
assert.equal(list.kindType, "person");
|
|
155
|
+
assert.deepEqual(list.modified, { $gte: modifiedDate.toISOString() });
|
|
156
|
+
assert.deepEqual(list.movedAt, { $gte: movedAt.toISOString() });
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
context("syncListUAVLocations", () => {
|
|
161
|
+
it("Scopes to the department and uav kindType", () => {
|
|
162
|
+
const { list } = syncListUAVLocations({ departmentId, movedAt, modifiedDate });
|
|
163
|
+
assert.equal(list.departmentId, departmentId);
|
|
164
|
+
assert.equal(list.kindType, "uav");
|
|
165
|
+
assert.deepEqual(list.modified, { $gte: modifiedDate.toISOString() });
|
|
166
|
+
assert.deepEqual(list.movedAt, { $gte: movedAt.toISOString() });
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
context("syncListAircraftLocations", () => {
|
|
171
|
+
const base = { departmentId, movedAt, modifiedDate };
|
|
172
|
+
|
|
173
|
+
it("Matches shared ADSB aircraft, scoped away from the caller's department", () => {
|
|
174
|
+
const { list } = syncListAircraftLocations({ ...base, longitude, latitude, diagonal });
|
|
175
|
+
assert.equal(list.shared, true);
|
|
176
|
+
assert.equal(list.source, "ADSB");
|
|
177
|
+
assert.deepEqual(list.kindType, { $in: ["fixed-wing", "helicopter"] });
|
|
178
|
+
assert.deepEqual(list.departmentId, { $ne: departmentId });
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("Adds a geo filter when given valid, non-zero coordinates", () => {
|
|
182
|
+
const { list } = syncListAircraftLocations({ ...base, longitude, latitude, diagonal });
|
|
183
|
+
assert.deepEqual(list.locationGeoJSON.$geoWithin.$centerSphere, [
|
|
184
|
+
[longitude, latitude],
|
|
185
|
+
diagonal / metersToRadians,
|
|
186
|
+
]);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("Omits the geo filter when coordinates are at the origin", () => {
|
|
190
|
+
const { list } = syncListAircraftLocations({ ...base, longitude: 0, latitude: 0, diagonal });
|
|
191
|
+
assert.isUndefined(list.locationGeoJSON);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("Omits the geo filter when coordinates are within the origin (<= 0.01)", () => {
|
|
195
|
+
const { list } = syncListAircraftLocations({ ...base, longitude: 0.01, latitude: 0.01, diagonal });
|
|
196
|
+
assert.isUndefined(list.locationGeoJSON);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("Omits the geo filter when latitude is out of range", () => {
|
|
200
|
+
const { list } = syncListAircraftLocations({ ...base, longitude, latitude: 90, diagonal });
|
|
201
|
+
assert.isUndefined(list.locationGeoJSON);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("Omits the geo filter when longitude is out of range", () => {
|
|
205
|
+
const { list } = syncListAircraftLocations({ ...base, longitude: 180, latitude, diagonal });
|
|
206
|
+
assert.isUndefined(list.locationGeoJSON);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
});
|
package/src/types.ts
CHANGED
|
@@ -27,6 +27,21 @@ export interface LocationPayload {
|
|
|
27
27
|
heading?: number;
|
|
28
28
|
vehicleId: string;
|
|
29
29
|
visibility: LocationVisibility[];
|
|
30
|
+
// Person records: link to PersonnelKnown.uuid. A person Location needs a
|
|
31
|
+
// non-empty personnelUUID or transponderUUID with both empty it would leak
|
|
32
|
+
// into the vehicle sync endpoints (filters on personnelUUID: "").
|
|
33
|
+
personnelUUID?: string;
|
|
34
|
+
// TrackerResponder.uuid for transponder-tracked seats; who occupies the seat
|
|
35
|
+
// is resolved at read time from the responder's assignment, personnelUUID
|
|
36
|
+
// stays empty on these records.
|
|
37
|
+
transponderUUID?: string;
|
|
38
|
+
// Identity overrides. When set, these replace the derived radioName-vehicleId
|
|
39
|
+
// identity so display-name changes don't fork records (e.g. "TXRX-<uuid>").
|
|
40
|
+
// Caused because up to this point these were derived/generated and changing
|
|
41
|
+
// would then change the record and break lookups causing a new record.
|
|
42
|
+
userId?: string;
|
|
43
|
+
session?: string;
|
|
44
|
+
username?: string;
|
|
30
45
|
}
|
|
31
46
|
|
|
32
47
|
export interface Vehicle {
|