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.
Files changed (82) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +46 -130
  3. package/dist/esm/quickbase.d.ts +1292 -125
  4. package/dist/esm/quickbase.js +666 -190
  5. package/dist/esm/quickbase.js.map +1 -1
  6. package/dist/esm/quickbase.min.js +1 -1
  7. package/dist/esm/quickbase.min.js.map +1 -1
  8. package/dist/umd/quickbase.umd.js +674 -189
  9. package/dist/umd/quickbase.umd.js.map +1 -1
  10. package/dist/umd/quickbase.umd.min.js +1 -1
  11. package/dist/umd/quickbase.umd.min.js.map +1 -1
  12. package/open-api/fix-spec-main.ts +69 -68
  13. package/open-api/generate-override-report.ts +214 -0
  14. package/open-api/generate-unified-interface.ts +162 -103
  15. package/open-api/output/override-report.html +11 -0
  16. package/open-api/output/override-report.json +14395 -58
  17. package/open-api/output/quickbase-fixed.json +583 -2
  18. package/open-api/schema/tags/fields.ts +331 -16
  19. package/open-api/schema/tags/index.ts +2 -4
  20. package/open-api/types/spec.ts +2 -1
  21. package/open-api/utils/generateJsDoc.ts +96 -0
  22. package/package.json +33 -33
  23. package/src/RateLimitError.ts +12 -0
  24. package/src/ThrottleBucket.ts +41 -0
  25. package/src/authorizationStrategy.ts +246 -0
  26. package/src/generated-unified/QuickbaseClient.ts +809 -119
  27. package/src/invokeMethod.ts +242 -0
  28. package/src/quickbaseClient.ts +117 -248
  29. package/src/rateLimiter.ts +25 -0
  30. package/src/tokenCache.ts +10 -26
  31. package/tests/setup.ts +2 -0
  32. package/tests/vitest/qb/apps/getApp.test.ts +6 -2
  33. package/tests/vitest/qb/apps/getAppEvents.test.ts +67 -0
  34. package/tests/vitest/qb/apps/updateApp.test.ts +58 -0
  35. package/tests/vitest/qb/auth/mockQuickbaseServer.ts +60 -0
  36. package/tests/vitest/qb/auth/retryUserToken401.test.ts +73 -0
  37. package/tests/vitest/qb/auth/ssoTokenIntegrationWithServer.test.ts +65 -0
  38. package/tests/vitest/qb/fields/createField.test.ts +1 -2
  39. package/tests/vitest/qb/fields/deleteFields.test.ts +10 -16
  40. package/tests/vitest/qb/fields/getFieldUsage.test.ts +58 -150
  41. package/tests/vitest/qb/fields/updateField.test.ts +93 -0
  42. package/tests/vitest/qb/formulas/runFormula.test.ts +96 -0
  43. package/tests/vitest/qb/index.ts +1 -0
  44. package/tests/vitest/qb/tables/createRelationship.test.ts +102 -0
  45. package/tests/vitest/qb/tables/deleteRelationship.test.ts +143 -0
  46. package/tests/vitest/qb/tables/updateRelationship.test.ts +250 -0
  47. package/tests/vitest/unit/ThrottleBucket.test.ts +95 -0
  48. package/tests/vitest/unit/apps/copyApp.test.ts +133 -40
  49. package/tests/vitest/unit/apps/getAppEvents.test.ts +193 -0
  50. package/tests/vitest/unit/apps/updateApp.test.ts +412 -0
  51. package/tests/vitest/unit/auth/fetchTempToken401.test.ts +7 -26
  52. package/tests/vitest/unit/auth/getTempToken.test.ts +42 -18
  53. package/tests/vitest/unit/auth/retryTempToken401.test.ts +95 -0
  54. package/tests/vitest/unit/auth/retryTempTokenMaxRetries.test.ts +235 -0
  55. package/tests/vitest/unit/auth/retryUserToken401.test.ts +69 -0
  56. package/tests/vitest/unit/auth/ssoTokenRefresh.test.ts +325 -0
  57. package/tests/vitest/unit/fields/createField.test.ts +180 -120
  58. package/tests/vitest/unit/fields/getFieldUsage.test.ts +47 -14
  59. package/tests/vitest/unit/fields/updateField.test.ts +285 -0
  60. package/tests/vitest/unit/formulas/runFormula.test.ts +205 -0
  61. package/tests/vitest/unit/index.ts +2 -1
  62. package/tests/vitest/unit/inferHttpMethod.test.ts +131 -0
  63. package/tests/vitest/unit/rateLimit.test.ts +191 -0
  64. package/tests/vitest/unit/tables/createRelationship.test.ts +212 -0
  65. package/tests/vitest/unit/tables/deleteRelationship.test.ts +97 -0
  66. package/tests/vitest/unit/tables/updateRelationship.test.ts +307 -0
  67. package/tests/vitest/unit/tokenLifespan.test.ts +204 -0
  68. package/tools/log-gen.ts +20 -7
  69. package/tools/log-gen.yaml +3446 -1848
  70. package/tsconfig.json +5 -2
  71. package/vitest.config.ts +2 -2
  72. package/notes.txt +0 -158
  73. package/tests/vitest/unit/auth/retryOn401.test.ts +0 -116
  74. package/tests/vitest/unit/auth/two401s.test.ts +0 -86
  75. package/tools/generate-types.ts +0 -29
  76. package/tools/openapi-schema-validator.ts +0 -38
  77. package/tools/spec-analyzer.ts +0 -163
  78. package/tools/split-spec.ts +0 -148
  79. package/tools/test-import.ts +0 -3
  80. package/tools/validate-spec.ts +0 -40
  81. /package/tests/playwright/qb/auth/{tokenPrefetch.test.ts → tempTokenPrefetch.test.ts} +0 -0
  82. /package/tests/playwright/qb/auth/{tokenRenewal.test.ts → tempTokenRenewal.test.ts} +0 -0
