tabletcommand-backend-models 7.4.33 → 7.4.35

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 (41) hide show
  1. package/build/index.js +1 -0
  2. package/build/index.js.map +1 -1
  3. package/build/models/audio-stream-authentication.js +52 -0
  4. package/build/models/audio-stream-authentication.js.map +1 -0
  5. package/build/models/department.js +42 -1
  6. package/build/models/department.js.map +1 -1
  7. package/build/test/audio-stream-authentication.js +74 -0
  8. package/build/test/audio-stream-authentication.js.map +1 -0
  9. package/build/test/audio-stream.js +212 -0
  10. package/build/test/audio-stream.js.map +1 -0
  11. package/build/test/department.js +48 -0
  12. package/build/test/department.js.map +1 -1
  13. package/build/test/mock.js +10 -0
  14. package/build/test/mock.js.map +1 -1
  15. package/build/types/audio-stream-authentication.js +3 -0
  16. package/build/types/audio-stream-authentication.js.map +1 -0
  17. package/definitions/index.d.ts +7 -0
  18. package/definitions/index.d.ts.map +1 -1
  19. package/definitions/models/audio-stream-authentication.d.ts +13 -0
  20. package/definitions/models/audio-stream-authentication.d.ts.map +1 -0
  21. package/definitions/models/department.d.ts.map +1 -1
  22. package/definitions/test/audio-stream-authentication.d.ts +2 -0
  23. package/definitions/test/audio-stream-authentication.d.ts.map +1 -0
  24. package/definitions/test/audio-stream.d.ts +2 -0
  25. package/definitions/test/audio-stream.d.ts.map +1 -0
  26. package/definitions/test/mock.d.ts +9 -0
  27. package/definitions/test/mock.d.ts.map +1 -1
  28. package/definitions/types/audio-stream-authentication.d.ts +9 -0
  29. package/definitions/types/audio-stream-authentication.d.ts.map +1 -0
  30. package/definitions/types/department.d.ts +10 -0
  31. package/definitions/types/department.d.ts.map +1 -1
  32. package/package.json +1 -1
  33. package/src/index.ts +9 -0
  34. package/src/models/audio-stream-authentication.ts +63 -0
  35. package/src/models/department.ts +44 -1
  36. package/src/test/audio-stream-authentication.ts +81 -0
  37. package/src/test/audio-stream.ts +230 -0
  38. package/src/test/department.ts +52 -0
  39. package/src/test/mock.ts +11 -0
  40. package/src/types/audio-stream-authentication.ts +9 -0
  41. package/src/types/department.ts +12 -0
@@ -24,6 +24,7 @@ import {
24
24
  AccountConfigurationCallerLocation,
25
25
  AccountConfigurationCallerLocationResolver,
26
26
  ADSBConfigType,
27
+ AudioStreamAuthType,
27
28
  AudioStreamGroupType,
28
29
  AudioStreamType,
29
30
  CADGetLocationSettingsType,
@@ -811,6 +812,25 @@ export default async function DepartmentModule(mongoose: MongooseModule) {
811
812
  type: String,
812
813
  default: "",
813
814
  },
815
+ // Extended properties for SOAP and other protocols
816
+ timeout: {
817
+ type: Number,
818
+ default: undefined,
819
+ },
820
+ namespace: {
821
+ type: String,
822
+ default: "",
823
+ },
824
+ soapVersion: {
825
+ type: String,
826
+ enum: ["1.1", "1.2"],
827
+ default: undefined,
828
+ },
829
+ customHeaders: {
830
+ type: Map,
831
+ of: String,
832
+ default: {},
833
+ },
814
834
  }, {
815
835
  _id: false,
816
836
  id: false,
@@ -1168,6 +1188,25 @@ export default async function DepartmentModule(mongoose: MongooseModule) {
1168
1188
  id: false,
1169
1189
  });
1170
1190
 
