quickbase-js 0.1.0-beta.10 → 0.1.0-beta.12
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 +16 -0
- package/README.md +56 -24
- package/dist/esm/quickbase.d.ts +1 -0
- package/dist/esm/quickbase.js +125 -87
- package/dist/esm/quickbase.js.map +1 -1
- package/dist/esm/quickbase.min.js +1 -1
- package/dist/esm/quickbase.min.js.map +1 -1
- package/dist/umd/quickbase.umd.js +125 -87
- package/dist/umd/quickbase.umd.js.map +1 -1
- package/dist/umd/quickbase.umd.min.js +1 -1
- package/dist/umd/quickbase.umd.min.js.map +1 -1
- package/examples/quicbase-jsCDN.html +2 -2
- package/package.json +1 -1
- package/src/ThrottleBucket.ts +52 -16
- package/src/authorizationStrategy.ts +62 -35
- package/src/generated-unified/QuickbaseClient.ts +1 -1
- package/src/invokeMethod.ts +33 -66
- package/src/quickbaseClient.ts +12 -7
- package/src/rateLimiter.ts +8 -3
- package/src/tokenCache.ts +4 -0
- package/tests/playwright/qb/auth/getAppWithTempTokenConcurrency.test.ts +208 -0
- package/tests/vitest/qb/ThrottleBucket.test.ts +77 -0
- package/tests/vitest/qb/auth/ssoTokenConcurrency.test.ts +68 -0
- package/tests/vitest/qb/auth/userTokenConcurrency.test.ts +63 -0
- package/tests/vitest/qb/fields/getFieldsUsage.test.ts +2 -2
- package/tests/vitest/unit/ThrottleBucket.test.ts +16 -10
- package/tests/vitest/unit/apps/getAppConcurrency.test.ts +151 -0
- package/tests/vitest/unit/auth/fetchTempToken401.test.ts +20 -2
- package/tests/vitest/unit/auth/userTokenConcurrency.test.ts +62 -0
- package/tests/vitest/unit/index.ts +1 -1
- package/tests/vitest/unit/quickbaseClientHttpMethods.test.ts +3 -3
- package/tools/log-gen.ts +1 -1
- package/tools/log-gen.yaml +130 -78
- package/api-examples.md +0 -49
- package/markdown-examples.md +0 -85
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
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.12](https://github.com/DrewBradfordXYZ/quickbase-js/compare/v0.1.0-beta.11...v0.1.0-beta.12) (2025-03-23)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* enable true concurrency with throttle bucket ([8dc4dee](https://github.com/DrewBradfordXYZ/quickbase-js/commit/8dc4deeaddb4365f4f64a8bd07390c7071f739f7))
|
|
11
|
+
|
|
12
|
+
## [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)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* only fetch auth dbid once for concurrent requests. temp tokens and SSO ([12f7f31](https://github.com/DrewBradfordXYZ/quickbase-js/commit/12f7f3117b2fd6e5db35d73e945dff0a79ef0481))
|
|
18
|
+
* SSO concurrency tests passes ([caf5fca](https://github.com/DrewBradfordXYZ/quickbase-js/commit/caf5fca4e86b0dda4a75c2856f9324634369f15a))
|
|
19
|
+
* temp token concurrency prevents redundant token fetches. getAppConcurrency.test.ts unit test passes ([2913e7b](https://github.com/DrewBradfordXYZ/quickbase-js/commit/2913e7b38d38ca0ceaa0af21b2e872f7bafb9d99))
|
|
20
|
+
|
|
5
21
|
## [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
22
|
|
|
7
23
|
|
package/README.md
CHANGED
|
@@ -1,22 +1,60 @@
|
|
|
1
1
|
# quickbase-js
|
|
2
2
|
|
|
3
|
-
A Typescript
|
|
3
|
+
A Typescript API client for the [Quickbase JSON RESTful API](https://developer.quickbase.com/).
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Support for various authentication strategies (`user token`, `temporary tokens`, `SSO token`), `rate limiting`, `429`(rate limit) error and `401`(auth) 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
|
-
|
|
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
|
-
###
|
|
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
|
-
###
|
|
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
|
-
###
|
|
114
|
+
### SSO Token
|
|
77
115
|
|
|
78
116
|
```javascript
|
|
79
117
|
import { quickbase } from "quickbase-js"; // Adjust the import path as needed
|
|
@@ -117,37 +155,31 @@ _User options:_
|
|
|
117
155
|
|
|
118
156
|
_Advanced user options:_
|
|
119
157
|
|
|
120
|
-
- **`
|
|
121
|
-
|
|
122
|
-
- **`
|
|
123
|
-
|
|
158
|
+
- **`throttle`** (`{ rate: number; burst: number }`, optional): Configures rate limiting to manage how many API requests are sent at once and how quickly additional requests follow. Defaults to `{ rate: 5, burst: 3 }`.
|
|
159
|
+
- **`burst`**: The number of requests that can start immediately (concurrent calls). Default is `3`, meaning up to 3 requests run at the same time.
|
|
160
|
+
- **`rate`**: The number of requests allowed per second after the initial burst. Default is `5`, pacing extra requests at one every ~200ms.
|
|
161
|
+
- **How It Works**: Taking the default setting for example: requests in general take ~500ms to finish. Imagine having 6 requests, 3 start right away finishing at ~500ms. Because the rate is set to 200ms, 2 request tokens have built up during this time, so the next 2 requests are sent out immediately when the first 3 finnish, leaving 1 burst spot available. The third request waits the extra 100ms to be dispatched starting at ~600ms using up the remaining burst spot, which takes another ~500ms to finish. All requests finishing around ~1100ms total. This keeps you safely under Quickbase’s 10 requests/sec limit while maximizing speed. A more liberal setting of `{ rate: 10, burst: 5 }` allows 5 requests at once, with request tokens building up every ~100ms, finishing 6 requests in ~1000ms.
|
|
124
162
|
- **`maxRetries`** (`number`, optional): The maximum number of retries for failed requests. Defaults to `3`.
|
|
125
|
-
|
|
126
163
|
- **`retryDelay`** (`number`, optional): The base delay (in milliseconds) between retries, which increases exponentially. Defaults to `1000`.
|
|
127
|
-
|
|
164
|
+
- **`tempTokenLifespan`** (`number`, optional): The lifespan (in milliseconds) of temporary tokens in the cache. Defaults to 4 minutes 50 seconds (290000 ms).
|
|
128
165
|
- **`convertDates`** (`boolean`, optional): Converts ISO date strings to `Date` objects in responses. Defaults to `true`.
|
|
129
166
|
|
|
130
167
|
_Overrides and development options:_
|
|
131
168
|
|
|
132
|
-
- **`fetchApi`** (`typeof fetch`, optional): For browser environments, this defaults to the built-in `fetch`. In Node.js, you
|
|
133
|
-
|
|
169
|
+
- **`fetchApi`** (`typeof fetch`, optional): For browser environments, this defaults to the built-in `fetch`. In Node.js, you can use `node-fetch` or another compatible library.
|
|
134
170
|
- **`baseUrl`** (`string`, optional): The base URL for the QuickBase API. Defaults to `"https://api.quickbase.com/v1"`.
|
|
135
|
-
|
|
136
171
|
- **`debug`** (`boolean`, optional): Enables debug logging to the console. Defaults to `false`.
|
|
137
|
-
|
|
138
|
-
- **`
|
|
139
|
-
|
|
140
|
-
- **`tokenCache`** (`TokenCache`, optional): If you would like to use your own token cache, you can provide an instance of `TokenCache`.
|
|
172
|
+
- **`tempToken`** (`string`, optional): Overrides the default behavior of tempTokens by providing your own tempToken.
|
|
173
|
+
- **`tokenCache`** (`TokenCache`, optional): Allows you to use your own token cache instance.
|
|
141
174
|
|
|
142
175
|
---
|
|
143
176
|
|
|
144
177
|
#### Configuration Notes
|
|
145
178
|
|
|
146
|
-
- **Authentication**: There are three mutually exclusive authentication methods: `userToken`, `useTempTokens
|
|
179
|
+
- **Authentication**: There are three mutually exclusive authentication methods: `userToken`, `useTempTokens`, and `useSso` with `samlToken`. Only one auth method may be active at a time.
|
|
180
|
+
- **`tokenCache`**: Temporary tokens are cached with their dbid and `tempTokenLifespan`. This is automatically set up when `useTempTokens` is enabled.
|
|
147
181
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
- **`tokenCache`**: Temporary tokens are cached with their dbid with a `tempTokenLifespan`. This is automatically setup for you when `useTempTokens` is enabled.
|
|
182
|
+
---
|
|
151
183
|
|
|
152
184
|
### Development workflow
|
|
153
185
|
|
package/dist/esm/quickbase.d.ts
CHANGED
package/dist/esm/quickbase.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
7468
|
-
this.tokenCache.
|
|
7469
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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,16 +7641,15 @@ 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`);
|
|
7617
7648
|
const hasBody = "body" in params && params.body !== undefined;
|
|
7618
7649
|
const body = hasBody ? params.body : undefined;
|
|
7619
7650
|
const restParams = hasBody
|
|
7620
|
-
? Object.fromEntries(Object.entries(params).filter(([key]) => key !== "body"))
|
|
7621
|
-
: { ...params };
|
|
7651
|
+
? Object.fromEntries(Object.entries(params).filter(([key]) => key !== "body" && key !== "startTime"))
|
|
7652
|
+
: { ...params, startTime: undefined };
|
|
7622
7653
|
const requestParameters = {
|
|
7623
7654
|
...restParams,
|
|
7624
7655
|
...(hasBody ? { generated: body } : {}),
|
|
@@ -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,26 @@ 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
|
-
|
|
7717
|
+
let acquired = false;
|
|
7707
7718
|
try {
|
|
7708
7719
|
await rateLimiter.throttle();
|
|
7709
|
-
|
|
7710
|
-
const
|
|
7711
|
-
|
|
7720
|
+
acquired = true;
|
|
7721
|
+
const postThrottleTime = Date.now(); // Capture post-throttle time
|
|
7722
|
+
if (params.startTime !== undefined)
|
|
7723
|
+
params.startTime = postThrottleTime; // Update startTime for testing
|
|
7724
|
+
const responsePromise = methodInfo.method(requestParameters, requestOptions);
|
|
7725
|
+
rateLimiter.release(); // Release slot immediately after starting fetch
|
|
7726
|
+
const response = await responsePromise;
|
|
7712
7727
|
return await processResponse(response);
|
|
7713
7728
|
}
|
|
7714
7729
|
catch (error) {
|
|
7715
|
-
if (
|
|
7716
|
-
|
|
7730
|
+
if (acquired)
|
|
7731
|
+
rateLimiter.release();
|
|
7717
7732
|
let status;
|
|
7718
7733
|
let message;
|
|
7719
7734
|
let response;
|
|
@@ -7726,31 +7741,13 @@ async function invokeMethod(methodName, params, methodMap, baseHeaders, authStra
|
|
|
7726
7741
|
({ message, status } = await parseErrorResponse(response));
|
|
7727
7742
|
}
|
|
7728
7743
|
else {
|
|
7729
|
-
|
|
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
|
-
}
|
|
7744
|
+
throw error;
|
|
7744
7745
|
}
|
|
7745
|
-
if (debug)
|
|
7746
|
-
console.log("[invokeMethod] Handling error with status:", status);
|
|
7747
7746
|
if (status === 429) {
|
|
7748
7747
|
if (!(error instanceof ResponseError)) {
|
|
7749
7748
|
throw new Error("Expected ResponseError for 429 handling");
|
|
7750
7749
|
}
|
|
7751
7750
|
const delay = await rateLimiter.handle429(error, attempt + 1);
|
|
7752
|
-
if (debug)
|
|
7753
|
-
console.log(`[${methodName}] 429 delay: ${delay}ms`);
|
|
7754
7751
|
if (attempt + 1 === maxAttempts) {
|
|
7755
7752
|
throw new RateLimitError(`API Error: ${message} (Status: ${status})`, status, response?.headers.get("Retry-After")
|
|
7756
7753
|
? parseInt(response.headers.get("Retry-After"), 10)
|
|
@@ -7760,14 +7757,20 @@ async function invokeMethod(methodName, params, methodMap, baseHeaders, authStra
|
|
|
7760
7757
|
attempt++;
|
|
7761
7758
|
continue;
|
|
7762
7759
|
}
|
|
7763
|
-
|
|
7764
|
-
|
|
7760
|
+
let newToken;
|
|
7761
|
+
try {
|
|
7762
|
+
newToken = await authStrategy.handleError(status, params, attempt, maxAttempts, debug, methodName);
|
|
7763
|
+
}
|
|
7764
|
+
catch (authError) {
|
|
7765
|
+
throw authError;
|
|
7766
|
+
}
|
|
7767
|
+
if (newToken !== null) {
|
|
7765
7768
|
token = newToken;
|
|
7766
7769
|
authStrategy.applyHeaders(baseHeaders, token);
|
|
7767
7770
|
requestOptions.headers = { ...baseHeaders };
|
|
7768
|
-
attempt++;
|
|
7769
7771
|
if (debug)
|
|
7770
7772
|
console.log(`[${methodName}] Retrying with token: ${token.substring(0, 10)}...`);
|
|
7773
|
+
attempt++;
|
|
7771
7774
|
continue;
|
|
7772
7775
|
}
|
|
7773
7776
|
throw new Error(`API Error: ${message} (Status: ${status})`);
|
|
@@ -7777,41 +7780,71 @@ async function invokeMethod(methodName, params, methodMap, baseHeaders, authStra
|
|
|
7777
7780
|
}
|
|
7778
7781
|
|
|
7779
7782
|
// src/ThrottleBucket.ts
|
|
7780
|
-
class
|
|
7783
|
+
class ConcurrentThrottleBucket {
|
|
7781
7784
|
tokens;
|
|
7782
7785
|
maxTokens;
|
|
7783
|
-
refillRate;
|
|
7786
|
+
refillRate;
|
|
7784
7787
|
lastRefill;
|
|
7785
|
-
|
|
7788
|
+
semaphore;
|
|
7786
7789
|
constructor(rate, burst) {
|
|
7787
7790
|
this.tokens = burst;
|
|
7788
7791
|
this.maxTokens = burst;
|
|
7789
7792
|
this.refillRate = rate;
|
|
7790
7793
|
this.lastRefill = Date.now();
|
|
7794
|
+
this.semaphore = new Semaphore(burst);
|
|
7791
7795
|
}
|
|
7792
7796
|
refill() {
|
|
7793
7797
|
const now = Date.now();
|
|
7794
|
-
const elapsed = (now - this.lastRefill) / 1000;
|
|
7798
|
+
const elapsed = (now - this.lastRefill) / 1000;
|
|
7795
7799
|
const newTokens = elapsed * this.refillRate;
|
|
7796
7800
|
this.tokens = Math.min(this.maxTokens, this.tokens + newTokens);
|
|
7797
7801
|
this.lastRefill = now;
|
|
7798
7802
|
}
|
|
7799
7803
|
async acquire() {
|
|
7800
|
-
|
|
7801
|
-
|
|
7802
|
-
this.pending = (async () => {
|
|
7803
|
-
await previous; // Wait for prior calls to complete
|
|
7804
|
+
await this.semaphore.acquire();
|
|
7805
|
+
try {
|
|
7804
7806
|
this.refill();
|
|
7805
|
-
|
|
7806
|
-
this.tokens
|
|
7807
|
-
|
|
7807
|
+
while (this.tokens < 1) {
|
|
7808
|
+
const waitTime = ((1 - this.tokens) / this.refillRate) * 1000;
|
|
7809
|
+
await new Promise((resolve) => setTimeout(resolve, waitTime));
|
|
7810
|
+
this.refill();
|
|
7808
7811
|
}
|
|
7809
|
-
const waitTime = ((1 - this.tokens) / this.refillRate) * 1000; // ms until next token
|
|
7810
|
-
await new Promise((resolve) => setTimeout(resolve, waitTime));
|
|
7811
|
-
this.refill();
|
|
7812
7812
|
this.tokens -= 1;
|
|
7813
|
-
}
|
|
7814
|
-
|
|
7813
|
+
}
|
|
7814
|
+
catch (error) {
|
|
7815
|
+
this.semaphore.release();
|
|
7816
|
+
throw error;
|
|
7817
|
+
}
|
|
7818
|
+
}
|
|
7819
|
+
release() {
|
|
7820
|
+
this.semaphore.release();
|
|
7821
|
+
}
|
|
7822
|
+
}
|
|
7823
|
+
class Semaphore {
|
|
7824
|
+
permits;
|
|
7825
|
+
waiting = [];
|
|
7826
|
+
constructor(maxPermits) {
|
|
7827
|
+
this.permits = maxPermits;
|
|
7828
|
+
}
|
|
7829
|
+
async acquire() {
|
|
7830
|
+
if (this.permits > 0) {
|
|
7831
|
+
this.permits -= 1;
|
|
7832
|
+
return;
|
|
7833
|
+
}
|
|
7834
|
+
return new Promise((resolve, reject) => {
|
|
7835
|
+
this.waiting.push({ resolve, reject });
|
|
7836
|
+
});
|
|
7837
|
+
}
|
|
7838
|
+
release() {
|
|
7839
|
+
this.permits += 1;
|
|
7840
|
+
if (this.waiting.length > 0 && this.permits > 0) {
|
|
7841
|
+
const { resolve } = this.waiting.shift();
|
|
7842
|
+
this.permits -= 1;
|
|
7843
|
+
resolve();
|
|
7844
|
+
}
|
|
7845
|
+
}
|
|
7846
|
+
available() {
|
|
7847
|
+
return this.permits;
|
|
7815
7848
|
}
|
|
7816
7849
|
}
|
|
7817
7850
|
|
|
@@ -7819,7 +7852,8 @@ class RateLimiter {
|
|
|
7819
7852
|
throttleBucket;
|
|
7820
7853
|
maxRetries;
|
|
7821
7854
|
retryDelay;
|
|
7822
|
-
constructor(throttleBucket,
|
|
7855
|
+
constructor(throttleBucket, // Updated type
|
|
7856
|
+
maxRetries = 3, retryDelay = 1000) {
|
|
7823
7857
|
this.throttleBucket = throttleBucket;
|
|
7824
7858
|
this.maxRetries = maxRetries;
|
|
7825
7859
|
this.retryDelay = retryDelay;
|
|
@@ -7829,8 +7863,12 @@ class RateLimiter {
|
|
|
7829
7863
|
await this.throttleBucket.acquire();
|
|
7830
7864
|
}
|
|
7831
7865
|
}
|
|
7866
|
+
release() {
|
|
7867
|
+
if (this.throttleBucket) {
|
|
7868
|
+
this.throttleBucket.release();
|
|
7869
|
+
}
|
|
7870
|
+
}
|
|
7832
7871
|
async handle429(error, attempt) {
|
|
7833
|
-
// Remove the throw here; let invokeMethod handle exhaustion
|
|
7834
7872
|
const retryAfter = error.response.headers.get("Retry-After");
|
|
7835
7873
|
return retryAfter
|
|
7836
7874
|
? parseInt(retryAfter, 10) * 1000
|
|
@@ -7838,12 +7876,12 @@ class RateLimiter {
|
|
|
7838
7876
|
}
|
|
7839
7877
|
}
|
|
7840
7878
|
|
|
7841
|
-
// src/quickbaseClient.ts
|
|
7842
7879
|
function quickbase(config) {
|
|
7843
|
-
const { realm, userToken, tempToken, useTempTokens, useSso, samlToken, fetchApi, debug, convertDates = true, tempTokenLifespan = 290000, throttle = { rate:
|
|
7880
|
+
const { realm, userToken, tempToken, useTempTokens, useSso, samlToken, fetchApi, debug, convertDates = true, tempTokenLifespan = 290000, throttle = { rate: 5, burst: 3 }, // Updated default to match passing test
|
|
7881
|
+
maxRetries = 3, retryDelay = 1000, tokenCache: providedTokenCache, baseUrl = "https://api.quickbase.com/v1", } = config;
|
|
7844
7882
|
const tokenCache = providedTokenCache || new TokenCache(tempTokenLifespan);
|
|
7845
7883
|
const throttleBucket = throttle
|
|
7846
|
-
? new
|
|
7884
|
+
? new ConcurrentThrottleBucket(throttle.rate, throttle.burst)
|
|
7847
7885
|
: null;
|
|
7848
7886
|
const rateLimiter = new RateLimiter(throttleBucket, maxRetries, retryDelay);
|
|
7849
7887
|
const defaultFetch = typeof globalThis.window !== "undefined"
|
|
@@ -7854,10 +7892,10 @@ function quickbase(config) {
|
|
|
7854
7892
|
throw new Error("No fetch implementation available");
|
|
7855
7893
|
}
|
|
7856
7894
|
const authStrategy = useSso
|
|
7857
|
-
? new SsoTokenStrategy(samlToken || "", realm, effectiveFetch, debug)
|
|
7895
|
+
? new SsoTokenStrategy(samlToken || "", realm, effectiveFetch, debug, baseUrl)
|
|
7858
7896
|
: useTempTokens
|
|
7859
7897
|
? new TempTokenStrategy(tokenCache, tempToken, effectiveFetch, realm, baseUrl)
|
|
7860
|
-
: new UserTokenStrategy(userToken || "");
|
|
7898
|
+
: new UserTokenStrategy(userToken || "", baseUrl);
|
|
7861
7899
|
const baseHeaders = {
|
|
7862
7900
|
"QB-Realm-Hostname": `${realm}.quickbase.com`,
|
|
7863
7901
|
"Content-Type": "application/json",
|