quickbase-js 0.1.0-beta.10 → 0.1.0-beta.11

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/CHANGELOG.md CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ## [0.1.0-beta.11](https://github.com/DrewBradfordXYZ/quickbase-js/compare/v0.1.0-beta.10...v0.1.0-beta.11) (2025-03-23)
6
+
7
+
8
+ ### Features
9
+
10
+ * only fetch auth dbid once for concurrent requests. temp tokens and SSO ([12f7f31](https://github.com/DrewBradfordXYZ/quickbase-js/commit/12f7f3117b2fd6e5db35d73e945dff0a79ef0481))
11
+ * SSO concurrency tests passes ([caf5fca](https://github.com/DrewBradfordXYZ/quickbase-js/commit/caf5fca4e86b0dda4a75c2856f9324634369f15a))
12
+ * temp token concurrency prevents redundant token fetches. getAppConcurrency.test.ts unit test passes ([2913e7b](https://github.com/DrewBradfordXYZ/quickbase-js/commit/2913e7b38d38ca0ceaa0af21b2e872f7bafb9d99))
13
+
5
14
  ## [0.1.0-beta.10](https://github.com/DrewBradfordXYZ/quickbase-js/compare/v0.1.0-beta.9...v0.1.0-beta.10) (2025-03-22)
6
15
 
7
16
 
package/README.md CHANGED
@@ -1,22 +1,60 @@
1
1
  # quickbase-js
2
2
 
3
- A Typescript library using the QuickBase OpenAPI spec and the typescipt-fetch generator. This library is designed for both browser and Node.js environments. Code pages have first class support.
3
+ A Typescript API client for the [Quickbase JSON RESTful API](https://developer.quickbase.com/).
4
4
 
5
- Authentication methods: `Temporary Tokens`, `User Tokens`, `SSO`.
5
+ Support for various authentication strategies (`user token`, `temporary tokens`, `SSO token`), `rate limiting`, `429` and `401` error handling retries and automatic `date` conversion. It uses a Proxy to dynamically invoke API methods derived from the OpenAPI specification.
6
+
7
+ Browser and Node.js support. ESM and UMD builds are available.
8
+
9
+ Authentication methods are handled in an opinionated manner. See configuration options for details and available settings.
6
10
 
7
11
  ## API Documentation
8
12
 
9
- https://quickbase-js.netlify.app/
13
+ <a href="https://quickbase-js.netlify.app/" target="_blank"> Documentation</a> for quickbase-js.
10
14
 
11
15
  ## Installation
12
16
 
17
+ #### Node.js install
18
+
13
19
  ```bash
14
20
  npm install --save quickbase-js
15
21
  ```
16
22
 
17
- ## Examples
23
+ #### CDN install.
24
+
25
+ Find and select the latest version. UMD is recommended for code pages.
26
+
27
+ <a href="https://www.jsdelivr.com/package/npm/quickbase-js?tab=files" target="_blank">CDN Files</a> for quickbase-js
28
+
29
+ ```html
30
+ <!DOCTYPE html>
31
+ <html>
32
+ <head>
33
+ <title>Quickbase</title>
34
+ <!-- Replace xx.xx.xx with the latest version from CDN Files-->
35
+ <script src="https://cdn.jsdelivr.net/npm/quickbase-js@xx.xx.xx/dist/umd/quickbase.umd.min.js"></script>
36
+ </head>
37
+ <body>
38
+ <script>
39
+ const realm = ""; // Replace with your realm
40
+ const appId = ""; // Replace with your app ID
41
+
42
+ const qb = QuickbaseJS.quickbase({
43
+ realm,
44
+ useTempTokens: true,
45
+ });
46
+
47
+ qb.getApp({ appId })
48
+ .then((app) => console.log(app.name))
49
+ .catch((err) => console.log(err.message));
50
+ </script>
51
+ </body>
52
+ </html>
53
+ ```
54
+
55
+ ## Authentication Examples
18
56
 
19
- ### Auth: User Token
57
+ ### User Token
20
58
 
21
59
  ```javascript
22
60
  import { quickbase } from "quickbase-js"; // Adjust the import path as needed
@@ -45,7 +83,7 @@ const getAppDetails = async () => {
45
83
  getAppDetails();
46
84
  ```
47
85
 
48
- ### Auth: Temporary Tokens (Code Pages)
86
+ ### Temporary Tokens (Code Pages)
49
87
 
50
88
  ```javascript
51
89
  import { quickbase } from "quickbase-js"; // Adjust the import path as needed
@@ -73,7 +111,7 @@ const getAppDetails = async () => {
73
111
  getAppDetails();
74
112
  ```
75
113
 
76
- ### Auth: SSO
114
+ ### SSO Token
77
115
 
78
116
  ```javascript
79
117
  import { quickbase } from "quickbase-js"; // Adjust the import path as needed
@@ -117,14 +155,14 @@ _User options:_
117
155
 
118
156
  _Advanced user options:_
119
157
 
120
- - **`tempTokenLifespan`** (`number`, optional): The lifespan (in milliseconds) of temporary tokens in the cache. Defaults to 4 minutes 50 seconds (290000 ms).
121
-
122
158
  - **`throttle`** (`{ rate: number; burst: number }`, optional): Configures rate limiting, where `rate` is requests per second and `burst` is the maximum burst of requests. Defaults to `{ rate: 10, burst: 10 }`.
123
159
 
124
160
  - **`maxRetries`** (`number`, optional): The maximum number of retries for failed requests. Defaults to `3`.
125
161
 
126
162
  - **`retryDelay`** (`number`, optional): The base delay (in milliseconds) between retries, which increases exponentially. Defaults to `1000`.
127
163
 
164
+ - **`tempTokenLifespan`** (`number`, optional): The lifespan (in milliseconds) of temporary tokens in the cache. Defaults to 4 minutes 50 seconds (290000 ms).
165
+
128
166
  - **`convertDates`** (`boolean`, optional): Converts ISO date strings to `Date` objects in responses. Defaults to `true`.
129
167
 
130
168
  _Overrides and development options:_
@@ -149,6 +187,8 @@ _Overrides and development options:_
149
187
 
150
188
  - **`tokenCache`**: Temporary tokens are cached with their dbid with a `tempTokenLifespan`. This is automatically setup for you when `useTempTokens` is enabled.
151
189
 
190
+ ---
191
+
152
192
  ### Development workflow
153
193
 
154
194
  ```bash
@@ -8051,6 +8051,7 @@ declare class TokenCache {
8051
8051
  constructor(tempTokenLifespan?: number);
8052
8052
  get(dbid: string): string | undefined;
8053
8053
  set(dbid: string, token: string, lifespan?: number): void;
8054
+ delete(dbid: string): void;
8054
8055
  clear(): void;
8055
8056
  }
8056
8057
 
@@ -7348,6 +7348,9 @@ class TokenCache {
7348
7348
  expiresAt: now + (lifespan || this.tempTokenLifespan),
7349
7349
  });
7350
7350
  }
7351
+ delete(dbid) {
7352
+ this.cache.delete(dbid);
7353
+ }
7351
7354
  clear() {
7352
7355
  this.cache.clear();
7353
7356
  }
@@ -7402,14 +7405,13 @@ function extractHttpMethod(method) {
7402
7405
  return match ? match[1].toUpperCase() : "GET"; // Fallback to GET if no match
7403
7406
  }
7404
7407
 
7405
- // src/authorizationStrategy.ts
7406
- // src/authorizationStrategy.ts (partial update)
7407
7408
  class TempTokenStrategy {
7408
7409
  tokenCache;
7409
7410
  initialTempToken;
7410
7411
  fetchApi;
7411
7412
  realm;
7412
7413
  baseUrl;
7414
+ pendingFetches = new Map();
7413
7415
  constructor(tokenCache, initialTempToken, fetchApi, realm, baseUrl = "https://api.quickbase.com/v1") {
7414
7416
  this.tokenCache = tokenCache;
7415
7417
  this.initialTempToken = initialTempToken;
@@ -7444,7 +7446,13 @@ class TempTokenStrategy {
7444
7446
  async getToken(dbid) {
7445
7447
  let token = this.tokenCache.get(dbid) || this.initialTempToken;
7446
7448
  if (!token && dbid) {
7447
- token = await this.fetchTempToken(dbid);
7449
+ if (this.pendingFetches.has(dbid)) {
7450
+ console.log(`[getToken] Waiting for existing fetch for dbid: ${dbid}`);
7451
+ return this.pendingFetches.get(dbid);
7452
+ }
7453
+ const fetchPromise = this.fetchTempToken(dbid).finally(() => this.pendingFetches.delete(dbid));
7454
+ this.pendingFetches.set(dbid, fetchPromise);
7455
+ token = await fetchPromise;
7448
7456
  }
7449
7457
  return token;
7450
7458
  }
@@ -7464,15 +7472,29 @@ class TempTokenStrategy {
7464
7472
  }
7465
7473
  if (debug)
7466
7474
  console.log(`Refreshing temp token for dbid: ${dbid}`);
7467
- const newToken = await this.fetchTempToken(dbid);
7468
- this.tokenCache.set(dbid, newToken);
7469
- return newToken;
7475
+ // Invalidate the cache for this dbid to force a fresh fetch
7476
+ this.tokenCache.delete(dbid);
7477
+ try {
7478
+ const newToken = await this.getToken(dbid);
7479
+ if (newToken) {
7480
+ this.tokenCache.set(dbid, newToken);
7481
+ return newToken;
7482
+ }
7483
+ return null;
7484
+ }
7485
+ catch (error) {
7486
+ if (debug)
7487
+ console.log(`[${methodName || "method"}] Failed to refresh token:`, error);
7488
+ throw error;
7489
+ }
7470
7490
  }
7471
7491
  }
7472
7492
  class UserTokenStrategy {
7473
7493
  userToken;
7474
- constructor(userToken) {
7494
+ baseUrl;
7495
+ constructor(userToken, baseUrl = "https://api.quickbase.com/v1") {
7475
7496
  this.userToken = userToken;
7497
+ this.baseUrl = baseUrl;
7476
7498
  }
7477
7499
  async getToken(_dbid) {
7478
7500
  return this.userToken;
@@ -7493,21 +7515,31 @@ class SsoTokenStrategy {
7493
7515
  realm;
7494
7516
  fetchApi;
7495
7517
  debug;
7518
+ baseUrl;
7496
7519
  currentToken;
7497
- constructor(samlToken, realm, fetchApi, debug = false) {
7520
+ pendingFetches = new Map();
7521
+ constructor(samlToken, realm, fetchApi, debug = false, baseUrl = "https://api.quickbase.com/v1") {
7498
7522
  this.samlToken = samlToken;
7499
7523
  this.realm = realm;
7500
7524
  this.fetchApi = fetchApi;
7501
7525
  this.debug = debug;
7526
+ this.baseUrl = baseUrl;
7502
7527
  }
7503
7528
  async getToken(_dbid) {
7504
7529
  if (!this.currentToken) {
7505
- this.currentToken = await this.fetchSsoToken({
7530
+ if (this.pendingFetches.has("sso")) {
7531
+ if (this.debug)
7532
+ console.log("[getToken] Waiting for existing SSO fetch");
7533
+ return this.pendingFetches.get("sso");
7534
+ }
7535
+ const fetchPromise = this.fetchSsoToken({
7506
7536
  grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
7507
7537
  requested_token_type: "urn:quickbase:params:oauth:token-type:temp_token",
7508
7538
  subject_token: this.samlToken,
7509
7539
  subject_token_type: "urn:ietf:params:oauth:token-type:saml2",
7510
- });
7540
+ }).finally(() => this.pendingFetches.delete("sso"));
7541
+ this.pendingFetches.set("sso", fetchPromise);
7542
+ this.currentToken = await fetchPromise;
7511
7543
  }
7512
7544
  return this.currentToken;
7513
7545
  }
@@ -7534,7 +7566,7 @@ class SsoTokenStrategy {
7534
7566
  subject_token: this.samlToken,
7535
7567
  subject_token_type: "urn:ietf:params:oauth:token-type:saml2",
7536
7568
  };
7537
- const response = await this.fetchApi(`https://api.quickbase.com/v1/auth/oauth/token`, {
7569
+ const response = await this.fetchApi(`${this.baseUrl}/auth/oauth/token`, {
7538
7570
  method: "POST",
7539
7571
  headers: {
7540
7572
  "QB-Realm-Hostname": `${this.realm}.quickbase.com`,
@@ -7561,7 +7593,7 @@ class SsoTokenStrategy {
7561
7593
  return newToken;
7562
7594
  }
7563
7595
  async fetchSsoToken(params) {
7564
- const response = await this.fetchApi(`https://api.quickbase.com/v1/auth/oauth/token`, {
7596
+ const response = await this.fetchApi(`${this.baseUrl}/auth/oauth/token`, {
7565
7597
  method: "POST",
7566
7598
  headers: {
7567
7599
  "QB-Realm-Hostname": `${this.realm}.quickbase.com`,
@@ -7609,8 +7641,7 @@ class RateLimitError extends Error {
7609
7641
  }
7610
7642
 
7611
7643
  // src/invokeMethod.ts
7612
- async function invokeMethod(methodName, params, methodMap, baseHeaders, authStrategy, rateLimiter, transformDates, debug, convertDates) {
7613
- console.log("[invokeMethod] Starting for method:", methodName);
7644
+ async function invokeMethod(methodName, params, methodMap, baseHeaders, authStrategy, rateLimiter, transformDates, debug, convertDates, attempt = 0, maxAttempts = rateLimiter.maxRetries + 1) {
7614
7645
  const methodInfo = methodMap[methodName];
7615
7646
  if (!methodInfo)
7616
7647
  throw new Error(`Method ${methodName} not found`);
@@ -7628,33 +7659,21 @@ async function invokeMethod(methodName, params, methodMap, baseHeaders, authStra
7628
7659
  method: methodInfo.httpMethod,
7629
7660
  };
7630
7661
  let dbid = extractDbid(params);
7631
- console.log("[invokeMethod] Extracted dbid:", dbid);
7632
7662
  let token = dbid
7633
7663
  ? await authStrategy.getToken(dbid)
7634
7664
  : await authStrategy.getToken("");
7635
- console.log("[invokeMethod] Initial token:", token);
7636
7665
  if (token) {
7637
7666
  authStrategy.applyHeaders(baseHeaders, token);
7638
7667
  requestOptions.headers = { ...baseHeaders };
7639
- console.log("[invokeMethod] Token found, headers set:", requestOptions.headers);
7640
7668
  if (methodName === "getTempTokenDBID") {
7641
- console.log("[invokeMethod] Using cached token:", token);
7642
7669
  return { temporaryAuthorization: token };
7643
7670
  }
7644
7671
  }
7645
- if (debug) {
7646
- console.log(`[${methodName}] requestParameters:`, requestParameters);
7647
- console.log(`[${methodName}] requestOptions:`, requestOptions);
7648
- }
7649
7672
  async function processResponse(rawResponse) {
7650
- if (debug)
7651
- console.log(`[${methodName}] rawResponse:`, rawResponse);
7652
7673
  if (rawResponse instanceof Response) {
7653
7674
  const contentType = rawResponse.headers
7654
7675
  .get("Content-Type")
7655
7676
  ?.toLowerCase();
7656
- if (debug)
7657
- console.log(`[${methodName}] contentType:`, contentType);
7658
7677
  if (contentType?.includes("application/json")) {
7659
7678
  const jsonResponse = await rawResponse.json();
7660
7679
  return transformDates(jsonResponse, convertDates);
@@ -7663,8 +7682,6 @@ async function invokeMethod(methodName, params, methodMap, baseHeaders, authStra
7663
7682
  }
7664
7683
  if (rawResponse && typeof rawResponse.value === "function") {
7665
7684
  const response = await rawResponse.value();
7666
- if (debug)
7667
- console.log(`[${methodName}] Resolved JSONApiResponse:`, response);
7668
7685
  return transformDates(response, convertDates);
7669
7686
  }
7670
7687
  return transformDates(rawResponse, convertDates);
@@ -7692,28 +7709,17 @@ async function invokeMethod(methodName, params, methodMap, baseHeaders, authStra
7692
7709
  }
7693
7710
  }
7694
7711
  catch (e) {
7695
- if (debug)
7696
- console.log(`[${methodName}] Error parsing response body:`, e);
7697
7712
  message = "Failed to parse error response";
7698
7713
  }
7699
- if (debug)
7700
- console.log("[invokeMethod] Parsed error - status:", status, "message:", message);
7701
7714
  return { message, status };
7702
7715
  }
7703
- let attempt = 0;
7704
- const maxAttempts = rateLimiter.maxRetries + 1;
7705
7716
  while (attempt < maxAttempts) {
7706
- console.log("[invokeMethod] Attempt:", attempt + 1, "of", maxAttempts);
7707
7717
  try {
7708
7718
  await rateLimiter.throttle();
7709
- console.log("[invokeMethod] About to call API for method:", methodName);
7710
7719
  const response = await methodInfo.method(requestParameters, requestOptions);
7711
- console.log("[invokeMethod] API call completed for method:", methodName);
7712
7720
  return await processResponse(response);
7713
7721
  }
7714
7722
  catch (error) {
7715
- if (debug)
7716
- console.log("[invokeMethod] Caught error:", error);
7717
7723
  let status;
7718
7724
  let message;
7719
7725
  let response;
@@ -7726,31 +7732,13 @@ async function invokeMethod(methodName, params, methodMap, baseHeaders, authStra
7726
7732
  ({ message, status } = await parseErrorResponse(response));
7727
7733
  }
7728
7734
  else {
7729
- if (debug)
7730
- console.log(`[${methodName}] Unexpected error:`, error);
7731
- // Attempt to handle as a fetch error with a response
7732
- if (error instanceof Error && "response" in error) {
7733
- response = error.response;
7734
- if (response instanceof Response) {
7735
- ({ message, status } = await parseErrorResponse(response));
7736
- }
7737
- else {
7738
- throw error; // Rethrow if no response
7739
- }
7740
- }
7741
- else {
7742
- throw error; // Rethrow truly unexpected errors
7743
- }
7735
+ throw error; // Rethrow truly unexpected errors
7744
7736
  }
7745
- if (debug)
7746
- console.log("[invokeMethod] Handling error with status:", status);
7747
7737
  if (status === 429) {
7748
7738
  if (!(error instanceof ResponseError)) {
7749
7739
  throw new Error("Expected ResponseError for 429 handling");
7750
7740
  }
7751
7741
  const delay = await rateLimiter.handle429(error, attempt + 1);
7752
- if (debug)
7753
- console.log(`[${methodName}] 429 delay: ${delay}ms`);
7754
7742
  if (attempt + 1 === maxAttempts) {
7755
7743
  throw new RateLimitError(`API Error: ${message} (Status: ${status})`, status, response?.headers.get("Retry-After")
7756
7744
  ? parseInt(response.headers.get("Retry-After"), 10)
@@ -7760,16 +7748,25 @@ async function invokeMethod(methodName, params, methodMap, baseHeaders, authStra
7760
7748
  attempt++;
7761
7749
  continue;
7762
7750
  }
7763
- const newToken = await authStrategy.handleError(status, params, attempt, maxAttempts, debug, methodName);
7764
- if (newToken) {
7751
+ // Handle authentication errors
7752
+ let newToken;
7753
+ try {
7754
+ newToken = await authStrategy.handleError(status, params, attempt, maxAttempts, debug, methodName);
7755
+ }
7756
+ catch (authError) {
7757
+ throw authError; // Propagate the fetchTempToken error and exit immediately
7758
+ }
7759
+ // Only proceed with retry if we have a valid new token
7760
+ if (newToken !== null) {
7765
7761
  token = newToken;
7766
7762
  authStrategy.applyHeaders(baseHeaders, token);
7767
7763
  requestOptions.headers = { ...baseHeaders };
7768
- attempt++;
7769
7764
  if (debug)
7770
7765
  console.log(`[${methodName}] Retrying with token: ${token.substring(0, 10)}...`);
7766
+ attempt++;
7771
7767
  continue;
7772
7768
  }
7769
+ // If no new token was provided, throw the original error
7773
7770
  throw new Error(`API Error: ${message} (Status: ${status})`);
7774
7771
  }
7775
7772
  }
@@ -7838,7 +7835,6 @@ class RateLimiter {
7838
7835
  }
7839
7836
  }
7840
7837
 
7841
- // src/quickbaseClient.ts
7842
7838
  function quickbase(config) {
7843
7839
  const { realm, userToken, tempToken, useTempTokens, useSso, samlToken, fetchApi, debug, convertDates = true, tempTokenLifespan = 290000, throttle = { rate: 10, burst: 10 }, maxRetries = 3, retryDelay = 1000, tokenCache: providedTokenCache, baseUrl = "https://api.quickbase.com/v1", } = config;
7844
7840
  const tokenCache = providedTokenCache || new TokenCache(tempTokenLifespan);
@@ -7854,10 +7850,10 @@ function quickbase(config) {
7854
7850
  throw new Error("No fetch implementation available");
7855
7851
  }
7856
7852
  const authStrategy = useSso
7857
- ? new SsoTokenStrategy(samlToken || "", realm, effectiveFetch, debug)
7853
+ ? new SsoTokenStrategy(samlToken || "", realm, effectiveFetch, debug, baseUrl)
7858
7854
  : useTempTokens
7859
7855
  ? new TempTokenStrategy(tokenCache, tempToken, effectiveFetch, realm, baseUrl)
7860
- : new UserTokenStrategy(userToken || "");
7856
+ : new UserTokenStrategy(userToken || "", baseUrl);
7861
7857
  const baseHeaders = {
7862
7858
  "QB-Realm-Hostname": `${realm}.quickbase.com`,
7863
7859
  "Content-Type": "application/json",