umami-api-js 0.2.2 → 1.0.0

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,96 @@ 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, and should be valid for 24 hours. 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! But how about after those 24 hours?
86
+
87
+ Once an `access_token` has become invalid, the server will no longer respond correctly to requests made with it, instead responding with [401](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/401). Thankfully, there are solutions to get and set new `access_token`s in a convenient way, so there is no need to create a new `api` object every day!
88
+
89
+ - If you'd like to manually get a new `access_token`, calling [`setNewToken()`](https://umami-api-js.taevas.xyz/classes/API.html#setnewtoken) will replace your previous token with a new one
90
+ - 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 it automatically as soon as the token expires
91
+ - By default, the [`set_token_on_401`](https://umami-api-js.taevas.xyz/classes/API.html#set_token_on_401) option is set to true, which (as its name indicates) will do that upon encountering a 401
92
+ - 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)
93
+
94
+ 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.
95
+
96
+ ### Retries
97
+
98
+ 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:
99
+
100
+ 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!
101
+
102
+ - [`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
103
+ - 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)
104
+ - [`retry_on_timeout`](https://umami-api-js.taevas.xyz/classes/API.html#retry_on_timeout) is set and a timeout has happened
105
+
106
+ 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`.
107
+
108
+ ### Calling the functions, but literally
109
+
110
+ 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:
111
+
112
+ ```typescript
113
+ // Obtaining a match, assuming an `api` object already exists and everything from the package is imported as `umami`
114
+ const team_1 = await api.getTeam("<team_id>"); // through the api object
115
+ const team_2 = await umami.Teams.get_TEAMID.call(api, "<team_id>"); // through the namespaces
116
+ // `team_1` and `team_2` are the same, because they're essentially using the same function!
117
+
118
+ // The same, but for obtaining the authenticated user
119
+ const me_1 = await api.getMyself();
120
+ const me_2 = await umami.Me.get.call(api);
121
+ // `me_1` and `me_2` are also the same!
122
+ ```
123
+
124
+ 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!
125
+
126
+ 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 */
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,19 +47,26 @@ 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
65
  /** 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) {
66
+ if (this.set_token_on_creation &&
67
+ typeof server_or_settings === "string" &&
68
+ username &&
69
+ password) {
63
70
  this.server = server_or_settings;
64
71
  this.username = username;
65
72
  this.password = password;
@@ -73,7 +80,7 @@ export class API {
73
80
  * @param username The username of the user you're logging in as
74
81
  * @param password The password of the user you're logging in as
75
82
  * @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
- */
83
+ */
77
84
  static async createAsync(server, username, password, settings) {
78
85
  // 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
86
  if (settings) {
@@ -89,15 +96,25 @@ export class API {
89
96
  }
90
97
  _token = "";
91
98
  /** The key that allows you to talk with the API */
92
- get token() { return this._token; }
93
- set token(token) { this._token = token; }
99
+ get token() {
100
+ return this._token;
101
+ }
102
+ set token(token) {
103
+ this._token = token;
104
+ }
94
105
  _token_type = "Bearer";
95
106
  /** Should always be "Bearer" */
96
- get token_type() { return this._token_type; }
97
- set token_type(token) { this._token_type = token; }
107
+ get token_type() {
108
+ return this._token_type;
109
+ }
110
+ set token_type(token) {
111
+ this._token_type = token;
112
+ }
98
113
  _expires = new Date(new Date().getTime() + 24 * 60 * 60 * 1000); // 24 hours default, is set through setNewToken anyway
99
114
  /** The expiration date of your token */
100
- get expires() { return this._expires; }
115
+ get expires() {
116
+ return this._expires;
117
+ }
101
118
  set expires(date) {
102
119
  this._expires = date;
103
120
  this.updateTokenTimer();
@@ -105,18 +122,30 @@ export class API {
105
122
  // CLIENT INFO
106
123
  _server = "";
107
124
  /** 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; }
125
+ get server() {
126
+ return this._server;
127
+ }
128
+ set server(server) {
129
+ this._server = server;
130
+ }
110
131
  _username = "";
111
132
  /** The username of the account */
112
- get username() { return this._username; }
113
- set username(username) { this._username = username; }
133
+ get username() {
134
+ return this._username;
135
+ }
136
+ set username(username) {
137
+ this._username = username;
138
+ }
114
139
  _password = "";
115
140
  /** The password of the account */
116
- get password() { return this._password; }
117
- set password(password) { this._password = password; }
141
+ get password() {
142
+ return this._password;
143
+ }
144
+ set password(password) {
145
+ this._password = password;
146
+ }
118
147
  _headers = {
119
- "Accept": "application/json",
148
+ Accept: "application/json",
120
149
  "Accept-Encoding": "gzip",
121
150
  "Content-Type": "application/json",
122
151
  "User-Agent": "umami-api-js (codeberg.org/Taevas/umami-api-js)",
@@ -125,8 +154,12 @@ export class API {
125
154
  * Used in practically all requests, those are all the headers the package uses excluding `Authorization`, the one with the token
126
155
  * @remarks If the User-Agent is not liked by https://isbot.js.org/, Umami might give responses akin to `{beep: "boop"}`
127
156
  */
128
- get headers() { return this._headers; }
129
- set headers(headers) { this._headers = headers; }
157
+ get headers() {
158
+ return this._headers;
159
+ }
160
+ set headers(headers) {
161
+ this._headers = headers;
162
+ }
130
163
  _user = {
131
164
  id: "",
132
165
  username: "",
@@ -135,14 +168,18 @@ export class API {
135
168
  isAdmin: false,
136
169
  };
137
170
  /** Information about the account that has been used to log in */
138
- get user() { return this._user; }
139
- set user(user) { this._user = user; }
171
+ get user() {
172
+ return this._user;
173
+ }
174
+ set user(user) {
175
+ this._user = user;
176
+ }
140
177
  number_of_requests = 0;
141
178
  // TOKEN HANDLING
142
179
  /** Has {@link API.setNewToken} been called and not yet returned anything? */
143
180
  is_setting_token = false;
144
181
  /** 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);
182
+ token_promise = new Promise((r) => r);
146
183
  /**
147
184
  * This contacts the server in order to get and set a new {@link API.token}!
148
185
  * @remarks The API object requires a {@link API.username} and a {@link API.password} to successfully get any token
@@ -158,8 +195,7 @@ export class API {
158
195
  this.token_promise = new Promise((resolve, reject) => {
159
196
  this.fetch(true, "post", ["auth", "login"], body)
160
197
  .then((response) => {
161
- response.json()
162
- .then((json) => {
198
+ response.json().then((json) => {
163
199
  if (!json.token) {
164
200
  const error_message = json.error_description ?? json.message ?? "No token obtained"; // Expect "Client authentication failed"
165
201
  this.log(true, "Unable to obtain a token! Here's what was received from the API:", json);
@@ -184,24 +220,36 @@ export class API {
184
220
  }
185
221
  _set_token_on_creation = true;
186
222
  /** 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; }
223
+ get set_token_on_creation() {
224
+ return this._set_token_on_creation;
225
+ }
226
+ set set_token_on_creation(bool) {
227
+ this._set_token_on_creation = bool;
228
+ }
189
229
  _set_token_on_401 = true;
190
230
  /** 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; }
231
+ get set_token_on_401() {
232
+ return this._set_token_on_401;
233
+ }
234
+ set set_token_on_401(bool) {
235
+ this._set_token_on_401 = bool;
236
+ }
193
237
  _set_token_on_expires = false;
194
238
  /**
195
239
  * If true, the application will silently call {@link API.setNewToken} when the {@link API.token} is set to expire,
196
240
  * as determined by {@link API.expires} (defaults to **false**)
197
241
  */
198
- get set_token_on_expires() { return this._set_token_on_expires; }
242
+ get set_token_on_expires() {
243
+ return this._set_token_on_expires;
244
+ }
199
245
  set set_token_on_expires(enabled) {
200
246
  this._set_token_on_expires = enabled;
201
247
  this.updateTokenTimer();
202
248
  }
203
249
  _token_timer;
204
- get token_timer() { return this._token_timer; }
250
+ get token_timer() {
251
+ return this._token_timer;
252
+ }
205
253
  set token_timer(timer) {
206
254
  // if a previous one already exists, clear it
207
255
  if (this._token_timer) {
@@ -238,43 +286,75 @@ export class API {
238
286
  // CLIENT CONFIGURATION
239
287
  _verbose = "none";
240
288
  /** Which events should be logged (defaults to **none**) */
241
- get verbose() { return this._verbose; }
242
- set verbose(verbose) { this._verbose = verbose; }
289
+ get verbose() {
290
+ return this._verbose;
291
+ }
292
+ set verbose(verbose) {
293
+ this._verbose = verbose;
294
+ }
243
295
  _timeout = 20;
244
296
  /**
245
297
  * The maximum **amount of seconds** requests should take before returning an answer (defaults to **20**)
246
298
  * @remarks 0 means no maximum, no timeout
247
299
  */
248
- get timeout() { return this._timeout; }
249
- set timeout(timeout) { this._timeout = timeout; }
300
+ get timeout() {
301
+ return this._timeout;
302
+ }
303
+ set timeout(timeout) {
304
+ this._timeout = timeout;
305
+ }
250
306
  _signal;
251
307
  /** The `AbortSignal` used in every request */
252
- get signal() { return this._signal; }
253
- set signal(signal) { this._signal = signal; }
308
+ get signal() {
309
+ return this._signal;
310
+ }
311
+ set signal(signal) {
312
+ this._signal = signal;
313
+ }
254
314
  // RETRIES
255
315
  _retry_maximum_amount = 4;
256
316
  /**
257
317
  * How many retries maximum before throwing an {@link APIError} (defaults to **4**)
258
318
  * @remarks Pro tip: Set that to 0 to **completely** disable retries!
259
319
  */
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; }
320
+ get retry_maximum_amount() {
321
+ return this._retry_maximum_amount;
322
+ }
323
+ set retry_maximum_amount(retry_maximum_amount) {
324
+ this._retry_maximum_amount = retry_maximum_amount;
325
+ }
262
326
  _retry_delay = 2;
263
327
  /** 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; }
328
+ get retry_delay() {
329
+ return this._retry_delay;
330
+ }
331
+ set retry_delay(retry_delay) {
332
+ this._retry_delay = retry_delay;
333
+ }
266
334
  _retry_on_new_token = true;
267
335
  /** 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; }
336
+ get retry_on_new_token() {
337
+ return this._retry_on_new_token;
338
+ }
339
+ set retry_on_new_token(retry_on_new_token) {
340
+ this._retry_on_new_token = retry_on_new_token;
341
+ }
270
342
  _retry_on_status_codes = [429];
271
343
  /** 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; }
344
+ get retry_on_status_codes() {
345
+ return this._retry_on_status_codes;
346
+ }
347
+ set retry_on_status_codes(retry_on_status_codes) {
348
+ this._retry_on_status_codes = retry_on_status_codes;
349
+ }
274
350
  _retry_on_timeout = false;
275
351
  /** 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; }
352
+ get retry_on_timeout() {
353
+ return this._retry_on_timeout;
354
+ }
355
+ set retry_on_timeout(retry_on_timeout) {
356
+ this._retry_on_timeout = retry_on_timeout;
357
+ }
278
358
  // OTHER METHODS
279
359
  /**
280
360
  * Use this instead of `console.log` to log any information
@@ -298,27 +378,43 @@ export class API {
298
378
  * @param info Relevant only when `fetch` calls itself, avoid setting it
299
379
  * @remarks Consider using the higher-level method called {@link API.request}
300
380
  */
301
- async fetch(is_token_related, method, endpoint, parameters = {}, info = { number_try: 1, has_new_token: false }) {
381
+ async fetch(is_token_related, method, endpoint, parameters = {}, info = {
382
+ number_try: 1,
383
+ has_new_token: false,
384
+ }) {
302
385
  let to_retry = false;
303
386
  let error_object;
304
- let error_code;
305
387
  let error_message = "no error message available";
306
388
  if (!is_token_related)
307
- await this.token_promise.catch(() => this.token_promise = new Promise(r => r));
389
+ await this.token_promise.catch(() => (this.token_promise = new Promise((r) => r)));
308
390
  for (const [p, v] of Object.entries(parameters)) {
309
- parameters[p] = v instanceof Date ? Number(v) : v; // Convert Dates to ms
391
+ // Convert Dates to ms
392
+ if (typeof v === "object" && !Array.isArray(v) && v !== null) {
393
+ // It's frankly unnecessary to have better-written code
394
+ for (const [p2, v2] of Object.entries(v)) {
395
+ parameters[p][p2] = v2 instanceof Date ? Number(v2) : v2;
396
+ }
397
+ }
398
+ parameters[p] = v instanceof Date ? Number(v) : v;
310
399
  }
311
400
  let url = `${this.server}`;
312
401
  if (url.slice(-1) !== "/")
313
402
  url += "/";
314
403
  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("&"));
404
+ if (method === "get" && parameters) {
405
+ // for GET requests specifically, requests need to be shaped in very particular ways
406
+ url +=
407
+ "?" +
408
+ Object.entries(adaptParametersForGETRequests(parameters))
409
+ .map((param) => {
410
+ if (!Array.isArray(param[1])) {
411
+ return `${param[0]}=${param[1]}`;
412
+ }
413
+ return param[1]
414
+ .map((array_element) => `${param[0]}=${array_element}`)
415
+ .join("&");
416
+ })
417
+ .join("&");
322
418
  }
323
419
  const signals = [];
324
420
  if (this.timeout > 0)
@@ -328,11 +424,13 @@ export class API {
328
424
  const response = await fetch(url, {
329
425
  method,
330
426
  headers: {
331
- "Authorization": is_token_related ? undefined : `${this.token_type} ${this.token}`,
332
- ...this.headers
427
+ Authorization: is_token_related
428
+ ? undefined
429
+ : `${this.token_type} ${this.token}`,
430
+ ...this.headers,
333
431
  },
334
432
  body: method !== "get" ? JSON.stringify(parameters) : undefined, // parameters are here if method is NOT GET
335
- signal: AbortSignal.any(signals)
433
+ signal: AbortSignal.any(signals),
336
434
  })
337
435
  .catch((error) => {
338
436
  if (error.name === "TimeoutError" && this.retry_on_timeout)
@@ -341,14 +439,13 @@ export class API {
341
439
  error_object = error;
342
440
  error_message = `${error.name} (${error.message ?? error.errno ?? error.type})`;
343
441
  })
344
- .finally(() => this.number_of_requests += 1);
442
+ .finally(() => (this.number_of_requests += 1));
345
443
  const request_id = `(${String(this.number_of_requests).padStart(8, "0")})`;
346
444
  if (response) {
347
445
  if (parameters.password)
348
446
  parameters.password = "<REDACTED>";
349
447
  this.log(this.verbose !== "none" && !response.ok, response.statusText, response.status, { method, endpoint, parameters }, request_id);
350
448
  if (!response.ok) {
351
- error_code = response.status;
352
449
  error_message = response.statusText;
353
450
  if (this.retry_on_status_codes.includes(response.status))
354
451
  to_retry = true;
@@ -357,7 +454,7 @@ export class API {
357
454
  if (this.set_token_on_401 && !info.has_new_token) {
358
455
  if (!this.is_setting_token) {
359
456
  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) {
457
+ if ((await this.setNewToken()) && this.retry_on_new_token) {
361
458
  to_retry = true;
362
459
  info.has_new_token = true;
363
460
  }
@@ -376,14 +473,19 @@ export class API {
376
473
  }
377
474
  if (to_retry === true && info.number_try <= this.retry_maximum_amount) {
378
475
  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 });
476
+ await new Promise((res) => setTimeout(res, this.retry_delay * 1000));
477
+ return await this.fetch(is_token_related, method, endpoint, parameters, {
478
+ number_try: info.number_try + 1,
479
+ has_new_token: info.has_new_token,
480
+ });
381
481
  }
382
482
  if (!response || !response.ok) {
383
- const resp = response ? {
384
- status_code: response.status,
385
- json: await response.json()
386
- } : undefined;
483
+ const resp = response
484
+ ? {
485
+ status_code: response.status,
486
+ json: await response.json(),
487
+ }
488
+ : undefined;
387
489
  throw new APIError(error_message, this.server, method, endpoint, parameters, resp, error_object);
388
490
  }
389
491
  return response;
@@ -402,11 +504,13 @@ export class API {
402
504
  return undefined; // 204 means the request worked as intended and did not give us anything, so just return nothing
403
505
  const arrBuff = await response.arrayBuffer();
404
506
  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
507
+ try {
508
+ // Assume the response is in JSON format as it often is, it'll fail into the catch block if it isn't anyway
406
509
  // My thorough testing leads me to believe nothing would change if the encoding was also "binary" here btw
407
510
  return correctType(JSON.parse(buff.toString("utf-8")));
408
511
  }
409
- catch { // Assume the response is supposed to not be in JSON format so return it as simple text
512
+ catch {
513
+ // Assume the response is supposed to not be in JSON format so return it as simple text
410
514
  return buff.toString("binary");
411
515
  }
412
516
  }
@@ -424,7 +528,10 @@ export class API {
424
528
  * @group Sending stats
425
529
  */
426
530
  async sendStats(websiteId, payload, type = "event") {
427
- return await this.request("post", ["send"], { payload: { website: websiteId, ...payload }, type });
531
+ return await this.request("post", ["send"], {
532
+ payload: { website: websiteId, ...payload },
533
+ type,
534
+ });
428
535
  }
429
536
  // ADMIN
430
537
  /** @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.0",
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
  },
@@ -34,6 +35,7 @@
34
35
  "ajv": "^8.17.1",
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
40
  "typedoc": "^0.28.16",
39
41
  "typescript": "^5.9.3"