umami-api-js 0.2.2 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,13 +1,15 @@
1
1
  # umami-api-js
2
2
 
3
+ [![release](https://codeberg.org/Taevas/umami-api-js/badges/release.svg)](https://codeberg.org/Taevas/umami-api-js/releases)
4
+ [![automated testing](https://codeberg.org/Taevas/umami-api-js/badges/workflows/test.yml/badge.svg)](https://codeberg.org/Taevas/umami-api-js/actions?workflow=test.yml)
3
5
  [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/V7V4J78L0)
4
6
 
5
7
  A package to interact with [the API of **self-hosted instances** of Umami](https://umami.is/docs/api).
6
8
 
7
- It has not been tested on [Umami Cloud](https://umami.is/docs/cloud) (the instance of Umami hosted by its creators) and is therefore not expected to work there.
8
-
9
9
  The documentation for the latest version of this package can be found at any time on [umami-api-js.taevas.xyz](https://umami-api-js.taevas.xyz)!
10
10
 
11
+ Please note: It has not been made to work on [Umami Cloud](https://umami.is/docs/cloud) (the instance of Umami hosted by its creators) and is therefore not expected to work there.
12
+
11
13
  ## How to install and get started
12
14
 
13
15
  Before installing, if using Node.js, check if you're running version 20 or above:
@@ -29,19 +31,97 @@ Finally, you will want to create an API object, which you will use for essential
29
31
 
30
32
  ```typescript
31
33
  // TypeScript
32
- import * as umami from "umami-api-js"
34
+ import * as umami from "umami-api-js";
33
35
 
34
36
  // The API of self-hosted Umami instances authenticates with the credentials of actual accounts
35
- const api = new umami.API("https://visitors.taevas.xyz/api", "<username>", "<password>") // first argument being the API route of a self-hosted Umami instance
37
+ const api = new umami.API(
38
+ "https://visitors.taevas.xyz/api",
39
+ "<username>",
40
+ "<password>",
41
+ ); // first argument being the API route of a self-hosted Umami instance
36
42
 
37
43
  // The website_id is featured in multiple places, like where you'd see the analytics script or the settings interface
38
44
  async function displayStats(website_id: string) {
39
45
  const now = new Date();
40
46
  const sevendaysago = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
41
- const stats = await api.getWebsiteStats(website_id, {startAt: sevendaysago, endAt: now, timezone: "UTC"});
47
+ const stats = await api.getWebsiteStats(website_id, {
48
+ startAt: sevendaysago,
49
+ endAt: now,
50
+ timezone: "UTC",
51
+ });
42
52
 
43
- console.log(`This website recently had ${stats.visits} visits from ${stats.visitors} visitors!`);
53
+ console.log(
54
+ `This website recently had ${stats.visits} visits from ${stats.visitors} visitors!`,
55
+ );
44
56
  }
45
57
 
46
58
  displayStats("f196d626-e609-4841-9a80-0dc60f523ed5");
47
59
  ```
60
+
61
+ ### Configuration options
62
+
63
+ Your [`api`](https://umami-api-js.taevas.xyz/classes/API.html) object has many properties (listed as _Accessors_ in the documentation) which you can modify in order to change its behaviour. There are two ways to do that, the first of which is to do it at any point after you've created your object:
64
+
65
+ ```typescript
66
+ const api = new umami.API("<api_endpoint>", "<username>", "<password>");
67
+ // Log all requests made by this api object
68
+ api.verbose = "all";
69
+ // Change the amount of seconds it takes for requests to timeout
70
+ api.timeout = 10;
71
+ ```
72
+
73
+ The other way would be at the same time you're creating your `api` object, like that:
74
+
75
+ ```typescript
76
+ // Same as above, in one line as the api object gets created
77
+ const api = new umami.API("<api_endpoint>", "<username>", "<password>", {
78
+ verbose: "all",
79
+ timeout: 10,
80
+ });
81
+ ```
82
+
83
+ ### Tokens
84
+
85
+ An [`access_token`](https://umami-api-js.taevas.xyz/classes/API.html#access_token) is required to access the API. When you first create your [`api`](https://umami-api-js.taevas.xyz/classes/API.html) object, that token is automatically set before any request is made, so you don't have to worry about that!
86
+
87
+ Unlike many other web applications, [Umami doesn't expire tokens after a certain amount of time](https://github.com/umami-software/umami/discussions/1170), meaning in theory that you would only ever need to request a new token if your credentials change. After changing the [username](https://umami-api-js.taevas.xyz/classes/API.html#username) or [password](https://umami-api-js.taevas.xyz/classes/API.html#password), you may manually call [`setNewToken()`](https://umami-api-js.taevas.xyz/classes/API.html#setnewtoken), which will replace your previous token with a new one!
88
+
89
+ This package was built with the expectation that tokens would eventually expire, so you've got some configuration options you can play around with:
90
+
91
+ - Keep the [`set_token_on_expires`](https://umami-api-js.taevas.xyz/classes/API.html#set_token_on_expires) option to true if you'd like the object to call `setNewToken()` automatically as soon as the token expires
92
+ - By default, the [`set_token_on_401`](https://umami-api-js.taevas.xyz/classes/API.html#set_token_on_401) option is set to false, which (as its name indicates) would do that upon encountering a 401
93
+ - When that happens, if [`retry_on_new_token`](https://umami-api-js.taevas.xyz/classes/API.html#retry_on_new_token) is set to true as it is by default, it will retry the request it has encountered a 401 on, with the new token! (note that loops are prevented, it won't retry or get another token if the same request with the new token gets a 401)
94
+
95
+ At any point in time, you can see when the current `access_token` is set to expire through the [`expires`](https://umami-api-js.taevas.xyz/classes/API.html#expires) property of the API.
96
+
97
+ ### Retries
98
+
99
+ Your `api` object has a configurable behaviour when it comes to handling requests that have failed for certain reasons. It may "retry" a request, meaning **not throwing and making the request again as if it hasn't failed** under the following circumstances:
100
+
101
+ NOTE: [`retry_maximum_amount`](https://umami-api-js.taevas.xyz/classes/API.html#retry_maximum_amount) must be above 0 for any retry to happen in the first place!
102
+
103
+ - [`set_token_on_401`](https://umami-api-js.taevas.xyz/classes/API.html#set_token_on_401) is set, a token is obtained thanks to it, and [`retry_on_new_token`](https://umami-api-js.taevas.xyz/classes/API.html#retry_on_new_token) is also set
104
+ - A failed request has a status code featured in [`retry_on_status_codes`](https://umami-api-js.taevas.xyz/classes/API.html#retry_on_status_codes)
105
+ - [`retry_on_timeout`](https://umami-api-js.taevas.xyz/classes/API.html#retry_on_timeout) is set and a timeout has happened
106
+
107
+ You can further configure retries through [`retry_delay`](https://umami-api-js.taevas.xyz/classes/API.html#retry_delay) and the aforementioned `retry_maximum_amount`.
108
+
109
+ ### Calling the functions, but literally
110
+
111
+ This package's functions can be accessed both through the api object and through namespaces! It essentially means that for convenience's sake, there are two ways to do anything:
112
+
113
+ ```typescript
114
+ // Obtaining a match, assuming an `api` object already exists and everything from the package is imported as `umami`
115
+ const team_1 = await api.getTeam("<team_id>"); // through the api object
116
+ const team_2 = await umami.Teams.get_TEAMID.call(api, "<team_id>"); // through the namespaces
117
+ // `team_1` and `team_2` are the same, because they're essentially using the same function!
118
+
119
+ // The same, but for obtaining the authenticated user
120
+ const me_1 = await api.getMyself();
121
+ const me_2 = await umami.Me.get.call(api);
122
+ // `me_1` and `me_2` are also the same!
123
+ ```
124
+
125
+ As you may have noticed, when calling the functions through the namespaces, instead of doing something like `get()`, we instead do `get.call()` and use [the call() method](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call) in order to provide a `this` value; the api object!
126
+
127
+ Of course, using [the apply() method](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply) would also work, so just do things the way you prefer or the way that is more intuitive to you!
package/dist/index.d.ts CHANGED
@@ -10,7 +10,7 @@ import { Teams } from "./namespaces/Teams.js";
10
10
  import { Users } from "./namespaces/Users.js";
11
11
  import { Websites } from "./namespaces/Websites.js";
12
12
  import { WebsiteStats } from "./namespaces/WebsiteStats.js";
13
- export { Admin, Events, Links, Me, Pixels, Websites, WebsiteStats, Realtime, Reports, Sessions, Teams, Users };
13
+ export { Admin, Events, Links, Me, Pixels, Websites, WebsiteStats, Realtime, Reports, Sessions, Teams, Users, };
14
14
  export interface DeletionResult {
15
15
  ok: boolean;
16
16
  }
@@ -114,7 +114,7 @@ export declare class API {
114
114
  * @param username The username of the user you're logging in as
115
115
  * @param password The password of the user you're logging in as
116
116
  * @param settings Additional settings you'd like to specify now rather than later, check out the Accessors at https://umami-api-js.taevas.xyz/classes/API.html
117
- */
117
+ */
118
118
  static createAsync(server: API["server"], username: API["username"], password: API["password"], settings?: Partial<API>): Promise<API>;
119
119
  private _token;
120
120
  /** The key that allows you to talk with the API */
@@ -125,9 +125,13 @@ export declare class API {
125
125
  get token_type(): string;
126
126
  set token_type(token: string);
127
127
  private _expires;
128
- /** The expiration date of your token */
129
- get expires(): Date;
130
- set expires(date: Date);
128
+ /**
129
+ * The expiration date of your token
130
+ * @remarks Umami v3.0.3 behaviour is to NOT expire tokens a given amount of time after creation,
131
+ * see https://github.com/umami-software/umami/discussions/1170
132
+ */
133
+ get expires(): Date | null;
134
+ set expires(date: Date | null);
131
135
  private _server;
132
136
  /** The base URL where requests should land, **should include the `/api` portion if applicable** */
133
137
  get server(): string;
@@ -171,7 +175,7 @@ export declare class API {
171
175
  get set_token_on_creation(): boolean;
172
176
  set set_token_on_creation(bool: boolean);
173
177
  private _set_token_on_401;
174
- /** If true, upon failing a request due to a 401, it will call {@link API.setNewToken} (defaults to **true**) */
178
+ /** If true, upon failing a request due to a 401, it will call {@link API.setNewToken} (defaults to **false**) */
175
179
  get set_token_on_401(): boolean;
176
180
  set set_token_on_401(bool: boolean);
177
181
  private _set_token_on_expires;
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ import { Users } from "./namespaces/Users.js";
11
11
  import { Websites } from "./namespaces/Websites.js";
12
12
  import { WebsiteStats } from "./namespaces/WebsiteStats.js";
13
13
  import { adaptParametersForGETRequests, correctType } from "./utilities.js";
14
- export { Admin, Events, Links, Me, Pixels, Websites, WebsiteStats, Realtime, Reports, Sessions, Teams, Users };
14
+ export { Admin, Events, Links, Me, Pixels, Websites, WebsiteStats, Realtime, Reports, Sessions, Teams, Users, };
15
15
  /** If the {@link API} throws an error, it should always be an {@link APIError}! */
16
16
  export class APIError extends Error {
17
17
  message;
@@ -47,22 +47,32 @@ export class APIError extends Error {
47
47
  /** An API instance is needed to make requests to the server! */
48
48
  export class API {
49
49
  constructor(server_or_settings, username, password, settings) {
50
- settings ??= typeof server_or_settings !== "string" ? server_or_settings : undefined;
50
+ settings ??=
51
+ typeof server_or_settings !== "string" ? server_or_settings : undefined;
51
52
  if (settings) {
52
53
  /** Delete every property that is `undefined` so the class defaults aren't overwritten by `undefined` */
53
54
  Object.keys(settings).forEach((key) => {
54
- settings[key] === undefined ? delete settings[key] : {};
55
+ settings[key] === undefined
56
+ ? delete settings[key]
57
+ : {};
55
58
  });
56
59
  Object.assign(this, settings);
57
60
  }
58
- if (this.is_setting_token) { // Very likely a clone created while the original didn't have a valid token
61
+ if (this.is_setting_token) {
62
+ // Very likely a clone created while the original didn't have a valid token
59
63
  this.set_token_on_expires = false; // In which case allow the clone to get its own token, but not renewing it automatically
60
64
  } // And if the clone keeps getting used, `set_token_on_401` being true should cover that
61
- /** We want to set a new token instantly if account credentials have been provided */
62
- if (this.set_token_on_creation && typeof server_or_settings === "string" && username && password) {
63
- this.server = server_or_settings;
65
+ if (username) {
64
66
  this.username = username;
67
+ }
68
+ if (password) {
65
69
  this.password = password;
70
+ }
71
+ if (typeof server_or_settings === "string") {
72
+ this.server = server_or_settings;
73
+ }
74
+ /** We want to set a new token instantly if account credentials have been provided */
75
+ if (this.set_token_on_creation && username && password) {
66
76
  this.setNewToken();
67
77
  }
68
78
  }
@@ -73,7 +83,7 @@ export class API {
73
83
  * @param username The username of the user you're logging in as
74
84
  * @param password The password of the user you're logging in as
75
85
  * @param settings Additional settings you'd like to specify now rather than later, check out the Accessors at https://umami-api-js.taevas.xyz/classes/API.html
76
- */
86
+ */
77
87
  static async createAsync(server, username, password, settings) {
78
88
  // We don't want `new API` to set the token, as we can't await it (and awaiting `token_promise` sounds like a bad idea)
79
89
  if (settings) {
@@ -89,15 +99,29 @@ export class API {
89
99
  }
90
100
  _token = "";
91
101
  /** The key that allows you to talk with the API */
92
- get token() { return this._token; }
93
- set token(token) { this._token = token; }
102
+ get token() {
103
+ return this._token;
104
+ }
105
+ set token(token) {
106
+ this._token = token;
107
+ }
94
108
  _token_type = "Bearer";
95
109
  /** Should always be "Bearer" */
96
- get token_type() { return this._token_type; }
97
- set token_type(token) { this._token_type = token; }
98
- _expires = new Date(new Date().getTime() + 24 * 60 * 60 * 1000); // 24 hours default, is set through setNewToken anyway
99
- /** The expiration date of your token */
100
- get expires() { return this._expires; }
110
+ get token_type() {
111
+ return this._token_type;
112
+ }
113
+ set token_type(token) {
114
+ this._token_type = token;
115
+ }
116
+ _expires = null;
117
+ /**
118
+ * The expiration date of your token
119
+ * @remarks Umami v3.0.3 behaviour is to NOT expire tokens a given amount of time after creation,
120
+ * see https://github.com/umami-software/umami/discussions/1170
121
+ */
122
+ get expires() {
123
+ return this._expires;
124
+ }
101
125
  set expires(date) {
102
126
  this._expires = date;
103
127
  this.updateTokenTimer();
@@ -105,18 +129,30 @@ export class API {
105
129
  // CLIENT INFO
106
130
  _server = "";
107
131
  /** The base URL where requests should land, **should include the `/api` portion if applicable** */
108
- get server() { return this._server; }
109
- set server(server) { this._server = server; }
132
+ get server() {
133
+ return this._server;
134
+ }
135
+ set server(server) {
136
+ this._server = server;
137
+ }
110
138
  _username = "";
111
139
  /** The username of the account */
112
- get username() { return this._username; }
113
- set username(username) { this._username = username; }
140
+ get username() {
141
+ return this._username;
142
+ }
143
+ set username(username) {
144
+ this._username = username;
145
+ }
114
146
  _password = "";
115
147
  /** The password of the account */
116
- get password() { return this._password; }
117
- set password(password) { this._password = password; }
148
+ get password() {
149
+ return this._password;
150
+ }
151
+ set password(password) {
152
+ this._password = password;
153
+ }
118
154
  _headers = {
119
- "Accept": "application/json",
155
+ Accept: "application/json",
120
156
  "Accept-Encoding": "gzip",
121
157
  "Content-Type": "application/json",
122
158
  "User-Agent": "umami-api-js (codeberg.org/Taevas/umami-api-js)",
@@ -125,8 +161,12 @@ export class API {
125
161
  * Used in practically all requests, those are all the headers the package uses excluding `Authorization`, the one with the token
126
162
  * @remarks If the User-Agent is not liked by https://isbot.js.org/, Umami might give responses akin to `{beep: "boop"}`
127
163
  */
128
- get headers() { return this._headers; }
129
- set headers(headers) { this._headers = headers; }
164
+ get headers() {
165
+ return this._headers;
166
+ }
167
+ set headers(headers) {
168
+ this._headers = headers;
169
+ }
130
170
  _user = {
131
171
  id: "",
132
172
  username: "",
@@ -135,14 +175,20 @@ export class API {
135
175
  isAdmin: false,
136
176
  };
137
177
  /** Information about the account that has been used to log in */
138
- get user() { return this._user; }
139
- set user(user) { this._user = user; }
178
+ get user() {
179
+ return this._user;
180
+ }
181
+ set user(user) {
182
+ this._user = user;
183
+ }
140
184
  number_of_requests = 0;
141
185
  // TOKEN HANDLING
142
186
  /** Has {@link API.setNewToken} been called and not yet returned anything? */
143
187
  is_setting_token = false;
144
188
  /** If {@link API.setNewToken} has been called, you can wait for it to be done through this promise */
145
- token_promise = new Promise(r => r);
189
+ token_promise = new Promise((r) => {
190
+ r();
191
+ }); // `{r()}` over `r` prevents Node.js weirdness when awaited
146
192
  /**
147
193
  * This contacts the server in order to get and set a new {@link API.token}!
148
194
  * @remarks The API object requires a {@link API.username} and a {@link API.password} to successfully get any token
@@ -158,8 +204,7 @@ export class API {
158
204
  this.token_promise = new Promise((resolve, reject) => {
159
205
  this.fetch(true, "post", ["auth", "login"], body)
160
206
  .then((response) => {
161
- response.json()
162
- .then((json) => {
207
+ response.json().then((json) => {
163
208
  if (!json.token) {
164
209
  const error_message = json.error_description ?? json.message ?? "No token obtained"; // Expect "Client authentication failed"
165
210
  this.log(true, "Unable to obtain a token! Here's what was received from the API:", json);
@@ -168,9 +213,10 @@ export class API {
168
213
  // Note: `json` currently only has `token` & `user`
169
214
  this.token = json.token;
170
215
  this.user = json.user;
171
- const expiration_date = new Date();
172
- expiration_date.setDate(expiration_date.getDate() + 1); // Assume 24 hours
173
- this.expires = expiration_date;
216
+ // If Umami ever sets expiration dates on tokens, the code to let the API object know should be here
217
+ // const expiration_date = new Date();
218
+ // expiration_date.setDate(expiration_date.getDate() + 1); // Assume 24 hours
219
+ // this.expires = expiration_date;
174
220
  resolve();
175
221
  });
176
222
  })
@@ -184,24 +230,36 @@ export class API {
184
230
  }
185
231
  _set_token_on_creation = true;
186
232
  /** If true, when creating your API object, a call to {@link API.setNewToken} will be automatically made (defaults to **true**) */
187
- get set_token_on_creation() { return this._set_token_on_creation; }
188
- set set_token_on_creation(bool) { this._set_token_on_creation = bool; }
189
- _set_token_on_401 = true;
190
- /** If true, upon failing a request due to a 401, it will call {@link API.setNewToken} (defaults to **true**) */
191
- get set_token_on_401() { return this._set_token_on_401; }
192
- set set_token_on_401(bool) { this._set_token_on_401 = bool; }
233
+ get set_token_on_creation() {
234
+ return this._set_token_on_creation;
235
+ }
236
+ set set_token_on_creation(bool) {
237
+ this._set_token_on_creation = bool;
238
+ }
239
+ _set_token_on_401 = false;
240
+ /** If true, upon failing a request due to a 401, it will call {@link API.setNewToken} (defaults to **false**) */
241
+ get set_token_on_401() {
242
+ return this._set_token_on_401;
243
+ }
244
+ set set_token_on_401(bool) {
245
+ this._set_token_on_401 = bool;
246
+ }
193
247
  _set_token_on_expires = false;
194
248
  /**
195
249
  * If true, the application will silently call {@link API.setNewToken} when the {@link API.token} is set to expire,
196
250
  * as determined by {@link API.expires} (defaults to **false**)
197
251
  */
198
- get set_token_on_expires() { return this._set_token_on_expires; }
252
+ get set_token_on_expires() {
253
+ return this._set_token_on_expires;
254
+ }
199
255
  set set_token_on_expires(enabled) {
200
256
  this._set_token_on_expires = enabled;
201
257
  this.updateTokenTimer();
202
258
  }
203
259
  _token_timer;
204
- get token_timer() { return this._token_timer; }
260
+ get token_timer() {
261
+ return this._token_timer;
262
+ }
205
263
  set token_timer(timer) {
206
264
  // if a previous one already exists, clear it
207
265
  if (this._token_timer) {
@@ -238,43 +296,75 @@ export class API {
238
296
  // CLIENT CONFIGURATION
239
297
  _verbose = "none";
240
298
  /** Which events should be logged (defaults to **none**) */
241
- get verbose() { return this._verbose; }
242
- set verbose(verbose) { this._verbose = verbose; }
299
+ get verbose() {
300
+ return this._verbose;
301
+ }
302
+ set verbose(verbose) {
303
+ this._verbose = verbose;
304
+ }
243
305
  _timeout = 20;
244
306
  /**
245
307
  * The maximum **amount of seconds** requests should take before returning an answer (defaults to **20**)
246
308
  * @remarks 0 means no maximum, no timeout
247
309
  */
248
- get timeout() { return this._timeout; }
249
- set timeout(timeout) { this._timeout = timeout; }
310
+ get timeout() {
311
+ return this._timeout;
312
+ }
313
+ set timeout(timeout) {
314
+ this._timeout = timeout;
315
+ }
250
316
  _signal;
251
317
  /** The `AbortSignal` used in every request */
252
- get signal() { return this._signal; }
253
- set signal(signal) { this._signal = signal; }
318
+ get signal() {
319
+ return this._signal;
320
+ }
321
+ set signal(signal) {
322
+ this._signal = signal;
323
+ }
254
324
  // RETRIES
255
325
  _retry_maximum_amount = 4;
256
326
  /**
257
327
  * How many retries maximum before throwing an {@link APIError} (defaults to **4**)
258
328
  * @remarks Pro tip: Set that to 0 to **completely** disable retries!
259
329
  */
260
- get retry_maximum_amount() { return this._retry_maximum_amount; }
261
- set retry_maximum_amount(retry_maximum_amount) { this._retry_maximum_amount = retry_maximum_amount; }
330
+ get retry_maximum_amount() {
331
+ return this._retry_maximum_amount;
332
+ }
333
+ set retry_maximum_amount(retry_maximum_amount) {
334
+ this._retry_maximum_amount = retry_maximum_amount;
335
+ }
262
336
  _retry_delay = 2;
263
337
  /** In seconds, how long should it wait after a request failed before retrying? (defaults to **2**) */
264
- get retry_delay() { return this._retry_delay; }
265
- set retry_delay(retry_delay) { this._retry_delay = retry_delay; }
338
+ get retry_delay() {
339
+ return this._retry_delay;
340
+ }
341
+ set retry_delay(retry_delay) {
342
+ this._retry_delay = retry_delay;
343
+ }
266
344
  _retry_on_new_token = true;
267
345
  /** Should it retry a request upon successfully setting a new token due to {@link API.set_token_on_401} being `true`? (defaults to **true**) */
268
- get retry_on_new_token() { return this._retry_on_new_token; }
269
- set retry_on_new_token(retry_on_new_token) { this._retry_on_new_token = retry_on_new_token; }
346
+ get retry_on_new_token() {
347
+ return this._retry_on_new_token;
348
+ }
349
+ set retry_on_new_token(retry_on_new_token) {
350
+ this._retry_on_new_token = retry_on_new_token;
351
+ }
270
352
  _retry_on_status_codes = [429];
271
353
  /** Upon failing a request and receiving a response, because of which received status code should the request be retried? (defaults to **[429]**) */
272
- get retry_on_status_codes() { return this._retry_on_status_codes; }
273
- set retry_on_status_codes(retry_on_status_codes) { this._retry_on_status_codes = retry_on_status_codes; }
354
+ get retry_on_status_codes() {
355
+ return this._retry_on_status_codes;
356
+ }
357
+ set retry_on_status_codes(retry_on_status_codes) {
358
+ this._retry_on_status_codes = retry_on_status_codes;
359
+ }
274
360
  _retry_on_timeout = false;
275
361
  /** Should it retry a request if that request failed because it has been aborted by the {@link API.timeout}? (defaults to **false**) */
276
- get retry_on_timeout() { return this._retry_on_timeout; }
277
- set retry_on_timeout(retry_on_timeout) { this._retry_on_timeout = retry_on_timeout; }
362
+ get retry_on_timeout() {
363
+ return this._retry_on_timeout;
364
+ }
365
+ set retry_on_timeout(retry_on_timeout) {
366
+ this._retry_on_timeout = retry_on_timeout;
367
+ }
278
368
  // OTHER METHODS
279
369
  /**
280
370
  * Use this instead of `console.log` to log any information
@@ -298,27 +388,45 @@ export class API {
298
388
  * @param info Relevant only when `fetch` calls itself, avoid setting it
299
389
  * @remarks Consider using the higher-level method called {@link API.request}
300
390
  */
301
- async fetch(is_token_related, method, endpoint, parameters = {}, info = { number_try: 1, has_new_token: false }) {
391
+ async fetch(is_token_related, method, endpoint, parameters = {}, info = {
392
+ number_try: 1,
393
+ has_new_token: false,
394
+ }) {
302
395
  let to_retry = false;
303
396
  let error_object;
304
- let error_code;
305
397
  let error_message = "no error message available";
306
398
  if (!is_token_related)
307
- await this.token_promise.catch(() => this.token_promise = new Promise(r => r));
399
+ await this.token_promise.catch(() => (this.token_promise = new Promise((r) => {
400
+ r();
401
+ })));
308
402
  for (const [p, v] of Object.entries(parameters)) {
309
- parameters[p] = v instanceof Date ? Number(v) : v; // Convert Dates to ms
403
+ // Convert Dates to ms
404
+ if (typeof v === "object" && !Array.isArray(v) && v !== null) {
405
+ // It's frankly unnecessary to have better-written code
406
+ for (const [p2, v2] of Object.entries(v)) {
407
+ parameters[p][p2] = v2 instanceof Date ? Number(v2) : v2;
408
+ }
409
+ }
410
+ parameters[p] = v instanceof Date ? Number(v) : v;
310
411
  }
311
412
  let url = `${this.server}`;
312
413
  if (url.slice(-1) !== "/")
313
414
  url += "/";
314
415
  url += endpoint.join("/");
315
- if (method === "get" && parameters) { // for GET requests specifically, requests need to be shaped in very particular ways
316
- url += "?" + (Object.entries(adaptParametersForGETRequests(parameters)).map((param) => {
317
- if (!Array.isArray(param[1])) {
318
- return `${param[0]}=${param[1]}`;
319
- }
320
- return param[1].map((array_element) => `${param[0]}=${array_element}`).join("&");
321
- }).join("&"));
416
+ if (method === "get" && parameters) {
417
+ // for GET requests specifically, requests need to be shaped in very particular ways
418
+ url +=
419
+ "?" +
420
+ Object.entries(adaptParametersForGETRequests(parameters))
421
+ .map((param) => {
422
+ if (!Array.isArray(param[1])) {
423
+ return `${param[0]}=${param[1]}`;
424
+ }
425
+ return param[1]
426
+ .map((array_element) => `${param[0]}=${array_element}`)
427
+ .join("&");
428
+ })
429
+ .join("&");
322
430
  }
323
431
  const signals = [];
324
432
  if (this.timeout > 0)
@@ -328,11 +436,13 @@ export class API {
328
436
  const response = await fetch(url, {
329
437
  method,
330
438
  headers: {
331
- "Authorization": is_token_related ? undefined : `${this.token_type} ${this.token}`,
332
- ...this.headers
439
+ Authorization: is_token_related
440
+ ? undefined
441
+ : `${this.token_type} ${this.token}`,
442
+ ...this.headers,
333
443
  },
334
444
  body: method !== "get" ? JSON.stringify(parameters) : undefined, // parameters are here if method is NOT GET
335
- signal: AbortSignal.any(signals)
445
+ signal: AbortSignal.any(signals),
336
446
  })
337
447
  .catch((error) => {
338
448
  if (error.name === "TimeoutError" && this.retry_on_timeout)
@@ -341,14 +451,13 @@ export class API {
341
451
  error_object = error;
342
452
  error_message = `${error.name} (${error.message ?? error.errno ?? error.type})`;
343
453
  })
344
- .finally(() => this.number_of_requests += 1);
454
+ .finally(() => (this.number_of_requests += 1));
345
455
  const request_id = `(${String(this.number_of_requests).padStart(8, "0")})`;
346
456
  if (response) {
347
457
  if (parameters.password)
348
458
  parameters.password = "<REDACTED>";
349
459
  this.log(this.verbose !== "none" && !response.ok, response.statusText, response.status, { method, endpoint, parameters }, request_id);
350
460
  if (!response.ok) {
351
- error_code = response.status;
352
461
  error_message = response.statusText;
353
462
  if (this.retry_on_status_codes.includes(response.status))
354
463
  to_retry = true;
@@ -357,7 +466,7 @@ export class API {
357
466
  if (this.set_token_on_401 && !info.has_new_token) {
358
467
  if (!this.is_setting_token) {
359
468
  this.log(true, "Your token might have expired, I will attempt to get a new token...", request_id);
360
- if (await this.setNewToken() && this.retry_on_new_token) {
469
+ if ((await this.setNewToken()) && this.retry_on_new_token) {
361
470
  to_retry = true;
362
471
  info.has_new_token = true;
363
472
  }
@@ -376,14 +485,19 @@ export class API {
376
485
  }
377
486
  if (to_retry === true && info.number_try <= this.retry_maximum_amount) {
378
487
  this.log(true, `Will request again in ${this.retry_delay} seconds...`, `(retry #${info.number_try}/${this.retry_maximum_amount})`, request_id);
379
- await new Promise(res => setTimeout(res, this.retry_delay * 1000));
380
- return await this.fetch(is_token_related, method, endpoint, parameters, { number_try: info.number_try + 1, has_new_token: info.has_new_token });
488
+ await new Promise((res) => setTimeout(res, this.retry_delay * 1000));
489
+ return await this.fetch(is_token_related, method, endpoint, parameters, {
490
+ number_try: info.number_try + 1,
491
+ has_new_token: info.has_new_token,
492
+ });
381
493
  }
382
494
  if (!response || !response.ok) {
383
- const resp = response ? {
384
- status_code: response.status,
385
- json: await response.json()
386
- } : undefined;
495
+ const resp = response
496
+ ? {
497
+ status_code: response.status,
498
+ json: await response.json(),
499
+ }
500
+ : undefined;
387
501
  throw new APIError(error_message, this.server, method, endpoint, parameters, resp, error_object);
388
502
  }
389
503
  return response;
@@ -402,11 +516,13 @@ export class API {
402
516
  return undefined; // 204 means the request worked as intended and did not give us anything, so just return nothing
403
517
  const arrBuff = await response.arrayBuffer();
404
518
  const buff = Buffer.from(arrBuff);
405
- try { // Assume the response is in JSON format as it often is, it'll fail into the catch block if it isn't anyway
519
+ try {
520
+ // Assume the response is in JSON format as it often is, it'll fail into the catch block if it isn't anyway
406
521
  // My thorough testing leads me to believe nothing would change if the encoding was also "binary" here btw
407
522
  return correctType(JSON.parse(buff.toString("utf-8")));
408
523
  }
409
- catch { // Assume the response is supposed to not be in JSON format so return it as simple text
524
+ catch {
525
+ // Assume the response is supposed to not be in JSON format so return it as simple text
410
526
  return buff.toString("binary");
411
527
  }
412
528
  }
@@ -424,7 +540,10 @@ export class API {
424
540
  * @group Sending stats
425
541
  */
426
542
  async sendStats(websiteId, payload, type = "event") {
427
- return await this.request("post", ["send"], { payload: { website: websiteId, ...payload }, type });
543
+ return await this.request("post", ["send"], {
544
+ payload: { website: websiteId, ...payload },
545
+ type,
546
+ });
428
547
  }
429
548
  // ADMIN
430
549
  /** @group Admin endpoints */
@@ -9,7 +9,12 @@ export var Events;
9
9
  Events.get_WEBSITEID_Events = get_WEBSITEID_Events;
10
10
  /** Gets event-data for a individual event: https://umami.is/docs/api/events#get-apiwebsiteswebsiteidevent-dataeventid */
11
11
  async function get_WEBSITEID_Eventdata_EVENTID(websiteId, eventId) {
12
- return await this.request("get", ["websites", websiteId, "event-data", eventId]);
12
+ return await this.request("get", [
13
+ "websites",
14
+ websiteId,
15
+ "event-data",
16
+ eventId,
17
+ ]);
13
18
  }
14
19
  Events.get_WEBSITEID_Eventdata_EVENTID = get_WEBSITEID_Eventdata_EVENTID;
15
20
  /**
@@ -23,7 +23,7 @@ export declare namespace Reports {
23
23
  endDate: Date | number;
24
24
  }
25
25
  type ReportType = "attribution" | "breakdown" | "funnel" | "goal" | "journey" | "retention" | "revenue" | "utm";
26
- /** Get all reports by website ID: https://umami.is/docs/api/reports#get-apireports (TODO UNTESTED) */
26
+ /** Get all reports by website ID: https://umami.is/docs/api/reports#get-apireports */
27
27
  function get(this: API, websiteId: Websites.Website["id"], parameters: {
28
28
  type: ReportType;
29
29
  } & Omit<GenericRequestParameters, "search">): Promise<Report[]>;
@@ -43,15 +43,15 @@ export declare namespace Reports {
43
43
  /** Updates a report: https://umami.is/docs/api/reports#post-apireportsreportid */
44
44
  function post_REPORTID(this: API, reportId: Report["id"], parameters?: {
45
45
  /** Your website id */
46
- websiteId?: Websites.Website["id"];
46
+ websiteId: Websites.Website["id"];
47
47
  /** Report type */
48
- type?: ReportType;
48
+ type: ReportType;
49
49
  /** Name of report */
50
50
  name?: Report["name"];
51
51
  /** Description of report */
52
52
  description?: Report["description"];
53
53
  /** Parameters for report */
54
- parameters?: Report["parameters"];
54
+ parameters: Report["parameters"];
55
55
  }): Promise<Report>;
56
56
  /** Deletes a report: https://umami.is/docs/api/reports#delete-apireportsreportid */
57
57
  function delete_REPORTID(this: API, reportId: Report["id"]): Promise<DeletionResult>;
@@ -121,7 +121,10 @@ export declare namespace Reports {
121
121
  num: number;
122
122
  total: number;
123
123
  }
124
- /** Track your goals for pageviews and events: https://umami.is/docs/api/reports#post-apireportsgoals (TODO UNTESTED) */
124
+ /**
125
+ * Track your goals for pageviews and events: https://umami.is/docs/api/reports#post-apireportsgoals
126
+ * @remarks Here be dragons: I have no idea how to use this endpoint without it returning a client/server error, my apologies - The package developer
127
+ */
125
128
  function postGoals(this: API, websiteId: Websites.Website["id"], parameters: Timestamps & {
126
129
  /** Can accept filter parameters */
127
130
  filters?: Filters;
@@ -129,6 +132,12 @@ export declare namespace Reports {
129
132
  type: "path" | "event";
130
133
  /** Conversion step value */
131
134
  value: string;
135
+ /** Undocumented */
136
+ operator?: "count" | "sum" | "average";
137
+ /** Undocumented */
138
+ property?: string;
139
+ /** Undocumented */
140
+ name?: string;
132
141
  }): Promise<Goals>;
133
142
  interface Journey {
134
143
  items: (string | null)[];
@@ -1,15 +1,21 @@
1
1
  /** Using reports throught [sic] the api: https://umami.is/docs/api/reports */
2
2
  export var Reports;
3
3
  (function (Reports) {
4
- /** Get all reports by website ID: https://umami.is/docs/api/reports#get-apireports (TODO UNTESTED) */
4
+ /** Get all reports by website ID: https://umami.is/docs/api/reports#get-apireports */
5
5
  async function get(websiteId, parameters) {
6
- const response = await this.request("get", ["reports"], { websiteId, ...parameters });
6
+ const response = await this.request("get", ["reports"], {
7
+ websiteId,
8
+ ...parameters,
9
+ });
7
10
  return response.data;
8
11
  }
9
12
  Reports.get = get;
10
13
  /** Creates a report: https://umami.is/docs/api/reports#post-apireports */
11
14
  async function post(websiteId, parameters) {
12
- return await this.request("post", ["reports"], { websiteId, ...parameters });
15
+ return await this.request("post", ["reports"], {
16
+ websiteId,
17
+ ...parameters,
18
+ });
13
19
  }
14
20
  Reports.post = post;
15
21
  /** Gets a report by ID: https://umami.is/docs/api/reports#get-apireportsreportid */
@@ -19,7 +25,7 @@ export var Reports;
19
25
  Reports.get_REPORTID = get_REPORTID;
20
26
  /** Updates a report: https://umami.is/docs/api/reports#post-apireportsreportid */
21
27
  async function post_REPORTID(reportId, parameters) {
22
- return await this.request("get", ["reports", reportId], parameters);
28
+ return await this.request("post", ["reports", reportId], parameters);
23
29
  }
24
30
  Reports.post_REPORTID = post_REPORTID;
25
31
  /** Deletes a report: https://umami.is/docs/api/reports#delete-apireportsreportid */
@@ -28,9 +34,15 @@ export var Reports;
28
34
  }
29
35
  Reports.delete_REPORTID = delete_REPORTID;
30
36
  /** Private function to deal more easily with the post functions that don't involve actual reports */
31
- async function postNonReports(api, type, websiteId, filters, parameters) {
37
+ async function postNonReports(api, type, websiteId, filters, parameters, name) {
32
38
  const endpoint = type === "goal" ? "goals" : type; // so close yet so far away from clean code
33
- return await api.request("post", ["reports", endpoint], { websiteId, type, filters: filters ?? {}, parameters });
39
+ return await api.request("post", ["reports", endpoint], {
40
+ websiteId,
41
+ type,
42
+ filters: filters ?? {},
43
+ name,
44
+ parameters,
45
+ });
34
46
  }
35
47
  /** See how users engage with your marketing and what drives conversions: https://umami.is/docs/api/reports#post-apireportsattribution */
36
48
  async function postAttribution(websiteId, parameters) {
@@ -62,14 +74,17 @@ export var Reports;
62
74
  });
63
75
  }
64
76
  Reports.postFunnel = postFunnel;
65
- /** Track your goals for pageviews and events: https://umami.is/docs/api/reports#post-apireportsgoals (TODO UNTESTED) */
77
+ /**
78
+ * Track your goals for pageviews and events: https://umami.is/docs/api/reports#post-apireportsgoals
79
+ * @remarks Here be dragons: I have no idea how to use this endpoint without it returning a client/server error, my apologies - The package developer
80
+ */
66
81
  async function postGoals(websiteId, parameters) {
67
82
  return await postNonReports(this, "goal", websiteId, parameters.filters, {
68
83
  startDate: parameters.startDate,
69
84
  endDate: parameters.endDate,
70
85
  type: parameters.type,
71
86
  value: parameters.value,
72
- });
87
+ }, parameters.name);
73
88
  }
74
89
  Reports.postGoals = postGoals;
75
90
  /** Understand how users nagivate [sic] through your website: https://umami.is/docs/api/reports#post-apireportsjourney */
@@ -10,7 +10,9 @@ export var Sessions;
10
10
  /** Gets summarized website session statistics: https://umami.is/docs/api/sessions#get-apiwebsiteswebsiteidsessionsstats */
11
11
  async function get_WEBSITEID_SessionsStats(websiteId, parameters) {
12
12
  const response = await this.request("get", ["websites", websiteId, "sessions", "stats"], parameters);
13
- Object.values(response).forEach((v) => { v.value = Number(v.value); });
13
+ Object.values(response).forEach((v) => {
14
+ v.value = Number(v.value);
15
+ });
14
16
  return response;
15
17
  }
16
18
  Sessions.get_WEBSITEID_SessionsStats = get_WEBSITEID_SessionsStats;
@@ -21,7 +23,12 @@ export var Sessions;
21
23
  Sessions.get_WEBSITEID_SessionsWeekly = get_WEBSITEID_SessionsWeekly;
22
24
  /** Gets session details for a individual session: https://umami.is/docs/api/sessions#get-apiwebsiteswebsiteidsessionssessionid */
23
25
  async function get_WEBSITEID_Sessions_SESSIONID(websiteId, sessionId) {
24
- return await this.request("get", ["websites", websiteId, "sessions", sessionId]);
26
+ return await this.request("get", [
27
+ "websites",
28
+ websiteId,
29
+ "sessions",
30
+ sessionId,
31
+ ]);
25
32
  }
26
33
  Sessions.get_WEBSITEID_Sessions_SESSIONID = get_WEBSITEID_Sessions_SESSIONID;
27
34
  /** Gets session activity for a individual session: https://umami.is/docs/api/sessions#get-apiwebsiteswebsiteidsessionssessionidactivity */
@@ -31,7 +38,13 @@ export var Sessions;
31
38
  Sessions.get_WEBSITEID_Sessions_SESSIONID_Activity = get_WEBSITEID_Sessions_SESSIONID_Activity;
32
39
  /** Gets session properties for a individual session: https://umami.is/docs/api/sessions#get-apiwebsiteswebsiteidsessionssessionidproperties */
33
40
  async function get_WEBSITEID_Sessions_SESSIONID_Properties(websiteId, sessionId) {
34
- return await this.request("get", ["websites", websiteId, "sessions", sessionId, "properties"]);
41
+ return await this.request("get", [
42
+ "websites",
43
+ websiteId,
44
+ "sessions",
45
+ sessionId,
46
+ "properties",
47
+ ]);
35
48
  }
36
49
  Sessions.get_WEBSITEID_Sessions_SESSIONID_Properties = get_WEBSITEID_Sessions_SESSIONID_Properties;
37
50
  /** Gets session data counts by property name: https://umami.is/docs/api/sessions#get-apiwebsiteswebsiteidsession-dataproperties */
@@ -53,7 +53,9 @@ export var Teams;
53
53
  Teams.get_TEAMID_Users_USERID = get_TEAMID_Users_USERID;
54
54
  /** Update a user's role on a team: https://umami.is/docs/api/teams#post-apiteamsteamidusersuserid */
55
55
  async function post_TEAMID_Users_USERID(teamId, userId, role) {
56
- return await this.request("post", ["teams", teamId, "users", userId], { role });
56
+ return await this.request("post", ["teams", teamId, "users", userId], {
57
+ role,
58
+ });
57
59
  }
58
60
  Teams.post_TEAMID_Users_USERID = post_TEAMID_Users_USERID;
59
61
  /** Remove a user from a team: https://umami.is/docs/api/teams#delete-apiteamsteamidusersuserid */
package/dist/utilities.js CHANGED
@@ -56,9 +56,10 @@ export function correctType(x, force_string) {
56
56
  const vals = Object.values(x);
57
57
  // If a key is any of those, the value is expected to be a string, so we use `force_string` to make correctType convert them to string for us
58
58
  const unconvertables = ["value"];
59
- const unconvertables_substrings = ["string", "name", "Id"]; // or if the key contains any of those substrings
59
+ const unconvertables_substrings = ["string", "name", "id"]; // or if the key contains any of those substrings
60
60
  for (let i = 0; i < keys.length; i++) {
61
- x[keys[i]] = correctType(vals[i], unconvertables.some((u) => keys[i] === u) || unconvertables_substrings.some((s) => keys[i].toLowerCase().includes(s)));
61
+ x[keys[i]] = correctType(vals[i], unconvertables.some((u) => keys[i] === u) ||
62
+ unconvertables_substrings.some((s) => keys[i].toLowerCase().includes(s)));
62
63
  }
63
64
  return x;
64
65
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "umami-api-js",
3
- "version": "0.2.2",
3
+ "version": "1.0.1",
4
4
  "description": "Package to easily access the Umami api!",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -8,6 +8,7 @@
8
8
  "scripts": {
9
9
  "prepublish": "npm run build",
10
10
  "build": "tsc",
11
+ "prettier": "prettier . --write",
11
12
  "test": "npm run build && node ./dist/test.js",
12
13
  "docs": "npx typedoc lib/index.ts --cname umami-api-js.taevas.xyz --plugin ./docs_plugins/visitors.ts"
13
14
  },
@@ -31,11 +32,12 @@
31
32
  "devDependencies": {
32
33
  "@types/chai": "^5.2.3",
33
34
  "@types/node": "^24.9.2",
34
- "ajv": "^8.17.1",
35
+ "ajv": "^8.18.0",
35
36
  "chai": "^6.2.2",
36
37
  "dotenv": "^17.2.3",
38
+ "prettier": "3.8.1",
37
39
  "ts-json-schema-generator": "^2.4.0",
38
- "typedoc": "^0.28.16",
40
+ "typedoc": "^0.28.17",
39
41
  "typescript": "^5.9.3"
40
42
  }
41
43
  }