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,96 @@
|
|
|
1
|
+
// tests/vitest/qb/formulas/runFormula.test.ts
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
4
|
+
import {
|
|
5
|
+
createClient,
|
|
6
|
+
QB_REALM,
|
|
7
|
+
QB_USER_TOKEN,
|
|
8
|
+
QB_TABLE_ID_1,
|
|
9
|
+
} from "@tests/setup.ts";
|
|
10
|
+
import {
|
|
11
|
+
RunFormulaRequest,
|
|
12
|
+
RunFormula200Response,
|
|
13
|
+
Upsert200Response,
|
|
14
|
+
} from "@/generated/models";
|
|
15
|
+
|
|
16
|
+
describe("QuickbaseClient Integration - runFormula", () => {
|
|
17
|
+
let client: ReturnType<typeof createClient>;
|
|
18
|
+
let testRecordId: number;
|
|
19
|
+
|
|
20
|
+
beforeAll(async () => {
|
|
21
|
+
// Validate environment variables
|
|
22
|
+
if (!QB_REALM) throw new Error("QB_REALM is not defined in .env");
|
|
23
|
+
if (!QB_USER_TOKEN) throw new Error("QB_USER_TOKEN is not defined in .env");
|
|
24
|
+
if (!QB_TABLE_ID_1) throw new Error("QB_TABLE_ID_1 is not defined in .env");
|
|
25
|
+
|
|
26
|
+
// Initialize client with user token only
|
|
27
|
+
client = createClient(undefined, {
|
|
28
|
+
realm: QB_REALM,
|
|
29
|
+
userToken: QB_USER_TOKEN,
|
|
30
|
+
debug: true,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Upsert a test record to ensure we have a valid rid
|
|
34
|
+
const upsertResponse: Upsert200Response = await client.upsert({
|
|
35
|
+
body: {
|
|
36
|
+
to: QB_TABLE_ID_1,
|
|
37
|
+
data: [
|
|
38
|
+
{
|
|
39
|
+
"6": { value: `Test Record ${Date.now()}` }, // Assuming field 6 is a text field (e.g., "Task Name")
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
fieldsToReturn: [3], // Return Record ID# (field ID 3)
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Extract the created record ID
|
|
47
|
+
testRecordId = upsertResponse.data[0]["3"].value as number;
|
|
48
|
+
console.log(`Upserted test record with ID: ${testRecordId}`);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
afterAll(async () => {
|
|
52
|
+
// Clean up the test record
|
|
53
|
+
if (testRecordId) {
|
|
54
|
+
await client.deleteRecords({
|
|
55
|
+
body: {
|
|
56
|
+
from: QB_TABLE_ID_1,
|
|
57
|
+
where: `{3.EX.${testRecordId}}`, // Delete by Record ID#
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
console.log(`Cleaned up test record with ID: ${testRecordId}`);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("runs simple formula without rid and returns correct result", async () => {
|
|
65
|
+
const request: RunFormulaRequest = {
|
|
66
|
+
from: QB_TABLE_ID_1,
|
|
67
|
+
formula: "1 + 1", // Simple arithmetic to verify functionality
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const response: RunFormula200Response = await client.runFormula({
|
|
71
|
+
body: request,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
console.log("Real API response (no rid):", response);
|
|
75
|
+
|
|
76
|
+
expect(response).toHaveProperty("result");
|
|
77
|
+
expect(response.result).toBe("2"); // Expect "2" as a string
|
|
78
|
+
}, 10000);
|
|
79
|
+
|
|
80
|
+
it("runs formula with rid and field ID 3, returns correct numeric result", async () => {
|
|
81
|
+
const request: RunFormulaRequest = {
|
|
82
|
+
from: QB_TABLE_ID_1,
|
|
83
|
+
formula: "Sum([Record ID#], 20)", // Field ID 3 is "Record ID#"
|
|
84
|
+
rid: testRecordId, // Use the upserted record ID
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const response: RunFormula200Response = await client.runFormula({
|
|
88
|
+
body: request,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
console.log("Real API response (with rid):", response);
|
|
92
|
+
|
|
93
|
+
expect(response).toHaveProperty("result");
|
|
94
|
+
expect(response.result).toBe(String(testRecordId + 20)); // Dynamic result based on the record ID
|
|
95
|
+
}, 10000);
|
|
96
|
+
});
|
package/tests/vitest/qb/index.ts
CHANGED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// tests/vitest/qb/tables/createRelationship.test.ts
|
|
2
|
+
|
|
3
|
+
import { test, expect } from "vitest";
|
|
4
|
+
import { quickbase } from "/home/drew/Projects/quickbase-js/src/quickbaseClient";
|
|
5
|
+
import {
|
|
6
|
+
QB_REALM,
|
|
7
|
+
QB_USER_TOKEN,
|
|
8
|
+
QB_TABLE_ID_1,
|
|
9
|
+
QB_TABLE_ID_2,
|
|
10
|
+
} from "/home/drew/Projects/quickbase-js/tests/setup.ts";
|
|
11
|
+
|
|
12
|
+
test(
|
|
13
|
+
"QuickbaseClient Integration - createRelationship > creates a new relationship in QuickBase",
|
|
14
|
+
{ timeout: 60000 }, // 60 seconds for create and delete
|
|
15
|
+
async () => {
|
|
16
|
+
// Validate environment variables
|
|
17
|
+
if (!QB_REALM) throw new Error("QB_REALM is not defined in .env");
|
|
18
|
+
if (!QB_USER_TOKEN) throw new Error("QB_USER_TOKEN is not defined in .env");
|
|
19
|
+
if (!QB_TABLE_ID_1) throw new Error("QB_TABLE_ID_1 is not defined in .env");
|
|
20
|
+
if (!QB_TABLE_ID_2) throw new Error("QB_TABLE_ID_2 is not defined in .env");
|
|
21
|
+
|
|
22
|
+
// Initialize client
|
|
23
|
+
const client = quickbase({
|
|
24
|
+
realm: QB_REALM,
|
|
25
|
+
userToken: QB_USER_TOKEN,
|
|
26
|
+
debug: true,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Define the relationship creation request
|
|
30
|
+
const uniqueLabel = `TestRelationship_${Date.now()}`;
|
|
31
|
+
const requestBody = {
|
|
32
|
+
parentTableId: QB_TABLE_ID_1,
|
|
33
|
+
foreignKeyField: { label: uniqueLabel },
|
|
34
|
+
lookupFieldIds: [6], // Assuming field 6 exists in QB_TABLE_ID_1
|
|
35
|
+
summaryFields: [
|
|
36
|
+
{
|
|
37
|
+
summaryFid: 6, // Assuming field 6 in QB_TABLE_ID_2 is numeric
|
|
38
|
+
label: `Sum_${uniqueLabel}`,
|
|
39
|
+
accumulationType: "SUM",
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
console.log("Config used:", {
|
|
45
|
+
realm: QB_REALM,
|
|
46
|
+
userToken: "[REDACTED]",
|
|
47
|
+
parentTableId: QB_TABLE_ID_1,
|
|
48
|
+
childTableId: QB_TABLE_ID_2,
|
|
49
|
+
});
|
|
50
|
+
console.log("Creating relationship with:", requestBody);
|
|
51
|
+
|
|
52
|
+
// Create the relationship
|
|
53
|
+
const response = await client.createRelationship({
|
|
54
|
+
tableId: QB_TABLE_ID_2,
|
|
55
|
+
body: requestBody,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
console.log("Real API response:", JSON.stringify(response, null, 2));
|
|
59
|
+
|
|
60
|
+
// Assertions for creation
|
|
61
|
+
expect(response).toBeDefined();
|
|
62
|
+
expect(response.id).toBeDefined();
|
|
63
|
+
expect(response.parentTableId).toBe(QB_TABLE_ID_1);
|
|
64
|
+
expect(response.childTableId).toBe(QB_TABLE_ID_2);
|
|
65
|
+
expect(response.foreignKeyField).toBeDefined();
|
|
66
|
+
expect(response.foreignKeyField?.label).toBe(uniqueLabel);
|
|
67
|
+
expect(response.lookupFields).toHaveLength(1);
|
|
68
|
+
expect(response.lookupFields[0].id).toBeGreaterThan(0);
|
|
69
|
+
expect(response.summaryFields).toHaveLength(1);
|
|
70
|
+
expect(response.summaryFields[0].label).toBe(`Sum_${uniqueLabel}`);
|
|
71
|
+
|
|
72
|
+
// Cleanup: Delete the relationship
|
|
73
|
+
const relationshipId = response.id;
|
|
74
|
+
console.log("Deleting relationship with ID:", relationshipId);
|
|
75
|
+
const deleteResponse = await client.deleteRelationship({
|
|
76
|
+
tableId: QB_TABLE_ID_2,
|
|
77
|
+
relationshipId,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
console.log(
|
|
81
|
+
"Delete relationship response:",
|
|
82
|
+
JSON.stringify(deleteResponse, null, 2)
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// Verify deletion
|
|
86
|
+
expect(deleteResponse).toBeDefined();
|
|
87
|
+
expect(deleteResponse.relationshipId).toBe(relationshipId);
|
|
88
|
+
|
|
89
|
+
// Confirm relationship is gone
|
|
90
|
+
const relationships = await client.getRelationships({
|
|
91
|
+
tableId: QB_TABLE_ID_2,
|
|
92
|
+
});
|
|
93
|
+
console.log(
|
|
94
|
+
"Relationships after deletion:",
|
|
95
|
+
JSON.stringify(relationships, null, 2)
|
|
96
|
+
);
|
|
97
|
+
const deletedRelationship = relationships.relationships.find(
|
|
98
|
+
(r) => r.id === relationshipId
|
|
99
|
+
);
|
|
100
|
+
expect(deletedRelationship).toBeUndefined(); // Should no longer exist
|
|
101
|
+
}
|
|
102
|
+
);
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// tests/vitest/qb/tables/deleteRelationship.test.ts
|
|
2
|
+
|
|
3
|
+
import { test, expect } from "vitest";
|
|
4
|
+
import { quickbase } from "/home/drew/Projects/quickbase-js/src/quickbaseClient";
|
|
5
|
+
import {
|
|
6
|
+
QB_REALM,
|
|
7
|
+
QB_USER_TOKEN,
|
|
8
|
+
QB_TABLE_ID_1,
|
|
9
|
+
QB_TABLE_ID_2,
|
|
10
|
+
} from "/home/drew/Projects/quickbase-js/tests/setup.ts";
|
|
11
|
+
|
|
12
|
+
test(
|
|
13
|
+
"QuickbaseClient Integration - deleteRelationship > creates and deletes a relationship in QuickBase",
|
|
14
|
+
{ timeout: 60000 }, // 60 seconds for two API calls
|
|
15
|
+
async () => {
|
|
16
|
+
// Validate environment variables
|
|
17
|
+
if (!QB_REALM) throw new Error("QB_REALM is not defined in .env");
|
|
18
|
+
if (!QB_USER_TOKEN) throw new Error("QB_USER_TOKEN is not defined in .env");
|
|
19
|
+
if (!QB_TABLE_ID_1) throw new Error("QB_TABLE_ID_1 is not defined in .env");
|
|
20
|
+
if (!QB_TABLE_ID_2) throw new Error("QB_TABLE_ID_2 is not defined in .env");
|
|
21
|
+
|
|
22
|
+
// Initialize client
|
|
23
|
+
const client = quickbase({
|
|
24
|
+
realm: QB_REALM,
|
|
25
|
+
userToken: QB_USER_TOKEN,
|
|
26
|
+
debug: true,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Step 1: Create a relationship
|
|
30
|
+
const uniqueLabel = `TestRelationship_${Date.now()}`;
|
|
31
|
+
const createRequestBody = {
|
|
32
|
+
parentTableId: QB_TABLE_ID_1,
|
|
33
|
+
foreignKeyField: { label: uniqueLabel },
|
|
34
|
+
lookupFieldIds: [6], // Assuming field 6 exists in QB_TABLE_ID_1
|
|
35
|
+
summaryFields: [
|
|
36
|
+
{
|
|
37
|
+
summaryFid: 6, // Assuming field 6 in QB_TABLE_ID_2 is numeric
|
|
38
|
+
label: `Sum_${uniqueLabel}`,
|
|
39
|
+
accumulationType: "SUM",
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
console.log("Config used:", {
|
|
45
|
+
realm: QB_REALM,
|
|
46
|
+
userToken: "[REDACTED]",
|
|
47
|
+
parentTableId: QB_TABLE_ID_1,
|
|
48
|
+
childTableId: QB_TABLE_ID_2,
|
|
49
|
+
});
|
|
50
|
+
console.log("Creating relationship with:", createRequestBody);
|
|
51
|
+
|
|
52
|
+
const createResponse = await client.createRelationship({
|
|
53
|
+
tableId: QB_TABLE_ID_2,
|
|
54
|
+
body: createRequestBody,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
console.log(
|
|
58
|
+
"Created relationship response:",
|
|
59
|
+
JSON.stringify(createResponse, null, 2)
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
// Verify creation
|
|
63
|
+
expect(createResponse).toBeDefined();
|
|
64
|
+
expect(createResponse.id).toBeDefined();
|
|
65
|
+
expect(createResponse.parentTableId).toBe(QB_TABLE_ID_1);
|
|
66
|
+
expect(createResponse.childTableId).toBe(QB_TABLE_ID_2);
|
|
67
|
+
expect(createResponse.foreignKeyField).toBeDefined();
|
|
68
|
+
expect(createResponse.foreignKeyField?.label).toBe(uniqueLabel);
|
|
69
|
+
|
|
70
|
+
const relationshipId = createResponse.id;
|
|
71
|
+
const foreignKeyFieldId = createResponse.foreignKeyField?.id;
|
|
72
|
+
|
|
73
|
+
// Step 2: Delete the relationship
|
|
74
|
+
console.log("Deleting relationship with ID:", relationshipId);
|
|
75
|
+
const deleteResponse = await client.deleteRelationship({
|
|
76
|
+
tableId: QB_TABLE_ID_2,
|
|
77
|
+
relationshipId,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
console.log(
|
|
81
|
+
"Delete relationship response:",
|
|
82
|
+
JSON.stringify(deleteResponse, null, 2)
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// Verify deletion (adjusted to match actual response)
|
|
86
|
+
expect(deleteResponse).toBeDefined();
|
|
87
|
+
expect(deleteResponse.relationshipId).toBe(relationshipId); // Updated to use "relationshipId"
|
|
88
|
+
|
|
89
|
+
// Step 3: Confirm relationship is gone (or non-functional)
|
|
90
|
+
try {
|
|
91
|
+
const relationships = await client.getRelationships({
|
|
92
|
+
tableId: QB_TABLE_ID_2,
|
|
93
|
+
});
|
|
94
|
+
console.log(
|
|
95
|
+
"Relationships after deletion:",
|
|
96
|
+
JSON.stringify(relationships, null, 2)
|
|
97
|
+
);
|
|
98
|
+
// Check if the relationship is still listed (metadata might persist)
|
|
99
|
+
const deletedRelationship = relationships.relationships.find(
|
|
100
|
+
(r) => r.id === relationshipId
|
|
101
|
+
);
|
|
102
|
+
if (deletedRelationship) {
|
|
103
|
+
console.log("Relationship metadata persists:", deletedRelationship);
|
|
104
|
+
expect(deletedRelationship.foreignKeyField).toBeUndefined(); // Should lack fields
|
|
105
|
+
}
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error("Error fetching relationships post-deletion:", error);
|
|
108
|
+
expect(error.message).toMatch(/404|not found/i); // Optional stricter check
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Cleanup remaining fields
|
|
112
|
+
const lookupFieldId = createResponse.lookupFields[0]?.id;
|
|
113
|
+
const summaryFieldId = createResponse.summaryFields[0]?.id;
|
|
114
|
+
|
|
115
|
+
if (foreignKeyFieldId) {
|
|
116
|
+
await client.deleteFields({
|
|
117
|
+
tableId: QB_TABLE_ID_2,
|
|
118
|
+
body: { fieldIds: [foreignKeyFieldId] },
|
|
119
|
+
});
|
|
120
|
+
console.log(
|
|
121
|
+
`Deleted foreign key field ${foreignKeyFieldId} from ${QB_TABLE_ID_2}`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
if (lookupFieldId) {
|
|
125
|
+
await client.deleteFields({
|
|
126
|
+
tableId: QB_TABLE_ID_2,
|
|
127
|
+
body: { fieldIds: [lookupFieldId] },
|
|
128
|
+
});
|
|
129
|
+
console.log(
|
|
130
|
+
`Deleted lookup field ${lookupFieldId} from ${QB_TABLE_ID_2}`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
if (summaryFieldId) {
|
|
134
|
+
await client.deleteFields({
|
|
135
|
+
tableId: QB_TABLE_ID_1,
|
|
136
|
+
body: { fieldIds: [summaryFieldId] },
|
|
137
|
+
});
|
|
138
|
+
console.log(
|
|
139
|
+
`Deleted summary field ${summaryFieldId} from ${QB_TABLE_ID_1}`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
);
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
// tests/vitest/qb/tables/updateRelationship.test.ts
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect } from "vitest";
|
|
4
|
+
import {
|
|
5
|
+
createClient,
|
|
6
|
+
QB_REALM,
|
|
7
|
+
QB_USER_TOKEN,
|
|
8
|
+
QB_TABLE_ID_1,
|
|
9
|
+
QB_TABLE_ID_2,
|
|
10
|
+
} from "@tests/setup.ts";
|
|
11
|
+
import {
|
|
12
|
+
UpdateRelationshipRequest,
|
|
13
|
+
CreateFieldRequest,
|
|
14
|
+
} from "@/generated/models";
|
|
15
|
+
|
|
16
|
+
describe("QuickbaseClient Integration - updateRelationship", () => {
|
|
17
|
+
let client: ReturnType<typeof createClient>;
|
|
18
|
+
let relationshipId: number;
|
|
19
|
+
let newLookupFieldId: number | undefined;
|
|
20
|
+
let newChildFieldId: number | undefined;
|
|
21
|
+
let generatedLookupFieldId: number | undefined;
|
|
22
|
+
let generatedSummaryFieldId: number | undefined;
|
|
23
|
+
|
|
24
|
+
// No beforeAll/afterAll since we'll create and delete within the test
|
|
25
|
+
|
|
26
|
+
it("creates, updates, deletes, and verifies a relationship", async () => {
|
|
27
|
+
// Validate environment variables
|
|
28
|
+
if (!QB_REALM) throw new Error("QB_REALM is not defined in .env");
|
|
29
|
+
if (!QB_USER_TOKEN) throw new Error("QB_USER_TOKEN is not defined in .env");
|
|
30
|
+
if (!QB_TABLE_ID_1) throw new Error("QB_TABLE_ID_1 is not defined in .env");
|
|
31
|
+
if (!QB_TABLE_ID_2) throw new Error("QB_TABLE_ID_2 is not defined in .env");
|
|
32
|
+
|
|
33
|
+
// Initialize client
|
|
34
|
+
client = createClient(fetch, {
|
|
35
|
+
realm: QB_REALM,
|
|
36
|
+
userToken: QB_USER_TOKEN,
|
|
37
|
+
debug: true,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Step 1: Create fields for the relationship update
|
|
41
|
+
const lookupFieldRequest: CreateFieldRequest = {
|
|
42
|
+
label: `Test Lookup Field_${Date.now()}`,
|
|
43
|
+
fieldType: "text",
|
|
44
|
+
};
|
|
45
|
+
const lookupResponse = await client.createField({
|
|
46
|
+
tableId: QB_TABLE_ID_1,
|
|
47
|
+
body: lookupFieldRequest,
|
|
48
|
+
});
|
|
49
|
+
newLookupFieldId = lookupResponse.id;
|
|
50
|
+
if (!newLookupFieldId) {
|
|
51
|
+
throw new Error("Failed to retrieve ID for newly created lookup field");
|
|
52
|
+
}
|
|
53
|
+
console.log(
|
|
54
|
+
`Created lookup field with ID ${newLookupFieldId} in parent table ${QB_TABLE_ID_1}`
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const childFieldRequest: CreateFieldRequest = {
|
|
58
|
+
label: `Test Child Numeric_${Date.now()}`,
|
|
59
|
+
fieldType: "numeric",
|
|
60
|
+
};
|
|
61
|
+
const childResponse = await client.createField({
|
|
62
|
+
tableId: QB_TABLE_ID_2,
|
|
63
|
+
body: childFieldRequest,
|
|
64
|
+
});
|
|
65
|
+
newChildFieldId = childResponse.id;
|
|
66
|
+
if (!newChildFieldId) {
|
|
67
|
+
throw new Error("Failed to retrieve ID for newly created child field");
|
|
68
|
+
}
|
|
69
|
+
console.log(
|
|
70
|
+
`Created child field with ID ${newChildFieldId} in child table ${QB_TABLE_ID_2}`
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// Step 2: Create a new relationship
|
|
74
|
+
const uniqueLabel = `TestRelationship_${Date.now()}`;
|
|
75
|
+
const createRequestBody = {
|
|
76
|
+
parentTableId: QB_TABLE_ID_1,
|
|
77
|
+
foreignKeyField: { label: uniqueLabel },
|
|
78
|
+
lookupFieldIds: [6], // Assuming field 6 exists in QB_TABLE_ID_1
|
|
79
|
+
summaryFields: [
|
|
80
|
+
{
|
|
81
|
+
summaryFid: 6, // Assuming field 6 in QB_TABLE_ID_2 is numeric
|
|
82
|
+
label: `Sum_${uniqueLabel}`,
|
|
83
|
+
accumulationType: "SUM",
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
console.log("Creating relationship with:", createRequestBody);
|
|
89
|
+
const createResponse = await client.createRelationship({
|
|
90
|
+
tableId: QB_TABLE_ID_2,
|
|
91
|
+
body: createRequestBody,
|
|
92
|
+
});
|
|
93
|
+
console.log(
|
|
94
|
+
"Created relationship response:",
|
|
95
|
+
JSON.stringify(createResponse, null, 2)
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
expect(createResponse).toBeDefined();
|
|
99
|
+
expect(createResponse.id).toBeDefined();
|
|
100
|
+
expect(createResponse.parentTableId).toBe(QB_TABLE_ID_1);
|
|
101
|
+
expect(createResponse.childTableId).toBe(QB_TABLE_ID_2);
|
|
102
|
+
expect(createResponse.foreignKeyField?.label).toBe(uniqueLabel);
|
|
103
|
+
|
|
104
|
+
relationshipId = createResponse.id;
|
|
105
|
+
|
|
106
|
+
// Step 3: Update the relationship - Add a lookup field
|
|
107
|
+
const tableId = QB_TABLE_ID_2;
|
|
108
|
+
const initialResponse = await client.getRelationships({ tableId });
|
|
109
|
+
const initialRelationship = initialResponse.relationships.find(
|
|
110
|
+
(r) => r.id === relationshipId
|
|
111
|
+
);
|
|
112
|
+
const currentLookupFields = (initialRelationship?.lookupFields || []).map(
|
|
113
|
+
(f) => f.id
|
|
114
|
+
);
|
|
115
|
+
console.log("Current lookupFields before update:", currentLookupFields);
|
|
116
|
+
|
|
117
|
+
const updateLookupRequest: UpdateRelationshipRequest = {
|
|
118
|
+
lookupFieldIds: [newLookupFieldId!],
|
|
119
|
+
};
|
|
120
|
+
console.log(
|
|
121
|
+
"Adding lookup field request:",
|
|
122
|
+
JSON.stringify(updateLookupRequest, null, 2)
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const updateLookupResponse = await client.updateRelationship({
|
|
126
|
+
tableId,
|
|
127
|
+
relationshipId,
|
|
128
|
+
body: updateLookupRequest,
|
|
129
|
+
});
|
|
130
|
+
console.log(
|
|
131
|
+
"Update lookup response:",
|
|
132
|
+
JSON.stringify(updateLookupResponse, null, 2)
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const newLookup = updateLookupResponse.lookupFields.find(
|
|
136
|
+
(f) =>
|
|
137
|
+
f.label.includes("Test Lookup Field") &&
|
|
138
|
+
!currentLookupFields.includes(f.id)
|
|
139
|
+
);
|
|
140
|
+
generatedLookupFieldId = newLookup?.id;
|
|
141
|
+
console.log(
|
|
142
|
+
`Captured generated lookup field ID: ${generatedLookupFieldId}`
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
expect(updateLookupResponse.id).toBe(relationshipId);
|
|
146
|
+
expect(updateLookupResponse.childTableId).toBe(QB_TABLE_ID_2);
|
|
147
|
+
expect(updateLookupResponse.parentTableId).toBe(QB_TABLE_ID_1);
|
|
148
|
+
expect(
|
|
149
|
+
updateLookupResponse.lookupFields.some((f) =>
|
|
150
|
+
f.label.includes("Test Lookup Field")
|
|
151
|
+
)
|
|
152
|
+
).toBe(true);
|
|
153
|
+
|
|
154
|
+
// Step 4: Update the relationship - Add a summary field
|
|
155
|
+
const currentSummaryFields = (updateLookupResponse.summaryFields || []).map(
|
|
156
|
+
(f) => f.id
|
|
157
|
+
);
|
|
158
|
+
console.log("Current summaryFields before update:", currentSummaryFields);
|
|
159
|
+
|
|
160
|
+
const updateSummaryRequest: UpdateRelationshipRequest = {
|
|
161
|
+
summaryFields: [
|
|
162
|
+
{
|
|
163
|
+
summaryFid: newChildFieldId!,
|
|
164
|
+
label: "Test Summary Field",
|
|
165
|
+
accumulationType: "SUM",
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
};
|
|
169
|
+
console.log(
|
|
170
|
+
"Adding summary field request:",
|
|
171
|
+
JSON.stringify(updateSummaryRequest, null, 2)
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const updateSummaryResponse = await client.updateRelationship({
|
|
175
|
+
tableId,
|
|
176
|
+
relationshipId,
|
|
177
|
+
body: updateSummaryRequest,
|
|
178
|
+
});
|
|
179
|
+
console.log(
|
|
180
|
+
"Update summary response:",
|
|
181
|
+
JSON.stringify(updateSummaryResponse, null, 2)
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
const newSummary = updateSummaryResponse.summaryFields.find(
|
|
185
|
+
(f) =>
|
|
186
|
+
f.label.includes("Test Summary Field") &&
|
|
187
|
+
!currentSummaryFields.includes(f.id)
|
|
188
|
+
);
|
|
189
|
+
generatedSummaryFieldId = newSummary?.id;
|
|
190
|
+
console.log(
|
|
191
|
+
`Captured generated summary field ID: ${generatedSummaryFieldId}`
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
expect(updateSummaryResponse.id).toBe(relationshipId);
|
|
195
|
+
expect(updateSummaryResponse.childTableId).toBe(QB_TABLE_ID_2);
|
|
196
|
+
expect(updateSummaryResponse.parentTableId).toBe(QB_TABLE_ID_1);
|
|
197
|
+
expect(
|
|
198
|
+
updateSummaryResponse.summaryFields.some((f) =>
|
|
199
|
+
f.label.includes("Test Summary Field")
|
|
200
|
+
)
|
|
201
|
+
).toBe(true);
|
|
202
|
+
|
|
203
|
+
// Step 5: Delete the relationship
|
|
204
|
+
console.log("Deleting relationship with ID:", relationshipId);
|
|
205
|
+
const deleteResponse = await client.deleteRelationship({
|
|
206
|
+
tableId: QB_TABLE_ID_2,
|
|
207
|
+
relationshipId,
|
|
208
|
+
});
|
|
209
|
+
console.log(
|
|
210
|
+
"Delete relationship response:",
|
|
211
|
+
JSON.stringify(deleteResponse, null, 2)
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
expect(deleteResponse).toBeDefined();
|
|
215
|
+
expect(deleteResponse.relationshipId).toBe(relationshipId);
|
|
216
|
+
|
|
217
|
+
// Step 6: Verify deletion
|
|
218
|
+
const relationships = await client.getRelationships({
|
|
219
|
+
tableId: QB_TABLE_ID_2,
|
|
220
|
+
});
|
|
221
|
+
console.log(
|
|
222
|
+
"Relationships after deletion:",
|
|
223
|
+
JSON.stringify(relationships, null, 2)
|
|
224
|
+
);
|
|
225
|
+
const deletedRelationship = relationships.relationships.find(
|
|
226
|
+
(r) => r.id === relationshipId
|
|
227
|
+
);
|
|
228
|
+
expect(deletedRelationship).toBeUndefined(); // Confirm relationship is gone
|
|
229
|
+
|
|
230
|
+
// Cleanup remaining fields from setup
|
|
231
|
+
if (newLookupFieldId) {
|
|
232
|
+
await client.deleteFields({
|
|
233
|
+
tableId: QB_TABLE_ID_1,
|
|
234
|
+
body: { fieldIds: [newLookupFieldId] },
|
|
235
|
+
});
|
|
236
|
+
console.log(
|
|
237
|
+
`Deleted lookup field ${newLookupFieldId} from ${QB_TABLE_ID_1}`
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
if (newChildFieldId) {
|
|
241
|
+
await client.deleteFields({
|
|
242
|
+
tableId: QB_TABLE_ID_2,
|
|
243
|
+
body: { fieldIds: [newChildFieldId] },
|
|
244
|
+
});
|
|
245
|
+
console.log(
|
|
246
|
+
`Deleted child field ${newChildFieldId} from ${QB_TABLE_ID_2}`
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
}, 60000);
|
|
250
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { ThrottleBucket } from "@/ThrottleBucket"; // Adjust path as needed
|
|
3
|
+
|
|
4
|
+
describe("ThrottleBucket Unit Tests", () => {
|
|
5
|
+
it("allows burst capacity immediately", async () => {
|
|
6
|
+
const bucket = new ThrottleBucket(5, 3); // rate: 5, burst: 3
|
|
7
|
+
const startTime = Date.now();
|
|
8
|
+
|
|
9
|
+
await Promise.all([bucket.acquire(), bucket.acquire(), bucket.acquire()]);
|
|
10
|
+
|
|
11
|
+
const duration = Date.now() - startTime;
|
|
12
|
+
expect(duration).toBeLessThan(50); // All 3 should be instant (within 50ms)
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("delays calls beyond burst at correct rate", async () => {
|
|
16
|
+
const bucket = new ThrottleBucket(5, 3); // rate: 5 tokens/sec, burst: 3
|
|
17
|
+
const timings: number[] = [];
|
|
18
|
+
const recordTime = async () => {
|
|
19
|
+
await bucket.acquire();
|
|
20
|
+
timings.push(Date.now());
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const startTime = Date.now();
|
|
24
|
+
await Promise.all([
|
|
25
|
+
recordTime(),
|
|
26
|
+
recordTime(),
|
|
27
|
+
recordTime(),
|
|
28
|
+
recordTime(), // 4th call should delay
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
const durations = timings.map((t) => t - startTime);
|
|
32
|
+
expect(durations.length).toBe(4);
|
|
33
|
+
expect(durations[0]).toBeLessThan(50); // 1st call instant
|
|
34
|
+
expect(durations[1]).toBeLessThan(50); // 2nd call instant
|
|
35
|
+
expect(durations[2]).toBeLessThan(50); // 3rd call instant
|
|
36
|
+
expect(durations[3]).toBeGreaterThanOrEqual(180); // 4th call delayed ~200ms
|
|
37
|
+
expect(durations[3]).toBeLessThan(250); // Shouldn’t exceed 250ms
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("maintains rate over sustained calls", async () => {
|
|
41
|
+
const bucket = new ThrottleBucket(2, 2); // rate: 2 tokens/sec, burst: 2
|
|
42
|
+
const timings: number[] = [];
|
|
43
|
+
const recordTime = async () => {
|
|
44
|
+
await bucket.acquire();
|
|
45
|
+
timings.push(Date.now());
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const startTime = Date.now();
|
|
49
|
+
await Promise.all([
|
|
50
|
+
recordTime(),
|
|
51
|
+
recordTime(),
|
|
52
|
+
recordTime(),
|
|
53
|
+
recordTime(),
|
|
54
|
+
recordTime(), // 5 calls total
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
const durations = timings.map((t) => t - startTime);
|
|
58
|
+
expect(durations.length).toBe(5);
|
|
59
|
+
expect(durations[0]).toBeLessThan(50); // 1st instant
|
|
60
|
+
expect(durations[1]).toBeLessThan(50); // 2nd instant
|
|
61
|
+
expect(durations[2]).toBeGreaterThanOrEqual(450); // 3rd delayed ~500ms (1/2 sec)
|
|
62
|
+
expect(durations[2]).toBeLessThan(550);
|
|
63
|
+
expect(durations[3]).toBeGreaterThanOrEqual(950); // 4th delayed ~1000ms
|
|
64
|
+
expect(durations[3]).toBeLessThan(1050);
|
|
65
|
+
expect(durations[4]).toBeGreaterThanOrEqual(1450); // 5th delayed ~1500ms
|
|
66
|
+
expect(durations[4]).toBeLessThan(1550);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("handles high concurrency without exceeding burst", async () => {
|
|
70
|
+
const bucket = new ThrottleBucket(5, 3); // rate: 5, burst: 3
|
|
71
|
+
const timings: number[] = [];
|
|
72
|
+
const recordTime = async () => {
|
|
73
|
+
await bucket.acquire();
|
|
74
|
+
timings.push(Date.now());
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const startTime = Date.now();
|
|
78
|
+
await Promise.all(
|
|
79
|
+
Array(10)
|
|
80
|
+
.fill(null)
|
|
81
|
+
.map(() => recordTime())
|
|
82
|
+
); // 10 concurrent calls
|
|
83
|
+
|
|
84
|
+
const durations = timings.map((t) => t - startTime);
|
|
85
|
+
expect(durations.length).toBe(10);
|
|
86
|
+
// First 3 should be instant
|
|
87
|
+
expect(durations.slice(0, 3).every((d) => d < 50)).toBe(true);
|
|
88
|
+
// Next calls should be staggered at ~200ms intervals
|
|
89
|
+
for (let i = 3; i < 10; i++) {
|
|
90
|
+
const expectedDelay = (i - 2) * 200; // After burst, 200ms per token
|
|
91
|
+
expect(durations[i]).toBeGreaterThanOrEqual(expectedDelay - 50);
|
|
92
|
+
expect(durations[i]).toBeLessThan(expectedDelay + 50);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
});
|