doover-js 0.6.4 → 0.6.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.
@@ -0,0 +1,26 @@
1
+ import type { RestClient } from "../http/rest-client";
2
+ import type { CustomerSite, Organisation, PaginatedResponse } from "../types/viewer";
3
+ export interface ListOrganisationsOptions {
4
+ page?: number;
5
+ perPage?: number;
6
+ search?: string;
7
+ archived?: boolean | "";
8
+ ordering?: string;
9
+ name?: string;
10
+ name__contains?: string;
11
+ name__icontains?: string;
12
+ id?: string | number;
13
+ [key: string]: string | number | boolean | "" | undefined;
14
+ }
15
+ export declare class OrganisationsApi {
16
+ private readonly rest;
17
+ private readonly controlApiUrl?;
18
+ constructor(rest: RestClient, controlApiUrl?: string | undefined);
19
+ listOrganisations(options?: ListOrganisationsOptions): Promise<PaginatedResponse<Organisation>>;
20
+ getOrganisation(id: string): Promise<Organisation>;
21
+ /**
22
+ * Convert an organisation record from `/organisations/` into the smaller
23
+ * customer-site bootstrap shape returned by `/site/`.
24
+ */
25
+ toCustomerSite(organisation: Organisation): CustomerSite;
26
+ }
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OrganisationsApi = void 0;
4
+ class OrganisationsApi {
5
+ constructor(rest, controlApiUrl) {
6
+ this.rest = rest;
7
+ this.controlApiUrl = controlApiUrl;
8
+ }
9
+ listOrganisations(options) {
10
+ return this.rest.get("/organisations/", buildListQuery(options), this.controlApiUrl);
11
+ }
12
+ getOrganisation(id) {
13
+ return this.rest.get(`/organisations/${id}/`, undefined, this.controlApiUrl);
14
+ }
15
+ /**
16
+ * Convert an organisation record from `/organisations/` into the smaller
17
+ * customer-site bootstrap shape returned by `/site/`.
18
+ */
19
+ toCustomerSite(organisation) {
20
+ return {
21
+ id: organisation.id,
22
+ name: organisation.name,
23
+ application_id: organisation.application_id,
24
+ archived: organisation.archived,
25
+ theme: organisation.theme ?? {},
26
+ };
27
+ }
28
+ }
29
+ exports.OrganisationsApi = OrganisationsApi;
30
+ function buildListQuery(options) {
31
+ if (!options)
32
+ return undefined;
33
+ const { page, perPage, ...rest } = options;
34
+ const query = {};
35
+ if (page !== undefined)
36
+ query.page = page;
37
+ if (perPage !== undefined)
38
+ query.per_page = perPage;
39
+ for (const [key, value] of Object.entries(rest)) {
40
+ if (value === undefined)
41
+ continue;
42
+ query[key] = value;
43
+ }
44
+ return query;
45
+ }
@@ -13,5 +13,6 @@ export declare class CookieAuth extends DooverAuth {
13
13
  headers?: Record<string, string>;
14
14
  }>;
15
15
  setToken(_token: string | null, _tokenExpires?: Date | number | null): void;
16
+ setRefreshToken(_refreshToken: string | null): void;
16
17
  ensureReady(): Promise<void>;
17
18
  }