@@ -0,0 +1,242 @@
1
+ // src/invokeMethod.ts
2
+
3
+ import { AuthorizationStrategy, extractDbid } from "./authorizationStrategy";
4
+ import { RateLimiter } from "./rateLimiter";
5
+ import { RateLimitError } from "./RateLimitError";
6
+ import { ResponseError } from "./generated/runtime";
7
+ import { QuickbaseClient } from "./quickbaseClient";
8
+ import { GetTempTokenDBID200Response } from "./generated/models";
9
+
10
+ export type ApiMethod<K extends keyof QuickbaseClient> = (
11
+ requestParameters: Parameters<QuickbaseClient[K]>[0],
12
+ initOverrides?: RequestInit
13
+ ) => Promise<ReturnType<QuickbaseClient[K]>>;
14
+
15
+ export interface MethodInfo<K extends keyof QuickbaseClient> {
16
+ api: any;
17
+ method: ApiMethod<K>;
18
+ paramMap: string[];
19
+ httpMethod: string;
20
+ }
21
+
22
+ export async function invokeMethod<K extends keyof QuickbaseClient>(
23
+ methodName: K,
24
+ params: Parameters<QuickbaseClient[K]>[0] & {
25
+ dbid?: string;
26
+ tableId?: string;
27
+ appId?: string;
28
+ },
29
+ methodMap: { [P in keyof QuickbaseClient]: MethodInfo<P> },
30
+ baseHeaders: Record<string, string>,
31
+ authStrategy: AuthorizationStrategy,
32
+ rateLimiter: RateLimiter,
33
+ fetchTempToken: (dbid: string) => Promise<string>,
34
+ transformDates: (obj: any, convertStringsToDates: boolean) => any,
35
+ debug: boolean | undefined,
36
+ convertDates: boolean
37
+ ): Promise<ReturnType<QuickbaseClient[K]>> {
38
+ console.log("[invokeMethod] Starting for method:", methodName);
39
+
40
+ const methodInfo = methodMap[methodName];
41
+ if (!methodInfo) throw new Error(`Method ${methodName} not found`);
42
+
43
+ const hasBody = "body" in params && params.body !== undefined;
44
+ const body = hasBody ? params.body : undefined;
45
+ const restParams: any = hasBody
46
+ ? Object.fromEntries(
47
+ Object.entries(params).filter(([key]) => key !== "body")
48
+ )
49
+ : { ...params };
50
+ const requestParameters: any = {
51
+ ...restParams,
52
+ ...(hasBody ? { generated: body } : {}),
53
+ };
54
+
55
+ const requestOptions: RequestInit = {
56
+ credentials: methodName === "getTempTokenDBID" ? "include" : "omit",
57
+ method: methodInfo.httpMethod,
58
+ };
59
+
60
+ let dbid: string | undefined = extractDbid(params);
61
+ console.log("[invokeMethod] Extracted dbid:", dbid);
62
+
63
+ let token = dbid
64
+ ? await authStrategy.getToken(dbid)
65
+ : await authStrategy.getToken("");
66
+ console.log("[invokeMethod] Initial token:", token);
67
+
68
+ if (!token && dbid) {
69
+ token = await fetchTempToken(dbid);
70
+ authStrategy.applyHeaders(baseHeaders, token);
71
+ requestOptions.headers = { ...baseHeaders };
72
+ console.log("[invokeMethod] Fetched new token:", token);
73
+ }
74
+
75
+ if (token) {
76
+ authStrategy.applyHeaders(baseHeaders, token);
77
+ requestOptions.headers = { ...baseHeaders };
78
+ console.log(
79
+ "[invokeMethod] Token found, headers set:",
80
+ requestOptions.headers
81
+ );
82
+ if (methodName === "getTempTokenDBID") {
83
+ console.log("[invokeMethod] Using cached token:", token);
84
+ return { temporaryAuthorization: token } as ReturnType<
85
+ QuickbaseClient[K]
86
+ >;
87
+ }
88
+ }
89
+
90
+ if (debug) {
91
+ console.log(`[${methodName}] requestParameters:`, requestParameters);
92
+ console.log(`[${methodName}] requestOptions:`, requestOptions);
93
+ }
94
+
95
+ async function processResponse(
96
+ rawResponse: any
97
+ ): Promise<ReturnType<QuickbaseClient[K]>> {
98
+ if (debug) console.log(`[${methodName}] rawResponse:`, rawResponse);
99
+ if (rawResponse instanceof Response) {
100
+ const contentType = rawResponse.headers
101
+ .get("Content-Type")
102
+ ?.toLowerCase();
103
+ if (debug) console.log(`[${methodName}] contentType:`, contentType);
104
+ if (contentType?.includes("application/json")) {
105
+ const jsonResponse = await rawResponse.json();
106
+ return transformDates(jsonResponse, convertDates) as ReturnType<
107
+ QuickbaseClient[K]
108
+ >;
109
+ }
110
+ return rawResponse as ReturnType<QuickbaseClient[K]>;
111
+ }
112
+ if (rawResponse && typeof rawResponse.value === "function") {
113
+ const response = await rawResponse.value();
114
+ if (debug)
115
+ console.log(`[${methodName}] Resolved JSONApiResponse:`, response);
116
+ return transformDates(response, convertDates) as ReturnType<
117
+ QuickbaseClient[K]
118
+ >;
119
+ }
120
+ return transformDates(rawResponse, convertDates) as ReturnType<
121
+ QuickbaseClient[K]
122
+ >;
123
+ }
124
+
125
+ async function parseErrorResponse(
126
+ response: Response
127
+ ): Promise<{ message: string; status: number }> {
128
+ let message = "Unknown error";
129
+ let status = response.status || 500;
130
+ try {
131
+ const contentType =
132
+ response.headers?.get("Content-Type")?.toLowerCase() ||
133
+ "application/json";
134
+ if (
135
+ contentType.includes("application/json") &&
136
+ typeof response.json === "function"
137
+ ) {
138
+ const errorBody = await response.json();
139
+ if (
140
+ errorBody &&
141
+ typeof errorBody === "object" &&
142
+ "message" in errorBody
143
+ ) {
144
+ message = errorBody.message;
145
+ } else {
146
+ message = "Invalid error response format";
147
+ }
148
+ } else if (typeof response.text === "function") {
149
+ message = (await response.text()) || message;
150
+ }
151
+ } catch (e) {
152
+ if (debug) console.log(`[${methodName}] Error parsing response body:`, e);
153
+ message = "Failed to parse error response";
154
+ }
155
+ console.log(
156
+ "[invokeMethod] Parsed error - status:",
157
+ status,
158
+ "message:",
159
+ message
160
+ );
161
+ return { message, status };
162
+ }
163
+
164
+ let attempt = 0;
165
+ const maxAttempts = rateLimiter.maxRetries + 1;
166
+
167
+ while (attempt < maxAttempts) {
168
+ console.log("[invokeMethod] Attempt:", attempt + 1, "of", maxAttempts);
169
+ try {
170
+ await rateLimiter.throttle();
171
+ console.log("[invokeMethod] About to call API for method:", methodName);
172
+ const response = await methodInfo.method(
173
+ requestParameters,
174
+ requestOptions
175
+ );
176
+ console.log("[invokeMethod] API call completed for method:", methodName);
177
+ return await processResponse(response);
178
+ } catch (error: unknown) {
179
+ let status: number;
180
+ let message: string;
181
+ let response: Response;
182
+
183
+ console.log("[invokeMethod] Caught error:", error);
184
+
185
+ if (error instanceof ResponseError && error.response) {
186
+ response = error.response;
187
+ ({ message, status } = await parseErrorResponse(response));
188
+ } else if (error instanceof Response) {
189
+ response = error;
190
+ ({ message, status } = await parseErrorResponse(response));
191
+ } else {
192
+ if (debug) console.log(`[${methodName}] Unexpected error:`, error);
193
+ throw error; // Rethrow non-response errors
194
+ }
195
+
196
+ console.log("[invokeMethod] Handling error with status:", status);
197
+
198
+ if (status === 429) {
199
+ if (!(error instanceof ResponseError)) {
200
+ throw new Error("Expected ResponseError for 429 handling"); // Shouldn't happen
201
+ }
202
+ const delay = await rateLimiter.handle429(error, attempt + 1);
203
+ if (debug) console.log(`[${methodName}] 429 delay: ${delay}ms`);
204
+ if (attempt + 1 === maxAttempts) {
205
+ throw new RateLimitError(
206
+ `API Error: ${message} (Status: ${status})`,
207
+ status,
208
+ response.headers.get("Retry-After")
209
+ ? parseInt(response.headers.get("Retry-After")!, 10)
210
+ : undefined
211
+ );
212
+ }
213
+ await new Promise((resolve) => setTimeout(resolve, delay));
214
+ attempt++;
215
+ continue;
216
+ }
217
+
218
+ const newToken = await authStrategy.handleError(
219
+ status,
220
+ params,
221
+ fetchTempToken,
222
+ attempt,
223
+ maxAttempts,
224
+ debug,
225
+ methodName
226
+ );
227
+ if (newToken) {
228
+ token = newToken;
229
+ authStrategy.applyHeaders(baseHeaders, token);
230
+ requestOptions.headers = { ...baseHeaders };
231
+ attempt++;
232
+ if (debug)
233
+ console.log(
234
+ `[${methodName}] Retrying with token: ${token.substring(0, 10)}...`
235
+ );
236
+ continue;
237
+ }
238
+ throw new Error(`API Error: ${message} (Status: ${status})`);
239
+ }
240
+ }
241
+ throw new Error(`API Error: Exhausted retries after ${maxAttempts} attempts`);
242
+ }