doover-js 0.4.4 → 0.4.5

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.
@@ -43,6 +43,266 @@ const helpers_1 = require("./helpers");
43
43
  (0, chai_1.expect)((fetchMock.getCall(0).args[1]?.headers).get("X-Doover-Sharing")).to.equal("internal");
44
44
  (0, chai_1.expect)((fetchMock.getCall(1).args[1]?.headers).get("X-Doover-Sharing")).to.equal(null);
45
45
  });
46
+ (0, mocha_1.it)("getAgents forwards include-archived/include-organisations/include-users query keys", async () => {
47
+ const fetchMock = (0, helpers_1.createFetchMock)(() => (0, helpers_1.createJsonResponse)({ agents: [], organisations: [], users: [] }));
48
+ const provider = new doover_data_provider_1.DooverDataProvider({
49
+ dataRestUrl: "https://api.example.com",
50
+ controlApiUrl: "https://control.example.com",
51
+ dataWssUrl: "wss://ws.example.com",
52
+ fetchImpl: fetchMock,
53
+ webSocketImpl: helpers_1.MockWebSocket,
54
+ });
55
+ await provider.getAgents({
56
+ includeArchived: true,
57
+ includeOrganisations: true,
58
+ includeUsers: true,
59
+ });
60
+ const url = fetchMock.getCall(0).args[0];
61
+ (0, chai_1.expect)(url.startsWith("https://control.example.com/agents?")).to.equal(true);
62
+ (0, chai_1.expect)(url).to.include("include-archived=true");
63
+ (0, chai_1.expect)(url).to.include("include-organisations=true");
64
+ (0, chai_1.expect)(url).to.include("include-users=true");
65
+ });
66
+ (0, mocha_1.it)("getAgents preserves raw organisations/users without merging by default", async () => {
67
+ const fetchMock = (0, helpers_1.createFetchMock)(() => (0, helpers_1.createJsonResponse)({
68
+ agents: [
69
+ {
70
+ id: "a1",
71
+ name: "agent-1",
72
+ display_name: "Agent 1",
73
+ type: "device",
74
+ organisation: "Org 1",
75
+ group: "g",
76
+ archived: false,
77
+ fa_icon: "fa-solid fa-robot",
78
+ fixed_location: { latitude: 1, longitude: 2 },
79
+ extra_config: {},
80
+ },
81
+ ],
82
+ organisations: [{ id: "o1", name: "Org 1" }],
83
+ users: [{ id: "u1", username: "alice" }],
84
+ }));
85
+ const provider = new doover_data_provider_1.DooverDataProvider({
86
+ dataRestUrl: "https://api.example.com",
87
+ controlApiUrl: "https://control.example.com",
88
+ dataWssUrl: "wss://ws.example.com",
89
+ fetchImpl: fetchMock,
90
+ webSocketImpl: helpers_1.MockWebSocket,
91
+ });
92
+ const result = await provider.getAgents({
93
+ includeOrganisations: true,
94
+ includeUsers: true,
95
+ });
96
+ (0, chai_1.expect)(result.agents).to.have.lengthOf(1);
97
+ (0, chai_1.expect)(result.agents?.[0].id).to.equal("a1");
98
+ (0, chai_1.expect)(result.organisations).to.deep.equal([{ id: "o1", name: "Org 1" }]);
99
+ (0, chai_1.expect)(result.users).to.deep.equal([{ id: "u1", username: "alice" }]);
100
+ (0, chai_1.expect)(result.results).to.equal(undefined);
101
+ (0, chai_1.expect)(result.count).to.equal(undefined);
102
+ });
103
+ (0, mocha_1.it)("getAgents merges normalized organisations and users when mergeIncludedAsAgents is true", async () => {
104
+ const fetchMock = (0, helpers_1.createFetchMock)(() => (0, helpers_1.createJsonResponse)({
105
+ agents: [
106
+ {
107
+ id: "a1",
108
+ name: "agent-1",
109
+ display_name: "Agent 1",
110
+ type: "device",
111
+ organisation: "Org 1",
112
+ group: "g",
113
+ archived: false,
114
+ fa_icon: "fa-solid fa-robot",
115
+ fixed_location: { latitude: 1, longitude: 2 },
116
+ extra_config: { foo: "bar" },
117
+ },
118
+ ],
119
+ organisations: [
120
+ {
121
+ id: "o1",
122
+ name: "Org One",
123
+ archived: false,
124
+ root_group: { name: "root" },
125
+ extra_config: { level: "premium" },
126
+ },
127
+ ],
128
+ users: [
129
+ {
130
+ id: "u1",
131
+ username: "alice",
132
+ email: "alice@example.com",
133
+ first_name: "Alice",
134
+ last_name: "Anderson",
135
+ custom_data: { theme: "dark" },
136
+ },
137
+ ],
138
+ }));
139
+ const provider = new doover_data_provider_1.DooverDataProvider({
140
+ dataRestUrl: "https://api.example.com",
141
+ controlApiUrl: "https://control.example.com",
142
+ dataWssUrl: "wss://ws.example.com",
143
+ fetchImpl: fetchMock,
144
+ webSocketImpl: helpers_1.MockWebSocket,
145
+ });
146
+ const result = await provider.getAgents({
147
+ includeOrganisations: true,
148
+ includeUsers: true,
149
+ mergeIncludedAsAgents: true,
150
+ });
151
+ (0, chai_1.expect)(result.agents).to.have.lengthOf(3);
152
+ (0, chai_1.expect)(result.results).to.equal(result.agents);
153
+ (0, chai_1.expect)(result.count).to.equal(3);
154
+ const [agent, organisation, user] = result.agents;
155
+ (0, chai_1.expect)(agent.id).to.equal("a1");
156
+ (0, chai_1.expect)(agent.type).to.equal("device");
157
+ (0, chai_1.expect)(organisation).to.deep.equal({
158
+ id: "o1",
159
+ organisation: "Org One",
160
+ name: "Org One",
161
+ display_name: "Org One",
162
+ archived: false,
163
+ group: "root",
164
+ fa_icon: "fa-solid fa-building",
165
+ type: "organisation",
166
+ fixed_location: { latitude: 0, longitude: 0 },
167
+ extra_config: { level: "premium" },
168
+ });
169
+ (0, chai_1.expect)(user).to.deep.equal({
170
+ id: "u1",
171
+ organisation: "",
172
+ name: "alice",
173
+ display_name: "Alice Anderson",
174
+ archived: false,
175
+ group: "",
176
+ fa_icon: "fa-solid fa-user",
177
+ type: "user",
178
+ fixed_location: { latitude: 0, longitude: 0 },
179
+ extra_config: { theme: "dark" },
180
+ });
181
+ });
182
+ (0, mocha_1.it)("getAgents merge respects includeOrganisations/includeUsers flags", async () => {
183
+ const fetchMock = (0, helpers_1.createFetchMock)(() => (0, helpers_1.createJsonResponse)({
184
+ agents: [
185
+ {
186
+ id: "a1",
187
+ name: "a1",
188
+ display_name: "a1",
189
+ type: "device",
190
+ organisation: "o",
191
+ group: "g",
192
+ archived: false,
193
+ fa_icon: "fa-solid fa-robot",
194
+ fixed_location: { latitude: 0, longitude: 0 },
195
+ extra_config: {},
196
+ },
197
+ ],
198
+ organisations: [{ id: "o1", name: "Org 1" }],
199
+ users: [{ id: "u1", username: "alice" }],
200
+ }));
201
+ const provider = new doover_data_provider_1.DooverDataProvider({
202
+ dataRestUrl: "https://api.example.com",
203
+ controlApiUrl: "https://control.example.com",
204
+ dataWssUrl: "wss://ws.example.com",
205
+ fetchImpl: fetchMock,
206
+ webSocketImpl: helpers_1.MockWebSocket,
207
+ });
208
+ const result = await provider.getAgents({
209
+ includeOrganisations: false,
210
+ includeUsers: true,
211
+ mergeIncludedAsAgents: true,
212
+ });
213
+ (0, chai_1.expect)(result.agents).to.have.lengthOf(2);
214
+ (0, chai_1.expect)(result.agents?.map((entry) => entry.type)).to.deep.equal([
215
+ "device",
216
+ "user",
217
+ ]);
218
+ (0, chai_1.expect)(result.count).to.equal(2);
219
+ });
220
+ (0, mocha_1.it)("getAgents user display_name falls back through full name, username, email, id", async () => {
221
+ let payload = {};
222
+ const fetchMock = (0, helpers_1.createFetchMock)(() => (0, helpers_1.createJsonResponse)(payload));
223
+ const provider = new doover_data_provider_1.DooverDataProvider({
224
+ dataRestUrl: "https://api.example.com",
225
+ controlApiUrl: "https://control.example.com",
226
+ dataWssUrl: "wss://ws.example.com",
227
+ fetchImpl: fetchMock,
228
+ webSocketImpl: helpers_1.MockWebSocket,
229
+ });
230
+ payload = {
231
+ agents: [],
232
+ users: [
233
+ {
234
+ id: "u-full",
235
+ username: "alice",
236
+ email: "alice@example.com",
237
+ first_name: "Alice",
238
+ last_name: "Anderson",
239
+ },
240
+ { id: "u-username", username: "bob", email: "bob@example.com" },
241
+ { id: "u-email", email: "carol@example.com" },
242
+ { id: "u-id-only" },
243
+ ],
244
+ };
245
+ const result = await provider.getAgents({
246
+ includeUsers: true,
247
+ mergeIncludedAsAgents: true,
248
+ });
249
+ const merged = result.agents ?? [];
250
+ (0, chai_1.expect)(merged.map((entry) => entry.display_name)).to.deep.equal([
251
+ "Alice Anderson",
252
+ "bob",
253
+ "carol@example.com",
254
+ "u-id-only",
255
+ ]);
256
+ (0, chai_1.expect)(merged.map((entry) => entry.name)).to.deep.equal([
257
+ "alice",
258
+ "bob",
259
+ "carol@example.com",
260
+ "u-id-only",
261
+ ]);
262
+ });
263
+ (0, mocha_1.it)("getAgents fills agent defaults when fa_icon, fixed_location, or extra_config are missing/null", async () => {
264
+ const fetchMock = (0, helpers_1.createFetchMock)(() => (0, helpers_1.createJsonResponse)({
265
+ agents: [
266
+ {
267
+ id: "a1",
268
+ name: "a1",
269
+ display_name: "a1",
270
+ type: "device",
271
+ organisation: "o",
272
+ group: "g",
273
+ archived: false,
274
+ fa_icon: null,
275
+ fixed_location: null,
276
+ extra_config: null,
277
+ },
278
+ {
279
+ id: "a2",
280
+ name: "a2",
281
+ display_name: "a2",
282
+ type: "device",
283
+ organisation: "o",
284
+ group: "g",
285
+ archived: false,
286
+ // fa_icon, fixed_location, extra_config all missing
287
+ },
288
+ ],
289
+ }));
290
+ const provider = new doover_data_provider_1.DooverDataProvider({
291
+ dataRestUrl: "https://api.example.com",
292
+ controlApiUrl: "https://control.example.com",
293
+ dataWssUrl: "wss://ws.example.com",
294
+ fetchImpl: fetchMock,
295
+ webSocketImpl: helpers_1.MockWebSocket,
296
+ });
297
+ const result = await provider.getAgents();
298
+ const [withNulls, withMissing] = result.agents ?? [];
299
+ (0, chai_1.expect)(withNulls.fa_icon).to.equal("fa-solid fa-robot");
300
+ (0, chai_1.expect)(withNulls.fixed_location).to.deep.equal({ latitude: 0, longitude: 0 });
301
+ (0, chai_1.expect)(withNulls.extra_config).to.deep.equal({});
302
+ (0, chai_1.expect)(withMissing.fa_icon).to.equal("fa-solid fa-robot");
303
+ (0, chai_1.expect)(withMissing.fixed_location).to.deep.equal({ latitude: 0, longitude: 0 });
304
+ (0, chai_1.expect)(withMissing.extra_config).to.deep.equal({});
305
+ });
46
306
  (0, mocha_1.it)("returns undefined for missing identifiers where expected", async () => {
47
307
  const fetchMock = (0, helpers_1.createFetchMock)();
48
308
  const provider = new doover_data_provider_1.DooverDataProvider({
@@ -12,7 +12,7 @@ export interface Agent {
12
12
  archived: boolean;
13
13
  group: string;
14
14
  fa_icon: string;
15
- type: "device" | "dashboard";
15
+ type: "device" | "dashboard" | "organisation" | "user";
16
16
  fixed_location: {
17
17
  latitude: number;
18
18
  longitude: number;
@@ -26,6 +26,18 @@ export interface User {
26
26
  name?: string;
27
27
  [key: string]: unknown;
28
28
  }
29
+ export interface GetAgentsOptions {
30
+ includeArchived?: boolean;
31
+ includeOrganisations?: boolean;
32
+ includeUsers?: boolean;
33
+ /**
34
+ * When `true`, normalized organisations and users are appended into the
35
+ * canonical `agents` list (and mirrored to `results` / `count`). Defaults
36
+ * to `false`, which preserves the legacy behaviour of returning only the
37
+ * raw `agents` array.
38
+ */
39
+ mergeIncludedAsAgents?: boolean;
40
+ }
29
41
  export interface AgentsResponse {
30
42
  agents?: Agent[];
31
43
  results?: Agent[];
@@ -42,7 +54,7 @@ export interface SubscriptionDetails {
42
54
  }
43
55
  export interface ChannelsDataProvider<TIdentifier extends ChannelIdentifier = ChannelIdentifier> {
44
56
  getMe: () => Promise<User>;
45
- getAgents: () => Promise<AgentsResponse>;
57
+ getAgents: (options?: GetAgentsOptions) => Promise<AgentsResponse>;
46
58
  getChannels: (identifier: TIdentifier, options?: {
47
59
  includeArchived?: boolean;
48
60
  }) => Promise<Channel[] | undefined>;
@@ -7,7 +7,7 @@ import type { DooverAuth } from "../auth/doover-auth";
7
7
  import { GatewayClient } from "../gateway/gateway-client";
8
8
  import { RestClient, type DooverClientConfig } from "../http/rest-client";
9
9
  import type { Aggregate, Channel, ConnectionDetails, JSONValue, MessageStructure, RpcRequest, RpcStatus } from "../types/common";
10
- import type { Agent, AgentsResponse, ChannelIdentifier, ChannelsDataProvider, SubscriptionDetails, User } from "../types/viewer";
10
+ import type { Agent, AgentsResponse, ChannelIdentifier, ChannelsDataProvider, GetAgentsOptions, SubscriptionDetails, User } from "../types/viewer";
11
11
  interface SubscriptionEntry {
12
12
  messageCallback: (identifier: ChannelIdentifier, message: MessageStructure) => void;
13
13
  aggregateCallback: (identifier: ChannelIdentifier, aggregate: Aggregate) => void;
@@ -30,7 +30,7 @@ export declare class DooverDataProvider implements ChannelsDataProvider<ChannelI
30
30
  private readonly subscriptions;
31
31
  constructor(config: DooverClientConfig, auth?: DooverAuth);
32
32
  getMe(): Promise<User>;
33
- getAgents(): Promise<AgentsResponse>;
33
+ getAgents(options?: GetAgentsOptions): Promise<AgentsResponse>;
34
34
  getChannels(identifier: ChannelIdentifier, options?: {
35
35
  includeArchived?: boolean;
36
36
  }): Promise<Channel[] | undefined>;
@@ -82,6 +82,11 @@ export declare class DooverDataProvider implements ChannelsDataProvider<ChannelI
82
82
  aggregatePath?: string;
83
83
  };
84
84
  getAgentInfo(agentId: string): Promise<Agent | undefined>;
85
+ private normalizeAgentEntry;
86
+ private normalizeOrganisationEntry;
87
+ private normalizeUserEntry;
88
+ private buildUserDisplayName;
89
+ private coerceExtraConfig;
85
90
  private requireChannel;
86
91
  private channelKey;
87
92
  private toIdentifier;
@@ -18,6 +18,11 @@ function isRpcMessageData(data) {
18
18
  "method" in data &&
19
19
  "request" in data);
20
20
  }
21
+ function isPlainObject(value) {
22
+ return (typeof value === "object" &&
23
+ value !== null &&
24
+ !Array.isArray(value));
25
+ }
21
26
  class DooverDataProvider {
22
27
  constructor(config, auth) {
23
28
  this.config = config;
@@ -53,12 +58,52 @@ class DooverDataProvider {
53
58
  getMe() {
54
59
  return this.rest.get("/users/me", undefined, this.config.controlApiUrl);
55
60
  }
56
- getAgents() {
57
- return this.rest.request({
61
+ async getAgents(options) {
62
+ const query = {};
63
+ if (options?.includeArchived) {
64
+ query["include-archived"] = true;
65
+ }
66
+ if (options?.includeOrganisations) {
67
+ query["include-organisations"] = true;
68
+ }
69
+ if (options?.includeUsers) {
70
+ query["include-users"] = true;
71
+ }
72
+ const raw = await this.rest.request({
58
73
  path: "/agents",
59
74
  baseUrl: this.config.controlApiUrl,
60
75
  omitSharingHeader: true,
76
+ query: Object.keys(query).length > 0 ? query : undefined,
61
77
  });
78
+ const rawAgents = Array.isArray(raw.agents) ? raw.agents : [];
79
+ const normalizedAgents = rawAgents.map((agent) => this.normalizeAgentEntry(agent));
80
+ if (!options?.mergeIncludedAsAgents) {
81
+ return {
82
+ ...raw,
83
+ agents: normalizedAgents,
84
+ };
85
+ }
86
+ const merged = [...normalizedAgents];
87
+ if (options.includeOrganisations) {
88
+ const rawOrganisations = Array.isArray(raw.organisations)
89
+ ? raw.organisations
90
+ : [];
91
+ for (const org of rawOrganisations) {
92
+ merged.push(this.normalizeOrganisationEntry(org));
93
+ }
94
+ }
95
+ if (options.includeUsers) {
96
+ const rawUsers = Array.isArray(raw.users) ? raw.users : [];
97
+ for (const user of rawUsers) {
98
+ merged.push(this.normalizeUserEntry(user));
99
+ }
100
+ }
101
+ return {
102
+ ...raw,
103
+ agents: merged,
104
+ results: merged,
105
+ count: merged.length,
106
+ };
62
107
  }
63
108
  async getChannels(identifier, options) {
64
109
  if (!identifier.agentId) {
@@ -263,6 +308,106 @@ class DooverDataProvider {
263
308
  extra_config: {},
264
309
  });
265
310
  }
311
+ normalizeAgentEntry(rawAgent) {
312
+ const source = isPlainObject(rawAgent)
313
+ ? rawAgent
314
+ : {};
315
+ const fixedLocation = source.fixed_location;
316
+ const normalizedFixedLocation = isPlainObject(fixedLocation) &&
317
+ typeof fixedLocation.latitude === "number" &&
318
+ typeof fixedLocation.longitude === "number"
319
+ ? {
320
+ latitude: fixedLocation.latitude,
321
+ longitude: fixedLocation.longitude,
322
+ }
323
+ : { latitude: 0, longitude: 0 };
324
+ const faIcon = typeof source.fa_icon === "string" && source.fa_icon.length > 0
325
+ ? source.fa_icon
326
+ : "fa-solid fa-robot";
327
+ return {
328
+ ...source,
329
+ id: typeof source.id === "string" ? source.id : String(source.id ?? ""),
330
+ organisation: typeof source.organisation === "string" ? source.organisation : "",
331
+ name: typeof source.name === "string" ? source.name : "",
332
+ display_name: typeof source.display_name === "string" ? source.display_name : "",
333
+ archived: typeof source.archived === "boolean" ? source.archived : false,
334
+ group: typeof source.group === "string" ? source.group : "",
335
+ fa_icon: faIcon,
336
+ type: source.type === "device" ||
337
+ source.type === "dashboard" ||
338
+ source.type === "organisation" ||
339
+ source.type === "user"
340
+ ? source.type
341
+ : "device",
342
+ fixed_location: normalizedFixedLocation,
343
+ extra_config: this.coerceExtraConfig(source.extra_config),
344
+ };
345
+ }
346
+ normalizeOrganisationEntry(rawOrganisation) {
347
+ const source = isPlainObject(rawOrganisation)
348
+ ? rawOrganisation
349
+ : {};
350
+ const name = typeof source.name === "string" ? source.name : "";
351
+ const rootGroup = isPlainObject(source.root_group) ? source.root_group : null;
352
+ const groupName = rootGroup && typeof rootGroup.name === "string" ? rootGroup.name : "";
353
+ return {
354
+ id: typeof source.id === "string" ? source.id : String(source.id ?? ""),
355
+ organisation: name,
356
+ name,
357
+ display_name: name,
358
+ archived: typeof source.archived === "boolean" ? source.archived : false,
359
+ group: groupName,
360
+ fa_icon: "fa-solid fa-building",
361
+ type: "organisation",
362
+ fixed_location: { latitude: 0, longitude: 0 },
363
+ extra_config: this.coerceExtraConfig(source.extra_config),
364
+ };
365
+ }
366
+ normalizeUserEntry(rawUser) {
367
+ const source = isPlainObject(rawUser)
368
+ ? rawUser
369
+ : {};
370
+ const id = typeof source.id === "string" ? source.id : String(source.id ?? "");
371
+ const username = typeof source.username === "string" ? source.username : "";
372
+ const email = typeof source.email === "string" ? source.email : "";
373
+ const name = username !== "" ? username : email !== "" ? email : id;
374
+ return {
375
+ id,
376
+ organisation: "",
377
+ name,
378
+ display_name: this.buildUserDisplayName(source),
379
+ archived: false,
380
+ group: "",
381
+ fa_icon: "fa-solid fa-user",
382
+ type: "user",
383
+ fixed_location: { latitude: 0, longitude: 0 },
384
+ extra_config: this.coerceExtraConfig(source.custom_data),
385
+ };
386
+ }
387
+ buildUserDisplayName(rawUser) {
388
+ const source = isPlainObject(rawUser)
389
+ ? rawUser
390
+ : {};
391
+ const firstName = typeof source.first_name === "string" ? source.first_name.trim() : "";
392
+ const lastName = typeof source.last_name === "string" ? source.last_name.trim() : "";
393
+ const fullName = `${firstName} ${lastName}`.trim();
394
+ if (fullName !== "") {
395
+ return fullName;
396
+ }
397
+ if (typeof source.username === "string" && source.username !== "") {
398
+ return source.username;
399
+ }
400
+ if (typeof source.email === "string" && source.email !== "") {
401
+ return source.email;
402
+ }
403
+ if (typeof source.id === "string" && source.id !== "") {
404
+ return source.id;
405
+ }
406
+ return String(source.id ?? "");
407
+ }
408
+ coerceExtraConfig(value) {
409
+ return isPlainObject(value) ? value : {};
410
+ }
266
411
  requireChannel(identifier) {
267
412
  if (!identifier.agentId || !identifier.channelName) {
268
413
  throw new errors_1.DooverValidationError("Identifier must contain agentId and channelName");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doover-js",
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
4
4
  "description": "TypeScript client for Doover.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",