tabletcommand-backend-models 7.4.90 → 7.4.92
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/README.md +68 -1
- package/build/index.js +2 -0
- package/build/index.js.map +1 -1
- package/build/models/esri.js +26 -0
- package/build/models/esri.js.map +1 -1
- package/build/models/status-beacon-transition.js +51 -0
- package/build/models/status-beacon-transition.js.map +1 -0
- package/build/models/status-beacon.js +174 -0
- package/build/models/status-beacon.js.map +1 -0
- package/build/test/0index.js +2 -0
- package/build/test/0index.js.map +1 -1
- package/build/test/esri.js +38 -0
- package/build/test/esri.js.map +1 -1
- package/build/test/mock.js +64 -0
- package/build/test/mock.js.map +1 -1
- package/build/test/status-beacon.js +112 -0
- package/build/test/status-beacon.js.map +1 -0
- package/build/types/status-beacon.js +3 -0
- package/build/types/status-beacon.js.map +1 -0
- package/definitions/index.d.ts +12 -0
- package/definitions/index.d.ts.map +1 -1
- package/definitions/models/esri.d.ts.map +1 -1
- package/definitions/models/status-beacon-transition.d.ts +13 -0
- package/definitions/models/status-beacon-transition.d.ts.map +1 -0
- package/definitions/models/status-beacon.d.ts +13 -0
- package/definitions/models/status-beacon.d.ts.map +1 -0
- package/definitions/test/mock.d.ts +62 -0
- package/definitions/test/mock.d.ts.map +1 -1
- package/definitions/test/status-beacon.d.ts +2 -0
- package/definitions/test/status-beacon.d.ts.map +1 -0
- package/definitions/types/status-beacon.d.ts +72 -0
- package/definitions/types/status-beacon.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/index.ts +4 -0
- package/src/models/esri.ts +28 -0
- package/src/models/status-beacon-transition.ts +60 -0
- package/src/models/status-beacon.ts +193 -0
- package/src/test/0index.ts +2 -0
- package/src/test/esri.ts +46 -0
- package/src/test/mock.ts +66 -0
- package/src/test/status-beacon.ts +121 -0
- package/src/types/status-beacon.ts +90 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MongooseModule,
|
|
3
|
+
} from "../helpers";
|
|
4
|
+
import { Model } from "mongoose";
|
|
5
|
+
import { StatusBeaconTransitionType } from "../types/status-beacon";
|
|
6
|
+
|
|
7
|
+
export interface StatusBeaconTransition extends StatusBeaconTransitionType { }
|
|
8
|
+
|
|
9
|
+
export default async function StatusBeaconTransitionModule(mongoose: MongooseModule) {
|
|
10
|
+
const { Schema } = mongoose;
|
|
11
|
+
|
|
12
|
+
const modelSchema = new Schema<StatusBeaconTransitionType>({
|
|
13
|
+
_id: {
|
|
14
|
+
type: Schema.Types.ObjectId,
|
|
15
|
+
auto: true,
|
|
16
|
+
},
|
|
17
|
+
departmentId: {
|
|
18
|
+
type: String,
|
|
19
|
+
default: "",
|
|
20
|
+
required: true,
|
|
21
|
+
},
|
|
22
|
+
integration: {
|
|
23
|
+
type: String,
|
|
24
|
+
default: "",
|
|
25
|
+
required: true,
|
|
26
|
+
},
|
|
27
|
+
triggerReason: {
|
|
28
|
+
type: String,
|
|
29
|
+
default: "",
|
|
30
|
+
},
|
|
31
|
+
healthState: {
|
|
32
|
+
type: String,
|
|
33
|
+
default: "",
|
|
34
|
+
},
|
|
35
|
+
consecutiveFailures: {
|
|
36
|
+
type: Number,
|
|
37
|
+
default: 0,
|
|
38
|
+
},
|
|
39
|
+
totalFailuresSinceLastHealthy: {
|
|
40
|
+
type: Number,
|
|
41
|
+
default: 0,
|
|
42
|
+
},
|
|
43
|
+
lastError: {
|
|
44
|
+
type: String,
|
|
45
|
+
default: "",
|
|
46
|
+
},
|
|
47
|
+
}, {
|
|
48
|
+
autoIndex: false,
|
|
49
|
+
timestamps: true,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
modelSchema.index({ departmentId: 1, integration: 1, createdAt: -1 }, {
|
|
53
|
+
name: "departmentId_1_integration_1_createdAt_-1",
|
|
54
|
+
});
|
|
55
|
+
modelSchema.index({ createdAt: 1 }, { name: "createdAt_1" });
|
|
56
|
+
|
|
57
|
+
return mongoose.model<StatusBeaconTransition>("StatusBeaconTransition", modelSchema, "massive_status_beacon_transition", { overwriteModels: true });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface StatusBeaconTransitionModel extends Model<StatusBeaconTransition> { }
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MongooseModule,
|
|
3
|
+
currentDate,
|
|
4
|
+
} from "../helpers";
|
|
5
|
+
import { Model } from "mongoose";
|
|
6
|
+
import {
|
|
7
|
+
StatusBeaconType,
|
|
8
|
+
QueueDepthsType,
|
|
9
|
+
TCSendHealthType,
|
|
10
|
+
WriteBackHealthType,
|
|
11
|
+
SyncHealthType,
|
|
12
|
+
} from "../types/status-beacon";
|
|
13
|
+
|
|
14
|
+
export interface StatusBeacon extends StatusBeaconType { }
|
|
15
|
+
|
|
16
|
+
export default async function StatusBeaconModule(mongoose: MongooseModule) {
|
|
17
|
+
const { Schema } = mongoose;
|
|
18
|
+
|
|
19
|
+
const QueueDepths = new Schema<QueueDepthsType>({
|
|
20
|
+
incident: { type: Number, default: 0 },
|
|
21
|
+
status: { type: Number, default: 0 },
|
|
22
|
+
vehicle: { type: Number, default: 0 },
|
|
23
|
+
total: { type: Number, default: 0 },
|
|
24
|
+
}, { _id: false, id: false });
|
|
25
|
+
|
|
26
|
+
const TCSendHealth = new Schema<TCSendHealthType>({
|
|
27
|
+
lastSuccessfulIncidentSend: { type: Date },
|
|
28
|
+
lastSuccessfulUnitStatusSend: { type: Date },
|
|
29
|
+
sendFailuresDelta: { type: Number, default: 0 },
|
|
30
|
+
lastSendError: { type: String, default: "" },
|
|
31
|
+
}, { _id: false, id: false });
|
|
32
|
+
|
|
33
|
+
const WriteBackHealth = new Schema<WriteBackHealthType>({
|
|
34
|
+
attemptsDelta: { type: Number, default: 0 },
|
|
35
|
+
failuresDelta: { type: Number, default: 0 },
|
|
36
|
+
lastSuccessfulWriteBack: { type: Date },
|
|
37
|
+
lastError: { type: String, default: "" },
|
|
38
|
+
}, { _id: false, id: false });
|
|
39
|
+
|
|
40
|
+
const SyncHealth = new Schema<SyncHealthType>({
|
|
41
|
+
lastCacheRefresh: { type: Date },
|
|
42
|
+
cacheAgeSeconds: { type: Number, default: 0 },
|
|
43
|
+
consecutiveEmptyPolls: { type: Number, default: 0 },
|
|
44
|
+
lastPollChanges: { type: Number, default: 0 },
|
|
45
|
+
incidentCacheGrowthDelta: { type: Number, default: 0 },
|
|
46
|
+
}, { _id: false, id: false });
|
|
47
|
+
|
|
48
|
+
const modelSchema = new Schema<StatusBeaconType>({
|
|
49
|
+
_id: {
|
|
50
|
+
type: Schema.Types.ObjectId,
|
|
51
|
+
auto: true,
|
|
52
|
+
},
|
|
53
|
+
departmentId: {
|
|
54
|
+
type: String,
|
|
55
|
+
default: "",
|
|
56
|
+
required: true,
|
|
57
|
+
},
|
|
58
|
+
// Identity
|
|
59
|
+
integration: {
|
|
60
|
+
type: String,
|
|
61
|
+
default: "",
|
|
62
|
+
required: true,
|
|
63
|
+
},
|
|
64
|
+
interfaceStartTime: {
|
|
65
|
+
type: Date,
|
|
66
|
+
default: currentDate,
|
|
67
|
+
},
|
|
68
|
+
// Health State
|
|
69
|
+
healthState: {
|
|
70
|
+
type: String,
|
|
71
|
+
default: "",
|
|
72
|
+
},
|
|
73
|
+
triggerReason: {
|
|
74
|
+
type: String,
|
|
75
|
+
default: "",
|
|
76
|
+
},
|
|
77
|
+
consecutiveFailures: {
|
|
78
|
+
type: Number,
|
|
79
|
+
default: 0,
|
|
80
|
+
},
|
|
81
|
+
consecutiveSuccesses: {
|
|
82
|
+
type: Number,
|
|
83
|
+
default: 0,
|
|
84
|
+
},
|
|
85
|
+
totalFailuresSinceLastHealthy: {
|
|
86
|
+
type: Number,
|
|
87
|
+
default: 0,
|
|
88
|
+
},
|
|
89
|
+
outageStartTime: {
|
|
90
|
+
type: Date,
|
|
91
|
+
},
|
|
92
|
+
// Cycle Metrics
|
|
93
|
+
pollCycle: {
|
|
94
|
+
type: Number,
|
|
95
|
+
default: 0,
|
|
96
|
+
},
|
|
97
|
+
lastPollMs: {
|
|
98
|
+
type: Number,
|
|
99
|
+
default: 0,
|
|
100
|
+
},
|
|
101
|
+
activeIncidents: {
|
|
102
|
+
type: Number,
|
|
103
|
+
default: 0,
|
|
104
|
+
},
|
|
105
|
+
activeUnits: {
|
|
106
|
+
type: Number,
|
|
107
|
+
default: 0,
|
|
108
|
+
},
|
|
109
|
+
// Data Flow Deltas
|
|
110
|
+
incidentsDelta: {
|
|
111
|
+
type: Number,
|
|
112
|
+
default: 0,
|
|
113
|
+
},
|
|
114
|
+
unitStatusDelta: {
|
|
115
|
+
type: Number,
|
|
116
|
+
default: 0,
|
|
117
|
+
},
|
|
118
|
+
// Queue Depths
|
|
119
|
+
queueDepths: {
|
|
120
|
+
type: QueueDepths,
|
|
121
|
+
default: () => ({}),
|
|
122
|
+
},
|
|
123
|
+
// Auth Token Health
|
|
124
|
+
tokenAgeSecs: {
|
|
125
|
+
type: Number,
|
|
126
|
+
},
|
|
127
|
+
tokenExpiresAt: {
|
|
128
|
+
type: Date,
|
|
129
|
+
},
|
|
130
|
+
// TC Send Health
|
|
131
|
+
tCSend: {
|
|
132
|
+
type: TCSendHealth,
|
|
133
|
+
default: () => ({}),
|
|
134
|
+
},
|
|
135
|
+
// Write-back Health
|
|
136
|
+
writeBack: {
|
|
137
|
+
type: WriteBackHealth,
|
|
138
|
+
default: () => ({}),
|
|
139
|
+
},
|
|
140
|
+
// Sync Health
|
|
141
|
+
sync: {
|
|
142
|
+
type: SyncHealth,
|
|
143
|
+
default: () => ({}),
|
|
144
|
+
},
|
|
145
|
+
// Debug & Config
|
|
146
|
+
debugPushActive: {
|
|
147
|
+
type: Boolean,
|
|
148
|
+
default: false,
|
|
149
|
+
},
|
|
150
|
+
lastConfigSave: {
|
|
151
|
+
type: Date,
|
|
152
|
+
},
|
|
153
|
+
lastConfigSaveFile: {
|
|
154
|
+
type: String,
|
|
155
|
+
default: "",
|
|
156
|
+
},
|
|
157
|
+
// Error & Version
|
|
158
|
+
lastError: {
|
|
159
|
+
type: String,
|
|
160
|
+
default: "",
|
|
161
|
+
},
|
|
162
|
+
interfaceVersion: {
|
|
163
|
+
type: String,
|
|
164
|
+
default: "",
|
|
165
|
+
},
|
|
166
|
+
cadVersion: {
|
|
167
|
+
type: String,
|
|
168
|
+
default: "",
|
|
169
|
+
},
|
|
170
|
+
unifiedVersion: {
|
|
171
|
+
type: String,
|
|
172
|
+
default: "",
|
|
173
|
+
},
|
|
174
|
+
}, {
|
|
175
|
+
autoIndex: false,
|
|
176
|
+
timestamps: true,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
modelSchema.index({ departmentId: 1, integration: 1 }, {
|
|
180
|
+
name: "departmentId_1_integration_1",
|
|
181
|
+
unique: true,
|
|
182
|
+
});
|
|
183
|
+
modelSchema.index({ healthState: 1 }, { name: "healthState_1" });
|
|
184
|
+
modelSchema.index({ triggerReason: 1 }, { name: "triggerReason_1" });
|
|
185
|
+
modelSchema.index({ "queueDepths.total": 1 }, { name: "queueDepths_total_1" });
|
|
186
|
+
modelSchema.index({ "tCSend.sendFailuresDelta": 1 }, { name: "tCSend_sendFailuresDelta_1" });
|
|
187
|
+
modelSchema.index({ "writeBack.failuresDelta": 1 }, { name: "writeBack_failuresDelta_1" });
|
|
188
|
+
modelSchema.index({ "sync.consecutiveEmptyPolls": 1 }, { name: "sync_consecutiveEmptyPolls_1" });
|
|
189
|
+
|
|
190
|
+
return mongoose.model<StatusBeacon>("StatusBeacon", modelSchema, "massive_status_beacon", { overwriteModels: true });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export interface StatusBeaconModel extends Model<StatusBeacon> { }
|
package/src/test/0index.ts
CHANGED
|
@@ -67,6 +67,8 @@ describe(" Models", function() {
|
|
|
67
67
|
assert.isFunction(models.RemoteLogStream, "Missing RemoteLogStream");
|
|
68
68
|
assert.isFunction(models.Session, "Missing Session");
|
|
69
69
|
assert.isFunction(models.SAML, "Missing SAML");
|
|
70
|
+
assert.isFunction(models.StatusBeacon, "Missing StatusBeacon");
|
|
71
|
+
assert.isFunction(models.StatusBeaconTransition, "Missing StatusBeaconTransition");
|
|
70
72
|
assert.isFunction(models.SMSLog, "Missing SMSLog");
|
|
71
73
|
assert.isFunction(models.SMTPUnhandled, "Missing SMTPUnhandled");
|
|
72
74
|
assert.isFunction(models.Template, "Missing Template");
|
package/src/test/esri.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { assert } from "chai";
|
|
2
2
|
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
3
|
+
import { rejects as assertRejects } from "node:assert/strict";
|
|
3
4
|
import * as m from "../index";
|
|
4
5
|
import * as config from "./config";
|
|
5
6
|
import mockModule from "./mock";
|
|
@@ -59,4 +60,49 @@ describe("Esri", function() {
|
|
|
59
60
|
const props = mapPropsFound[0];
|
|
60
61
|
assert.equal(props?.download, true);
|
|
61
62
|
});
|
|
63
|
+
|
|
64
|
+
it("enforces unique arcGISAuth.username constraint", async function() {
|
|
65
|
+
await new models.Esri({
|
|
66
|
+
departmentId: new mongoose.Types.ObjectId(),
|
|
67
|
+
arcGISAuth: { username: "tc_dup_user" },
|
|
68
|
+
}).save();
|
|
69
|
+
|
|
70
|
+
// A second department claiming the same non-empty username must be rejected.
|
|
71
|
+
// assertRejects fails if the save does NOT reject, so there is no false-positive.
|
|
72
|
+
await assertRejects(
|
|
73
|
+
new models.Esri({
|
|
74
|
+
departmentId: new mongoose.Types.ObjectId(),
|
|
75
|
+
arcGISAuth: { username: "tc_dup_user" },
|
|
76
|
+
}).save(),
|
|
77
|
+
/duplicate key/
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("normalizes an empty arcGISAuth.username to null", async function() {
|
|
82
|
+
// EsriAuth.username defaults to "" (schema shared with auth); an arcGISAuth
|
|
83
|
+
// present without a real username collapses to null so it is never indexed.
|
|
84
|
+
const saved = await new models.Esri({
|
|
85
|
+
departmentId: new mongoose.Types.ObjectId(),
|
|
86
|
+
arcGISAuth: { username: "" },
|
|
87
|
+
}).save();
|
|
88
|
+
assert.isNull(saved.arcGISAuth);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("allows multiple missing/empty arcGISAuth.username and distinct values", async function() {
|
|
92
|
+
// missing arcGISAuth entirely (defaults to null) — not indexed
|
|
93
|
+
await new models.Esri({ departmentId: new mongoose.Types.ObjectId() }).save();
|
|
94
|
+
await new models.Esri({ departmentId: new mongoose.Types.ObjectId() }).save();
|
|
95
|
+
// empty-string username — normalized to null by the pre-save hook, so not indexed.
|
|
96
|
+
// Both must coexist: without normalization the second would collide on "" under $exists.
|
|
97
|
+
const empty1 = await new models.Esri({ departmentId: new mongoose.Types.ObjectId(), arcGISAuth: { username: "" } }).save();
|
|
98
|
+
const empty2 = await new models.Esri({ departmentId: new mongoose.Types.ObjectId(), arcGISAuth: { username: "" } }).save();
|
|
99
|
+
assert.isNull(empty1.arcGISAuth);
|
|
100
|
+
assert.isNull(empty2.arcGISAuth);
|
|
101
|
+
// distinct non-empty usernames — allowed
|
|
102
|
+
await new models.Esri({ departmentId: new mongoose.Types.ObjectId(), arcGISAuth: { username: "tc_user_a" } }).save();
|
|
103
|
+
await new models.Esri({ departmentId: new mongoose.Types.ObjectId(), arcGISAuth: { username: "tc_user_b" } }).save();
|
|
104
|
+
|
|
105
|
+
const count = await models.Esri.countDocuments({});
|
|
106
|
+
assert.equal(count, 6);
|
|
107
|
+
});
|
|
62
108
|
});
|
package/src/test/mock.ts
CHANGED
|
@@ -1504,6 +1504,70 @@ export default function mockModule(dependencies: { mongoose: Mongoose; }) {
|
|
|
1504
1504
|
sendNotification: false
|
|
1505
1505
|
};
|
|
1506
1506
|
|
|
1507
|
+
const statusBeacon = {
|
|
1508
|
+
_id: new mongoose.Types.ObjectId(),
|
|
1509
|
+
departmentId,
|
|
1510
|
+
integration: "test-cad-integration",
|
|
1511
|
+
interfaceStartTime: new Date(),
|
|
1512
|
+
healthState: "healthy",
|
|
1513
|
+
triggerReason: "poll_success",
|
|
1514
|
+
consecutiveFailures: 0,
|
|
1515
|
+
consecutiveSuccesses: 5,
|
|
1516
|
+
totalFailuresSinceLastHealthy: 0,
|
|
1517
|
+
outageStartTime: undefined,
|
|
1518
|
+
pollCycle: 42,
|
|
1519
|
+
lastPollMs: 350,
|
|
1520
|
+
activeIncidents: 3,
|
|
1521
|
+
activeUnits: 12,
|
|
1522
|
+
incidentsDelta: 1,
|
|
1523
|
+
unitStatusDelta: 2,
|
|
1524
|
+
queueDepths: {
|
|
1525
|
+
incident: 0,
|
|
1526
|
+
status: 0,
|
|
1527
|
+
vehicle: 0,
|
|
1528
|
+
total: 0,
|
|
1529
|
+
},
|
|
1530
|
+
tokenAgeSecs: 1800,
|
|
1531
|
+
tokenExpiresAt: new Date(),
|
|
1532
|
+
tCSend: {
|
|
1533
|
+
lastSuccessfulIncidentSend: new Date(),
|
|
1534
|
+
lastSuccessfulUnitStatusSend: new Date(),
|
|
1535
|
+
sendFailuresDelta: 0,
|
|
1536
|
+
lastSendError: "",
|
|
1537
|
+
},
|
|
1538
|
+
writeBack: {
|
|
1539
|
+
attemptsDelta: 0,
|
|
1540
|
+
failuresDelta: 0,
|
|
1541
|
+
lastSuccessfulWriteBack: new Date(),
|
|
1542
|
+
lastError: "",
|
|
1543
|
+
},
|
|
1544
|
+
sync: {
|
|
1545
|
+
lastCacheRefresh: new Date(),
|
|
1546
|
+
cacheAgeSeconds: 30,
|
|
1547
|
+
consecutiveEmptyPolls: 0,
|
|
1548
|
+
lastPollChanges: 2,
|
|
1549
|
+
incidentCacheGrowthDelta: 1,
|
|
1550
|
+
},
|
|
1551
|
+
debugPushActive: false,
|
|
1552
|
+
lastConfigSave: new Date(),
|
|
1553
|
+
lastConfigSaveFile: "config-v1.json",
|
|
1554
|
+
lastError: "",
|
|
1555
|
+
interfaceVersion: "1.2.3",
|
|
1556
|
+
cadVersion: "4.5.6",
|
|
1557
|
+
unifiedVersion: "7.8.9",
|
|
1558
|
+
};
|
|
1559
|
+
|
|
1560
|
+
const statusBeaconTransition = {
|
|
1561
|
+
_id: new mongoose.Types.ObjectId(),
|
|
1562
|
+
departmentId,
|
|
1563
|
+
integration: "test-cad-integration",
|
|
1564
|
+
triggerReason: "consecutive_failures",
|
|
1565
|
+
healthState: "degraded",
|
|
1566
|
+
consecutiveFailures: 3,
|
|
1567
|
+
totalFailuresSinceLastHealthy: 5,
|
|
1568
|
+
lastError: "connection timeout",
|
|
1569
|
+
};
|
|
1570
|
+
|
|
1507
1571
|
const validationReport = {
|
|
1508
1572
|
_id: new mongoose.Types.ObjectId(),
|
|
1509
1573
|
departmentId: new mongoose.Types.ObjectId("56131f724143487a10000001"),
|
|
@@ -1601,6 +1665,8 @@ export default function mockModule(dependencies: { mongoose: Mongoose; }) {
|
|
|
1601
1665
|
releaseNote,
|
|
1602
1666
|
session,
|
|
1603
1667
|
saml,
|
|
1668
|
+
statusBeacon,
|
|
1669
|
+
statusBeaconTransition,
|
|
1604
1670
|
template,
|
|
1605
1671
|
user,
|
|
1606
1672
|
userDevice,
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { assert } from "chai";
|
|
2
|
+
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
3
|
+
import * as m from "../index";
|
|
4
|
+
import * as config from "./config";
|
|
5
|
+
import mockModule from "./mock";
|
|
6
|
+
|
|
7
|
+
describe("StatusBeacon", function() {
|
|
8
|
+
let models: m.BackendModels, mongoose: m.MongooseModule;
|
|
9
|
+
let testItem: Partial<m.StatusBeacon>;
|
|
10
|
+
beforeEach(async function() {
|
|
11
|
+
const c = await m.connect(config.url);
|
|
12
|
+
models = c.models;
|
|
13
|
+
mongoose = c.mongoose;
|
|
14
|
+
|
|
15
|
+
const mock = mockModule({ mongoose });
|
|
16
|
+
testItem = mock.statusBeacon;
|
|
17
|
+
await mock.beforeEach();
|
|
18
|
+
});
|
|
19
|
+
afterEach(async function() {
|
|
20
|
+
await mongoose.disconnect();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("is saved", async function() {
|
|
24
|
+
const item = new models.StatusBeacon(testItem);
|
|
25
|
+
const sut = await item.save();
|
|
26
|
+
|
|
27
|
+
assert.isNotNull(sut._id);
|
|
28
|
+
assert.equal(testItem.departmentId, sut.departmentId);
|
|
29
|
+
assert.equal(testItem.integration, sut.integration);
|
|
30
|
+
assert.equal(testItem.healthState, sut.healthState);
|
|
31
|
+
assert.equal(testItem.triggerReason, sut.triggerReason);
|
|
32
|
+
assert.equal(testItem.consecutiveFailures, sut.consecutiveFailures);
|
|
33
|
+
assert.equal(testItem.consecutiveSuccesses, sut.consecutiveSuccesses);
|
|
34
|
+
assert.equal(testItem.totalFailuresSinceLastHealthy, sut.totalFailuresSinceLastHealthy);
|
|
35
|
+
assert.equal(testItem.pollCycle, sut.pollCycle);
|
|
36
|
+
assert.equal(testItem.lastPollMs, sut.lastPollMs);
|
|
37
|
+
assert.equal(testItem.activeIncidents, sut.activeIncidents);
|
|
38
|
+
assert.equal(testItem.activeUnits, sut.activeUnits);
|
|
39
|
+
assert.equal(testItem.incidentsDelta, sut.incidentsDelta);
|
|
40
|
+
assert.equal(testItem.unitStatusDelta, sut.unitStatusDelta);
|
|
41
|
+
assert.equal(testItem.tokenAgeSecs, sut.tokenAgeSecs);
|
|
42
|
+
assert.equal(testItem.debugPushActive, sut.debugPushActive);
|
|
43
|
+
assert.equal(testItem.lastConfigSaveFile, sut.lastConfigSaveFile);
|
|
44
|
+
assert.equal(testItem.lastError, sut.lastError);
|
|
45
|
+
assert.equal(testItem.interfaceVersion, sut.interfaceVersion);
|
|
46
|
+
assert.equal(testItem.cadVersion, sut.cadVersion);
|
|
47
|
+
assert.equal(testItem.unifiedVersion, sut.unifiedVersion);
|
|
48
|
+
assert.isNotNull(sut.createdAt);
|
|
49
|
+
assert.isNotNull(sut.updatedAt);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("saves nested queueDepths", async function() {
|
|
53
|
+
const item = new models.StatusBeacon(testItem);
|
|
54
|
+
const sut = await item.save();
|
|
55
|
+
|
|
56
|
+
assert.equal(testItem.queueDepths?.incident, sut.queueDepths.incident);
|
|
57
|
+
assert.equal(testItem.queueDepths?.status, sut.queueDepths.status);
|
|
58
|
+
assert.equal(testItem.queueDepths?.vehicle, sut.queueDepths.vehicle);
|
|
59
|
+
assert.equal(testItem.queueDepths?.total, sut.queueDepths.total);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("saves nested tCSend health", async function() {
|
|
63
|
+
const item = new models.StatusBeacon(testItem);
|
|
64
|
+
const sut = await item.save();
|
|
65
|
+
|
|
66
|
+
assert.equal(testItem.tCSend?.sendFailuresDelta, sut.tCSend.sendFailuresDelta);
|
|
67
|
+
assert.equal(testItem.tCSend?.lastSendError, sut.tCSend.lastSendError);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("saves nested writeBack health", async function() {
|
|
71
|
+
const item = new models.StatusBeacon(testItem);
|
|
72
|
+
const sut = await item.save();
|
|
73
|
+
|
|
74
|
+
assert.equal(testItem.writeBack?.attemptsDelta, sut.writeBack.attemptsDelta);
|
|
75
|
+
assert.equal(testItem.writeBack?.failuresDelta, sut.writeBack.failuresDelta);
|
|
76
|
+
assert.equal(testItem.writeBack?.lastError, sut.writeBack.lastError);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("saves nested sync health", async function() {
|
|
80
|
+
const item = new models.StatusBeacon(testItem);
|
|
81
|
+
const sut = await item.save();
|
|
82
|
+
|
|
83
|
+
assert.equal(testItem.sync?.cacheAgeSeconds, sut.sync.cacheAgeSeconds);
|
|
84
|
+
assert.equal(testItem.sync?.consecutiveEmptyPolls, sut.sync.consecutiveEmptyPolls);
|
|
85
|
+
assert.equal(testItem.sync?.lastPollChanges, sut.sync.lastPollChanges);
|
|
86
|
+
assert.equal(testItem.sync?.incidentCacheGrowthDelta, sut.sync.incidentCacheGrowthDelta);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("StatusBeaconTransition", function() {
|
|
91
|
+
let models: m.BackendModels, mongoose: m.MongooseModule;
|
|
92
|
+
let testItem: Partial<m.StatusBeaconTransition>;
|
|
93
|
+
beforeEach(async function() {
|
|
94
|
+
const c = await m.connect(config.url);
|
|
95
|
+
models = c.models;
|
|
96
|
+
mongoose = c.mongoose;
|
|
97
|
+
|
|
98
|
+
const mock = mockModule({ mongoose });
|
|
99
|
+
testItem = mock.statusBeaconTransition;
|
|
100
|
+
await mock.beforeEach();
|
|
101
|
+
});
|
|
102
|
+
afterEach(async function() {
|
|
103
|
+
await mongoose.disconnect();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("is saved", async function() {
|
|
107
|
+
const item = new models.StatusBeaconTransition(testItem);
|
|
108
|
+
const sut = await item.save();
|
|
109
|
+
|
|
110
|
+
assert.isNotNull(sut._id);
|
|
111
|
+
assert.equal(testItem.departmentId, sut.departmentId);
|
|
112
|
+
assert.equal(testItem.integration, sut.integration);
|
|
113
|
+
assert.equal(testItem.triggerReason, sut.triggerReason);
|
|
114
|
+
assert.equal(testItem.healthState, sut.healthState);
|
|
115
|
+
assert.equal(testItem.consecutiveFailures, sut.consecutiveFailures);
|
|
116
|
+
assert.equal(testItem.totalFailuresSinceLastHealthy, sut.totalFailuresSinceLastHealthy);
|
|
117
|
+
assert.equal(testItem.lastError, sut.lastError);
|
|
118
|
+
assert.isNotNull(sut.createdAt);
|
|
119
|
+
assert.isNotNull(sut.updatedAt);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Types } from "mongoose";
|
|
2
|
+
|
|
3
|
+
export interface QueueDepthsType {
|
|
4
|
+
incident: number;
|
|
5
|
+
status: number;
|
|
6
|
+
vehicle: number;
|
|
7
|
+
total: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface TCSendHealthType {
|
|
11
|
+
lastSuccessfulIncidentSend?: Date;
|
|
12
|
+
lastSuccessfulUnitStatusSend?: Date;
|
|
13
|
+
sendFailuresDelta: number;
|
|
14
|
+
lastSendError: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface WriteBackHealthType {
|
|
18
|
+
attemptsDelta: number;
|
|
19
|
+
failuresDelta: number;
|
|
20
|
+
lastSuccessfulWriteBack?: Date;
|
|
21
|
+
lastError: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SyncHealthType {
|
|
25
|
+
lastCacheRefresh?: Date;
|
|
26
|
+
cacheAgeSeconds: number;
|
|
27
|
+
consecutiveEmptyPolls: number;
|
|
28
|
+
lastPollChanges: number;
|
|
29
|
+
incidentCacheGrowthDelta: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface StatusBeaconType {
|
|
33
|
+
_id: Types.ObjectId;
|
|
34
|
+
departmentId: string;
|
|
35
|
+
// Identity
|
|
36
|
+
integration: string;
|
|
37
|
+
interfaceStartTime: Date; // UTC time the integration service process started; never resets — use for uptime
|
|
38
|
+
// Health State
|
|
39
|
+
healthState: string;
|
|
40
|
+
triggerReason: string;
|
|
41
|
+
consecutiveFailures: number;
|
|
42
|
+
consecutiveSuccesses: number;
|
|
43
|
+
totalFailuresSinceLastHealthy: number;
|
|
44
|
+
outageStartTime?: Date; // UTC time the current outage began; absent when healthy
|
|
45
|
+
// Cycle Metrics
|
|
46
|
+
pollCycle: number;
|
|
47
|
+
lastPollMs: number;
|
|
48
|
+
activeIncidents: number;
|
|
49
|
+
activeUnits: number;
|
|
50
|
+
// Data Flow Deltas
|
|
51
|
+
incidentsDelta: number;
|
|
52
|
+
unitStatusDelta: number;
|
|
53
|
+
// Queue Depths
|
|
54
|
+
queueDepths: QueueDepthsType;
|
|
55
|
+
// Auth Token Health (Bearer auth only; null otherwise)
|
|
56
|
+
tokenAgeSecs?: number;
|
|
57
|
+
tokenExpiresAt?: Date;
|
|
58
|
+
// TC Send Health
|
|
59
|
+
tCSend: TCSendHealthType;
|
|
60
|
+
// Write-back Health
|
|
61
|
+
writeBack: WriteBackHealthType;
|
|
62
|
+
// Sync Health
|
|
63
|
+
sync: SyncHealthType;
|
|
64
|
+
// Debug & Config
|
|
65
|
+
debugPushActive: boolean;
|
|
66
|
+
lastConfigSave?: Date;
|
|
67
|
+
lastConfigSaveFile: string;
|
|
68
|
+
// Error & Version
|
|
69
|
+
lastError: string;
|
|
70
|
+
interfaceVersion: string;
|
|
71
|
+
cadVersion: string;
|
|
72
|
+
unifiedVersion: string;
|
|
73
|
+
// System (managed by Mongoose timestamps)
|
|
74
|
+
createdAt: Date;
|
|
75
|
+
updatedAt: Date;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface StatusBeaconTransitionType {
|
|
79
|
+
_id: Types.ObjectId;
|
|
80
|
+
departmentId: string;
|
|
81
|
+
integration: string;
|
|
82
|
+
triggerReason: string;
|
|
83
|
+
healthState: string;
|
|
84
|
+
consecutiveFailures: number;
|
|
85
|
+
totalFailuresSinceLastHealthy: number;
|
|
86
|
+
lastError: string;
|
|
87
|
+
// System (managed by Mongoose timestamps)
|
|
88
|
+
createdAt: Date;
|
|
89
|
+
updatedAt: Date;
|
|
90
|
+
}
|