1191
+ const AudioStreamAuth = new Schema<AudioStreamAuthType>({
1192
+ authId: {
1193
+ type: Schema.Types.ObjectId,
1194
+ ref: "AudioStreamAuthentication",
1195
+ required: true,
1196
+ },
1197
+ app: {
1198
+ type: String,
1199
+ required: true,
1200
+ },
1201
+ streamKey: {
1202
+ type: String,
1203
+ required: true,
1204
+ },
1205
+ }, {
1206
+ _id: false,
1207
+ id: false,
1208
+ });
1209
+
1171
1210
  const AudioStream = new Schema<AudioStreamType>({
1172
1211
  // eg. Central Dispatch Talk Group or also Available on 89.5 MHz
1173
1212
  description: {
@@ -1186,7 +1225,11 @@ export default async function DepartmentModule(mongoose: MongooseModule) {
1186
1225
  order: {
1187
1226
  type: Number,
1188
1227
  default: 0,
1189
- }
1228
+ },
1229
+ authentication: {
1230
+ type: AudioStreamAuth,
1231
+ default: undefined,
1232
+ },
1190
1233
  }, {
1191
1234
  _id: false,
1192
1235
  id: false,
@@ -0,0 +1,81 @@
1
+ import { assert } from "chai";
2
+ import "mocha";
3
+
4
+ import * as m from "../index";
5
+ import * as config from "./config";
6
+ import mockModule from "./mock";
7
+
8
+ describe("AudioStreamAuthentication", function() {
9
+ let models: m.BackendModels, mongoose: m.MongooseModule;
10
+ let testItem: Partial<m.AudioStreamAuthentication>;
11
+ beforeEach(async function() {
12
+ const c = await m.connect(config.url);
13
+ models = c.models;
14
+ mongoose = c.mongoose;
15
+ const mock = mockModule({
16
+ mongoose
17
+ });
18
+ testItem = mock.audioStreamAuthentication;
19
+ await mock.cleanup();
20
+ });
21
+
22
+ afterEach(async function() {
23
+ await mongoose.disconnect();
24
+ });
25
+
26
+ it("is saved", async function() {
27
+ assert.isObject(testItem);
28
+ const item = new models.AudioStreamAuthentication(testItem);
29
+ const sut = await item.save();
30
+ assert.isNotNull(sut._id);
31
+ assert.isNotNull(sut.id);
32
+ assert.equal(sut._id.toString(), testItem._id?.toString());
33
+
34
+ assert.equal(sut.name, "test-stream");
35
+ assert.equal(sut.rotateInterval, 3600);
36
+
37
+ assert.isObject(sut.secret);
38
+ assert.equal(sut.secret?.iv, testItem.secret?.iv);
39
+ assert.equal(sut.secret?.encryptedData, testItem.secret?.encryptedData);
40
+ });
41
+
42
+ it("enforces unique name constraint", async function() {
43
+ const item1 = new models.AudioStreamAuthentication(testItem);
44
+ await item1.save();
45
+
46
+ const item2 = new models.AudioStreamAuthentication({
47
+ name: "test-stream",
48
+ secret: {
49
+ iv: "different-iv",
50
+ encryptedData: "different-data"
51
+ },
52
+ rotateInterval: 7200,
53
+ });
54
+
55
+ try {
56
+ await item2.save();
57
+ assert.fail("Should have thrown duplicate key error");
58
+ } catch (error) {
59
+ assert.include(String((error as Error).message), "duplicate key");
60
+ }
61
+ });
62
+
63
+ it("allows different names", async function() {
64
+ const item1 = new models.AudioStreamAuthentication(testItem);
65
+ await item1.save();
66
+
67
+ const item2 = new models.AudioStreamAuthentication({
68
+ name: "different-stream",
69
+ secret: {
70
+ iv: "different-iv",
71
+ encryptedData: "different-data"
72
+ },
73
+ rotateInterval: 7200,
74
+ });
75
+
76
+ const sut = await item2.save();
77
+ assert.isNotNull(sut._id);
78
+ assert.equal(sut.name, "different-stream");
79
+ assert.equal(sut.rotateInterval, 7200);
80
+ });
81
+ });
@@ -0,0 +1,230 @@
1
+ import { assert } from "chai";
2
+ import "mocha";
3
+
4
+ import * as m from "../index";
5
+ import * as config from "./config";
6
+ import mockModule from "./mock";
7
+
8
+ describe("AudioStream", function() {
9
+ let models: m.BackendModels, mongoose: m.MongooseModule;
10
+ let authRecord: m.AudioStreamAuthentication;
11
+
12
+ beforeEach(async function() {
13
+ const c = await m.connect(config.url);
14
+ models = c.models;
15
+ mongoose = c.mongoose;
16
+ const mock = mockModule({
17
+ mongoose
18
+ });
19
+ await mock.cleanup();
20
+
21
+ // Create an AudioStreamAuthentication record to reference
22
+ const authItem = new models.AudioStreamAuthentication({
23
+ name: "test-auth-record",
24
+ secret: {
25
+ iv: "test-iv",
26
+ encryptedData: "test-encrypted-data"
27
+ },
28
+ rotateInterval: 3600,
29
+ });
30
+ authRecord = await authItem.save();
31
+ });
32
+
33
+ afterEach(async function() {
34
+ await mongoose.disconnect();
35
+ });
36
+
37
+ it("saves Department with AudioStream authentication", async function() {
38
+ const department = new models.Department({
39
+ department: "Test Fire Department",
40
+ apikey: "test-api-key-123",
41
+ audioConfiguration: [
42
+ {
43
+ group: "Fire Dispatch",
44
+ order: 1,
45
+ streams: [
46
+ {
47
+ description: "Main Dispatch Channel",
48
+ channel: "CH1",
49
+ url: "https://example.com/stream1",
50
+ order: 1,
51
+ authentication: {
52
+ authId: authRecord._id,
53
+ app: "test-app",
54
+ streamKey: "stream-key-123",
55
+ }
56
+ }
57
+ ]
58
+ }
59
+ ]
60
+ });
61
+
62
+ const saved = await department.save();
63
+
64
+ assert.isNotNull(saved._id);
65
+ assert.isArray(saved.audioConfiguration);
66
+ assert.equal(saved.audioConfiguration.length, 1);
67
+
68
+ const group = saved.audioConfiguration[0];
69
+ if (!group) {
70
+ assert.fail("Group should be defined");
71
+ return;
72
+ }
73
+ assert.equal(group.group, "Fire Dispatch");
74
+ assert.equal(group.order, 1);
75
+ assert.equal(group.streams.length, 1);
76
+
77
+ const stream = group.streams[0];
78
+ if (!stream) {
79
+ assert.fail("Stream should be defined");
80
+ return;
81
+ }
82
+ assert.equal(stream.description, "Main Dispatch Channel");
83
+ assert.equal(stream.channel, "CH1");
84
+ assert.equal(stream.url, "https://example.com/stream1");
85
+ assert.equal(stream.order, 1);
86
+
87
+ // Verify authentication
88
+ assert.isObject(stream.authentication);
89
+ if (!stream.authentication) {
90
+ assert.fail("Authentication should be defined");
91
+ return;
92
+ }
93
+ assert.equal(stream.authentication.authId.toString(), authRecord._id.toString());
94
+ assert.equal(stream.authentication.app, "test-app");
95
+ assert.equal(stream.authentication.streamKey, "stream-key-123");
96
+ });
97
+
98
+ it("saves Department with AudioStream without authentication", async function() {
99
+ const department = new models.Department({
100
+ department: "Test Fire Department 2",
101
+ apikey: "test-api-key-456",
102
+ audioConfiguration: [
103
+ {
104
+ group: "Police Dispatch",
105
+ order: 1,
106
+ streams: [
107
+ {
108
+ description: "Public Channel",
109
+ channel: "CH2",
110
+ url: "https://example.com/stream2",
111
+ order: 1,
112
+ }
113
+ ]
114
+ }
115
+ ]
116
+ });
117
+
118
+ const saved = await department.save();
119
+
120
+ assert.isNotNull(saved._id);
121
+ assert.isArray(saved.audioConfiguration);
122
+ assert.equal(saved.audioConfiguration.length, 1);
123
+
124
+ const group = saved.audioConfiguration[0];
125
+ if (!group) {
126
+ assert.fail("Group should be defined");
127
+ return;
128
+ }
129
+ const stream = group.streams[0];
130
+ if (!stream) {
131
+ assert.fail("Stream should be defined");
132
+ return;
133
+ }
134
+ assert.equal(stream.description, "Public Channel");
135
+ assert.isUndefined(stream.authentication);
136
+ });
137
+
138
+ it("saves Department with mixed AudioStreams (with and without auth)", async function() {
139
+ const department = new models.Department({
140
+ department: "Test Mixed Department",
141
+ apikey: "test-api-key-789",
142
+ audioConfiguration: [
143
+ {
144
+ group: "Dispatch Channels",
145
+ order: 1,
146
+ streams: [
147
+ {
148
+ description: "Secure Channel",
149
+ channel: "CH1",
150
+ url: "https://example.com/secure",
151
+ order: 1,
152
+ authentication: {
153
+ authId: authRecord._id,
154
+ app: "secure-app",
155
+ streamKey: "secure-key-456",
156
+ }
157
+ },
158
+ {
159
+ description: "Public Channel",
160
+ channel: "CH2",
161
+ url: "https://example.com/public",
162
+ order: 2,
163
+ }
164
+ ]
165
+ }
166
+ ]
167
+ });
168
+
169
+ const saved = await department.save();
170
+
171
+ const group = saved.audioConfiguration[0];
172
+ if (!group) {
173
+ assert.fail("Group should be defined");
174
+ return;
175
+ }
176
+ assert.equal(group.streams.length, 2);
177
+
178
+ const secureStream = group.streams[0];
179
+ if (!secureStream) {
180
+ assert.fail("Secure stream should be defined");
181
+ return;
182
+ }
183
+ assert.isObject(secureStream.authentication);
184
+ if (!secureStream.authentication) {
185
+ assert.fail("Authentication should be defined");
186
+ return;
187
+ }
188
+ assert.equal(secureStream.authentication.app, "secure-app");
189
+
190
+ const publicStream = group.streams[1];
191
+ if (!publicStream) {
192
+ assert.fail("Public stream should be defined");
193
+ return;
194
+ }
195
+ assert.isUndefined(publicStream.authentication);
196
+ });
197
+
198
+ it("validates required authentication fields when present", async function() {
199
+ const department = new models.Department({
200
+ department: "Test Invalid Department",
201
+ apikey: "test-api-key-invalid",
202
+ audioConfiguration: [
203
+ {
204
+ group: "Test Group",
205
+ order: 1,
206
+ streams: [
207
+ {
208
+ description: "Invalid Stream",
209
+ channel: "CH1",
210
+ url: "https://example.com/invalid",
211
+ order: 1,
212
+ authentication: {
213
+ authId: authRecord._id,
214
+ app: "test-app",
215
+ // Missing streamKey - should fail validation
216
+ } as unknown as { authId: m.AudioStreamAuthentication["_id"]; app: string; streamKey: string; }
217
+ }
218
+ ]
219
+ }
220
+ ]
221
+ });
222
+
223
+ try {
224
+ await department.save();
225
+ assert.fail("Should have thrown validation error");
226
+ } catch (error) {
227
+ assert.include(String((error as Error).message), "streamKey");
228
+ }
229
+ });
230
+ });
@@ -15,6 +15,7 @@ describe("Department", function() {
15
15
  mongoose
16
16
  });
17
17
  testItem = mock.department;
18
+ await mock.cleanup();
18
19
  });
19
20
 
20
21
  afterEach(async function() {
@@ -99,4 +100,55 @@ describe("Department", function() {
99
100
  assert.isObject(callerResolver);
100
101
  assert.equal(callerResolver?.type, m.AccountCallerType.Dummy);
101
102
  });
103
+
104
+ it("saves SOAP forwarding connection properties", async function() {
105
+ // **Feature: eso-soap-incident-forwarding, Property 6: Configuration Validation**
106
+ // **Validates: Requirements 3.2, 6.5**
107
+ const soapConnection: m.ForwardingConnectionType = {
108
+ active: true,
109
+ connectionType: "eso",
110
+ fields: [{
111
+ key: "IncidentNumber",
112
+ value: "incidentNumber",
113
+ required: true,
114
+ enabled: true,
115
+ transformationRequired: false,
116
+ }],
117
+ callTypes: ["FIRE"],
118
+ forwardAll: false,
119
+ apiUrl: "https://eso.example.com/soap",
120
+ authType: "basic",
121
+ authUser: "user",
122
+ authKey: "pass",
123
+ authKeySecret: "",
124
+ label: "ESO Service",
125
+ description: "ESO SOAP service",
126
+ timeout: 30000,
127
+ namespace: "http://eso.namespace",
128
+ soapVersion: "1.2" as const,
129
+ customHeaders: {
130
+ SOAPAction: "SubmitIncident"
131
+ }
132
+ };
133
+
134
+ const testItemWithForwarding = testItem as m.Department;
135
+ testItemWithForwarding.forwarding = {
136
+ enabled: true,
137
+ connections: [soapConnection]
138
+ };
139
+
140
+ const item = new models.Department(testItemWithForwarding);
141
+ const sut = await item.save();
142
+
143
+ // Validate SOAP-specific properties are saved correctly
144
+ const connection = sut.forwarding.connections[0];
145
+ assert.isNotNull(connection);
146
+ assert.isDefined(connection);
147
+ assert.equal(connection.connectionType, "eso");
148
+ assert.equal(connection.timeout, 30000);
149
+ assert.equal(connection.namespace, "http://eso.namespace");
150
+ assert.equal(connection.soapVersion, "1.2");
151
+ assert.instanceOf(connection.customHeaders, Map);
152
+ assert.equal(connection.customHeaders.get("SOAPAction"), "SubmitIncident");
153
+ });
102
154
  });
package/src/test/mock.ts CHANGED
@@ -939,6 +939,16 @@ export default function mockModule(dependencies: { mongoose: Mongoose; }) {
939
939
  },
940
940
  };
