quickbase-js 0.1.0-beta.7 → 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 +55 -0
- package/README.md +46 -130
- package/dist/esm/quickbase.d.ts +1292 -125
- package/dist/esm/quickbase.js +666 -190
- 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 +674 -189
- 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/fix-spec-main.ts +69 -68
- package/open-api/generate-override-report.ts +214 -0
- package/open-api/generate-unified-interface.ts +162 -103
- package/open-api/output/override-report.html +11 -0
- package/open-api/output/override-report.json +14395 -58
- package/open-api/output/quickbase-fixed.json +583 -2
- package/open-api/schema/tags/fields.ts +331 -16
- package/open-api/schema/tags/index.ts +2 -4
- package/open-api/types/spec.ts +2 -1
- package/open-api/utils/generateJsDoc.ts +96 -0
- package/package.json +33 -33
- package/src/RateLimitError.ts +12 -0
- package/src/ThrottleBucket.ts +41 -0
- package/src/authorizationStrategy.ts +246 -0
- package/src/generated-unified/QuickbaseClient.ts +809 -119
- package/src/invokeMethod.ts +242 -0
- package/src/quickbaseClient.ts +117 -248
- package/src/rateLimiter.ts +25 -0
- package/src/tokenCache.ts +10 -26
- package/tests/setup.ts +2 -0
- package/tests/vitest/qb/apps/getApp.test.ts +6 -2
- package/tests/vitest/qb/apps/getAppEvents.test.ts +67 -0
- package/tests/vitest/qb/apps/updateApp.test.ts +58 -0
- package/tests/vitest/qb/auth/mockQuickbaseServer.ts +60 -0
- package/tests/vitest/qb/auth/retryUserToken401.test.ts +73 -0
- package/tests/vitest/qb/auth/ssoTokenIntegrationWithServer.test.ts +65 -0
- package/tests/vitest/qb/fields/createField.test.ts +1 -2
- package/tests/vitest/qb/fields/deleteFields.test.ts +10 -16
- package/tests/vitest/qb/fields/getFieldUsage.test.ts +58 -150
- package/tests/vitest/qb/fields/updateField.test.ts +93 -0
- package/tests/vitest/qb/formulas/runFormula.test.ts +96 -0
- package/tests/vitest/qb/index.ts +1 -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 +250 -0
- package/tests/vitest/unit/ThrottleBucket.test.ts +95 -0
- package/tests/vitest/unit/apps/copyApp.test.ts +133 -40
- package/tests/vitest/unit/apps/getAppEvents.test.ts +193 -0
- package/tests/vitest/unit/apps/updateApp.test.ts +412 -0
- package/tests/vitest/unit/auth/fetchTempToken401.test.ts +7 -26
- package/tests/vitest/unit/auth/getTempToken.test.ts +42 -18
- package/tests/vitest/unit/auth/retryTempToken401.test.ts +95 -0
- package/tests/vitest/unit/auth/retryTempTokenMaxRetries.test.ts +235 -0
- package/tests/vitest/unit/auth/retryUserToken401.test.ts +69 -0
- package/tests/vitest/unit/auth/ssoTokenRefresh.test.ts +325 -0
- package/tests/vitest/unit/fields/createField.test.ts +180 -120
- package/tests/vitest/unit/fields/getFieldUsage.test.ts +47 -14
- package/tests/vitest/unit/fields/updateField.test.ts +285 -0
- 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 +191 -0
- 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 +307 -0
- package/tests/vitest/unit/tokenLifespan.test.ts +204 -0
- package/tools/log-gen.ts +20 -7
- package/tools/log-gen.yaml +3446 -1848
- package/tsconfig.json +5 -2
- package/vitest.config.ts +2 -2
- package/notes.txt +0 -158
- package/tests/vitest/unit/auth/retryOn401.test.ts +0 -116
- package/tests/vitest/unit/auth/two401s.test.ts +0 -86
- package/tools/generate-types.ts +0 -29
- package/tools/openapi-schema-validator.ts +0 -38
- package/tools/spec-analyzer.ts +0 -163
- package/tools/split-spec.ts +0 -148
- package/tools/test-import.ts +0 -3
- package/tools/validate-spec.ts +0 -40
- /package/tests/playwright/qb/auth/{tokenPrefetch.test.ts → tempTokenPrefetch.test.ts} +0 -0
- /package/tests/playwright/qb/auth/{tokenRenewal.test.ts → tempTokenRenewal.test.ts} +0 -0
package/src/quickbaseClient.ts
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
// src/quickbaseClient.ts
|
|
2
|
+
|
|
2
3
|
import { QuickbaseClient as IQuickbaseClient } from "./generated-unified/QuickbaseClient";
|
|
3
|
-
import { Configuration, HTTPHeaders
|
|
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";
|
|
8
|
+
import { invokeMethod, MethodInfo } from "./invokeMethod";
|
|
9
|
+
import {
|
|
10
|
+
TempTokenStrategy,
|
|
11
|
+
UserTokenStrategy,
|
|
12
|
+
AuthorizationStrategy,
|
|
13
|
+
SsoTokenStrategy,
|
|
14
|
+
} from "./authorizationStrategy";
|
|
15
|
+
import { ThrottleBucket } from "./ThrottleBucket";
|
|
16
|
+
import { RateLimiter } from "./rateLimiter";
|
|
7
17
|
|
|
8
18
|
export * from "./generated/models/index";
|
|
9
19
|
|
|
@@ -14,26 +24,17 @@ export interface QuickbaseConfig {
|
|
|
14
24
|
userToken?: string;
|
|
15
25
|
tempToken?: string;
|
|
16
26
|
useTempTokens?: boolean;
|
|
27
|
+
useSso?: boolean;
|
|
28
|
+
samlToken?: string;
|
|
17
29
|
debug?: boolean;
|
|
18
30
|
fetchApi?: typeof fetch;
|
|
19
|
-
convertDates?: boolean;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
type ApiMethod<K extends keyof QuickbaseClient> = (
|
|
29
|
-
requestParameters: Parameters<QuickbaseClient[K]>[0],
|
|
30
|
-
initOverrides?: RequestInit
|
|
31
|
-
) => Promise<ReturnType<QuickbaseClient[K]>>;
|
|
32
|
-
|
|
33
|
-
interface MethodInfo<K extends keyof QuickbaseClient> {
|
|
34
|
-
api: any;
|
|
35
|
-
method: ApiMethod<K>;
|
|
36
|
-
paramMap: string[];
|
|
31
|
+
convertDates?: boolean;
|
|
32
|
+
tempTokenLifespan?: number;
|
|
33
|
+
throttle?: { rate: number; burst: number };
|
|
34
|
+
maxRetries?: number;
|
|
35
|
+
retryDelay?: number;
|
|
36
|
+
tokenCache?: TokenCache;
|
|
37
|
+
baseUrl?: string;
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
type MethodMap = {
|
|
@@ -48,18 +49,6 @@ const getParamNames = (fn: (...args: any[]) => any): string[] =>
|
|
|
48
49
|
.map((p) => p.trim().split("=")[0]?.trim())
|
|
49
50
|
.filter((p) => p && !p.match(/^\{/) && p !== "options");
|
|
50
51
|
|
|
51
|
-
const extractDbid = (
|
|
52
|
-
params: Partial<TempTokenParams>,
|
|
53
|
-
errorMessage: string
|
|
54
|
-
): string => {
|
|
55
|
-
const dbid = params.dbid || params.tableId || params.appId;
|
|
56
|
-
if (!dbid) {
|
|
57
|
-
throw new Error(errorMessage);
|
|
58
|
-
}
|
|
59
|
-
return dbid;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
// Utility to convert ISO date strings to Date objects recursively, with optional conversion
|
|
63
52
|
function transformDates(obj: any, convertStringsToDates: boolean = true): any {
|
|
64
53
|
if (obj === null || obj === undefined) return obj;
|
|
65
54
|
if (obj instanceof Date) return obj;
|
|
@@ -86,47 +75,82 @@ function transformDates(obj: any, convertStringsToDates: boolean = true): any {
|
|
|
86
75
|
return obj;
|
|
87
76
|
}
|
|
88
77
|
|
|
78
|
+
function inferHttpMethod(methodSource: string, debug?: boolean): string {
|
|
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";
|
|
86
|
+
if (debug) {
|
|
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);
|
|
93
|
+
}
|
|
94
|
+
return method;
|
|
95
|
+
}
|
|
96
|
+
|
|
89
97
|
export function quickbase(config: QuickbaseConfig): QuickbaseClient {
|
|
90
98
|
const {
|
|
91
99
|
realm,
|
|
92
100
|
userToken,
|
|
93
|
-
tempToken
|
|
101
|
+
tempToken,
|
|
94
102
|
useTempTokens,
|
|
103
|
+
useSso,
|
|
104
|
+
samlToken,
|
|
95
105
|
fetchApi,
|
|
96
106
|
debug,
|
|
97
|
-
convertDates = true,
|
|
107
|
+
convertDates = true,
|
|
108
|
+
tempTokenLifespan = 290000,
|
|
109
|
+
throttle = { rate: 10, burst: 10 },
|
|
110
|
+
maxRetries = 3,
|
|
111
|
+
retryDelay = 1000,
|
|
112
|
+
tokenCache: providedTokenCache,
|
|
113
|
+
baseUrl = "https://api.quickbase.com/v1",
|
|
98
114
|
} = config;
|
|
99
|
-
const baseUrl = `https://api.quickbase.com/v1`;
|
|
100
115
|
|
|
101
|
-
const tokenCache = new TokenCache();
|
|
116
|
+
const tokenCache = providedTokenCache || new TokenCache(tempTokenLifespan);
|
|
117
|
+
const throttleBucket = throttle
|
|
118
|
+
? new ThrottleBucket(throttle.rate, throttle.burst)
|
|
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 || "");
|
|
102
137
|
|
|
103
138
|
const baseHeaders: HTTPHeaders = {
|
|
104
139
|
"QB-Realm-Hostname": `${realm}.quickbase.com`,
|
|
105
140
|
"Content-Type": "application/json",
|
|
106
141
|
};
|
|
107
142
|
|
|
108
|
-
if (initialTempToken) {
|
|
109
|
-
baseHeaders["Authorization"] = `QB-TEMP-TOKEN ${initialTempToken}`;
|
|
110
|
-
} else if (userToken && !useTempTokens) {
|
|
111
|
-
baseHeaders["Authorization"] = `QB-USER-TOKEN ${userToken}`;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const defaultFetch: typeof fetch | undefined =
|
|
115
|
-
typeof globalThis.window !== "undefined"
|
|
116
|
-
? globalThis.window.fetch.bind(globalThis.window)
|
|
117
|
-
: undefined;
|
|
118
143
|
const configuration = new Configuration({
|
|
119
144
|
basePath: baseUrl,
|
|
120
145
|
headers: { ...baseHeaders },
|
|
121
|
-
fetchApi:
|
|
146
|
+
fetchApi: effectiveFetch,
|
|
122
147
|
credentials: "omit",
|
|
123
148
|
});
|
|
124
149
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
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
|
+
// }
|
|
130
154
|
|
|
131
155
|
const apiInstances = Object.fromEntries(
|
|
132
156
|
Object.entries(apis)
|
|
@@ -137,8 +161,6 @@ export function quickbase(config: QuickbaseConfig): QuickbaseClient {
|
|
|
137
161
|
])
|
|
138
162
|
);
|
|
139
163
|
|
|
140
|
-
const methodMap = buildMethodMap();
|
|
141
|
-
|
|
142
164
|
function buildMethodMap(): MethodMap {
|
|
143
165
|
const methodMap: Partial<MethodMap> = {};
|
|
144
166
|
const isValidMethod = (name: string) =>
|
|
@@ -157,36 +179,35 @@ export function quickbase(config: QuickbaseConfig): QuickbaseClient {
|
|
|
157
179
|
const simplifiedName = simplifyName(
|
|
158
180
|
rawMethodName
|
|
159
181
|
) as keyof QuickbaseClient;
|
|
160
|
-
const
|
|
182
|
+
const rawMethodKey = `${rawMethodName}Raw` as keyof typeof api;
|
|
183
|
+
const method =
|
|
184
|
+
api[rawMethodKey] || api[rawMethodName as keyof typeof api];
|
|
161
185
|
const boundMethod = method.bind(api as any) as unknown;
|
|
162
186
|
if (typeof boundMethod === "function" && boundMethod.length <= 2) {
|
|
187
|
+
const methodSource = method.toString();
|
|
163
188
|
methodMap[simplifiedName] = {
|
|
164
189
|
api,
|
|
165
|
-
method: boundMethod as
|
|
190
|
+
method: boundMethod as any,
|
|
166
191
|
paramMap: getParamNames(method),
|
|
192
|
+
httpMethod: inferHttpMethod(methodSource, debug),
|
|
167
193
|
};
|
|
168
194
|
}
|
|
169
195
|
});
|
|
170
196
|
}
|
|
197
|
+
if (debug) {
|
|
198
|
+
console.log("[buildMethodMap] Methods:", Object.keys(methodMap));
|
|
199
|
+
}
|
|
171
200
|
return methodMap as MethodMap;
|
|
172
201
|
}
|
|
173
202
|
|
|
174
|
-
const
|
|
175
|
-
const effectiveFetch = fetchApi || defaultFetch;
|
|
176
|
-
if (!effectiveFetch) {
|
|
177
|
-
throw new Error(
|
|
178
|
-
"No fetch implementation available for fetching temp token"
|
|
179
|
-
);
|
|
180
|
-
}
|
|
203
|
+
const methodMap = buildMethodMap();
|
|
181
204
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
);
|
|
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
|
+
});
|
|
190
211
|
|
|
191
212
|
if (!response.ok) {
|
|
192
213
|
const errorBody: { message?: string } = await response.json();
|
|
@@ -198,204 +219,52 @@ export function quickbase(config: QuickbaseConfig): QuickbaseClient {
|
|
|
198
219
|
}
|
|
199
220
|
|
|
200
221
|
const tokenResult = await response.json();
|
|
222
|
+
console.log(`[fetchTempToken] Response:`, tokenResult);
|
|
201
223
|
const token = tokenResult.temporaryAuthorization;
|
|
202
224
|
if (!token) {
|
|
203
225
|
throw new Error("No temporary token returned from API");
|
|
204
226
|
}
|
|
205
|
-
tokenCache.set(dbid, token);
|
|
227
|
+
tokenCache.set(dbid, token, tempTokenLifespan);
|
|
206
228
|
if (debug) {
|
|
207
229
|
console.log(`Fetched and cached new token for dbid: ${dbid}`, token);
|
|
208
230
|
}
|
|
209
231
|
return token;
|
|
210
232
|
};
|
|
211
233
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
throw new Error(`Method ${methodName} not found`);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Safely handle body extraction
|
|
223
|
-
const hasBody = "body" in params && params.body !== undefined;
|
|
224
|
-
const body = hasBody ? (params as any).body : undefined;
|
|
225
|
-
const restParams: any = hasBody
|
|
226
|
-
? Object.fromEntries(
|
|
227
|
-
Object.entries(params).filter(([key]) => key !== "body")
|
|
228
|
-
)
|
|
229
|
-
: { ...params };
|
|
230
|
-
|
|
231
|
-
// Construct requestParameters with 'generated' for body
|
|
232
|
-
const requestParameters: any = {
|
|
233
|
-
...restParams,
|
|
234
|
-
...(body ? { generated: { ...body } } : {}),
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
let requestOptions: RequestInit = {
|
|
238
|
-
credentials: "omit",
|
|
239
|
-
};
|
|
240
|
-
|
|
241
|
-
const selectedToken =
|
|
242
|
-
initialTempToken || (userToken && !useTempTokens ? userToken : undefined);
|
|
243
|
-
|
|
244
|
-
if (methodName === "getTempTokenDBID" && useTempTokens) {
|
|
245
|
-
const dbid = extractDbid(params, "No dbid provided for getTempTokenDBID");
|
|
246
|
-
const cachedToken = tokenCache.get(dbid);
|
|
247
|
-
if (cachedToken) {
|
|
248
|
-
return { temporaryAuthorization: cachedToken } as ReturnType<
|
|
249
|
-
QuickbaseClient[K]
|
|
250
|
-
>;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
let authorizationToken = selectedToken;
|
|
255
|
-
if (useTempTokens && !authorizationToken) {
|
|
256
|
-
const dbid = extractDbid(
|
|
257
|
-
params,
|
|
258
|
-
`No dbid found in params for ${methodName} to fetch temp token`
|
|
259
|
-
);
|
|
260
|
-
const cachedToken = tokenCache.get(dbid);
|
|
261
|
-
authorizationToken = cachedToken || (await fetchTempToken(dbid));
|
|
262
|
-
if (methodName === "getTempTokenDBID") {
|
|
263
|
-
return { temporaryAuthorization: authorizationToken } as ReturnType<
|
|
264
|
-
QuickbaseClient[K]
|
|
265
|
-
>;
|
|
266
|
-
}
|
|
267
|
-
requestOptions.headers = {
|
|
268
|
-
...baseHeaders,
|
|
269
|
-
Authorization: `QB-TEMP-TOKEN ${authorizationToken}`,
|
|
270
|
-
};
|
|
271
|
-
} else if (authorizationToken) {
|
|
272
|
-
requestOptions.headers = {
|
|
273
|
-
...baseHeaders,
|
|
274
|
-
Authorization: `QB-USER-TOKEN ${authorizationToken}`,
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
if (debug) {
|
|
279
|
-
console.log(`[${methodName}] requestParameters:`, requestParameters);
|
|
280
|
-
console.log(`[${methodName}] requestOptions:`, requestOptions);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
try {
|
|
284
|
-
const response = await methodInfo.method(
|
|
285
|
-
requestParameters,
|
|
286
|
-
requestOptions
|
|
234
|
+
const proxyHandler: ProxyHandler<QuickbaseClient> = {
|
|
235
|
+
get: (_, prop: string) => {
|
|
236
|
+
console.log(
|
|
237
|
+
"[proxy] Accessing:",
|
|
238
|
+
prop,
|
|
239
|
+
"in methodMap:",
|
|
240
|
+
prop in methodMap
|
|
287
241
|
);
|
|
288
|
-
if (debug) {
|
|
289
|
-
console.log(`[${methodName}] rawResponse:`, response);
|
|
290
|
-
}
|
|
291
|
-
if (response instanceof Response) {
|
|
292
|
-
const contentType = response.headers.get("Content-Type")?.toLowerCase();
|
|
293
|
-
if (debug) {
|
|
294
|
-
console.log(`[${methodName}] contentType:`, contentType);
|
|
295
|
-
}
|
|
296
|
-
if (contentType?.includes("application/octet-stream")) {
|
|
297
|
-
return (await response.arrayBuffer()) as ReturnType<
|
|
298
|
-
QuickbaseClient[K]
|
|
299
|
-
>;
|
|
300
|
-
} else if (
|
|
301
|
-
contentType?.includes("application/x-yaml") ||
|
|
302
|
-
contentType?.includes("text/yaml")
|
|
303
|
-
) {
|
|
304
|
-
return (await response.text()) as ReturnType<QuickbaseClient[K]>;
|
|
305
|
-
} else if (contentType?.includes("application/json")) {
|
|
306
|
-
const jsonResponse = await response.json();
|
|
307
|
-
if (debug) {
|
|
308
|
-
console.log(`[${methodName}] jsonResponse:`, jsonResponse);
|
|
309
|
-
}
|
|
310
|
-
const transformedResponse = transformDates(
|
|
311
|
-
jsonResponse,
|
|
312
|
-
convertDates
|
|
313
|
-
);
|
|
314
|
-
if (debug) {
|
|
315
|
-
console.log(
|
|
316
|
-
`[${methodName}] transformedResponse:`,
|
|
317
|
-
transformedResponse
|
|
318
|
-
);
|
|
319
|
-
}
|
|
320
|
-
return transformedResponse as ReturnType<QuickbaseClient[K]>;
|
|
321
|
-
}
|
|
322
|
-
return response as ReturnType<QuickbaseClient[K]>;
|
|
323
|
-
} else {
|
|
324
|
-
if (debug) {
|
|
325
|
-
console.log(
|
|
326
|
-
`[${methodName}] non-Response return, applying transform:`,
|
|
327
|
-
response
|
|
328
|
-
);
|
|
329
|
-
}
|
|
330
|
-
const transformedResponse = transformDates(response, convertDates);
|
|
331
|
-
if (debug) {
|
|
332
|
-
console.log(
|
|
333
|
-
`[${methodName}] transformedNonResponse:`,
|
|
334
|
-
transformedResponse
|
|
335
|
-
);
|
|
336
|
-
}
|
|
337
|
-
return transformedResponse as ReturnType<QuickbaseClient[K]>;
|
|
338
|
-
}
|
|
339
|
-
} catch (error) {
|
|
340
|
-
if (
|
|
341
|
-
error instanceof ResponseError &&
|
|
342
|
-
error.response.status === 401 &&
|
|
343
|
-
retryCount < 1 &&
|
|
344
|
-
useTempTokens
|
|
345
|
-
) {
|
|
346
|
-
if (debug) {
|
|
347
|
-
console.log(
|
|
348
|
-
`Authorization error for ${methodName}, refreshing token:`,
|
|
349
|
-
error.message
|
|
350
|
-
);
|
|
351
|
-
}
|
|
352
|
-
const dbid = extractDbid(
|
|
353
|
-
params,
|
|
354
|
-
`No dbid to refresh token after authorization error`
|
|
355
|
-
);
|
|
356
|
-
authorizationToken = await fetchTempToken(dbid);
|
|
357
|
-
requestOptions.headers = {
|
|
358
|
-
...baseHeaders,
|
|
359
|
-
Authorization: `QB-TEMP-TOKEN ${authorizationToken}`,
|
|
360
|
-
};
|
|
361
|
-
if (debug) {
|
|
362
|
-
console.log(`Retrying ${methodName} with new token`);
|
|
363
|
-
}
|
|
364
|
-
return invokeMethod(methodName, params, retryCount + 1);
|
|
365
|
-
}
|
|
366
|
-
if (error instanceof ResponseError) {
|
|
367
|
-
let errorMessage = error.message;
|
|
368
|
-
try {
|
|
369
|
-
const errorBody: { message?: string } = await error.response.json();
|
|
370
|
-
if (debug) {
|
|
371
|
-
console.log(`Error response body for ${methodName}:`, errorBody);
|
|
372
|
-
}
|
|
373
|
-
errorMessage = errorBody.message || errorMessage;
|
|
374
|
-
} catch (e) {
|
|
375
|
-
// Silent fail on parse error
|
|
376
|
-
}
|
|
377
|
-
throw new Error(
|
|
378
|
-
`API Error: ${errorMessage} (Status: ${error.response.status})`
|
|
379
|
-
);
|
|
380
|
-
}
|
|
381
|
-
throw error;
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
const proxy = new Proxy<QuickbaseClient>({} as QuickbaseClient, {
|
|
386
|
-
get: (_, prop: string): ((params: any) => Promise<any>) | undefined => {
|
|
387
242
|
if (prop in methodMap) {
|
|
388
243
|
const methodName = prop as keyof QuickbaseClient;
|
|
389
244
|
return (params: Parameters<QuickbaseClient[typeof methodName]>[0]) =>
|
|
390
|
-
invokeMethod(
|
|
245
|
+
invokeMethod(
|
|
246
|
+
methodName,
|
|
247
|
+
params,
|
|
248
|
+
methodMap,
|
|
249
|
+
baseHeaders,
|
|
250
|
+
authStrategy,
|
|
251
|
+
rateLimiter,
|
|
252
|
+
fetchTempToken,
|
|
253
|
+
transformDates,
|
|
254
|
+
debug,
|
|
255
|
+
convertDates
|
|
256
|
+
);
|
|
391
257
|
}
|
|
258
|
+
console.log("[proxy] Method not found:", prop);
|
|
392
259
|
return undefined;
|
|
393
260
|
},
|
|
394
|
-
}
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const proxy = new Proxy<QuickbaseClient>({} as QuickbaseClient, proxyHandler);
|
|
395
264
|
|
|
396
265
|
if (debug) {
|
|
397
266
|
console.log("[createClient] Config:", config);
|
|
398
|
-
console.log("[createClient]
|
|
267
|
+
console.log("[createClient] Proxy created:", proxy);
|
|
399
268
|
}
|
|
400
269
|
|
|
401
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
|
}
|
package/tests/setup.ts
CHANGED
|
@@ -38,12 +38,14 @@ export const createClient = (
|
|
|
38
38
|
userToken,
|
|
39
39
|
debug: true,
|
|
40
40
|
fetchApi,
|
|
41
|
+
throttle: { rate: 10, burst: 10 }, // Throttle at 10 req/s with 10 burst capacity
|
|
41
42
|
...config, // Allow overrides if provided
|
|
42
43
|
});
|
|
43
44
|
console.log("[createClient] Config:", {
|
|
44
45
|
realm,
|
|
45
46
|
userToken,
|
|
46
47
|
debug: true,
|
|
48
|
+
throttle: { rate: 10, burst: 10 },
|
|
47
49
|
...config,
|
|
48
50
|
});
|
|
49
51
|
console.log("[createClient] Returning:", client);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// tests/vitest/qb/apps/getApp.test.ts
|
|
1
2
|
import { test, expect } from "vitest";
|
|
2
3
|
import { quickbase } from "../../../../src/quickbaseClient";
|
|
3
4
|
|
|
@@ -20,8 +21,8 @@ test(
|
|
|
20
21
|
expect(result).toEqual({
|
|
21
22
|
id: getAppId,
|
|
22
23
|
name: "qb-copy",
|
|
23
|
-
created: expect.any(Date),
|
|
24
|
-
updated: expect.any(Date),
|
|
24
|
+
created: expect.any(Date),
|
|
25
|
+
updated: expect.any(Date),
|
|
25
26
|
description: "",
|
|
26
27
|
timeZone: "(UTC-08:00) Pacific Time (US & Canada)",
|
|
27
28
|
dateFormat: "MM-DD-YYYY",
|
|
@@ -35,6 +36,9 @@ test(
|
|
|
35
36
|
mustBeRealmApproved: false,
|
|
36
37
|
useIPFilter: false,
|
|
37
38
|
},
|
|
39
|
+
ancestorId: undefined,
|
|
40
|
+
dataClassification: undefined,
|
|
41
|
+
variables: [{ name: "TestVar", value: "TestValue" }],
|
|
38
42
|
});
|
|
39
43
|
}
|
|
40
44
|
);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { test, expect } from "vitest";
|
|
2
|
+
import { quickbase } from "../../../../src/quickbaseClient";
|
|
3
|
+
|
|
4
|
+
test(
|
|
5
|
+
"QuickbaseClient Integration - getAppEvents > fetches real app events from QuickBase",
|
|
6
|
+
{ timeout: 10000 },
|
|
7
|
+
async () => {
|
|
8
|
+
const config = {
|
|
9
|
+
realm: process.env.QB_REALM || "",
|
|
10
|
+
userToken: process.env.QB_USER_TOKEN || "",
|
|
11
|
+
debug: true,
|
|
12
|
+
};
|
|
13
|
+
const client = quickbase(config);
|
|
14
|
+
const appId = "buwai2zpe"; // Same app ID as getApp test
|
|
15
|
+
|
|
16
|
+
console.log("Config used:", config);
|
|
17
|
+
const result = await client.getAppEvents({ appId });
|
|
18
|
+
console.log("Real API response:", result);
|
|
19
|
+
|
|
20
|
+
// Validate the response is an array of events
|
|
21
|
+
expect(result).toBeInstanceOf(Array);
|
|
22
|
+
|
|
23
|
+
// Check that each event has the required properties
|
|
24
|
+
result.forEach((event) => {
|
|
25
|
+
expect(event).toHaveProperty("type");
|
|
26
|
+
expect([
|
|
27
|
+
"qb-action",
|
|
28
|
+
"webhook",
|
|
29
|
+
"email-notification",
|
|
30
|
+
"subscription",
|
|
31
|
+
"reminder",
|
|
32
|
+
"automation",
|
|
33
|
+
]).toContain(event.type);
|
|
34
|
+
|
|
35
|
+
expect(event).toHaveProperty("owner");
|
|
36
|
+
expect(event.owner).toHaveProperty("email");
|
|
37
|
+
expect(typeof event.owner.email).toBe("string");
|
|
38
|
+
expect(event.owner).toHaveProperty("id");
|
|
39
|
+
expect(typeof event.owner.id).toBe("string");
|
|
40
|
+
expect(event.owner).toHaveProperty("name");
|
|
41
|
+
expect(typeof event.owner.name).toBe("string");
|
|
42
|
+
// userName is optional, so we don't enforce it
|
|
43
|
+
|
|
44
|
+
expect(event).toHaveProperty("isActive");
|
|
45
|
+
expect(typeof event.isActive).toBe("boolean");
|
|
46
|
+
|
|
47
|
+
expect(event).toHaveProperty("tableId");
|
|
48
|
+
expect(typeof event.tableId).toBe("string");
|
|
49
|
+
|
|
50
|
+
// Name is present for all types except automation
|
|
51
|
+
if (event.type !== "automation") {
|
|
52
|
+
expect(event).toHaveProperty("name");
|
|
53
|
+
expect(typeof event.name).toBe("string");
|
|
54
|
+
} else {
|
|
55
|
+
expect(event.name).toBeUndefined();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// URL is only present for automation
|
|
59
|
+
if (event.type === "automation") {
|
|
60
|
+
expect(event).toHaveProperty("url");
|
|
61
|
+
expect(typeof event.url).toBe("string");
|
|
62
|
+
} else {
|
|
63
|
+
expect(event.url).toBeUndefined();
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
);
|