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.
- package/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +102 -0
- package/build.js +66 -0
- package/dist/quickbaseClient.d.ts +3888 -0
- package/dist/quickbaseClient.js +5812 -0
- package/dist/quickbaseClient.js.map +1 -0
- package/examples/react.js +22 -0
- package/examples/vanillajs.html +23 -0
- package/logGen.yaml +1820 -0
- package/package.json +72 -0
- package/playwright.config.ts +37 -0
- package/rollup.config.js +70 -0
- package/src/code-generation/fix-spec-definitions.ts +136 -0
- package/src/code-generation/fix-spec-main.ts +176 -0
- package/src/code-generation/fix-spec-paths.ts +163 -0
- package/src/code-generation/generate-unified-interface.ts +152 -0
- package/src/code-generation/output/quickbase-fixed.json +8435 -0
- package/src/code-generation/regenerate-client.ts +110 -0
- package/src/generated-unified/QuickbaseClient.ts +59 -0
- package/src/index.ts +1 -0
- package/src/log-gen.ts +122 -0
- package/src/quickbaseClient.ts +320 -0
- package/src/specs/QuickBase_RESTful_API_2025-03-04T06_22_39.725Z.json +46878 -0
- package/src/tokenCache.ts +51 -0
- package/src/utils.ts +5 -0
- package/test-artifacts/.last-run.json +4 -0
- package/test-artifacts/test-results.json +1801 -0
- package/test-results.json +2050 -0
- package/tests/playwright/qb/auth/cacheSwitching.test.ts +189 -0
- package/tests/playwright/qb/auth/fetchTempToken401.test.ts +217 -0
- package/tests/playwright/qb/auth/getAppWithUserToken.test.ts +151 -0
- package/tests/playwright/qb/auth/getTempToken.test.ts +180 -0
- package/tests/playwright/qb/auth/tokenPrefetch.test.ts +192 -0
- package/tests/playwright/qb/auth/tokenRenewal.test.ts +110 -0
- package/tests/setup.ts +19 -0
- package/tests/vitest/qb/apps/getApp.test.ts +46 -0
- package/tests/vitest/qb/fields/getFields.test.ts +234 -0
- package/tests/vitest/qb/index.ts +4 -0
- package/tests/vitest/qb/tables/getAppTables.test.ts +67 -0
- package/tests/vitest/unit/apps/getApp.test.ts +81 -0
- package/tests/vitest/unit/auth/fetchTempToken401.test.ts +59 -0
- package/tests/vitest/unit/auth/getTempToken.test.ts +83 -0
- package/tests/vitest/unit/auth/retryOn401.test.ts +72 -0
- package/tests/vitest/unit/auth/two401s.test.ts +77 -0
- package/tests/vitest/unit/fields/getFields.test.ts +115 -0
- package/tests/vitest/unit/index.ts +4 -0
- package/tests/vitest/unit/tables/getAppTables.test.ts +72 -0
- package/text.txt +104 -0
- package/tsconfig.json +28 -0
- package/versionrc.json +9 -0
- 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
|
+
}
|