doover-js 0.4.3 → 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.
@@ -1,16 +1,15 @@
1
1
  import { type UseQueryResult } from "@tanstack/react-query";
2
- import type { Aggregate } from "../types/common";
3
- import type { AgentAggregate } from "../types/openapi";
2
+ import type { Aggregate, JSONValue, AgentAggregate } from "../types/common";
4
3
  export declare function multiAgentAggregatesQueryKey(channelName: string, agentIds: string[]): readonly ["doover", "channel", string, "aggregates", string];
5
4
  export interface UseMultiAgentAggregatesOptions {
6
5
  /** If false, skip per-agent live subscriptions. Defaults true. */
7
6
  liveUpdates?: boolean;
8
7
  }
9
- export interface UseMultiAgentAggregatesResult {
8
+ export interface UseMultiAgentAggregatesResult<TData> {
10
9
  /** Keyed by `agent_id` for O(1) lookup. */
11
- aggregatesByAgent: Record<string, Aggregate>;
10
+ aggregatesByAgent: Record<string, Aggregate<TData>>;
12
11
  query: UseQueryResult<{
13
- results: AgentAggregate[];
12
+ results: AgentAggregate<TData>[];
14
13
  count: number;
15
14
  }>;
16
15
  }
@@ -19,4 +18,4 @@ export interface UseMultiAgentAggregatesResult {
19
18
  * agent's slot live via its own gateway subscription. Reconciles per-agent
20
19
  * `channelSync`/`aggregateUpdate` events into the batched query cache.
21
20
  */
22
- export declare function useMultiAgentAggregates(channelName: string, agentIds: string[], options?: UseMultiAgentAggregatesOptions): UseMultiAgentAggregatesResult;
21
+ export declare function useMultiAgentAggregates<TData = Record<string, JSONValue>>(channelName: string, agentIds: string[], options?: UseMultiAgentAggregatesOptions): UseMultiAgentAggregatesResult<TData>;
@@ -13,13 +13,13 @@ export interface UseMultiAgentChannelMessagesOptions {
13
13
  /** Optional first-page `before` cursor (snowflake id). */
14
14
  initialBefore?: string;
15
15
  }
16
- interface Page {
17
- results: MessageStructure[];
16
+ interface Page<TData> {
17
+ results: MessageStructure<TData>[];
18
18
  next?: string;
19
19
  }
20
- export interface UseMultiAgentChannelMessagesResult extends Omit<UseInfiniteQueryResult<InfiniteData<Page>>, "data"> {
21
- messages: MessageStructure[];
22
- data: InfiniteData<Page> | undefined;
20
+ export interface UseMultiAgentChannelMessagesResult<TData> extends Omit<UseInfiniteQueryResult<InfiniteData<Page<TData>>>, "data"> {
21
+ messages: MessageStructure<TData>[];
22
+ data: InfiniteData<Page<TData>> | undefined;
23
23
  }
24
- export declare function useMultiAgentChannelMessages(channelName: string, agentIds: string[], options?: UseMultiAgentChannelMessagesOptions): UseMultiAgentChannelMessagesResult;
24
+ export declare function useMultiAgentChannelMessages<TData = unknown>(channelName: string, agentIds: string[], options?: UseMultiAgentChannelMessagesOptions): UseMultiAgentChannelMessagesResult<TData>;
25
25
  export {};
@@ -26,12 +26,13 @@ function useMultiAgentChannelMessages(channelName, agentIds, options) {
26
26
  queryClient.setQueryData(key, (current) => {
27
27
  if (!current)
28
28
  return current;
29
+ const typed = message;
29
30
  const [firstPage, ...rest] = current.pages;
30
31
  const firstResults = firstPage?.results ?? [];
31
32
  return {
32
33
  ...current,
33
34
  pages: [
34
- { ...firstPage, results: [message, ...firstResults] },
35
+ { ...firstPage, results: [typed, ...firstResults] },
35
36
  ...rest,
36
37
  ],
37
38
  };
@@ -6,4 +6,4 @@ import type { ChannelIdentifier } from "../types/viewer";
6
6
  * to the channel and resolves with the created `MessageStructure` (including
7
7
  * its server-assigned `id` and `timestamp`).
8
8
  */
9
- export declare function useSendMessage(identifier: ChannelIdentifier): UseMutationResult<MessageStructure, Error, object>;
9
+ export declare function useSendMessage<TData extends object = object>(identifier: ChannelIdentifier): UseMutationResult<MessageStructure<TData>, Error, TData>;
@@ -18,4 +18,4 @@ export interface UseUpdateAggregateOptions {
18
18
  * aggregate into the react-query cache for its channel so consumers of
19
19
  * `useChannelAggregate` see the change immediately.
20
20
  */
21
- export declare function useUpdateAggregate(identifier: ChannelIdentifier, options?: UseUpdateAggregateOptions): UseMutationResult<Aggregate, Error, object>;
21
+ export declare function useUpdateAggregate<TData extends object = object>(identifier: ChannelIdentifier, options?: UseUpdateAggregateOptions): UseMutationResult<Aggregate<TData>, Error, TData>;
@@ -16,9 +16,9 @@ function useUpdateAggregate(identifier, options) {
16
16
  const replace = options?.replace ?? false;
17
17
  const params = options?.params;
18
18
  return (0, react_query_1.useMutation)({
19
- mutationFn: (data) => replace
19
+ mutationFn: (data) => (replace
20
20
  ? client.viewer.putAggregate(identifier, data, params)
21
- : client.viewer.updateAggregate(identifier, data, params),
21
+ : client.viewer.updateAggregate(identifier, data, params)),
22
22
  onSuccess: (aggregate) => {
23
23
  queryClient.setQueryData((0, useChannelAggregate_1.channelAggregateQueryKey)(identifier.agentId, identifier.channelName), aggregate);
24
24
  },
@@ -15,9 +15,9 @@ export interface UpdateMessageCallOptions {
15
15
  }
16
16
  export interface UseUpdateMessageOptions extends UpdateMessageCallOptions {
17
17
  }
18
- export interface UpdateMessageVariables {
18
+ export interface UpdateMessageVariables<TBody extends MessageBody = MessageBody> {
19
19
  messageId: string;
20
- body: MessageBody;
20
+ body: TBody;
21
21
  /**
22
22
  * Per-call overrides. Extensible — add new knobs here without breaking
23
23
  * the variables shape.
@@ -30,4 +30,4 @@ export interface UpdateMessageVariables {
30
30
  * invocation can override `replace` / `params` via `variables.options` for
31
31
  * mutation sites that sometimes PUT and sometimes PATCH the same channel.
32
32
  */
33
- export declare function useUpdateMessage(identifier: ChannelIdentifier, options?: UseUpdateMessageOptions): UseMutationResult<unknown, Error, UpdateMessageVariables>;
33
+ export declare function useUpdateMessage<TBody extends MessageBody = MessageBody>(identifier: ChannelIdentifier, options?: UseUpdateMessageOptions): UseMutationResult<unknown, Error, UpdateMessageVariables<TBody>>;
@@ -14,7 +14,7 @@ function useUpdateMessage(identifier, options) {
14
14
  const defaultReplace = options?.replace ?? false;
15
15
  const defaultParams = options?.params;
16
16
  return (0, react_query_1.useMutation)({
17
- mutationFn: ({ messageId, body, options: call }) => {
17
+ mutationFn: ({ messageId, body, options: call, }) => {
18
18
  if (!identifier.agentId || !identifier.channelName) {
19
19
  throw new Error("useUpdateMessage requires both agentId and channelName on the identifier.");
20
20
  }
@@ -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.3",
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",