quickbase-js 1.0.1-beta.0

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 (52) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +21 -0
  3. package/README.md +102 -0
  4. package/build.js +66 -0
  5. package/dist/quickbaseClient.d.ts +3888 -0
  6. package/dist/quickbaseClient.js +5812 -0
  7. package/dist/quickbaseClient.js.map +1 -0
  8. package/examples/react.js +22 -0
  9. package/examples/vanillajs.html +23 -0
  10. package/logGen.yaml +1820 -0
  11. package/package.json +72 -0
  12. package/playwright.config.ts +37 -0
  13. package/rollup.config.js +70 -0
  14. package/src/code-generation/fix-spec-definitions.ts +136 -0
  15. package/src/code-generation/fix-spec-main.ts +176 -0
  16. package/src/code-generation/fix-spec-paths.ts +163 -0
  17. package/src/code-generation/generate-unified-interface.ts +152 -0
  18. package/src/code-generation/output/quickbase-fixed.json +8435 -0
  19. package/src/code-generation/regenerate-client.ts +110 -0
  20. package/src/generated-unified/QuickbaseClient.ts +59 -0
  21. package/src/index.ts +1 -0
  22. package/src/log-gen.ts +122 -0
  23. package/src/quickbaseClient.ts +320 -0
  24. package/src/specs/QuickBase_RESTful_API_2025-03-04T06_22_39.725Z.json +46878 -0
  25. package/src/tokenCache.ts +51 -0
  26. package/src/utils.ts +5 -0
  27. package/test-artifacts/.last-run.json +4 -0
  28. package/test-artifacts/test-results.json +1801 -0
  29. package/test-results.json +2050 -0
  30. package/tests/playwright/qb/auth/cacheSwitching.test.ts +189 -0
  31. package/tests/playwright/qb/auth/fetchTempToken401.test.ts +217 -0
  32. package/tests/playwright/qb/auth/getAppWithUserToken.test.ts +151 -0
  33. package/tests/playwright/qb/auth/getTempToken.test.ts +180 -0
  34. package/tests/playwright/qb/auth/tokenPrefetch.test.ts +192 -0
  35. package/tests/playwright/qb/auth/tokenRenewal.test.ts +110 -0
  36. package/tests/setup.ts +19 -0
  37. package/tests/vitest/qb/apps/getApp.test.ts +46 -0
  38. package/tests/vitest/qb/fields/getFields.test.ts +234 -0
  39. package/tests/vitest/qb/index.ts +4 -0
  40. package/tests/vitest/qb/tables/getAppTables.test.ts +67 -0
  41. package/tests/vitest/unit/apps/getApp.test.ts +81 -0
  42. package/tests/vitest/unit/auth/fetchTempToken401.test.ts +59 -0
  43. package/tests/vitest/unit/auth/getTempToken.test.ts +83 -0
  44. package/tests/vitest/unit/auth/retryOn401.test.ts +72 -0
  45. package/tests/vitest/unit/auth/two401s.test.ts +77 -0
  46. package/tests/vitest/unit/fields/getFields.test.ts +115 -0
  47. package/tests/vitest/unit/index.ts +4 -0
  48. package/tests/vitest/unit/tables/getAppTables.test.ts +72 -0
  49. package/text.txt +104 -0
  50. package/tsconfig.json +28 -0
  51. package/versionrc.json +9 -0
  52. package/vitest.config.ts +21 -0
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env node
2
+ import { execSync } from "child_process";
3
+ import { existsSync } from "fs";
4
+ import { join } from "path";
5
+ import { fileURLToPath } from "url";
6
+ import fetch from "node-fetch";
7
+ import readline from "readline";
8
+
9
+ const CURRENT_JAR_VERSION = "7.12.0";
10
+ const MAVEN_METADATA_URL =
11
+ "https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/maven-metadata.xml";
12
+ const CODEGEN_DIR = join(fileURLToPath(import.meta.url), "..");
13
+ const JARS_DIR = join(CODEGEN_DIR, "..", "openapi-jars");
14
+ const SPEC_INPUT = join(CODEGEN_DIR, "output", "quickbase-fixed.json"); // Updated path
15
+ const OUTPUT_DIR = join(CODEGEN_DIR, "..", "generated");
16
+
17
+ async function getLatestVersion(): Promise<string> {
18
+ const response = await fetch(MAVEN_METADATA_URL);
19
+ if (!response.ok)
20
+ throw new Error(`Failed to fetch Maven metadata: ${response.statusText}`);
21
+ const text = await response.text();
22
+ const match = text.match(/<latest>(.*?)<\/latest>/);
23
+ if (!match)
24
+ throw new Error("Couldn’t parse latest version from Maven metadata");
25
+ return match[1];
26
+ }
27
+
28
+ async function checkAndPromptForUpdate(messages: string[]): Promise<string> {
29
+ const latestVersion = await getLatestVersion();
30
+ if (latestVersion === CURRENT_JAR_VERSION) {
31
+ messages.push(`Current version (${CURRENT_JAR_VERSION}) is the latest.`);
32
+ return CURRENT_JAR_VERSION;
33
+ }
34
+
35
+ messages.push(
36
+ `Newer version available: ${latestVersion} (current: ${CURRENT_JAR_VERSION})`
37
+ );
38
+ const rl = readline.createInterface({
39
+ input: process.stdin,
40
+ output: process.stdout,
41
+ });
42
+
43
+ return new Promise((resolve) => {
44
+ const prompt = "Do you want to update to the latest version? (y/n): ";
45
+ messages.push(prompt);
46
+ rl.question(prompt, (answer) => {
47
+ rl.close();
48
+ if (answer.toLowerCase() === "y" || answer.toLowerCase() === "yes") {
49
+ messages.push(`Switching to version ${latestVersion}...`);
50
+ resolve(latestVersion);
51
+ } else {
52
+ messages.push(`Sticking with version ${CURRENT_JAR_VERSION}.`);
53
+ resolve(CURRENT_JAR_VERSION);
54
+ }
55
+ });
56
+ });
57
+ }
58
+
59
+ async function ensureJarExists(
60
+ version: string,
61
+ messages: string[]
62
+ ): Promise<string> {
63
+ const jarPath = join(JARS_DIR, `openapi-generator-cli-${version}.jar`);
64
+ const jarUrl = `https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/${version}/openapi-generator-cli-${version}.jar`;
65
+
66
+ if (!existsSync(jarPath)) {
67
+ messages.push(`Downloading OpenAPI Generator CLI v${version}...`);
68
+ try {
69
+ execSync(`curl -L -o ${jarPath} ${jarUrl}`, { stdio: "inherit" });
70
+ messages.push(`Downloaded ${jarPath}`);
71
+ } catch (error) {
72
+ messages.push("Failed to download JAR.");
73
+ throw error;
74
+ }
75
+ } else {
76
+ messages.push(`Using existing ${jarPath}`);
77
+ }
78
+ return jarPath;
79
+ }
80
+
81
+ function regenerateClient(jarPath: string, messages: string[]) {
82
+ messages.push("Regenerating client from spec...");
83
+ const command = `java -jar ${jarPath} generate -i ${SPEC_INPUT} -g typescript-fetch -o ${OUTPUT_DIR}`;
84
+ try {
85
+ execSync(command, { stdio: "inherit" });
86
+ messages.push("Client regeneration complete.");
87
+ } catch (error) {
88
+ messages.push("Regeneration failed.");
89
+ throw error;
90
+ }
91
+ }
92
+
93
+ async function main() {
94
+ const messages: string[] = [];
95
+ try {
96
+ const versionToUse = await checkAndPromptForUpdate(messages);
97
+ const jarPath = await ensureJarExists(versionToUse, messages);
98
+ regenerateClient(jarPath, messages);
99
+ } catch (error) {
100
+ console.error("Error occurred during process:", error);
101
+ } finally {
102
+ console.log("\n--- Key Messages ---");
103
+ messages.forEach((msg) => console.log(msg));
104
+ }
105
+ }
106
+
107
+ main().catch((error) => {
108
+ console.error("Error in regeneration process:", error);
109
+ process.exit(1);
110
+ });
@@ -0,0 +1,59 @@
1
+ // Generated on 2025-03-10T01:23:11.216Z
2
+ import { App, Field, GetTempTokenDBID200Response, Record, ReportRunResponse, Table, Upsert200Response, Upsert207Response } from "../generated/models";
3
+
4
+ export interface QuickbaseClient {
5
+ createApp: (params: { generated?: any }) => Promise<{ [key: string]: any }>;
6
+ getApp: (params: { appId: string }) => Promise<App>;
7
+ getAppEvents: (params: { appId: string }) => Promise<any[]>;
8
+ copyApp: (params: { appId: string; generated?: any }) => Promise<{ [key: string]: any }>;
9
+ getAppTables: (params: { appId: string }) => Promise<Table[]>;
10
+ getTable: (params: { appId: string; tableId: string }) => Promise<{ [key: string]: any }>;
11
+ updateTable: (params: { appId: string; tableId: string; generated?: { [key: string]: any } }) => Promise<{ [key: string]: any }>;
12
+ deleteTable: (params: { appId: string; tableId: string }) => Promise<{ [key: string]: any }>;
13
+ getRelationships: (params: { skip?: number; tableId: string }) => Promise<{ [key: string]: any }>;
14
+ createRelationship: (params: { tableId: string; generated?: any }) => Promise<{ [key: string]: any }>;
15
+ updateRelationship: (params: { tableId: string; relationshipId: any; generated?: any }) => Promise<{ [key: string]: any }>;
16
+ deleteRelationship: (params: { tableId: string; relationshipId: any }) => Promise<{ [key: string]: any }>;
17
+ getTableReports: (params: { tableId: string }) => Promise<any[]>;
18
+ getReport: (params: { tableId: string; reportId: string }) => Promise<{ [key: string]: any }>;
19
+ runReport: (params: { reportId: string; generated: any }) => Promise<ReportRunResponse[]>;
20
+ getFields: (params: { tableId: string; includeFieldPerms?: boolean }) => Promise<Field[]>;
21
+ getField: (params: { tableId: string; includeFieldPerms?: boolean; fieldId: number }) => Promise<{ [key: string]: any }>;
22
+ updateField: (params: { tableId: string; fieldId: number; generated?: any }) => Promise<{ [key: string]: any }>;
23
+ getFieldsUsage: (params: { tableId: string; skip?: number; top?: number }) => Promise<any[]>;
24
+ getFieldUsage: (params: { tableId: string; fieldId: number }) => Promise<any[]>;
25
+ runFormula: (params: { generated?: any }) => Promise<{ [key: string]: any }>;
26
+ upsert: (params: { generated?: { [key: string]: any } }) => Promise<{ [key: string]: any }>;
27
+ deleteRecords: (params: { generated?: { [key: string]: any } }) => Promise<{ [key: string]: any }>;
28
+ upsertRecords: (params: { generated: { [key: string]: string }[] }) => Promise<Upsert200Response[] | Upsert207Response[]>;
29
+ getTempTokenDBID: (params: { dbid: string }) => Promise<GetTempTokenDBID200Response>;
30
+ exchangeSsoToken: (params: { generated?: any }) => Promise<{ [key: string]: any }>;
31
+ cloneUserToken: (params: { generated?: any }) => Promise<{ [key: string]: any }>;
32
+ transferUserToken: (params: { generated?: any }) => Promise<{ [key: string]: any }>;
33
+ deactivateUserToken: (params: { }) => Promise<{ [key: string]: any }>;
34
+ deleteUserToken: (params: { }) => Promise<{ [key: string]: any }>;
35
+ downloadFile: (params: { tableId: string; recordId: number; fieldId: number; versionNumber: number }) => Promise<any>;
36
+ deleteFile: (params: { tableId: string; recordId: number; fieldId: number; versionNumber: number }) => Promise<{ [key: string]: any }>;
37
+ getUsers: (params: { accountId?: any; generated?: { [key: string]: any } }) => Promise<{ [key: string]: any }>;
38
+ denyUsers: (params: { accountId?: any; generated?: string[] }) => Promise<{ [key: string]: any }>;
39
+ denyUsersAndGroups: (params: { accountId?: any; shouldDeleteFromGroups: boolean; generated?: string[] }) => Promise<{ [key: string]: any }>;
40
+ undenyUsers: (params: { accountId?: any; generated?: string[] }) => Promise<{ [key: string]: any }>;
41
+ addMembersToGroup: (params: { gid: any; generated?: string[] }) => Promise<{ [key: string]: any }>;
42
+ removeMembersFromGroup: (params: { gid: any; generated?: string[] }) => Promise<{ [key: string]: any }>;
43
+ addManagersToGroup: (params: { gid: any; generated?: string[] }) => Promise<{ [key: string]: any }>;
44
+ removeManagersFromGroup: (params: { gid: any; generated?: string[] }) => Promise<{ [key: string]: any }>;
45
+ addSubgroupsToGroup: (params: { gid: any; generated?: string[] }) => Promise<{ [key: string]: any }>;
46
+ removeSubgroupsFromGroup: (params: { gid: any; generated?: string[] }) => Promise<{ [key: string]: any }>;
47
+ audit: (params: { generated?: any }) => Promise<{ [key: string]: any }>;
48
+ platformAnalyticReads: (params: { day?: string }) => Promise<{ [key: string]: any }>;
49
+ platformAnalyticEventSummaries: (params: { accountId?: any; generated?: any }) => Promise<any>;
50
+ exportSolution: (params: { solutionId: string; qBLVersion?: string }) => Promise<any>;
51
+ updateSolution: (params: { solutionId: string; generated?: any; xQBLErrorsAsSuccess?: boolean }) => Promise<any>;
52
+ createSolution: (params: { generated?: any; xQBLErrorsAsSuccess?: boolean }) => Promise<any>;
53
+ exportSolutionToRecord: (params: { solutionId: string; tableId: string; fieldId: number; xQBLErrorsAsSuccess?: boolean; qBLVersion?: string }) => Promise<any>;
54
+ createSolutionFromRecord: (params: { tableId: string; fieldId: number; recordId: number; xQBLErrorsAsSuccess?: boolean }) => Promise<any>;
55
+ updateSolutionToRecord: (params: { solutionId: string; tableId: string; fieldId: number; recordId: number; xQBLErrorsAsSuccess?: boolean }) => Promise<any>;
56
+ changesetSolution: (params: { solutionId: string; generated?: any; xQBLErrorsAsSuccess?: boolean }) => Promise<any>;
57
+ changesetSolutionFromRecord: (params: { solutionId: string; tableId: string; fieldId: number; recordId: number; xQBLErrorsAsSuccess?: boolean }) => Promise<any>;
58
+ generateDocument: (params: { templateId: any; tableId: string; recordId?: any; filename: string; accept?: string; format?: string; margin?: string; unit?: string; pageSize?: string; orientation?: string; realm?: string }) => Promise<{ [key: string]: any }>;
59
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./quickbaseClient.ts";
package/src/log-gen.ts ADDED
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env node
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { fileURLToPath } from "url";
5
+ import yaml from "js-yaml"; // Requires "js-yaml": "^4.1.0" in package.json
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+
10
+ const rootDir: string = "./";
11
+ const excludeDirs: string[] = ["node_modules", ".git"];
12
+
13
+ const importantFolders: string[] = [];
14
+
15
+ const importantFiles: string[] = [
16
+ "package.json",
17
+ "tsconfig.json",
18
+ "rollup.config.js",
19
+ "vitest.config.ts",
20
+ "generated-unified/QuickbaseClient.ts",
21
+ "src/quickbaseClient.ts",
22
+ "src/tokenCache.ts",
23
+ "src/code-generation/fix-spec-main.ts",
24
+ "src/code-generation/fix-spec-paths.ts",
25
+ "src/code-generation/fix-spec-definitions.ts",
26
+ "src/code-generation/regenerate-client.ts",
27
+ "src/code-generation/generate-unified-interface.ts",
28
+ ];
29
+
30
+ const projectGoals: string[] = [
31
+ "Implement a QuickBase API client in TypeScript.",
32
+ "Generate client methods from the QuickBase RESTful API spec.",
33
+ "Provide a simple and intuitive interface for interacting with QuickBase.",
34
+ "Support both Node.js and browser environments.",
35
+ "Use case: JS frameworks like React, Vue, and Angular hosted in QuickBase code pages.",
36
+ "Node.js is supported using User Tokens",
37
+ "Use OpenAPI to generate the TypeScript types and methods.",
38
+ "Use the latest ES6+ features and TypeScript features.",
39
+ "#",
40
+ "No manual updating methods when the QuickBase API changes.",
41
+ "The proxy magic dynamically maps generated methods.",
42
+ "No manual endpoint definitions—endpoints come from the generated apis (e.g., AppsApi.getApp).",
43
+ "#",
44
+ "Per-Instance Store: tokenCache is defined per quickbaseClient call, so each instance has its own isolated TokenCache.",
45
+ "No Singleton: No shared state—tokens are fetched and cached within the current instance.",
46
+ ];
47
+
48
+ interface TreeNode {
49
+ name: string;
50
+ type: "directory" | "file";
51
+ contents?: string;
52
+ children?: TreeNode[];
53
+ }
54
+
55
+ function buildTree(dir: string): TreeNode {
56
+ const node: TreeNode = {
57
+ name: path.basename(dir) || "",
58
+ type: "directory",
59
+ children: [],
60
+ };
61
+
62
+ const files: fs.Dirent[] = fs.readdirSync(dir, { withFileTypes: true });
63
+
64
+ files.forEach((file: fs.Dirent) => {
65
+ const filePath: string = path.join(dir, file.name);
66
+ const relativePath: string = path
67
+ .relative(rootDir, filePath)
68
+ .replace(/\\/g, "/");
69
+
70
+ if (file.isDirectory() && excludeDirs.includes(file.name)) return;
71
+
72
+ const childNode: TreeNode = {
73
+ name: file.name,
74
+ type: file.isDirectory() ? "directory" : "file",
75
+ };
76
+
77
+ if (file.isDirectory()) {
78
+ childNode.children = buildTree(filePath).children;
79
+ } else {
80
+ const isInImportantFolder = importantFolders.some(
81
+ (folder) =>
82
+ relativePath.startsWith(folder + "/") || relativePath === folder
83
+ );
84
+ if (importantFiles.includes(relativePath) || isInImportantFolder) {
85
+ try {
86
+ childNode.contents = fs.readFileSync(filePath, "utf8");
87
+ } catch (err: unknown) {
88
+ childNode.contents = `[Error reading file: ${
89
+ (err as Error).message
90
+ }]`;
91
+ }
92
+ }
93
+ }
94
+
95
+ node.children!.push(childNode);
96
+ });
97
+
98
+ return node;
99
+ }
100
+
101
+ function generateTreeSnapshot(): string {
102
+ const snapshot = {
103
+ project: "quickbase-js",
104
+ date: new Date().toLocaleDateString(),
105
+ root: path.resolve(rootDir),
106
+ goals: projectGoals,
107
+ tree: buildTree(rootDir),
108
+ };
109
+
110
+ return yaml.dump(snapshot, { lineWidth: -1 }); // No line wrapping for readability
111
+ }
112
+
113
+ try {
114
+ const output: string = generateTreeSnapshot();
115
+ fs.writeFileSync("logGen.yaml", output, "utf8");
116
+ const lineCount: number = output.split("\n").length - 1; // Exclude trailing newline
117
+ console.log("logGen.yaml");
118
+ console.log(`Lines: ${lineCount}`);
119
+ } catch (error: unknown) {
120
+ console.error("Failed to generate tree snapshot:", (error as Error).message);
121
+ process.exit(1);
122
+ }
@@ -0,0 +1,320 @@
1
+ // src/quickbaseClient.ts
2
+ import { QuickbaseClient as IQuickbaseClient } from "./generated-unified/QuickbaseClient.ts";
3
+ import {
4
+ Configuration,
5
+ HTTPHeaders,
6
+ ResponseError,
7
+ } from "./generated/runtime.ts";
8
+ import * as apis from "./generated/apis/index.ts";
9
+ import { simplifyName } from "./utils.ts";
10
+ import { TokenCache } from "./tokenCache.ts";
11
+
12
+ // Re-export all model types from generated/models
13
+ export * from "./generated/models";
14
+
15
+ // Rename the interface to avoid naming conflict if needed
16
+ export interface QuickbaseClient extends IQuickbaseClient {}
17
+
18
+ export interface QuickbaseConfig {
19
+ realm: string;
20
+ userToken?: string;
21
+ tempToken?: string;
22
+ useTempTokens?: boolean;
23
+ debug?: boolean;
24
+ fetchApi?: typeof fetch;
25
+ }
26
+
27
+ export interface TempTokenParams {
28
+ appId?: string;
29
+ tableId?: string;
30
+ dbid?: string;
31
+ }
32
+
33
+ type ApiMethod<K extends keyof QuickbaseClient> = (
34
+ requestParameters: Parameters<QuickbaseClient[K]>[0],
35
+ initOverrides?: RequestInit
36
+ ) => Promise<ReturnType<QuickbaseClient[K]>>;
37
+
38
+ interface MethodInfo<K extends keyof QuickbaseClient> {
39
+ api: any;
40
+ method: ApiMethod<K>;
41
+ paramMap: string[];
42
+ }
43
+
44
+ type MethodMap = { [K in keyof QuickbaseClient]: MethodInfo<K> };
45
+
46
+ const getParamNames = (fn: (...args: any[]) => any): string[] =>
47
+ fn
48
+ .toString()
49
+ .slice(fn.toString().indexOf("(") + 1, fn.toString().indexOf(")"))
50
+ .split(",")
51
+ .map((p) => p.trim().split("=")[0].trim())
52
+ .filter((p) => p && !p.match(/^\{/) && p !== "options");
53
+
54
+ // New utility function to extract dbid
55
+ const extractDbid = (
56
+ params: Partial<TempTokenParams>,
57
+ errorMessage: string
58
+ ): string => {
59
+ const dbid = params.appId || params.tableId || params.dbid;
60
+ if (!dbid) {
61
+ throw new Error(errorMessage);
62
+ }
63
+ return dbid;
64
+ };
65
+
66
+ export function quickbaseClient(config: QuickbaseConfig): QuickbaseClient {
67
+ const {
68
+ realm,
69
+ userToken,
70
+ tempToken: initialTempToken,
71
+ useTempTokens,
72
+ fetchApi,
73
+ debug,
74
+ } = config;
75
+ const baseUrl = `https://api.quickbase.com/v1`;
76
+
77
+ const tokenCache = new TokenCache();
78
+
79
+ const headers: HTTPHeaders = {
80
+ "QB-Realm-Hostname": `${realm}.quickbase.com`,
81
+ "Content-Type": "application/json",
82
+ };
83
+
84
+ if (initialTempToken) {
85
+ headers["Authorization"] = `QB-TEMP-TOKEN ${initialTempToken}`;
86
+ } else if (userToken && !useTempTokens) {
87
+ headers["Authorization"] = `QB-USER-TOKEN ${userToken}`;
88
+ }
89
+
90
+ const defaultFetch: typeof fetch | undefined =
91
+ typeof globalThis.window !== "undefined"
92
+ ? globalThis.window.fetch.bind(globalThis.window)
93
+ : undefined;
94
+ const configuration = new Configuration({
95
+ basePath: baseUrl,
96
+ headers: { ...headers },
97
+ fetchApi: fetchApi || defaultFetch,
98
+ credentials: useTempTokens ? "include" : "omit",
99
+ });
100
+
101
+ if (!configuration.fetchApi && typeof globalThis.window === "undefined") {
102
+ throw new Error(
103
+ "fetchApi must be provided in non-browser environments (e.g., Node.js)"
104
+ );
105
+ }
106
+
107
+ const apiInstances = Object.fromEntries(
108
+ Object.entries(apis)
109
+ .filter(([name]) => name.endsWith("Api"))
110
+ .map(([name, ApiClass]) => [
111
+ name.replace("Api", "").toLowerCase(),
112
+ new ApiClass(configuration),
113
+ ])
114
+ );
115
+
116
+ const methodMap = buildMethodMap();
117
+
118
+ function buildMethodMap(): MethodMap {
119
+ const methodMap: Partial<MethodMap> = {};
120
+ const isValidMethod = (name: string) =>
121
+ !name.startsWith("_") &&
122
+ name !== "constructor" &&
123
+ !["Middleware", "Pre", "Post", "Raw"].some((s) => name.includes(s));
124
+
125
+ for (const [apiName, api] of Object.entries(apiInstances)) {
126
+ Object.getOwnPropertyNames(Object.getPrototypeOf(api))
127
+ .filter(
128
+ (name) =>
129
+ isValidMethod(name) &&
130
+ typeof api[name as keyof typeof api] === "function"
131
+ )
132
+ .forEach((rawMethodName) => {
133
+ const simplifiedName = simplifyName(
134
+ rawMethodName
135
+ ) as keyof QuickbaseClient;
136
+ const method = api[rawMethodName as keyof typeof api];
137
+ const boundMethod = method.bind(api) as unknown;
138
+ if (typeof boundMethod === "function" && boundMethod.length <= 2) {
139
+ methodMap[simplifiedName] = {
140
+ api,
141
+ method: boundMethod as ApiMethod<typeof simplifiedName>,
142
+ paramMap: getParamNames(method),
143
+ };
144
+ if (debug)
145
+ console.log(`Mapped ${rawMethodName} to ${simplifiedName}`);
146
+ }
147
+ });
148
+ }
149
+ return methodMap as MethodMap;
150
+ }
151
+
152
+ const fetchTempToken = async (dbid: string): Promise<string> => {
153
+ const effectiveFetch = fetchApi || defaultFetch;
154
+ if (!effectiveFetch) {
155
+ throw new Error(
156
+ "No fetch implementation available for fetching temp token"
157
+ );
158
+ }
159
+
160
+ const response = await effectiveFetch(
161
+ `https://api.quickbase.com/v1/auth/temporary/${dbid}`,
162
+ {
163
+ method: "GET",
164
+ headers: { ...headers },
165
+ credentials: "include",
166
+ }
167
+ );
168
+
169
+ if (!response.ok) {
170
+ const errorBody: { message?: string } = await response.json();
171
+ throw new Error(
172
+ `API Error: ${errorBody.message || "Unknown error"} (Status: ${
173
+ response.status
174
+ })`
175
+ );
176
+ }
177
+
178
+ const tokenResult = await response.json();
179
+ const token = tokenResult.temporaryAuthorization;
180
+ if (!token) {
181
+ throw new Error("No temporary token returned from API");
182
+ }
183
+ tokenCache.set(dbid, token);
184
+ if (debug) {
185
+ console.log(
186
+ `Fetched and cached new token for dbid: ${dbid}`,
187
+ token,
188
+ `Expires at: ${new Date(
189
+ Date.now() + (4 * 60 + 50) * 1000
190
+ ).toISOString()}`
191
+ );
192
+ }
193
+ return token;
194
+ };
195
+
196
+ const invokeMethod = async <K extends keyof QuickbaseClient>(
197
+ methodName: K,
198
+ params: Parameters<QuickbaseClient[K]>[0] & Partial<TempTokenParams>,
199
+ retryCount: number = 0
200
+ ): Promise<ReturnType<QuickbaseClient[K]>> => {
201
+ const methodInfo = methodMap[methodName];
202
+ if (!methodInfo) {
203
+ console.error(`Method ${methodName} not found in methodMap`, methodMap);
204
+ throw new Error(`Method ${methodName} not found`);
205
+ }
206
+
207
+ let token =
208
+ initialTempToken || (userToken && !useTempTokens ? userToken : undefined);
209
+ let initOverrides: RequestInit = {};
210
+
211
+ if (methodName === "getTempTokenDBID" && useTempTokens) {
212
+ const dbid = extractDbid(params, "No dbid provided for getTempTokenDBID");
213
+ const cachedToken = tokenCache.get(dbid);
214
+ if (cachedToken) {
215
+ return { temporaryAuthorization: cachedToken } as ReturnType<
216
+ QuickbaseClient[K]
217
+ >;
218
+ }
219
+ }
220
+
221
+ if (useTempTokens && !token) {
222
+ const dbid = extractDbid(
223
+ params,
224
+ `No dbid found in params for ${methodName} to fetch temp token`
225
+ );
226
+ if (debug)
227
+ console.log(`Cache state before fetch for ${dbid}:`, tokenCache.dump());
228
+ const cachedToken = tokenCache.get(dbid);
229
+ if (cachedToken) {
230
+ token = cachedToken;
231
+ if (debug) console.log(`Reusing cached token for dbid: ${dbid}`, token);
232
+ } else {
233
+ if (typeof globalThis.window === "undefined" && !fetchApi) {
234
+ throw new Error(
235
+ "Temporary tokens require a browser environment or a custom fetchApi with browser-like session support"
236
+ );
237
+ }
238
+ token = await fetchTempToken(dbid);
239
+ if (methodName === "getTempTokenDBID") {
240
+ return { temporaryAuthorization: token } as ReturnType<
241
+ QuickbaseClient[K]
242
+ >;
243
+ }
244
+ }
245
+ initOverrides.headers = {
246
+ ...headers,
247
+ Authorization: `QB-TEMP-TOKEN ${token}`,
248
+ };
249
+ }
250
+
251
+ if (debug) console.log(`Invoking ${methodName} with params:`, params);
252
+ if (debug)
253
+ console.log(`Calling method with args:`, [params, initOverrides]);
254
+ const args: [any, RequestInit | undefined] =
255
+ methodInfo.paramMap.length === 1 &&
256
+ methodInfo.paramMap[0] === "requestParameters"
257
+ ? [params, initOverrides]
258
+ : [params, initOverrides];
259
+
260
+ try {
261
+ const response = await methodInfo.method(...args);
262
+ if (debug) console.log(`Response from ${methodName}:`, response);
263
+ return response;
264
+ } catch (error) {
265
+ if (
266
+ error instanceof ResponseError &&
267
+ error.response.status === 401 &&
268
+ retryCount < 1
269
+ ) {
270
+ if (debug)
271
+ console.log(
272
+ `Authorization error for ${methodName}, refreshing token:`,
273
+ error.message
274
+ );
275
+ const dbid = extractDbid(
276
+ params,
277
+ `No dbid to refresh token after authorization error`
278
+ );
279
+ token = await fetchTempToken(dbid);
280
+ initOverrides.headers = {
281
+ ...headers,
282
+ Authorization: `QB-TEMP-TOKEN ${token}`,
283
+ };
284
+ if (debug) console.log(`Retrying ${methodName} with new token`);
285
+ return invokeMethod(methodName, params, retryCount + 1);
286
+ }
287
+ if (error instanceof ResponseError) {
288
+ let errorMessage = error.message;
289
+ try {
290
+ const errorBody: { message?: string } = await error.response.json();
291
+ console.log(`Error response body for ${methodName}:`, errorBody);
292
+ errorMessage = errorBody.message || errorMessage;
293
+ } catch (e) {
294
+ console.log(
295
+ `Failed to parse error response for ${methodName}:`,
296
+ error.message
297
+ );
298
+ }
299
+ throw new Error(
300
+ `API Error: ${errorMessage} (Status: ${error.response.status})`
301
+ );
302
+ }
303
+ throw error;
304
+ }
305
+ };
306
+
307
+ return new Proxy<QuickbaseClient>({} as QuickbaseClient, {
308
+ get: (_, prop: string): ((params: any) => Promise<any>) | undefined => {
309
+ if (prop in methodMap) {
310
+ const methodName = prop as keyof QuickbaseClient;
311
+ return (params: Parameters<QuickbaseClient[typeof methodName]>[0]) => {
312
+ if (debug) console.log(`Proxy called ${methodName} with:`, params);
313
+ return invokeMethod(methodName, params);
314
+ };
315
+ }
316
+ console.warn(`Method ${prop} not found in methodMap`);
317
+ return undefined;
318
+ },
319
+ });
320
+ }