quickbase-js 0.1.0-beta.8 → 0.1.0-beta.9
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 +29 -0
- package/README.md +46 -130
- package/dist/esm/quickbase.d.ts +822 -119
- package/dist/esm/quickbase.js +373 -236
- 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 +373 -236
- 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/open-api/generate-unified-interface.ts +97 -17
- package/open-api/utils/generateJsDoc.ts +96 -0
- package/package.json +3 -1
- package/src/ThrottleBucket.ts +17 -12
- package/src/authorizationStrategy.ts +246 -0
- package/src/generated-unified/QuickbaseClient.ts +809 -119
- package/src/invokeMethod.ts +150 -265
- package/src/quickbaseClient.ts +85 -58
- package/src/rateLimiter.ts +25 -0
- package/src/tokenCache.ts +10 -26
- package/tests/vitest/qb/auth/mockQuickbaseServer.ts +60 -0
- package/tests/vitest/qb/auth/retryUserToken401.test.ts +59 -42
- package/tests/vitest/qb/auth/ssoTokenIntegrationWithServer.test.ts +65 -0
- package/tests/vitest/qb/fields/getFieldUsage.test.ts +58 -150
- package/tests/vitest/qb/formulas/runFormula.test.ts +96 -0
- package/tests/vitest/qb/tables/createRelationship.test.ts +102 -0
- package/tests/vitest/qb/tables/deleteRelationship.test.ts +143 -0
- package/tests/vitest/qb/tables/updateRelationship.test.ts +118 -145
- package/tests/vitest/unit/ThrottleBucket.test.ts +95 -0
- package/tests/vitest/unit/apps/copyApp.test.ts +133 -40
- package/tests/vitest/unit/apps/updateApp.test.ts +42 -11
- package/tests/vitest/unit/auth/fetchTempToken401.test.ts +3 -2
- package/tests/vitest/unit/auth/getTempToken.test.ts +42 -18
- package/tests/vitest/unit/auth/retryTempToken401.test.ts +46 -64
- package/tests/vitest/unit/auth/retryTempTokenMaxRetries.test.ts +235 -0
- package/tests/vitest/unit/auth/retryUserToken401.test.ts +56 -43
- package/tests/vitest/unit/auth/ssoTokenRefresh.test.ts +325 -0
- package/tests/vitest/unit/fields/createField.test.ts +180 -116
- package/tests/vitest/unit/fields/getFieldUsage.test.ts +17 -3
- package/tests/vitest/unit/fields/updateField.test.ts +12 -7
- package/tests/vitest/unit/formulas/runFormula.test.ts +205 -0
- package/tests/vitest/unit/index.ts +2 -1
- package/tests/vitest/unit/inferHttpMethod.test.ts +131 -0
- package/tests/vitest/unit/rateLimit.test.ts +4 -2
- package/tests/vitest/unit/tables/createRelationship.test.ts +212 -0
- package/tests/vitest/unit/tables/deleteRelationship.test.ts +97 -0
- package/tests/vitest/unit/tables/updateRelationship.test.ts +12 -10
- package/tests/vitest/unit/tokenLifespan.test.ts +149 -83
- package/tools/log-gen.ts +14 -5
- package/tools/log-gen.yaml +710 -268
- package/tests/vitest/unit/auth/retryTempTokenTwo401.test.ts +0 -81
package/src/quickbaseClient.ts
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
// src/quickbaseClient.ts
|
|
2
|
+
|
|
2
3
|
import { QuickbaseClient as IQuickbaseClient } from "./generated-unified/QuickbaseClient";
|
|
3
4
|
import { Configuration, HTTPHeaders } from "./generated/runtime";
|
|
4
5
|
import * as apis from "./generated/apis";
|
|
5
6
|
import { TokenCache } from "./tokenCache";
|
|
6
7
|
import { simplifyName } from "./utils";
|
|
7
|
-
import { invokeMethod, MethodInfo } from "./invokeMethod";
|
|
8
|
+
import { invokeMethod, MethodInfo } from "./invokeMethod";
|
|
9
|
+
import {
|
|
10
|
+
TempTokenStrategy,
|
|
11
|
+
UserTokenStrategy,
|
|
12
|
+
AuthorizationStrategy,
|
|
13
|
+
SsoTokenStrategy,
|
|
14
|
+
} from "./authorizationStrategy";
|
|
8
15
|
import { ThrottleBucket } from "./ThrottleBucket";
|
|
16
|
+
import { RateLimiter } from "./rateLimiter";
|
|
9
17
|
|
|
10
18
|
export * from "./generated/models/index";
|
|
11
19
|
|
|
@@ -16,13 +24,17 @@ export interface QuickbaseConfig {
|
|
|
16
24
|
userToken?: string;
|
|
17
25
|
tempToken?: string;
|
|
18
26
|
useTempTokens?: boolean;
|
|
27
|
+
useSso?: boolean;
|
|
28
|
+
samlToken?: string;
|
|
19
29
|
debug?: boolean;
|
|
20
30
|
fetchApi?: typeof fetch;
|
|
21
31
|
convertDates?: boolean;
|
|
22
|
-
|
|
32
|
+
tempTokenLifespan?: number;
|
|
23
33
|
throttle?: { rate: number; burst: number };
|
|
24
34
|
maxRetries?: number;
|
|
25
35
|
retryDelay?: number;
|
|
36
|
+
tokenCache?: TokenCache;
|
|
37
|
+
baseUrl?: string;
|
|
26
38
|
}
|
|
27
39
|
|
|
28
40
|
type MethodMap = {
|
|
@@ -64,11 +76,20 @@ function transformDates(obj: any, convertStringsToDates: boolean = true): any {
|
|
|
64
76
|
}
|
|
65
77
|
|
|
66
78
|
function inferHttpMethod(methodSource: string, debug?: boolean): string {
|
|
67
|
-
const
|
|
68
|
-
|
|
79
|
+
const methodMatches = [
|
|
80
|
+
...methodSource.matchAll(/method:\s*['"]?(\w+)['"]?/gi),
|
|
81
|
+
];
|
|
82
|
+
const method =
|
|
83
|
+
methodMatches.length > 0
|
|
84
|
+
? methodMatches[methodMatches.length - 1][1].toUpperCase()
|
|
85
|
+
: "GET";
|
|
69
86
|
if (debug) {
|
|
70
|
-
|
|
71
|
-
|
|
87
|
+
console.log(`[inferHttpMethod] Source:`, methodSource);
|
|
88
|
+
console.log(
|
|
89
|
+
`[inferHttpMethod] All matches:`,
|
|
90
|
+
methodMatches.map((m) => m[0])
|
|
91
|
+
);
|
|
92
|
+
console.log(`[inferHttpMethod] Extracted method:`, method);
|
|
72
93
|
}
|
|
73
94
|
return method;
|
|
74
95
|
}
|
|
@@ -77,50 +98,59 @@ export function quickbase(config: QuickbaseConfig): QuickbaseClient {
|
|
|
77
98
|
const {
|
|
78
99
|
realm,
|
|
79
100
|
userToken,
|
|
80
|
-
tempToken
|
|
101
|
+
tempToken,
|
|
81
102
|
useTempTokens,
|
|
103
|
+
useSso,
|
|
104
|
+
samlToken,
|
|
82
105
|
fetchApi,
|
|
83
106
|
debug,
|
|
84
107
|
convertDates = true,
|
|
85
|
-
|
|
108
|
+
tempTokenLifespan = 290000,
|
|
86
109
|
throttle = { rate: 10, burst: 10 },
|
|
87
110
|
maxRetries = 3,
|
|
88
111
|
retryDelay = 1000,
|
|
112
|
+
tokenCache: providedTokenCache,
|
|
113
|
+
baseUrl = "https://api.quickbase.com/v1",
|
|
89
114
|
} = config;
|
|
90
|
-
const baseUrl = `https://api.quickbase.com/v1`;
|
|
91
115
|
|
|
92
|
-
const tokenCache = new TokenCache(
|
|
116
|
+
const tokenCache = providedTokenCache || new TokenCache(tempTokenLifespan);
|
|
93
117
|
const throttleBucket = throttle
|
|
94
118
|
? new ThrottleBucket(throttle.rate, throttle.burst)
|
|
95
119
|
: null;
|
|
120
|
+
const rateLimiter = new RateLimiter(throttleBucket, maxRetries, retryDelay);
|
|
121
|
+
|
|
122
|
+
const defaultFetch: typeof fetch | undefined =
|
|
123
|
+
typeof globalThis.window !== "undefined"
|
|
124
|
+
? globalThis.window.fetch.bind(globalThis.window)
|
|
125
|
+
: undefined;
|
|
126
|
+
const effectiveFetch = fetchApi || defaultFetch;
|
|
127
|
+
|
|
128
|
+
if (!effectiveFetch) {
|
|
129
|
+
throw new Error("No fetch implementation available");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const authStrategy: AuthorizationStrategy = useSso
|
|
133
|
+
? new SsoTokenStrategy(samlToken || "", realm, effectiveFetch, debug)
|
|
134
|
+
: useTempTokens
|
|
135
|
+
? new TempTokenStrategy(tokenCache, tempToken)
|
|
136
|
+
: new UserTokenStrategy(userToken || "");
|
|
96
137
|
|
|
97
138
|
const baseHeaders: HTTPHeaders = {
|
|
98
139
|
"QB-Realm-Hostname": `${realm}.quickbase.com`,
|
|
99
140
|
"Content-Type": "application/json",
|
|
100
141
|
};
|
|
101
142
|
|
|
102
|
-
if (initialTempToken) {
|
|
103
|
-
baseHeaders["Authorization"] = `QB-TEMP-TOKEN ${initialTempToken}`;
|
|
104
|
-
} else if (userToken && !useTempTokens) {
|
|
105
|
-
baseHeaders["Authorization"] = `QB-USER-TOKEN ${userToken}`;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const defaultFetch: typeof fetch | undefined =
|
|
109
|
-
typeof globalThis.window !== "undefined"
|
|
110
|
-
? globalThis.window.fetch.bind(globalThis.window)
|
|
111
|
-
: undefined;
|
|
112
143
|
const configuration = new Configuration({
|
|
113
144
|
basePath: baseUrl,
|
|
114
145
|
headers: { ...baseHeaders },
|
|
115
|
-
fetchApi:
|
|
146
|
+
fetchApi: effectiveFetch,
|
|
116
147
|
credentials: "omit",
|
|
117
148
|
});
|
|
118
149
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
150
|
+
// Removed redundant check:
|
|
151
|
+
// if (!configuration.fetchApi && typeof globalThis.window === "undefined") {
|
|
152
|
+
// throw new Error("fetchApi must be provided in non-browser environments (e.g., Node.js)");
|
|
153
|
+
// }
|
|
124
154
|
|
|
125
155
|
const apiInstances = Object.fromEntries(
|
|
126
156
|
Object.entries(apis)
|
|
@@ -131,8 +161,6 @@ export function quickbase(config: QuickbaseConfig): QuickbaseClient {
|
|
|
131
161
|
])
|
|
132
162
|
);
|
|
133
163
|
|
|
134
|
-
const methodMap = buildMethodMap();
|
|
135
|
-
|
|
136
164
|
function buildMethodMap(): MethodMap {
|
|
137
165
|
const methodMap: Partial<MethodMap> = {};
|
|
138
166
|
const isValidMethod = (name: string) =>
|
|
@@ -159,32 +187,27 @@ export function quickbase(config: QuickbaseConfig): QuickbaseClient {
|
|
|
159
187
|
const methodSource = method.toString();
|
|
160
188
|
methodMap[simplifiedName] = {
|
|
161
189
|
api,
|
|
162
|
-
method: boundMethod as any,
|
|
190
|
+
method: boundMethod as any,
|
|
163
191
|
paramMap: getParamNames(method),
|
|
164
192
|
httpMethod: inferHttpMethod(methodSource, debug),
|
|
165
193
|
};
|
|
166
194
|
}
|
|
167
195
|
});
|
|
168
196
|
}
|
|
197
|
+
if (debug) {
|
|
198
|
+
console.log("[buildMethodMap] Methods:", Object.keys(methodMap));
|
|
199
|
+
}
|
|
169
200
|
return methodMap as MethodMap;
|
|
170
201
|
}
|
|
171
202
|
|
|
172
|
-
const
|
|
173
|
-
const effectiveFetch = fetchApi || defaultFetch;
|
|
174
|
-
if (!effectiveFetch) {
|
|
175
|
-
throw new Error(
|
|
176
|
-
"No fetch implementation available for fetching temp token"
|
|
177
|
-
);
|
|
178
|
-
}
|
|
203
|
+
const methodMap = buildMethodMap();
|
|
179
204
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
);
|
|
205
|
+
const fetchTempToken = async (dbid: string): Promise<string> => {
|
|
206
|
+
const response = await effectiveFetch(`${baseUrl}/auth/temporary/${dbid}`, {
|
|
207
|
+
method: "GET",
|
|
208
|
+
headers: { ...baseHeaders },
|
|
209
|
+
credentials: "include",
|
|
210
|
+
});
|
|
188
211
|
|
|
189
212
|
if (!response.ok) {
|
|
190
213
|
const errorBody: { message?: string } = await response.json();
|
|
@@ -196,19 +219,26 @@ export function quickbase(config: QuickbaseConfig): QuickbaseClient {
|
|
|
196
219
|
}
|
|
197
220
|
|
|
198
221
|
const tokenResult = await response.json();
|
|
222
|
+
console.log(`[fetchTempToken] Response:`, tokenResult);
|
|
199
223
|
const token = tokenResult.temporaryAuthorization;
|
|
200
224
|
if (!token) {
|
|
201
225
|
throw new Error("No temporary token returned from API");
|
|
202
226
|
}
|
|
203
|
-
tokenCache.set(dbid, token);
|
|
227
|
+
tokenCache.set(dbid, token, tempTokenLifespan);
|
|
204
228
|
if (debug) {
|
|
205
229
|
console.log(`Fetched and cached new token for dbid: ${dbid}`, token);
|
|
206
230
|
}
|
|
207
231
|
return token;
|
|
208
232
|
};
|
|
209
233
|
|
|
210
|
-
const
|
|
211
|
-
get: (_, prop: string)
|
|
234
|
+
const proxyHandler: ProxyHandler<QuickbaseClient> = {
|
|
235
|
+
get: (_, prop: string) => {
|
|
236
|
+
console.log(
|
|
237
|
+
"[proxy] Accessing:",
|
|
238
|
+
prop,
|
|
239
|
+
"in methodMap:",
|
|
240
|
+
prop in methodMap
|
|
241
|
+
);
|
|
212
242
|
if (prop in methodMap) {
|
|
213
243
|
const methodName = prop as keyof QuickbaseClient;
|
|
214
244
|
return (params: Parameters<QuickbaseClient[typeof methodName]>[0]) =>
|
|
@@ -217,27 +247,24 @@ export function quickbase(config: QuickbaseConfig): QuickbaseClient {
|
|
|
217
247
|
params,
|
|
218
248
|
methodMap,
|
|
219
249
|
baseHeaders,
|
|
220
|
-
|
|
250
|
+
authStrategy,
|
|
251
|
+
rateLimiter,
|
|
221
252
|
fetchTempToken,
|
|
222
253
|
transformDates,
|
|
223
|
-
initialTempToken,
|
|
224
|
-
userToken,
|
|
225
|
-
useTempTokens,
|
|
226
254
|
debug,
|
|
227
|
-
convertDates
|
|
228
|
-
0, // retryCount
|
|
229
|
-
throttleBucket,
|
|
230
|
-
maxRetries,
|
|
231
|
-
retryDelay
|
|
255
|
+
convertDates
|
|
232
256
|
);
|
|
233
257
|
}
|
|
258
|
+
console.log("[proxy] Method not found:", prop);
|
|
234
259
|
return undefined;
|
|
235
260
|
},
|
|
236
|
-
}
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const proxy = new Proxy<QuickbaseClient>({} as QuickbaseClient, proxyHandler);
|
|
237
264
|
|
|
238
265
|
if (debug) {
|
|
239
266
|
console.log("[createClient] Config:", config);
|
|
240
|
-
console.log("[createClient]
|
|
267
|
+
console.log("[createClient] Proxy created:", proxy);
|
|
241
268
|
}
|
|
242
269
|
|
|
243
270
|
return proxy;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// src/rateLimiter.ts
|
|
2
|
+
import { ThrottleBucket } from "./ThrottleBucket";
|
|
3
|
+
import { ResponseError } from "./generated/runtime";
|
|
4
|
+
|
|
5
|
+
export class RateLimiter {
|
|
6
|
+
constructor(
|
|
7
|
+
private throttleBucket: ThrottleBucket | null,
|
|
8
|
+
public maxRetries: number = 3,
|
|
9
|
+
private retryDelay: number = 1000
|
|
10
|
+
) {}
|
|
11
|
+
|
|
12
|
+
async throttle(): Promise<void> {
|
|
13
|
+
if (this.throttleBucket) {
|
|
14
|
+
await this.throttleBucket.acquire();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async handle429(error: ResponseError, attempt: number): Promise<number> {
|
|
19
|
+
// Remove the throw here; let invokeMethod handle exhaustion
|
|
20
|
+
const retryAfter = error.response.headers.get("Retry-After");
|
|
21
|
+
return retryAfter
|
|
22
|
+
? parseInt(retryAfter, 10) * 1000
|
|
23
|
+
: this.retryDelay * Math.pow(2, attempt - 1);
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/tokenCache.ts
CHANGED
|
@@ -1,50 +1,34 @@
|
|
|
1
|
+
// src/tokenCache.ts
|
|
1
2
|
interface CachedToken {
|
|
2
3
|
token: string;
|
|
3
|
-
expiresAt: number;
|
|
4
|
+
expiresAt: number;
|
|
4
5
|
}
|
|
5
6
|
|
|
6
7
|
export class TokenCache {
|
|
7
8
|
private cache: Map<string, CachedToken>;
|
|
8
|
-
private readonly
|
|
9
|
+
private readonly tempTokenLifespan: number;
|
|
9
10
|
|
|
10
|
-
constructor(
|
|
11
|
-
// Default 4:50
|
|
11
|
+
constructor(tempTokenLifespan: number = 4 * 60 * 1000 + 50 * 1000) {
|
|
12
12
|
this.cache = new Map<string, CachedToken>();
|
|
13
|
-
this.
|
|
13
|
+
this.tempTokenLifespan = tempTokenLifespan;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
get(dbid: string): string | undefined {
|
|
17
17
|
const entry = this.cache.get(dbid);
|
|
18
18
|
const now = Date.now();
|
|
19
|
-
if (entry && entry.expiresAt > now)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return undefined; // Expired or not found
|
|
19
|
+
if (entry && entry.expiresAt > now) return entry.token;
|
|
20
|
+
if (entry) this.cache.delete(dbid); // Clean up expired temp tokens
|
|
21
|
+
return undefined;
|
|
23
22
|
}
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
getEntry(dbid: string): CachedToken | undefined {
|
|
27
|
-
const entry = this.cache.get(dbid);
|
|
28
|
-
const now = Date.now();
|
|
29
|
-
if (entry && entry.expiresAt > now) {
|
|
30
|
-
return entry;
|
|
31
|
-
}
|
|
32
|
-
return undefined; // Expired or not found
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
set(dbid: string, token: string): void {
|
|
24
|
+
set(dbid: string, token: string, lifespan?: number): void {
|
|
36
25
|
const now = Date.now();
|
|
37
26
|
this.cache.set(dbid, {
|
|
38
27
|
token,
|
|
39
|
-
expiresAt: now + this.
|
|
28
|
+
expiresAt: now + (lifespan || this.tempTokenLifespan),
|
|
40
29
|
});
|
|
41
30
|
}
|
|
42
31
|
|
|
43
|
-
dump(): [string, CachedToken][] {
|
|
44
|
-
return Array.from(this.cache.entries());
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// New method to clear the cache
|
|
48
32
|
clear(): void {
|
|
49
33
|
this.cache.clear();
|
|
50
34
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// tests/vitest/qb/auth/mockQuickbaseServer.ts
|
|
2
|
+
import express from "express";
|
|
3
|
+
|
|
4
|
+
export function startMockQuickbaseServer(port: number = 3000) {
|
|
5
|
+
const app = express();
|
|
6
|
+
app.use(express.json());
|
|
7
|
+
let failNextCall = false;
|
|
8
|
+
|
|
9
|
+
app.post("/v1/auth/oauth/token", (req, res) => {
|
|
10
|
+
const { subject_token } = req.body;
|
|
11
|
+
console.log("[Mock Server] Received SSO token request:", req.body);
|
|
12
|
+
if (!subject_token) {
|
|
13
|
+
return res.status(400).json({ message: "Missing SAML token" });
|
|
14
|
+
}
|
|
15
|
+
res.json({
|
|
16
|
+
access_token: "mock-temp-token",
|
|
17
|
+
token_type: "Bearer",
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
app.get("/v1/apps/:appId", (req, res) => {
|
|
22
|
+
const authHeader = req.headers.authorization;
|
|
23
|
+
console.log(
|
|
24
|
+
"[Mock Server] Received getApp request:",
|
|
25
|
+
req.params,
|
|
26
|
+
"Auth:",
|
|
27
|
+
authHeader
|
|
28
|
+
);
|
|
29
|
+
if (!authHeader || !authHeader.startsWith("QB-TEMP-TOKEN")) {
|
|
30
|
+
return res.status(401).json({ message: "Unauthorized" });
|
|
31
|
+
}
|
|
32
|
+
if (failNextCall) {
|
|
33
|
+
failNextCall = false;
|
|
34
|
+
return res.status(401).json({ message: "Token expired" });
|
|
35
|
+
}
|
|
36
|
+
res.json({
|
|
37
|
+
id: req.params.appId,
|
|
38
|
+
name: "Mock App",
|
|
39
|
+
created: "2025-03-19T12:00:00Z",
|
|
40
|
+
updated: "2025-03-19T12:00:00Z",
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
app.post("/mock/toggle-fail", (req, res) => {
|
|
45
|
+
failNextCall = true;
|
|
46
|
+
console.log("[Mock Server] Toggling next getApp call to fail with 401");
|
|
47
|
+
res.sendStatus(200);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const server = app.listen(port, () => {
|
|
51
|
+
console.log(`Mock QuickBase server running on port ${port}`);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
server,
|
|
56
|
+
close: () => server.close(),
|
|
57
|
+
toggleFail: () =>
|
|
58
|
+
fetch(`http://localhost:${port}/mock/toggle-fail`, { method: "POST" }),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -1,56 +1,73 @@
|
|
|
1
1
|
import { describe, it, vi, expect, beforeEach } from "vitest";
|
|
2
2
|
import { quickbase, QuickbaseClient } from "../../../../src/quickbaseClient";
|
|
3
|
-
import { ResponseError } from "../../../../src/generated/runtime";
|
|
4
3
|
|
|
5
4
|
describe("QuickbaseClient Integration - User Token Retry on 401", () => {
|
|
6
5
|
let client: QuickbaseClient;
|
|
7
|
-
const mockFetch = vi.fn();
|
|
8
6
|
|
|
9
7
|
beforeEach(() => {
|
|
8
|
+
const userToken = process.env.QB_USER_TOKEN;
|
|
9
|
+
const realm = process.env.QB_REALM;
|
|
10
|
+
const tableId = process.env.QB_TABLE_ID_1; // Consistent env var name
|
|
11
|
+
|
|
12
|
+
if (!userToken) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
"QB_USER_TOKEN environment variable is required for integration test"
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
if (!tableId) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
"QB_TABLE_ID_1 environment variable is required for integration test"
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
10
23
|
const config = {
|
|
11
|
-
realm
|
|
12
|
-
userToken
|
|
24
|
+
realm,
|
|
25
|
+
userToken,
|
|
13
26
|
debug: true,
|
|
14
|
-
fetchApi: mockFetch,
|
|
15
27
|
};
|
|
16
|
-
console.log("[quickbaseTest] Config:",
|
|
28
|
+
console.log("[quickbaseTest] Config:", {
|
|
29
|
+
realm,
|
|
30
|
+
userToken: userToken.substring(0, 10) + "...",
|
|
31
|
+
tableId, // Log to confirm
|
|
32
|
+
debug: true,
|
|
33
|
+
});
|
|
17
34
|
client = quickbase(config);
|
|
18
35
|
});
|
|
19
36
|
|
|
20
|
-
it(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
37
|
+
it(
|
|
38
|
+
"successfully retrieves fields with user token, retrying on transient 401 if needed",
|
|
39
|
+
async () => {
|
|
40
|
+
const consoleSpy = vi.spyOn(console, "log");
|
|
41
|
+
|
|
42
|
+
const tableId = process.env.QB_TABLE_ID_1!; // Match beforeEach
|
|
43
|
+
let fields;
|
|
44
|
+
try {
|
|
45
|
+
fields = await client.getFields({ tableId });
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error("[integrationTest] Failed to retrieve fields:", error);
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
expect(fields).toBeInstanceOf(Array);
|
|
52
|
+
expect(fields.length).toBeGreaterThan(0);
|
|
53
|
+
expect(fields[0]).toHaveProperty("id");
|
|
54
|
+
expect(fields[0]).toHaveProperty("label");
|
|
55
|
+
|
|
56
|
+
console.log("[integrationTest] Fields retrieved:", fields.length);
|
|
57
|
+
|
|
58
|
+
const retryLog = consoleSpy.mock.calls.some((call) =>
|
|
59
|
+
call[0].includes("Retrying getFields with existing user token")
|
|
60
|
+
);
|
|
61
|
+
if (retryLog) {
|
|
62
|
+
console.log("[integrationTest] Retry on 401 detected");
|
|
63
|
+
} else {
|
|
64
|
+
console.log(
|
|
65
|
+
"[integrationTest] No 401 retries observed (expected if no transient failure)"
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
consoleSpy.mockRestore();
|
|
70
|
+
},
|
|
71
|
+
{ timeout: 10000 }
|
|
72
|
+
);
|
|
56
73
|
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// tests/vitest/qb/auth/ssoTokenIntegrationWithServer.test.ts
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect, beforeAll, afterAll, vi } from "vitest"; // Add 'vi' to imports
|
|
4
|
+
import { quickbase } from "../../../../src/quickbaseClient";
|
|
5
|
+
import { startMockQuickbaseServer } from "./mockQuickbaseServer";
|
|
6
|
+
|
|
7
|
+
describe("QuickbaseClient Integration - SSO with Mock Server", () => {
|
|
8
|
+
let mockServer: ReturnType<typeof startMockQuickbaseServer>;
|
|
9
|
+
const mockBaseUrl = "http://localhost:3000";
|
|
10
|
+
|
|
11
|
+
beforeAll(async () => {
|
|
12
|
+
mockServer = startMockQuickbaseServer(3000);
|
|
13
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
14
|
+
console.log("Server setup complete, port 3000 should be listening");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterAll(() => {
|
|
18
|
+
mockServer.close();
|
|
19
|
+
console.log("Server closed");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it(
|
|
23
|
+
"uses SSO token from mock server and retries on 401",
|
|
24
|
+
{ timeout: 10000 }, // Move timeout to second argument
|
|
25
|
+
async () => {
|
|
26
|
+
const consoleSpy = vi.spyOn(console, "log"); // Spy on console.log to capture logs
|
|
27
|
+
const client = quickbase({
|
|
28
|
+
realm: "testrealm",
|
|
29
|
+
samlToken: "mock-saml-token",
|
|
30
|
+
useSso: true,
|
|
31
|
+
fetchApi: (url, options) => {
|
|
32
|
+
const rewrittenUrl = url.replace(
|
|
33
|
+
"https://api.quickbase.com/v1",
|
|
34
|
+
mockBaseUrl + "/v1"
|
|
35
|
+
);
|
|
36
|
+
console.log(
|
|
37
|
+
"[fetchApi] Requesting:",
|
|
38
|
+
rewrittenUrl,
|
|
39
|
+
"Options:",
|
|
40
|
+
options
|
|
41
|
+
);
|
|
42
|
+
return fetch(rewrittenUrl, options);
|
|
43
|
+
},
|
|
44
|
+
debug: true,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
await mockServer.toggleFail();
|
|
48
|
+
const response = await client.getApp({ appId: "mockappid" });
|
|
49
|
+
|
|
50
|
+
expect(response).toBeDefined();
|
|
51
|
+
expect(response.id).toBe("mockappid");
|
|
52
|
+
expect(response.name).toBe("Mock App");
|
|
53
|
+
|
|
54
|
+
// Verify retry happened
|
|
55
|
+
const retryLog = consoleSpy.mock.calls.some((call) =>
|
|
56
|
+
call[0].includes(
|
|
57
|
+
"Authorization error for getApp (SSO), refreshing token"
|
|
58
|
+
)
|
|
59
|
+
);
|
|
60
|
+
expect(retryLog).toBe(true); // Ensure retry logic was triggered
|
|
61
|
+
|
|
62
|
+
consoleSpy.mockRestore();
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
});
|