@@ -22,6 +22,9 @@ class CookieAuth extends doover_auth_1.DooverAuth {
22
22
  setToken(_token, _tokenExpires) {
23
23
  // No-op for cookie auth.
24
24
  }
25
+ setRefreshToken(_refreshToken) {
26
+ // No-op for cookie auth.
27
+ }
25
28
  async ensureReady() {
26
29
  // Nothing to prepare for cookie auth.
27
30
  }
@@ -31,6 +31,11 @@ export declare abstract class DooverAuth {
31
31
  * Subclasses that do not use tokens may no-op.
32
32
  */
33
33
  abstract setToken(token: string | null, tokenExpires?: Date | number | null): void;
34
+ /**
35
+ * Imperatively set or clear the refresh token. Subclasses that do not use
36
+ * refresh tokens may no-op.
37
+ */
38
+ abstract setRefreshToken(refreshToken: string | null): void;
34
39
  /**
35
40
  * Ensure the auth layer is ready (e.g. token refreshed if needed).
36
41
  * Called before every HTTP request and before opening a websocket.
@@ -26,6 +26,7 @@ export declare class DooverTokenAuth extends DooverAuth {
26
26
  headers?: Record<string, string>;
27
27
  }>;
28
28
  setToken(token: string | null, tokenExpires?: Date | number | null): void;
29
+ setRefreshToken(refreshToken: string | null): void;
29
30
  ensureReady(): Promise<void>;
30
31
  attachProfile(profile: AuthProfile, configManager?: AuthProfileStore): void;
31
32
  private needsRefresh;
@@ -48,6 +48,11 @@ class DooverTokenAuth extends doover_auth_1.DooverAuth {
48
48
  setToken(token, tokenExpires) {
49
49
  this.token = token;
50
50
  this.tokenExpires = this.resolveExpiry(tokenExpires, token);
51
+ this.persistToProfile();
52
+ }
53
+ setRefreshToken(refreshToken) {
54
+ this.refreshToken = refreshToken;
55
+ this.persistToProfile();
51
56
  }
52
57
  async ensureReady() {
53
58
  if (!this.needsRefresh()) {
@@ -1,3 +1,4 @@
1
+ import { OrganisationsApi } from "../apis/organisations-api";
1
2
  import type { DooverAuth } from "../auth/doover-auth";
2
3
  import { RestClient, type DooverClientConfig } from "../http/rest-client";
3
4
  import { DooverDataProvider } from "../viewer/doover-data-provider";
@@ -15,6 +16,7 @@ export declare class DooverClient implements DataClient {
15
16
  readonly alarms: AlarmsApiLike;
16
17
  readonly connections: ConnectionsApiLike;
17
18
  readonly notifications: NotificationsApiLike;
19
+ readonly organisations: OrganisationsApi;
18
20
  readonly permissions: PermissionsApiLike;
19
21
  readonly processors: ProcessorsApiLike;
20
22
  readonly turn: TurnApiLike;
@@ -8,6 +8,7 @@ const channels_api_1 = require("../apis/channels-api");
8
8
  const connections_api_1 = require("../apis/connections-api");
9
9
  const messages_api_1 = require("../apis/messages-api");
10
10
  const notifications_api_1 = require("../apis/notifications-api");
11
+ const organisations_api_1 = require("../apis/organisations-api");
11
12
  const permissions_api_1 = require("../apis/permissions-api");
12
13
  const processors_api_1 = require("../apis/processors-api");
13
14
  const turn_api_1 = require("../apis/turn-api");
@@ -63,6 +64,7 @@ class DooverClient {
63
64
  this.alarms = (0, provenance_1.wrapSubclient)(new alarms_api_1.AlarmsApi(this.rest), "alarms", stamper);
64
65
  this.connections = (0, provenance_1.wrapSubclient)(new connections_api_1.ConnectionsApi(this.rest), "connections", stamper);
65
66
  this.notifications = (0, provenance_1.wrapSubclient)(new notifications_api_1.NotificationsApi(this.rest), "notifications", stamper);
67
+ this.organisations = (0, provenance_1.wrapSubclient)(new organisations_api_1.OrganisationsApi(this.rest, config.controlApiUrl), "organisations", stamper);
66
68
  this.permissions = (0, provenance_1.wrapSubclient)(new permissions_api_1.PermissionsApi(this.rest), "permissions", stamper);
67
69
  this.processors = (0, provenance_1.wrapSubclient)(new processors_api_1.ProcessorsApi(this.rest), "processors", stamper);
68
70
  this.turn = (0, provenance_1.wrapSubclient)(new turn_api_1.TurnApi(this.rest), "turn", stamper);
@@ -47,7 +47,7 @@ class RestClient {
47
47
  if (!options.omitSharingHeader) {
48
48
  headers.set("X-Doover-Sharing", this.config.sharing);
49
49
  }
50
- if (this.config.organisationId) {
50
+ if (this.config.organisationId && !headers.has("X-Doover-Organisation")) {
51
51
  headers.set("X-Doover-Organisation", this.config.organisationId);
52
52
  }
53
53
  const impersonated = this.getImpersonatedUserId();
package/dist/index.d.ts CHANGED
@@ -13,6 +13,8 @@ export { ChannelsApi } from "./apis/channels-api";
13
13
  export { ConnectionsApi } from "./apis/connections-api";
14
14
  export { MessagesApi } from "./apis/messages-api";
15
15
  export { NotificationsApi } from "./apis/notifications-api";
16
+ export { OrganisationsApi } from "./apis/organisations-api";
17
+ export type { ListOrganisationsOptions } from "./apis/organisations-api";
16
18
  export { PermissionsApi } from "./apis/permissions-api";
17
19
  export { ProcessorsApi } from "./apis/processors-api";
18
20
  export { TurnApi } from "./apis/turn-api";
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.generateSnowflakeIdAtTime = exports.extractSnowflakeId = exports.addTimestampToMessage = exports.DV_AUDIT_CHANNEL = exports.getIdentifierFromPath = exports.DooverDataProvider = exports.DooverValidationError = exports.DooverGatewayError = exports.DooverApiError = exports.RestClient = exports.GatewayClient = exports.buildAuth = exports.DooverAuthError = exports.AuthProfile = exports.DooverTokenAuth = exports.CookieAuth = exports.DooverAuth = exports.AmbiguousWriteError = exports.UnsupportedCapabilityError = exports.ALL_CAPABILITIES = exports.DooverRpcError = exports.RpcDispatcher = exports.UsersApi = exports.TurnApi = exports.ProcessorsApi = exports.PermissionsApi = exports.NotificationsApi = exports.MessagesApi = exports.ConnectionsApi = exports.ChannelsApi = exports.AlarmsApi = exports.AggregatesApi = exports.AgentsApi = exports.DooverStatsCollector = exports.resetDooverClient = exports.peekDooverClient = exports.getDooverClient = exports.MultiplexClient = exports.LocalAgentClient = exports.DooverClient = void 0;
3
+ exports.generateSnowflakeIdAtTime = exports.extractSnowflakeId = exports.addTimestampToMessage = exports.DV_AUDIT_CHANNEL = exports.getIdentifierFromPath = exports.DooverDataProvider = exports.DooverValidationError = exports.DooverGatewayError = exports.DooverApiError = exports.RestClient = exports.GatewayClient = exports.buildAuth = exports.DooverAuthError = exports.AuthProfile = exports.DooverTokenAuth = exports.CookieAuth = exports.DooverAuth = exports.AmbiguousWriteError = exports.UnsupportedCapabilityError = exports.ALL_CAPABILITIES = exports.DooverRpcError = exports.RpcDispatcher = exports.UsersApi = exports.TurnApi = exports.ProcessorsApi = exports.PermissionsApi = exports.OrganisationsApi = exports.NotificationsApi = exports.MessagesApi = exports.ConnectionsApi = exports.ChannelsApi = exports.AlarmsApi = exports.AggregatesApi = exports.AgentsApi = exports.DooverStatsCollector = exports.resetDooverClient = exports.peekDooverClient = exports.getDooverClient = exports.MultiplexClient = exports.LocalAgentClient = exports.DooverClient = void 0;
4
4
  var doover_client_1 = require("./client/doover-client");
5
5
  Object.defineProperty(exports, "DooverClient", { enumerable: true, get: function () { return doover_client_1.DooverClient; } });
6
6
  var local_agent_client_1 = require("./client/local-agent-client");
@@ -27,6 +27,8 @@ var messages_api_1 = require("./apis/messages-api");
27
27
  Object.defineProperty(exports, "MessagesApi", { enumerable: true, get: function () { return messages_api_1.MessagesApi; } });
28
28
  var notifications_api_1 = require("./apis/notifications-api");
29
29
  Object.defineProperty(exports, "NotificationsApi", { enumerable: true, get: function () { return notifications_api_1.NotificationsApi; } });
30
+ var organisations_api_1 = require("./apis/organisations-api");
31
+ Object.defineProperty(exports, "OrganisationsApi", { enumerable: true, get: function () { return organisations_api_1.OrganisationsApi; } });
30
32
  var permissions_api_1 = require("./apis/permissions-api");
31
33
  Object.defineProperty(exports, "PermissionsApi", { enumerable: true, get: function () { return permissions_api_1.PermissionsApi; } });
32
34
  var processors_api_1 = require("./apis/processors-api");
@@ -0,0 +1,72 @@
1
+ import type { RestClient } from "../http/rest-client";
2
+ export type RefineRecord = Record<string, unknown> & {
3
+ id?: string | number;
4
+ };
5
+ export type RefineMeta = Record<string, unknown> | undefined;
6
+ export interface RefinePagination {
7
+ currentPage?: number;
8
+ pageSize?: number;
9
+ }
10
+ export interface RefineSorter {
11
+ field: string;
12
+ order?: "asc" | "desc";
13
+ }
14
+ export interface RefineCrudFilter {
15
+ field: string;
16
+ operator: string;
17
+ value: unknown;
18
+ }
19
+ export interface RefineGetListParams {
20
+ resource: string;
21
+ pagination?: RefinePagination;
22
+ sorters?: RefineSorter[];
23
+ filters?: RefineCrudFilter[];
24
+ meta?: RefineMeta;
25
+ }
26
+ export interface RefineOneParams {
27
+ resource: string;
28
+ id: string | number;
29
+ meta?: RefineMeta;
30
+ }
31
+ export interface RefineMutationParams {
32
+ resource: string;
33
+ id?: string | number;
34
+ variables?: unknown;
35
+ meta?: RefineMeta;
36
+ }
37
+ export interface RefineCustomParams {
38
+ url: string;
39
+ method: string;
40
+ headers?: Record<string, string>;
41
+ payload?: unknown;
42
+ meta?: RefineMeta;
43
+ }
44
+ export interface RefineDataProviderLike {
45
+ getOne: (params: RefineOneParams) => Promise<{
46
+ data: unknown;
47
+ }>;
48
+ getList: <TData extends RefineRecord = RefineRecord>(params: RefineGetListParams) => Promise<{
49
+ data: TData[];
50
+ total: number;
51
+ }>;
52
+ create: (params: RefineMutationParams) => Promise<{
53
+ data: unknown;
54
+ }>;
55
+ update: (params: RefineMutationParams & {
56
+ id: string | number;
57
+ }) => Promise<{
58
+ data: unknown;
59
+ }>;
60
+ deleteOne: (params: RefineOneParams) => Promise<{
61
+ data: unknown;
62
+ }>;
63
+ custom: (params: RefineCustomParams) => Promise<{
64
+ data: unknown;
65
+ }>;
66
+ getApiUrl: () => string;
67
+ }
68
+ export interface CreateRefineDataProviderOptions {
69
+ baseUrl?: string;
70
+ }
71
+ export declare function createRefineDataProvider(rest: RestClient, options?: CreateRefineDataProviderOptions): RefineDataProviderLike;
72
+ export declare function getApiPath(resource: string, meta?: RefineMeta): string;
@@ -0,0 +1,219 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createRefineDataProvider = createRefineDataProvider;
4
+ exports.getApiPath = getApiPath;
5
+ function createRefineDataProvider(rest, options = {}) {
6
+ const baseUrl = options.baseUrl ?? rest.config.controlApiUrl;
7
+ return {
8
+ getOne: async ({ resource, id, meta }) => {
9
+ const data = await rest.request({
10
+ path: `/${getApiPath(resource, meta)}/${id}/`,
11
+ headers: getMetaHeaders(meta),
12
+ baseUrl,
13
+ });
14
+ return { data };
15
+ },
16
+ getList: async ({ resource, pagination, sorters, filters, meta, }) => {
17
+ const data = await rest.request({
18
+ path: `/${getApiPath(resource, meta)}/`,
19
+ query: buildListQuery({ pagination, sorters, filters, meta }),
20
+ headers: getMetaHeaders(meta),
21
+ baseUrl,
22
+ });
23
+ return {
24
+ data: data.results,
25
+ total: data.count,
26
+ };
27
+ },
28
+ create: async ({ resource, variables, meta }) => {
29
+ const mergedMeta = isRecord(variables) ? { ...meta, ...variables } : meta;
30
+ const data = await rest.request({
31
+ path: `/${getApiPath(resource, mergedMeta)}/`,
32
+ method: "POST",
33
+ body: prepareVariables(variables, meta),
34
+ headers: getMetaHeaders(meta),
35
+ baseUrl,
36
+ });
37
+ return { data };
38
+ },
39
+ update: async ({ resource, id, variables, meta }) => {
40
+ const data = await rest.request({
41
+ path: `/${getApiPath(resource, meta)}/${id}/`,
42
+ method: "PATCH",
43
+ body: prepareVariables(variables, meta),
44
+ headers: getMetaHeaders(meta),
45
+ baseUrl,
46
+ });
47
+ return { data };
48
+ },
49
+ deleteOne: async ({ resource, id, meta }) => {
50
+ const data = await rest.request({
51
+ path: `/${getApiPath(resource, meta)}/${id}/`,
52
+ method: "DELETE",
53
+ headers: getMetaHeaders(meta),
54
+ baseUrl,
55
+ });
56
+ return { data: data ?? null };
57
+ },
58
+ custom: async ({ url, method, headers, payload, meta }) => {
59
+ const { path, query } = splitCustomUrl(url);
60
+ const data = await rest.request({
61
+ path,
62
+ method,
63
+ headers: { ...headers, ...getMetaHeaders(meta) },
64
+ body: payload == null ? undefined : payload,
65
+ query,
66
+ baseUrl,
67
+ });
68
+ return { data };
69
+ },
70
+ getApiUrl: () => baseUrl,
71
+ };
72
+ }
73
+ function getMetaHeaders(meta) {
74
+ if (!meta?.organisation)
75
+ return undefined;
76
+ return { "X-Doover-Organisation": String(idValue(meta.organisation)) };
77
+ }
78
+ function getApiPath(resource, meta) {
79
+ const apiPath = meta?.api_path;
80
+ if (apiPath !== undefined) {
81
+ const paths = typeof apiPath === "string" ? [apiPath] : apiPath;
82
+ if (Array.isArray(paths)) {
83
+ for (const path of paths) {
84
+ if (typeof path !== "string")
85
+ continue;
86
+ try {
87
+ return fillApiPath(path, meta);
88
+ }
89
+ catch (err) {
90
+ if (paths.length === 1)
91
+ throw err;
92
+ }
93
+ }
94
+ }
95
+ }
96
+ return resource;
97
+ }
98
+ function fillApiPath(path, meta) {
99
+ let apiPath = path;
100
+ const matches = [...apiPath.matchAll(/:([^/]+)/g)].map((m) => m[1]);
101
+ for (const match of matches) {
102
+ const value = lookupPathParam(match, meta);
103
+ if (value === undefined) {
104
+ throw new Error(`Missing value for ${match} in meta`);
105
+ }
106
+ apiPath = apiPath.replace(`:${match}`, String(value));
107
+ }
108
+ return apiPath;
109
+ }
110
+ function lookupPathParam(match, meta) {
111
+ if (!meta)
112
+ return undefined;
113
+ if (match in meta && meta[match] !== undefined)
114
+ return idValue(meta[match]);
115
+ const withoutId = match.replace(/id$/i, "");
116
+ if (withoutId in meta && meta[withoutId] !== undefined) {
117
+ return idValue(meta[withoutId]);
118
+ }
119
+ return undefined;
120
+ }
121
+ function idValue(value) {
122
+ return isRecord(value) && "id" in value ? value.id : value;
123
+ }
124
+ function buildListQuery({ pagination, sorters, filters, meta, }) {
125
+ const query = {};
126
+ if (pagination) {
127
+ query.page = pagination.currentPage ?? 1;
128
+ if (pagination.pageSize)
129
+ query.per_page = pagination.pageSize;
130
+ }
131
+ if (sorters && sorters.length > 0) {
132
+ query.ordering = `${sorters[0].order === "desc" ? "-" : ""}${sorters[0].field}`;
133
+ }
134
+ let hasArchivedFilter = false;
135
+ for (const filter of filters ?? []) {
136
+ if (!filter.field || !filter.operator)
137
+ continue;
138
+ if (filter.operator === "contains" && filter.field === "search") {
139
+ query.search = String(filter.value ?? "");
140
+ continue;
141
+ }
142
+ if (filter.field === "archived" || filter.field === "device_archived") {
143
+ hasArchivedFilter = hasArchivedFilter || filter.field === "archived";
144
+ if (filter.operator === "eq" || filter.operator === "in") {
145
+ query[filter.field] =
146
+ filter.value === "__all__" ? "" : String(filter.value ?? "");
147
+ }
148
+ continue;
149
+ }
150
+ if (filter.operator === "contains") {
151
+ query[`${filter.field}__icontains`] = String(filter.value ?? "");
152
+ }
153
+ else if (filter.operator === "containss") {
154
+ query[`${filter.field}__contains`] = String(filter.value ?? "");
155
+ }
156
+ else if (filter.operator === "eq" || filter.operator === "in") {
157
+ query[filter.field] = String(filter.value ?? "");
158
+ }
159
+ }
160
+ if (meta?.canArchive === true && !hasArchivedFilter) {
161
+ query.archived = "false";
162
+ }
163
+ return query;
164
+ }
165
+ function prepareVariables(variables, meta) {
166
+ if (!isRecord(variables))
167
+ return variables;
168
+ const next = { ...variables };
169
+ const renameFields = isRecord(meta?.renameFields) ? meta.renameFields : null;
170
+ if (renameFields) {
171
+ for (const [from, to] of Object.entries(renameFields)) {
172
+ if (typeof to !== "string")
173
+ continue;
174
+ const value = next[from];
175
+ const mapped = Array.isArray(value)
176
+ ? value.map((item) => idValue(item))
177
+ : idValue(value);
178
+ next[from] = mapped;
179
+ next[to] = mapped;
180
+ }
181
+ }
182
+ if (Object.values(next).some(isFileLike)) {
183
+ const formData = new FormData();
184
+ for (const [key, value] of Object.entries(next)) {
185
+ if (value == null)
186
+ continue;
187
+ if (isFileLike(value)) {
188
+ formData.append(key, value);
189
+ }
190
+ else if (typeof value === "object") {
191
+ formData.append(key, JSON.stringify(value));
192
+ }
193
+ else {
194
+ formData.append(key, String(value));
195
+ }
196
+ }
197
+ return formData;
198
+ }
199
+ return next;
200
+ }
201
+ function splitCustomUrl(url) {
202
+ const [rawPath, rawQuery] = url.replace(/^\//, "").split("?", 2);
203
+ const path = `/${rawPath.replace(/\/?$/, "/")}`;
204
+ if (!rawQuery)
205
+ return { path, query: undefined };
206
+ const params = new URLSearchParams(rawQuery);
207
+ const query = {};
208
+ for (const key of new Set(params.keys())) {
209
+ const values = params.getAll(key);
210
+ query[key] = values.length > 1 ? values : values[0] ?? "";
211
+ }
212
+ return { path, query };
213
+ }
214
+ function isRecord(value) {
215
+ return typeof value === "object" && value !== null && !Array.isArray(value);
216
+ }
217
+ function isFileLike(value) {
218
+ return typeof File !== "undefined" && value instanceof File;
219
+ }
@@ -243,6 +243,7 @@ describe("ProcessorsApi overloads", () => {
243
243
  });
244
244
  });
245
245
  const agents_api_1 = require("../apis/agents-api");
246
+ const organisations_api_1 = require("../apis/organisations-api");
246
247
  describe("AgentsApi listAgents", () => {
247
248
  it("calls /agents/ on controlApiUrl with omitSharingHeader", async () => {
248
249
  const rest = makeRestStub();
@@ -283,3 +284,47 @@ describe("AgentsApi listAgents", () => {
283
284
  (0, chai_1.expect)(types).to.deep.equal(["device", "organisation", "user"]);
284
285
  });
285
286
  });
287
+ describe("OrganisationsApi", () => {
288
+ it("lists organisations on controlApiUrl with expected query params", async () => {
289
+ const rest = makeRestStub();
290
+ const api = new organisations_api_1.OrganisationsApi(rest, "https://control.example.com");
291
+ rest.get = ((...args) => {
292
+ rest.calls.push({ method: "get", args });
293
+ return Promise.resolve({ count: 0, results: [] });
294
+ });
295
+ await api.listOrganisations({
296
+ page: 2,
297
+ perPage: 50,
298
+ search: "pump",
299
+ archived: false,
300
+ });
301
+ (0, chai_1.expect)(rest.calls).to.deep.equal([
302
+ {
303
+ method: "get",
304
+ args: [
305
+ "/organisations/",
306
+ { page: 2, per_page: 50, search: "pump", archived: false },
307
+ "https://control.example.com",
308
+ ],
309
+ },
310
+ ]);
311
+ });
312
+ it("maps an organisation to customer-site bootstrap shape", () => {
313
+ const rest = makeRestStub();
314
+ const api = new organisations_api_1.OrganisationsApi(rest);
315
+ (0, chai_1.expect)(api.toCustomerSite({
316
+ id: "org-1",
317
+ name: "Org One",
318
+ archived: false,
319
+ application_id: "app-1",
320
+ domains: [],
321
+ theme: { accent_colour: "#0c71c3" },
322
+ })).to.deep.equal({
323
+ id: "org-1",
324
+ name: "Org One",
325
+ archived: false,
326
+ application_id: "app-1",
327
+ theme: { accent_colour: "#0c71c3" },
328
+ });
329
+ });
330
+ });
@@ -91,6 +91,25 @@ class MemoryProfileStore {
91
91
  Authorization: "Bearer new-token",
92
92
  });
93
93
  });
94
+ (0, mocha_1.it)("setRefreshToken updates the refresh token used by ensureReady", async () => {
95
+ const fetchMock = (0, helpers_1.createFetchMock)(() => (0, helpers_1.createJsonResponse)({
96
+ access_token: "new-access-token",
97
+ expires_in: 3600,
98
+ }));
99
+ const auth = new doover_token_auth_1.DooverTokenAuth({
100
+ token: null,
101
+ authServerUrl: "https://auth.example.com",
102
+ authServerClientId: "client-1",
103
+ fetchImpl: fetchMock,
104
+ });
105
+ auth.setRefreshToken("new-refresh-token");
106
+ await auth.ensureReady();
107
+ (0, chai_1.expect)(fetchMock.callCount).to.equal(1);
108
+ (0, chai_1.expect)(fetchMock.firstCall.args[0]).to.contain("refresh_token=new-refresh-token");
109
+ (0, chai_1.expect)(await auth.getHttpHeaders()).to.deep.equal({
110
+ Authorization: "Bearer new-access-token",
111
+ });
112
+ });
94
113
  // -- Refresh behaviour ----------------------------------------------------
95
114
  (0, mocha_1.it)("does not refresh when token is clearly valid", async () => {
96
115
  const farFuture = Date.now() + 3600000;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const chai_1 = require("chai");
4
+ const mocha_1 = require("mocha");
5
+ const refine_1 = require("../refine");
6
+ const rest_client_1 = require("../http/rest-client");
7
+ const helpers_1 = require("./helpers");
8
+ (0, mocha_1.describe)("createRefineDataProvider", () => {
9
+ (0, mocha_1.it)("builds list requests with api_path, pagination, sorters, filters and organisation header", async () => {
10
+ const fetchMock = (0, helpers_1.createFetchMock)(() => (0, helpers_1.createJsonResponse)({ count: 1, results: [{ id: "1", name: "Device" }] }));
11
+ const rest = new rest_client_1.RestClient({
12
+ dataRestUrl: "https://data.example.com/api",
13
+ controlApiUrl: "https://control.example.com",
14
+ dataWssUrl: "wss://data.example.com/gateway",
15
+ organisationId: "org-1",
16
+ fetchImpl: fetchMock,
17
+ });
18
+ const provider = (0, refine_1.createRefineDataProvider)(rest);
19
+ const result = await provider.getList({
20
+ resource: "devices",
21
+ pagination: { currentPage: 2, pageSize: 25 },
22
+ sorters: [{ field: "name", order: "desc" }],
23
+ filters: [
24
+ { field: "search", operator: "contains", value: "pump" },
25
+ { field: "archived", operator: "eq", value: false },
26
+ ],
27
+ meta: { api_path: "organisations/:organisationId/devices", organisationId: "org-2" },
28
+ });
29
+ (0, chai_1.expect)(result).to.deep.equal({
30
+ data: [{ id: "1", name: "Device" }],
31
+ total: 1,
32
+ });
33
+ (0, chai_1.expect)(fetchMock.callCount).to.equal(1);
34
+ const [url, init] = fetchMock.getCall(0).args;
35
+ (0, chai_1.expect)(url).to.equal("https://control.example.com/organisations/org-2/devices/?page=2&per_page=25&ordering=-name&search=pump&archived=false");
36
+ (0, chai_1.expect)((init?.headers).get("X-Doover-Organisation")).to.equal("org-1");
37
+ const explicit = await provider.getList({
38
+ resource: "devices",
39
+ meta: { organisation: "org-3" },
40
+ });
41
+ (0, chai_1.expect)(explicit.total).to.equal(1);
42
+ (0, chai_1.expect)((fetchMock.getCall(1).args[1]?.headers).get("X-Doover-Organisation")).to.equal("org-3");
43
+ });
44
+ (0, mocha_1.it)("renames object fields and sends JSON mutations through RestClient", async () => {
45
+ const fetchMock = (0, helpers_1.createFetchMock)((_url, init) => (0, helpers_1.createJsonResponse)(JSON.parse(String(init?.body))));
46
+ const rest = new rest_client_1.RestClient({
47
+ dataRestUrl: "https://data.example.com/api",
48
+ controlApiUrl: "https://control.example.com",
49
+ dataWssUrl: "wss://data.example.com/gateway",
50
+ fetchImpl: fetchMock,
51
+ });
52
+ const provider = (0, refine_1.createRefineDataProvider)(rest);
53
+ const result = await provider.update({
54
+ resource: "devices",
55
+ id: "device-1",
56
+ variables: { group: { id: "group-1", name: "Group" } },
57
+ meta: { renameFields: { group: "group_id" } },
58
+ });
59
+ (0, chai_1.expect)(result.data).to.deep.equal({
60
+ group: "group-1",
61
+ group_id: "group-1",
62
+ });
63
+ });
64
+ });
65
+ (0, mocha_1.describe)("getApiPath", () => {
66
+ (0, mocha_1.it)("falls back across api_path candidates", () => {
67
+ (0, chai_1.expect)((0, refine_1.getApiPath)("domains", {
68
+ api_path: ["organisations/:organisationId/domains", "organisations/domains"],
69
+ })).to.equal("organisations/domains");
70
+ });
71
+ });
@@ -29,6 +29,43 @@ export interface User {
29
29
  __source?: SourceProvenance;
30
30
  [key: string]: unknown;
31
31
  }
32
+ export interface Theme {
33
+ [key: string]: unknown;
34
+ }
35
+ export interface OrganisationDomain {
36
+ id: string;
37
+ hostname: string;
38
+ default: boolean;
39
+ }
40
+ export interface Organisation {
41
+ id: string;
42
+ name: string;
43
+ archived: boolean;
44
+ application_id: string;
45
+ domains: OrganisationDomain[];
46
+ root_group?: unknown;
47
+ retention_period?: number;
48
+ legacy_doover_api_key?: string | null;
49
+ llm_api_key?: string | null;
50
+ extra_config?: Record<string, unknown>;
51
+ theme?: Theme;
52
+ __source?: SourceProvenance;
53
+ [key: string]: unknown;
54
+ }
55
+ export interface CustomerSite {
56
+ id: string;
57
+ name: string;
58
+ application_id: string;
59
+ archived: boolean;
60
+ theme: Theme;
61
+ }
62
+ export interface PaginatedResponse<T> {
63
+ count: number;
64
+ results: T[];
65
+ next?: string | null;
66
+ previous?: string | null;
67
+ [key: string]: unknown;
68
+ }
32
69
  export interface GetAgentsOptions {
33
70
  includeArchived?: boolean;
34
71
  includeOrganisations?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doover-js",
3
- "version": "0.6.4",
3
+ "version": "0.6.5",
4
4
  "description": "TypeScript client for Doover.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -16,6 +16,10 @@
16
16
  "./react": {
17
17
  "types": "./dist/react/index.d.ts",
18
18
  "default": "./dist/react/index.js"
19
+ },
20
+ "./refine": {
21
+ "types": "./dist/refine/index.d.ts",
22
+ "default": "./dist/refine/index.js"
19
23
  }
20
24
  },
21
25
  "repository": {
@@ -42,10 +46,14 @@
42
46
  ],
43
47
  "license": "Apache-2.0",
44
48
  "peerDependencies": {
49
+ "@refinedev/core": "^5",
45
50
  "@tanstack/react-query": "^5",
46
51
  "react": "^18 || ^19"
47
52
  },
48
53
  "peerDependenciesMeta": {
54
+ "@refinedev/core": {
55
+ "optional": true
56
+ },
49
57
  "@tanstack/react-query": {
50
58
  "optional": true
51
59
  },