941
941
 
942
+ const audioStreamAuthentication = {
943
+ _id: new mongoose.Types.ObjectId(),
944
+ name: "test-stream",
945
+ secret: {
946
+ iv: "a1b2c3d4e5f6",
947
+ encryptedData: "encrypted-secret-data-here"
948
+ },
949
+ rotateInterval: 3600,
950
+ };
951
+
942
952
  const gstMapping = {
943
953
  _id: new mongoose.Types.ObjectId(),
944
954
  departmentId: "d123",
@@ -1475,6 +1485,7 @@ export default function mockModule(dependencies: { mongoose: Mongoose; }) {
1475
1485
  agency,
1476
1486
  arcGISGroup,
1477
1487
  assignment,
1488
+ audioStreamAuthentication,
1478
1489
  battalion,
1479
1490
  cadIncident,
1480
1491
  cadIncidentBlock,
@@ -0,0 +1,9 @@
1
+ import { Types } from "mongoose";
2
+ import { EncryptedDataType } from "./common";
3
+
4
+ export interface AudioStreamAuthenticationType {
5
+ _id: Types.ObjectId,
6
+ secret: EncryptedDataType,
7
+ rotateInterval: number,
8
+ name: string,
9
+ }
@@ -243,11 +243,18 @@ export interface WebDisclaimerType {
243
243
  enabled: boolean,
244
244
  }
245
245
 
246
+ export interface AudioStreamAuthType {
247
+ authId: Types.ObjectId,
248
+ app: string,
249
+ streamKey: string,
250
+ }
251
+
246
252
  export interface AudioStreamType {
247
253
  description: string,
248
254
  channel: string,
249
255
  url: string,
250
256
  order: number,
257
+ authentication?: AudioStreamAuthType,
251
258
  }
252
259
 
253
260
  export interface AudioStreamGroupType {
@@ -371,6 +378,11 @@ export interface ForwardingConnectionType {
371
378
  authKeySecret: string,
372
379
  label: string,
373
380
  description: string,
381
+ // Extended properties for SOAP and other protocols
382
+ timeout?: number,
383
+ namespace?: string,
384
+ soapVersion?: "1.1" | "1.2",
385
+ customHeaders?: Record<string, string>,
374
386
  }
375
387
 
376
388
  export interface VehicleStatusWebhookConnectionType {