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
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { test, expect } from "vitest";
|
|
2
|
+
import { quickbase } from "../../../../src/quickbaseClient";
|
|
3
|
+
|
|
4
|
+
test(
|
|
5
|
+
"QuickbaseClient Integration - updateApp > updates real app data in 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 before
|
|
15
|
+
|
|
16
|
+
// Original app data (to restore later if needed)
|
|
17
|
+
const originalApp = await client.getApp({ appId });
|
|
18
|
+
console.log("Original app data:", originalApp);
|
|
19
|
+
|
|
20
|
+
// Update request
|
|
21
|
+
const updateRequest = {
|
|
22
|
+
name: "qb-copy-updated",
|
|
23
|
+
description: "Updated via integration test",
|
|
24
|
+
variables: [{ name: "TestVar", value: "TestValue" }],
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
console.log("Config used:", config);
|
|
28
|
+
console.log("Update request:", updateRequest);
|
|
29
|
+
const result = await client.updateApp({ appId, body: updateRequest });
|
|
30
|
+
console.log("Real API response:", result);
|
|
31
|
+
|
|
32
|
+
// Validate the response
|
|
33
|
+
expect(result).toHaveProperty("id", appId);
|
|
34
|
+
expect(result).toHaveProperty("name", "qb-copy-updated");
|
|
35
|
+
expect(result).toHaveProperty(
|
|
36
|
+
"description",
|
|
37
|
+
"Updated via integration test"
|
|
38
|
+
);
|
|
39
|
+
expect(result).toHaveProperty("variables");
|
|
40
|
+
expect(result.variables).toContainEqual({
|
|
41
|
+
name: "TestVar",
|
|
42
|
+
value: "TestValue",
|
|
43
|
+
});
|
|
44
|
+
expect(result).toHaveProperty("created");
|
|
45
|
+
expect(result.created).toBeInstanceOf(Date);
|
|
46
|
+
expect(result).toHaveProperty("updated");
|
|
47
|
+
expect(result.updated).toBeInstanceOf(Date);
|
|
48
|
+
|
|
49
|
+
// Optional: Restore original name to avoid permanent changes
|
|
50
|
+
const restoreRequest = {
|
|
51
|
+
name: originalApp.name,
|
|
52
|
+
description: originalApp.description,
|
|
53
|
+
variables: originalApp.variables,
|
|
54
|
+
};
|
|
55
|
+
await client.updateApp({ appId, body: restoreRequest });
|
|
56
|
+
console.log("Restored app to:", originalApp.name);
|
|
57
|
+
}
|
|
58
|
+
);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// tests/vitest/qb/auth/mockQuickbaseServer.ts
|
|
2
|
+
import express from "express";
|
|
3
|
+
|
|
4
|
+
export function startMockQuickbaseServer(port: number = 3000) {
|
|
5
|
+
const app = express();
|
|
6
|
+
app.use(express.json());
|
|
7
|
+
let failNextCall = false;
|
|
8
|
+
|
|
9
|
+
app.post("/v1/auth/oauth/token", (req, res) => {
|
|
10
|
+
const { subject_token } = req.body;
|
|
11
|
+
console.log("[Mock Server] Received SSO token request:", req.body);
|
|
12
|
+
if (!subject_token) {
|
|
13
|
+
return res.status(400).json({ message: "Missing SAML token" });
|
|
14
|
+
}
|
|
15
|
+
res.json({
|
|
16
|
+
access_token: "mock-temp-token",
|
|
17
|
+
token_type: "Bearer",
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
app.get("/v1/apps/:appId", (req, res) => {
|
|
22
|
+
const authHeader = req.headers.authorization;
|
|
23
|
+
console.log(
|
|
24
|
+
"[Mock Server] Received getApp request:",
|
|
25
|
+
req.params,
|
|
26
|
+
"Auth:",
|
|
27
|
+
authHeader
|
|
28
|
+
);
|
|
29
|
+
if (!authHeader || !authHeader.startsWith("QB-TEMP-TOKEN")) {
|
|
30
|
+
return res.status(401).json({ message: "Unauthorized" });
|
|
31
|
+
}
|
|
32
|
+
if (failNextCall) {
|
|
33
|
+
failNextCall = false;
|
|
34
|
+
return res.status(401).json({ message: "Token expired" });
|
|
35
|
+
}
|
|
36
|
+
res.json({
|
|
37
|
+
id: req.params.appId,
|
|
38
|
+
name: "Mock App",
|
|
39
|
+
created: "2025-03-19T12:00:00Z",
|
|
40
|
+
updated: "2025-03-19T12:00:00Z",
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
app.post("/mock/toggle-fail", (req, res) => {
|
|
45
|
+
failNextCall = true;
|
|
46
|
+
console.log("[Mock Server] Toggling next getApp call to fail with 401");
|
|
47
|
+
res.sendStatus(200);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const server = app.listen(port, () => {
|
|
51
|
+
console.log(`Mock QuickBase server running on port ${port}`);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
server,
|
|
56
|
+
close: () => server.close(),
|
|
57
|
+
toggleFail: () =>
|
|
58
|
+
fetch(`http://localhost:${port}/mock/toggle-fail`, { method: "POST" }),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, it, vi, expect, beforeEach } from "vitest";
|
|
2
|
+
import { quickbase, QuickbaseClient } from "../../../../src/quickbaseClient";
|
|
3
|
+
|
|
4
|
+
describe("QuickbaseClient Integration - User Token Retry on 401", () => {
|
|
5
|
+
let client: QuickbaseClient;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
const userToken = process.env.QB_USER_TOKEN;
|
|
9
|
+
const realm = process.env.QB_REALM;
|
|
10
|
+
const tableId = process.env.QB_TABLE_ID_1; // Consistent env var name
|
|
11
|
+
|
|
12
|
+
if (!userToken) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
"QB_USER_TOKEN environment variable is required for integration test"
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
if (!tableId) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
"QB_TABLE_ID_1 environment variable is required for integration test"
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const config = {
|
|
24
|
+
realm,
|
|
25
|
+
userToken,
|
|
26
|
+
debug: true,
|
|
27
|
+
};
|
|
28
|
+
console.log("[quickbaseTest] Config:", {
|
|
29
|
+
realm,
|
|
30
|
+
userToken: userToken.substring(0, 10) + "...",
|
|
31
|
+
tableId, // Log to confirm
|
|
32
|
+
debug: true,
|
|
33
|
+
});
|
|
34
|
+
client = quickbase(config);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it(
|
|
38
|
+
"successfully retrieves fields with user token, retrying on transient 401 if needed",
|
|
39
|
+
async () => {
|
|
40
|
+
const consoleSpy = vi.spyOn(console, "log");
|
|
41
|
+
|
|
42
|
+
const tableId = process.env.QB_TABLE_ID_1!; // Match beforeEach
|
|
43
|
+
let fields;
|
|
44
|
+
try {
|
|
45
|
+
fields = await client.getFields({ tableId });
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error("[integrationTest] Failed to retrieve fields:", error);
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
expect(fields).toBeInstanceOf(Array);
|
|
52
|
+
expect(fields.length).toBeGreaterThan(0);
|
|
53
|
+
expect(fields[0]).toHaveProperty("id");
|
|
54
|
+
expect(fields[0]).toHaveProperty("label");
|
|
55
|
+
|
|
56
|
+
console.log("[integrationTest] Fields retrieved:", fields.length);
|
|
57
|
+
|
|
58
|
+
const retryLog = consoleSpy.mock.calls.some((call) =>
|
|
59
|
+
call[0].includes("Retrying getFields with existing user token")
|
|
60
|
+
);
|
|
61
|
+
if (retryLog) {
|
|
62
|
+
console.log("[integrationTest] Retry on 401 detected");
|
|
63
|
+
} else {
|
|
64
|
+
console.log(
|
|
65
|
+
"[integrationTest] No 401 retries observed (expected if no transient failure)"
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
consoleSpy.mockRestore();
|
|
70
|
+
},
|
|
71
|
+
{ timeout: 10000 }
|
|
72
|
+
);
|
|
73
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// tests/vitest/qb/auth/ssoTokenIntegrationWithServer.test.ts
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect, beforeAll, afterAll, vi } from "vitest"; // Add 'vi' to imports
|
|
4
|
+
import { quickbase } from "../../../../src/quickbaseClient";
|
|
5
|
+
import { startMockQuickbaseServer } from "./mockQuickbaseServer";
|
|
6
|
+
|
|
7
|
+
describe("QuickbaseClient Integration - SSO with Mock Server", () => {
|
|
8
|
+
let mockServer: ReturnType<typeof startMockQuickbaseServer>;
|
|
9
|
+
const mockBaseUrl = "http://localhost:3000";
|
|
10
|
+
|
|
11
|
+
beforeAll(async () => {
|
|
12
|
+
mockServer = startMockQuickbaseServer(3000);
|
|
13
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
14
|
+
console.log("Server setup complete, port 3000 should be listening");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterAll(() => {
|
|
18
|
+
mockServer.close();
|
|
19
|
+
console.log("Server closed");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it(
|
|
23
|
+
"uses SSO token from mock server and retries on 401",
|
|
24
|
+
{ timeout: 10000 }, // Move timeout to second argument
|
|
25
|
+
async () => {
|
|
26
|
+
const consoleSpy = vi.spyOn(console, "log"); // Spy on console.log to capture logs
|
|
27
|
+
const client = quickbase({
|
|
28
|
+
realm: "testrealm",
|
|
29
|
+
samlToken: "mock-saml-token",
|
|
30
|
+
useSso: true,
|
|
31
|
+
fetchApi: (url, options) => {
|
|
32
|
+
const rewrittenUrl = url.replace(
|
|
33
|
+
"https://api.quickbase.com/v1",
|
|
34
|
+
mockBaseUrl + "/v1"
|
|
35
|
+
);
|
|
36
|
+
console.log(
|
|
37
|
+
"[fetchApi] Requesting:",
|
|
38
|
+
rewrittenUrl,
|
|
39
|
+
"Options:",
|
|
40
|
+
options
|
|
41
|
+
);
|
|
42
|
+
return fetch(rewrittenUrl, options);
|
|
43
|
+
},
|
|
44
|
+
debug: true,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
await mockServer.toggleFail();
|
|
48
|
+
const response = await client.getApp({ appId: "mockappid" });
|
|
49
|
+
|
|
50
|
+
expect(response).toBeDefined();
|
|
51
|
+
expect(response.id).toBe("mockappid");
|
|
52
|
+
expect(response.name).toBe("Mock App");
|
|
53
|
+
|
|
54
|
+
// Verify retry happened
|
|
55
|
+
const retryLog = consoleSpy.mock.calls.some((call) =>
|
|
56
|
+
call[0].includes(
|
|
57
|
+
"Authorization error for getApp (SSO), refreshing token"
|
|
58
|
+
)
|
|
59
|
+
);
|
|
60
|
+
expect(retryLog).toBe(true); // Ensure retry logic was triggered
|
|
61
|
+
|
|
62
|
+
consoleSpy.mockRestore();
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
});
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// tests/vitest/qb/fields/createField.test.ts (updated)
|
|
2
1
|
import { describe, it, expect, afterAll } from "vitest";
|
|
3
2
|
import { createClient, QB_TABLE_ID_1 } from "../../../setup.ts";
|
|
4
3
|
|
|
@@ -62,7 +61,7 @@ describe("QuickbaseClient Integration - createField", () => {
|
|
|
62
61
|
},
|
|
63
62
|
})
|
|
64
63
|
).rejects.toThrow("API Error: Bad Request (Status: 400)");
|
|
65
|
-
});
|
|
64
|
+
}, 10000); // Increased timeout to 10 seconds
|
|
66
65
|
|
|
67
66
|
it("fails with missing label", async () => {
|
|
68
67
|
await expect(
|
|
@@ -4,12 +4,10 @@ import { createClient, QB_TABLE_ID_1 } from "../../../setup.ts";
|
|
|
4
4
|
|
|
5
5
|
describe("QuickbaseClient Integration - deleteFields", () => {
|
|
6
6
|
const client = createClient();
|
|
7
|
-
|
|
8
7
|
let fieldIdsToDelete: number[] = [];
|
|
9
8
|
|
|
10
9
|
beforeAll(async () => {
|
|
11
10
|
try {
|
|
12
|
-
// Create test fields to delete
|
|
13
11
|
const fieldLabels = ["DeleteTest1", "DeleteTest2"];
|
|
14
12
|
for (const label of fieldLabels) {
|
|
15
13
|
const response = await client.createField({
|
|
@@ -28,7 +26,7 @@ describe("QuickbaseClient Integration - deleteFields", () => {
|
|
|
28
26
|
}
|
|
29
27
|
} catch (error) {
|
|
30
28
|
console.error("beforeAll failed:", error);
|
|
31
|
-
throw error;
|
|
29
|
+
throw error;
|
|
32
30
|
}
|
|
33
31
|
});
|
|
34
32
|
|
|
@@ -36,41 +34,37 @@ describe("QuickbaseClient Integration - deleteFields", () => {
|
|
|
36
34
|
try {
|
|
37
35
|
const response = await client.deleteFields({
|
|
38
36
|
tableId: QB_TABLE_ID_1,
|
|
39
|
-
body: {
|
|
40
|
-
fieldIds: fieldIdsToDelete,
|
|
41
|
-
},
|
|
37
|
+
body: { fieldIds: fieldIdsToDelete },
|
|
42
38
|
});
|
|
43
|
-
|
|
44
39
|
console.log("deleteFields response:", JSON.stringify(response, null, 2));
|
|
45
|
-
|
|
46
40
|
expect(response).toBeDefined();
|
|
47
41
|
expect(response.deletedFieldIds).toEqual(
|
|
48
42
|
expect.arrayContaining(fieldIdsToDelete)
|
|
49
43
|
);
|
|
50
|
-
expect(response.errors ?? []).toHaveLength(0);
|
|
44
|
+
expect(response.errors ?? []).toHaveLength(0);
|
|
51
45
|
} catch (error) {
|
|
52
46
|
console.error("deleteFields failed:", error);
|
|
53
|
-
throw error;
|
|
47
|
+
throw error;
|
|
54
48
|
}
|
|
55
49
|
});
|
|
56
50
|
|
|
57
|
-
it("handles partial success with errors", async () => {
|
|
51
|
+
it("handles partial success with errors", { timeout: 10000 }, async () => {
|
|
58
52
|
const newField = await client.createField({
|
|
59
53
|
tableId: QB_TABLE_ID_1,
|
|
60
|
-
body: {
|
|
61
|
-
label: "DeleteTest3",
|
|
62
|
-
fieldType: "text",
|
|
63
|
-
},
|
|
54
|
+
body: { label: "DeleteTest3", fieldType: "text" },
|
|
64
55
|
});
|
|
65
56
|
console.log(`Created field DeleteTest3 with ID ${newField.id}`);
|
|
66
57
|
|
|
67
|
-
const invalidFieldId = 999999;
|
|
58
|
+
const invalidFieldId = 999999;
|
|
68
59
|
const fieldIds = [newField.id, invalidFieldId];
|
|
69
60
|
|
|
61
|
+
console.log("Sending deleteFields request at:", new Date());
|
|
70
62
|
const response = await client.deleteFields({
|
|
71
63
|
tableId: QB_TABLE_ID_1,
|
|
72
64
|
body: { fieldIds },
|
|
73
65
|
});
|
|
66
|
+
console.log("deleteFields response received at:", new Date());
|
|
67
|
+
console.log("Response:", JSON.stringify(response, null, 2));
|
|
74
68
|
|
|
75
69
|
expect(response.deletedFieldIds).toContain(newField.id);
|
|
76
70
|
expect(response.deletedFieldIds).not.toContain(invalidFieldId);
|
|
@@ -1,166 +1,72 @@
|
|
|
1
|
-
// tests/vitest/qb/fields/
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
QB_REALM,
|
|
6
|
-
QB_USER_TOKEN,
|
|
7
|
-
QB_TABLE_ID_1,
|
|
8
|
-
} from "../../../setup.ts";
|
|
1
|
+
// tests/vitest/qb/fields/getFieldsUsage.test.ts
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect } from "vitest";
|
|
4
|
+
import { createClient, QB_TABLE_ID_1 } from "../../../setup.ts";
|
|
9
5
|
import { FieldUsage } from "@/generated/models";
|
|
10
6
|
|
|
11
|
-
describe("QuickbaseClient Integration -
|
|
7
|
+
describe("QuickbaseClient Integration - getFieldsUsage", () => {
|
|
12
8
|
const client = createClient();
|
|
13
|
-
const createdFieldIds: number[] = [];
|
|
14
|
-
|
|
15
|
-
afterAll(async () => {
|
|
16
|
-
if (createdFieldIds.length > 0) {
|
|
17
|
-
try {
|
|
18
|
-
const response = await client.deleteFields({
|
|
19
|
-
tableId: QB_TABLE_ID_1,
|
|
20
|
-
body: { fieldIds: createdFieldIds },
|
|
21
|
-
});
|
|
22
|
-
console.log(
|
|
23
|
-
`Cleaned up fields: ${JSON.stringify(response.deletedFieldIds)}`,
|
|
24
|
-
response.errors && response.errors.length > 0
|
|
25
|
-
? `Errors: ${response.errors}`
|
|
26
|
-
: ""
|
|
27
|
-
);
|
|
28
|
-
} catch (error) {
|
|
29
|
-
console.error("Cleanup failed:", error);
|
|
30
|
-
console.log(`Fields left in table: ${createdFieldIds}`);
|
|
31
|
-
}
|
|
32
|
-
} else {
|
|
33
|
-
console.log("No fields to clean up.");
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it(
|
|
38
|
-
"creates a field, retrieves its usage statistics, and deletes it",
|
|
39
|
-
async () => {
|
|
40
|
-
// Step 1: Create a field
|
|
41
|
-
const uniqueLabel = `TestField_${Date.now()}`;
|
|
42
|
-
console.log("Creating field with label:", uniqueLabel);
|
|
43
|
-
const createResponse = await client.createField({
|
|
44
|
-
tableId: QB_TABLE_ID_1,
|
|
45
|
-
body: {
|
|
46
|
-
label: uniqueLabel,
|
|
47
|
-
fieldType: "text",
|
|
48
|
-
fieldHelp: "Created for getFieldUsage integration test",
|
|
49
|
-
addToForms: true,
|
|
50
|
-
},
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
expect(createResponse.id).toBeDefined();
|
|
54
|
-
const fieldId = createResponse.id;
|
|
55
|
-
createdFieldIds.push(fieldId);
|
|
56
|
-
console.log(`Created field ${uniqueLabel} with ID ${fieldId}`);
|
|
57
|
-
|
|
58
|
-
// Brief delay to ensure field is registered
|
|
59
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
60
|
-
|
|
61
|
-
// Step 2: Get field usage with raw response logging
|
|
62
|
-
console.log("Fetching field usage for:", {
|
|
63
|
-
tableId: QB_TABLE_ID_1,
|
|
64
|
-
fieldId,
|
|
65
|
-
});
|
|
66
|
-
const rawResponse = await fetch(
|
|
67
|
-
`https://api.quickbase.com/v1/fields/usage/${fieldId}?tableId=${QB_TABLE_ID_1}`,
|
|
68
|
-
{
|
|
69
|
-
method: "GET",
|
|
70
|
-
headers: {
|
|
71
|
-
"QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
|
|
72
|
-
Authorization: `QB-USER-TOKEN ${QB_USER_TOKEN}`,
|
|
73
|
-
"Content-Type": "application/json",
|
|
74
|
-
},
|
|
75
|
-
}
|
|
76
|
-
);
|
|
77
|
-
const rawBody = await rawResponse.text();
|
|
78
|
-
console.log("Raw API response:", {
|
|
79
|
-
status: rawResponse.status,
|
|
80
|
-
statusText: rawResponse.statusText,
|
|
81
|
-
body: rawBody,
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
const responseArray: FieldUsage[] = await client.getFieldUsage({
|
|
85
|
-
fieldId,
|
|
86
|
-
tableId: QB_TABLE_ID_1,
|
|
87
|
-
});
|
|
88
|
-
console.log("Parsed API response (array):", responseArray);
|
|
89
9
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
expect(response).toBeDefined();
|
|
96
|
-
expect(response.field).toHaveProperty("id", fieldId);
|
|
97
|
-
expect(response.field).toHaveProperty("name", uniqueLabel);
|
|
98
|
-
expect(response.field).toHaveProperty("type", "text");
|
|
99
|
-
expect(typeof response.field.name).toBe("string");
|
|
100
|
-
expect(typeof response.field.type).toBe("string");
|
|
10
|
+
it("retrieves field usage statistics for a valid table", async () => {
|
|
11
|
+
console.log("Starting retrieval of full field usage list");
|
|
12
|
+
const response = await client.getFieldsUsage({
|
|
13
|
+
tableId: QB_TABLE_ID_1,
|
|
14
|
+
});
|
|
101
15
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
expect(
|
|
106
|
-
expect(
|
|
107
|
-
expect(
|
|
108
|
-
expect(
|
|
109
|
-
expect(
|
|
110
|
-
expect(
|
|
111
|
-
expect(
|
|
112
|
-
expect(
|
|
113
|
-
expect(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
expect(response.usage).toHaveProperty("roles");
|
|
120
|
-
expect(response.usage).toHaveProperty("tableImports");
|
|
121
|
-
expect(response.usage).toHaveProperty("tableRules");
|
|
122
|
-
expect(response.usage).toHaveProperty("webhooks");
|
|
16
|
+
expect(response).toBeInstanceOf(Array);
|
|
17
|
+
expect(response.length).toBeGreaterThan(0); // Assumes the table has fields
|
|
18
|
+
response.forEach((usage: FieldUsage) => {
|
|
19
|
+
expect(usage.field).toHaveProperty("id");
|
|
20
|
+
expect(usage.field).toHaveProperty("name");
|
|
21
|
+
expect(usage.field).toHaveProperty("type");
|
|
22
|
+
expect(usage.usage).toHaveProperty("actions");
|
|
23
|
+
expect(usage.usage.actions).toHaveProperty("count");
|
|
24
|
+
expect(typeof usage.usage.actions.count).toBe("number");
|
|
25
|
+
expect(usage.usage).toHaveProperty("dashboards");
|
|
26
|
+
expect(usage.usage).toHaveProperty("forms");
|
|
27
|
+
expect(usage.usage).toHaveProperty("reports");
|
|
28
|
+
});
|
|
29
|
+
console.log(
|
|
30
|
+
`Retrieved ${response.length} field usage entries for table ${QB_TABLE_ID_1}`
|
|
31
|
+
);
|
|
32
|
+
}, 15000); // 15-second timeout
|
|
123
33
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
34
|
+
it("retrieves field usage with skip and top parameters", async () => {
|
|
35
|
+
console.log(
|
|
36
|
+
"Starting retrieval of full field usage list for skip/top test"
|
|
37
|
+
);
|
|
38
|
+
const fullResponse = await client.getFieldsUsage({
|
|
39
|
+
tableId: QB_TABLE_ID_1,
|
|
40
|
+
});
|
|
41
|
+
console.log(`Full list retrieved: ${fullResponse.length} fields`);
|
|
129
42
|
|
|
130
|
-
|
|
131
|
-
|
|
43
|
+
const totalFields = fullResponse.length;
|
|
44
|
+
if (totalFields <= 1) {
|
|
45
|
+
console.log("Skipping skip/top test: table has too few fields");
|
|
46
|
+
return; // Skip if not enough fields to test pagination
|
|
47
|
+
}
|
|
132
48
|
|
|
133
|
-
console.log("
|
|
49
|
+
console.log("Starting retrieval with skip=1 and top=1");
|
|
50
|
+
const response = await client.getFieldsUsage({
|
|
134
51
|
tableId: QB_TABLE_ID_1,
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
await expect(
|
|
138
|
-
client.getFieldUsage({
|
|
139
|
-
fieldId: nonExistentFieldId,
|
|
140
|
-
tableId: QB_TABLE_ID_1,
|
|
141
|
-
})
|
|
142
|
-
).rejects.toMatchObject({
|
|
143
|
-
message: expect.stringContaining(
|
|
144
|
-
"API Error: No such field (Status: 404)"
|
|
145
|
-
),
|
|
52
|
+
skip: 1,
|
|
53
|
+
top: 1,
|
|
146
54
|
});
|
|
147
55
|
|
|
56
|
+
expect(response).toBeInstanceOf(Array);
|
|
57
|
+
expect(response.length).toBe(1); // Should return exactly 1 field
|
|
58
|
+
expect(response[0].field.id).not.toBe(fullResponse[0].field.id); // Should skip the first field
|
|
148
59
|
console.log(
|
|
149
|
-
`
|
|
60
|
+
`Retrieved ${response.length} field usage entries with skip=1 and top=1`
|
|
150
61
|
);
|
|
151
|
-
});
|
|
62
|
+
}, 15000); // 15-second timeout
|
|
152
63
|
|
|
153
|
-
it("fails with invalid table ID",
|
|
64
|
+
it("fails with invalid table ID", async () => {
|
|
154
65
|
const invalidTableId = "invalid_dbid";
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
console.log("Attempting to fetch usage with invalid table ID:", {
|
|
158
|
-
tableId: invalidTableId,
|
|
159
|
-
fieldId,
|
|
160
|
-
});
|
|
66
|
+
console.log(`Starting retrieval with invalid table ID ${invalidTableId}`);
|
|
67
|
+
const startTime = Date.now();
|
|
161
68
|
await expect(
|
|
162
|
-
client.
|
|
163
|
-
fieldId,
|
|
69
|
+
client.getFieldsUsage({
|
|
164
70
|
tableId: invalidTableId,
|
|
165
71
|
})
|
|
166
72
|
).rejects.toMatchObject({
|
|
@@ -168,7 +74,9 @@ describe("QuickbaseClient Integration - getFieldUsage", () => {
|
|
|
168
74
|
"API Error: Access denied (Status: 401)"
|
|
169
75
|
),
|
|
170
76
|
});
|
|
171
|
-
|
|
172
|
-
console.log(
|
|
173
|
-
|
|
77
|
+
const duration = Date.now() - startTime;
|
|
78
|
+
console.log(
|
|
79
|
+
`Confirmed 401 for invalid table ID ${invalidTableId} in ${duration}ms`
|
|
80
|
+
);
|
|
81
|
+
}, 30000); // Increased to 30-second timeout
|
|
174
82
|
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// tests/vitest/qb/fields/updateField.test.ts
|
|
2
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
3
|
+
import { createClient, QB_TABLE_ID_1 } from "../../../setup.ts";
|
|
4
|
+
|
|
5
|
+
describe("QuickbaseClient Integration - updateField", () => {
|
|
6
|
+
const client = createClient();
|
|
7
|
+
let fieldIdToUpdate: number;
|
|
8
|
+
|
|
9
|
+
// Setup: Create a test field to update
|
|
10
|
+
beforeAll(async () => {
|
|
11
|
+
try {
|
|
12
|
+
const response = await client.createField({
|
|
13
|
+
tableId: QB_TABLE_ID_1,
|
|
14
|
+
body: {
|
|
15
|
+
label: "UpdateTestField",
|
|
16
|
+
fieldType: "text",
|
|
17
|
+
fieldHelp: "Test field for updating",
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
fieldIdToUpdate = response.id;
|
|
21
|
+
console.log(`Created field UpdateTestField with ID ${fieldIdToUpdate}`);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.error("beforeAll failed:", error);
|
|
24
|
+
throw error; // Fail the suite if setup fails
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Teardown: Delete the test field
|
|
29
|
+
afterAll(async () => {
|
|
30
|
+
try {
|
|
31
|
+
if (fieldIdToUpdate) {
|
|
32
|
+
const response = await client.deleteFields({
|
|
33
|
+
tableId: QB_TABLE_ID_1,
|
|
34
|
+
body: { fieldIds: [fieldIdToUpdate] },
|
|
35
|
+
});
|
|
36
|
+
console.log(
|
|
37
|
+
`Deleted field ${fieldIdToUpdate}:`,
|
|
38
|
+
JSON.stringify(response, null, 2)
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error("afterAll cleanup failed:", error);
|
|
43
|
+
// Don’t throw here to avoid masking test results
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it(
|
|
48
|
+
"updates a field successfully with user token",
|
|
49
|
+
{ timeout: 10000 },
|
|
50
|
+
async () => {
|
|
51
|
+
try {
|
|
52
|
+
const updatedLabel = "UpdatedTestField";
|
|
53
|
+
console.log("Sending updateField request at:", new Date());
|
|
54
|
+
const response = await client.updateField({
|
|
55
|
+
tableId: QB_TABLE_ID_1,
|
|
56
|
+
fieldId: fieldIdToUpdate,
|
|
57
|
+
body: {
|
|
58
|
+
label: updatedLabel,
|
|
59
|
+
fieldHelp: "Updated help text",
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
console.log("updateField response received at:", new Date());
|
|
63
|
+
console.log("Response:", JSON.stringify(response, null, 2));
|
|
64
|
+
|
|
65
|
+
// Verify the update was successful
|
|
66
|
+
expect(response).toBeDefined();
|
|
67
|
+
expect(response.id).toBe(fieldIdToUpdate);
|
|
68
|
+
expect(response.label).toBe(updatedLabel);
|
|
69
|
+
expect(response.fieldHelp).toBe("Updated help text");
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error("updateField failed:", error);
|
|
72
|
+
throw error; // Re-throw to fail the test with details
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
it("fails with invalid field ID", { timeout: 10000 }, async () => {
|
|
78
|
+
try {
|
|
79
|
+
await expect(
|
|
80
|
+
client.updateField({
|
|
81
|
+
tableId: QB_TABLE_ID_1,
|
|
82
|
+
fieldId: 999999, // Invalid field ID
|
|
83
|
+
body: {
|
|
84
|
+
label: "InvalidUpdate",
|
|
85
|
+
},
|
|
86
|
+
})
|
|
87
|
+
).rejects.toThrow("API Error: Item not found (Status: 404)"); // Updated to match actual error
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error("updateField with invalid ID failed unexpectedly:", error);
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
});
|